Compare commits

...

481 Commits

Author SHA1 Message Date
RiotRobot 7d07ab7c7e v2.3.2 2019-09-16 17:40:19 +01:00
RiotRobot f8ff3aac58 Prepare changelog for v2.3.2 2019-09-16 17:40:19 +01:00
David Baker 299a7728d1 Merge pull request #1034 from matrix-org/travis/t2
[Release] Fix addPendingEvent with pending event order == chronological
2019-09-16 17:02:32 +01:00
David Baker 39dc6a1742 Fix addPendingEvent with pending event order == chronological
When the pending event order setting was set to 'chronological'
(the default) `addPendingEvent` would NPE because Room no longer
has a `this._filter` property. It should get the filter from the
event timeline set instead, as it does in the previous line when
checking or the presence of a filter.

We should strongly consider changing the default pending event order
to 'detached' and probably removing 'chronological' or comitting to
support it properly: it's not really tested and is prone to breakage
like this.

Applies flumpt's fix from https://github.com/matrix-org/matrix-js-sdk/issues/599
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/599
2019-09-16 09:36:14 -06:00
RiotRobot f21c5aa7f2 v2.3.2-rc.1 2019-09-13 16:13:27 +01:00
RiotRobot e9bc3f26a5 Prepare changelog for v2.3.2-rc.1 2019-09-13 16:13:27 +01:00
David Baker 23eaddd6ea Merge pull request #1033 from matrix-org/travis/t1
Synapse admin functions to release
2019-09-13 16:07:58 +01:00
Travis Ralston 8143ce8450 Update src/client.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-09-13 09:01:16 -06:00
Travis Ralston 0a487ec43e Add Synapse admin functions for deactivating a user
For https://github.com/matrix-org/matrix-react-sdk/pull/3371
2019-09-13 09:01:05 -06:00
Travis Ralston 0edb483802 Merge pull request #1032 from matrix-org/travis/t1
[To Release] Add matrix base API to report an event
2019-09-13 08:27:31 -06:00
Michael Telatynski 06a32ce0a1 Add matrix base API to report an event
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-09-13 08:24:49 -06:00
RiotRobot a57ec87c67 v2.3.1 2019-09-12 12:48:36 +01:00
RiotRobot 4e62491ea4 Prepare changelog for v2.3.1 2019-09-12 12:48:36 +01:00
RiotRobot 5758029c1e v2.3.1-rc.1 2019-09-11 18:38:22 +01:00
RiotRobot 8f08710c58 Prepare changelog for v2.3.1-rc.1 2019-09-11 18:38:21 +01:00
David Baker 90f98105f0 Merge pull request #1031 from matrix-org/dbkr/update_profile_on_redact_2
Update room members on member event redaction
2019-09-11 18:12:46 +01:00
David Baker 90354aa330 Update room members on member event redaction
If a member event was redacted, we weren't updating the current
state.
2019-09-11 18:09:54 +01:00
RiotRobot 06adc34fb3 v2.3.0 2019-08-05 11:46:46 +01:00
RiotRobot 87bf07f95e Prepare changelog for v2.3.0 2019-08-05 11:46:46 +01:00
Travis Ralston ab512d087c Merge pull request #1008 from matrix-org/travis/tombstone-push-rel
[release] Support rewriting push rules when our internal defaults change
2019-08-01 08:30:01 -06:00
Travis Ralston 6799c29921 Appease the tests 2019-08-01 08:23:19 -06:00
Travis Ralston a3f1da1981 Appease the linter 2019-08-01 08:23:19 -06:00
Travis Ralston 3b225651cc Support rewriting push rules when our internal defaults change
and change our internal default for tombstones
2019-08-01 08:23:19 -06:00
RiotRobot aa8c2ca277 v2.3.0-rc.1 2019-07-31 16:20:54 +01:00
RiotRobot 84509087ac Prepare changelog for v2.3.0-rc.1 2019-07-31 16:20:54 +01:00
J. Ryan Stinnett 2450d461fd Merge pull request #1002 from matrix-org/jryans/is-v2-auth
Add support for IS v2 API with authentication
2019-07-30 18:13:08 +01:00
J. Ryan Stinnett 50c590ae26 Note cleanup issue 2019-07-30 10:38:53 +01:00
J. Ryan Stinnett 516dff06ee Rename isAccessToken to identityAccessToken 2019-07-30 10:06:52 +01:00
Travis Ralston 9a8af05bfb Merge pull request #1001 from matrix-org/hs/recursive-tombstone-fixes
Tombstone bugfixes
2019-07-29 08:52:59 -06:00
Will Hunt c9bf61c387 Simplify Set 2019-07-29 15:29:18 +01:00
Will Hunt 4f0f2e8c16 Fix issues with recursive tombstones 2019-07-29 15:27:32 +01:00
J. Ryan Stinnett 6f042a2142 Add IS v2 support to other IS APIs
This adds v2 support with fallback to other IS APIs in the SDK.
2019-07-29 14:55:40 +01:00
J. Ryan Stinnett 91416bdbb2 Add IS v1 API fallback for lookup 2019-07-29 14:44:15 +01:00
J. Ryan Stinnett 9b093f7569 Add first pass of IS v2 API with authentication
This only updates the `/lookup` API so far. It also doesn't handle falling back
to v1.
2019-07-29 13:15:19 +01:00
David Baker 6cca73b999 Merge pull request #988 from matrix-org/dbkr/terms
Support for MSC2140 (terms of service for IS/IM)
2019-07-23 10:32:05 +01:00
David Baker fafd6df13e Use standard _matrix path for terms endpoints 2019-07-22 18:53:36 +01:00
RiotRobot 8f77870526 Merge branch 'master' into develop 2019-07-18 15:44:16 +01:00
RiotRobot eb0462e89b v2.2.0 2019-07-18 15:42:21 +01:00
RiotRobot 80748d7d85 Prepare changelog for v2.2.0 2019-07-18 15:42:20 +01:00
David Baker b694d53b73 Revert 8004e82c50
We need to switch the paths over all at once, so we can't commit
this yet: leave it until scalar suypports the new API then we can
update develop to use the _matrix paths.

(Also for some reason I broke the IS path too)
2019-07-15 14:26:16 +01:00
David Baker 8004e82c50 Use _matrix prefix for terms API 2019-07-15 13:51:39 +01:00
J. Ryan Stinnett 85b5849228 Upgrade lodash 2019-07-12 19:25:47 +01:00
J. Ryan Stinnett 6b86777e96 Upgrade lodash 2019-07-12 19:25:21 +01:00
RiotRobot efe64a4817 v2.2.0-rc.2 2019-07-12 17:25:20 +01:00
RiotRobot ad777f36b2 Prepare changelog for v2.2.0-rc.2 2019-07-12 17:25:20 +01:00
J. Ryan Stinnett de77ad867c Merge pull request #995 from matrix-org/jryans/v2.2.0/devices
Add a request method to /devices
2019-07-12 16:47:06 +01:00
Travis Ralston 9b35f86497 Add a request method to /devices
Turns out `HTTP /devices undefined` is invalid.

Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/990
2019-07-12 16:38:43 +01:00
Travis Ralston 84fc8b1931 Merge pull request #994 from matrix-org/travis/fix-devices
Add a request method to /devices
2019-07-12 08:12:59 -06:00
RiotRobot 455c85fb69 v2.2.0-rc.1 2019-07-12 11:12:26 +01:00
RiotRobot 2a2fed695b Prepare changelog for v2.2.0-rc.1 2019-07-12 11:12:25 +01:00
Travis Ralston c1f28bd410 Add a request method to /devices
Turns out `HTTP /devices undefined` is invalid.

Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/990
2019-07-11 14:35:04 -06:00
David Baker c74e0bb6b3 tell people what an IS/IM are 2019-07-11 16:29:27 +01:00
David Baker 5b9e158035 unused param
getTerms is un-authed so doesn't need the access token
2019-07-11 16:28:41 +01:00
Travis Ralston a8d200dd02 Merge pull request #993 from matrix-org/travis/sas-is-done
End the verification timer when verification is done
2019-07-11 08:20:30 -06:00
Travis Ralston 34ae967cb8 Merge pull request #990 from matrix-org/travis/stably-use-stable-apis
Stabilize usage of stably stable APIs (in a stable way)
2019-07-11 08:20:14 -06:00
RiotRobot a67f14825e Merge branch 'master' into develop 2019-07-11 10:36:44 +01:00
RiotRobot 50c14d0ab8 v2.1.1 2019-07-11 10:34:58 +01:00
RiotRobot 0edb6e6f6f Prepare changelog for v2.1.1 2019-07-11 10:34:58 +01:00
Travis Ralston 36d0dacda1 Process ephemeral events outside timeline handling 2019-07-11 10:21:44 +01:00
Bruno Windels 83b74070aa Merge pull request #987 from matrix-org/bwindels/include-orig-in-history
Expose original_event for /relations
2019-07-11 08:09:44 +00:00
Travis Ralston f80af68686 End the verification timer when verification is done
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/980

This also improves cleanliness in the tests to cancel/terminate timers when needed.
2019-07-10 14:51:12 -06:00
Travis Ralston fe4ac06f43 Use the correct media endpoints 2019-07-10 13:24:11 -06:00
Travis Ralston eaaa3e980a Use unstable prefix for key backup 2019-07-10 13:17:31 -06:00
Travis Ralston 07629bfb9a unstable -> stable 2019-07-10 13:11:46 -06:00
Travis Ralston 88fdeca2bf Merge pull request #989 from matrix-org/travis/edu-timeline
Process ephemeral events outside timeline handling
2019-07-10 10:38:02 -06:00
Travis Ralston a3c8eac38b Process ephemeral events outside timeline handling 2019-07-10 10:26:21 -06:00
Bruno Windels bd5380c0b4 Merge pull request #986 from matrix-org/bwindels/include-ssa-for-replace
Don't accept any locally known edits earlier than the last known server-side aggregated edit
2019-07-10 14:38:26 +00:00
David Baker c3b5767999 update comment to reflect right version 2019-07-10 12:19:12 +01:00
David Baker 9e5c2732c9 consistent spacing 2019-07-10 12:18:41 +01:00
David Baker b8957fa917 omit null params 2019-07-10 12:17:52 +01:00
David Baker 52c139dcdc Forgot /terms for ISes
and IMs shouldn't have a slash
2019-07-10 12:15:31 +01:00
David Baker 39d4bf1494 SERVICE_TYPES 2019-07-10 12:08:13 +01:00
David Baker 4c713e3387 s/servicetypes/service-types/ 2019-07-10 11:53:59 +01:00
David Baker bb486f5148 SERVICE_TYPES
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-07-10 11:47:36 +01:00
David Baker 524fea1297 lint 2019-07-10 10:43:54 +01:00
David Baker e9528ebb98 Support for MSC2140 (terms of service for IS/IM) 2019-07-09 18:50:01 +01:00
Bruno Windels de18283c3b map, decrypt and return original_event if present 2019-07-09 17:52:58 +02:00
Bruno Windels cc1c7561a3 Merge pull request #984 from matrix-org/bwindels/replace-server-date
Get edit date transparently from server aggregations or local echo
2019-07-09 15:03:02 +00:00
Bruno Windels 5d928f07a0 don't accept any edit earlier than the server-side set edit. 2019-07-09 15:06:07 +02:00
Bruno Windels 01b882480f method to get edit date transparently from server aggregations or local echo 2019-07-09 11:29:19 +02:00
Travis Ralston 21d52fdbdd Merge pull request #982 from matrix-org/travis/soft-logout-keys
Add a function to flag keys for backup without scheduling a backup
2019-07-08 11:48:09 -06:00
RiotRobot f9baff2a3a Merge branch 'master' into develop 2019-07-08 10:46:33 +01:00
RiotRobot 71eca4ffcc v2.1.0 2019-07-08 10:44:47 +01:00
RiotRobot bc8dca5105 Prepare changelog for v2.1.0 2019-07-08 10:44:47 +01:00
David Baker 3ae3dffff7 Lint 2019-07-08 10:37:24 +01:00
David Baker 81c6023940 Fix exception whilst syncing
event.getPushRules() may return null (for better or worse...).
Use client.getPushRulesForEvent which will calculate them if they
haven't already been calculated.

Fixes https://github.com/vector-im/riot-web/issues/10269
2019-07-08 10:37:24 +01:00
Travis Ralston 3a0f27fa7e Add a function to flag keys for backup without scheduling a backup
For https://github.com/vector-im/riot-web/issues/10263

Starting/scheduling the backup won't help us because the token would be invalid from a server perspective. Instead, we should update what needs to be done and return a count.
2019-07-05 13:50:11 -06:00
J. Ryan Stinnett 60e339bac0 Merge pull request #981 from matrix-org/jryans/reactions-send-marks-unread
Block read marker and read receipt from advancing into pending events
2019-07-05 17:55:52 +01:00
J. Ryan Stinnett ecb88f45b7 Merge pull request #977 from matrix-org/jryans/upgrade-deps
Upgrade dependencies
2019-07-05 17:50:10 +01:00
J. Ryan Stinnett 24a869d15b Update copyright header 2019-07-05 15:07:56 +01:00
J. Ryan Stinnett 1d427a1ea8 Block read marker and read receipt from advancing into pending events
This changes the methods that update the read marker and read receipts to
prevent advancing into pending events.

Part of https://github.com/vector-im/riot-web/issues/9952
2019-07-05 13:59:04 +01:00
J. Ryan Stinnett 90e25867ad Merge pull request #976 from matrix-org/jryans/push-rule-reactions
Add default push rule to ignore reactions
2019-07-05 12:01:31 +01:00
David Baker 2ae56e61cb Merge pull request #979 from matrix-org/dbkr/fix_sync_exception
Fix exception whilst syncing
2019-07-05 11:08:00 +01:00
David Baker 56c0830328 Lint 2019-07-05 10:32:33 +01:00
David Baker 093f139d34 Fix exception whilst syncing
event.getPushRules() may return null (for better or worse...).
Use client.getPushRulesForEvent which will calculate them if they
haven't already been calculated.

Fixes https://github.com/vector-im/riot-web/issues/10269
2019-07-05 10:26:47 +01:00
Travis Ralston 1083efc212 Merge pull request #975 from matrix-org/travis/soft-logout-base
Include the error object when raising Session.logged_out
2019-07-04 09:57:22 -06:00
J. Ryan Stinnett 69773c2619 Upgrade dependencies 2019-07-04 15:04:34 +01:00
J. Ryan Stinnett 2525b5a5d8 Add default push rule to ignore reactions
This adds a default push rule to ignore reactions as proposed in
[MSC2153](https://github.com/matrix-org/matrix-doc/pull/2153). By adding it here
in the client directly, we can try out the idea early even if it hasn't appeared
in the user's HS yet.

Part of https://github.com/vector-im/riot-web/issues/10208
2019-07-04 14:41:04 +01:00
Travis Ralston ff9c84ff94 Fix tests 2019-07-03 16:50:24 -06:00
Travis Ralston 3aa2bf8a76 Include the error object when raising Session.logged_out
Note: The `call` argument previously defined in the SDK was never actually populated, and appears to be a documentation error when the definition was copied from `Call.incoming` directly above it.
2019-07-03 16:42:33 -06:00
RiotRobot a229ece693 v2.1.0-rc.1 2019-07-03 16:40:08 +01:00
RiotRobot b435137332 Prepare changelog for v2.1.0-rc.1 2019-07-03 16:40:07 +01:00
Travis Ralston 2cdbc9f4db Merge pull request #974 from matrix-org/travis/e2e-self-notif
Handle self read receipts for fixing e2e notification counts
2019-07-03 09:16:22 -06:00
Michael Telatynski aa6884e484 Merge pull request #973 from matrix-org/t3chguy/show_hidden_redactions_missing_redacts
Add redacts field to event.toJSON
2019-07-03 13:10:16 +01:00
Bruno Windels 4f4d694687 Merge pull request #972 from matrix-org/bwindels/handle-associated-failures
Handle associated event send failures
2019-07-03 09:02:49 +00:00
Travis Ralston 1b47999e80 Handle self read receipts for fixing e2e notification counts
Fixes https://github.com/vector-im/riot-web/issues/9421

This also adds a context to the ReEmitter so we have access to the Room at the time of read receipt. Without this, we have to bind handlers to every encrypted room (which is tedious to maintain) or figure out which room `$something` belong to (CPU intensive).
2019-07-02 13:12:29 -06:00
Michael Telatynski 02427651dd Add redacts field to event.toJSON
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-07-02 19:44:16 +01:00
Bruno Windels 8eeb088e50 allow to clear replacement on redacted events
this is needed when cancelling sending anything related to an event,
to not keep the event as edited when we cancel the edit
before the redaction.
2019-07-02 13:12:13 +02:00
Travis Ralston 3f62581556 Merge pull request #971 from matrix-org/travis/cleanup-debugging
Remove irrelevant debug line from timeline handling
2019-07-01 09:47:36 -06:00
Travis Ralston b53a7f6ee8 Remove irrelevant debug line from timeline handling
See https://github.com/matrix-org/matrix-js-sdk/pull/877#discussion_r271704218
2019-06-28 16:29:18 -06:00
Bruno Windels a4591afba6 clarify when this will return something 2019-06-27 17:53:49 +02:00
Bruno Windels 6ea4c77dd5 expose local echo redaction event on redacted event 2019-06-27 17:53:29 +02:00
Bruno Windels dc3d90d696 keep local redaction event to return accurate status 2019-06-27 17:17:53 +02:00
Bruno Windels 00de919eb4 Merge pull request #969 from matrix-org/bwindels/e2e-edit-history
Handle relations in encrypted rooms
2019-06-27 13:15:59 +00:00
Bruno Windels fdacc2f7ab dont try to decrypt reactions 2019-06-27 15:03:47 +02:00
J. Ryan Stinnett 5a64c29228 Fix logging typo
Regression from https://github.com/matrix-org/matrix-js-sdk/pull/924
2019-06-27 13:27:13 +01:00
Bruno Windels c5cddf1607 take reactions into account when fetching relations from e2e rooms 2019-06-27 14:06:57 +02:00
Bruno Windels 44c7844a4b handle relations in encrypted rooms 2019-06-27 12:35:34 +02:00
Bruno Windels f293da5b34 Merge pull request #967 from matrix-org/bwindels/edit-history
Relations endpoint support
2019-06-26 14:38:46 +00:00
Bruno Windels 4f044de79e add jsdocs 2019-06-26 15:48:39 +02:00
Bruno Windels da116e6077 remove c/p leftover 2019-06-26 15:48:27 +02:00
Bruno Windels 2489180c47 rename pagination token option to from, more in line with cs api 2019-06-26 15:47:59 +02:00
J. Ryan Stinnett 4ec4d330aa Merge pull request #968 from matrix-org/jryans/reactions-disable-encryption
Disable event encryption for reactions
2019-06-26 11:29:47 +01:00
Bruno Windels c75ca1c2d6 fix lint and encode path params properly 2019-06-26 12:10:24 +02:00
Bruno Windels 67462e9fc4 support paginating relations 2019-06-26 12:00:25 +02:00
Bruno Windels 424b6303ef basic support for /relations endpoint 2019-06-26 12:00:25 +02:00
J. Ryan Stinnett 59c4e2c354 Disable event encryption for reactions
For reactions, there is a very little gained by encrypting the entire event, as
relation data is already kept in the clear. Event encryption for a reaction
effectively only obscures the event type, but the purpose is still obvious from
the relation data, so nothing is really gained. It also causes quite a few
problems, such as:

  * triggers notifications via default push rules
  * prevents server-side bundling for reactions

The reaction key / content / emoji value does warrant encrypting, but this will
be handled separately by encrypting just this value.

See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642
Fixes https://github.com/vector-im/riot-web/issues/10130
2019-06-26 10:59:44 +01:00
Travis Ralston ecca8bc86e Merge pull request #966 from matrix-org/travis/known-safe
Change the known safe room version to version 4
2019-06-25 17:29:24 -06:00
Travis Ralston 43ca920b10 Change the known safe room version to version 4
The default room version in the spec is v4 due to widespread adoption. We should mirror that.
2019-06-25 15:19:28 -06:00
Travis Ralston e1a3f8f053 Merge pull request #965 from matrix-org/travis/detect-ll-r0.5
Check for lazy-loading support in the spec versions instead
2019-06-25 09:02:20 -06:00
Travis Ralston 6e7cb63e7d Check for lazy-loading support in the spec versions instead
Fixes https://github.com/vector-im/riot-web/issues/9966
2019-06-24 13:03:06 -06:00
Travis Ralston ab1177d987 Merge pull request #963 from huan/patch-1
Use cameCase instead of underscore
2019-06-24 11:09:36 -06:00
Huan LI (李卓桓) ee0a1d281d Use cameCase instead of underscore
To make all the variable names to be consistent
2019-06-23 11:56:05 +08:00
Travis Ralston aef7b9a1dc Merge pull request #961 from matrix-org/travis/sas-timeouts
Time out verification attempts after 10 minutes of inactivity
2019-06-20 14:29:18 -06:00
Travis Ralston 7cb8de5b69 Appease the linter 2019-06-20 14:17:51 -06:00
Travis Ralston 5c2fb1c42b Null check and naming 2019-06-20 14:12:16 -06:00
Travis Ralston 553857583d Merge pull request #962 from matrix-org/travis/sas-cancel
Don't handle key verification requests which are immediately cancelled
2019-06-20 09:10:31 -06:00
Travis Ralston 6d0923153f Don't handle key verification requests which are immediately cancelled
Fixes https://github.com/vector-im/riot-web/issues/10083
Fixes https://github.com/vector-im/riot-web/issues/9197
Fixes https://github.com/vector-im/riot-web/issues/8629

The issue is partially fixed by https://github.com/matrix-org/matrix-react-sdk/pull/3123 in that users would no longer see "Incoming request", but would launch their client to a bunch of "key verification cancelled" dialogs. To work around this, we just don't handle key verification requests which we know are cancelled.

The changes are a bit awkward (flagging the event as cancelled instead of filtering it) because:
* We probably don't want to prevent events getting sent over the EventEmitter because applications may still rely on them.
* The cypto side only has visibility of 1 event at a time, so it needs to have some kind of flag to rely on.

An attempt has been made to generalize the new event flag for possible future cases.
2019-06-19 16:46:38 -06:00
Travis Ralston e34eb48914 Don't timeout cancelled requests
The cancelled flag is used upstream and is therefore public.
2019-06-19 14:44:36 -06:00
Travis Ralston 05ab6ef3ab Time out verification attempts after 10 minutes of inactivity
Fixes https://github.com/vector-im/riot-web/issues/10117
2019-06-19 14:15:58 -06:00
RiotRobot 81eefc1377 v2.0.1 2019-06-19 15:43:38 +01:00
RiotRobot 895d854e1c Prepare changelog for v2.0.1 2019-06-19 15:43:38 +01:00
RiotRobot e432d4f808 v2.0.1-rc.2 2019-06-18 15:28:47 +01:00
RiotRobot 56c0ae294b Prepare changelog for v2.0.1-rc.2 2019-06-18 15:28:47 +01:00
David Baker 0b9d68b4f2 Merge remote-tracking branch 'origin/develop' into release-v2.0.1 2019-06-18 15:24:51 +01:00
Bruno Windels e2e034f795 Merge pull request #960 from matrix-org/bwindels/redactions-blended-echo
return 'sending' status for an event that is only locally redacted
2019-06-18 13:09:03 +00:00
Bruno Windels bb5e3d51b8 remove redundant localecho part from method name 2019-06-18 14:58:17 +02:00
Bruno Windels 70b23614b5 comment typo 2019-06-18 14:55:58 +02:00
Bruno Windels 24a75e3765 return 'sending' status for an event that is only locally redacted 2019-06-18 13:46:34 +02:00
Hubert Chathi d694ee3ef3 Merge pull request #954 from uhoreg/fix_verification_request
Key verification request fixes
2019-06-14 15:39:01 -04:00
David Baker efbdf4e1a8 Merge pull request #956 from matrix-org/dbkr/resurrect_riot_bot
Add flag to force saving sync store
2019-06-14 17:20:27 +01:00
David Baker 44bfc2e846 Add flag to force saving sync store
Add a 'force' flag to to the save method of the store to force the
store to sace its data even if it wouldn't normally.
2019-06-14 15:57:49 +01:00
Travis Ralston 0121bdbb75 welcome back, Olm 2019-06-14 08:23:27 -06:00
David Baker b8ba77a7b5 Merge pull request #953 from matrix-org/dbkr/simplify_email_reg
Expose the inhibit_login flag to register
2019-06-13 18:22:23 +01:00
David Baker 65dd5cc6ad use right variable 2019-06-13 16:30:39 +01:00
David Baker 8aeb994839 Expose the inhibit_login flag to register 2019-06-13 16:23:11 +01:00
Hubert Chathi 64daa444dd Key verification request fixes
- fix requestVerification in MatrixClient to match the function in crypto
  - reorder the arguments so that the arguments actually do what they say they
    do
  - pass through the third argument, which was accidentally omitted
- ignore verification requests from ourselves
- also fix a comment
2019-06-13 10:55:06 -04:00
Bruno Windels 26aab4f38d Merge pull request #947 from matrix-org/bwindels/relations-unsent
Support redactions and relations of/with unsent events.
2019-06-13 11:32:37 +00:00
Bruno Windels 6059df1b67 move CANCELLED check deeper into aggregation path 2019-06-13 12:28:02 +02:00
Bruno Windels 2a0c85c772 add hasAssociation helper 2019-06-13 12:28:02 +02:00
Bruno Windels 3488fbe64c expand comment why need to preserve redaction local echo on remote echo 2019-06-13 12:28:02 +02:00
Bruno Windels 811a98ad19 whitespace, newlines 2019-06-13 12:28:02 +02:00
Bruno Windels 4462f4b90e add isRedaction helper on Event 2019-06-13 12:28:02 +02:00
Bruno Windels 4143a79f7b rename related id to associated id 2019-06-13 12:26:38 +02:00
Bruno Windels 3ed9b00398 clarify why we need to listen for remote echo of related event 2019-06-13 12:26:38 +02:00
Bruno Windels b005b75331 comment typo
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-06-13 12:26:38 +02:00
Bruno Windels a9f9e2cf35 comment typo
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-06-13 12:26:38 +02:00
Bruno Windels 5602b94dcb make sure where not re-adding cancelled events when undoing local red. 2019-06-13 12:26:38 +02:00
Bruno Windels 930de640ac don't add events from /sync that have been locally redacted
it'll cause the reactions counter to go up and down while reactions
and redactions come in.

In case the local redaction gets cancelled,
Room._revertRedactionLocalEcho will add the relation back to
the relations collection.
2019-06-13 12:26:38 +02:00
Bruno Windels 6d9fba8191 preserve (locally) redacted state after applying remote echo
because the RedactionDimensions was trying to redact an event
that was already redacted after it's remote echo had come in
but it's redaction hadn't synced yet.
2019-06-13 12:26:38 +02:00
Bruno Windels 624c6f0a6e get the txnId from the correct place to delete event after remote echo 2019-06-13 12:26:38 +02:00
Bruno Windels 7d2f7fae45 fix tests 2019-06-13 12:26:38 +02:00
Bruno Windels 3f917b39c9 fix lint 2019-06-13 12:26:38 +02:00
Bruno Windels f1336a5ce7 rename target id to related id and add jsdoc comments 2019-06-13 12:26:38 +02:00
Bruno Windels 7a10d504b2 emit Relations.redaction synchronously, timeout should not be needed
listeners shouldn't care about the original event, as it's removed
from the Relations collection already.
2019-06-13 12:26:38 +02:00
Bruno Windels 831aec6488 emit remote id once received so enqueued relations have it when sent 2019-06-13 12:26:38 +02:00
Bruno Windels 6eb229ac1e first look in pending event list for event being redacted
in case the redacted event hasn't been sent yet
2019-06-13 12:26:38 +02:00
Bruno Windels c58db665dd give the client a chance to run room.updatePendingEvent after sending
before the next event is sent. This is needed to update the target id
if it was the local id of the event that was just sent.
2019-06-13 12:26:38 +02:00
Bruno Windels e222fb1783 enqueue relations and redactions as well
as they might need to wait until their target has been sent
2019-06-13 12:26:38 +02:00
RiotRobot 31a0192c2d v2.0.1-rc.1 2019-06-12 11:47:36 +01:00
RiotRobot 53f8091e3a Prepare changelog for v2.0.1-rc.1 2019-06-12 11:47:35 +01:00
David Baker 012cbf7995 Merge pull request #952 from matrix-org/jryans/file-api-changes
Fix content uploads for modern browsers
2019-06-11 13:11:53 +01:00
J. Ryan Stinnett ac26c91cba Fix content uploads for modern browsers
Modern browsers now expose a `stream` function on the Blob and File interfaces.
This conflicts with an older style of passing data to the `uploadContent` SDK
method, which supported supplying the data to upload in the `stream` property of
an object.

Since this old style is still in active use in the Matrix JS ecosystem, we
preserve the backwards compatibility for now by checking whether `stream` is a
function.

This fix has been tested in Firefox Nightly (69), Firefox Release (67), Chrome
Canary (77), and Chrome Stable (75).

Fixes https://github.com/vector-im/riot-web/issues/9913
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/949
2019-06-11 13:02:42 +01:00
David Baker c13162aada Merge pull request #951 from matrix-org/dbkr/one_request_at_a_time_two
Don't overlap auth submissions with polls
2019-06-11 10:42:32 +01:00
David Baker 9fb6eea8b7 Document what to use instead 2019-06-10 18:04:30 +01:00
David Baker 23c4f19cda lint 2019-06-10 16:29:55 +01:00
David Baker 3b34570749 doc background flag deprecation 2019-06-10 16:26:09 +01:00
David Baker 0412ca5810 make busyChanged optional 2019-06-10 16:24:20 +01:00
David Baker c80518bf3e s/setBusy/busyChanged/ 2019-06-10 16:23:06 +01:00
David Baker 61ee6eb8af This should be null, not false
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-06-10 16:19:45 +01:00
David Baker 654e8b41fa Don't overlap auth submissions with polls
Wait for polls to complete before submitting auth dicts, otherwise
we risk the requests overlapping and both returning a 200.

Also introduces a setBusy interface to interactive-auth to explicitly
set the busy status, since otherwise it doesn't get set until the
request actually starts.
2019-06-10 15:18:46 +01:00
J. Ryan Stinnett 7080458f7e Merge pull request #945 from matrix-org/jryans/funding
Add funding details for GitHub sponsor button
2019-06-07 20:42:28 +01:00
J. Ryan Stinnett 08d236f5ec Add funding details for GitHub sponsor button 2019-06-07 11:46:56 +01:00
David Baker e332a7d113 Merge pull request #944 from matrix-org/dbkr/verify_signature_modifies_the_object_because_everything_is_awful
Fix backup sig validation with multiple sigs
2019-06-07 11:26:13 +01:00
David Baker 7879709f62 Fix backup sig validation with multiple sigs
verifySignature modifies the object so we need to clone if we're
verifying more than one signature.

Fixes https://github.com/vector-im/riot-web/issues/9357
2019-06-07 11:05:44 +01:00
David Baker 56e030762e Merge pull request #943 from matrix-org/dbkr/wait_for_token_request
Don't send another token request while one's in flight
2019-06-06 19:18:20 +01:00
David Baker bac73150ca Don't send another token request while one's in flight
Otherwise we end up with more tokens than are strictly necessary
2019-06-06 19:03:29 +01:00
David Baker 2e1fb15ada Merge pull request #942 from matrix-org/dbkr/one_poll_at_a_time
Don't poll UI auth again until current poll finishes
2019-06-06 18:36:34 +01:00
David Baker ae9bcd6f6c Don't poll UI auth again until current poll finishes
On slow networks/servers we were ending up with lots of requests in
flight.
2019-06-06 18:31:54 +01:00
Travis Ralston c18c679b9b Merge pull request #938 from matrix-org/travis/fail-fast-but-not-too-fast
Provide the discovered URLs when a liveliness error occurs
2019-06-06 09:03:39 -06:00
Travis Ralston d014ee0b72 Merge pull request #941 from matrix-org/travis/redact-v3
Encode event IDs when redacting events
2019-06-06 08:29:15 -06:00
Travis Ralston a30ef7250b Encode event IDs when redacting events
Because v3 rooms are a thing.
2019-06-05 15:27:55 -06:00
Hubert Chathi 570ce4f4b7 Merge pull request #940 from uhoreg/fix_missing_logger
add missing logger
2019-06-05 14:49:38 -04:00
Hubert Chathi 3c7c9048eb add missing logger 2019-06-05 14:04:39 -04:00
Hubert Chathi 41243757ee Merge pull request #939 from uhoreg/unpicky_verification
verification: don't error if we don't know about some keys
2019-06-05 12:18:53 -04:00
Hubert Chathi 2af311bd7d verification: don't error if we don't know about some keys 2019-06-05 11:56:37 -04:00
Bruno Windels 1bc9ee7110 Merge pull request #937 from matrix-org/bwindels/redactions-local-echo
Local echo for redactions
2019-06-05 07:49:31 +00:00
Bruno Windels 4e040f8e77 correct comments about redaction events 2019-06-05 09:41:52 +02:00
Travis Ralston 26c1c6db3b Fix tests and populate the right IS validation object 2019-06-04 23:51:41 -06:00
Travis Ralston d38da83656 Provide the discovered URLs when a liveliness error occurs
See https://github.com/vector-im/riot-web/issues/9828
2019-06-04 23:39:31 -06:00
Bruno Windels 58f163ed5c emit Room.redactionCancelled event when undoing redaction local echo 2019-06-04 18:45:13 +02:00
Bruno Windels c0c9f0122c remove leftover newline 2019-06-04 16:08:14 +02:00
Bruno Windels d33395e46d improve naming and commenting for _aggregateNonLiveRelation 2019-06-04 15:54:31 +02:00
Bruno Windels b83c7d3929 unneeded check, as redacted_because is now also set for local echo 2019-06-04 15:49:52 +02:00
Bruno Windels b5df016b1b remove unused method 2019-06-04 15:28:40 +02:00
Bruno Windels a8b6be3b38 also set redacted_because with redaction local echo 2019-06-04 13:37:24 +02:00
Bruno Windels 78cf175f5a also look for redaction local echo event in pendingList
also re-aggregate the relation if it's redaction has been cancelled
2019-06-04 11:55:48 +02:00
Travis Ralston 1b78856a7d Merge pull request #934 from matrix-org/travis/re-check-version
Refresh safe room versions when the server looks more modern than us
2019-06-04 01:40:57 -06:00
Bruno Windels 8194287391 make redactEvent go through same local-echo aware path as other events 2019-06-03 18:37:25 +02:00
Bruno Windels 2eecea9a07 handle redactions in room pending event logic 2019-06-03 18:37:01 +02:00
Bruno Windels 465032dd4f support marking an event as redacted in a way we can undo it later
in case the redaction can't be sent
2019-06-03 18:36:03 +02:00
Travis Ralston e473315a89 Check for the correct capability when refreshing 2019-06-03 09:56:55 -06:00
Travis Ralston 9d34ad5287 Merge pull request #935 from matrix-org/travis/v4-rooms
Add v4 as a safe room version
2019-06-03 07:11:13 -06:00
Travis Ralston a532cc5cf9 Add v4 as a safe room version
It's listed as stable in the spec, and this is for our fallback.
2019-06-02 23:36:02 -06:00
Travis Ralston 60c6c5bc41 Refresh safe room versions when the server looks more modern than us
Fixes https://github.com/vector-im/riot-web/issues/9845
2019-06-02 23:34:58 -06:00
J. Ryan Stinnett 0cbbbe8503 Merge pull request #933 from matrix-org/jryans/disable-guard-for-in
Disable guard-for-in rule
2019-05-31 18:10:57 +01:00
J. Ryan Stinnett 47a8d3e50a Disable guard-for-in rule
The Google code style config for ESLint turns on `guard-for-in` to require
for-in loops to check own properties. This makes it annoying to iterate objects,
and we seem to disable the rule by line comments when it comes up anyway, so
this just disables it globally.
2019-05-31 16:36:54 +01:00
RiotRobot 304da09f3b Merge branch 'master' into develop 2019-05-31 10:30:58 +01:00
RiotRobot acd4dcb56e v2.0.0 2019-05-31 10:29:21 +01:00
RiotRobot c170456cde Prepare changelog for v2.0.0 2019-05-31 10:29:20 +01:00
J. Ryan Stinnett 4e739e4b06 Merge pull request #932 from matrix-org/jryans/login-change-release
Saves access_token and user_id after login for all login types
2019-05-31 10:05:34 +01:00
J. Ryan Stinnett 137c6919f6 Fix undefined logger in webstorage.js 2019-05-31 10:01:32 +01:00
J. Ryan Stinnett 842ce30190 Fix lint error in login.spec.js 2019-05-31 09:54:47 +01:00
J. Ryan Stinnett df1539040c Fix lint error in login.spec.js 2019-05-31 09:54:11 +01:00
Sergii Stotskyi 2a04459bb2 fix(login): saves access_token and user_id after login for all login types
Signed-off-by: Sergii Stotskyi <sergiy.stotskiy@gmail.com>

Fixes #876
2019-05-31 09:40:51 +01:00
J. Ryan Stinnett 6367bf7c75 Merge pull request #931 from matrix-org/jryans/key-backup-base-x
Fix recovery key encoding for base-x 3.0.5
2019-05-30 18:29:17 +01:00
Travis Ralston a0456dc430 Merge pull request #924 from jkasun/loglevel-extend
Extend loglevel logging for the whole project
2019-05-30 11:20:49 -06:00
Travis Ralston 52ec831b16 Merge pull request #930 from stalniy/fix/save-login-state
fix(login): saves access_token and user_id after login for all login types
2019-05-30 11:20:01 -06:00
J. Ryan Stinnett 8263062fab Fix recovery key encoding for base-x 3.0.5
This fixes our recovery key encoding to work with base-x 3.0.5, which requires the
encoding input to be in a `Buffer`. (base-x is a dependency of bs58.)

Unfortunately, base-x hasn't marked this as breaking change through a major
version increment, so semantic versioning can't help us here to ensure we get
particular version.

I have verified that the change to `Buffer` works with both old and new base-x,
so we should be okay no matter what version is actually selected.

For extra fun, base-x 3.0.5 also uses newer JS features, which `uglify-js`
doesn't support. I have migrated to `terser` instead, which is what we're using
for Riot these days (via Webpack).

Fixes https://github.com/vector-im/riot-web/issues/9901
2019-05-30 15:42:39 +01:00
janith 9a2bf78a8e logger.dir changed to a log 2019-05-30 12:58:32 +05:30
janith cb16f7a60b Minor Fixes 2019-05-30 09:35:37 +05:30
janith ad84631ddb Change logger import to ES6 2019-05-30 09:27:25 +05:30
RiotRobot d78426d708 Merge branch 'master' into develop 2019-05-29 15:53:51 +01:00
RiotRobot a9543df6db v1.2.0 2019-05-29 15:52:20 +01:00
RiotRobot aae388be93 Prepare changelog for v1.2.0 2019-05-29 15:52:19 +01:00
Sergii Stotskyi 4ef970b4da fix(login): saves access_token and user_id after login for all login types
Signed-off-by: Sergii Stotskyi <sergiy.stotskiy@gmail.com>

Fixes #876
2019-05-28 17:47:49 +03:00
Travis Ralston b199f133b3 fixup readme 2019-05-27 10:41:04 -06:00
Travis Ralston 7d1b183a1b Merge pull request #929 from spantaleev/fix-non-integer-media-sizes
Do not try to request thumbnails with non-integer sizes
2019-05-27 08:37:03 -06:00
Slavi Pantaleev 49d119e92e Do not try to request thumbnails with non-integer sizes
Issue described in: https://github.com/vector-im/riot-web/issues/9690

matrix-react-sdk was patched separately, so that it won't call `mxcUrlToHttp()`
(and in turn `getHttpUriForMxc()`) with non-integer sizes.

This patch fixes the JS SDK as well, hoping to prevent the same issue
from happening on other clients (besides matrix-react-sdk / riot-web).

Signed-off-by: Slavi Pantaleev <slavi@devture.com>
2019-05-27 11:25:35 +03:00
Janith Kasun feed1da570 Merge branch 'develop' into loglevel-extend 2019-05-24 18:00:40 +05:30
Travis Ralston a3ad835d84 Merge pull request #928 from matrix-org/revert-927-travis/wk-discovery
Revert "Add a bunch of debugging to .well-known IS validation"
2019-05-23 14:47:48 -06:00
Travis Ralston f8afee8ebd Revert "Add a bunch of debugging to .well-known IS validation" 2019-05-23 14:47:38 -06:00
Travis Ralston 7e955fc312 Merge pull request #927 from matrix-org/travis/wk-discovery
Add a bunch of debugging to .well-known IS validation
2019-05-23 14:19:33 -06:00
Travis Ralston eebf92366f Add a bunch of debugging to .well-known IS validation 2019-05-23 14:00:17 -06:00
RiotRobot 3c23e166a7 Upgrade jsdoc 2019-05-23 16:30:30 +01:00
RiotRobot 26a8439ce4 Upgrade jsdoc 2019-05-23 16:28:26 +01:00
RiotRobot 2c2e8fa1ac v1.2.0-rc.1 2019-05-23 16:11:35 +01:00
RiotRobot ddc2fa74b9 Prepare changelog for v1.2.0-rc.1 2019-05-23 16:11:35 +01:00
David Baker 93d51b83c3 Merge pull request #926 from matrix-org/dbkr/uiauth_send_email
interactive-auth now handles requesting email tokens
2019-05-22 14:24:08 +01:00
David Baker f83eae4a46 typing hard 2019-05-22 13:11:05 +01:00
David Baker 87c6d11fca PR feedback 2019-05-22 13:09:24 +01:00
David Baker e87ac86e48 interactive-auth now handles requesting email tokens
interactive-auth now has a callback to request the email token which
it will call at the appropriate time (now at the start of the
auth process if the chosen flow contain an email auth stage).

This does make this a breaking change, although not sure this is
used outside of Riot. We could make it backwards compatible by
having an option for the new behaviour. It may not be worthwhile
though.

https://github.com/vector-im/riot-web/issues/9586
2019-05-22 11:51:05 +01:00
Janith Kasun de8063a43a Merge branch 'develop' into loglevel-extend 2019-05-19 09:40:38 +05:30
jkasun a73dabcb67 Console logging to loglevel 2019-05-19 09:29:40 +05:30
Bruno Windels 7782e81101 Merge pull request #923 from matrix-org/bwindels/caneditcheckfix
allow access to unreplaced message content
2019-05-17 12:32:45 +00:00
Bruno Windels aa70687d9e allow access to unreplaced message content 2019-05-17 13:25:21 +01:00
Bruno Windels 38d32de06b Merge pull request #922 from matrix-org/bwindels/editedmarker
Add method to retrieve replacing event
2019-05-17 11:04:00 +00:00
Bruno Windels 7720c72b73 correct return type 2019-05-17 12:03:36 +01:00
Bruno Windels ff9505073f add method to retrieve replacing event 2019-05-17 11:56:28 +01:00
David Baker 21ee1c31a7 Merge pull request #921 from matrix-org/dbkr/really_log_useful_info_on_verify_failure
More logging when signature verification fails
2019-05-17 10:18:34 +01:00
David Baker fd01ba1fcf More logging when signature verification fails
Still no luck diagnosing https://github.com/vector-im/riot-web/issues/9357
so adding more logging.
2019-05-17 10:08:27 +01:00
Bruno Windels fbf53524ed Merge pull request #920 from matrix-org/bwindels/message-editing-local-echo
Local echo for m.replace relations
2019-05-16 15:22:12 +00:00
Bruno Windels 1f2a701ace remove double newline 2019-05-16 16:14:53 +01:00
Bruno Windels 0b87a573b3 reduce indenting by returning early, also actually pass replacement!
also add comment
2019-05-16 16:14:19 +01:00
Bruno Windels 74438716af PR feedback/cleanup 2019-05-16 15:54:15 +01:00
Bruno Windels d10b348e74 clear event replacement on redaction
as redaction supersedes a replacement
2019-05-16 15:53:46 +01:00
Bruno Windels 68e9be47d9 check if an incoming event is the target of a Relations 2019-05-16 15:49:55 +01:00
Bruno Windels bddd03c2fd separate setTargetEvent method
call makeReplaced from addEvent instead so it's all done from Relations
2019-05-16 15:48:03 +01:00
Bruno Windels e23ba50dd8 only call aggregation code for local echo for relations 2019-05-16 14:42:09 +01:00
Bruno Windels 261ab7ae68 don't block local echo for m.replace anymore! 2019-05-16 14:40:55 +01:00
Bruno Windels 21c8c76dc3 helper method to get sending status of event or replacement 2019-05-16 14:40:39 +01:00
Bruno Windels 266d0f9d05 remove flag for edits as it's not handled separately anymore 2019-05-16 14:40:17 +01:00
Bruno Windels 69d25c1498 emit from MatrixEvent.makeReplaced instead of Room
now that event can be replaced from Relations instead of Room

Also make `makeReplaced` non-destructive by not touching the original
event.content, so it can be undone by later calls.
2019-05-16 14:38:48 +01:00
Bruno Windels 3cd2b3925a re-evaluate last replacement on redaction and local echo cancellation 2019-05-16 14:36:38 +01:00
Bruno Windels 33e9eb371e use relation handling in timelineset for replacements 2019-05-16 14:35:50 +01:00
Bruno Windels 07572d1e8d helper method to get last valid replacement 2019-05-16 14:34:08 +01:00
Bruno Windels dde4f558f3 set targetEvent on Relations, once known 2019-05-16 14:33:24 +01:00
J. Ryan Stinnett 79d2574ea7 Merge pull request #919 from matrix-org/jryans/reactions-status-bar
Track relations as pending and remove when cancelled
2019-05-16 13:39:41 +01:00
J. Ryan Stinnett 51fb5c4a15 Update aggregation by sender on remove
This updates the aggregation by sender in the relations collection on removal.
It also changes this aggregation to use a Set, so consumers will need to update.
2019-05-16 12:40:19 +01:00
J. Ryan Stinnett 875c6b973b Remove cancelled relations from the relations collection
This listens for event status changes on sending events in case they might be
cancelled and removes them from aggregation if so.

Part of https://github.com/vector-im/riot-web/issues/9731
2019-05-16 12:29:28 +01:00
J. Ryan Stinnett 21e1312dd7 Allow relations into the pending event list
Relations actually should go into the pending event list, just like messages.
This is the easiest way to keep them in a holding area in case of unverified
devices, etc.

We still want the relations to local echo immediately, so we directly trigger
the aggregation on the timeline sets.
2019-05-16 12:26:36 +01:00
J. Ryan Stinnett a722ef3b03 Merge pull request #916 from matrix-org/jryans/stringify-events
Add stringify helper to summarise events when debugging
2019-05-16 09:44:39 +01:00
J. Ryan Stinnett 80ba5d29f2 Add stringify helper to summarise events when debugging 2019-05-16 09:31:19 +01:00
Bruno Windels a35e6a0f54 Merge pull request #918 from matrix-org/bwindels/message-edit-editor3
Message editing: filter out replacements for senders that are not the original sender
2019-05-15 17:21:03 +00:00
J. Ryan Stinnett bdc1958c08 Merge pull request #917 from matrix-org/jryans/encrypted-reactions
Wait until decrypt before aggregating
2019-05-15 17:26:07 +01:00
Bruno Windels 3f2bac71c6 filter out replacements for senders that are not the original sender 2019-05-15 15:52:37 +01:00
J. Ryan Stinnett 6b9a11b697 Wait until decrypt before aggregating
For encrypted annotations, we need to wait until the event has been decrypted
before adding it to the relations collection.
2019-05-15 15:48:33 +01:00
J. Ryan Stinnett f17ecba519 Add getRelation helper
This adds a `getRelation` helper to ensure we always read relation info from the
wire content as required in E2E rooms.
2019-05-15 15:48:33 +01:00
Bruno Windels ce0b014a5a Merge pull request #914 from matrix-org/bwindels/message-edit-editor2
Message editing: mark original event as replaced instead of replacing the event object
2019-05-15 14:11:38 +00:00
Bruno Windels ad48d2997e prevent earlier replacements from messing things up 2019-05-15 14:54:52 +01:00
Bruno Windels 1c1781ce76 make replacements work in e2e rooms 2019-05-15 14:54:21 +01:00
Bruno Windels 5fd001354a replace content when replacing instead of evaluating in getContent 2019-05-15 14:53:44 +01:00
Bruno Windels a18bdad44f dont replace a redacted event 2019-05-15 13:56:13 +01:00
Bruno Windels 600dff62e8 detect relations on encrypted events properly 2019-05-15 13:56:00 +01:00
Bruno Windels db7a402e9b mark original event as replaced instead of replacing the event object
this is more in line with what happens on the server-side,
and also doesn't break existing reply relations.
2019-05-15 12:05:39 +01:00
Bruno Windels 0e53f9052f Merge pull request #913 from matrix-org/bwindels/message-edit-editor
Support for replacing message through m.replace relationship.
2019-05-15 09:22:34 +00:00
Bruno Windels 62e69cacb7 remove leftover code, fix lint 2019-05-14 15:52:02 +01:00
Bruno Windels 852c88c341 add unstableClientRelationReplacements in js-sdk 2019-05-14 15:33:49 +01:00
Bruno Windels 455f52f1f5 remove logging 2019-05-14 15:25:47 +01:00
Bruno Windels df6012c58d completely avoid local echo for edits for now 2019-05-14 15:25:07 +01:00
Bruno Windels f68a3dde46 cleanup 2019-05-14 15:24:57 +01:00
Bruno Windels 25e6b1cac8 handle m.replace relations in room, emit Room.replaceEvent 2019-05-14 15:24:36 +01:00
Travis Ralston 4ad20526db Merge pull request #912 from matrix-org/travis/wk/timeouts
Use a short timeout for .well-known requests
2019-05-14 08:24:09 -06:00
Bruno Windels 18cd017f58 support marking an as replacing another
and take if the timestamp of the original event if so
also helper methods
2019-05-14 15:23:22 +01:00
Bruno Windels 0161664b6c add support for replacing an existing event in a timeline(set) 2019-05-14 15:22:46 +01:00
Travis Ralston 25df31bf96 Use a short timeout for .well-known requests
Applies to verification of the homeserver, identity server, and fetching of the .well-known objects. Does not affect other HTTP requests.

See https://github.com/vector-im/riot-web/issues/9290
2019-05-13 18:39:59 -06:00
Travis Ralston 09438b440e Fix spelling error in txnId for redactions
Fixes https://github.com/vector-im/riot-web/issues/9700
2019-05-13 13:43:44 -06:00
J. Ryan Stinnett 6a5f5b249e Merge pull request #911 from matrix-org/jryans/aggregations
Redaction and change events for relations
2019-05-13 15:20:15 +01:00
J. Ryan Stinnett 3a20114c39 Change to event-level beforeRedaction event for efficiency
To avoid an O(n^2) situation with every relations container trying to process
every redaction that may occur in a room, this switches to an event-level
notification, so that the specific relations container who cares can listen to
just the events it wants to know about.
2019-05-13 14:35:39 +01:00
J. Ryan Stinnett f411d50253 Clarify before redaction event timing 2019-05-13 14:17:23 +01:00
J. Ryan Stinnett 00851df25c Always add pending relation events to the timeline sets directly
This special cases pending relation events to go directly to the timeline sets
and ignores the `pendingEventOrdering` option. This feels a bit strange in the
code, so we should revisit this choice when we stabilized relation support.
2019-05-13 13:52:37 +01:00
J. Ryan Stinnett 8822d255b3 Fix indentation in src/models/event.js 2019-05-13 13:52:37 +01:00
J. Ryan Stinnett 4b4ba86167 Add Relations.add event for additional relations in collection
This adds a `Relations.add` event that consumers can listen for to be notified
each time an additional relation event is added to a relations collection.

Part of https://github.com/vector-im/riot-web/issues/9485
Part of https://github.com/vector-im/riot-web/issues/9572
2019-05-13 13:52:37 +01:00
J. Ryan Stinnett 7ea820f6e1 Add Event.relationsCreated event to listen for future relations collections
If you ask for relations but none currently exist, we return `null` to avoid the
overhead of many empty relations objects in memory. However, we still want some
way to alert consumers when one _is_ later made, so this event level event
provides that.

Part of https://github.com/vector-im/riot-web/issues/9572
Part of https://github.com/vector-im/riot-web/issues/9485
2019-05-13 13:52:37 +01:00
J. Ryan Stinnett 53d8cf0852 Update relation collections after redaction
This watches for redactions of relations and updates the relations collection
to match, including various aggregations. In addition, a redaction event is
emitted on the redaction collection to notify consumers of the change.

Part of https://github.com/vector-im/riot-web/issues/9574
Part of https://github.com/vector-im/riot-web/issues/9485
2019-05-13 13:52:37 +01:00
J. Ryan Stinnett 761806c678 Add support for class properties
This enables compiler and linting features to allow class properties like we do
in the React SDK.
2019-05-13 13:52:37 +01:00
J. Ryan Stinnett d6abd639f3 Merge pull request #910 from matrix-org/jryans/aggregations
Add basic read path for relations
2019-05-09 12:01:57 +01:00
J. Ryan Stinnett 6078bbbe24 Add basic read path for relations
This adds a read path for relations (gated behind an unstable option). A few
basic client-side grouping and sorting operations are supported. Consumers are
expected to ask the `EventTimelineSet` for a relation container when desired.
2019-05-08 18:05:52 +01:00
J. Ryan Stinnett c1c81df4de Intern rel_type for relations
In anticipation of relations being quite frequently used, we should intern
strings of common fields, such as `rel_type`.
2019-05-08 17:41:06 +01:00
David Baker ee8a4698a9 Merge branch 'master' into develop 2019-05-07 15:25:23 +01:00
David Baker c1956d3f05 v1.1.0 2019-05-07 15:23:32 +01:00
David Baker 56316dc5d9 Prepare changelog for v1.1.0 2019-05-07 15:23:32 +01:00
Travis Ralston b5c74b5666 Merge pull request #860 from matrix-org/travis/tombstone-notif
Add a concept of default push rules, using it for tombstone notifications
2019-05-03 11:21:37 -06:00
Travis Ralston dc946dffbc Add some words to explain why we do things the way we do 2019-05-03 11:19:46 -06:00
David Baker 937baadb9b Merge pull request #907 from matrix-org/dbkr/yarn_upgrade_may19
yarn upgrade
2019-05-03 12:00:03 +01:00
David Baker 8d0c03b4f0 Add customer resolution for base-x
to fix dependency version to 3.0.4 (ie. the version that exports
ES5 rather than ES6)
2019-05-03 11:46:11 +01:00
David Baker a3fba73044 Set base-x back to 3.0.4
3.0.5 exports ES6 which breaks the build.

Also specifically depending on version 3.0.4 in the package.json
doesn't look like it has the desired effect now (yarn just installs
two separate versions) so remove that.
2019-05-03 11:03:29 +01:00
David Baker 116cf31199 yarn upgrade 2019-05-03 10:50:23 +01:00
J. Ryan Stinnett cdb78e4c75 Remove noisy debug logs
The debug logs in the sync loop haven't been helpful so far, and they are quite
noisy pushing other logs out of the way, so this change removes them.
2019-04-30 15:43:54 +01:00
David Baker 0bb9c56e94 Merge branch 'develop' into release-v1.1.0 2019-04-30 11:50:24 +01:00
David Baker 821f1c876b Undo unintentional commenting 2019-04-30 11:48:38 +01:00
David Baker e9b95f8567 v1.1.0-rc.1 2019-04-30 11:43:35 +01:00
David Baker 103d811441 Prepare changelog for v1.1.0-rc.1 2019-04-30 11:43:34 +01:00
David Baker bb4f5a3fa1 Get the name of the pipeline right 2019-04-26 18:07:04 +01:00
David Baker f5cbdeac8f Trigger react-sdk build in buildkite pipeline 2019-04-26 18:06:04 +01:00
Hubert Chathi 56062e8e4e Merge pull request #903 from uhoreg/olm-3.1.0
use the release version of olm 3.1.0
2019-04-24 18:19:15 -04:00
Hubert Chathi 03c85d48e5 update the yarn.lock with the olm release 2019-04-24 12:07:10 -04:00
Hubert Chathi dd8f0fbdcb use the release version of olm 3.1.0 2019-04-23 18:06:09 -04:00
Travis Ralston fac61a76e9 Merge pull request #901 from matrix-org/travis/olm2
Use new Olm repo link in README
2019-04-18 08:26:12 -06:00
Travis Ralston 3b09ab3ca1 Use new Olm repo link in README 2019-04-17 23:50:51 -06:00
Travis Ralston 16bfe79305 Merge pull request #897 from matrix-org/travis/wk-custom
Support being fed a .well-known config object for validation
2019-04-17 09:58:22 -06:00
J. Ryan Stinnett d668f97c98 Clarify comment
Co-Authored-By: turt2live <travpc@gmail.com>
2019-04-17 09:44:52 -06:00
Bruno Windels 18bd10b03d Merge pull request #900 from matrix-org/bwindels/stylepreviewbar
emit self-membership event at end of handling sync update
2019-04-17 12:31:16 +00:00
Bruno Windels 6b1d089caf Merge branch 'develop' into bwindels/stylepreviewbar 2019-04-17 14:24:25 +02:00
Travis Ralston af93401385 Use more appropriate errors for some situations 2019-04-16 11:13:36 -06:00
Travis Ralston fa5add3d99 Use the right error codes 2019-04-16 11:03:58 -06:00
Travis Ralston fb971580e0 Merge branch 'develop' into travis/wk-custom 2019-04-16 11:01:49 -06:00
Bruno Windels dcaea98e33 emit self-membership event at end of handling sync update
otherwise the room state isn't updated yet (and we can't for
example distinguish a leave from a kick)

this is only used for updating the UI,
seems safe to emit this event at a later point
2019-04-16 17:16:24 +02:00
Travis Ralston b95079d1c5 Merge pull request #898 from matrix-org/travis/packages.matrix.org
Use packages.matrix.org for Olm
2019-04-16 09:14:21 -06:00
J. Ryan Stinnett e59f36cdc0 Merge pull request #899 from matrix-org/travis/fix-tests
Fix tests on develop
2019-04-16 09:36:56 +01:00
Travis Ralston 0f2f041d8b Use constants for autodiscovery errors
To ease usage
2019-04-15 22:04:24 -06:00
Travis Ralston 33d2837ec3 Use packages.matrix.org for Olm
See https://github.com/vector-im/riot-web/issues/9497
2019-04-15 21:23:18 -06:00
Travis Ralston 7cede221de Support being fed a .well-known config object for validation
Used by Riot to consume the user's provided config. This also includes a change to carry over custom keys on m.homeserver and m.identity_server which aren't intentionally controlled.
2019-04-15 21:22:49 -06:00
Travis Ralston fe47435fc7 Fix tests for autodiscovery 2019-04-15 21:12:17 -06:00
Travis Ralston 491226a916 Use the right this in _shouldAbortSync 2019-04-15 21:04:29 -06:00
Travis Ralston deb7433453 Use toMatch for presence events
We don't pass the reference through, so the test fails with toEqual
2019-04-15 20:17:42 -06:00
Travis Ralston 14973a35c2 Use the right error object 2019-04-15 19:46:30 -06:00
Travis Ralston b5779f8654 Merge pull request #895 from matrix-org/travis/stop-client-on-logout
Stop syncing when the token is invalid
2019-04-15 11:30:24 -06:00
Travis Ralston f7d1984257 De-duplicate usage of shouldAbortSync 2019-04-15 11:29:55 -06:00
Bruno Windels 3fba683090 Merge pull request #887 from jkasun/fix_redact_put
change event redact,  POST request to PUT request
2019-04-15 15:13:59 +00:00
jkasun 430da8ac09 JS Doc Fix 2019-04-15 20:34:39 +05:30
Travis Ralston dcd9b5c382 Stop syncing when the token is invalid
Fixes https://github.com/vector-im/riot-web/issues/9451
2019-04-14 21:38:51 -06:00
jkasun 348c293962 Argument Length Check, Duplicate Fix for Redact Funcation 2019-04-13 22:23:57 +05:30
Travis Ralston 34309da10c Merge pull request #894 from matrix-org/travis/guests/better-errors
Expose better autodiscovery error messages
2019-04-12 10:21:05 -06:00
Travis Ralston 6db973f430 Expose better autodiscovery error messages
Fixes https://github.com/vector-im/riot-web/issues/7925
2019-04-11 15:50:43 -06:00
J. Ryan Stinnett 9f27bafa62 Merge pull request #892 from jryans/degraded-storage
Explicitly guard store usage during sync startup
2019-04-09 17:12:07 +01:00
Travis Ralston b05136146a Merge pull request #893 from matrix-org/travis/v3-safe
Flag v3 rooms as safe
2019-04-09 10:00:56 -06:00
Travis Ralston 13d3be637b Copyright 2019 NV 2019-04-09 09:58:33 -06:00
J. Ryan Stinnett 44de7fad6f Preserve previous error flow 2019-04-09 16:44:36 +01:00
J. Ryan Stinnett b99243406b Move logs inside try block 2019-04-09 16:40:13 +01:00
Travis Ralston 0e9ec811b0 Merge pull request #890 from matrix-org/travis/cached-capabilities
Cache failed capabilities lookups for shorter amounts of time
2019-04-09 09:29:42 -06:00
Travis Ralston 6979177fb2 Log errors for capabilities requests 2019-04-09 09:27:38 -06:00
Travis Ralston 3aa8bfa6ca Flag v3 rooms as safe
The spec says they are, so we might as well too.
2019-04-09 09:25:07 -06:00
Travis Ralston 17b356b08e Merge pull request #891 from matrix-org/travis/fix-notifs
Fix highlight notifications for unencrypted rooms
2019-04-09 09:19:58 -06:00
Travis Ralston f72ae490a8 Appease the linter 2019-04-09 09:05:20 -06:00
J. Ryan Stinnett b0c3d0d2e3 Explicitly guard store usage during sync startup
This adds explicit `try` blocks in the spots where we interact with the store
during sync startup. This shouldn't be necessary as the store should already be
catching this and degrading as of
https://github.com/matrix-org/matrix-js-sdk/pull/884, but that doesn't seem to
have been enough for the affected user in
https://github.com/vector-im/riot-web/issues/7769, as they are seeing sync just
stop when storing without any further detail.
2019-04-09 15:06:54 +01:00
Travis Ralston 9dc344999e Fix highlight notifications for unencrypted rooms
A logic error introduced by https://github.com/matrix-org/matrix-js-sdk/pull/886 meant that all unencrypted rooms were not getting highlight notifications.
2019-04-08 15:57:44 -06:00
Travis Ralston 663c096400 Cache failed capabilities lookups for shorter amounts of time
This should fix https://github.com/vector-im/riot-web/issues/9225 for showing up too often/too long.
2019-04-08 12:24:00 -06:00
J. Ryan Stinnett 420b4d119d Merge pull request #889 from jryans/guard-missing-crypto
Document checking crypto state before using `hasUnverifiedDevices`
2019-04-08 16:56:13 +01:00
J. Ryan Stinnett 58b752c63b Document checking crypto state before using hasUnverifiedDevices
It's unclear what `hasUnverifiedDevices` should do when crypto is disabled on
the current device. Let's at least document that callers should first check
crypto status.
2019-04-08 16:24:25 +01:00
J. Ryan Stinnett 4740232fa4 Merge pull request #888 from jryans/degraded-storage
Add logging to sync startup path
2019-04-08 16:20:29 +01:00
J. Ryan Stinnett 0cc9994b8b Add logging to sync startup path
In https://github.com/vector-im/riot-web/issues/7769, we're seeing sync startup
fail to complete, but the actual error isn't being logged. Hopefully these extra
debug logs will provide more insight into the failing step.
2019-04-08 15:55:52 +01:00
Bruno Windels 1e78628b23 Merge branch 'master' into develop 2019-04-08 16:04:45 +02:00
Bruno Windels 00f5ddc93c v1.0.4 2019-04-08 16:03:26 +02:00
Bruno Windels f79f2105fd Prepare changelog for v1.0.4 2019-04-08 16:03:25 +02:00
Travis Ralston 828c51467f Refuse to set forwards pagination token on live timeline
Should fix the error seen in https://github.com/matrix-org/riot-web-rageshakes/issues/1389 (https://github.com/vector-im/riot-web/issues/8593)
2019-04-08 15:48:03 +02:00
Travis Ralston 963e271bce Refuse to link live timelines into the forwards/backwards position
See https://github.com/vector-im/riot-web/issues/8593#issuecomment-478681816

Previously (https://github.com/matrix-org/matrix-js-sdk/pull/873) we allowed half-linking timelines to each other if they satisfy the conditions, however this appears to not be helping. Instead, it seems like the timelines are getting stuck in a position where one direction is spliced but the other is broken. To avoid this case, we'll just avoid splicing in both directions when one of the directions is invalid.
2019-04-08 15:47:40 +02:00
Travis Ralston 0ab41215f0 Add a tiny bit of logging to work out what timelines are doing
See https://github.com/vector-im/riot-web/issues/8593
2019-04-08 15:47:21 +02:00
Travis Ralston d153c4da07 log the timeline that broke 2019-04-08 15:47:03 +02:00
Travis Ralston 91aa783c3d Use better words for warnings 2019-04-08 15:46:56 +02:00
Travis Ralston a54845bf76 Appease the linter 2019-04-08 15:46:48 +02:00
Travis Ralston 8a56a5f1ed Refuse splicing the live timeline into a broken position
Credit to Matthew for basically solving this.

Theoretically fixes spontaneous timeline corruption: https://github.com/vector-im/riot-web/issues/8593

When the live timeline ends up in a position where it can no longer be live (such as becoming the second timeline in the set, rather than the first) we end up getting neighbouring timeline errors. By refusing to splice the live timeline into such a position, we hopefully keep the live timeline in a position of still being live for when it is next used.

The running theory that leads to this fix is multiple limited syncs coming in, causing holes in the timeline. When trying to patch up the holes, the timeline set would end up splicing all over the place, leading to potentially splicing the live timeline into a broken position.
2019-04-08 15:46:41 +02:00
Travis Ralston f585c80491 Merge pull request #886 from matrix-org/travis/e2e-notifs-2
Track e2e highlights better, particularly in 'Mentions Only' rooms
2019-04-08 07:40:31 -06:00
jkasun c495b12cef change event redact, POST request to PUT request 2019-04-07 00:19:02 +05:30
Travis Ralston 1d6f7f862f Track e2e highlights better, particularly in 'Mentions Only' rooms
Fixes https://github.com/vector-im/riot-web/issues/9280

The server is unable to calculate encrypted highlights for us, so we calculate them. This also means the server always sends a zero for highlight_count, and therefore in sync.js we now trust our judgement over the server's. In future, this check will need to be altered to support server-side encrypted notifications if that happens. This fixes the part of 9280 where the badge count ends up disappearing unless the message received also happens to be a mention.

The changes in client.js are more to support rooms which are mentions only. Because the server doesn't send an unread_count for these rooms, the total notifications will always be zero. Therefore, we try and calculate that. In order to do that, we need to assume that our highlight count is also wrong and calculate it appropriately.
2019-04-05 14:49:38 -06:00
Hubert Chathi 0a82c84006 Merge pull request #882 from uhoreg/fix_mac
support both the incorrect and correct MAC methods
2019-04-05 16:45:45 -04:00
Travis Ralston a614a02b90 Merge pull request #885 from matrix-org/travis/refuse-forwards-token
Refuse to set forwards pagination token on live timeline
2019-04-05 11:54:33 -06:00
Travis Ralston 0945e2c5c6 Refuse to set forwards pagination token on live timeline
Should fix the error seen in https://github.com/matrix-org/riot-web-rageshakes/issues/1389 (https://github.com/vector-im/riot-web/issues/8593)
2019-04-05 11:36:01 -06:00
J. Ryan Stinnett b1b49413d0 Merge pull request #884 from jryans/degraded-storage
Degrade `IndexedDBStore` back to memory only on failure
2019-04-05 11:32:42 +01:00
Hubert Chathi 01af303d63 fix the selection of the verification methods, and test more things 2019-04-04 14:08:30 -04:00
Hubert Chathi 751060305c update the name of the MAC method 2019-04-04 14:07:16 -04:00
J. Ryan Stinnett 389fcfaf3d Ensure IDB store maintains all memory state
A few of the IDB store methods weren't updating memory store state, so let's
improve those so we can reliably fall back to it from IDB at any time.
2019-04-04 17:39:50 +01:00
J. Ryan Stinnett 3eb0c534a5 Clarify why it's safe to change store types on demand 2019-04-04 15:54:26 +01:00
J. Ryan Stinnett 8a2f84b678 Emit event when IndexedDBStore degrades
This allows for optional tracking of when the store degrades to see how often it
happens in the field.
2019-04-04 12:06:41 +01:00
J. Ryan Stinnett dd00735409 Degrade IndexedDBStore back to memory only on failure
IndexedDB may fail at any moment with `QuoteExceededError` when space is low or
other random issues we can't control. Since `IndexedDBStore` is just a cache for
improving performance, we can give up on it if it fails.

This causes `IndexedDBStore` to degrade in place back to using memory only. This
allow (for example) login to complete even if IndexedDB is exploding.

Hopefully improves https://github.com/vector-im/riot-web/issues/7769
2019-04-04 12:06:41 +01:00
J. Ryan Stinnett 6ba7e85e24 Ensure we have crypto before accessing it in Room model
If crypto startup has failed, we shouldn't try to access any of its methods.
This fixes a variant of this in the `Room` model.
2019-04-04 12:06:41 +01:00
Travis Ralston 941d93c2f4 Merge pull request #877 from matrix-org/travis/tlexpl-full-abort
Refuse to link live timelines into the forwards/backwards position when either is invalid
2019-04-03 09:59:09 -06:00
David Baker c73e9cbc7c Merge pull request #883 from matrix-org/dbkr/logging_for_9357
Key backup logging improvements
2019-04-03 11:45:10 +01:00
David Baker ae89e4bf21 Key backup logging improvements
To try & diagnose https://github.com/vector-im/riot-web/issues/9357
2019-04-03 11:00:31 +01:00
Hubert Chathi d1e64d0cfb support both the incorrect and correct MAC methods
also do some refactoring to make it easier to support choices in the other
methods in the future
2019-04-02 23:36:49 -04:00
David Baker 8f9f9590d9 Merge pull request #880 from matrix-org/dbkr/more_logging_for_7769
Don't assume aborts are always from txn.abort()
2019-04-02 18:33:32 +01:00
David Baker ed68093310 lint 2019-04-02 18:30:49 +01:00
David Baker 23655e748d Don't assume aborts are always from txn.abort()
According to https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/abort_event
a txn can abort for any number of reasons, not just because of an
abort call, so we can't assume our abort error is set.

This should give us more info on https://github.com/vector-im/riot-web/issues/7769
2019-04-02 18:25:23 +01:00
David Baker 98624871bd Merge pull request #878 from matrix-org/dbkr/logging_for_7769
Add a bunch of logging
2019-04-02 13:34:26 +01:00
Will Hunt 7b154c0834 Revert "Remove "Event sent to" line from client.js"
This reverts commit d9f056242f.
2019-04-02 13:24:58 +01:00
Will Hunt d9f056242f Remove "Event sent to" line from client.js
Emitting a log line here means that bridges emit this for each event sent in stdout, which bloats forever.log
2019-04-02 13:22:27 +01:00
David Baker a4268d288e Add a bunch of logging
to try & diagnose https://github.com/vector-im/riot-web/issues/7769
2019-04-02 12:30:24 +01:00
Travis Ralston f90c91dded Refuse to link live timelines into the forwards/backwards position
See https://github.com/vector-im/riot-web/issues/8593#issuecomment-478681816

Previously (https://github.com/matrix-org/matrix-js-sdk/pull/873) we allowed half-linking timelines to each other if they satisfy the conditions, however this appears to not be helping. Instead, it seems like the timelines are getting stuck in a position where one direction is spliced but the other is broken. To avoid this case, we'll just avoid splicing in both directions when one of the directions is invalid.
2019-04-01 18:43:48 -06:00
Travis Ralston 5c8890c3c1 Add a tiny bit of logging to work out what timelines are doing
See https://github.com/vector-im/riot-web/issues/8593
2019-04-01 11:11:35 -06:00
David Baker cd3c6809a9 Merge branch 'master' into develop 2019-04-01 13:33:51 +01:00
David Baker e2c17528c2 v1.0.3 2019-04-01 13:31:45 +01:00
David Baker e00f565f37 Prepare changelog for v1.0.3 2019-04-01 13:31:44 +01:00
J. Ryan Stinnett 085e797c30 Merge pull request #874 from matrix-org/travis/1.0.6-fire/872
Add existence check to local storage based crypto store
2019-04-01 09:26:39 +01:00
J. Ryan Stinnett d753db590b Add existence check to local storage based crypto store
This supports additional diagnostics of stores in
https://github.com/vector-im/riot-web/issues/9309.
2019-03-29 16:19:54 -06:00
Travis Ralston eab074a27b Merge pull request #873 from matrix-org/travis/refuse-splicing
Refuse splicing the live timeline into a broken position
2019-03-28 19:20:58 -06:00
Travis Ralston e2a3e3816f log the timeline that broke 2019-03-28 19:14:17 -06:00
Travis Ralston 01dd57adab Use better words for warnings 2019-03-28 18:59:40 -06:00
Travis Ralston 08e674b695 Appease the linter 2019-03-28 18:34:57 -06:00
Travis Ralston 9f70970e61 Refuse splicing the live timeline into a broken position
Credit to Matthew for basically solving this.

Theoretically fixes spontaneous timeline corruption: https://github.com/vector-im/riot-web/issues/8593

When the live timeline ends up in a position where it can no longer be live (such as becoming the second timeline in the set, rather than the first) we end up getting neighbouring timeline errors. By refusing to splice the live timeline into such a position, we hopefully keep the live timeline in a position of still being live for when it is next used.

The running theory that leads to this fix is multiple limited syncs coming in, causing holes in the timeline. When trying to patch up the holes, the timeline set would end up splicing all over the place, leading to potentially splicing the live timeline into a broken position.
2019-03-28 18:30:41 -06:00
J. Ryan Stinnett e9ffd5a125 Merge pull request #872 from jryans/storage-existence-check-ls-crypto
Add existence check to local storage based crypto store
2019-03-28 17:49:08 +00:00
J. Ryan Stinnett 20f4469361 Add existence check to local storage based crypto store
This supports additional diagnostics of stores in
https://github.com/vector-im/riot-web/issues/9309.
2019-03-28 12:20:51 +00:00
David Baker feac096dc2 v1.0.3-rc.1 2019-03-27 17:29:28 +00:00
David Baker 67985d449a Prepare changelog for v1.0.3-rc.1 2019-03-27 17:29:27 +00:00
J. Ryan Stinnett a6de59c198 Update matrix-mock-request 2019-03-27 15:02:48 +00:00
J. Ryan Stinnett 01eeb98e35 Merge pull request #871 from jryans/storage-edge-cases
Add IndexedDB existence checks
2019-03-26 15:32:23 +00:00
J. Ryan Stinnett 49a7defbf0 Close the DB right away
This seems to be needed in Chrome to avoid blocking future opens.
2019-03-26 14:58:45 +00:00
J. Ryan Stinnett c1ba5de686 Add missing word 2019-03-26 11:25:59 +00:00
J. Ryan Stinnett 81428f23d1 Tweak code style 2019-03-26 11:21:22 +00:00
Travis Ralston 8af86bb746 Merge pull request #869 from matrix-org/travis/timeline-explosion-workaround
Emit sync errors for capturing by clients
2019-03-25 11:33:01 -06:00
J. Ryan Stinnett 8513f5c413 Add an IndexedDB existence check to the crypto store
This will be useful for future storage diagnostics as part of
https://github.com/vector-im/riot-web/issues/9271.
2019-03-25 16:11:59 +00:00
J. Ryan Stinnett eadec35093 Add an IndexedDB existence check to the main store
This will be useful for future storage diagnostics as part of
https://github.com/vector-im/riot-web/issues/9271.
2019-03-25 16:11:51 +00:00
Travis Ralston d6dbd621b8 Merge pull request #868 from matrix-org/travis/leave-room-chain
Add functions for getting room upgrade history and leaving those rooms
2019-03-25 09:57:10 -06:00
Travis Ralston 7168f76614 Emit sync errors for capturing by clients
For https://github.com/vector-im/riot-web/issues/9260
2019-03-22 20:19:38 -06:00
Travis Ralston 1cda95f23c Add functions for getting room upgrade history and leaving those rooms
Required for https://github.com/vector-im/riot-web/issues/8539
2019-03-22 17:34:05 -06:00
J. Ryan Stinnett bb1cd2bbce Merge pull request #867 from jryans/real-name
Clarify the meaning of 'real name' for contribution
2019-03-22 17:26:21 +00:00
J. Ryan Stinnett 3f90ac5712 Merge pull request #865 from jryans/storage-edge-cases
Remove `sessionStore` to `cryptoStore` migration path
2019-03-22 17:25:51 +00:00
J. Ryan Stinnett 8d249a843c Reformat contribution doc to 80 chars 2019-03-22 11:53:43 +00:00
J. Ryan Stinnett 858b41d835 Clarify the meaning of 'real name' for contribution
This applies the clarification already adopted by Synapse:

https://github.com/matrix-org/synapse/commit/ec766b25303b420850e6d2875f156f23109acf6a
2019-03-22 11:51:07 +00:00
Travis Ralston 61aea05af0 Merge pull request #866 from matrix-org/travis/verbose-capabilities
Add debugging for spurious room version warnings
2019-03-20 17:10:23 -06:00
Travis Ralston e7c764d5f5 Add debugging for spurious room version warnings
See https://github.com/vector-im/riot-web/issues/9225
2019-03-20 17:05:09 -06:00
J. Ryan Stinnett 09a9afe4e7 Clarify the current state of each store
This moves docs about the 3 stores used next to each other and clarifies their
purpose for future readers.
2019-03-20 16:56:47 +00:00
J. Ryan Stinnett 5a26503da7 Remove sessionStore to cryptoStore migration path
The code to migrate from the `sessionStore` to `cryptoStore` originally appeared
in https://github.com/matrix-org/matrix-js-sdk/pull/584 (2017-12-06). At this
point, it seems safe to assume most sessions that need migrating have already
done so. Removing this code simplifies store handling and removes the
`sessionStore` from most places in JS SDK.
2019-03-20 16:56:47 +00:00
J. Ryan Stinnett 5faf5ea1f8 Merge pull request #864 from jryans/storage-notes
Add investigation notes for browser storage
2019-03-20 16:35:13 +00:00
J. Ryan Stinnett 0754c29c22 Add investigation notes for browser storage
This collects my notes from investigating the state of browser storage as part
of https://github.com/vector-im/riot-web/issues/9109.
2019-03-20 16:33:47 +00:00
Hubert Chathi d5c6dcf111 Merge pull request #862 from uhoreg/check_resolver
make sure resolve object is defined before calling it
2019-03-20 10:32:10 -04:00
J. Ryan Stinnett 6a57ddd33c Merge pull request #861 from matrix-org/jryans/storage-edge-cases
Rename `MatrixInMemoryStore` to `MemoryStore`
2019-03-20 11:05:55 +00:00
Hubert Chathi bd711cdc1f make sure resolve object is defined before calling it 2019-03-19 23:21:49 -04:00
J. Ryan Stinnett e669e493c9 Add deprecation notice to MatrixInMemoryStore 2019-03-19 15:04:05 +00:00
J. Ryan Stinnett 48f290196c Rename MatrixInMemoryStore to MemoryStore
None of the other store classes use the `Matrix` prefix, and I find the mismatch
confusing (it leads me to think it might have a different purpose than the
others).

This change removes the prefix from the store for consistency. The old name is
left as an export for existing SDK consumers.
2019-03-19 14:24:47 +00:00
David Baker f8985dbb39 Merge branch 'master' into develop 2019-03-18 13:50:07 +00:00
Travis Ralston 42f181cc7b Appease the linter 2019-03-15 14:11:29 -06:00
Travis Ralston b3d2d39b60 Add tombstone rule as a default rule in support of MSC1930
Part of https://github.com/vector-im/riot-web/issues/8447

See also https://github.com/matrix-org/matrix-doc/pull/1930
2019-03-15 14:07:15 -06:00
Travis Ralston e323d917a4 Support default push rules for when servers are outdated 2019-03-15 14:06:28 -06:00
Travis Ralston 73c7733ebc Merge pull request #859 from matrix-org/travis/buildkite
Use Buildkite for CI
2019-03-15 11:43:50 -06:00
Travis Ralston 87f7f9443e Delete Travis (CI)
We now use BuildKite
2019-03-13 17:27:17 -06:00
Travis Ralston af6bbbc59b Add a basic BuildKite :pipeline: 2019-03-13 17:23:30 -06:00
J. Ryan Stinnett 5b35a364a9 Ignore package-lock.json in case of confused npm users 2019-03-13 15:49:18 +00:00
Hubert Chathi d56ebadbc4 Merge pull request #857 from uhoreg/fix_ensureolm_race
only create one session at a time per device
2019-03-13 11:21:35 -04:00
J. Ryan Stinnett 04accdeddc Ignore *.log files, such as from npm or Yarn 2019-03-13 14:40:42 +00:00
Hubert Chathi 8c2001adbf don't reject on error
because we ignore it anyways, and it makes an unrelated unit test fail
2019-03-12 17:24:34 -04:00
Hubert Chathi 79ca235e7c only create one session at a time per device 2019-03-12 16:04:26 -04:00
94 changed files with 4699 additions and 1782 deletions
+2
View File
@@ -1,6 +1,8 @@
{
"presets": ["es2015"],
"plugins": [
"transform-class-properties",
// this transforms async functions into generator functions, which
// are then made to use the regenerator module by babel's
// transform-regnerator plugin (which is enabled by es2015).
+34
View File
@@ -0,0 +1,34 @@
steps:
- label: ":eslint: Lint"
command:
- "yarn install"
- "yarn lint"
plugins:
- docker#v3.0.1:
image: "node:10"
- label: ":karma: Tests"
command:
- "yarn install"
- "yarn test"
plugins:
- docker#v3.0.1:
image: "node:10"
- label: "📃 Docs"
command:
- "yarn install"
- "yarn gendoc"
plugins:
- docker#v3.0.1:
image: "node:10"
- wait
- label: "🐴 Trigger matrix-react-sdk"
trigger: "matrix-react-sdk"
branches: "develop"
build:
branch: "develop"
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
async: true
+9
View File
@@ -14,6 +14,9 @@ module.exports = {
es6: true,
},
extends: ["eslint:recommended", "google"],
plugins: [
"babel",
],
rules: {
// rules we've always adhered to or now do
"max-len": ["error", {
@@ -50,6 +53,7 @@ module.exports = {
// rules we do not want from the google styleguide
"object-curly-spacing": ["off"],
"spaced-comment": ["off"],
"guard-for-in": ["off"],
// in principle we prefer single quotes, but life is too short
quotes: ["off"],
@@ -73,5 +77,10 @@ module.exports = {
"asyncArrow": "always",
}],
"arrow-parens": "off",
// eslint's built in no-invalid-this rule breaks with class properties
"no-invalid-this": "off",
// so we replace it with a version that is class property aware
"babel/no-invalid-this": "error",
}
}
+2
View File
@@ -0,0 +1,2 @@
patreon: matrixdotorg
liberapay: matrixdotorg
+2
View File
@@ -3,6 +3,8 @@
node_modules
/.npmrc
/*.log
package-lock.json
.lock-wscript
build/Release
coverage
-8
View File
@@ -1,8 +0,0 @@
language: node_js
node_js:
- "10.11.0"
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0
- export PATH=$HOME/.yarn/bin:$PATH
script:
- ./travis.sh
+359 -1
View File
@@ -1,3 +1,361 @@
Changes in [2.3.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2) (2019-09-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.2-rc.1...v2.3.2)
* [Release] Fix addPendingEvent with pending event order == chronological
[\#1034](https://github.com/matrix-org/matrix-js-sdk/pull/1034)
Changes in [2.3.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2-rc.1) (2019-09-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1...v2.3.2-rc.1)
* Synapse admin functions to release
[\#1033](https://github.com/matrix-org/matrix-js-sdk/pull/1033)
* [To Release] Add matrix base API to report an event
[\#1032](https://github.com/matrix-org/matrix-js-sdk/pull/1032)
Changes in [2.3.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1) (2019-09-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1-rc.1...v2.3.1)
* No changes since rc.1
Changes in [2.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1-rc.1) (2019-09-11)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0...v2.3.1-rc.1)
* Update room members on member event redaction
[\#1031](https://github.com/matrix-org/matrix-js-sdk/pull/1031)
Changes in [2.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0) (2019-08-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0-rc.1...v2.3.0)
* [release] Support rewriting push rules when our internal defaults change
[\#1008](https://github.com/matrix-org/matrix-js-sdk/pull/1008)
Changes in [2.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0-rc.1) (2019-07-31)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0...v2.3.0-rc.1)
* Add support for IS v2 API with authentication
[\#1002](https://github.com/matrix-org/matrix-js-sdk/pull/1002)
* Tombstone bugfixes
[\#1001](https://github.com/matrix-org/matrix-js-sdk/pull/1001)
* Support for MSC2140 (terms of service for IS/IM)
[\#988](https://github.com/matrix-org/matrix-js-sdk/pull/988)
* Add a request method to /devices
[\#994](https://github.com/matrix-org/matrix-js-sdk/pull/994)
Changes in [2.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0) (2019-07-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.2...v2.2.0)
* Upgrade lodash dependencies
Changes in [2.2.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.2) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.1...v2.2.0-rc.2)
* Fix regression from 2.2.0-rc.1 in request to /devices
[\#995](https://github.com/matrix-org/matrix-js-sdk/pull/995)
Changes in [2.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.1) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.1...v2.2.0-rc.1)
* End the verification timer when verification is done
[\#993](https://github.com/matrix-org/matrix-js-sdk/pull/993)
* Stabilize usage of stably stable APIs (in a stable way)
[\#990](https://github.com/matrix-org/matrix-js-sdk/pull/990)
* Expose original_event for /relations
[\#987](https://github.com/matrix-org/matrix-js-sdk/pull/987)
* Process ephemeral events outside timeline handling
[\#989](https://github.com/matrix-org/matrix-js-sdk/pull/989)
* Don't accept any locally known edits earlier than the last known server-side
aggregated edit
[\#986](https://github.com/matrix-org/matrix-js-sdk/pull/986)
* Get edit date transparently from server aggregations or local echo
[\#984](https://github.com/matrix-org/matrix-js-sdk/pull/984)
* Add a function to flag keys for backup without scheduling a backup
[\#982](https://github.com/matrix-org/matrix-js-sdk/pull/982)
* Block read marker and read receipt from advancing into pending events
[\#981](https://github.com/matrix-org/matrix-js-sdk/pull/981)
* Upgrade dependencies
[\#977](https://github.com/matrix-org/matrix-js-sdk/pull/977)
* Add default push rule to ignore reactions
[\#976](https://github.com/matrix-org/matrix-js-sdk/pull/976)
* Fix exception whilst syncing
[\#979](https://github.com/matrix-org/matrix-js-sdk/pull/979)
* Include the error object when raising Session.logged_out
[\#975](https://github.com/matrix-org/matrix-js-sdk/pull/975)
Changes in [2.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.1) (2019-07-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.0...v2.1.1)
* Process emphemeral events outside timeline handling
[\#989](https://github.com/matrix-org/matrix-js-sdk/pull/989)
Changes in [2.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.0) (2019-07-08)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.0-rc.1...v2.1.0)
* Fix exception whilst syncing
[\#979](https://github.com/matrix-org/matrix-js-sdk/pull/979)
Changes in [2.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.0-rc.1) (2019-07-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1...v2.1.0-rc.1)
* Handle self read receipts for fixing e2e notification counts
[\#974](https://github.com/matrix-org/matrix-js-sdk/pull/974)
* Add redacts field to event.toJSON
[\#973](https://github.com/matrix-org/matrix-js-sdk/pull/973)
* Handle associated event send failures
[\#972](https://github.com/matrix-org/matrix-js-sdk/pull/972)
* Remove irrelevant debug line from timeline handling
[\#971](https://github.com/matrix-org/matrix-js-sdk/pull/971)
* Handle relations in encrypted rooms
[\#969](https://github.com/matrix-org/matrix-js-sdk/pull/969)
* Relations endpoint support
[\#967](https://github.com/matrix-org/matrix-js-sdk/pull/967)
* Disable event encryption for reactions
[\#968](https://github.com/matrix-org/matrix-js-sdk/pull/968)
* Change the known safe room version to version 4
[\#966](https://github.com/matrix-org/matrix-js-sdk/pull/966)
* Check for lazy-loading support in the spec versions instead
[\#965](https://github.com/matrix-org/matrix-js-sdk/pull/965)
* Use camelCase instead of underscore
[\#963](https://github.com/matrix-org/matrix-js-sdk/pull/963)
* Time out verification attempts after 10 minutes of inactivity
[\#961](https://github.com/matrix-org/matrix-js-sdk/pull/961)
* Don't handle key verification requests which are immediately cancelled
[\#962](https://github.com/matrix-org/matrix-js-sdk/pull/962)
Changes in [2.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1) (2019-06-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1-rc.2...v2.0.1)
No changes since rc.2
Changes in [2.0.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1-rc.2) (2019-06-18)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.1-rc.1...v2.0.1-rc.2)
* return 'sending' status for an event that is only locally redacted
[\#960](https://github.com/matrix-org/matrix-js-sdk/pull/960)
* Key verification request fixes
[\#954](https://github.com/matrix-org/matrix-js-sdk/pull/954)
* Add flag to force saving sync store
[\#956](https://github.com/matrix-org/matrix-js-sdk/pull/956)
* Expose the inhibit_login flag to register
[\#953](https://github.com/matrix-org/matrix-js-sdk/pull/953)
* Support redactions and relations of/with unsent events.
[\#947](https://github.com/matrix-org/matrix-js-sdk/pull/947)
Changes in [2.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.1-rc.1) (2019-06-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.0.0...v2.0.1-rc.1)
* Fix content uploads for modern browsers
[\#952](https://github.com/matrix-org/matrix-js-sdk/pull/952)
* Don't overlap auth submissions with polls
[\#951](https://github.com/matrix-org/matrix-js-sdk/pull/951)
* Add funding details for GitHub sponsor button
[\#945](https://github.com/matrix-org/matrix-js-sdk/pull/945)
* Fix backup sig validation with multiple sigs
[\#944](https://github.com/matrix-org/matrix-js-sdk/pull/944)
* Don't send another token request while one's in flight
[\#943](https://github.com/matrix-org/matrix-js-sdk/pull/943)
* Don't poll UI auth again until current poll finishes
[\#942](https://github.com/matrix-org/matrix-js-sdk/pull/942)
* Provide the discovered URLs when a liveliness error occurs
[\#938](https://github.com/matrix-org/matrix-js-sdk/pull/938)
* Encode event IDs when redacting events
[\#941](https://github.com/matrix-org/matrix-js-sdk/pull/941)
* add missing logger
[\#940](https://github.com/matrix-org/matrix-js-sdk/pull/940)
* verification: don't error if we don't know about some keys
[\#939](https://github.com/matrix-org/matrix-js-sdk/pull/939)
* Local echo for redactions
[\#937](https://github.com/matrix-org/matrix-js-sdk/pull/937)
* Refresh safe room versions when the server looks more modern than us
[\#934](https://github.com/matrix-org/matrix-js-sdk/pull/934)
* Add v4 as a safe room version
[\#935](https://github.com/matrix-org/matrix-js-sdk/pull/935)
* Disable guard-for-in rule
[\#933](https://github.com/matrix-org/matrix-js-sdk/pull/933)
* Extend loglevel logging for the whole project
[\#924](https://github.com/matrix-org/matrix-js-sdk/pull/924)
* fix(login): saves access_token and user_id after login for all login types
[\#930](https://github.com/matrix-org/matrix-js-sdk/pull/930)
* Do not try to request thumbnails with non-integer sizes
[\#929](https://github.com/matrix-org/matrix-js-sdk/pull/929)
* Revert "Add a bunch of debugging to .well-known IS validation"
[\#928](https://github.com/matrix-org/matrix-js-sdk/pull/928)
* Add a bunch of debugging to .well-known IS validation
[\#927](https://github.com/matrix-org/matrix-js-sdk/pull/927)
Changes in [2.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.0.0) (2019-05-31)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.2.0...v2.0.0)
BREAKING CHANGES
----------------
* This package now publishes in ES6 / ES2015 syntax to NPM
* Saves access_token and user_id after login for all login types
[\#932](https://github.com/matrix-org/matrix-js-sdk/pull/932)
* Fix recovery key encoding for base-x 3.0.5
[\#931](https://github.com/matrix-org/matrix-js-sdk/pull/931)
Changes in [1.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.2.0) (2019-05-29)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.2.0-rc.1...v1.2.0)
Changes in [1.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.2.0-rc.1) (2019-05-23)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.1.0...v1.2.0-rc.1)
* interactive-auth now handles requesting email tokens
[\#926](https://github.com/matrix-org/matrix-js-sdk/pull/926)
* allow access to unreplaced message content
[\#923](https://github.com/matrix-org/matrix-js-sdk/pull/923)
* Add method to retrieve replacing event
[\#922](https://github.com/matrix-org/matrix-js-sdk/pull/922)
* More logging when signature verification fails
[\#921](https://github.com/matrix-org/matrix-js-sdk/pull/921)
* Local echo for m.replace relations
[\#920](https://github.com/matrix-org/matrix-js-sdk/pull/920)
* Track relations as pending and remove when cancelled
[\#919](https://github.com/matrix-org/matrix-js-sdk/pull/919)
* Add stringify helper to summarise events when debugging
[\#916](https://github.com/matrix-org/matrix-js-sdk/pull/916)
* Message editing: filter out replacements for senders that are not the
original sender
[\#918](https://github.com/matrix-org/matrix-js-sdk/pull/918)
* Wait until decrypt before aggregating
[\#917](https://github.com/matrix-org/matrix-js-sdk/pull/917)
* Message editing: mark original event as replaced instead of replacing the
event object
[\#914](https://github.com/matrix-org/matrix-js-sdk/pull/914)
* Support for replacing message through m.replace relationship.
[\#913](https://github.com/matrix-org/matrix-js-sdk/pull/913)
* Use a short timeout for .well-known requests
[\#912](https://github.com/matrix-org/matrix-js-sdk/pull/912)
* Redaction and change events for relations
[\#911](https://github.com/matrix-org/matrix-js-sdk/pull/911)
* Add basic read path for relations
[\#910](https://github.com/matrix-org/matrix-js-sdk/pull/910)
* Add a concept of default push rules, using it for tombstone notifications
[\#860](https://github.com/matrix-org/matrix-js-sdk/pull/860)
* yarn upgrade
[\#907](https://github.com/matrix-org/matrix-js-sdk/pull/907)
Changes in [1.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0) (2019-05-07)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.1.0-rc.1...v1.1.0)
* No Changes since rc.1
Changes in [1.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.1.0-rc.1) (2019-04-30)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.4...v1.1.0-rc.1)
* use the release version of olm 3.1.0
[\#903](https://github.com/matrix-org/matrix-js-sdk/pull/903)
* Use new Olm repo link in README
[\#901](https://github.com/matrix-org/matrix-js-sdk/pull/901)
* Support being fed a .well-known config object for validation
[\#897](https://github.com/matrix-org/matrix-js-sdk/pull/897)
* emit self-membership event at end of handling sync update
[\#900](https://github.com/matrix-org/matrix-js-sdk/pull/900)
* Use packages.matrix.org for Olm
[\#898](https://github.com/matrix-org/matrix-js-sdk/pull/898)
* Fix tests on develop
[\#899](https://github.com/matrix-org/matrix-js-sdk/pull/899)
* Stop syncing when the token is invalid
[\#895](https://github.com/matrix-org/matrix-js-sdk/pull/895)
* change event redact, POST request to PUT request
[\#887](https://github.com/matrix-org/matrix-js-sdk/pull/887)
* Expose better autodiscovery error messages
[\#894](https://github.com/matrix-org/matrix-js-sdk/pull/894)
* Explicitly guard store usage during sync startup
[\#892](https://github.com/matrix-org/matrix-js-sdk/pull/892)
* Flag v3 rooms as safe
[\#893](https://github.com/matrix-org/matrix-js-sdk/pull/893)
* Cache failed capabilities lookups for shorter amounts of time
[\#890](https://github.com/matrix-org/matrix-js-sdk/pull/890)
* Fix highlight notifications for unencrypted rooms
[\#891](https://github.com/matrix-org/matrix-js-sdk/pull/891)
* Document checking crypto state before using `hasUnverifiedDevices`
[\#889](https://github.com/matrix-org/matrix-js-sdk/pull/889)
* Add logging to sync startup path
[\#888](https://github.com/matrix-org/matrix-js-sdk/pull/888)
* Track e2e highlights better, particularly in 'Mentions Only' rooms
[\#886](https://github.com/matrix-org/matrix-js-sdk/pull/886)
* support both the incorrect and correct MAC methods
[\#882](https://github.com/matrix-org/matrix-js-sdk/pull/882)
* Refuse to set forwards pagination token on live timeline
[\#885](https://github.com/matrix-org/matrix-js-sdk/pull/885)
* Degrade `IndexedDBStore` back to memory only on failure
[\#884](https://github.com/matrix-org/matrix-js-sdk/pull/884)
* Refuse to link live timelines into the forwards/backwards position when
either is invalid
[\#877](https://github.com/matrix-org/matrix-js-sdk/pull/877)
* Key backup logging improvements
[\#883](https://github.com/matrix-org/matrix-js-sdk/pull/883)
* Don't assume aborts are always from txn.abort()
[\#880](https://github.com/matrix-org/matrix-js-sdk/pull/880)
* Add a bunch of logging
[\#878](https://github.com/matrix-org/matrix-js-sdk/pull/878)
* Refuse splicing the live timeline into a broken position
[\#873](https://github.com/matrix-org/matrix-js-sdk/pull/873)
* Add existence check to local storage based crypto store
[\#872](https://github.com/matrix-org/matrix-js-sdk/pull/872)
Changes in [1.0.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.4) (2019-04-08)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3...v1.0.4)
* Hotfix: more logging and potential fixes for timeline corruption issue, see ticket https://github.com/vector-im/riot-web/issues/8593.
Changes in [1.0.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.3) (2019-04-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.3-rc.1...v1.0.3)
* Add existence check to local storage based crypto store
[\#874](https://github.com/matrix-org/matrix-js-sdk/pull/874)
Changes in [1.0.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.3-rc.1) (2019-03-27)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.2...v1.0.3-rc.1)
* Add IndexedDB existence checks
[\#871](https://github.com/matrix-org/matrix-js-sdk/pull/871)
* Emit sync errors for capturing by clients
[\#869](https://github.com/matrix-org/matrix-js-sdk/pull/869)
* Add functions for getting room upgrade history and leaving those rooms
[\#868](https://github.com/matrix-org/matrix-js-sdk/pull/868)
* Clarify the meaning of 'real name' for contribution
[\#867](https://github.com/matrix-org/matrix-js-sdk/pull/867)
* Remove `sessionStore` to `cryptoStore` migration path
[\#865](https://github.com/matrix-org/matrix-js-sdk/pull/865)
* Add debugging for spurious room version warnings
[\#866](https://github.com/matrix-org/matrix-js-sdk/pull/866)
* Add investigation notes for browser storage
[\#864](https://github.com/matrix-org/matrix-js-sdk/pull/864)
* make sure resolve object is defined before calling it
[\#862](https://github.com/matrix-org/matrix-js-sdk/pull/862)
* Rename `MatrixInMemoryStore` to `MemoryStore`
[\#861](https://github.com/matrix-org/matrix-js-sdk/pull/861)
* Use Buildkite for CI
[\#859](https://github.com/matrix-org/matrix-js-sdk/pull/859)
* only create one session at a time per device
[\#857](https://github.com/matrix-org/matrix-js-sdk/pull/857)
Changes in [1.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.2) (2019-03-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.2-rc.1...v1.0.2)
@@ -177,7 +535,7 @@ Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta
BREAKING CHANGE
----------------
* js-sdk now uses Olm 3.0. Apps using Olm must update to 3.0 to
continue using Olm with the js-sdk. The js-sdk will call Olm's
init() method when the client is started.
+13 -9
View File
@@ -24,7 +24,7 @@ works. Develop is the unstable branch where all the development actually
happens: the workflow is that contributors should fork the develop branch to
make a 'feature' branch for a particular contribution, and then make a pull
request to merge this back into the matrix.org 'official' develop branch. We
use github's pull request workflow to review the contribution, and either ask
use GitHub's pull request workflow to review the contribution, and either ask
you to make any refinements needed or merge it and make them ourselves. The
changes will then land on master when we next do a release.
@@ -60,8 +60,8 @@ Sign off
~~~~~~~~
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've adopted the
same lightweight approach that the Linux Kernel
and you agree to license it under the same terms as the project's license, we've
adopted the same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
@@ -109,12 +109,16 @@ include the line in your commit or pull request comment::
Signed-off-by: Your Name <your@email.example.org>
...using your real name; unfortunately pseudonyms and anonymous contributions
can't be accepted. Git makes this trivial - just use the -s flag when you do
``git commit``, having first set ``user.name`` and ``user.email`` git configs
(which you should have done anyway :)
We accept contributions under a legally identifiable name, such as your name on
government documentation or common-law names (names claimed by legitimate usage
or repute). Unfortunately, we cannot accept anonymous contributions at this
time.
If you forgot to sign off your commits before making your pull request and are on git 2.17+
you can mass signoff using rebase::
Git allows you to add this signoff automatically when using the ``-s`` flag to
``git commit``, which uses the name and email set in your ``user.name`` and
``user.email`` git configs.
If you forgot to sign off your commits before making your pull request and are
on Git 2.17+ you can mass signoff using rebase::
git rebase --signoff origin/develop
+8 -9
View File
@@ -1,8 +1,7 @@
Matrix Javascript SDK
=====================
[![Build Status](http://matrix.org/jenkins/buildStatus/icon?job=JavascriptSDK)](http://matrix.org/jenkins/job/JavascriptSDK/)
This is the [Matrix](https://matrix.org) Client-Server v1/v2 alpha SDK for
This is the [Matrix](https://matrix.org) Client-Server r0 SDK for
JavaScript. This SDK can be run in a browser or in Node.js.
Quickstart
@@ -80,7 +79,7 @@ client.on("Room.timeline", function(event, room, toStartOfTimeline) {
});
```
By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
By default, the `matrix-js-sdk` client uses the `MemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
```javascript
Object.keys(client.store.rooms).forEach((roomId) => {
@@ -298,8 +297,8 @@ End-to-end encryption support
=============================
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](http://matrix.org/git/olm). It is left up to the application to make
libolm available, via the ``Olm`` global.
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the ``Olm`` global.
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
@@ -318,18 +317,18 @@ specification.
To provide the Olm library in a browser application:
* download the transpiled libolm (from https://matrix.org/packages/npm/olm/).
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
To provide the Olm library in a node.js application:
* ``yarn add https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``
(replace the URL with the latest version you want to use from
https://matrix.org/packages/npm/olm/)
https://packages.matrix.org/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you can
use ``yarn add https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``. If your
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``. If your
application also works without e2e crypto enabled, add ``--optional`` to mark it
as an optional dependency.
+70
View File
@@ -0,0 +1,70 @@
# Browser Storage Notes
## Overview
Browsers examined: Firefox 67, Chrome 75
The examination below applies to the default, non-persistent storage policy.
## Quota Measurement
Browsers appear to enforce and measure the quota in terms of space on disk, not
data stored, so you may be able to store more data than the simple sum of all
input data depending on how compressible your data is.
## Quota Limit
Specs and documentation suggest we should consistently receive
`QuotaExceededError` when we're near space limits, but the reality is a bit
blurrier.
When we are low on disk space overall or near the group limit / origin quota:
* Chrome
* Log database may fail to start with AbortError
* IndexedDB fails to start for crypto: AbortError in connect from
indexeddb-store-worker
* When near the quota, QuotaExceededError is used more consistently
* Firefox
* The first error will be QuotaExceededError
* Future write attempts will fail with various errors when space is low,
including nonsense like "InvalidStateError: A mutation operation was
attempted on a database that did not allow mutations."
* Once you start getting errors, the DB is effectively wedged in read-only
mode
* Can revive access if you reopen the DB
## Cache Eviction
While the Storage Standard says all storage for an origin group should be
limited by a single quota, in practice, browsers appear to handle `localStorage`
separately from the others, so it has a separate quota limit and isn't evicted
when low on space.
* Chrome, Firefox
* IndexedDB for origin deleted
* Local Storage remains in place
## Persistent Storage
Storage Standard offers a `navigator.storage.persist` API that can be used to
request persistent storage that won't be deleted by the browser because of low
space.
* Chrome
* Chrome 75 seems to grant this without any prompt based on [interaction
criteria](https://developers.google.com/web/updates/2016/06/persistent-storage)
* Firefox
* Firefox 67 shows a prompt to grant
* Reverting persistent seems to require revoking permission _and_ clearing
site data
## Storage Estimation
Storage Standard offers a `navigator.storage.estimate` API to get some clue of
how much space remains.
* Chrome, Firefox
* Can run this at any time to request an estimate of space remaining
* Firefox
* Returns `0` for `usage` if a site is persisted
+8 -7
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "1.0.2",
"version": "2.3.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
@@ -9,12 +9,12 @@
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "yarn test:build && yarn test:run",
"check": "yarn test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"gendoc": "babel --no-babelrc --plugins transform-class-properties -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "yarn start:init && yarn start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
"clean": "rimraf lib dist",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
"dist": "yarn build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 101 src spec",
@@ -54,7 +54,6 @@
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"base-x": "3.0.4",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
@@ -68,25 +67,27 @@
"babel-cli": "^6.18.0",
"babel-eslint": "^10.0.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^5.3.0",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.2",
"matrix-mock-request": "^1.2.3",
"mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.4.0",
"olm": "https://matrix.org/packages/npm/olm/olm-3.1.0-pre1.tgz",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.0.tgz",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"uglify-js": "^2.8.26",
"terser": "^4.0.0",
"watchify": "^3.11.1"
},
"browserify": {
+7 -6
View File
@@ -27,6 +27,7 @@ import MockHttpBackend from 'matrix-mock-request';
import expect from 'expect';
import Promise from 'bluebird';
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
import logger from '../src/logger';
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@@ -82,7 +83,7 @@ TestClient.prototype.toString = function() {
* @return {Promise}
*/
TestClient.prototype.start = function() {
console.log(this + ': starting');
logger.log(this + ': starting');
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
@@ -100,7 +101,7 @@ TestClient.prototype.start = function() {
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(this + ': started');
logger.log(this + ': started');
});
};
@@ -122,7 +123,7 @@ TestClient.prototype.expectDeviceKeyUpload = function() {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
console.log(self + ': received device keys');
logger.log(self + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
@@ -159,7 +160,7 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({});
console.log('%s: received %i one-time keys', this,
logger.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
@@ -223,11 +224,11 @@ TestClient.prototype.getSigningKey = function() {
* @returns {Promise} promise which completes once the sync has been flushed
*/
TestClient.prototype.flushSync = function() {
console.log(`${this}: flushSync`);
logger.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(`${this}: flushSync completed`);
logger.log(`${this}: flushSync completed`);
});
};
+5 -4
View File
@@ -20,6 +20,7 @@ import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
import logger from '../../src/logger';
const ROOM_ID = "!room:id";
@@ -71,7 +72,7 @@ function getSyncResponse(roomMembers) {
describe("DeviceList management:", function() {
if (!global.Olm) {
console.warn('not running deviceList tests: Olm not present');
logger.warn('not running deviceList tests: Olm not present');
return;
}
@@ -87,7 +88,7 @@ describe("DeviceList management:", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
@@ -108,7 +109,7 @@ describe("DeviceList management:", function() {
return aliceTestClient.flushSync();
}).then(function() {
console.log("Forcing alice to download our device keys");
logger.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
@@ -121,7 +122,7 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
logger.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
+8 -7
View File
@@ -36,6 +36,7 @@ import Promise from 'bluebird';
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
const TestClient = require('../TestClient').default;
import logger from '../../src/logger';
let aliTestClient;
const roomId = "!room:localhost";
@@ -95,7 +96,7 @@ function expectBobQueryKeys() {
const aliKeys = {};
aliKeys[aliDeviceId] = aliTestClient.deviceKeys;
console.log("query result will be", aliKeys);
logger.log("query result will be", aliKeys);
bobTestClient.httpBackend.when(
"POST", "/keys/query",
@@ -334,7 +335,7 @@ function recvMessage(httpBackend, client, sender, message) {
if (event.getType() == "m.room.member") {
return;
}
console.log(client.credentials.userId + " received event",
logger.log(client.credentials.userId + " received event",
event);
client.removeListener("event", onEvent);
@@ -405,7 +406,7 @@ describe("MatrixClient crypto", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
@@ -607,7 +608,7 @@ describe("MatrixClient crypto", function() {
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
console.log(bobUserId + " received event",
logger.log(bobUserId + " received event",
event);
resolve(event);
};
@@ -734,7 +735,7 @@ describe("MatrixClient crypto", function() {
return Promise.resolve()
.then(() => {
console.log(aliTestClient + ': starting');
logger.log(aliTestClient + ': starting');
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
@@ -746,7 +747,7 @@ describe("MatrixClient crypto", function() {
aliTestClient.client.startClient({});
return httpBackend.flushAllExpected().then(() => {
console.log(aliTestClient + ': started');
logger.log(aliTestClient + ': started');
});
})
.then(() => httpBackend.when("POST", "/keys/upload")
@@ -755,7 +756,7 @@ describe("MatrixClient crypto", function() {
expect(content.one_time_keys).toNotEqual({});
expect(Object.keys(content.one_time_keys).length)
.toBeGreaterThanOrEqualTo(1);
console.log('received %i one-time keys',
logger.log('received %i one-time keys',
Object.keys(content.one_time_keys).length);
// cancel futher calls by telling the client
// we have more than we need
+25 -4
View File
@@ -15,7 +15,7 @@ describe("MatrixClient events", function() {
const selfAccessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
@@ -157,7 +157,7 @@ describe("MatrixClient events", function() {
return;
}
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(event.event).toMatch(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence,
);
@@ -302,11 +302,32 @@ describe("MatrixClient events", function() {
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(event, member) {
client.on("Session.logged_out", function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
@@ -5,6 +5,7 @@ const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
import logger from '../../src/logger';
const baseUrl = "http://localhost.or.something";
const userId = "@alice:localhost";
@@ -84,7 +85,7 @@ function startClient(httpBackend, client) {
// set up a promise which will resolve once the client is initialised
const deferred = Promise.defer();
client.on("sync", function(state) {
console.log("sync", state);
logger.log("sync", state);
if (state != "SYNCING") {
return;
}
@@ -102,7 +103,7 @@ describe("getEventTimeline support", function() {
let client;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
@@ -227,7 +228,7 @@ describe("MatrixClient event timelines", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
@@ -669,11 +670,11 @@ describe("MatrixClient event timelines", function() {
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
logger.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
logger.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}),
@@ -684,7 +685,7 @@ describe("MatrixClient event timelines", function() {
]).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
logger.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
+10 -10
View File
@@ -4,7 +4,7 @@ const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
const MemoryStore = publicGlobals.MemoryStore;
const Filter = publicGlobals.Filter;
const utils = require("../test-utils");
const MockStorageApi = require("../MockStorageApi");
@@ -21,9 +21,9 @@ describe("MatrixClient", function() {
const accessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
store = new MatrixInMemoryStore();
store = new MemoryStore();
const mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
@@ -48,7 +48,7 @@ describe("MatrixClient", function() {
const buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
@@ -87,7 +87,7 @@ describe("MatrixClient", function() {
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, { "content_uri": "uri" });
@@ -107,7 +107,7 @@ describe("MatrixClient", function() {
it("should parse errors into a MatrixError", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy();
@@ -355,9 +355,9 @@ describe("MatrixClient", function() {
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
};
console.log("Ed25519: " + ed25519key);
console.log("boris:", sign(borisKeys.dev1));
console.log("chaz:", sign(chazKeys.dev2));
logger.log("Ed25519: " + ed25519key);
logger.log("boris:", sign(borisKeys.dev1));
logger.log("chaz:", sign(chazKeys.dev2));
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
@@ -396,7 +396,7 @@ describe("MatrixClient", function() {
const auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device",
"DELETE", "/_matrix/client/r0/devices/my_device",
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
+2 -2
View File
@@ -58,7 +58,7 @@ describe("MatrixClient opts", function() {
};
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
});
@@ -128,7 +128,7 @@ describe("MatrixClient opts", function() {
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new sdk.MatrixInMemoryStore(),
store: new sdk.MemoryStore(),
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
+1 -1
View File
@@ -20,7 +20,7 @@ describe("MatrixClient retrying", function() {
let room;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
@@ -104,7 +104,7 @@ describe("MatrixClient room timelines", function() {
}
beforeEach(function(done) {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
+1 -1
View File
@@ -23,7 +23,7 @@ describe("MatrixClient syncing", function() {
const roomTwo = "!bar:localhost";
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
+19 -18
View File
@@ -23,6 +23,7 @@ import expect from 'expect';
const utils = require('../../lib/utils');
const testUtils = require('../test-utils');
const TestClient = require('../TestClient').default;
import logger from '../../src/logger';
const ROOM_ID = "!room:id";
@@ -203,7 +204,7 @@ function getSyncResponse(roomMembers) {
describe("megolm", function() {
if (!global.Olm) {
console.warn('not running megolm tests: Olm not present');
logger.warn('not running megolm tests: Olm not present');
return;
}
const Olm = global.Olm;
@@ -282,7 +283,7 @@ describe("megolm", function() {
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs",
@@ -416,7 +417,7 @@ describe("megolm", function() {
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
@@ -555,7 +556,7 @@ describe("megolm", function() {
).respond(200, function(path, content) {
const ct = content.ciphertext;
const r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
logger.log('Decrypted received megolm message', r);
expect(r.message_index).toEqual(0);
const decrypted = JSON.parse(r.plaintext);
@@ -600,7 +601,7 @@ describe("megolm", function() {
return aliceTestClient.flushSync();
}).then(function() {
console.log('Forcing alice to download our device keys');
logger.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
@@ -611,10 +612,10 @@ describe("megolm", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log('Telling alice to block our device');
logger.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
console.log('Telling alice to send a megolm message');
logger.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
@@ -656,7 +657,7 @@ describe("megolm", function() {
return aliceTestClient.flushSync();
}).then(function() {
console.log("Fetching bob's devices and marking known");
logger.log("Fetching bob's devices and marking known");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz'),
@@ -669,17 +670,17 @@ describe("megolm", function() {
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
});
}).then(function() {
console.log('Telling alice to send a megolm message');
logger.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
console.log('sendToDevice: ', content);
logger.log('sendToDevice: ', content);
const m = content.messages['@bob:xyz'].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(1); // normal message
const decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
console.log('decrypted sendToDevice:', decrypted);
logger.log('decrypted sendToDevice:', decrypted);
expect(decrypted.type).toEqual('m.room_key');
megolmSessionId = decrypted.content.session_id;
return {};
@@ -688,7 +689,7 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
console.log('/send:', content);
logger.log('/send:', content);
expect(content.session_id).toEqual(megolmSessionId);
return {
event_id: '$event_id',
@@ -704,14 +705,14 @@ describe("megolm", function() {
}),
]);
}).then(function() {
console.log('Telling alice to block our device');
logger.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
console.log('Telling alice to send another megolm message');
logger.log('Telling alice to send another megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, function(path, content) {
console.log('/send:', content);
logger.log('/send:', content);
expect(content.session_id).toNotEqual(megolmSessionId);
return {
event_id: '$event_id',
@@ -792,7 +793,7 @@ describe("megolm", function() {
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/',
).respond(200, function(path, content) {
console.log("sendToDevice: ", content);
logger.log("sendToDevice: ", content);
const m = content.messages[aliceTestClient.userId].DEVICE_ID;
const ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(0); // pre-key message
@@ -812,7 +813,7 @@ describe("megolm", function() {
).respond(200, function(path, content) {
const ct = content.ciphertext;
const r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
logger.log('Decrypted received megolm message', r);
decrypted = JSON.parse(r.plaintext);
return {
@@ -865,7 +866,7 @@ describe("megolm", function() {
return aliceTestClient.flushSync();
}).then(function() {
// this will block
console.log('Forcing alice to download our device keys');
logger.log('Forcing alice to download our device keys');
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
// so will this.
+4 -3
View File
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// try to load the olm library.
import logger from '../src/logger';
// try to load the olm library.
try {
global.Olm = require('olm');
console.log('loaded libolm');
logger.log('loaded libolm');
} catch (e) {
console.warn("unable to run crypto tests: libolm not available");
logger.warn("unable to run crypto tests: libolm not available");
}
+6 -5
View File
@@ -5,6 +5,7 @@ import Promise from 'bluebird';
// load olm before the sdk if possible
import './olm-loader';
import logger from '../src/logger';
import sdk from '..';
const MatrixEvent = sdk.MatrixEvent;
@@ -25,7 +26,7 @@ module.exports.syncPromise = function(client, count) {
const p = new Promise((resolve, reject) => {
const cb = (state) => {
console.log(`${Date.now()} syncPromise(${count}): ${state}`);
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state == 'SYNCING') {
resolve();
} else {
@@ -48,8 +49,8 @@ module.exports.syncPromise = function(client, count) {
module.exports.beforeEach = function(context) {
const desc = context.currentTest.fullTitle();
console.log(desc);
console.log(new Array(1 + desc.length).join("="));
logger.log(desc);
logger.log(new Array(1 + desc.length).join("="));
};
/**
@@ -232,11 +233,11 @@ module.exports.awaitDecryption = function(event) {
return Promise.resolve(event);
}
console.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
return new Promise((resolve, reject) => {
event.once('Event.decrypted', (ev) => {
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
resolve(ev);
});
});
+24 -24
View File
@@ -30,7 +30,7 @@ describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});
@@ -94,7 +94,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
@@ -117,7 +117,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
@@ -140,7 +140,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
@@ -163,7 +163,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
@@ -191,7 +191,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
base_url: null,
},
"m.identity_server": {
@@ -217,7 +217,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
base_url: null,
},
"m.identity_server": {
@@ -245,7 +245,7 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
base_url: null,
},
"m.identity_server": {
@@ -274,8 +274,8 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
@@ -303,8 +303,8 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
@@ -334,8 +334,8 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
@@ -439,14 +439,14 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
error: AutoDiscovery.ERROR_INVALID_IS,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
@@ -479,14 +479,14 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
error: AutoDiscovery.ERROR_INVALID_IS,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
@@ -520,15 +520,15 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
error: AutoDiscovery.ERROR_INVALID_IS,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
@@ -561,15 +561,15 @@ describe("AutoDiscovery", function() {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
error: AutoDiscovery.ERROR_INVALID_IS,
// We still expect the base_url to be here for debugging purposes
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
+8 -8
View File
@@ -9,7 +9,7 @@ describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("getHttpUriForMxc", function() {
@@ -31,7 +31,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid",
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
@@ -43,7 +43,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
});
@@ -52,7 +52,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
});
@@ -61,7 +61,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade",
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
});
@@ -73,21 +73,21 @@ describe("ContentRepo", function() {
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
"?width=32&height=64",
);
});
+5 -9
View File
@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,11 +16,10 @@ limitations under the License.
*/
import DeviceList from '../../../lib/crypto/DeviceList';
import MockStorageApi from '../../MockStorageApi';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import testUtils from '../../test-utils';
import utils from '../../../lib/utils';
import logger from '../../../src/logger';
import expect from 'expect';
import Promise from 'bluebird';
@@ -57,18 +56,15 @@ const signedDeviceList = {
describe('DeviceList', function() {
let downloadSpy;
let sessionStore;
let cryptoStore;
let deviceLists = [];
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
deviceLists = [];
downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore();
});
@@ -85,7 +81,7 @@ describe('DeviceList', function() {
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
const dl = new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
deviceLists.push(dl);
return dl;
}
@@ -139,7 +135,7 @@ describe('DeviceList', function() {
}).then(() => {
// uh-oh; user restarts before second request completes. The new instance
// should know we never got a complete device list.
console.log("Creating new devicelist to simulate app reload");
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.reset();
const dl2 = createTestDeviceList();
const queryDefer3 = Promise.defer();
+5 -7
View File
@@ -5,12 +5,12 @@ import Promise from 'bluebird';
import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import Crypto from '../../../../lib/crypto';
import logger from '../../../../src/logger';
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
@@ -22,7 +22,7 @@ const Olm = global.Olm;
describe("MegolmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
logger.warn('Not running megolm unit tests: libolm not present');
return;
}
@@ -32,7 +32,7 @@ describe("MegolmDecryption", function() {
let mockBaseApis;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
await Olm.init();
@@ -40,10 +40,9 @@ describe("MegolmDecryption", function() {
mockBaseApis = {};
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
const olmDevice = new OlmDevice(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@@ -264,10 +263,9 @@ describe("MegolmDecryption", function() {
it("re-uses sessions for sequential messages", async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
const olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = expect.createSpy();
await olmDevice.init();
+63 -6
View File
@@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018,2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,18 +17,19 @@ limitations under the License.
import '../../../olm-loader';
import expect from 'expect';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import logger from '../../../../src/logger';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import olmlib from '../../../../lib/crypto/olmlib';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
const olmDevice = new OlmDevice(cryptoStore);
return olmDevice;
}
@@ -45,7 +46,7 @@ async function setupSession(initiator, opponent) {
describe("OlmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
logger.warn('Not running megolm unit tests: libolm not present');
return;
}
@@ -53,7 +54,7 @@ describe("OlmDecryption", function() {
let bobOlmDevice;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
await global.Olm.init();
@@ -82,5 +83,61 @@ describe("OlmDecryption", function() {
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
});
it("creates only one session at a time", async function() {
// if we call ensureOlmSessionsForDevices multiple times, it should
// only try to create one session at a time, even if the server is
// slow
let count = 0;
const baseApis = {
claimOneTimeKeys: () => {
// simulate a very slow server (.5 seconds to respond)
count++;
return new Promise((resolve, reject) => {
setTimeout(reject, 500);
});
},
};
const devicesByUser = {
"@bob:example.com": [
DeviceInfo.fromStorage({
keys: {
"curve25519:ABCDEFG": "akey",
},
}, "ABCDEFG"),
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
// start two tasks that try to ensure that there's an olm session
const promises = Promise.all([
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUser,
)),
]);
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
// after .2s, both tasks should have started, but one should be
// waiting on the other before trying to create a session, so
// claimOneTimeKeys should have only been called once
expect(count).toBe(1);
await promises;
// after waiting for both tasks to complete, the first task should
// have failed, so the second task should have tried to create a
// new session and will have called claimOneTimeKeys
expect(count).toBe(2);
});
});
});
+4 -3
View File
@@ -28,6 +28,7 @@ import testUtils from '../../test-utils';
import OlmDevice from '../../../lib/crypto/OlmDevice';
import Crypto from '../../../lib/crypto';
import logger from '../../../src/logger';
const Olm = global.Olm;
@@ -112,7 +113,7 @@ function makeTestClient(sessionStore, cryptoStore) {
describe("MegolmBackup", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
logger.warn('Not running megolm backup unit tests: libolm not present');
return;
}
@@ -125,7 +126,7 @@ describe("MegolmBackup", function() {
let megolmDecryption;
beforeEach(async function() {
await Olm.init();
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new Olm.PkEncryption();
@@ -138,7 +139,7 @@ describe("MegolmBackup", function() {
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore(mockStorage);
olmDevice = new OlmDevice(sessionStore, cryptoStore);
olmDevice = new OlmDevice(cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
@@ -13,15 +13,15 @@ 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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run device verification tests: libolm not available");
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
@@ -30,7 +30,7 @@ const Olm = global.Olm;
describe("QR code verification", function() {
if (!global.Olm) {
console.warn('Not running device verification tests: libolm not present');
logger.warn('Not running device verification tests: libolm not present');
return;
}
@@ -13,11 +13,12 @@ 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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run device verification tests: libolm not available");
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
@@ -32,7 +33,7 @@ import {makeTestClients} from './util';
describe("verification request", function() {
if (!global.Olm) {
console.warn('Not running device verification unit tests: libolm not present');
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
@@ -68,8 +69,14 @@ describe("verification request", function() {
bob.on("crypto.verification.request", (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
});
const aliceVerifier = await alice.requestVerification("@bob:example.com");
expect(aliceVerifier).toBeAn(SAS);
// XXX: Private function access (but it's a test, so we're okay)
aliceVerifier._endTimer();
});
});
+160 -91
View File
@@ -13,11 +13,12 @@ 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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run device verification tests: libolm not available");
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
@@ -37,7 +38,7 @@ import {makeTestClients} from './util';
describe("SAS verification", function() {
if (!global.Olm) {
console.warn('Not running device verification unit tests: libolm not present');
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
@@ -56,107 +57,175 @@ describe("SAS verification", function() {
await sas.verify()
.catch(spy);
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
});
it("should verify a key", async function() {
const [alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
"Dynabook",
);
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.downloadKeys = () => {
return Promise.resolve();
};
describe("verification", function() {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
const bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
beforeEach(async function() {
[alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
"Dynabook",
);
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.downloadKeys = () => {
return Promise.resolve();
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
}
}
});
resolve(verifier);
});
resolve(verifier);
});
aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
);
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
}
}
});
});
const aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
);
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
it("should verify a key", async function() {
let macMethod;
const origSendToDevice = alice.sendToDevice;
bob.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.getUserId()][alice.deviceId]
.message_authentication_code;
}
}
return origSendToDevice.call(this, type, map);
};
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
]);
// make sure that it uses the preferred method
expect(macMethod).toBe("hkdf-hmac-sha256");
// make sure Alice and Bob verified each other
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
it("should be able to verify using the old MAC", async function() {
// pretend that Alice can only understand the old (incorrect) MAC,
// and make sure that she can still verify with Bob
let macMethod;
const origSendToDevice = alice.sendToDevice;
alice.sendToDevice = function(type, map) {
if (type === "m.key.verification.start") {
// Note: this modifies not only the message that Bob
// receives, but also the copy of the message that Alice
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.getUserId()][bob.deviceId]
.message_authentication_codes = ['hmac-sha256'];
}
return origSendToDevice.call(this, type, map);
};
bob.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.getUserId()][alice.deviceId]
.message_authentication_code;
}
return origSendToDevice.call(this, type, map);
};
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
]);
expect(macMethod).toBe("hmac-sha256");
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
]);
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
it("should send a cancellation message on error", async function() {
+1 -1
View File
@@ -29,7 +29,7 @@ export async function makeTestClients(userInfos, options) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line no-invalid-this
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: msg,
});
+1 -1
View File
@@ -18,7 +18,7 @@ describe("EventTimeline", function() {
let timeline;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
// XXX: this is a horrid hack; should use sinon or something instead to mock
const timelineSet = { room: { roomId: roomId }};
+3 -2
View File
@@ -21,10 +21,11 @@ import testUtils from '../test-utils';
import expect from 'expect';
import Promise from 'bluebird';
import logger from '../../src/logger';
describe("MatrixEvent", () => {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe(".attemptDecryption", () => {
@@ -48,7 +49,7 @@ describe("MatrixEvent", () => {
const crypto = {
decryptEvent: function() {
++callCount;
console.log(`decrypt: ${callCount}`);
logger.log(`decrypt: ${callCount}`);
if (callCount == 1) {
// schedule a second decryption attempt while
// the first one is still running.
+1 -1
View File
@@ -12,7 +12,7 @@ describe("Filter", function() {
let filter;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
filter = new Filter(userId);
});
+6 -5
View File
@@ -24,6 +24,7 @@ const InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
import expect from 'expect';
import logger from '../../src/logger';
// Trivial client object to test interactive auth
// (we do not need TestClient here)
@@ -35,7 +36,7 @@ class FakeClient {
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
it("should start an auth stage and complete it", function(done) {
@@ -64,7 +65,7 @@ describe("InteractiveAuth", function() {
// first we expect a call here
stateUpdated.andCall(function(stage) {
console.log('aaaa');
logger.log('aaaa');
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
@@ -75,7 +76,7 @@ describe("InteractiveAuth", function() {
// .. which should trigger a call here
const requestRes = {"a": "b"};
doRequest.andCall(function(authData) {
console.log('cccc');
logger.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
@@ -106,7 +107,7 @@ describe("InteractiveAuth", function() {
// first we expect a call to doRequest
doRequest.andCall(function(authData) {
console.log("request1", authData);
logger.log("request1", authData);
expect(authData).toEqual({});
const err = new MatrixError({
session: "sessionId",
@@ -132,7 +133,7 @@ describe("InteractiveAuth", function() {
// submitAuthDict should trigger another call to doRequest
doRequest.andCall(function(authData) {
console.log("request2", authData);
logger.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
+25
View File
@@ -0,0 +1,25 @@
import expect from 'expect';
import TestClient from '../TestClient';
describe('Login request', function() {
let client;
beforeEach(function() {
client = new TestClient();
});
afterEach(function() {
client.stop();
});
it('should store "access_token" and "user_id" if in response', async function() {
const response = { user_id: 1, access_token: Date.now().toString(16) };
client.httpBackend.when('POST', '/login').respond(200, response);
client.httpBackend.flush('/login', 1, 100);
await client.client.login('m.login.any', { user: 'test', password: '12312za' });
expect(client.client.getAccessToken()).toBe(response.access_token);
expect(client.client.getUserId()).toBe(response.user_id);
});
});
+5 -4
View File
@@ -7,6 +7,7 @@ const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
import logger from '../../src/logger';
describe("MatrixClient", function() {
const userId = "@alice:bar";
@@ -69,7 +70,7 @@ describe("MatrixClient", function() {
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
console.log(logLine);
logger.log(logLine);
if (!next) { // no more things to return
if (pendingLookup) {
@@ -91,7 +92,7 @@ describe("MatrixClient", function() {
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
console.log(
logger.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
@@ -124,7 +125,7 @@ describe("MatrixClient", function() {
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
clock = lolex.install();
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
@@ -353,7 +354,7 @@ describe("MatrixClient", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
const expected = expectedStates.shift();
console.log(
logger.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
);
if (!expected) {
+3 -3
View File
@@ -15,7 +15,7 @@ describe("realtime-callbacks", function() {
}
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
clock = lolex.install();
const fakeDate = clock.Date;
callbacks.setNow(fakeDate.now.bind(fakeDate));
@@ -56,8 +56,8 @@ describe("realtime-callbacks", function() {
it("should set 'this' to the global object", function() {
let passed = false;
const callback = function() {
expect(this).toBe(global); // eslint-disable-line no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line no-invalid-this
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
passed = true;
};
callbacks.setTimeout(callback);
+1 -1
View File
@@ -14,7 +14,7 @@ describe("RoomMember", function() {
let member;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
member = new RoomMember(roomId, userA);
});
+1 -1
View File
@@ -17,7 +17,7 @@ describe("RoomState", function() {
let state;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
state = new RoomState(roomId);
state.setStateEvents([
utils.mkMembership({ // userA joined
+5 -2
View File
@@ -19,7 +19,7 @@ describe("Room", function() {
let room;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
room = new Room(roomId);
// mock RoomStates
room.oldState = room.getLiveTimeline()._startState =
@@ -104,7 +104,7 @@ describe("Room", function() {
user_ids: [userA],
},
});
room.addLiveEvents([typing]);
room.addEphemeralEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
});
@@ -1318,6 +1318,9 @@ describe("Room", function() {
// events should already be MatrixEvents
return function(event) {return event;};
},
isCryptoEnabled() {
return true;
},
isRoomEncrypted: function() {
return false;
},
+40 -28
View File
@@ -26,7 +26,7 @@ describe("MatrixScheduler", function() {
});
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
clock = lolex.install();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {
@@ -48,7 +48,7 @@ describe("MatrixScheduler", function() {
clock.uninstall();
});
it("should process events in a queue in a FIFO manner", function(done) {
it("should process events in a queue in a FIFO manner", async function() {
retryFn = function() {
return 0;
};
@@ -57,28 +57,30 @@ describe("MatrixScheduler", function() {
};
const deferA = Promise.defer();
const deferB = Promise.defer();
let resolvedA = false;
let yieldedA = false;
scheduler.setProcessFunction(function(event) {
if (resolvedA) {
if (yieldedA) {
expect(event).toEqual(eventB);
return deferB.promise;
} else {
yieldedA = true;
expect(event).toEqual(eventA);
return deferA.promise;
}
});
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB).done(function() {
expect(resolvedA).toBe(true);
done();
});
deferA.resolve({});
resolvedA = true;
deferB.resolve({});
const abPromise = Promise.all([
scheduler.queueEvent(eventA),
scheduler.queueEvent(eventB),
]);
deferB.resolve({b: true});
deferA.resolve({a: true});
const [a, b] = await abPromise;
expect(a.a).toEqual(true);
expect(b.b).toEqual(true);
});
it("should invoke the retryFn on failure and wait the amount of time specified",
function(done) {
async function() {
const waitTimeMs = 1500;
const retryDefer = Promise.defer();
retryFn = function() {
@@ -97,24 +99,26 @@ describe("MatrixScheduler", function() {
return defer.promise;
} else if (procCount === 2) {
// don't care about this defer
return Promise.defer().promise;
return new Promise();
}
expect(procCount).toBeLessThan(3);
});
scheduler.queueEvent(eventA);
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
await Promise.resolve();
expect(procCount).toEqual(1);
defer.reject({});
retryDefer.promise.done(function() {
expect(procCount).toEqual(1);
clock.tick(waitTimeMs);
expect(procCount).toEqual(2);
done();
});
await retryDefer.promise;
expect(procCount).toEqual(1);
clock.tick(waitTimeMs);
await Promise.resolve();
expect(procCount).toEqual(2);
});
it("should give up if the retryFn on failure returns -1 and try the next event",
function(done) {
async function() {
// Queue A & B.
// Reject A and return -1 on retry.
// Expect B to be tried next and the promise for A to be rejected.
@@ -122,8 +126,8 @@ describe("MatrixScheduler", function() {
return -1;
};
queueFn = function() {
return "yep";
};
return "yep";
};
const deferA = Promise.defer();
const deferB = Promise.defer();
@@ -142,13 +146,17 @@ describe("MatrixScheduler", function() {
const globalA = scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
await Promise.resolve();
expect(procCount).toEqual(1);
deferA.reject({});
globalA.catch(function() {
try {
await globalA;
} catch(err) {
await Promise.resolve();
expect(procCount).toEqual(2);
done();
});
}
});
it("should treat each queue separately", function(done) {
@@ -300,7 +308,11 @@ describe("MatrixScheduler", function() {
expect(ev).toEqual(eventA);
return defer.promise;
});
expect(procCount).toEqual(1);
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
Promise.resolve().then(() => {
expect(procCount).toEqual(1);
});
});
it("should not call the processFn if there are no queued events", function() {
+1 -1
View File
@@ -26,7 +26,7 @@ describe("SyncAccumulator", function() {
let sa;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
sa = new SyncAccumulator({
maxTimelineEntries: 10,
});
+2 -2
View File
@@ -68,7 +68,7 @@ function createLinkedTimelines() {
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("minIndex", function() {
@@ -164,7 +164,7 @@ describe("TimelineWindow", function() {
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("load", function() {
+1 -1
View File
@@ -11,7 +11,7 @@ describe("User", function() {
let user;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
user = new User(userId);
});
+1 -1
View File
@@ -7,7 +7,7 @@ import expect from 'expect';
describe("utils", function() {
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
});
describe("encodeParams", function() {
+7 -1
View File
@@ -34,12 +34,18 @@ export default class Reemitter {
}
reEmit(source, eventNames) {
// We include the source as the last argument for event handlers which may need it,
// such as read receipt listeners on the client class which won't have the context
// of the room.
const forSource = (handler, ...args) => {
handler(...args, source);
};
for (const eventName of eventNames) {
if (this.boundHandlers[eventName] === undefined) {
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
}
const boundHandler = this.boundHandlers[eventName];
const boundHandler = forSource.bind(this, this.boundHandlers[eventName]);
source.on(eventName, boundHandler);
}
}
+235 -100
View File
@@ -17,7 +17,7 @@ limitations under the License.
/** @module auto-discovery */
import Promise from 'bluebird';
const logger = require("./logger");
import logger from './logger';
import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec.
@@ -102,6 +102,56 @@ export class AutoDiscovery {
// translate the meaning of the states in the spec, but also
// support our own if needed.
static get ERROR_INVALID() {
return "Invalid homeserver discovery response";
}
static get ERROR_GENERIC_FAILURE() {
return "Failed to get autodiscovery configuration from server";
}
static get ERROR_INVALID_HS_BASE_URL() {
return "Invalid base_url for m.homeserver";
}
static get ERROR_INVALID_HOMESERVER() {
return "Homeserver URL does not appear to be a valid Matrix homeserver";
}
static get ERROR_INVALID_IS_BASE_URL() {
return "Invalid base_url for m.identity_server";
}
static get ERROR_INVALID_IDENTITY_SERVER() {
return "Identity server URL does not appear to be a valid identity server";
}
static get ERROR_INVALID_IS() {
return "Invalid identity server discovery response";
}
static get ERROR_MISSING_WELLKNOWN() {
return "No .well-known JSON file found";
}
static get ERROR_INVALID_JSON() {
return "Invalid JSON";
}
static get ALL_ERRORS() {
return [
AutoDiscovery.ERROR_INVALID,
AutoDiscovery.ERROR_GENERIC_FAILURE,
AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
AutoDiscovery.ERROR_INVALID_HOMESERVER,
AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
AutoDiscovery.ERROR_INVALID_IS,
AutoDiscovery.ERROR_MISSING_WELLKNOWN,
AutoDiscovery.ERROR_INVALID_JSON,
];
}
/**
* The auto discovery failed. The client is expected to communicate
* the error to the user and refuse logging in.
@@ -137,6 +187,176 @@ export class AutoDiscovery {
*/
static get SUCCESS() { return "SUCCESS"; }
/**
* Validates and verifies client configuration information for purposes
* of logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be included, and will be transparently brought into the
* response object unaltered.
* @param {string} wellknown The configuration object itself, as returned
* by the .well-known auto-discovery endpoint.
* @return {Promise<DiscoveredClientConfig>} Resolves to the verified
* configuration, which may include error states. Rejects on unexpected
* failure, not when verification fails.
*/
static async fromDiscoveryConfig(wellknown) {
// Step 1 is to get the config, which is provided to us here.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
},
};
if (!wellknown || !wellknown["m.homeserver"]) {
logger.error("No m.homeserver key in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
return Promise.resolve(clientConfig);
}
if (!wellknown["m.homeserver"]["base_url"]) {
logger.error("No m.homeserver base_url in config");
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
}
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
wellknown["m.homeserver"]["base_url"],
);
if (!hsUrl) {
logger.error("Invalid base_url for m.homeserver");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
return Promise.resolve(clientConfig);
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
logger.error("Invalid /versions response");
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
// Supply the base_url to the caller because they may be ignoring liveliness
// errors, like this one.
clientConfig["m.homeserver"].base_url = hsUrl;
return Promise.resolve(clientConfig);
}
// Step 4: Now that the homeserver looks valid, update our client config.
clientConfig["m.homeserver"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: hsUrl,
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch. Note that we also fail the homeserver check in the
// object because according to the spec we're supposed to FAIL_ERROR
// if *anything* goes wrong with the IS validation, including invalid
// format. This means we're supposed to stop discovery completely.
const failingClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID_IS,
// We'll provide the base_url that was previously valid for
// debugging purposes.
base_url: clientConfig["m.homeserver"].base_url,
},
"m.identity_server": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID_IS,
base_url: null,
},
};
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
wellknown["m.identity_server"]["base_url"],
);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
return Promise.resolve(failingClientConfig);
}
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
logger.error("Invalid /api/v1 response");
failingClientConfig["m.identity_server"].error =
AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
// Supply the base_url to the caller because they may be ignoring
// liveliness errors, like this one.
failingClientConfig["m.identity_server"].base_url = isUrl;
return Promise.resolve(failingClientConfig);
}
}
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl,
};
}
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown)
.map((k) => {
if (k === "m.homeserver" || k === "m.identity_server") {
// Only copy selected parts of the config to avoid overwriting
// properties computed by the validation logic above.
const notProps = ["error", "state", "base_url"];
for (const prop of Object.keys(wellknown[k])) {
if (notProps.includes(prop)) continue;
clientConfig[k][prop] = wellknown[k][prop];
}
} else {
// Just copy the whole thing over otherwise
clientConfig[k] = wellknown[k];
}
});
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
* Attempts to automatically discover client configuration information
* prior to logging in. Such information includes the homeserver URL
@@ -171,7 +391,7 @@ export class AutoDiscovery {
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid homeserver discovery response",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
@@ -188,10 +408,8 @@ export class AutoDiscovery {
const wellknown = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!wellknown || wellknown.action !== "SUCCESS"
|| !wellknown.raw["m.homeserver"]
|| !wellknown.raw["m.homeserver"]["base_url"]) {
logger.error("No m.homeserver key in well-known response");
if (!wellknown || wellknown.action !== "SUCCESS") {
logger.error("No response or error when parsing .well-known");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") {
clientConfig["m.homeserver"] = {
@@ -202,99 +420,13 @@ export class AutoDiscovery {
} else {
// this can only ever be FAIL_PROMPT at this point.
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
}
return Promise.resolve(clientConfig);
}
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.homeserver"]["base_url"],
);
if (!hsUrl) {
logger.error("Invalid base_url for m.homeserver");
return Promise.resolve(clientConfig);
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
logger.error("Invalid /versions response");
return Promise.resolve(clientConfig);
}
// Step 4: Now that the homeserver looks valid, update our client config.
clientConfig["m.homeserver"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: hsUrl,
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
if (wellknown.raw["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch. Note that we also fail the homeserver check in the
// object because according to the spec we're supposed to FAIL_ERROR
// if *anything* goes wrong with the IS validation, including invalid
// format. This means we're supposed to stop discovery completely.
const failingClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
// We'll provide the base_url that was previously valid for
// debugging purposes.
base_url: clientConfig["m.homeserver"].base_url,
},
"m.identity_server": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
base_url: null,
},
};
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.identity_server"]["base_url"],
);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
return Promise.resolve(failingClientConfig);
}
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
logger.error("Invalid /api/v1 response");
return Promise.resolve(failingClientConfig);
}
}
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl,
};
}
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown.raw)
.filter((k) => k !== "m.homeserver" && k !== "m.identity_server")
.map((k) => clientConfig[k] = wellknown.raw[k]);
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
// Step 2: Validate and parse the config
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
}
/**
@@ -358,14 +490,14 @@ export class AutoDiscovery {
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri: url },
{ method: "GET", uri: url, timeout: 5000 },
(err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
let action = "FAIL_PROMPT";
let reason = (err ? err.message : null) || "General failure";
if (response.statusCode === 404) {
action = "IGNORE";
reason = "No .well-known JSON file found";
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
resolve({raw: {}, action: action, reason: reason, error: err});
return;
@@ -374,12 +506,15 @@ export class AutoDiscovery {
try {
resolve({raw: JSON.parse(body), action: "SUCCESS"});
} catch (e) {
let reason = "General failure";
if (e.name === "SyntaxError") reason = "Invalid JSON";
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
reason = AutoDiscovery.ERROR_INVALID_JSON;
}
resolve({
raw: {},
action: "FAIL_PROMPT",
reason: reason, error: e,
reason: reason,
error: e,
});
}
},
+231 -89
View File
@@ -1,6 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,8 +25,23 @@ limitations under the License.
* @module base-apis
*/
import { SERVICE_TYPES } from './service-types';
import logger from './logger';
const httpApi = require("./http-api");
const utils = require("./utils");
const PushProcessor = require("./pushprocessor");
function termsUrlForService(serviceType, baseUrl) {
switch (serviceType) {
case SERVICE_TYPES.IS:
return baseUrl + httpApi.PREFIX_IDENTITY_V2 + '/terms';
case SERVICE_TYPES.IM:
return baseUrl + '/_matrix/integrations/v1/terms';
default:
throw new Error('Unsupported service type');
}
}
/**
* Low-level wrappers for the Matrix APIs
@@ -151,13 +168,14 @@ MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
* threepid uses during registration in the ID server. Set 'msisdn' to
* true to bind msisdn.
* @param {string} guestAccessToken
* @param {string} inhibitLogin
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.register = function(
username, password,
sessionId, auth, bindThreepids, guestAccessToken,
sessionId, auth, bindThreepids, guestAccessToken, inhibitLogin,
callback,
) {
// backwards compat
@@ -166,6 +184,10 @@ MatrixBaseApis.prototype.register = function(
} else if (bindThreepids === null || bindThreepids === undefined) {
bindThreepids = {};
}
if (typeof inhibitLogin === 'function') {
callback = inhibitLogin;
inhibitLogin = undefined;
}
if (auth === undefined || auth === null) {
auth = {};
@@ -192,6 +214,9 @@ MatrixBaseApis.prototype.register = function(
if (guestAccessToken !== undefined && guestAccessToken !== null) {
params.guest_access_token = guestAccessToken;
}
if (inhibitLogin !== undefined && inhibitLogin !== null) {
params.inhibit_login = inhibitLogin;
}
// Temporary parameter added to make the register endpoint advertise
// msisdn flows. This exists because there are clients that break
// when given stages they don't recognise. This parameter will cease
@@ -263,8 +288,7 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
return this._http.authedRequest(
(error, response) => {
if (loginType === "m.login.password" && response &&
response.access_token && response.user_id) {
if (response && response.access_token && response.user_id) {
this._http.opts.accessToken = response.access_token;
this.credentials = {
userId: response.user_id,
@@ -384,9 +408,8 @@ MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
body.erase = erase;
}
return this._http.authedRequestWithPrefix(
return this._http.authedRequest(
undefined, "POST", '/account/deactivate', undefined, body,
httpApi.PREFIX_R0,
);
};
@@ -431,6 +454,35 @@ MatrixBaseApis.prototype.createRoom = function(options, callback) {
callback, "POST", "/createRoom", undefined, options,
);
};
/**
* Fetches relations for a given event
* @param {string} roomId the room of the event
* @param {string} eventId the id of the event
* @param {string} relationType the rel_type of the relations requested
* @param {string} eventType the event type of the relations requested
* @param {Object} opts options with optional values for the request.
* @param {Object} opts.from the pagination token returned from a previous request as `next_batch` to return following relations.
* @return {Object} the response, with chunk and next_batch.
*/
MatrixBaseApis.prototype.fetchRelations =
async function(roomId, eventId, relationType, eventType, opts) {
const queryParams = {};
if (opts.from) {
queryParams.from = opts.from;
}
const queryString = utils.encodeParams(queryParams);
const path = utils.encodeUri(
"/rooms/$roomId/relations/$eventId/$relationType/$eventType?" + queryString, {
$roomId: roomId,
$eventId: eventId,
$relationType: relationType,
$eventType: eventType,
});
const response = await this._http.authedRequestWithPrefix(
undefined, "GET", path, null, null, httpApi.PREFIX_UNSTABLE,
);
return response;
};
/**
* @param {string} roomId
@@ -888,21 +940,6 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s
);
};
/**
* @param {string} roomId
* @param {string} eventId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
const path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
$roomId: roomId,
$eventId: eventId,
});
return this._http.authedRequest(callback, "POST", path, undefined, {});
};
/**
* @param {string} roomId
* @param {Number} limit
@@ -1308,9 +1345,7 @@ MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
'medium': medium,
'address': address,
};
return this._http.authedRequestWithPrefix(
undefined, "POST", path, null, data, httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "POST", path, null, data);
};
/**
@@ -1343,10 +1378,8 @@ MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback)
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getDevices = function() {
const path = "/devices";
return this._http.authedRequestWithPrefix(
undefined, "GET", path, undefined, undefined,
httpApi.PREFIX_UNSTABLE,
return this._http.authedRequest(
undefined, 'GET', "/devices", undefined, undefined,
);
};
@@ -1363,11 +1396,7 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
$device_id: device_id,
});
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "PUT", path, undefined, body);
};
/**
@@ -1389,10 +1418,7 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
body.auth = auth;
}
return this._http.authedRequestWithPrefix(
undefined, "DELETE", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, body);
};
/**
@@ -1410,10 +1436,8 @@ MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
body.auth = auth;
}
return this._http.authedRequestWithPrefix(
undefined, "POST", "/delete_devices", undefined, body,
httpApi.PREFIX_UNSTABLE,
);
const path = "/delete_devices";
return this._http.authedRequest(undefined, "POST", path, undefined, body);
};
@@ -1455,7 +1479,9 @@ MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPushRules = function(callback) {
return this._http.authedRequest(callback, "GET", "/pushrules/");
return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => {
return PushProcessor.rewriteDefaultRules(rules);
});
};
/**
@@ -1589,9 +1615,7 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
} else {
path = "/keys/upload";
}
return this._http.authedRequestWithPrefix(
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(callback, "POST", path, undefined, content);
};
/**
@@ -1626,10 +1650,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
content.device_keys[u] = {};
});
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
};
/**
@@ -1657,10 +1678,8 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
query[deviceId] = key_algorithm;
}
const content = {one_time_keys: queries};
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/claim", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
const path = "/keys/claim";
return this._http.authedRequest(undefined, "POST", path, undefined, content);
};
/**
@@ -1679,20 +1698,39 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
to: newToken,
};
return this._http.authedRequestWithPrefix(
undefined, "GET", "/keys/changes", qps, undefined,
httpApi.PREFIX_UNSTABLE,
);
const path = "/keys/changes";
return this._http.authedRequest(undefined, "GET", path, qps, undefined);
};
// Identity Server Operations
// ==========================
/**
* Register with an Identity Server using the OpenID token from the user's
* Homeserver, which can be retrieved via
* {@link module:client~MatrixClient#getOpenIdToken}.
*
* Note that the `/account/register` endpoint (as well as IS authentication in
* general) was added as part of the v2 API version.
*
* @param {object} hsOpenIdToken
* @return {module:client.Promise} Resolves: with object containing an Identity
* Server access token.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
const uri = this.idBaseUrl + httpApi.PREFIX_IDENTITY_V2 + "/account/register";
return this._http.requestOtherUrl(
undefined, "POST", uri,
null, hsOpenIdToken,
);
};
/**
* Requests an email verification token directly from an Identity Server.
*
* Note that the Home Server offers APIs to proxy this API for specific
* Note that the Homeserver offers APIs to proxy this API for specific
* situations, allowing for better feedback to the user.
*
* @param {string} email The email address to request a token for
@@ -1705,22 +1743,50 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
* @param {string} nextLink Optional If specified, the client will be redirected
* to this link after validation.
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
* @throws Error if no Identity Server is set
*/
MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
sendAttempt, nextLink, callback) {
MatrixBaseApis.prototype.requestEmailToken = async function(
email,
clientSecret,
sendAttempt,
nextLink,
callback,
identityAccessToken,
) {
const params = {
client_secret: clientSecret,
email: email,
send_attempt: sendAttempt,
next_link: nextLink,
};
return this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
};
/**
@@ -1734,44 +1800,94 @@ MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} token The token, as enetered by the user.
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
*/
MatrixBaseApis.prototype.submitMsisdnToken = function(sid, clientSecret, token) {
MatrixBaseApis.prototype.submitMsisdnToken = async function(
sid,
clientSecret,
msisdnToken,
identityAccessToken,
) {
const params = {
sid: sid,
client_secret: clientSecret,
token: token,
token: msisdnToken,
};
return this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
try {
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
throw err;
}
};
/**
* Looks up the public Matrix ID mapping for a given 3rd party
* identifier from the Identity Server
*
* @param {string} medium The medium of the threepid, eg. 'email'
* @param {string} address The textual address of the threepid
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: A threepid mapping
* object or the empty object if no mapping
* exists
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
MatrixBaseApis.prototype.lookupThreePid = async function(
medium,
address,
callback,
identityAccessToken,
) {
const params = {
medium: medium,
address: address,
};
return this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1,
);
try {
const response = await this._http.idServerRequest(
undefined, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
};
@@ -1800,10 +1916,7 @@ MatrixBaseApis.prototype.sendToDevice = function(
messages: contentMap,
};
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "PUT", path, undefined, body);
};
// Third party Lookup API
@@ -1815,9 +1928,8 @@ MatrixBaseApis.prototype.sendToDevice = function(
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequestWithPrefix(
return this._http.authedRequest(
undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE,
).then((response) => {
// sanity check
if (!response || typeof(response) !== 'object') {
@@ -1842,10 +1954,7 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
$protocol: protocol,
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "GET", path, params, undefined);
};
/**
@@ -1861,12 +1970,45 @@ MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
$protocol: protocol,
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
return this._http.authedRequest(undefined, "GET", path, params, undefined);
};
MatrixBaseApis.prototype.getTerms = function(serviceType, baseUrl) {
const url = termsUrlForService(serviceType, baseUrl);
return this._http.requestOtherUrl(
undefined, 'GET', url,
);
};
MatrixBaseApis.prototype.agreeToTerms = function(
serviceType, baseUrl, accessToken, termsUrls,
) {
const url = termsUrlForService(serviceType, baseUrl);
const headers = {
Authorization: "Bearer " + accessToken,
};
return this._http.requestOtherUrl(
undefined, 'POST', url, null, { user_accepts: termsUrls }, { headers },
);
};
/**
* Reports an event as inappropriate to the server, which may then notify the appropriate people.
* @param {string} roomId The room in which the event being reported is located.
* @param {string} eventId The event to report.
* @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive.
* @param {string} reason The reason the content is being reported. May be blank.
* @returns {module:client.Promise} Resolves to an empty object if successful
*/
MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason) {
const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
$roomId: roomId,
$eventId: eventId,
});
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
};
/**
* MatrixBaseApis object
*/
+478 -64
View File
@@ -2,6 +2,7 @@
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd
Copyright 2019 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.
@@ -45,6 +46,7 @@ const olmlib = require("./crypto/olmlib");
import ReEmitter from './ReEmitter';
import RoomList from './crypto/RoomList';
import logger from '../src/logger';
import Crypto from './crypto';
import { isCryptoAvailable } from './crypto';
@@ -70,7 +72,7 @@ function keysFromRecoverySession(sessions, decryptionKey, roomId) {
decrypted.room_id = roomId;
keys.push(decrypted);
} catch (e) {
console.log("Failed to decrypt session from backup");
logger.log("Failed to decrypt session from backup");
}
}
return keys;
@@ -106,19 +108,26 @@ function keyFromRecoverySession(session, decryptionKey) {
*
* @param {string} opts.userId The user ID for this user.
*
* @param {Object=} opts.store The data store to use. If not specified,
* this client will not store any HTTP responses.
* @param {Object=} opts.store
* The data store used for sync data from the homeserver. If not specified,
* this client will not store any HTTP responses. The `createClient` helper
* will create a default store if needed.
*
* @param {module:store/session/webstorage~WebStorageSessionStore} opts.sessionStore
* A store to be used for end-to-end crypto session data. Most data has been
* migrated out of here to `cryptoStore` instead. If not specified,
* end-to-end crypto will be disabled. The `createClient` helper
* _will not_ create this store at the moment.
*
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
* A store to be used for end-to-end crypto session data. If not specified,
* end-to-end crypto will be disabled. The `createClient` helper will create
* a default store if needed.
*
* @param {string=} opts.deviceId A unique identifier for this device; used for
* tracking things like crypto keys and access tokens. If not specified,
* end-to-end crypto will be disabled.
*
* @param {Object=} opts.sessionStore A store to be used for end-to-end crypto
* session data. This should be a {@link
* module:store/session/webstorage~WebStorageSessionStore|WebStorageSessionStore},
* or an object implementing the same interface. If not specified,
* end-to-end crypto will be disabled.
*
* @param {Object} opts.scheduler Optional. The scheduler to use. If not
* specified, this client will not retry requests on failure. This client
* will supply its own processing function to
@@ -141,8 +150,10 @@ function keyFromRecoverySession(session, decryptionKey) {
* maintain support for back-paginating the live timeline after a '/sync'
* result with a gap.
*
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
* crypto store implementation.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @param {Array} [opts.verificationMethods] Optional. The verification method
* that the application can handle. Each element should be an item from {@link
@@ -209,6 +220,7 @@ function MatrixClient(opts) {
this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {};
this._notifTimelineSet = null;
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
this._crypto = null;
this._cryptoStore = opts.cryptoStore;
@@ -220,7 +232,7 @@ function MatrixClient(opts) {
// List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
this._roomList = new RoomList(this._cryptoStore, this._sessionStore);
this._roomList = new RoomList(this._cryptoStore);
// The pushprocessor caches useful things, so keep one and re-use it
this._pushProcessor = new PushProcessor(this);
@@ -238,22 +250,77 @@ function MatrixClient(opts) {
const actions = this._pushProcessor.actionsForEvent(event);
event.setPushActions(actions); // Might as well while we're here
const room = this.getRoom(event.getRoomId());
if (!room) return;
const currentCount = room.getUnreadNotificationCount("highlight");
// Ensure the unread counts are kept up to date if the event is encrypted
// We also want to make sure that the notification count goes up if we already
// have encrypted events to avoid other code from resetting 'highlight' to zero.
const oldHighlight = oldActions && oldActions.tweaks
? !!oldActions.tweaks.highlight : false;
const newHighlight = actions && actions.tweaks
? !!actions.tweaks.highlight : false;
if (oldHighlight !== newHighlight) {
const room = this.getRoom(event.getRoomId());
if (oldHighlight !== newHighlight || currentCount > 0) {
// TODO: Handle mentions received while the client is offline
// See also https://github.com/vector-im/riot-web/issues/9069
if (room && !room.hasUserReadEvent(this.getUserId(), event.getId())) {
const current = room.getUnreadNotificationCount("highlight");
const newCount = newHighlight ? current + 1 : current - 1;
if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
let newCount = currentCount;
if (newHighlight && !oldHighlight) newCount++;
if (!newHighlight && oldHighlight) newCount--;
room.setUnreadNotificationCount("highlight", newCount);
// Fix 'Mentions Only' rooms from not having the right badge count
const totalCount = room.getUnreadNotificationCount('total');
if (totalCount < newCount) {
room.setUnreadNotificationCount('total', newCount);
}
}
}
});
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/riot-web/issues/9421
this.on("Room.receipt", (event, room) => {
if (room && this.isRoomEncrypted(room.roomId)) {
// Figure out if we've read something or if it's just informational
const content = event.getContent();
const isSelf = Object.keys(content).filter(eid => {
return Object.keys(content[eid]['m.read']).includes(this.getUserId());
}).length > 0;
if (!isSelf) return;
// Work backwards to determine how many events are unread. We also set
// a limit for how back we'll look to avoid spinning CPU for too long.
// If we hit the limit, we assume the count is unchanged.
const maxHistory = 20;
const events = room.getLiveTimeline().getEvents();
let highlightCount = 0;
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - maxHistory) return; // limit reached
const event = events[i];
if (room.hasUserReadEvent(this.getUserId(), event.getId())) {
// If the user has read the event, then the counting is done.
break;
}
highlightCount += this.getPushActionsForEvent(
event,
).tweaks.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
// will come from the server.
room.setUnreadNotificationCount("highlight", highlightCount);
}
});
}
utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
@@ -424,13 +491,16 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
/**
* Gets the capabilities of the homeserver. Always returns an object of
* capability keys and their options, which may be empty.
* @param {boolean} fresh True to ignore any cached values.
* @return {module:client.Promise} Resolves to the capabilities of the homeserver
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getCapabilities = function() {
if (this._cachedCapabilities) {
const now = new Date().getTime();
if (now - this._cachedCapabilities.lastUpdated <= CAPABILITIES_CACHE_MS) {
MatrixClient.prototype.getCapabilities = function(fresh=false) {
const now = new Date().getTime();
if (this._cachedCapabilities && !fresh) {
if (now < this._cachedCapabilities.expiration) {
logger.log("Returning cached capabilities");
return Promise.resolve(this._cachedCapabilities.capabilities);
}
}
@@ -438,13 +508,25 @@ MatrixClient.prototype.getCapabilities = function() {
// We swallow errors because we need a default object anyhow
return this._http.authedRequest(
undefined, "GET", "/capabilities",
).catch(() => null).then((r) => {
).catch((e) => {
logger.error(e);
return null; // otherwise consume the error
}).then((r) => {
if (!r) r = {};
const capabilities = r["capabilities"] || {};
// If the capabilities missed the cache, cache it for a shorter amount
// of time to try and refresh them later.
const cacheMs = Object.keys(capabilities).length
? CAPABILITIES_CACHE_MS
: 60000 + (Math.random() * 5000);
this._cachedCapabilities = {
capabilities: capabilities,
lastUpdated: new Date().getTime(),
expiration: now + cacheMs,
};
logger.log("Caching capabilities: ", capabilities);
return capabilities;
});
};
@@ -470,7 +552,7 @@ MatrixClient.prototype.initCrypto = async function() {
}
if (this._crypto) {
console.warn("Attempt to re-initialise e2e encryption on MatrixClient");
logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
}
@@ -484,6 +566,7 @@ MatrixClient.prototype.initCrypto = async function() {
}
// initialise the list of encrypted rooms (whether or not crypto is enabled)
logger.log("Crypto: initialising roomlist...");
await this._roomList.init();
const userId = this.getUserId();
@@ -518,6 +601,7 @@ MatrixClient.prototype.initCrypto = async function() {
"crypto.warning",
]);
logger.log("Crypto: initialising crypto object...");
await crypto.init();
this.olmVersion = Crypto.getOlmVersion();
@@ -693,19 +777,19 @@ async function _setDeviceVerification(
* Request a key verification from another user.
*
* @param {string} userId the user to request verification with
* @param {Array} devices array of device IDs to send requests to. Defaults to
* all devices owned by the user
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
* @param {Array} devices array of device IDs to send requests to. Defaults to
* all devices owned by the user
*
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
* when the request is accepted by the other user
*/
MatrixClient.prototype.requestVerification = function(userId, devices, methods) {
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerification(userId, devices);
return this._crypto.requestVerification(userId, methods, devices);
};
/**
@@ -895,7 +979,8 @@ MatrixClient.prototype.checkKeyBackup = function() {
*/
MatrixClient.prototype.getKeyBackupVersion = function() {
return this._http.authedRequest(
undefined, "GET", "/room_keys/version",
undefined, "GET", "/room_keys/version", undefined, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
).then((res) => {
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
const err = "Unknown backup algorithm: " + res.algorithm;
@@ -1039,6 +1124,7 @@ MatrixClient.prototype.createKeyBackupVersion = function(info) {
return this._crypto._signObject(data.auth_data).then(() => {
return this._http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, data,
{prefix: httpApi.PREFIX_UNSTABLE},
);
}).then((res) => {
this.enableKeyBackup({
@@ -1068,6 +1154,7 @@ MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
return this._http.authedRequest(
undefined, "DELETE", path, undefined, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
);
};
@@ -1109,6 +1196,7 @@ MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "PUT", path.path, path.queryData, data,
{prefix: httpApi.PREFIX_UNSTABLE},
);
};
@@ -1124,6 +1212,19 @@ MatrixClient.prototype.scheduleAllGroupSessionsForBackup = async function() {
await this._crypto.scheduleAllGroupSessionsForBackup();
};
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
*/
MatrixClient.prototype.flagAllGroupSessionsForBackup = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.flagAllGroupSessionsForBackup();
};
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
try {
decodeRecoveryKey(recoveryKey);
@@ -1183,7 +1284,8 @@ MatrixClient.prototype._restoreKeyBackup = function(
}
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData,
undefined, "GET", path.path, path.queryData, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
).then((res) => {
if (res.rooms) {
for (const [roomId, roomData] of Object.entries(res.rooms)) {
@@ -1211,7 +1313,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
key.session_id = targetSessionId;
keys.push(key);
} catch (e) {
console.log("Failed to decrypt session from backup");
logger.log("Failed to decrypt session from backup");
}
}
@@ -1232,7 +1334,8 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "DELETE", path.path, path.queryData,
undefined, "DELETE", path.path, path.queryData, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
);
};
@@ -1655,6 +1758,21 @@ MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
*/
MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
callback) {
return this._sendCompleteEvent(roomId, {
type: eventType,
content: content,
}, txnId, callback);
};
/**
* @param {string} roomId
* @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
* @param {string} txnId the txnId.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
callback) {
if (utils.isFunction(txnId)) {
callback = txnId; txnId = undefined;
}
@@ -1663,22 +1781,35 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
txnId = this.makeTxnId();
}
console.log(`sendEvent of type ${eventType} in ${roomId} with txnId ${txnId}`);
// we always construct a MatrixEvent when sending because the store and
// scheduler use them. We'll extract the params back out if it turns out
// the client has no scheduler or store.
const room = this.getRoom(roomId);
const localEvent = new MatrixEvent({
const localEvent = new MatrixEvent(Object.assign(eventObject, {
event_id: "~" + roomId + ":" + txnId,
user_id: this.credentials.userId,
room_id: roomId,
type: eventType,
origin_server_ts: new Date().getTime(),
content: content,
});
}));
const room = this.getRoom(roomId);
// if this is a relation or redaction of an event
// that hasn't been sent yet (e.g. with a local id starting with a ~)
// then listen for the remote echo of that event so that by the time
// this event does get sent, we have the correct event_id
const targetId = localEvent.getAssociatedId();
if (targetId && targetId.startsWith("~")) {
const target = room.getPendingEvents().find(e => e.getId() === targetId);
target.once("Event.localEventIdReplaced", () => {
localEvent.updateAssociatedId(target.getId());
});
}
const type = localEvent.getType();
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
localEvent._txnId = txnId;
localEvent.status = EventStatus.SENDING;
localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'.
if (room) {
@@ -1745,7 +1876,7 @@ function _sendEvent(client, room, event, callback) {
return res;
}, function(err) {
// the request failed to send.
console.error("Error sending event", err.stack || err);
logger.error("Error sending event", err.stack || err);
try {
// set the error on the event before we update the status:
@@ -1761,7 +1892,7 @@ function _sendEvent(client, room, event, callback) {
callback(err);
}
} catch (err2) {
console.error("Exception in error handler!", err2.stack || err);
logger.error("Exception in error handler!", err2.stack || err);
}
throw err;
});
@@ -1794,6 +1925,20 @@ function _encryptEventIfNeeded(client, event, room) {
return null;
}
if (event.getType() === "m.reaction") {
// For reactions, there is a very little gained by encrypting the entire
// event, as relation data is already kept in the clear. Event
// encryption for a reaction effectively only obscures the event type,
// but the purpose is still obvious from the relation data, so nothing
// is really gained. It also causes quite a few problems, such as:
// * triggers notifications via default push rules
// * prevents server-side bundling for reactions
// The reaction key / content / emoji value does warrant encrypting, but
// this will be handled separately by encrypting just this value.
// See https://github.com/matrix-org/matrix-doc/pull/1849#pullrequestreview-248763642
return null;
}
if (!client._crypto) {
throw new Error(
"This room is configured to use encryption, but your client does " +
@@ -1803,12 +1948,27 @@ function _encryptEventIfNeeded(client, event, room) {
return client._crypto.encryptEvent(event, room);
}
/**
* Returns the eventType that should be used taking encryption into account
* for a given eventType.
* @param {MatrixClient} client the client
* @param {string} roomId the room for the events `eventType` relates to
* @param {string} eventType the event type
* @return {string} the event type taking encryption into account
*/
function _getEncryptedIfNeededEventType(client, roomId, eventType) {
if (eventType === "m.reaction") {
return eventType;
}
const isEncrypted = client.isRoomEncrypted(roomId);
return isEncrypted ? "m.room.encrypted" : eventType;
}
function _updatePendingEventStatus(room, event, newStatus) {
if (room) {
room.updatePendingEvent(event, newStatus);
} else {
event.status = newStatus;
event.setStatus(newStatus);
}
}
@@ -1830,6 +1990,11 @@ function _sendEventHttpRequest(client, event) {
pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
}
path = utils.encodeUri(pathTemplate, pathParams);
} else if (event.isRedaction()) {
const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`;
path = utils.encodeUri(pathTemplate, Object.assign({
$redactsEventId: event.event.redacts,
}, pathParams));
} else {
path = utils.encodeUri(
"/rooms/$roomId/send/$eventType/$txnId", pathParams,
@@ -1839,13 +2004,30 @@ function _sendEventHttpRequest(client, event) {
return client._http.authedRequest(
undefined, "PUT", path, undefined, event.getWireContent(),
).then((res) => {
console.log(
logger.log(
`Event sent to ${event.getRoomId()} with event id ${res.event_id}`,
);
return res;
});
}
/**
* @param {string} roomId
* @param {string} eventId
* @param {string} [txnId] transaction id. One will be made up if not
* supplied.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback) {
return this._sendCompleteEvent(roomId, {
type: "m.room.redaction",
content: {},
redacts: eventId,
}, txnId, callback);
};
/**
* @param {string} roomId
* @param {Object} content
@@ -2028,7 +2210,12 @@ MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) {
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendReadReceipt = function(event, callback) {
MatrixClient.prototype.sendReadReceipt = async function(event, callback) {
const eventId = event.getId();
const room = this.getRoom(event.getRoomId());
if (room && room.hasPendingEvent(eventId)) {
throw new Error(`Cannot set read receipt to a pending event (${eventId})`);
}
return this.sendReceipt(event, "m.read", callback);
};
@@ -2038,20 +2225,25 @@ MatrixClient.prototype.sendReadReceipt = function(event, callback) {
* and displayed as a horizontal line in the timeline that is visually distinct to the
* position of the user's own read receipt.
* @param {string} roomId ID of the room that has been read
* @param {string} eventId ID of the event that has been read
* @param {string} rmEventId ID of the event that has been read
* @param {string} rrEvent the event tracked by the read receipt. This is here for
* convenience because the RR and the RM are commonly updated at the same time as each
* other. The local echo of this receipt will be done if set. Optional.
* @return {module:client.Promise} Resolves: the empty object, {}.
*/
MatrixClient.prototype.setRoomReadMarkers = function(roomId, eventId, rrEvent) {
const rmEventId = eventId;
let rrEventId;
MatrixClient.prototype.setRoomReadMarkers = async function(roomId, rmEventId, rrEvent) {
const room = this.getRoom(roomId);
if (room && room.hasPendingEvent(rmEventId)) {
throw new Error(`Cannot set read marker to a pending event (${rmEventId})`);
}
// Add the optional RR update, do local echo like `sendReceipt`
let rrEventId;
if (rrEvent) {
rrEventId = rrEvent.getId();
const room = this.getRoom(roomId);
if (room && room.hasPendingEvent(rrEventId)) {
throw new Error(`Cannot set read receipt to a pending event (${rrEventId})`);
}
if (room) {
room._addLocalEchoReceipt(this.credentials.userId, rrEvent, "m.read");
}
@@ -2123,6 +2315,87 @@ MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callba
);
};
/**
* Determines the history of room upgrades for a given room, as far as the
* client can see. Returns an array of Rooms where the first entry is the
* oldest and the last entry is the newest (likely current) room. If the
* provided room is not found, this returns an empty list. This works in
* both directions, looking for older and newer rooms of the given room.
* @param {string} roomId The room ID to search from
* @param {boolean} verifyLinks If true, the function will only return rooms
* which can be proven to be linked. For example, rooms which have a create
* event pointing to an old room which the client is not aware of or doesn't
* have a matching tombstone would not be returned.
* @return {Room[]} An array of rooms representing the upgrade
* history.
*/
MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=false) {
let currentRoom = this.getRoom(roomId);
if (!currentRoom) return [];
const upgradeHistory = [currentRoom];
// Work backwards first, looking at create events.
let createEvent = currentRoom.currentState.getStateEvents("m.room.create", "");
while (createEvent) {
logger.log(`Looking at ${createEvent.getId()}`);
const predecessor = createEvent.getContent()['predecessor'];
if (predecessor && predecessor['room_id']) {
logger.log(`Looking at predecessor ${predecessor['room_id']}`);
const refRoom = this.getRoom(predecessor['room_id']);
if (!refRoom) break; // end of the chain
if (verifyLinks) {
const tombstone = refRoom.currentState
.getStateEvents("m.room.tombstone", "");
if (!tombstone
|| tombstone.getContent()['replacement_room'] !== refRoom.roomId) {
break;
}
}
// Insert at the front because we're working backwards from the currentRoom
upgradeHistory.splice(0, 0, refRoom);
createEvent = refRoom.currentState.getStateEvents("m.room.create", "");
} else {
// No further create events to look at
break;
}
}
// Work forwards next, looking at tombstone events
let tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", "");
while (tombstoneEvent) {
const refRoom = this.getRoom(tombstoneEvent.getContent()['replacement_room']);
if (!refRoom) break; // end of the chain
if (refRoom.roomId === currentRoom.roomId) break; // Tombstone is referencing it's own room
if (verifyLinks) {
createEvent = refRoom.currentState.getStateEvents("m.room.create", "");
if (!createEvent || !createEvent.getContent()['predecessor']) break;
const predecessor = createEvent.getContent()['predecessor'];
if (predecessor['room_id'] !== currentRoom.roomId) break;
}
// Push to the end because we're looking forwards
upgradeHistory.push(refRoom);
const roomIds = new Set(upgradeHistory.map((ref) => ref.roomId));
if (roomIds.size < upgradeHistory.length) {
// The last room added to the list introduced a previous roomId
// To avoid recursion, return the last rooms - 1
return upgradeHistory.slice(0, upgradeHistory.length - 1);
}
// Set the current room to the reference room so we know where we're at
currentRoom = refRoom;
tombstoneEvent = currentRoom.currentState.getStateEvents("m.room.tombstone", "");
}
return upgradeHistory;
};
/**
* @param {string} roomId
* @param {string} userId
@@ -2190,6 +2463,50 @@ MatrixClient.prototype.leave = function(roomId, callback) {
callback);
};
/**
* Leaves all rooms in the chain of room upgrades based on the given room. By
* default, this will leave all the previous and upgraded rooms, including the
* given room. To only leave the given room and any previous rooms, keeping the
* upgraded (modern) rooms untouched supply `false` to `includeFuture`.
* @param {string} roomId The room ID to start leaving at
* @param {boolean} includeFuture If true, the whole chain (past and future) of
* upgraded rooms will be left.
* @return {module:client.Promise} Resolves when completed with an object keyed
* by room ID and value of the error encountered when leaving or null.
*/
MatrixClient.prototype.leaveRoomChain = function(roomId, includeFuture=true) {
const upgradeHistory = this.getRoomUpgradeHistory(roomId);
let eligibleToLeave = upgradeHistory;
if (!includeFuture) {
eligibleToLeave = [];
for (const room of upgradeHistory) {
eligibleToLeave.push(room);
if (room.roomId === roomId) {
break;
}
}
}
const populationResults = {}; // {roomId: Error}
const promises = [];
const doLeave = (roomId) => {
return this.leave(roomId).then(() => {
populationResults[roomId] = null;
}).catch((err) => {
populationResults[roomId] = err;
return null; // suppress error
});
};
for (const room of eligibleToLeave) {
promises.push(doLeave(room.roomId));
}
return Promise.all(promises).then(() => populationResults);
};
/**
* @param {string} roomId
* @param {string} userId
@@ -2761,9 +3078,8 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
params.from = token;
}
promise =
this._http.authedRequestWithPrefix(undefined, "GET", path, params,
undefined, httpApi.PREFIX_UNSTABLE,
promise = this._http.authedRequest(
undefined, "GET", path, params, undefined,
).then(function(res) {
const token = res.next_token;
const matrixEvents = [];
@@ -3396,7 +3712,7 @@ MatrixClient.prototype.syncLeftRooms = function() {
// cleanup locks
this._syncLeftRoomsPromise.then(function(res) {
console.log("Marking success of sync left room request");
logger.log("Marking success of sync left room request");
self._syncedLeftRooms = true; // flip the bit on success
}).finally(function() {
self._syncLeftRoomsPromise = null; // cleanup ongoing request state
@@ -3570,6 +3886,55 @@ MatrixClient.prototype.getTurnServers = function() {
return this._turnServers || [];
};
// Synapse-specific APIs
// =====================
/**
* Determines if the current user is an administrator of the Synapse homeserver.
* Returns false if untrue or the homeserver does not appear to be a Synapse
* homeserver. <strong>This function is implementation specific and may change
* as a result.</strong>
* @return {boolean} true if the user appears to be a Synapse administrator.
*/
MatrixClient.prototype.isSynapseAdministrator = function() {
return this.whoisSynapseUser(this.getUserId())
.then(() => true)
.catch(() => false);
};
/**
* Performs a whois lookup on a user using Synapse's administrator API.
* <strong>This function is implementation specific and may change as a
* result.</strong>
* @param {string} userId the User ID to look up.
* @return {object} the whois response - see Synapse docs for information.
*/
MatrixClient.prototype.whoisSynapseUser = function(userId) {
const path = utils.encodeUri(
"/_synapse/admin/v1/whois/$userId",
{ $userId: userId },
);
return this._http.authedRequest(
undefined, 'GET', path, undefined, undefined, {prefix: ''},
);
};
/**
* Deactivates a user using Synapse's administrator API. <strong>This
* function is implementation specific and may change as a result.</strong>
* @param {string} userId the User ID to deactivate.
* @return {object} the deactivate response - see Synapse docs for information.
*/
MatrixClient.prototype.deactivateSynapseUser = function(userId) {
const path = utils.encodeUri(
"/_synapse/admin/v1/deactivate/$userId",
{ $userId: userId },
);
return this._http.authedRequest(
undefined, 'POST', path, undefined, undefined, {prefix: ''},
);
};
// Higher level APIs
// =================
@@ -3641,7 +4006,7 @@ MatrixClient.prototype.startClient = async function(opts) {
if (this._syncApi) {
// This shouldn't happen since we thought the client was not running
console.error("Still have sync object whilst not running: stopping old one");
logger.error("Still have sync object whilst not running: stopping old one");
this._syncApi.stop();
}
@@ -3684,7 +4049,7 @@ MatrixClient.prototype._storeClientOptions = function() {
* clean shutdown.
*/
MatrixClient.prototype.stopClient = function() {
console.log('stopping MatrixClient');
logger.log('stopping MatrixClient');
this.clientRunning = false;
// TODO: f.e. Room => self.store.storeRoom(room) ?
@@ -3716,9 +4081,13 @@ MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
prefix: '',
},
);
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
this._serverSupportsLazyLoading =
unstableFeatures && unstableFeatures["m.lazy_load_members"];
(versions && versions.includes("r0.5.0"))
|| (unstableFeatures && unstableFeatures["m.lazy_load_members"]);
}
return this._serverSupportsLazyLoading;
};
@@ -3752,6 +4121,47 @@ MatrixClient.prototype.getCanResetTimelineCallback = function() {
return this._canResetTimelineCallback;
};
/**
* Returns relations for a given event. Handles encryption transparently,
* with the caveat that the amount of events returned might be 0, even though you get a nextBatch.
* When the returned promise resolves, all messages should have finished trying to decrypt.
* @param {string} roomId the room of the event
* @param {string} eventId the id of the event
* @param {string} relationType the rel_type of the relations requested
* @param {string} eventType the event type of the relations requested
* @param {Object} opts options with optional values for the request.
* @param {Object} opts.from the pagination token returned from a previous request as `nextBatch` to return following relations.
* @return {Object} an object with `events` as `MatrixEvent[]` and optionally `nextBatch` if more relations are available.
*/
MatrixClient.prototype.relations =
async function(roomId, eventId, relationType, eventType, opts = {}) {
const fetchedEventType = _getEncryptedIfNeededEventType(this, roomId, eventType);
const result = await this.fetchRelations(
roomId,
eventId,
relationType,
fetchedEventType,
opts);
const mapper = this.getEventMapper();
let originalEvent;
if (result.original_event) {
originalEvent = mapper(result.original_event);
}
let events = result.chunk.map(mapper);
if (fetchedEventType === "m.room.encrypted") {
const allEvents = originalEvent ? events.concat(originalEvent) : events;
await Promise.all(allEvents.map(e => {
return new Promise(resolve => e.once("Event.decrypted", resolve));
}));
events = events.filter(e => e.getType() === eventType);
}
return {
originalEvent,
events,
nextBatch: result.next_batch,
};
};
function setupCallEventHandler(client) {
const candidatesByCall = {
// callId: [Candidate]
@@ -3825,7 +4235,7 @@ function setupCallEventHandler(client) {
return; // stale/old invite event
}
if (call) {
console.log(
logger.log(
"WARN: Already have a MatrixCall with id %s but got an " +
"invite. Clobbering.",
content.call_id,
@@ -3836,7 +4246,7 @@ function setupCallEventHandler(client) {
forceTURN: client._forceTURN,
});
if (!call) {
console.log(
logger.log(
"Incoming call ID " + content.call_id + " but this client " +
"doesn't support WebRTC",
);
@@ -3881,14 +4291,14 @@ function setupCallEventHandler(client) {
if (existingCall.state === 'wait_local_media' ||
existingCall.state === 'create_offer' ||
existingCall.callId > call.callId) {
console.log(
logger.log(
"Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId,
);
existingCall._replacedBy(call);
call.answer();
} else {
console.log(
logger.log(
"Glare detected: rejecting incoming call " + call.callId +
" and keeping outgoing call " + existingCall.callId,
);
@@ -3958,7 +4368,7 @@ function checkTurnServers(client) {
client.turnServer().done(function(res) {
if (res.uris) {
console.log("Got TURN URIs: " + res.uris + " refresh in " +
logger.log("Got TURN URIs: " + res.uris + " refresh in " +
res.ttl + " secs");
// map the response to a format that can be fed to
// RTCPeerConnection
@@ -3974,7 +4384,7 @@ function checkTurnServers(client) {
}, (res.ttl || (60 * 60)) * 1000 * 0.9);
}
}, function(err) {
console.error("Failed to get TURN URIs");
logger.error("Failed to get TURN URIs");
client._checkTurnServersTimeoutID =
setTimeout(function() {
checkTurnServers(client);
@@ -4005,6 +4415,10 @@ function _PojoToMatrixEventMapper(client) {
]);
event.attemptDecryption(client._crypto);
}
const room = client.getRoom(event.getRoomId());
if (room) {
room.reEmitter.reEmit(event, ["Event.replaced"]);
}
return event;
}
return mapper;
@@ -4234,7 +4648,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* when then login session can be renewed by using a refresh token.
* @event module:client~MatrixClient#"Session.logged_out"
* @example
* matrixClient.on("Session.logged_out", function(call){
* matrixClient.on("Session.logged_out", function(errorObj){
* // show the login screen
* });
*/
+6 -5
View File
@@ -47,14 +47,14 @@ module.exports = {
}
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/v1/download/";
let prefix = "/_matrix/media/r0/download/";
const params = {};
if (width) {
params.width = width;
params.width = Math.round(width);
}
if (height) {
params.height = height;
params.height = Math.round(height);
}
if (resizeMethod) {
params.method = resizeMethod;
@@ -62,7 +62,7 @@ module.exports = {
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
prefix = "/_matrix/media/r0/thumbnail/";
}
const fragmentOffset = serverAndMediaId.indexOf("#");
@@ -83,6 +83,7 @@ module.exports = {
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
@@ -99,7 +100,7 @@ module.exports = {
height: height,
};
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
+7 -30
View File
@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -61,9 +61,8 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
constructor(baseApis, cryptoStore, olmDevice) {
this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore;
// userId -> {
// deviceId -> {
@@ -108,30 +107,13 @@ export default class DeviceList {
* Load the device tracking state from storage
*/
async load() {
let shouldDeleteSessionStore = false;
await this._cryptoStore.doTxn(
// migrate from session store if there's data there and not here
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
if (deviceData === null) {
logger.log("Migrating e2e device data...");
this._devices = this._sessionStore.getAllEndToEndDevices() || {};
this._deviceTrackingStatus = (
this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
);
this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
shouldDeleteSessionStore = true;
} else {
this._devices = deviceData ? deviceData.devices : {},
this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
}
this._devices = deviceData ? deviceData.devices : {},
this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
this._userByIdentityKey = {};
for (const user of Object.keys(this._devices)) {
const userDevices = this._devices[user];
@@ -146,11 +128,6 @@ export default class DeviceList {
},
);
if (shouldDeleteSessionStore) {
// migrated data is now safely persisted: remove from old store
this._sessionStore.removeEndToEndDeviceData();
}
for (const u of Object.keys(this._deviceTrackingStatus)) {
// if a download was in progress when we got shut down, it isn't any more.
if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
+42 -105
View File
@@ -1,6 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -59,20 +59,17 @@ function checkPayloadLength(payloadString) {
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
*
* Accounts and sessions are kept pickled in a sessionStore.
* Accounts and sessions are kept pickled in the cryptoStore.
*
* @constructor
* @alias module:crypto/OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto. This is deprecated and being replaced by cryptoStore.
* @param {Object} cryptoStore A store for crypto data
*
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(sessionStore, cryptoStore) {
this._sessionStore = sessionStore;
function OlmDevice(cryptoStore) {
this._cryptoStore = cryptoStore;
this._pickleKey = "DEFAULT_KEY";
@@ -81,7 +78,7 @@ function OlmDevice(sessionStore, cryptoStore) {
this.deviceEd25519Key = null;
this._maxOneTimeKeys = null;
// we don't bother stashing outboundgroupsessions in the sessionstore -
// we don't bother stashing outboundgroupsessions in the cryptoStore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
@@ -102,6 +99,10 @@ function OlmDevice(sessionStore, cryptoStore) {
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are objects of the form "{id: <event id>, timestamp: <ts>}"
this._inboundGroupSessionMessageIndexes = {};
// Keep track of sessions that we're starting, so that we don't start
// multiple sessions for the same device at the same time.
this._sessionsInProgress = {};
}
/**
@@ -114,14 +115,10 @@ function OlmDevice(sessionStore, cryptoStore) {
* Reads the device keys from the OlmAccount object.
*/
OlmDevice.prototype.init = async function() {
await this._migrateFromSessionStore();
let e2eKeys;
const account = new global.Olm.Account();
try {
await _initialiseAccount(
this._sessionStore, this._cryptoStore, this._pickleKey, account,
);
await _initialiseAccount(this._cryptoStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
@@ -133,7 +130,7 @@ OlmDevice.prototype.init = async function() {
this.deviceEd25519Key = e2eKeys.ed25519;
};
async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account) {
async function _initialiseAccount(cryptoStore, pickleKey, account) {
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount !== null) {
@@ -154,95 +151,6 @@ OlmDevice.getOlmVersion = function() {
return global.Olm.get_library_version();
};
OlmDevice.prototype._migrateFromSessionStore = async function() {
// account
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount === null) {
// Migrate from sessionStore
pickledAccount = this._sessionStore.getEndToEndAccount();
if (pickledAccount !== null) {
logger.log("Migrating account from session store");
this._cryptoStore.storeAccount(txn, pickledAccount);
}
}
});
},
);
// remove the old account now the transaction has completed. Either we've
// migrated it or decided not to, either way we want to blow away the old data.
this._sessionStore.removeEndToEndAccount();
// sessions
const sessions = this._sessionStore.getAllEndToEndSessions();
if (Object.keys(sessions).length > 0) {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => {
// Don't migrate sessions from localstorage if we already have sessions
// in indexeddb, since this means we've already migrated and an old version
// has run against the same localstorage and created some spurious sessions.
this._cryptoStore.countEndToEndSessions(txn, (count) => {
if (count) {
logger.log("Crypto store already has sessions: not migrating");
return;
}
let numSessions = 0;
for (const deviceKey of Object.keys(sessions)) {
for (const sessionId of Object.keys(sessions[deviceKey])) {
numSessions++;
this._cryptoStore.storeEndToEndSession(
deviceKey, sessionId, sessions[deviceKey][sessionId], txn,
);
}
}
logger.log(
"Migrating " + numSessions + " sessions from session store",
);
});
},
);
this._sessionStore.removeAllEndToEndSessions();
}
// inbound group sessions
const ibGroupSessions = this._sessionStore.getAllEndToEndInboundGroupSessionKeys();
if (Object.keys(ibGroupSessions).length > 0) {
let numIbSessions = 0;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
// We always migrate inbound group sessions, even if we already have some
// in the new store. They should be be safe to migrate.
for (const s of ibGroupSessions) {
try {
this._cryptoStore.addEndToEndInboundGroupSession(
s.senderKey, s.sessionId,
JSON.parse(
this._sessionStore.getEndToEndInboundGroupSession(
s.senderKey, s.sessionId,
),
), txn,
);
} catch (e) {
logger.warn(
"Failed to migrate session " + s.senderKey + "/" +
s.sessionId + ": " + e.stack || e,
);
}
++numIbSessions;
}
logger.log(
"Migrated " + numIbSessions +
" inbound group sessions from session store",
);
},
);
this._sessionStore.removeAllEndToEndInboundGroupSessions();
}
};
/**
* extract our OlmAccount from the crypto store and call the given function
* with the account object
@@ -553,6 +461,15 @@ OlmDevice.prototype.createInboundSession = async function(
* @return {Promise<string[]>} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
logger.log("waiting for session to be created");
try {
await this._sessionsInProgress[theirDeviceIdentityKey];
} catch (e) {
// if the session failed to be created, just fall through and
// return an empty result
}
}
let sessionIds;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
@@ -573,10 +490,18 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function
* that marked the session as being in-progress.
* @return {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey);
OlmDevice.prototype.getSessionIdForDevice = async function(
theirDeviceIdentityKey, nowait,
) {
const sessionInfos = await this.getSessionInfoForDevice(
theirDeviceIdentityKey, nowait,
);
if (sessionInfos.length === 0) {
return null;
}
@@ -611,9 +536,21 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe
* message and is therefore past the pre-key stage), and 'sessionId'.
*
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function
* that marked the session as being in-progress.
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) {
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
logger.log("waiting for session to be created");
try {
await this._sessionsInProgress[deviceIdentityKey];
} catch (e) {
// if the session failed to be created, then just fall through and
// return an empty result
}
}
const info = [];
await this._cryptoStore.doTxn(
+3 -22
View File
@@ -1,5 +1,5 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -26,40 +26,21 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
* @alias module:crypto/RoomList
*/
export default class RoomList {
constructor(cryptoStore, sessionStore) {
constructor(cryptoStore) {
this._cryptoStore = cryptoStore;
this._sessionStore = sessionStore;
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
this._roomEncryption = {};
}
async init() {
let removeSessionStoreRooms = false;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.getEndToEndRooms(txn, (result) => {
if (result === null || Object.keys(result).length === 0) {
// migrate from session store, if there's data there
const sessStoreRooms = this._sessionStore.getAllEndToEndRooms();
if (sessStoreRooms !== null) {
for (const roomId of Object.keys(sessStoreRooms)) {
this._cryptoStore.storeEndToEndRoom(
roomId, sessStoreRooms[roomId], txn,
);
}
}
this._roomEncryption = sessStoreRooms;
removeSessionStoreRooms = true;
} else {
this._roomEncryption = result;
}
this._roomEncryption = result;
});
},
);
if (removeSessionStoreRooms) {
this._sessionStore.removeAllEndToEndRooms();
}
}
getRoomEncryption(roomId) {
+4 -4
View File
@@ -23,8 +23,8 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../../src/logger';
const logger = require("../../logger");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
@@ -278,7 +278,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
logger.log("Failed to back up group session", e);
});
}
@@ -955,7 +955,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
logger.log("Failed to back up group session", e);
});
}
}).catch((e) => {
@@ -1088,7 +1088,7 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
logger.log("Failed to back up group session", e);
});
}
// have another go at decrypting events sent with this session.
+1 -1
View File
@@ -22,7 +22,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
const logger = require("../../logger");
import logger from '../../logger';
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
+64 -52
View File
@@ -25,7 +25,7 @@ const anotherjson = require('another-json');
import Promise from 'bluebird';
import {EventEmitter} from 'events';
const logger = require("../logger");
import logger from '../logger';
const utils = require("../utils");
const OlmDevice = require("./OlmDevice");
const olmlib = require("./olmlib");
@@ -134,9 +134,9 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
this._sendingBackups = false; // Are we currently sending backups?
this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
this._olmDevice = new OlmDevice(cryptoStore);
this._deviceList = new DeviceList(
baseApis, cryptoStore, sessionStore, this._olmDevice,
baseApis, cryptoStore, this._olmDevice,
);
// the last time we did a check for the number of one-time-keys on the
@@ -197,27 +197,11 @@ utils.inherits(Crypto, EventEmitter);
* Returns a promise which resolves once the crypto module is ready for use.
*/
Crypto.prototype.init = async function() {
logger.log("Crypto: initialising Olm...");
await global.Olm.init();
const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount());
let cryptoStoreHasAccount;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
cryptoStoreHasAccount = Boolean(pickledAccount);
});
},
);
if (sessionStoreHasAccount && !cryptoStoreHasAccount) {
// we're about to migrate to the crypto store
this.emit("crypto.warning", 'CRYPTO_WARNING_ACCOUNT_MIGRATED');
} else if (sessionStoreHasAccount && cryptoStoreHasAccount) {
// There's an account in both stores: an old version of
// the code has been run against this store.
this.emit("crypto.warning", 'CRYPTO_WARNING_OLD_VERSION_DETECTED');
}
logger.log("Crypto: initialising Olm device...");
await this._olmDevice.init();
logger.log("Crypto: loading device list...");
await this._deviceList.load();
// build our device keys: these will later be uploaded
@@ -226,6 +210,7 @@ Crypto.prototype.init = async function() {
this._deviceKeys["curve25519:" + this._deviceId] =
this._olmDevice.deviceCurve25519Key;
logger.log("Crypto: fetching own devices...");
let myDevices = this._deviceList.getRawStoredDevicesForUser(
this._userId,
);
@@ -235,7 +220,8 @@ Crypto.prototype.init = async function() {
}
if (!myDevices[this._deviceId]) {
// add our own deviceinfo to the sessionstore
// add our own deviceinfo to the cryptoStore
logger.log("Crypto: adding this device to the store...");
const deviceInfo = {
keys: this._deviceKeys,
algorithms: this._supportedAlgorithms,
@@ -250,6 +236,7 @@ Crypto.prototype.init = async function() {
this._deviceList.saveIfDirty();
}
logger.log("Crypto: checking for key backup...");
this._checkAndStartKeyBackup();
};
@@ -260,9 +247,9 @@ Crypto.prototype.init = async function() {
* to it.
*/
Crypto.prototype._checkAndStartKeyBackup = async function() {
console.log("Checking key backup status...");
logger.log("Checking key backup status...");
if (this._baseApis.isGuest()) {
console.log("Skipping key backup check since user is guest");
logger.log("Skipping key backup check since user is guest");
this._checkedForBackup = true;
return null;
}
@@ -270,7 +257,7 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
try {
backupInfo = await this._baseApis.getKeyBackupVersion();
} catch (e) {
console.log("Error checking for active key backup", e);
logger.log("Error checking for active key backup", e);
if (e.httpStatus / 100 === 4) {
// well that's told us. we won't try again.
this._checkedForBackup = true;
@@ -282,27 +269,27 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) {
console.log(
logger.log(
"Found usable key backup v" + backupInfo.version +
": enabling key backups",
);
this._baseApis.enableKeyBackup(backupInfo);
} else if (!trustInfo.usable && this.backupInfo) {
console.log("No usable key backup: disabling key backup");
logger.log("No usable key backup: disabling key backup");
this._baseApis.disableKeyBackup();
} else if (!trustInfo.usable && !this.backupInfo) {
console.log("No usable key backup: not enabling key backup");
logger.log("No usable key backup: not enabling key backup");
} else if (trustInfo.usable && this.backupInfo) {
// may not be the same version: if not, we should switch
if (backupInfo.version !== this.backupInfo.version) {
console.log(
logger.log(
"On backup version " + this.backupInfo.version + " but found " +
"version " + backupInfo.version + ": switching.",
);
this._baseApis.disableKeyBackup();
this._baseApis.enableKeyBackup(backupInfo);
} else {
console.log("Backup version " + backupInfo.version + " still current");
logger.log("Backup version " + backupInfo.version + " still current");
}
}
@@ -370,7 +357,12 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
const mySigs = backupInfo.auth_data.signatures[this._userId] || [];
for (const keyId of Object.keys(mySigs)) {
const sigInfo = { deviceId: keyId.split(':')[1] }; // XXX: is this how we're supposed to get the device ID?
const keyIdParts = keyId.split(':');
if (keyIdParts[0] !== 'ed25519') {
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
continue;
}
const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID?
const device = this._deviceList.getStoredDevice(
this._userId, sigInfo.deviceId,
);
@@ -379,14 +371,20 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
try {
await olmlib.verifySignature(
this._olmDevice,
backupInfo.auth_data,
// verifySignature modifies the object so we need to copy
// if we verify more than one sig
Object.assign({}, backupInfo.auth_data),
this._userId,
device.deviceId,
device.getFingerprint(),
);
sigInfo.valid = true;
} catch (e) {
logger.info("Bad signature from device " + device.deviceId, e);
logger.info(
"Bad signature from key ID " + keyId + " userID " + this._userId +
" device ID " + device.deviceId + " fingerprint: " +
device.getFingerprint(), backupInfo.auth_data, e,
);
sigInfo.valid = false;
}
} else {
@@ -942,7 +940,7 @@ Crypto.prototype.forceDiscardSession = function(roomId) {
};
/**
* Configure a room to use encryption (ie, save a flag in the sessionstore).
* Configure a room to use encryption (ie, save a flag in the cryptoStore).
*
* @param {string} roomId The room ID to enable encryption in.
*
@@ -1180,7 +1178,7 @@ Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
numFailures = 0;
} catch (err) {
numFailures++;
console.log("Key backup request failed", err);
logger.log("Key backup request failed", err);
if (err.data) {
if (
err.data.errcode == 'M_NOT_FOUND' ||
@@ -1290,6 +1288,18 @@ Crypto.prototype.backupGroupSession = async function(
* upload in the background as soon as possible.
*/
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
await this.flagAllGroupSessionsForBackup();
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
};
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
*/
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
await this._cryptoStore.doTxn(
'readwrite',
[
@@ -1307,9 +1317,7 @@ Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
return remaining;
};
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
@@ -1650,6 +1658,11 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
* @param {module:models/event.MatrixEvent} event verification request event
*/
Crypto.prototype._onKeyVerificationRequest = function(event) {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification request from " + event.getSender());
return;
}
const content = event.getContent();
if (!("from_device" in content) || typeof content.from_device !== "string"
|| !("transaction_id" in content) || typeof content.from_device !== "string"
@@ -1669,6 +1682,11 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
}
const sender = event.getSender();
if (sender === this._userId && content.from_device === this._deviceId) {
// ignore requests from ourselves, because it doesn't make sense for a
// device to verify itself
return;
}
if (this._verificationTransactions.has(sender)) {
if (this._verificationTransactions.get(sender).has(content.transaction_id)) {
// transaction already exists: cancel it and drop the existing
@@ -1721,7 +1739,7 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
},
);
} else {
// notify the application that of the verification request, so it can
// notify the application of the verification request, so it can
// decide what to do with it
const request = {
event: event,
@@ -1761,6 +1779,11 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
* @param {module:models/event.MatrixEvent} event verification start event
*/
Crypto.prototype._onKeyVerificationStart = function(event) {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification start from " + event.getSender());
return;
}
const sender = event.getSender();
const content = event.getContent();
const transactionId = content.transaction_id;
@@ -1886,7 +1909,7 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
if (!handler) {
return;
} else if (event.getType() === "m.key.verification.cancel") {
console.log(event);
logger.log(event);
if (handler.verifier) {
handler.verifier.cancel(event);
} else if (handler.request && handler.request.cancel) {
@@ -2358,17 +2381,6 @@ class IncomingRoomKeyRequestCancellation {
* Fires when the app may wish to warn the user about something related
* the end-to-end crypto.
*
* Comes with a type which is one of:
* * CRYPTO_WARNING_ACCOUNT_MIGRATED: Account data has been migrated from an older
* version of the store in such a way that older clients will no longer be
* able to read it. The app may wish to warn the user against going back to
* an older version of the app.
* * CRYPTO_WARNING_OLD_VERSION_DETECTED: js-sdk has detected that an older version
* of js-sdk has been run against the same store after a migration has been
* performed. This is likely have caused unexpected behaviour in the old
* version. For example, the old version and the new version may have two
* different identity keys.
*
* @event module:client~MatrixClient#"crypto.warning"
* @param {string} type One of the strings listed above
*/
+60 -15
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,7 +24,7 @@ limitations under the License.
import Promise from 'bluebird';
const anotherjson = require('another-json');
const logger = require("../logger");
import logger from '../logger';
const utils = require("../utils");
/**
@@ -137,6 +138,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
// [userId, deviceId], ...
];
const result = {};
const resolveSession = {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
@@ -148,7 +150,36 @@ module.exports.ensureOlmSessionsForDevices = async function(
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
const sessionId = await olmDevice.getSessionIdForDevice(key);
if (!olmDevice._sessionsInProgress[key]) {
// pre-emptively mark the session as in-progress to avoid race
// conditions. If we find that we already have a session, then
// we'll resolve
olmDevice._sessionsInProgress[key] = new Promise(
(resolve, reject) => {
resolveSession[key] = {
resolve: (...args) => {
delete olmDevice._sessionsInProgress[key];
resolve(...args);
},
reject: (...args) => {
delete olmDevice._sessionsInProgress[key];
reject(...args);
},
};
},
);
}
const sessionId = await olmDevice.getSessionIdForDevice(
key, resolveSession[key],
);
if (sessionId !== null && resolveSession[key]) {
// we found a session, but we had marked the session as
// in-progress, so unmark it and unblock anything that was
// waiting
delete olmDevice._sessionsInProgress[key];
resolveSession[key].resolve();
delete resolveSession[key];
}
if (sessionId === null || force) {
devicesWithoutSession.push([userId, deviceId]);
}
@@ -163,16 +194,19 @@ module.exports.ensureOlmSessionsForDevices = async function(
return result;
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
const oneTimeKeyAlgorithm = "signed_curve25519";
const res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
);
let res;
try {
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
resolver.resolve();
}
logger.log("failed to claim one-time keys", e, devicesWithoutSession);
throw e;
}
const otk_res = res.one_time_keys || {};
const promises = [];
@@ -185,6 +219,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
if (result[userId][deviceId].sessionId && !force) {
// we already have a result for this device
continue;
@@ -199,10 +234,12 @@ module.exports.ensureOlmSessionsForDevices = async function(
}
if (!oneTimeKey) {
logger.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId;
logger.warn(msg);
if (resolveSession[key]) {
resolveSession[key].resolve();
}
continue;
}
@@ -210,7 +247,15 @@ module.exports.ensureOlmSessionsForDevices = async function(
_verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => {
if (resolveSession[key]) {
resolveSession[key].resolve(sid);
}
result[userId][deviceId].sessionId = sid;
}, (e) => {
if (resolveSession[key]) {
resolveSession[key].resolve();
}
throw e;
}),
);
}
+1 -1
View File
@@ -21,7 +21,7 @@ import bs58 from 'bs58';
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key) {
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
const buf = new Buffer(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
@@ -694,9 +694,17 @@ function promiseifyTxn(txn) {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
}
};
txn.onabort = (event) => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
} else {
logger.log("Error performing indexeddb txn", event);
reject(event.target.error);
}
};
txn.onabort = () => reject(txn._mx_abortexception);
});
}
@@ -22,6 +22,7 @@ import LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import {InvalidCryptoStoreError} from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers";
/**
* Internal module. indexeddb storage for e2e.
@@ -48,6 +49,10 @@ export default class IndexedDBCryptoStore {
this._backendPromise = null;
}
static exists(indexedDB, dbName) {
return IndexedDBHelpers.exists(indexedDB, dbName);
}
/**
* Ensure the database exists and is up-to-date, or fall back to
* a local storage or in-memory store.
@@ -85,6 +90,7 @@ export default class IndexedDBCryptoStore {
};
req.onerror = (ev) => {
logger.log("Error connecting to indexeddb", ev);
reject(ev.target.error);
};
@@ -154,6 +160,7 @@ export default class IndexedDBCryptoStore {
};
req.onerror = (ev) => {
logger.log("Error deleting data from indexeddb", ev);
reject(ev.target.error);
};
@@ -57,6 +57,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
this.store = webStore;
}
static exists(webStore) {
const length = webStore.length;
for (let i = 0; i < length; i++) {
if (webStore.key(i).startsWith(E2E_PREFIX)) {
return true;
}
}
return false;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
+58 -5
View File
@@ -21,6 +21,10 @@ limitations under the License.
import {MatrixEvent} from '../../models/event';
import {EventEmitter} from 'events';
import logger from '../../logger';
import {newTimeoutError} from "./Error";
const timeoutException = new Error("Verification timed out");
export default class VerificationBase extends EventEmitter {
/**
@@ -59,9 +63,34 @@ export default class VerificationBase extends EventEmitter {
this.transactionId = transactionId;
this.startEvent = startEvent;
this.request = request;
this.cancelled = false;
this._parent = parent;
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
}
_resetTimer() {
console.log("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
}
this._transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) {
console.log("Triggering verification timeout");
this.cancel(timeoutException);
}
}, 10 * 60 * 1000); // 10 minutes
}
_endTimer() {
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
this._transactionTimeoutTimer = null;
}
}
_sendToDevice(type, content) {
@@ -91,6 +120,7 @@ export default class VerificationBase extends EventEmitter {
} else if (e.getType() === this._expectedEvent) {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
} else {
this._expectedEvent = undefined;
@@ -108,17 +138,23 @@ export default class VerificationBase extends EventEmitter {
}
done() {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this._resolve();
}
}
cancel(e) {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
if (this.userId && this.deviceId && this.transactionId) {
// send a cancellation to the other user (if it wasn't
// cancelled by the other user)
if (e instanceof MatrixEvent) {
if (e === timeoutException) {
const timeoutEvent = newTimeoutError();
this._sendToDevice(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) {
const sender = e.getSender();
if (sender !== this.userId) {
const content = e.getContent();
@@ -145,7 +181,9 @@ export default class VerificationBase extends EventEmitter {
}
}
if (this._promise !== null) {
this._reject(e);
// when we cancel without a promise, we end up with a promise
// but no reject function. If cancel is called again, we'd error.
if (this._reject) this._reject(e);
} else {
this._promise = Promise.reject(e);
}
@@ -167,15 +205,18 @@ export default class VerificationBase extends EventEmitter {
this._promise = new Promise((resolve, reject) => {
this._resolve = (...args) => {
this._done = true;
this._endTimer();
resolve(...args);
};
this._reject = (...args) => {
this._done = true;
this._endTimer();
reject(...args);
};
});
if (this._doVerification && !this._started) {
this._started = true;
this._resetTimer(); // restart the timeout
Promise.resolve(this._doVerification())
.then(this.done.bind(this), this.cancel.bind(this));
}
@@ -183,17 +224,29 @@ export default class VerificationBase extends EventEmitter {
}
async _verifyKeys(userId, keys, verifier) {
// we try to verify all the keys that we're told about, but we might
// not know about all of them, so keep track of the keys that we know
// about, and ignore the rest
const verifiedDevices = [];
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = await this._baseApis.getStoredDevice(userId, deviceId);
if (!device) {
throw new Error(`Could not find device ${deviceId}`);
logger.warn(`verification: Could not find device ${deviceId} to verify`);
} else {
await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
}
}
for (const keyId of Object.keys(keys)) {
const deviceId = keyId.split(':', 2)[1];
// if none of the keys could be verified, then error because the app
// should be informed about that
if (!verifiedDevices.length) {
throw new Error("No devices could be verified");
}
for (const deviceId of verifiedDevices) {
await this._baseApis.setDeviceVerified(userId, deviceId);
}
}
+77 -46
View File
@@ -143,17 +143,44 @@ function generateEmojiSas(sasBytes) {
return emojis.map((num) => emojiMapping[num]);
}
const sasGenerators = {
decimal: generateDecimalSas,
emoji: generateEmojiSas,
};
function generateSas(sasBytes, methods) {
const sas = {};
if (methods.includes("decimal")) {
sas["decimal"] = generateDecimalSas(sasBytes);
}
if (methods.includes("emoji")) {
sas["emoji"] = generateEmojiSas(sasBytes);
for (const method of methods) {
if (method in sasGenerators) {
sas[method] = sasGenerators[method](sasBytes);
}
}
return sas;
}
const macMethods = {
"hkdf-hmac-sha256": "calculate_mac",
"hmac-sha256": "calculate_mac_long_kdf",
};
/* lists of algorithms/methods that are supported. The key agreement, hashes,
* and MAC lists should be sorted in order of preference (most preferred
* first).
*/
const KEY_AGREEMENT_LIST = ["curve25519"];
const HASHES_LIST = ["sha256"];
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
const SAS_LIST = Object.keys(sasGenerators);
const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
const HASHES_SET = new Set(HASHES_LIST);
const MAC_SET = new Set(MAC_LIST);
const SAS_SET = new Set(SAS_LIST);
function intersection(anArray, aSet) {
return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
}
/**
* @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base}
@@ -181,11 +208,11 @@ export default class SAS extends Base {
const initialMessage = {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
key_agreement_protocols: ["curve25519"],
hashes: ["sha256"],
message_authentication_codes: ["hmac-sha256"],
key_agreement_protocols: KEY_AGREEMENT_LIST,
hashes: HASHES_LIST,
message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: ["decimal", "emoji"],
short_authentication_string: SAS_LIST,
transaction_id: this.transactionId,
};
this._sendToDevice("m.key.verification.start", initialMessage);
@@ -193,19 +220,19 @@ export default class SAS extends Base {
let e = await this._waitForEvent("m.key.verification.accept");
let content = e.getContent();
if (!(content.key_agreement_protocol === "curve25519"
&& content.hash === "sha256"
&& content.message_authentication_code === "hmac-sha256"
&& content.short_authentication_string instanceof Array
&& (content.short_authentication_string.includes("decimal")
|| content.short_authentication_string.includes("emoji")))) {
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
if (!(KEY_AGREEMENT_SET.has(content.key_agreement_protocol)
&& HASHES_SET.has(content.hash)
&& MAC_SET.has(content.message_authentication_code)
&& sasMethods.length)) {
throw newUnknownMethodError();
}
if (typeof content.commitment !== "string") {
throw newInvalidMessageError();
}
const macMethod = content.message_authentication_code;
const hashCommitment = content.commitment;
const sasMethods = content.short_authentication_string;
const olmSAS = new global.Olm.SAS();
try {
this._sendToDevice("m.key.verification.key", {
@@ -217,6 +244,7 @@ export default class SAS extends Base {
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
// TODO: use selected hash function (when we support multiple)
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
throw newMismatchedCommitmentError();
}
@@ -231,7 +259,7 @@ export default class SAS extends Base {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS);
this._sendMAC(olmSAS, macMethod);
resolve();
},
cancel: () => reject(newUserCancelledError()),
@@ -245,7 +273,7 @@ export default class SAS extends Base {
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content);
await this._checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
@@ -253,34 +281,37 @@ export default class SAS extends Base {
async _doRespondVerification() {
let content = this.startEvent.getContent();
if (!(content.key_agreement_protocols instanceof Array
&& content.key_agreement_protocols.includes("curve25519")
&& content.hashes instanceof Array
&& content.hashes.includes("sha256")
&& content.message_authentication_codes instanceof Array
&& content.message_authentication_codes.includes("hmac-sha256")
&& content.short_authentication_string instanceof Array
&& (content.short_authentication_string.includes("decimal")
|| content.short_authentication_string.includes("emoji")))) {
// Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then
// fetching the first element from the array will give our preferred
// method out of the ones offered by the other party.
const keyAgreement
= intersection(
KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols),
)[0];
const hashMethod
= intersection(HASHES_LIST, new Set(content.hashes))[0];
const macMethod
= intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
// FIXME: allow app to specify what SAS methods can be used
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
if (!(keyAgreement !== undefined
&& hashMethod !== undefined
&& macMethod !== undefined
&& sasMethods.length)) {
throw newUnknownMethodError();
}
const olmSAS = new global.Olm.SAS();
const sasMethods = [];
// FIXME: allow app to specify what SAS methods can be used
if (content.short_authentication_string.includes("decimal")) {
sasMethods.push("decimal");
}
if (content.short_authentication_string.includes("emoji")) {
sasMethods.push("emoji");
}
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._sendToDevice("m.key.verification.accept", {
key_agreement_protocol: "curve25519",
hash: "sha256",
message_authentication_code: "hmac-sha256",
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
short_authentication_string: sasMethods,
// TODO: use selected hash function (when we support multiple)
commitment: olmutil.sha256(commitmentStr),
});
@@ -302,7 +333,7 @@ export default class SAS extends Base {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS);
this._sendMAC(olmSAS, macMethod);
resolve();
},
cancel: () => reject(newUserCancelledError()),
@@ -316,13 +347,13 @@ export default class SAS extends Base {
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content);
await this._checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
_sendMAC(olmSAS) {
_sendMAC(olmSAS, method) {
const keyId = `ed25519:${this._baseApis.deviceId}`;
const mac = {};
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
@@ -330,24 +361,24 @@ export default class SAS extends Base {
+ this.userId + this.deviceId
+ this.transactionId;
mac[keyId] = olmSAS.calculate_mac(
mac[keyId] = olmSAS[macMethods[method]](
this._baseApis.getDeviceEd25519Key(),
baseInfo + keyId,
);
const keys = olmSAS.calculate_mac(
const keys = olmSAS[macMethods[method]](
keyId,
baseInfo + "KEY_IDS",
);
this._sendToDevice("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content) {
async _checkMAC(olmSAS, content, method) {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
if (content.keys !== olmSAS.calculate_mac(
if (content.keys !== olmSAS[macMethods[method]](
Object.keys(content.mac).sort().join(","),
baseInfo + "KEY_IDS",
)) {
@@ -355,7 +386,7 @@ export default class SAS extends Base {
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== olmSAS.calculate_mac(
if (keyInfo !== olmSAS[macMethods[method]](
device.keys[keyId],
baseInfo + keyId,
)) {
+42 -11
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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.
@@ -22,6 +23,7 @@ import Promise from 'bluebird';
const parseContentType = require('content-type').parse;
const utils = require("./utils");
import logger from '../src/logger';
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
@@ -45,10 +47,15 @@ module.exports.PREFIX_R0 = "/_matrix/client/r0";
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for the identity API
* URI path for v1 of the the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the v2 identity API
*/
module.exports.PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
/**
* URI path for the media repo API
*/
@@ -101,7 +108,7 @@ module.exports.MatrixHttpApi.prototype = {
};
return {
base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload",
path: "/_matrix/media/r0/upload",
params: params,
};
},
@@ -164,9 +171,21 @@ module.exports.MatrixHttpApi.prototype = {
const contentType = opts.type || file.type || 'application/octet-stream';
const fileName = opts.name || file.name;
// we used to recommend setting file.stream to the thing to upload on
// nodejs.
const body = file.stream ? file.stream : file;
// We used to recommend setting file.stream to the thing to upload on
// Node.js. As of 2019-06-11, this is still in widespread use in various
// clients, so we should preserve this for simple objects used in
// Node.js. File API objects (via either the File or Blob interfaces) in
// the browser now define a `stream` method, which leads to trouble
// here, so we also check the type of `stream`.
let body = file;
if (body.stream && typeof body.stream !== "function") {
logger.warn(
"Using `file.stream` as the content to upload. Future " +
"versions of the js-sdk will change this to expect `file` to " +
"be the content directly.",
);
body = body.stream;
}
// backwards-compatibility hacks where we used to do different things
// between browser and node.
@@ -175,7 +194,7 @@ module.exports.MatrixHttpApi.prototype = {
if (global.XMLHttpRequest) {
rawResponse = false;
} else {
console.warn(
logger.warn(
"Returning the raw JSON from uploadContent(). Future " +
"versions of the js-sdk will change this default, to " +
"return the parsed object. Set opts.rawResponse=false " +
@@ -188,7 +207,7 @@ module.exports.MatrixHttpApi.prototype = {
let onlyContentUri = opts.onlyContentUri;
if (!rawResponse && onlyContentUri === undefined) {
if (global.XMLHttpRequest) {
console.warn(
logger.warn(
"Returning only the content-uri from uploadContent(). " +
"Future versions of the js-sdk will change this " +
"default, to return the whole response object. Set " +
@@ -278,7 +297,7 @@ module.exports.MatrixHttpApi.prototype = {
});
}
});
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
let url = this.opts.baseUrl + "/_matrix/media/r0/upload";
const queryArgs = [];
@@ -314,7 +333,7 @@ module.exports.MatrixHttpApi.prototype = {
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/v1",
prefix: "/_matrix/media/r0",
headers: {"Content-Type": contentType},
json: false,
bodyParser: bodyParser,
@@ -355,7 +374,14 @@ module.exports.MatrixHttpApi.prototype = {
return this.uploads;
},
idServerRequest: function(callback, method, path, params, prefix) {
idServerRequest: function(
callback,
method,
path,
params,
prefix,
accessToken,
) {
const fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) {
@@ -376,6 +402,11 @@ module.exports.MatrixHttpApi.prototype = {
} else {
opts.form = params;
}
if (accessToken) {
opts.headers = {
Authorization: `Bearer ${accessToken}`,
};
}
const defer = Promise.defer();
this.opts.request(
@@ -457,7 +488,7 @@ module.exports.MatrixHttpApi.prototype = {
const self = this;
requestPromise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
self.event_emitter.emit("Session.logged_out", err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
"no_consent",
+52
View File
@@ -0,0 +1,52 @@
/*
Copyright 2019 New Vector Ltd
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 Promise from 'bluebird';
/**
* Check if an IndexedDB database exists. The only way to do so is to try opening it, so
* we do that and then delete it did not exist before.
*
* @param {Object} indexedDB The `indexedDB` interface
* @param {string} dbName The database name to test for
* @returns {boolean} Whether the database exists
*/
export function exists(indexedDB, dbName) {
return new Promise((resolve, reject) => {
let exists = true;
const req = indexedDB.open(dbName);
req.onupgradeneeded = () => {
// Since we did not provide an explicit version when opening, this event
// should only fire if the DB did not exist before at any version.
exists = false;
};
req.onblocked = () => reject();
req.onsuccess = () => {
const db = req.result;
db.close();
if (!exists) {
// The DB did not exist before, but has been created as part of this
// existence check. Delete it now to restore previous state. Delete can
// actually take a while to complete in some browsers, so don't wait for
// it. This won't block future open calls that a store might issue next to
// properly set up the DB.
indexedDB.deleteDatabase(dbName);
}
resolve(exists);
};
req.onerror = ev => reject(ev.target.error);
});
}
+151 -69
View File
@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 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.
@@ -21,6 +22,7 @@ import Promise from 'bluebird';
const url = require("url");
const utils = require("./utils");
import logger from '../src/logger';
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -47,11 +49,18 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* @param {object?} opts.authData error response from the last request. If
* null, a request will be made with no auth before starting.
*
* @param {function(object?, bool?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request and a flag set
* to true if this request is a background request. Should return a
* promise which resolves to the successful response or rejects with a
* MatrixError.
* @param {function(object?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request. Also passes a
* second deprecated arg which is a flag set to true if this request
* is a background request. The busyChanged callback should be used
* instead of the backfround flag. Should return a promise which resolves
* to the successful response or rejects with a MatrixError.
*
* @param {function(bool): module:client.Promise} opts.busyChanged
* called whenever the interactive auth logic becomes busy submitting
* information provided by the user or finsihes. After this has been
* called with true the UI should indicate that a request is in progress
* until it is called again with false.
*
* @param {function(string, object?)} opts.stateUpdated
* called when the status of the UI auth changes, ie. when the state of
@@ -88,22 +97,37 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* @param {string?} opts.emailSid If returning from having completed m.login.email.identity
* auth, the sid for the email verification session.
*
* @param {function?} opts.requestEmailToken A function that takes the email address (string),
* clientSecret (string), attempt number (int) and sessionId (string) and calls the
* relevant requestToken function and returns the promise returned by that function.
* If the resulting promise rejects, the rejection will propagate through to the
* attemptAuth promise.
*
*/
function InteractiveAuth(opts) {
this._matrixClient = opts.matrixClient;
this._data = opts.authData || {};
this._requestCallback = opts.doRequest;
this._busyChangedCallback = opts.busyChanged;
// startAuthStage included for backwards compat
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this._completionDeferred = null;
this._resolveFunc = null;
this._rejectFunc = null;
this._inputs = opts.inputs || {};
this._requestEmailTokenCallback = opts.requestEmailToken;
if (opts.sessionId) this._data.session = opts.sessionId;
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret();
this._emailSid = opts.emailSid;
if (this._emailSid === undefined) this._emailSid = null;
this._requestingEmailToken = false;
this._chosenFlow = null;
this._currentStage = null;
// if we are currently trying to submit an auth dict (which includes polling)
// the promise the will resolve/reject when it completes
this._submitPromise = null;
}
InteractiveAuth.prototype = {
@@ -115,19 +139,22 @@ InteractiveAuth.prototype = {
* no suitable authentication flow can be found
*/
attemptAuth: function() {
this._completionDeferred = Promise.defer();
// This promise will be quite long-lived and will resolve when the
// request is authenticated and completes successfully.
return new Promise((resolve, reject) => {
this._resolveFunc = resolve;
this._rejectFunc = reject;
// wrap in a promise so that if _startNextAuthStage
// throws, it rejects the promise in a consistent way
return Promise.resolve().then(() => {
// if we have no flows, try a request (we'll have
// just a session ID in _data if resuming)
if (!this._data.flows) {
this._doRequest(this._data);
if (this._busyChangedCallback) this._busyChangedCallback(true);
this._doRequest(this._data).finally(() => {
if (this._busyChangedCallback) this._busyChangedCallback(false);
});
} else {
this._startNextAuthStage();
}
return this._completionDeferred.promise;
});
},
@@ -136,8 +163,11 @@ InteractiveAuth.prototype = {
* completed out-of-band. If so, the attemptAuth promise will
* be resolved.
*/
poll: function() {
poll: async function() {
if (!this._data.session) return;
// if we currently have a request in flight, there's no point making
// another just to check what the status is
if (this._submitPromise) return;
let authDict = {};
if (this._currentStage == EMAIL_STAGE_TYPE) {
@@ -194,6 +224,10 @@ InteractiveAuth.prototype = {
return params[loginType];
},
getChosenFlow() {
return this._chosenFlow;
},
/**
* submit a new auth dict and fire off the request. This will either
* make attemptAuth resolve/reject, or cause the startAuthStage callback
@@ -206,18 +240,44 @@ InteractiveAuth.prototype = {
* in the attemptAuth promise being rejected. This can be set to true
* for requests that just poll to see if auth has been completed elsewhere.
*/
submitAuthDict: function(authData, background) {
if (!this._completionDeferred) {
submitAuthDict: async function(authData, background) {
if (!this._resolveFunc) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
if (!background && this._busyChangedCallback) {
this._busyChangedCallback(true);
}
// if we're currently trying a request, wait for it to finish
// as otherwise we can get multiple 200 responses which can mean
// things like multiple logins for register requests.
// (but discard any expections as we only care when its done,
// not whether it worked or not)
while (this._submitPromise) {
try {
await this._submitPromise;
} catch (e) {
}
}
// use the sessionid from the last request.
const auth = {
session: this._data.session,
};
utils.extend(auth, authData);
this._doRequest(auth, background);
try {
// NB. the 'background' flag is deprecated by the busyChanged
// callback and is here for backwards compat
this._submitPromise = this._doRequest(auth, background);
await this._submitPromise;
} finally {
this._submitPromise = null;
if (!background && this._busyChangedCallback) {
this._busyChangedCallback(false);
}
}
},
/**
@@ -253,58 +313,78 @@ InteractiveAuth.prototype = {
* This can be set to true for requests that just poll to see if auth has
* been completed elsewhere.
*/
_doRequest: function(auth, background) {
const self = this;
// hackery to make sure that synchronous exceptions end up in the catch
// handler (without the additional event loop entailed by q.fcall or an
// extra Promise.resolve().then)
let prom;
_doRequest: async function(auth, background) {
try {
prom = this._requestCallback(auth, background);
} catch (e) {
prom = Promise.reject(e);
}
const result = await this._requestCallback(auth, background);
this._resolveFunc(result);
} catch (error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = Boolean(this._data.flows) || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure.
if (!background) {
this._rejectFunc(error);
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
logger.log(
"Background poll request failed doing UI auth: ignoring",
error,
);
}
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = this._data.flows;
error.data.completed = this._data.completed;
error.data.session = this._data.session;
}
this._data = error.data;
this._startNextAuthStage();
prom = prom.then(
function(result) {
console.log("result from request: ", result);
self._completionDeferred.resolve(result);
}, function(error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = Boolean(self._data.flows) || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure. fail the whole lot.
throw error;
if (
!this._emailSid &&
!this._requestingEmailToken &&
this._chosenFlow.stages.includes('m.login.email.identity')
) {
// If we've picked a flow with email auth, we send the email
// now because we want the request to fail as soon as possible
// if the email address is not valid (ie. already taken or not
// registered, depending on what the operation is).
this._requestingEmailToken = true;
try {
const requestTokenResult = await this._requestEmailTokenCallback(
this._inputs.emailAddress,
this._clientSecret,
1, // TODO: Multiple send attempts?
this._data.session,
);
this._emailSid = requestTokenResult.sid;
// NB. promise is not resolved here - at some point, doRequest
// will be called again and if the user has jumped through all
// the hoops correctly, auth will be complete and the request
// will succeed.
// Also, we should expose the fact that this request has compledted
// so clients can know that the email has actually been sent.
} catch (e) {
// we failed to request an email token, so fail the request.
// This could be due to the email already beeing registered
// (or not being registered, depending on what we're trying
// to do) or it could be a network failure. Either way, pass
// the failure up as the user can't complete auth if we can't
// send the email, foe whatever reason.
this._rejectFunc(e);
} finally {
this._requestingEmailToken = false;
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = self._data.flows;
error.data.completed = self._data.completed;
error.data.session = self._data.session;
}
self._data = error.data;
self._startNextAuthStage();
},
);
if (!background) {
prom = prom.catch((e) => {
this._completionDeferred.reject(e);
});
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
prom = prom.catch((error) => {
console.log("Ignoring error from UI auth: " + error);
});
}
}
prom.done();
},
/**
@@ -320,7 +400,7 @@ InteractiveAuth.prototype = {
}
this._currentStage = nextStage;
if (nextStage == 'm.login.dummy') {
if (nextStage === 'm.login.dummy') {
this.submitAuthDict({
type: 'm.login.dummy',
});
@@ -350,10 +430,12 @@ InteractiveAuth.prototype = {
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseStage: function() {
const flow = this._chooseFlow();
console.log("Active flow => %s", JSON.stringify(flow));
const nextStage = this._firstUncompletedStage(flow);
console.log("Next stage: %s", nextStage);
if (this._chosenFlow === null) {
this._chosenFlow = this._chooseFlow();
}
logger.log("Active flow => %s", JSON.stringify(this._chosenFlow));
const nextStage = this._firstUncompletedStage(this._chosenFlow);
logger.log("Next stage: %s", nextStage);
return nextStage;
},
+12 -4
View File
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 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.
@@ -22,8 +23,14 @@ module.exports.ContentHelpers = require("./content-helpers");
module.exports.MatrixEvent = require("./models/event").MatrixEvent;
/** The {@link module:models/event.EventStatus|EventStatus} enum. */
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
/** The {@link module:store/memory.MemoryStore|MemoryStore} class. */
module.exports.MemoryStore = require("./store/memory").MemoryStore;
/**
* The {@link module:store/memory.MemoryStore|MemoryStore} class was previously
* exported as `MatrixInMemoryStore`, so this is preserved for SDK consumers.
* @deprecated Prefer `MemoryStore` going forward.
*/
module.exports.MatrixInMemoryStore = module.exports.MemoryStore;
/** The {@link module:store/indexeddb.IndexedDBStore|IndexedDBStore} class. */
module.exports.IndexedDBStore = require("./store/indexeddb").IndexedDBStore;
/** The {@link module:store/indexeddb.IndexedDBStoreBackend|IndexedDBStoreBackend} class. */
@@ -70,6 +77,7 @@ module.exports.InteractiveAuth = require("./interactive-auth");
/** The {@link module:auto-discovery|AutoDiscovery} class. */
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
module.exports.SERVICE_TYPES = require('./service-types').SERVICE_TYPES;
module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default;
@@ -164,7 +172,7 @@ module.exports.setCryptoStoreFactory = function(fac) {
* this is a string, it is assumed to be the base URL. These configuration
* options will be passed directly to {@link module:client~MatrixClient}.
* @param {Object} opts.store If not set, defaults to
* {@link module:store/memory.MatrixInMemoryStore}.
* {@link module:store/memory.MemoryStore}.
* @param {Object} opts.scheduler If not set, defaults to
* {@link module:scheduler~MatrixScheduler}.
* @param {requestFunction} opts.request If not set, defaults to the function
@@ -187,7 +195,7 @@ module.exports.createClient = function(opts) {
};
}
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
opts.store = opts.store || new module.exports.MemoryStore({
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
+183 -6
View File
@@ -20,6 +20,9 @@ limitations under the License.
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const EventTimeline = require("./event-timeline");
import {EventStatus} from "./event";
import logger from '../../src/logger';
import Relations from './relations';
// var DEBUG = false;
const DEBUG = true;
@@ -27,7 +30,7 @@ const DEBUG = true;
let debuglog;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console);
debuglog = logger.log.bind(logger);
} else {
debuglog = function() {};
}
@@ -54,22 +57,38 @@ if (DEBUG) {
* map from event_id to timeline and index.
*
* @constructor
* @param {?Room} room the optional room for this timelineSet
* @param {Object} opts hash of options inherited from Room.
* opts.timelineSupport gives whether timeline support is enabled
* opts.filter is the filter object, if any, for this timelineSet.
* @param {?Room} room
* Room for this timelineSet. May be null for non-room cases, such as the
* notification timeline.
* @param {Object} opts Options inherited from Room.
*
* @param {boolean} [opts.timelineSupport = false]
* Set to true to enable improved timeline support.
* @param {Object} [opts.filter = null]
* The filter object, if any, for this timelineSet.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*/
function EventTimelineSet(room, opts) {
this.room = room;
this._timelineSupport = Boolean(opts.timelineSupport);
this._liveTimeline = new EventTimeline(this);
this._unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
// just a list - *not* ordered.
this._timelines = [this._liveTimeline];
this._eventIdToTimeline = {};
this._filter = opts.filter || null;
if (this._unstableClientRelationAggregation) {
// A tree of objects to access a set of relations for an event, as in:
// this._relations[relatesToEventId][relationType][relationEventType]
this._relations = {};
}
}
utils.inherits(EventTimelineSet, EventEmitter);
@@ -405,11 +424,38 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
}
// time to join the timelines.
console.info("Already have timeline for " + eventId +
logger.info("Already have timeline for " + eventId +
" - joining timeline " + timeline + " to " +
existingTimeline);
// Variables to keep the line length limited below.
const existingIsLive = existingTimeline === this._liveTimeline;
const timelineIsLive = timeline === this._liveTimeline;
const backwardsIsLive = direction === EventTimeline.BACKWARDS && existingIsLive;
const forwardsIsLive = direction === EventTimeline.FORWARDS && timelineIsLive;
if (backwardsIsLive || forwardsIsLive) {
// The live timeline should never be spliced into a non-live position.
// We use independent logging to better discover the problem at a glance.
if (backwardsIsLive) {
logger.warn(
"Refusing to set a preceding existingTimeLine on our " +
"timeline as the existingTimeLine is live (" + existingTimeline + ")",
);
}
if (forwardsIsLive) {
logger.warn(
"Refusing to set our preceding timeline on a existingTimeLine " +
"as our timeline is live (" + timeline + ")",
);
}
continue; // abort splicing - try next event
}
timeline.setNeighbouringTimeline(existingTimeline, direction);
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
timeline = existingTimeline;
didUpdate = true;
}
@@ -418,6 +464,14 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
// new information, we update the pagination token for whatever
// timeline we ended up on.
if (lastEventWasNew || !didUpdate) {
if (direction === EventTimeline.FORWARDS && timeline === this._liveTimeline) {
logger.warn({lastEventWasNew, didUpdate}); // for debugging
logger.warn(
`Refusing to set forwards pagination token of live timeline ` +
`${timeline} to ${paginationToken}`,
);
return;
}
timeline.setPaginationToken(paginationToken, direction);
}
};
@@ -487,6 +541,9 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
this.setRelationsTarget(event);
this.aggregateRelations(event);
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
@@ -621,6 +678,126 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
return null;
};
/**
* Get a collection of relations to a given event in this timeline set.
*
* @param {String} eventId
* The ID of the event that you'd like to access relation events for.
* For example, with annotations, this would be the ID of the event being annotated.
* @param {String} relationType
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
*
* @returns {Relations}
* A container for relation events.
*/
EventTimelineSet.prototype.getRelationsForEvent = function(
eventId, relationType, eventType,
) {
if (!this._unstableClientRelationAggregation) {
throw new Error("Client-side relation aggregation is disabled");
}
if (!eventId || !relationType || !eventType) {
throw new Error("Invalid arguments for `getRelationsForEvent`");
}
// debuglog("Getting relations for: ", eventId, relationType, eventType);
const relationsForEvent = this._relations[eventId] || {};
const relationsWithRelType = relationsForEvent[relationType] || {};
return relationsWithRelType[eventType];
};
/**
* Set an event as the target event if any Relations exist for it already
*
* @param {MatrixEvent} event
* The event to check as relation target.
*/
EventTimelineSet.prototype.setRelationsTarget = function(event) {
if (!this._unstableClientRelationAggregation) {
return;
}
const relationsForEvent = this._relations[event.getId()];
if (!relationsForEvent) {
return;
}
// don't need it for non m.replace relations for now
const relationsWithRelType = relationsForEvent["m.replace"];
if (!relationsWithRelType) {
return;
}
// only doing replacements for messages for now (e.g. edits)
const relationsWithEventType = relationsWithRelType["m.room.message"];
if (relationsWithEventType) {
relationsWithEventType.setTargetEvent(event);
}
};
/**
* Add relation events to the relevant relation collection.
*
* @param {MatrixEvent} event
* The new relation event to be aggregated.
*/
EventTimelineSet.prototype.aggregateRelations = function(event) {
if (!this._unstableClientRelationAggregation) {
return;
}
if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
return;
}
// If the event is currently encrypted, wait until it has been decrypted.
if (event.isBeingDecrypted()) {
event.once("Event.decrypted", () => {
this.aggregateRelations(event);
});
return;
}
const relation = event.getRelation();
if (!relation) {
return;
}
const relatesToEventId = relation.event_id;
const relationType = relation.rel_type;
const eventType = event.getType();
// debuglog("Aggregating relation: ", event.getId(), eventType, relation);
let relationsForEvent = this._relations[relatesToEventId];
if (!relationsForEvent) {
relationsForEvent = this._relations[relatesToEventId] = {};
}
let relationsWithRelType = relationsForEvent[relationType];
if (!relationsWithRelType) {
relationsWithRelType = relationsForEvent[relationType] = {};
}
let relationsWithEventType = relationsWithRelType[eventType];
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.room,
);
const relatesToEvent = this.findEventById(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
}
}
relationsWithEventType.addEvent(event);
};
/**
* The EventTimelineSet class.
*/
+1 -1
View File
@@ -276,7 +276,7 @@ EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
if (this.getNeighbouringTimeline(direction)) {
throw new Error("timeline already has a neighbouring timeline - " +
"cannot reset neighbour");
"cannot reset neighbour (direction: " + direction + ")");
}
if (direction == EventTimeline.BACKWARDS) {
+355 -27
View File
@@ -24,13 +24,14 @@ limitations under the License.
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import utils from '../utils.js';
import logger from '../../src/logger';
/**
* Enum for event statuses.
* @readonly
* @enum {string}
*/
module.exports.EventStatus = {
const EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
@@ -48,8 +49,15 @@ module.exports.EventStatus = {
/** The event was cancelled before it was successfully sent. */
CANCELLED: "cancelled",
};
module.exports.EventStatus = EventStatus;
const interns = {};
function intern(str) {
if (!interns[str]) {
interns[str] = str;
}
return interns[str];
}
/**
* Construct a Matrix Event object
@@ -87,20 +95,25 @@ module.exports.MatrixEvent = function MatrixEvent(
if (!event[prop]) {
return;
}
if (!interns[event[prop]]) {
interns[event[prop]] = event[prop];
}
event[prop] = interns[event[prop]];
event[prop] = intern(event[prop]);
});
["membership", "avatar_url", "displayname"].forEach((prop) => {
if (!event.content || !event.content[prop]) {
return;
}
if (!interns[event.content[prop]]) {
interns[event.content[prop]] = event.content[prop];
event.content[prop] = intern(event.content[prop]);
});
["rel_type"].forEach((prop) => {
if (
!event.content ||
!event.content["m.relates_to"] ||
!event.content["m.relates_to"][prop]
) {
return;
}
event.content[prop] = interns[event.content[prop]];
event.content["m.relates_to"][prop] = intern(event.content["m.relates_to"][prop]);
});
this.event = event || {};
@@ -111,6 +124,9 @@ module.exports.MatrixEvent = function MatrixEvent(
this.error = null;
this.forwardLooking = true;
this._pushActions = null;
this._replacingEvent = null;
this._localRedactionEvent = null;
this._isCancelled = false;
this._clearEvent = {};
@@ -209,12 +225,33 @@ utils.extend(module.exports.MatrixEvent.prototype, {
},
/**
* Get the (decrypted, if necessary) event content JSON.
* Get the (decrypted, if necessary) event content JSON, even if the event
* was replaced by another event.
*
* @return {Object} The event content JSON, or an empty object.
*/
getOriginalContent: function() {
if (this._localRedactionEvent) {
return {};
}
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the (decrypted, if necessary) event content JSON,
* or the content from the replacing event, if any.
* See `makeReplaced`.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this._clearEvent.content || this.event.content || {};
if (this._localRedactionEvent) {
return {};
} else if (this._replacingEvent) {
return this._replacingEvent.getContent()["m.new_content"] || {};
} else {
return this.getOriginalContent();
}
},
/**
@@ -352,7 +389,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
if (
this._clearEvent && this._clearEvent.content &&
this._clearEvent.content.msgtype !== "m.bad.encrypted"
this._clearEvent.content.msgtype !== "m.bad.encrypted"
) {
// we may want to just ignore this? let's start with rejecting it.
throw new Error(
@@ -367,7 +404,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
// new info.
//
if (this._decryptionPromise) {
console.log(
logger.log(
`Event ${this.getId()} already being decrypted; queueing a retry`,
);
this._retryDecryption = true;
@@ -441,7 +478,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
console.error(
logger.error(
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
);
this._decryptionPromise = null;
@@ -467,7 +504,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
//
if (this._retryDecryption) {
// decryption error, but we have a retry queued.
console.log(
logger.log(
`Got error decrypting event (id=${this.getId()}: ` +
`${e}), but retrying`,
);
@@ -476,7 +513,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
// decryption error, no retries queued. Warn about the error and
// set it to m.bad.encrypted.
console.warn(
logger.warn(
`Error decrypting event (id=${this.getId()}): ${e.detailedString}`,
);
@@ -637,6 +674,27 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this.event.unsigned || {};
},
unmarkLocallyRedacted: function() {
const value = this._localRedactionEvent;
this._localRedactionEvent = null;
if (this.event.unsigned) {
this.event.unsigned.redacted_because = null;
}
return !!value;
},
markLocallyRedacted: function(redactionEvent) {
if (this._localRedactionEvent) {
return;
}
this.emit("Event.beforeRedaction", this, redactionEvent);
this._localRedactionEvent = redactionEvent;
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redactionEvent.event;
},
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
@@ -650,6 +708,11 @@ utils.extend(module.exports.MatrixEvent.prototype, {
throw new Error("invalid redaction_event in makeRedacted");
}
this._localRedactionEvent = null;
this.emit("Event.beforeRedaction", this, redaction_event);
this._replacingEvent = null;
// we attempt to replicate what we would see from the server if
// the event had been redacted before we saw it.
//
@@ -692,40 +755,305 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return Boolean(this.getUnsigned().redacted_because);
},
/**
* Check if this event is a redaction of another event
*
* @return {boolean} True if this event is a redaction
*/
isRedaction: function() {
return this.getType() === "m.room.redaction";
},
/**
* Get the push actions, if known, for this event
*
* @return {?Object} push actions
*/
getPushActions: function() {
getPushActions: function() {
return this._pushActions;
},
},
/**
* Set the push actions for this event.
*
* @param {Object} pushActions push actions
*/
setPushActions: function(pushActions) {
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
},
/**
* Replace the `event` property and recalculate any properties based on it.
* @param {Object} event the object to assign to the `event` property
*/
handleRemoteEcho: function(event) {
/**
* Replace the `event` property and recalculate any properties based on it.
* @param {Object} event the object to assign to the `event` property
*/
handleRemoteEcho: function(event) {
const oldUnsigned = this.getUnsigned();
const oldId = this.getId();
this.event = event;
// if this event was redacted before it was sent, it's locally marked as redacted.
// At this point, we've received the remote echo for the event, but not yet for
// the redaction that we are sending ourselves. Preserve the locally redacted
// state by copying over redacted_because so we don't get a flash of
// redacted, not-redacted, redacted as remote echos come in
if (oldUnsigned.redacted_because) {
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = oldUnsigned.redacted_because;
}
// successfully sent.
this.status = null;
},
this.setStatus(null);
if (this.getId() !== oldId) {
// emit the event if it changed
this.emit("Event.localEventIdReplaced", this);
}
},
/**
* Whether the event is in any phase of sending, send failure, waiting for
* remote echo, etc.
*
* @return {boolean}
*/
isSending() {
return !!this.status;
},
/**
* Update the event's sending status and emit an event as well.
*
* @param {String} status The new status
*/
setStatus(status) {
this.status = status;
this.emit("Event.status", this, status);
},
replaceLocalEventId(eventId) {
this.event.event_id = eventId;
this.emit("Event.localEventIdReplaced", this);
},
/**
* Get whether the event is a relation event, and of a given type if
* `relType` is passed in.
*
* @param {string?} relType if given, checks that the relation is of the
* given type
* @return {boolean}
*/
isRelation(relType = undefined) {
// Relation info is lifted out of the encrypted content when sent to
// encrypted rooms, so we have to check `getWireContent` for this.
const content = this.getWireContent();
const relation = content && content["m.relates_to"];
return relation && relation.rel_type && relation.event_id &&
((relType && relation.rel_type === relType) || !relType);
},
/**
* Get relation info for the event, if any.
*
* @return {Object}
*/
getRelation() {
if (!this.isRelation()) {
return null;
}
return this.getWireContent()["m.relates_to"];
},
/**
* Set an event that replaces the content of this event, through an m.replace relation.
*
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
*/
makeReplaced(newEvent) {
// don't allow redacted events to be replaced.
// if newEvent is null we allow to go through though,
// as with local redaction, the replacing event might get
// cancelled, which should be reflected on the target event.
if (this.isRedacted() && newEvent) {
return;
}
if (this._replacingEvent !== newEvent) {
this._replacingEvent = newEvent;
this.emit("Event.replaced", this);
}
},
/**
* Returns the status of any associated edit or redaction
* (not for reactions/annotations as their local echo doesn't affect the orignal event),
* or else the status of the event.
*
* @return {EventStatus}
*/
getAssociatedStatus() {
if (this._replacingEvent) {
return this._replacingEvent.status;
} else if (this._localRedactionEvent) {
return this._localRedactionEvent.status;
}
return this.status;
},
getServerAggregatedRelation(relType) {
const relations = this.getUnsigned()["m.relations"];
if (relations) {
return relations[relType];
}
},
/**
* Returns the event ID of the event replacing the content of this event, if any.
*
* @return {string?}
*/
replacingEventId() {
const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
return replaceRelation.event_id;
} else if (this._replacingEvent) {
return this._replacingEvent.getId();
}
},
/**
* Returns the event replacing the content of this event, if any.
* Replacements are aggregated on the server, so this would only
* return an event in case it came down the sync, or for local echo of edits.
*
* @return {MatrixEvent?}
*/
replacingEvent() {
return this._replacingEvent;
},
/**
* Returns the origin_server_ts of the event replacing the content of this event, if any.
*
* @return {Date?}
*/
replacingEventDate() {
const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
const ts = replaceRelation.origin_server_ts;
if (Number.isFinite(ts)) {
return new Date(ts);
}
} else if (this._replacingEvent) {
return this._replacingEvent.getDate();
}
},
/**
* Returns the event that wants to redact this event, but hasn't been sent yet.
* @return {MatrixEvent} the event
*/
localRedactionEvent() {
return this._localRedactionEvent;
},
/**
* For relations and redactions, returns the event_id this event is referring to.
*
* @return {string?}
*/
getAssociatedId() {
const relation = this.getRelation();
if (relation) {
return relation.event_id;
} else if (this.isRedaction()) {
return this.event.redacts;
}
},
/**
* Checks if this event is associated with another event. See `getAssociatedId`.
*
* @return {bool}
*/
hasAssocation() {
return !!this.getAssociatedId();
},
/**
* Update the related id with a new one.
*
* Used to replace a local id with remote one before sending
* an event with a related id.
*
* @param {string} eventId the new event id
*/
updateAssociatedId(eventId) {
const relation = this.getRelation();
if (relation) {
relation.event_id = eventId;
} else if (this.isRedaction()) {
this.event.redacts = eventId;
}
},
/**
* Flags an event as cancelled due to future conditions. For example, a verification
* request event in the same sync transaction may be flagged as cancelled to warn
* listeners that a cancellation event is coming down the same pipe shortly.
* @param {boolean} cancelled Whether the event is to be cancelled or not.
*/
flagCancelled(cancelled = true) {
this._isCancelled = cancelled;
},
/**
* Gets whether or not the event is flagged as cancelled. See flagCancelled() for
* more information.
* @returns {boolean} True if the event is cancelled, false otherwise.
*/
isCancelled() {
return this._isCancelled;
},
/**
* Summarise the event as JSON for debugging. If encrypted, include both the
* decrypted and encrypted view of the event. This is named `toJSON` for use
* with `JSON.stringify` which checks objects for functions named `toJSON`
* and will call them to customise the output if they are defined.
*
* @return {Object}
*/
toJSON() {
const event = {
type: this.getType(),
sender: this.getSender(),
content: this.getContent(),
event_id: this.getId(),
origin_server_ts: this.getTs(),
unsigned: this.getUnsigned(),
room_id: this.getRoomId(),
};
// if this is a redaction then attach the redacts key
if (this.isRedaction()) {
event.redacts = this.event.redacts;
}
if (!this.isEncrypted()) {
return event;
}
return {
decrypted: event,
encrypted: this.event,
};
},
});
/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted
*
* This is specified here:
* http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#redactions
* http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions
*
* Also:
* - We keep 'unsigned' since that is created by the local server
+342
View File
@@ -0,0 +1,342 @@
/*
Copyright 2019 New Vector Ltd
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 EventEmitter from 'events';
import { EventStatus } from '../../lib/models/event';
/**
* A container for relation events that supports easy access to common ways of
* aggregating such events. Each instance holds events that of a single relation
* type and event type. All of the events also relate to the same original event.
*
* The typical way to get one of these containers is via
* EventTimelineSet#getRelationsForEvent.
*/
export default class Relations extends EventEmitter {
/**
* @param {String} relationType
* The type of relation involved, such as "m.annotation", "m.reference",
* "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
* @param {?Room} room
* Room for this container. May be null for non-room cases, such as the
* notification timeline.
*/
constructor(relationType, eventType, room) {
super();
this.relationType = relationType;
this.eventType = eventType;
this._relations = new Set();
this._annotationsByKey = {};
this._annotationsBySender = {};
this._sortedAnnotationsByKey = [];
this._targetEvent = null;
}
/**
* Add relation events to this collection.
*
* @param {MatrixEvent} event
* The new relation event to be added.
*/
addEvent(event) {
if (this._relations.has(event)) {
return;
}
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
return;
}
const relationType = relation.rel_type;
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
return;
}
// If the event is in the process of being sent, listen for cancellation
// so we can remove the event from the collection.
if (event.isSending()) {
event.on("Event.status", this._onEventStatus);
}
this._relations.add(event);
if (this.relationType === "m.annotation") {
this._addAnnotationToAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
event.on("Event.beforeRedaction", this._onBeforeRedaction);
this.emit("Relations.add", event);
}
/**
* Remove relation event from this collection.
*
* @param {MatrixEvent} event
* The relation event to remove.
*/
_removeEvent(event) {
if (!this._relations.has(event)) {
return;
}
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
return;
}
const relationType = relation.rel_type;
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
return;
}
this._relations.delete(event);
if (this.relationType === "m.annotation") {
this._removeAnnotationFromAggregation(event);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
this.emit("Relations.remove", event);
}
/**
* Listens for event status changes to remove cancelled events.
*
* @param {MatrixEvent} event The event whose status has changed
* @param {EventStatus} status The new status
*/
_onEventStatus = (event, status) => {
if (!event.isSending()) {
// Sending is done, so we don't need to listen anymore
event.removeListener("Event.status", this._onEventStatus);
return;
}
if (status !== EventStatus.CANCELLED) {
return;
}
// Event was cancelled, remove from the collection
event.removeListener("Event.status", this._onEventStatus);
this._removeEvent(event);
}
/**
* Get all relation events in this collection.
*
* These are currently in the order of insertion to this collection, which
* won't match timeline order in the case of scrollback.
* TODO: Tweak `addEvent` to insert correctly for scrollback.
*
* @return {Array}
* Relation events in insertion order.
*/
getRelations() {
return [...this._relations];
}
_addAnnotationToAggregation(event) {
const { key } = event.getRelation();
if (!key) {
return;
}
let eventsForKey = this._annotationsByKey[key];
if (!eventsForKey) {
eventsForKey = this._annotationsByKey[key] = new Set();
this._sortedAnnotationsByKey.push([key, eventsForKey]);
}
// Add the new event to the set for this key
eventsForKey.add(event);
// Re-sort the [key, events] pairs in descending order of event count
this._sortedAnnotationsByKey.sort((a, b) => {
const aEvents = a[1];
const bEvents = b[1];
return bEvents.size - aEvents.size;
});
const sender = event.getSender();
let eventsFromSender = this._annotationsBySender[sender];
if (!eventsFromSender) {
eventsFromSender = this._annotationsBySender[sender] = new Set();
}
// Add the new event to the set for this sender
eventsFromSender.add(event);
}
_removeAnnotationFromAggregation(event) {
const { key } = event.getRelation();
if (!key) {
return;
}
const eventsForKey = this._annotationsByKey[key];
if (eventsForKey) {
eventsForKey.delete(event);
// Re-sort the [key, events] pairs in descending order of event count
this._sortedAnnotationsByKey.sort((a, b) => {
const aEvents = a[1];
const bEvents = b[1];
return bEvents.size - aEvents.size;
});
}
const sender = event.getSender();
const eventsFromSender = this._annotationsBySender[sender];
if (eventsFromSender) {
eventsFromSender.delete(event);
}
}
/**
* For relations that have been redacted, we want to remove them from
* aggregation data sets and emit an update event.
*
* To do so, we listen for `Event.beforeRedaction`, which happens:
* - after the server accepted the redaction and remote echoed back to us
* - before the original event has been marked redacted in the client
*
* @param {MatrixEvent} redactedEvent
* The original relation event that is about to be redacted.
*/
_onBeforeRedaction = (redactedEvent) => {
if (!this._relations.has(redactedEvent)) {
return;
}
this._relations.delete(redactedEvent);
if (this.relationType === "m.annotation") {
// Remove the redacted annotation from aggregation by key
this._removeAnnotationFromAggregation(redactedEvent);
} else if (this.relationType === "m.replace" && this._targetEvent) {
this._targetEvent.makeReplaced(this.getLastReplacement());
}
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
this.emit("Relations.redaction");
}
/**
* Get all events in this collection grouped by key and sorted by descending
* event count in each group.
*
* This is currently only supported for the annotation relation type.
*
* @return {Array}
* An array of [key, events] pairs sorted by descending event count.
* The events are stored in a Set (which preserves insertion order).
*/
getSortedAnnotationsByKey() {
if (this.relationType !== "m.annotation") {
// Other relation types are not grouped currently.
return null;
}
return this._sortedAnnotationsByKey;
}
/**
* Get all events in this collection grouped by sender.
*
* This is currently only supported for the annotation relation type.
*
* @return {Object}
* An object with each relation sender as a key and the matching Set of
* events for that sender as a value.
*/
getAnnotationsBySender() {
if (this.relationType !== "m.annotation") {
// Other relation types are not grouped currently.
return null;
}
return this._annotationsBySender;
}
/**
* Returns the most recent (and allowed) m.replace relation, if any.
*
* This is currently only supported for the m.replace relation type,
* once the target event is known, see `addEvent`.
*
* @return {MatrixEvent?}
*/
getLastReplacement() {
if (this.relationType !== "m.replace") {
// Aggregating on last only makes sense for this relation type
return null;
}
if (!this._targetEvent) {
// Don't know which replacements to accept yet.
// This method shouldn't be called before the original
// event is known anyway.
return null;
}
// the all-knowning server tells us that the event at some point had
// this timestamp for its replacement, so any following replacement should definitely not be less
const replaceRelation =
this._targetEvent.getServerAggregatedRelation("m.replace");
const minTs = replaceRelation && replaceRelation.origin_server_ts;
return this.getRelations().reduce((last, event) => {
if (event.getSender() !== this._targetEvent.getSender()) {
return last;
}
if (minTs && minTs > event.getTs()) {
return last;
}
if (last && last.getTs() > event.getTs()) {
return last;
}
return event;
}, null);
}
/*
* @param {MatrixEvent} targetEvent the event the relations are related to.
*/
setTargetEvent(event) {
if (this._targetEvent) {
return;
}
this._targetEvent = event;
if (this.relationType === "m.replace") {
const replacement = this.getLastReplacement();
// this is the initial update, so only call it if we already have something
// to not emit Event.replaced needlessly
if (replacement) {
this._targetEvent.makeReplaced(replacement);
}
}
}
}
+1 -1
View File
@@ -249,7 +249,7 @@ RoomMember.prototype.getDMInviter = function() {
* "crop" or "scale".
* @param {Boolean} allowDefault (optional) Passing false causes this method to
* return null if the user has no avatar image. Otherwise, a default image URL
* will be returned. Default: true.
* will be returned. Default: true. (Deprecated)
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
* returned even if it is a direct hyperlink rather than a matrix content URL.
* If false, any non-matrix content URLs will be ignored. Setting this option to
+4 -3
View File
@@ -21,6 +21,7 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const RoomMember = require("./room-member");
import logger from '../../src/logger';
// possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1;
@@ -447,7 +448,7 @@ RoomState.prototype.clearOutOfBandMembers = function() {
delete this.members[userId];
}
});
console.log(`LL: RoomState removed ${count} members...`);
logger.log(`LL: RoomState removed ${count} members...`);
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
@@ -456,11 +457,11 @@ RoomState.prototype.clearOutOfBandMembers = function() {
* @param {MatrixEvent[]} stateEvents array of membership state events
*/
RoomState.prototype.setOutOfBandMembers = function(stateEvents) {
console.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
console.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
logger.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
this._oobMemberFlags.status = OOB_STATUS_FINISHED;
stateEvents.forEach((e) => this._setOutOfBandMember(e));
};
+211 -58
View File
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 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.
@@ -29,6 +30,7 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import logger from '../../src/logger';
import ReEmitter from '../ReEmitter';
// These constants are used as sane defaults when the homeserver doesn't support
@@ -37,8 +39,8 @@ import ReEmitter from '../ReEmitter';
// room versions which are considered okay for people to run without being asked
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
// return an m.room_versions capability.
const KNOWN_SAFE_ROOM_VERSION = '1';
const SAFE_ROOM_VERSIONS = ['1', '2'];
const KNOWN_SAFE_ROOM_VERSION = '4';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4'];
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -92,9 +94,12 @@ function synthesizeReceipt(userId, event, receiptType) {
* "<b>detached</b>", pending messages will appear in a separate list,
* accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
* timeline support.
* @param {boolean} [opts.unstableClientRelationAggregation = false]
* Optional. Set to true to enable client-side aggregation of event relations
* via `EventTimelineSet#getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
@@ -206,7 +211,7 @@ utils.inherits(Room, EventEmitter);
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
console.warn("Room " + this.room_id + " does not have an m.room.create event");
logger.warn("Room " + this.room_id + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version'];
@@ -259,7 +264,36 @@ Room.prototype.getRecommendedVersion = async function() {
}
}
let result = this._checkVersionAgainstCapability(versionCap);
if (result.urgent && result.needsUpgrade) {
// Something doesn't feel right: we shouldn't need to update
// because the version we're on should be in the protocol's
// namespace. This usually means that the server was updated
// before the client was, making us think the newest possible
// room version is not stable. As a solution, we'll refresh
// the capability we're using to determine this.
logger.warn(
"Refreshing room version capability because the server looks " +
"to be supporting a newer room version we don't know about.",
);
const caps = await this._client.getCapabilities(true);
versionCap = caps["m.room_versions"];
if (!versionCap) {
logger.warn("No room version capability - assuming upgrade required.");
return result;
} else {
result = this._checkVersionAgainstCapability(versionCap);
}
}
return result;
};
Room.prototype._checkVersionAgainstCapability = function(versionCap) {
const currentVersion = this.getVersion();
logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
logger.log(`[${this.roomId}] Version capability: `, versionCap);
const result = {
version: currentVersion,
@@ -268,7 +302,7 @@ Room.prototype.getRecommendedVersion = async function() {
};
// If the room is on the default version then nothing needs to change
if (currentVersion === versionCap.default) return Promise.resolve(result);
if (currentVersion === versionCap.default) return result;
const stableVersions = Object.keys(versionCap.available)
.filter((v) => versionCap.available[v] === 'stable');
@@ -280,12 +314,17 @@ Room.prototype.getRecommendedVersion = async function() {
result.version = versionCap.default;
result.needsUpgrade = true;
result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
return Promise.resolve(result);
if (result.urgent) {
logger.warn(`URGENT upgrade required on ${this.roomId}`);
} else {
logger.warn(`Non-urgent upgrade required on ${this.roomId}`);
}
return result;
}
// The room is on a stable, but non-default, version by this point.
// No upgrade needed.
return Promise.resolve(result);
return result;
};
/**
@@ -308,13 +347,30 @@ Room.prototype.userMayUpgradeRoom = function(userId) {
Room.prototype.getPendingEvents = function() {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call getPendingEventList with pendingEventOrdering == " +
"Cannot call getPendingEvents with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList;
};
/**
* Check whether the pending event list contains a given event by ID.
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
Room.prototype.hasPendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call hasPendingEvent with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList.some(event => event.getId() === eventId);
};
/**
* Get the live unfiltered timeline for this room.
*
@@ -461,7 +517,7 @@ Room.prototype._loadMembers = async function() {
if (rawMembersEvents === null) {
fromServer = true;
rawMembersEvents = await this._loadMembersFromServer();
console.log(`LL: got ${rawMembersEvents.length} ` +
logger.log(`LL: got ${rawMembersEvents.length} ` +
`members from server for room ${this.roomId}`);
}
const memberEvents = rawMembersEvents.map(this._client.getEventMapper());
@@ -489,7 +545,7 @@ Room.prototype.loadMembersIfNeeded = function() {
const inMemoryUpdate = this._loadMembers().then((result) => {
this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isRoomEncrypted(this.roomId)) {
if (this._client.isCryptoEnabled() && this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
@@ -505,21 +561,21 @@ Room.prototype.loadMembersIfNeeded = function() {
const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand())
.map((m) => m.events.member.event);
console.log(`LL: telling store to write ${oobMembers.length}`
logger.log(`LL: telling store to write ${oobMembers.length}`
+ ` members for room ${this.roomId}`);
const store = this._client.store;
return store.setOutOfBandMembers(this.roomId, oobMembers)
// swallow any IDB error as we don't want to fail
// because of this
.catch((err) => {
console.log("LL: storing OOB room members failed, oh well",
logger.log("LL: storing OOB room members failed, oh well",
err);
});
}
}).catch((err) => {
// as this is not awaited anywhere,
// at least show the error in the console
console.error(err);
logger.error(err);
});
this._membersPromise = inMemoryUpdate;
@@ -545,9 +601,9 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() {
*/
Room.prototype._cleanupAfterLeaving = function() {
this.clearLoadedMembersIfNeeded().catch((err) => {
console.error(`error after clearing loaded members from ` +
logger.error(`error after clearing loaded members from ` +
`room ${this.roomId} after leaving`);
console.dir(err);
logger.log(err);
});
};
@@ -591,6 +647,11 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
/**
* Returns whether there are any devices in the room that are unverified
*
* Note: Callers should first check if crypto is enabled on this device. If it is
* disabled, then we aren't tracking room devices at all, so we can't answer this, and an
* error will be thrown.
*
* @return {bool} the result
*/
Room.prototype.hasUnverifiedDevices = async function() {
@@ -720,7 +781,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {boolean} allowDefault True to allow an identicon for this room if an
* avatar URL wasn't explicitly set. Default: true.
* avatar URL wasn't explicitly set. Default: true. (Deprecated)
* @return {?string} the avatar URL or null.
*/
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
@@ -754,20 +815,20 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
* @return {array} The room's alias as an array of strings
*/
Room.prototype.getAliases = function() {
const alias_strings = [];
const aliasStrings = [];
const alias_events = this.currentState.getStateEvents("m.room.aliases");
if (alias_events) {
for (let i = 0; i < alias_events.length; ++i) {
const alias_event = alias_events[i];
if (utils.isArray(alias_event.getContent().aliases)) {
const aliasEvents = this.currentState.getStateEvents("m.room.aliases");
if (aliasEvents) {
for (let i = 0; i < aliasEvents.length; ++i) {
const aliasEvent = aliasEvents[i];
if (utils.isArray(aliasEvent.getContent().aliases)) {
Array.prototype.push.apply(
alias_strings, alias_event.getContent().aliases,
aliasStrings, aliasEvent.getContent().aliases,
);
}
}
}
return alias_strings;
return aliasStrings;
};
/**
@@ -989,14 +1050,25 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
* @private
*/
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
let i;
if (event.getType() === "m.room.redaction") {
if (event.isRedaction()) {
const redactId = event.event.redacts;
// if we know about this event, redact its contents now.
const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(event);
// If this is in the current state, replace it with the redacted version
if (redactedEvent.getStateKey()) {
const currentStateEvent = this.currentState.getStateEvents(
redactedEvent.getType(),
redactedEvent.getStateKey(),
);
if (currentStateEvent.getId() === redactedEvent.getId()) {
this.currentState.setStateEvents([redactedEvent]);
}
}
this.emit("Room.redaction", event, this);
// TODO: we stash user displaynames (among other things) in
@@ -1023,7 +1095,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
}
// add to our timeline sets
for (i = 0; i < this._timelineSets.length; i++) {
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
}
@@ -1087,15 +1159,35 @@ Room.prototype.addPendingEvent = function(event, txnId) {
if (this._opts.pendingEventOrdering == "detached") {
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
console.warn("Setting event as NOT_SENT due to messages in the same state");
event.status = EventStatus.NOT_SENT;
logger.warn("Setting event as NOT_SENT due to messages in the same state");
event.setStatus(EventStatus.NOT_SENT);
}
this._pendingEventList.push(event);
if (event.isRelation()) {
// For pending events, add them to the relations collection immediately.
// (The alternate case below already covers this as part of adding to
// the timeline set.)
this._aggregateNonLiveRelation(event);
}
if (event.isRedaction()) {
const redactId = event.event.redacts;
let redactedEvent = this._pendingEventList &&
this._pendingEventList.find(e => e.getId() === redactId);
if (!redactedEvent) {
redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
}
if (redactedEvent) {
redactedEvent.markLocallyRedacted(event);
this.emit("Room.redaction", event, this);
}
}
} else {
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
@@ -1108,6 +1200,30 @@ Room.prototype.addPendingEvent = function(event, txnId) {
this.emit("Room.localEchoUpdated", event, this, null, null);
};
/**
* Used to aggregate the local echo for a relation, and also
* for re-applying a relation after it's redaction has been cancelled,
* as the local echo for the redaction of the relation would have
* un-aggregated the relation. Note that this is different from regular messages,
* which are just kept detached for their local echo.
*
* Also note that live events are aggregated in the live EventTimelineSet.
* @param {module:models/event.MatrixEvent} event the relation event that needs to be aggregated.
*/
Room.prototype._aggregateNonLiveRelation = function(event) {
// TODO: We should consider whether this means it would be a better
// design to lift the relations handling up to the room instead.
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.aggregateRelations(event);
}
} else {
timelineSet.aggregateRelations(event);
}
}
};
/**
* Deal with the echo of a message we sent.
@@ -1129,7 +1245,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
const oldStatus = localEvent.status;
// no longer pending
delete this._txnToEvent[remoteEvent.transaction_id];
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];
// if it's in the pending list, remove it
if (this._pendingEventList) {
@@ -1197,7 +1313,7 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
console.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
@@ -1229,11 +1345,11 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
newStatus);
}
event.status = newStatus;
event.setStatus(newStatus);
if (newStatus == EventStatus.SENT) {
// update the event id
event.event.event_id = newEventId;
event.replaceLocalEventId(newEventId);
// if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
@@ -1244,12 +1360,13 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
} else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline.
if (this._pendingEventList) {
utils.removeElement(
this._pendingEventList,
function(ev) {
return ev.getId() == oldEventId;
}, false,
);
const idx = this._pendingEventList.findIndex(ev => ev.getId() === oldEventId);
if (idx !== -1) {
const [removedEvent] = this._pendingEventList.splice(idx, 1);
if (removedEvent.isRedaction()) {
this._revertRedactionLocalEcho(removedEvent);
}
}
}
this.removeEvent(oldEventId);
}
@@ -1257,6 +1374,23 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
};
Room.prototype._revertRedactionLocalEcho = function(redactionEvent) {
const redactId = redactionEvent.event.redacts;
if (!redactId) {
return;
}
const redactedEvent = this.getUnfilteredTimelineSet()
.findEventById(redactId);
if (redactedEvent) {
redactedEvent.unmarkLocallyRedacted();
// re-render after undoing redaction
this.emit("Room.redactionCancelled", redactionEvent, this);
// reapply relation now redaction failed
if (redactedEvent.isRelation()) {
this._aggregateNonLiveRelation(redactedEvent);
}
}
};
/**
* Add some events to this room. This can include state events, message
@@ -1298,28 +1432,33 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
}
for (i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]);
} else if (events[i].getType() === "m.receipt") {
this.addReceipt(events[i]);
}
// N.B. account_data is added directly by /sync to avoid
// having to maintain an event.isAccountData() here
else {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvent(events[i], duplicateStrategy);
}
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvent(events[i], duplicateStrategy);
}
};
/**
* Adds/handles ephemeral events such as typing notifications and read receipts.
* @param {MatrixEvent[]} events A list of events to process
*/
Room.prototype.addEphemeralEvents = function(events) {
for (const event of events) {
if (event.getType() === 'm.typing') {
this.currentState.setTypingEvent(event);
} else if (event.getType() === 'm.receipt') {
this.addReceipt(event);
} // else ignore - life is too short for us to care about these events
}
};
/**
* Removes events from this room.
* @param {String[]} event_ids A list of event_ids to remove.
* @param {String[]} eventIds A list of eventIds to remove.
*/
Room.prototype.removeEvents = function(event_ids) {
for (let i = 0; i < event_ids.length; ++i) {
this.removeEvent(event_ids[i]);
Room.prototype.removeEvents = function(eventIds) {
for (let i = 0; i < eventIds.length; ++i) {
this.removeEvent(eventIds[i]);
}
};
@@ -1335,6 +1474,9 @@ Room.prototype.removeEvent = function(eventId) {
for (let i = 0; i < this._timelineSets.length; i++) {
const removed = this._timelineSets[i].removeEvent(eventId);
if (removed) {
if (removed.isRedaction()) {
this._revertRedactionLocalEcho(removed);
}
removedAny = true;
}
}
@@ -1758,10 +1900,21 @@ module.exports = Room;
* event).
*
* @event module:client~MatrixClient#"Room.redaction"
* @param {MatrixEvent} event The matrix event which was redacted
* @param {MatrixEvent} event The matrix redaction event
* @param {Room} room The room containing the redacted event
*/
/**
* Fires when an event that was previously redacted isn't anymore.
* This happens when the redaction couldn't be sent and
* was subsequently cancelled by the user. Redactions have a local echo
* which is undone in this scenario.
*
* @event module:client~MatrixClient#"Room.redactionCancelled"
* @param {MatrixEvent} event The matrix redaction event that was cancelled.
* @param {Room} room The room containing the unredacted event
*/
/**
* Fires whenever the name of a room is updated.
* @event module:client~MatrixClient#"Room.name"
+113 -1
View File
@@ -23,6 +23,58 @@ import {escapeRegExp, globToRegexp} from "./utils";
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
// The default override rules to apply when calculating actions for an event. These
// defaults apply under no other circumstances to avoid confusing the client with server
// state. We do this for two reasons:
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
// more details.
// 2. We often want to start using push rules ahead of the server supporting them,
// and so we can put them here.
const DEFAULT_OVERRIDE_RULES = [
{
// For homeservers which don't support MSC1930 yet
rule_id: ".m.rule.tombstone",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.room.tombstone",
},
{
kind: "event_match",
key: "state_key",
pattern: "",
},
],
actions: [
"notify",
{
set_tweak: "highlight",
value: true,
},
],
},
{
// For homeservers which don't support MSC2153 yet
rule_id: ".m.rule.reaction",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.reaction",
},
],
actions: [
"dont_notify",
],
},
];
/**
* Construct a Push Processor.
* @constructor
@@ -312,6 +364,33 @@ function PushProcessor(client) {
return actionObj;
};
const applyRuleDefaults = function(clientRuleset) {
// Deep clone the object before we mutate it
const ruleset = JSON.parse(JSON.stringify(clientRuleset));
if (!clientRuleset['global']) {
clientRuleset['global'] = {};
}
if (!clientRuleset['global']['override']) {
clientRuleset['global']['override'] = [];
}
// Apply default overrides
const globalOverrides = clientRuleset['global']['override'];
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (!existingRule) {
const ruleId = override.rule_id;
console.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return ruleset;
};
this.ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
@@ -331,7 +410,8 @@ function PushProcessor(client) {
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
const rules = applyRuleDefaults(client.pushRules);
return pushActionsForEventAndRulesets(ev, rules);
};
/**
@@ -380,6 +460,38 @@ PushProcessor.actionListToActionsObject = function(actionlist) {
return actionobj;
};
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
PushProcessor.rewriteDefaultRules = function(incomingRules) {
let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {};
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Fix default override rules
newRules.global.override = newRules.global.override.map(r => {
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
if (!defaultRule) return r;
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
r.default = defaultRule.default;
r.conditions = defaultRule.conditions;
r.actions = defaultRule.actions;
return r;
});
return newRules;
};
/**
* @typedef {Object} PushAction
* @type {Object}
+3 -2
View File
@@ -24,6 +24,7 @@ limitations under the License.
*/
"use strict";
import logger from '../src/logger';
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
@@ -39,7 +40,7 @@ let _realCallbackKey;
// each is an object with keys [runAt, func, params, key].
const _callbackList = [];
// var debuglog = console.log.bind(console);
// var debuglog = logger.log.bind(logger);
const debuglog = function() {};
/**
@@ -170,7 +171,7 @@ function _runCallbacks() {
try {
cb.func.apply(global, cb.params);
} catch (e) {
console.error("Uncaught exception in callback function",
logger.error("Uncaught exception in callback function",
e.stack || e);
}
}
+12 -3
View File
@@ -21,6 +21,7 @@ limitations under the License.
*/
const utils = require("./utils");
import Promise from 'bluebird';
import logger from '../src/logger';
const DEBUG = false; // set true to enable console logging.
@@ -176,7 +177,8 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
* @see module:scheduler~queueAlgorithm
*/
MatrixScheduler.QUEUE_MESSAGES = function(event) {
if (event.getType() === "m.room.message") {
// enqueue messages or events that associate with another event (redactions and relations)
if (event.getType() === "m.room.message" || event.hasAssocation()) {
// put these events in the 'message' queue.
return "message";
}
@@ -219,7 +221,14 @@ function _processQueue(scheduler, queueName) {
);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
scheduler._procFn(obj.event).done(function(res) {
// First wait for a resolved promise, so the resolve handlers for
// the deferred of the previously sent event can run.
// This way enqueued relations/redactions to enqueued events can receive
// the remove id of their target before being sent.
Promise.resolve().then(() => {
return scheduler._procFn(obj.event);
}).then(function(res) {
// remove this from the queue
_removeNextEvent(scheduler, queueName);
debuglog("Queue '%s' sent event %s", queueName, obj.event.getId());
@@ -269,7 +278,7 @@ function _removeNextEvent(scheduler, queueName) {
function debuglog() {
if (DEBUG) {
console.log(...arguments);
logger.log(...arguments);
}
}
+20
View File
@@ -0,0 +1,20 @@
/*
Copyright 2019 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.
*/
export const SERVICE_TYPES = Object.freeze({
IS: 'SERVICE_TYPE_IS', // An Identity Service
IM: 'SERVICE_TYPE_IM', // An Integration Manager
});
+27 -21
View File
@@ -18,6 +18,8 @@ limitations under the License.
import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
import * as IndexedDBHelpers from "../indexeddb-helpers";
import logger from '../../src/logger';
const VERSION = 3;
@@ -132,6 +134,10 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
this._isNewlyCreated = false;
};
LocalIndexedDBStoreBackend.exists = function(indexedDB, dbName) {
dbName = "matrix-js-sdk:" + (dbName || "default");
return IndexedDBHelpers.exists(indexedDB, dbName);
};
LocalIndexedDBStoreBackend.prototype = {
/**
@@ -141,7 +147,7 @@ LocalIndexedDBStoreBackend.prototype = {
*/
connect: function() {
if (!this._disconnected) {
console.log(
logger.log(
`LocalIndexedDBStoreBackend.connect: already connected or connecting`,
);
return Promise.resolve();
@@ -149,14 +155,14 @@ LocalIndexedDBStoreBackend.prototype = {
this._disconnected = false;
console.log(
logger.log(
`LocalIndexedDBStoreBackend.connect: connecting...`,
);
const req = this.indexedDB.open(this._dbName, VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
console.log(
logger.log(
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
);
if (oldVersion < 1) { // The database did not previously exist.
@@ -173,16 +179,16 @@ LocalIndexedDBStoreBackend.prototype = {
};
req.onblocked = () => {
console.log(
logger.log(
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
);
};
console.log(
logger.log(
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
);
return reqAsEventPromise(req).then((ev) => {
console.log(
logger.log(
`LocalIndexedDBStoreBackend.connect: connected`,
);
this.db = ev.target.result;
@@ -210,7 +216,7 @@ LocalIndexedDBStoreBackend.prototype = {
this._loadAccountData(),
this._loadSyncData(),
]).then(([accountData, syncData]) => {
console.log(
logger.log(
`LocalIndexedDBStoreBackend: loaded initial data`,
);
this._syncAccumulator.accumulate({
@@ -268,7 +274,7 @@ LocalIndexedDBStoreBackend.prototype = {
reject(err);
};
}).then((events) => {
console.log(`LL: got ${events && events.length}` +
logger.log(`LL: got ${events && events.length}` +
` membershipEvents from storage for room ${roomId} ...`);
return events;
});
@@ -282,7 +288,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @param {event[]} membershipEvents the membership events to store
*/
setOutOfBandMembers: async function(roomId, membershipEvents) {
console.log(`LL: backend about to store ${membershipEvents.length}` +
logger.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`);
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
const store = tx.objectStore("oob_membership_events");
@@ -301,7 +307,7 @@ LocalIndexedDBStoreBackend.prototype = {
};
store.put(markerObject);
await txnAsPromise(tx);
console.log(`LL: backend done storing for ${roomId}!`);
logger.log(`LL: backend done storing for ${roomId}!`);
},
clearOutOfBandMembers: async function(roomId) {
@@ -336,7 +342,7 @@ LocalIndexedDBStoreBackend.prototype = {
[roomId, maxStateKey],
);
console.log(`LL: Deleting all users + marker in storage for ` +
logger.log(`LL: Deleting all users + marker in storage for ` +
`room ${roomId}, with key range:`,
[roomId, minStateKey], [roomId, maxStateKey]);
await reqAsPromise(writeStore.delete(membersKeyRange));
@@ -349,11 +355,11 @@ LocalIndexedDBStoreBackend.prototype = {
*/
clearDatabase: function() {
return new Promise((resolve, reject) => {
console.log(`Removing indexeddb instance: ${this._dbName}`);
logger.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this.indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
console.log(
logger.log(
`can't yet delete indexeddb ${this._dbName}` +
` because it is open elsewhere`,
);
@@ -363,14 +369,14 @@ LocalIndexedDBStoreBackend.prototype = {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that we can still
// use the app.
console.warn(
logger.warn(
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
);
resolve();
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
logger.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
});
@@ -429,7 +435,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData, groupsData) {
console.log("Persisting sync data up to ", nextBatch);
logger.log("Persisting sync data up to ", nextBatch);
return Promise.try(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
@@ -503,7 +509,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise<Object[]>} A list of raw global account events.
*/
_loadAccountData: function() {
console.log(
logger.log(
`LocalIndexedDBStoreBackend: loading account data...`,
);
return Promise.try(() => {
@@ -512,7 +518,7 @@ LocalIndexedDBStoreBackend.prototype = {
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((result) => {
console.log(
logger.log(
`LocalIndexedDBStoreBackend: loaded account data`,
);
return result;
@@ -525,7 +531,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
*/
_loadSyncData: function() {
console.log(
logger.log(
`LocalIndexedDBStoreBackend: loading sync data...`,
);
return Promise.try(() => {
@@ -534,11 +540,11 @@ LocalIndexedDBStoreBackend.prototype = {
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((results) => {
console.log(
logger.log(
`LocalIndexedDBStoreBackend: loaded sync data`,
);
if (results.length > 1) {
console.warn("loadSyncData: More than 1 sync row found.");
logger.warn("loadSyncData: More than 1 sync row found.");
}
return (results.length > 0 ? results[0] : {});
});
+5 -4
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../src/logger';
/**
* An IndexedDB store backend where the actual backend sits in a web
@@ -140,7 +141,7 @@ RemoteIndexedDBStoreBackend.prototype = {
// tell the worker the db name.
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
console.log("IndexedDB worker is ready");
logger.log("IndexedDB worker is ready");
});
}
return this._startPromise;
@@ -170,13 +171,13 @@ RemoteIndexedDBStoreBackend.prototype = {
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
if (msg.seq === undefined) {
console.error("Got reply from worker with no seq");
logger.error("Got reply from worker with no seq");
return;
}
const def = this._inFlight[msg.seq];
if (def === undefined) {
console.error("Got reply for unknown seq " + msg.seq);
logger.error("Got reply for unknown seq " + msg.seq);
return;
}
delete this._inFlight[msg.seq];
@@ -189,7 +190,7 @@ RemoteIndexedDBStoreBackend.prototype = {
def.reject(error);
}
} else {
console.warn("Unrecognised message from worker: " + msg);
logger.warn("Unrecognised message from worker: " + msg);
}
},
};
+3 -2
View File
@@ -17,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import logger from '../../src/logger';
/**
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
@@ -129,8 +130,8 @@ class IndexedDBStoreWorker {
result: ret,
});
}, (err) => {
console.error("Error running command: "+msg.command);
console.error(err);
logger.error("Error running command: "+msg.command);
logger.error(err);
this.postMessage.call(null, {
command: 'cmd_fail',
seq: msg.seq,
+100 -40
View File
@@ -15,13 +15,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable babel/no-invalid-this */
import Promise from 'bluebird';
import {MatrixInMemoryStore} from "./memory";
import {MemoryStore} from "./memory";
import utils from "../utils";
import {EventEmitter} from 'events';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
import User from "../models/user";
import {MatrixEvent} from "../models/event";
import logger from '../../src/logger';
/**
* This is an internal module. See {@link IndexedDBStore} for the public class.
@@ -37,9 +41,9 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
/**
* Construct a new Indexed Database store, which extends MatrixInMemoryStore.
* Construct a new Indexed Database store, which extends MemoryStore.
*
* This store functions like a MatrixInMemoryStore except it periodically persists
* This store functions like a MemoryStore except it periodically persists
* the contents of the store to an IndexedDB backend.
*
* All data is still kept in-memory but can be loaded from disk by calling
@@ -62,7 +66,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
* </pre>
*
* @constructor
* @extends MatrixInMemoryStore
* @extends MemoryStore
* @param {Object} opts Options object.
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
* <code>window.indexedDB</code>
@@ -79,7 +83,7 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
* database.
*/
const IndexedDBStore = function IndexedDBStore(opts) {
MatrixInMemoryStore.call(this, opts);
MemoryStore.call(this, opts);
if (!opts.indexedDB) {
throw new Error('Missing required option: indexedDB');
@@ -109,23 +113,28 @@ const IndexedDBStore = function IndexedDBStore(opts) {
// user_id : timestamp
};
};
utils.inherits(IndexedDBStore, MatrixInMemoryStore);
utils.inherits(IndexedDBStore, MemoryStore);
utils.extend(IndexedDBStore.prototype, EventEmitter.prototype);
IndexedDBStore.exists = function(indexedDB, dbName) {
return LocalIndexedDBStoreBackend.exists(indexedDB, dbName);
};
/**
* @return {Promise} Resolved when loaded from indexed db.
*/
IndexedDBStore.prototype.startup = function() {
if (this.startedUp) {
console.log(`IndexedDBStore.startup: already started`);
logger.log(`IndexedDBStore.startup: already started`);
return Promise.resolve();
}
console.log(`IndexedDBStore.startup: connecting to backend`);
logger.log(`IndexedDBStore.startup: connecting to backend`);
return this.backend.connect().then(() => {
console.log(`IndexedDBStore.startup: loading presence events`);
logger.log(`IndexedDBStore.startup: loading presence events`);
return this.backend.getUserPresenceEvents();
}).then((userPresenceEvents) => {
console.log(`IndexedDBStore.startup: processing presence events`);
logger.log(`IndexedDBStore.startup: processing presence events`);
userPresenceEvents.forEach(([userId, rawEvent]) => {
const u = new User(userId);
if (rawEvent) {
@@ -142,36 +151,36 @@ IndexedDBStore.prototype.startup = function() {
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
IndexedDBStore.prototype.getSavedSync = function() {
IndexedDBStore.prototype.getSavedSync = degradable(function() {
return this.backend.getSavedSync();
};
}, "getSavedSync");
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
IndexedDBStore.prototype.isNewlyCreated = function() {
IndexedDBStore.prototype.isNewlyCreated = degradable(function() {
return this.backend.isNewlyCreated();
};
}, "isNewlyCreated");
/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
*/
IndexedDBStore.prototype.getSavedSyncToken = function() {
IndexedDBStore.prototype.getSavedSyncToken = degradable(function() {
return this.backend.getNextBatchToken();
},
}, "getSavedSyncToken"),
/**
* Delete all data from this store.
* @return {Promise} Resolves if the data was deleted from the database.
*/
IndexedDBStore.prototype.deleteAllData = function() {
MatrixInMemoryStore.prototype.deleteAllData.call(this);
IndexedDBStore.prototype.deleteAllData = degradable(function() {
MemoryStore.prototype.deleteAllData.call(this);
return this.backend.clearDatabase().then(() => {
console.log("Deleted indexeddb data.");
logger.log("Deleted indexeddb data.");
}, (err) => {
console.error(`Failed to delete indexeddb data: ${err}`);
logger.error(`Failed to delete indexeddb data: ${err}`);
throw err;
});
};
});
/**
* Whether this store would like to save its data
@@ -189,17 +198,19 @@ IndexedDBStore.prototype.wantsSave = function() {
/**
* Possibly write data to the database.
*
* @param {bool} force True to force a save to happen
* @return {Promise} Promise resolves after the write completes
* (or immediately if no write is performed)
*/
IndexedDBStore.prototype.save = function() {
if (this.wantsSave()) {
IndexedDBStore.prototype.save = function(force) {
if (force || this.wantsSave()) {
return this._reallySave();
}
return Promise.resolve();
};
IndexedDBStore.prototype._reallySave = function() {
IndexedDBStore.prototype._reallySave = degradable(function() {
this._syncTs = Date.now(); // set now to guard against multi-writes
// work out changed users (this doesn't handle deletions but you
@@ -215,14 +226,12 @@ IndexedDBStore.prototype._reallySave = function() {
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
}
return this.backend.syncToDatabase(userTuples).catch((err) => {
console.error("sync fail:", err);
});
};
return this.backend.syncToDatabase(userTuples);
});
IndexedDBStore.prototype.setSyncData = function(syncData) {
IndexedDBStore.prototype.setSyncData = degradable(function(syncData) {
return this.backend.setSyncData(syncData);
};
}, "setSyncData");
/**
* Returns the out-of-band membership events for this room that
@@ -231,9 +240,9 @@ IndexedDBStore.prototype.setSyncData = function(syncData) {
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
IndexedDBStore.prototype.getOutOfBandMembers = degradable(function(roomId) {
return this.backend.getOutOfBandMembers(roomId);
};
}, "getOutOfBandMembers");
/**
* Stores the out-of-band membership events for this room. Note that
@@ -243,20 +252,71 @@ IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents) {
IndexedDBStore.prototype.setOutOfBandMembers = degradable(function(
roomId,
membershipEvents,
) {
MemoryStore.prototype.setOutOfBandMembers.call(this, roomId, membershipEvents);
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
};
}, "setOutOfBandMembers");
IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
IndexedDBStore.prototype.clearOutOfBandMembers = degradable(function(roomId) {
MemoryStore.prototype.clearOutOfBandMembers.call(this);
return this.backend.clearOutOfBandMembers(roomId);
};
}, "clearOutOfBandMembers");
IndexedDBStore.prototype.getClientOptions = function() {
IndexedDBStore.prototype.getClientOptions = degradable(function() {
return this.backend.getClientOptions();
};
}, "getClientOptions");
IndexedDBStore.prototype.storeClientOptions = function(options) {
IndexedDBStore.prototype.storeClientOptions = degradable(function(options) {
MemoryStore.prototype.storeClientOptions.call(this, options);
return this.backend.storeClientOptions(options);
};
}, "storeClientOptions");
module.exports.IndexedDBStore = IndexedDBStore;
/**
* All member functions of `IndexedDBStore` that access the backend use this wrapper to
* watch for failures after initial store startup, including `QuotaExceededError` as
* free disk space changes, etc.
*
* When IndexedDB fails via any of these paths, we degrade this back to a `MemoryStore`
* in place so that the current operation and all future ones are in-memory only.
*
* @param {Function} func The degradable work to do.
* @param {String} fallback The method name for fallback.
* @returns {Function} A wrapped member function.
*/
function degradable(func, fallback) {
return async function(...args) {
try {
return await func.call(this, ...args);
} catch (e) {
logger.error("IndexedDBStore failure, degrading to MemoryStore", e);
this.emit("degraded", e);
try {
// We try to delete IndexedDB after degrading since this store is only a
// cache (the app will still function correctly without the data).
// It's possible that deleting repair IndexedDB for the next app load,
// potenially by making a little more space available.
logger.log("IndexedDBStore trying to delete degraded data");
await this.backend.clearDatabase();
logger.log("IndexedDBStore delete after degrading succeeeded");
} catch (e) {
logger.warn("IndexedDBStore delete after degrading failed", e);
}
// Degrade the store from being an instance of `IndexedDBStore` to instead be
// an instance of `MemoryStore` so that future API calls use the memory path
// directly and skip IndexedDB entirely. This should be safe as
// `IndexedDBStore` already extends from `MemoryStore`, so we are making the
// store become its parent type in a way. The mutator methods of
// `IndexedDBStore` also maintain the state that `MemoryStore` uses (many are
// not overridden at all).
Object.setPrototypeOf(this, MemoryStore.prototype);
if (fallback) {
return await MemoryStore.prototype[fallback].call(this, ...args);
}
}
};
}
+13 -4
View File
@@ -17,7 +17,7 @@ limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* This is an internal module. See {@link MemoryStore} for the public class.
* @module store/memory
*/
const utils = require("../utils");
@@ -31,7 +31,7 @@ import Promise from 'bluebird';
* @param {LocalStorage} opts.localStorage The local storage instance to persist
* some forms of data such as tokens. Rooms will NOT be stored.
*/
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
module.exports.MemoryStore = function MemoryStore(opts) {
opts = opts || {};
this.rooms = {
// roomId: Room
@@ -58,7 +58,7 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
this._clientOptions = {};
};
module.exports.MatrixInMemoryStore.prototype = {
module.exports.MemoryStore.prototype = {
/**
* Retrieve the token to stream from.
@@ -335,8 +335,10 @@ module.exports.MatrixInMemoryStore.prototype = {
/**
* Save does nothing as there is no backing data store.
* @param {bool} force True to force a save (but the memory
* store still can't save anything)
*/
save: function() {},
save: function(force) {},
/**
* Startup does nothing as this store doesn't require starting up.
@@ -385,6 +387,7 @@ module.exports.MatrixInMemoryStore.prototype = {
};
return Promise.resolve();
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
@@ -395,6 +398,7 @@ module.exports.MatrixInMemoryStore.prototype = {
getOutOfBandMembers: function(roomId) {
return Promise.resolve(this._oobMembers[roomId] || null);
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
@@ -408,6 +412,11 @@ module.exports.MatrixInMemoryStore.prototype = {
return Promise.resolve();
},
clearOutOfBandMembers: function() {
this._oobMembers = {};
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve(this._clientOptions);
},
+2 -1
View File
@@ -22,6 +22,7 @@ limitations under the License.
*/
const utils = require("../../utils");
import logger from '../../logger';
const DEBUG = false; // set true to enable console logging.
const E2E_PREFIX = "session.e2e.";
@@ -257,7 +258,7 @@ function removeByPrefix(store, prefix) {
function debuglog() {
if (DEBUG) {
console.log(...arguments);
logger.log(...arguments);
}
}
+2 -1
View File
@@ -21,6 +21,7 @@ limitations under the License.
*/
import utils from "./utils";
import logger from '../src/logger';
/**
@@ -168,7 +169,7 @@ class SyncAccumulator {
}
break;
default:
console.error("Unknown cateogory: ", category);
logger.error("Unknown cateogory: ", category);
}
}
+127 -31
View File
@@ -32,6 +32,8 @@ const Group = require('./models/group');
const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
const PushProcessor = require("./pushprocessor");
import logger from '../src/logger';
import {InvalidStoreError} from './errors';
@@ -58,7 +60,7 @@ function debuglog(...params) {
if (!DEBUG) {
return;
}
console.log(...params);
logger.log(...params);
}
@@ -116,17 +118,25 @@ function SyncApi(client, opts) {
*/
SyncApi.prototype.createRoom = function(roomId) {
const client = this.client;
const {
timelineSupport,
unstableClientRelationAggregation,
} = client;
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: this.opts.lazyLoadMembers,
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport: client.timelineSupport,
timelineSupport,
unstableClientRelationAggregation,
});
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline",
"Room.redaction",
"Room.redactionCancelled",
"Room.receipt", "Room.tags",
"Room.timelineReset",
"Room.localEchoUpdated",
"Room.accountData",
"Room.myMembership",
"Room.replaceEvent",
]);
this._registerStateListeners(room);
return room;
@@ -386,7 +396,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
peekRoom.addLiveEvents(events);
self._peekPoll(peekRoom, res.end);
}, function(err) {
console.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
logger.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
setTimeout(function() {
self._peekPoll(peekRoom, token);
}, 30 * 1000);
@@ -445,6 +455,16 @@ SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
return false;
};
SyncApi.prototype._shouldAbortSync = function(error) {
if (error.errcode === "M_UNKNOWN_TOKEN") {
// The logout already happened, we just need to stop.
logger.warn("Token no longer valid - assuming logout");
this.stop();
return true;
}
return false;
};
/**
* Main entry point
*/
@@ -472,13 +492,17 @@ SyncApi.prototype.sync = function() {
async function getPushRules() {
try {
debuglog("Getting push rules...");
const result = await client.getPushRules();
debuglog("Got push rules");
client.pushRules = result;
} catch (err) {
logger.error("Getting push rules failed", err);
if (self._shouldAbortSync(err)) return;
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying push rules...");
await self.recoverFromSyncStartupError(savedSyncPromise, err);
getPushRules();
return;
@@ -487,22 +511,35 @@ SyncApi.prototype.sync = function() {
}
const checkLazyLoadStatus = async () => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && client.isGuest()) {
this.opts.lazyLoadMembers = false;
}
if (this.opts.lazyLoadMembers) {
debuglog("Checking server lazy load support...");
const supported = await client.doesServerSupportLazyLoading();
if (supported) {
this.opts.filter = await client.createFilter(
Filter.LAZY_LOADING_SYNC_FILTER,
);
try {
debuglog("Creating and storing lazy load sync filter...");
this.opts.filter = await client.createFilter(
Filter.LAZY_LOADING_SYNC_FILTER,
);
debuglog("Created and stored lazy load sync filter");
} catch (err) {
logger.error(
"Creating and storing lazy load sync filter failed",
err,
);
throw err;
}
} else {
console.log("LL: lazy loading requested but not supported " +
debuglog("LL: lazy loading requested but not supported " +
"by server, so disabling");
this.opts.lazyLoadMembers = false;
}
}
// need to vape the store when enabling LL and wasn't enabled before
debuglog("Checking whether lazy loading has changed in store...");
const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) {
this._storeIsInvalid = true;
@@ -513,18 +550,26 @@ SyncApi.prototype.sync = function() {
// we leave the state as 'ERROR' which isn't great since this normally means
// we're retrying. The client must be stopped before clearing the stores anyway
// so the app should stop the client, clear the store and start it again.
console.warn("InvalidStoreError: store is not usable: stopping sync.");
logger.warn("InvalidStoreError: store is not usable: stopping sync.");
return;
}
if (this.opts.lazyLoadMembers && this.opts.crypto) {
this.opts.crypto.enableLazyLoading();
}
await this.client._storeClientOptions();
try {
debuglog("Storing client options...");
await this.client._storeClientOptions();
debuglog("Stored client options");
} catch (err) {
logger.error("Storing client options failed", err);
throw err;
}
getFilter(); // Now get the filter and start syncing
};
async function getFilter() {
debuglog("Getting filter...");
let filter;
if (self.opts.filter) {
filter = self.opts.filter;
@@ -539,8 +584,11 @@ SyncApi.prototype.sync = function() {
getFilterName(client.credentials.userId), filter,
);
} catch (err) {
logger.error("Getting filter failed", err);
if (self._shouldAbortSync(err)) return;
// wait for saved sync to complete before doing anything else,
// otherwise the sync state will end up being incorrect
debuglog("Waiting for saved sync before retrying filter...");
await self.recoverFromSyncStartupError(savedSyncPromise, err);
getFilter();
return;
@@ -554,11 +602,12 @@ SyncApi.prototype.sync = function() {
if (self._currentSyncRequest === null) {
// Send this first sync request here so we can then wait for the saved
// sync data to finish processing before we process the results of this one.
console.log("Sending first sync request...");
debuglog("Sending first sync request...");
self._currentSyncRequest = self._doSyncRequest({ filterId }, savedSyncToken);
}
// Now wait for the saved sync to finish...
debuglog("Waiting for saved sync before starting sync processing...");
await savedSyncPromise;
self._sync({ filterId });
}
@@ -570,13 +619,19 @@ SyncApi.prototype.sync = function() {
// Pull the saved sync token out first, before the worker starts sending
// all the sync data which could take a while. This will let us send our
// first incremental sync request before we've processed our saved data.
debuglog("Getting saved sync token...");
savedSyncPromise = client.store.getSavedSyncToken().then((tok) => {
debuglog("Got saved sync token");
savedSyncToken = tok;
debuglog("Getting saved sync...");
return client.store.getSavedSync();
}).then((savedSync) => {
debuglog(`Got reply from saved sync, exists? ${!!savedSync}`);
if (savedSync) {
return self._syncFromCache(savedSync);
}
}).catch(err => {
logger.error("Getting saved sync failed", err);
});
// Now start the first incremental sync request: this can also
// take a while so if we set it going now, we can wait for it
@@ -648,7 +703,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
try {
await this._processSyncResponse(syncEventData, data);
} catch(e) {
console.error("Error processing cached sync", e.stack || e);
logger.error("Error processing cached sync", e.stack || e);
}
// Don't emit a prepared if we've bailed because the store is invalid:
@@ -723,7 +778,10 @@ SyncApi.prototype._sync = async function(syncOptions) {
} catch(e) {
// log the exception with stack if we have it, else fall back
// to the plain description
console.error("Caught /sync error", e.stack || e);
logger.error("Caught /sync error", e.stack || e);
// Emit the exception for client handling
this.client.emit("sync.unexpectedError", e);
}
// update this as it may have changed
@@ -834,11 +892,15 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
return;
}
console.error("/sync error %s", err);
console.error(err);
logger.error("/sync error %s", err);
logger.error(err);
if(this._shouldAbortSync(err)) {
return;
}
this._failedSyncCount++;
console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
logger.log('Number of consecutive failed sync requests:', this._failedSyncCount);
debuglog("Starting keep-alive");
// Note that we do *not* mark the sync connection as
@@ -969,8 +1031,9 @@ SyncApi.prototype._processSyncResponse = async function(
// honour push rules that were previously cached. Base rules
// will be updated when we recieve push rules via getPushRules
// (see SyncApi.prototype.sync) before syncing over the network.
if (accountDataEvent.getType() == 'm.push_rules') {
client.pushRules = accountDataEvent.getContent();
if (accountDataEvent.getType() === 'm.push_rules') {
const rules = accountDataEvent.getContent();
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
}
client.emit("accountData", accountDataEvent);
return accountDataEvent;
@@ -982,8 +1045,26 @@ SyncApi.prototype._processSyncResponse = async function(
if (data.to_device && utils.isArray(data.to_device.events) &&
data.to_device.events.length > 0
) {
const cancelledKeyVerificationTxns = [];
data.to_device.events
.map(client.getEventMapper())
.map((toDeviceEvent) => { // map is a cheap inline forEach
// We want to flag m.key.verification.start events as cancelled
// if there's an accompanying m.key.verification.cancel event, so
// we pull out the transaction IDs from the cancellation events
// so we can flag the verification events as cancelled in the loop
// below.
if (toDeviceEvent.getType() === "m.key.verification.cancel") {
const txnId = toDeviceEvent.getContent()['transaction_id'];
if (txnId) {
cancelledKeyVerificationTxns.push(txnId);
}
}
// as mentioned above, .map is a cheap inline forEach, so return
// the unmodified event.
return toDeviceEvent;
})
.forEach(
function(toDeviceEvent) {
const content = toDeviceEvent.getContent();
@@ -992,13 +1073,21 @@ SyncApi.prototype._processSyncResponse = async function(
content.msgtype == "m.bad.encrypted"
) {
// the mapper already logged a warning.
console.log(
logger.log(
'Ignoring undecryptable to-device event from ' +
toDeviceEvent.getSender(),
);
return;
}
if (toDeviceEvent.getType() === "m.key.verification.start"
|| toDeviceEvent.getType() === "m.key.verification.request") {
const txnId = content['transaction_id'];
if (cancelledKeyVerificationTxns.includes(txnId)) {
toDeviceEvent.flagCancelled();
}
}
client.emit("toDeviceEvent", toDeviceEvent);
},
);
@@ -1048,7 +1137,6 @@ SyncApi.prototype._processSyncResponse = async function(
const stateEvents =
self._mapSyncEventsFormat(inviteObj.invite_state, room);
room.updateMyMembership("invite");
self._processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) {
room.recalculate();
@@ -1058,6 +1146,7 @@ SyncApi.prototype._processSyncResponse = async function(
stateEvents.forEach(function(e) {
client.emit("event", e);
});
room.updateMyMembership("invite");
});
// Handle joins
@@ -1073,12 +1162,19 @@ SyncApi.prototype._processSyncResponse = async function(
room.setUnreadNotificationCount(
'total', joinObj.unread_notifications.notification_count,
);
room.setUnreadNotificationCount(
'highlight', joinObj.unread_notifications.highlight_count,
);
}
room.updateMyMembership("join");
// We track unread notifications ourselves in encrypted rooms, so don't
// bother setting it here. We trust our calculations better than the
// server's for this case, and therefore will assume that our non-zero
// count is accurate.
const encrypted = client.isRoomEncrypted(room.roomId);
if (!encrypted
|| (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) {
room.setUnreadNotificationCount(
'highlight', joinObj.unread_notifications.highlight_count,
);
}
}
joinObj.timeline = joinObj.timeline || {};
@@ -1151,10 +1247,8 @@ SyncApi.prototype._processSyncResponse = async function(
room.setSummary(joinObj.summary);
}
// XXX: should we be adding ephemeralEvents to the timeline?
// It feels like that for symmetry with room.addAccountData()
// there should be a room.addEphemeralEvents() or similar.
room.addLiveEvents(ephemeralEvents);
// we deliberately don't add ephemeral events to the timeline
room.addEphemeralEvents(ephemeralEvents);
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
@@ -1192,6 +1286,8 @@ SyncApi.prototype._processSyncResponse = async function(
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
room.updateMyMembership("join");
});
// Handle leaves (e.g. kicked rooms)
@@ -1204,8 +1300,6 @@ SyncApi.prototype._processSyncResponse = async function(
const accountDataEvents =
self._mapSyncEventsFormat(leaveObj.account_data);
room.updateMyMembership("leave");
self._processRoomEvents(room, stateEvents, timelineEvents);
room.addAccountData(accountDataEvents);
@@ -1226,6 +1320,8 @@ SyncApi.prototype._processSyncResponse = async function(
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
room.updateMyMembership("leave");
});
// update the notification timeline, if appropriate.
+2 -1
View File
@@ -19,6 +19,7 @@ limitations under the License.
import Promise from 'bluebird';
const EventTimeline = require("./models/event-timeline");
import logger from '../src/logger';
/**
* @private
@@ -28,7 +29,7 @@ const DEBUG = false;
/**
* @private
*/
const debuglog = DEBUG ? console.log.bind(console) : function() {};
const debuglog = DEBUG ? logger.log.bind(logger) : function() {};
/**
* the number of times we ask the server for more events before giving up
+11 -10
View File
@@ -21,6 +21,7 @@ limitations under the License.
*/
const utils = require("../utils");
const EventEmitter = require("events").EventEmitter;
import logger from '../../src/logger';
const DEBUG = true; // set true to enable console logging.
// events: hangup, error(err), replaced(call), state(state, oldState)
@@ -195,7 +196,7 @@ MatrixCall.prototype.placeScreenSharingCall =
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.playElement = function(element, queueId) {
console.log("queuing play on " + queueId + " and element " + element);
logger.log("queuing play on " + queueId + " and element " + element);
// XXX: FIXME: Does this leak elements, given the old promises
// may hang around and retain a reference to them?
if (this.mediaPromises[queueId]) {
@@ -206,10 +207,10 @@ MatrixCall.prototype.playElement = function(element, queueId) {
// these failures may be non-fatal (as in the case of unmounts)
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
logger.log("previous promise completed for " + queueId);
return element.play();
}, function() {
console.log("previous promise failed for " + queueId);
logger.log("previous promise failed for " + queueId);
return element.play();
});
} else {
@@ -224,14 +225,14 @@ MatrixCall.prototype.playElement = function(element, queueId) {
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.pauseElement = function(element, queueId) {
console.log("queuing pause on " + queueId + " and element " + element);
logger.log("queuing pause on " + queueId + " and element " + element);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
logger.log("previous promise completed for " + queueId);
return element.pause();
}, function() {
console.log("previous promise failed for " + queueId);
logger.log("previous promise failed for " + queueId);
return element.pause();
});
} else {
@@ -250,15 +251,15 @@ MatrixCall.prototype.pauseElement = function(element, queueId) {
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.assignElement = function(element, srcObject, queueId) {
console.log("queuing assign on " + queueId + " element " + element + " for " +
logger.log("queuing assign on " + queueId + " element " + element + " for " +
srcObject);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
logger.log("previous promise completed for " + queueId);
element.srcObject = srcObject;
}, function() {
console.log("previous promise failed for " + queueId);
logger.log("previous promise failed for " + queueId);
element.srcObject = srcObject;
});
} else {
@@ -1159,7 +1160,7 @@ const callError = function(code, msg) {
const debuglog = function() {
if (DEBUG) {
console.log(...arguments);
logger.log(...arguments);
}
};
-9
View File
@@ -1,9 +0,0 @@
#!/bin/bash
set -ex
yarn lint
yarn test
yarn gendoc
+624 -622
View File
File diff suppressed because it is too large Load Diff