Compare commits

...

1540 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
David Baker ef594d52e4 v1.0.2 2019-03-18 13:47:54 +00:00
David Baker 23bbb2f8c6 Prepare changelog for v1.0.2 2019-03-18 13:47:53 +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
David Baker 70575f9e33 v1.0.2-rc.1 2019-03-13 14:24:57 +00:00
David Baker 8e16586d84 Merge branch 'develop' into release-v1.0.2 2019-03-13 14:24:26 +00:00
David Baker 6920dfb800 Yarn needs --new-version to create a new version 2019-03-13 14:23:51 +00:00
David Baker 02d93770aa Prepare changelog for v1.0.2-rc.1 2019-03-13 14:20:18 +00:00
David Baker cd124231c5 Merge pull request #858 from jryans/yarn-ci
Use modern Yarn version on Travis CI
2019-03-13 13:22:39 +00:00
J. Ryan Stinnett cd75848882 Use modern Yarn version on Travis CI
Travis CI uses a quite old version of Yarn by default. This adds Yarn's
recommended incantation for using the latest stable version.
2019-03-13 12:28:07 +00:00
J. Ryan Stinnett 1bae15ede9 Recommend using the latest Node LTS version 2019-03-13 10:50:58 +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
J. Ryan Stinnett 4570fcaa8a Ignore .npmrc 2019-03-12 12:02:04 +00:00
J. Ryan Stinnett 90670cf1be Merge pull request #856 from jryans/yarn
Switch to `yarn` for dependency management
2019-03-11 13:27:04 +00:00
J. Ryan Stinnett cc86f427d2 Convert Olm to a dev dependency
This gives more natural behavior of downloading Olm by default when working on
this module, so that all tests including crypto are run.
2019-03-08 15:58:08 +00:00
J. Ryan Stinnett 2144791d52 Update scripts and docs to use yarn where appropriate
Most `npm` operations are replaced with `yarn`, which generally has better
behavior. However, steps like publish that write to the NPM registry are left to
`npm`, which currently handles these tasks best.
2019-03-08 15:45:57 +00:00
J. Ryan Stinnett 33aabf44e7 Convert from npm to yarn lock file
The npm lock file was imported into yarn. A yarn install pass was then run to
double-check the lock file for sanity.
2019-03-08 10:47:58 +00:00
Hubert Chathi fc1ea27380 Merge pull request #855 from uhoreg/fix_key_requests
More key request fixes
2019-03-06 14:46:43 -05:00
Hubert Chathi 81946294d8 use lolex to fake the timer 2019-03-06 14:42:52 -05:00
Matthew Hodgson 77270fa78c Merge pull request #851 from matrix-org/travis/e2e-notifs
Calculate encrypted notification counts
2019-03-06 17:11:22 +00:00
Hubert Chathi 9e29289dcc use a different transaction ID when re-sending a key request 2019-03-06 12:02:48 -05:00
Travis Ralston 6198943976 Add a mention that we should be handling gaps in /sync 2019-03-06 09:56:36 -07:00
David Baker 8beb836ccd Merge pull request #854 from matrix-org/dbkr/deps_190206
Update dependencies
2019-03-06 14:08:26 +00:00
David Baker b7c0e39c1a Update dependencies
To get non-vulnerable versions
2019-03-06 13:50:35 +00:00
David Baker 777acae2e5 Merge branch 'master' into develop 2019-03-06 11:17:25 +00:00
David Baker e77389c1ce v1.0.1 2019-03-06 11:10:42 +00:00
David Baker eb24e2e1f1 Prepare changelog for v1.0.1 2019-03-06 11:10:41 +00:00
Hubert Chathi 114244f8bb fix typo 2019-03-06 00:23:01 -05:00
Travis Ralston 54769d9136 Add jsdoc 2019-03-05 14:25:32 -07:00
Travis Ralston 2f2deb5333 Expose the clear event content directly from an event 2019-03-05 14:04:51 -07:00
Travis Ralston 37f106d4af More safely set the push actions for an encrypted event 2019-03-05 14:04:39 -07:00
David Baker 36ee7cdbfc v1.0.1-rc.2 2019-03-05 18:53:16 +00:00
David Baker 2b564498ee Prepare changelog for v1.0.1-rc.2 2019-03-05 18:53:15 +00:00
David Baker 0ddba16fa1 Merge pull request #853 from matrix-org/bwindels/dontswallowcryptotxnerrors
dont swallow txn errors in crypto store
2019-03-05 18:36:15 +00:00
Hubert Chathi 550086eb67 Merge pull request #850 from uhoreg/ensure_key_request
make sure key requests get sent
2019-03-05 10:57:15 -05:00
Hubert Chathi 055ce673cd fix jsdoc 2019-03-05 10:54:02 -05:00
David Baker 5308595658 Merge pull request #852 from matrix-org/dbkr/getusermedia_deviceid_ideal
Use 'ideal' rather than 'exact' for deviceid
2019-03-05 14:23:39 +00:00
David Baker e726e29f39 Better logging here too 2019-03-05 13:02:55 +00:00
David Baker 33b12fa6b5 Use 'ideal' rather than 'exact' for deviceid
We were using 'exact' which means we fail outright if the device
we wanted isn't available. This means if a user selects a specific
device then later unplugs it, we fail to open a capture device
the next time they make a call even if there's one available.
Using 'ideal' uses the chosen device in preference, but something
else if it isn't available.

Also log the name of the exception when we fail to open a capture
device to give us more of an idea of what's gone wrong.

Should help fix https://github.com/vector-im/riot-web/issues/8993
2019-03-05 12:59:29 +00:00
Travis Ralston 829cd05cba Appease the linter 2019-03-04 21:46:48 -07:00
Travis Ralston 4834e12a3a Calculate unread badges for encrypted events 2019-03-04 21:41:20 -07:00
Travis Ralston bcd4ad130c Use the decrypted event content when checking the push rules
Otherwise we'll be looking at the encrypted source, and that doesn't help anyone.
2019-03-04 21:33:57 -07:00
Travis Ralston 998d9e010e Support flushing the cache on calculated push rules
Needed for encrypted events to be able to pass some push rules.
2019-03-04 21:24:25 -07:00
Hubert Chathi 5480e8e1d5 refactor key sharing requests
use sendRoomKeyRequest with a new resend flag, instead of cancelRoomKeyRequest,
when requesting keys, so that we make sure that we send a new request if there
is no previous request

fixes https://github.com/vector-im/riot-web/issues/6838
2019-03-04 17:09:56 -05:00
Hubert Chathi 98fdcabc00 stop client after each test 2019-03-04 16:59:54 -05:00
Bruno Windels 236397816d Merge pull request #849 from matrix-org/bwindels/dontswallowcryptotxnerrors
Don't swallow txn errors in crypto store
2019-03-04 13:05:17 +01:00
Bruno Windels 755c55de3e dont swallow txn errors in crypto store 2019-03-04 12:58:05 +01:00
Hubert Chathi 526da71992 Merge pull request #848 from uhoreg/fix_partial_keyshare
handle partially-shared sessions better
2019-03-01 12:18:54 -05:00
Hubert Chathi 86ef262799 fix c+p comment 2019-03-01 12:15:59 -05:00
Hubert Chathi 282904d4be restore the order of backup vs retry decryption 2019-03-01 08:54:04 -05:00
Hubert Chathi a1be24307a lint 2019-02-28 22:54:46 -05:00
Hubert Chathi 4b5623691b handle partially-shared sessions better
- don't cancel key requests if we can't decrypt everything in the session
- overwrite the session key if we get a better version
2019-02-28 16:01:29 -05:00
David Baker 7bdf1e9b92 v1.0.1-rc.1 2019-02-28 14:28:36 +00:00
David Baker af1db8a606 Prepare changelog for v1.0.1-rc.1 2019-02-28 14:28:36 +00:00
Travis Ralston a99bb3c4c9 Merge pull request #847 from matrix-org/travis/fix-megolm-error
Fix "e is undefined" masking the original error in MegolmDecryption
2019-02-26 13:59:16 -07:00
Travis Ralston fd155c15bd Excessive checks are excessive 2019-02-26 13:50:09 -07:00
Travis Ralston aaa43631aa Fix "e is undefined" masking the original error in MegolmDecryption 2019-02-26 13:15:03 -07:00
Hubert Chathi d2557bc943 only set the dirty flag if something was actually changed 2019-02-22 08:37:34 -05:00
Hubert Chathi 33a3506981 speling is hard 2019-02-15 17:14:29 -05:00
J. Ryan Stinnett 03a54353be v1.0.0 2019-02-14 16:48:12 +00:00
J. Ryan Stinnett c6328923e6 Prepare changelog for v1.0.0 2019-02-14 16:48:11 +00:00
J. Ryan Stinnett 1ecb820bb0 Merge branch 'develop' into release-v1.0.0 2019-02-14 16:46:34 +00:00
J. Ryan Stinnett 0be2319288 Merge pull request #841 from jryans/package-lock-release
Try again to commit package-lock.json
2019-02-14 15:57:21 +00:00
J. Ryan Stinnett 073a025b83 Try again to commit package-lock.json 2019-02-14 14:20:31 +00:00
J. Ryan Stinnett e83836d487 v1.0.0-rc.2 2019-02-14 10:47:41 +00:00
J. Ryan Stinnett 065c61e05c Prepare changelog for v1.0.0-rc.2 2019-02-14 10:47:41 +00:00
J. Ryan Stinnett 139a6bd903 Merge branch 'develop' into release-v1.0.0 2019-02-14 10:43:04 +00:00
J. Ryan Stinnett 3fa0ee59d4 Merge pull request #839 from matrix-org/dbkr/commit_package_lock
Release script: commit package-lock.json
2019-02-13 18:57:38 +00:00
David Baker bd3d26422d git st is not a standard thing 2019-02-13 18:20:16 +00:00
David Baker 370ef9fc69 Merge pull request #840 from matrix-org/dbkr/recheck_key_backup
Add method to force re-check of key backup
2019-02-13 18:19:35 +00:00
David Baker a087fb37a3 Add method to force re-check of key backup
Also detect when the key backup version changes and do the right
thing

https://github.com/vector-im/riot-web/issues/8524
2019-02-13 15:40:07 +00:00
David Baker 68c8fe0fa9 Release script: commit package-lock.json
Commit the package-lock.json when bumping the version, otherwise
the versions get out of sync, and this is going to matter more now
that jenkins runs `npm ci` which is fussy about these things.
2019-02-13 13:40:46 +00:00
Bruno Windels 4309749979 Merge pull request #838 from matrix-org/bwindels/e2eiconsanddialog
Fix: dont check for unverified devices in left members
2019-02-13 11:41:57 +01:00
Bruno Windels 1a677804a4 use getEncryptionTargetMembers instead of doing membership checks ourselves 2019-02-13 11:22:38 +01:00
Bruno Windels a427e2a75c dont check devices for left members 2019-02-12 18:33:44 +01:00
David Baker 3c735b0ac1 v1.0.0-rc.1 2019-02-08 18:33:52 +00:00
David Baker 4f446c3909 Prepare changelog for v1.0.0-rc.1 2019-02-08 18:33:51 +00:00
Hubert Chathi 999ed1b5b3 Merge pull request #837 from uhoreg/emoji_sas
change hex SAS verification to decimal and emoji
2019-02-08 11:44:53 -05:00
David Baker 8fa19f4a0f More en_GBification 2019-02-08 14:56:08 +00:00
David Baker 71a01ec234 Replace symbol characters with the emoji variants
The symbol ones are often rendered as little black & white icons: replace them with their variants from the emoji range

Also change to en_GB
2019-02-08 14:11:47 +00:00
David Baker 32f033a9da Merge pull request #836 from matrix-org/dbkr/trust_on_decrypt
Trust on decrypt
2019-02-08 11:36:44 +00:00
David Baker dade385147 Put error constant on the class 2019-02-08 10:43:17 +00:00
David Baker 6cf2e54f9a Fix double-santa 2019-02-08 09:59:42 +00:00
Hubert Chathi fb673b0304 change hex SAS verification to decimal and emoji 2019-02-08 00:56:30 -05:00
David Baker 1a425af3f2 Pass backup info here too 2019-02-07 15:51:51 +00:00
David Baker 9bafed2c26 OK fine, we'll use the session store 2019-02-07 15:33:49 +00:00
David Baker bb2d0b0f62 lint 2019-02-07 14:47:03 +00:00
David Baker 5e4f10a80c Trust on decrypt
Trust backups that we've restored by saving the matching pubkey
locally.

NB. Contains technically breaking API changes to the backup restore
(takes backupInfo rather than version).
2019-02-07 14:37:25 +00:00
David Baker 9e12fc4d7d Merge pull request #835 from matrix-org/dbkr/always_track_own_device_list
Always track our own devices
2019-02-07 09:30:13 +00:00
David Baker 1caf2b7f83 Always track our own devices
It's generally a reasonable assumption that we'll be interested in
them, and important for key backup.

Fixes https://github.com/vector-im/riot-web/issues/8213
2019-02-06 16:48:57 +00:00
David Baker 6f17e3e659 Merge pull request #834 from matrix-org/dbkr/lint_consistency
Make linting rules more consistent
2019-02-05 13:12:25 +00:00
David Baker 17e2cd755d Make linting rules more consistent
* Put back babel-eslint for class-properties
 * Allow arrow functions without params

This makes the style more consistent with react-sdk.

NB. The line lengths are still inconsistent but it's not clear which
way to go on that yet.
2019-02-05 11:58:53 +00:00
Bruno Windels a6970d4de8 Merge pull request #833 from matrix-org/bwindels/e2eicons
add method to room to check for unverified devices
2019-02-01 18:14:51 +00:00
Bruno Windels bc99c1f3ce add method to room to check for unverified devices 2019-02-01 14:37:11 +01:00
David Baker 5e3ff7fc27 Re-apply changelog that somehow got lost 2019-01-30 13:12:32 +00:00
Bruno Windels ffe3f966fe Merge pull request #831 from matrix-org/experimental
Merge redesign into develop
2019-01-30 12:47:33 +00:00
Travis Ralston c60c19a28e Merge pull request #830 from matrix-org/travis/rver-cap-support
Supporting infrastructure for educated decisions on when to upgrade rooms
2019-01-29 11:28:57 -07:00
Travis Ralston 4ea785b604 Add some prose for what safe versions are 2019-01-29 10:46:40 -07:00
Travis Ralston 2d4e9d0d3f Add safety for when the endpoint doesn't exist 2019-01-28 17:18:57 -07:00
Travis Ralston 971d572fbf Supporting infrastructure for educated decisions on when to upgrade rooms
Part of https://github.com/vector-im/riot-web/issues/8251
2019-01-28 16:03:27 -07:00
Hubert Chathi 244e1b84f7 Initial implementation of key verification 2019-01-23 13:34:25 -05:00
David Baker e5cdc99a34 Merge pull request #826 from matrix-org/dbkr/key_backup_status_unknown_device
Include signature info for unknown devices
2019-01-18 10:58:21 +00:00
David Baker 9a5768219f Doc function API changes 2019-01-18 10:04:22 +00:00
Travis Ralston cee8f57318 Merge pull request #828 from matrix-org/travis/v2-is-safe
Flag v2 rooms as "safe"
2019-01-17 16:36:27 -07:00
Travis Ralston 1a40e0a83a Flag v2 rooms as "safe"
We'll still need something like https://github.com/matrix-org/matrix-doc/pull/1804 to make this work correctly, but this fixes the immediate issue in https://github.com/vector-im/riot-web/issues/8154
2019-01-17 16:14:53 -07:00
Travis Ralston d0072d930f Merge pull request #827 from matrix-org/develop
Develop->Experimental
2019-01-17 15:51:22 -07:00
David Baker 385062c4d7 Include signature info for unknown devices
Add a 'deviceId' property and leave 'device' undefined for unknown
devices.

https://github.com/vector-im/riot-web/issues/8142
2019-01-17 16:43:26 +00:00
J. Ryan Stinnett 9245638b25 Set key backup guard flag to avoid duplicate uploads
Not sure how this was missed, but anyway, guard against duplicate uploads with
the existing flag.
2019-01-17 08:19:09 -06:00
J. Ryan Stinnett 1865542192 Emit when user's status message changes 2019-01-15 08:59:31 -06:00
J. Ryan Stinnett 2563abda11 Ensure we have power to set custom status
If we're in a non-DM room of 2 people, we may not have power to set state events
like custom status.  Ensure that we do before sending.
2019-01-15 08:59:31 -06:00
David Baker 59b80d8fbd Remove babel-eslint as we no longer need it 2019-01-10 10:42:43 +00:00
David Baker 68bb8182e4 Update ESLint
* Bump version
 * Add a couple of rules to match our existing precedent
 * Fix a few genuine lint errors
 * Ignore a guard-for-in (not sure why eslint doesn't like this?)
 * Update max warnings
2019-01-10 10:42:34 +00:00
David Baker c979ff6696 Merge pull request #821 from matrix-org/dbkr/update_eslint
Update ESLint
2019-01-10 09:35:24 +00:00
J. Ryan Stinnett 25681e888c Change initial key backup to background
Alters the APIs used for initial key backup so that the actual upload happens in
the background after all session are marked for backup.
2019-01-09 12:03:55 -06:00
J. Ryan Stinnett 5cfd082b00 Schedule key upload on enabling backup
This ensures a partially completed backup will continue to make progress.
2019-01-09 12:03:55 -06:00
J. Ryan Stinnett 0cbced43bd Emit when count of sessions to backup changes
This will be used in the React SDK to display upload progress when there are
many sessions to upload.
2019-01-09 12:03:55 -06:00
David Baker b3e8d7e07e Remove babel-eslint as we no longer need it 2019-01-09 17:33:42 +00:00
David Baker f4a7395e3a Update ESLint
* Bump version
 * Add a couple of rules to match our existing precedent
 * Fix a few genuine lint errors
 * Ignore a guard-for-in (not sure why eslint doesn't like this?)
 * Update max warnings
2019-01-09 17:29:30 +00:00
J. Ryan Stinnett 14b42abfa4 Re-check key backup status on version mismatch
This ensures we will report the updated status when consumer code asks for it.

Fixes part of https://github.com/vector-im/riot-web/issues/8048.
2019-01-09 04:07:58 -06:00
J. Ryan Stinnett e8022e985e Merge pull request #815 from matrix-org/develop
Merge develop into experimental
2019-01-03 13:54:34 -06:00
David Baker f6c8687dc8 Merge pull request #812 from matrix-org/dbkr/getallsessions
Add a getAllEndToEndSessions to crypto store
2019-01-03 10:09:18 +00:00
J. Ryan Stinnett 472d8faace Merge pull request #668 from matrix-org/t3chguy/fix_displayname_logic
T3chguy/fix displayname logic
2019-01-02 17:40:19 -06:00
Michael Telatynski fc5f3c2fcc re-add empty check after removing hidden chars 2018-12-30 00:20:33 +00:00
Travis Ralston fb756208d8 Merge pull request #814 from trashhalo/rebase-tip
Contributing: Note that rebase lets you mass signoff commits

replace find loop + slice with a single filter. More readable

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

keep public APIs consistent with previous behaviour now that its fuzzy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

make tests not fail, because of order of occurrence

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

do falsey displayname check regardless of whether we have roomstate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

add tests for the fuzzy disambiguation between members in a room

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

remove duplicated call
2018-12-30 00:18:05 +00:00
Travis Ralston 6a98e93845 Merge pull request #814 from trashhalo/rebase-tip
Contributing: Note that rebase lets you mass signoff commits
2018-12-28 19:40:41 -07:00
Stephen Solka 79e155acfb Note that rebase lets you mass signoff commits
Signed-off-by: Stephen Solka <stephen0q@gmail.com>
2018-12-28 21:36:06 -05:00
David Baker 59ae6e3dc0 Missing bracket 2018-12-21 19:21:40 +00:00
David Baker e628ed3ef4 Add a getAllEndToEndSessions to crypto store
So we can migrate them from place to place
2018-12-21 19:12:01 +00:00
J. Ryan Stinnett 11d40e9daa Merge pull request #672 from matrix-org/t3chguy/unhomoglyph
take into account homoglyphs when calculating similar display names
2018-12-20 14:38:48 +00:00
Michael Telatynski a07f0631b7 Include unhomoglyph in package-lock.json - it must have felt left out :L
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-12-19 22:28:40 +00:00
Michael Telatynski 790d1dd8f7 Merge branches 'develop' and 't3chguy/unhomoglyph' of github.com:matrix-org/matrix-js-sdk into t3chguy/unhomoglyph 2018-12-19 22:21:09 +00:00
J. Ryan Stinnett c92e510a4d Pin to base-x 3.0.4
This avoids API changes in 3.0.5 (requiring `Buffer` instances), but more
importantly also avoids dealing with ES6 in dependencies for another day.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-18 19:47:17 +00:00
David Baker c48a6c0601 Merge pull request #809 from jryans/key-backup-version-change
Emit for key backup failures
2018-12-18 09:34:43 +00:00
J. Ryan Stinnett 383f3f9834 Merge branch 'develop' into t3chguy/unhomoglyph 2018-12-18 01:01:41 +00:00
J. Ryan Stinnett 3c1e9ba6e9 Merge pull request #646 from leonlianght/develop
emit oldEventId on "updatePendingEvent"
2018-12-18 00:41:52 +00:00
J. Ryan Stinnett 26893b9877 Merge pull request #589 from johbo/add-thirdparty-user
Add getThirdpartyUser to base api
2018-12-18 00:38:09 +00:00
J. Ryan Stinnett 2b734b8e69 Emit for key backup failures
Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-14 15:17:22 +00:00
J. Ryan Stinnett c5f6f87a6c Add await to ensure we wait for backup errors
Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-14 15:17:22 +00:00
David Baker 66cdb62a3d Reorganize key backup flow
This will allow the key backup flow to propagate errors for things like version
mismatches more easily.

In addition, it raises the limit of keys sent per request from 10 to 200 to cut
down on the number of requests.
2018-12-14 15:17:10 +00:00
Travis Ralston f53e33723b Merge pull request #805 from matrix-org/travis/custom-status
Support custom status messages
2018-12-13 10:24:37 -07:00
Travis Ralston 06bc6e7568 Update jsdoc to match _unstable_ prefix 2018-12-13 09:42:22 -07:00
J. Ryan Stinnett 5e3f42ec5a Use olm-loader in all crypto tests
Standardize on importing `olm-loader` rather than pasting the same boilerplate
in different tests.  Importantly, `spec/unit/crypto.spec.js` did not include any
loading approach, so it would only find Olm if some other test loaded it first.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-13 08:59:57 +00:00
Travis Ralston 08b3dfa3b5 Prefix the status message API with _unstable
It's not a formal feature of Matrix yet, so we should try and avoid people relying on it. This makes it appear as a private API and is very clearly labeled as not intended for use.
2018-12-12 23:05:03 -07:00
J. Ryan Stinnett 6cf9563441 Avoid checking key backup status if guest
Fixes vector-im/riot-web#7839.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-12 22:26:01 +00:00
Travis Ralston fb65c7f4ba Support setting status message in rooms that look like 1:1s
Part of https://github.com/vector-im/riot-web/issues/1528
2018-12-12 13:21:13 -07:00
Travis Ralston c4452909e7 Support .well-known autodiscovery in the js-sdk (#799)
* Support .well-known autodiscovery in the js-sdk

It's much more useful here than in the react-sdk as it can be reused by more applications. This is also required to make the react-sdk a little easier to manage .well-known lookups as soon it'll be doing it in several places. 

Automatic discovery is an abstract concept in the spec and could include more than .well-known in the future, so this is made to be generic enough to support future mechanisms and other resources to discover. There's also a ton of comments (more than normally needed) as people may wish to use this as a reference in their own implementation and it doesn't hurt to explain what everything is doing.

Many of the functions are air lifted from the react-sdk and modified to work within the confines of the js-sdk.

* Swap out uglify-js for uglify-es

So we can start using ES6 dependencies without figuring out how to update babel. 

`uglify-es` is compatible with `uglify-js@3` (we were using `@2`) , which is why the same command is used. This commit includes changes to the command line to make the thing run the same as before too.

* Appease the linter

* Appease the linter some more

* Appease the linter: the tiebreaker

* Appease the linter yet again

* Switch to using the already available URL libraries

* Remove excess logging
2018-12-12 08:38:05 -07:00
Bruno Windels 2d3669b03b Merge pull request #749 from matrix-org/bwindels/releaseimprovements
Extra checks to avoid release script blowing up mid-process.
2018-12-12 09:07:12 +00:00
Travis Ralston 848e6e5897 Support reading custom status messages
Part of https://github.com/vector-im/riot-web/issues/1528
2018-12-11 21:41:15 -07:00
David Baker c723b76138 Merge branch 'master' into develop 2018-12-10 13:37:15 +00:00
David Baker 57f6b0af09 v0.14.2 2018-12-10 13:31:37 +00:00
David Baker 1c4082af45 Prepare changelog for v0.14.2 2018-12-10 13:31:37 +00:00
David Baker eece5d318e Merge pull request #800 from matrix-org/travis/mto-via
Move glob regex utilities out of the pushprocessor and into a more generic place
2018-12-07 17:47:56 +00:00
David Baker bb6ade2165 v0.14.2-rc.1 2018-12-06 11:11:29 +00:00
David Baker 586b010811 Prepare changelog for v0.14.2-rc.1 2018-12-06 11:11:28 +00:00
Travis Ralston 84ab0fde51 Appease the linter 2018-12-05 18:13:28 -07:00
Travis Ralston ec18df2c2a Move glob regex utilities out of the pushprocessor and into a more generic place 2018-12-05 18:01:12 -07:00
David Baker f50503e7c1 Merge pull request #794 from uhoreg/e2e_backup-test-fix
fix some assertions in e2e backup unit test
2018-12-04 11:22:20 +00:00
Travis Ralston c619e5c381 Merge pull request #798 from matrix-org/hs/fix-media-config
Config should be called with auth
2018-12-03 18:29:56 -07:00
Will Hunt e7c4a74ed6 Config should be called with auth 2018-12-04 01:27:08 +00:00
Hubert Chathi f8ea019f02 fix some assertions in e2e backup unit test 2018-11-29 11:52:19 -05:00
David Baker 6db8dd620d Fix https://github.com/matrix-org/matrix-js-sdk/pull/792 2018-11-28 16:11:28 +00:00
David Baker bdc1fa4c03 Merge pull request #792 from matrix-org/dbkr/dont_reestablish_unknwon_device
Don't re-establish sessions with unknown devices
2018-11-28 16:04:31 +00:00
David Baker 4e66a2d436 Don't re-establish sessions with unknown devices
as it won't work
2018-11-28 15:53:13 +00:00
David Baker 0fa948448e Merge branch 'master' into develop 2018-11-22 16:48:36 +00:00
David Baker 76c675cd09 v0.14.1 2018-11-22 16:46:26 +00:00
David Baker 85a4a594c5 Prepare changelog for v0.14.1 2018-11-22 16:46:25 +00:00
David Baker f70746c50f Handle crypto db version upgrades 2018-11-22 16:38:10 +00:00
David Baker 712490b671 Use a vaguely recent node 2018-11-21 18:44:21 +00:00
David Baker b580e68469 Merge pull request #736 from matrix-org/dbkr/e2e_backups
Support for e2e key backups
2018-11-21 18:27:45 +00:00
David Baker bd2cf18fbc Merge pull request #786 from matrix-org/dbkr/e2e_backups_passphrase
Passphrase Support for e2e backups
2018-11-21 18:24:59 +00:00
David Baker 092f4217b0 docs 2018-11-21 17:56:02 +00:00
David Baker abd2ac7168 Rename backup API call in test 2018-11-20 16:34:04 +00:00
David Baker eeea70640e Add randomString factored out from client secret 2018-11-20 16:28:29 +00:00
David Baker 6047838f53 lint 2018-11-20 16:17:58 +00:00
David Baker cb51799246 Make backup restore work 2018-11-20 16:15:29 +00:00
David Baker 44d99277fe Support passphrase-based e2e key backups 2018-11-20 13:09:59 +00:00
David Baker 5b8e643541 Merge branch 'master' into develop 2018-11-19 15:11:40 +00:00
David Baker ae85c209ab v0.14.0 2018-11-19 15:09:43 +00:00
David Baker 2306caa62f Prepare changelog for v0.14.0 2018-11-19 15:09:42 +00:00
Richard van der Hoff 17c11ae23f Merge pull request #783 from matrix-org/rav/sso_login_type
Add 'getSsoLoginUrl' function
2018-11-19 14:13:52 +01:00
Richard van der Hoff 5b51096e37 fix default login type for getSsoLoginUrl 2018-11-19 13:08:41 +00:00
Travis Ralston ac79d6bcee Merge pull request #784 from matrix-org/bwindels/missingheroescrash
Fix: don't set the room name to null when heroes are missing.
2018-11-16 13:23:57 -07:00
David Baker b6e056f832 Merge pull request #785 from matrix-org/dbkr/crypto_db_version_upgrade
Handle crypto db version upgrades
2018-11-16 17:32:40 +00:00
David Baker d99a22d68d Update to new API
Also fix test & remove debug logging from test
2018-11-16 14:46:18 +00:00
David Baker 2602c155d0 Handle crypto db version upgrades 2018-11-16 11:31:08 +00:00
Bruno Windels 80f562643f never return null as a name 2018-11-16 10:38:44 +01:00
Richard van der Hoff 578cb4e268 Add 'getSsoLoginUrl' function 2018-11-15 19:00:37 +00:00
David Baker c53c6a94d7 Update package-lock
so versions are consistent
2018-11-15 16:38:35 +00:00
David Baker 907cf19f05 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-11-15 16:35:52 +00:00
David Baker 88682e1c3b Merge pull request #780 from matrix-org/dbkr/olm_session_unwedge
Restart broken Olm sessions
2018-11-15 16:34:10 +00:00
David Baker 20a4edf899 Merge pull request #776 from matrix-org/dbkr/use_session_last_received_message
Use the last olm session that got a message
2018-11-15 16:33:59 +00:00
David Baker 3222b11346 v0.14.0-rc.1 2018-11-15 15:06:39 +00:00
David Baker fc9d6a6d47 Prepare changelog for v0.14.0-rc.1 2018-11-15 15:06:39 +00:00
David Baker c9917e4079 lint 2018-11-15 10:03:16 +00:00
Bruno Windels b233ab87bb Merge branch 'master' into develop 2018-11-14 18:21:01 +01:00
Bruno Windels 73c3a709de Merge branch 'release-v0.13.1' 2018-11-14 18:19:24 +01:00
Bruno Windels 6ce7b30b72 v0.13.1 2018-11-14 18:18:12 +01:00
Bruno Windels 980d55a2f3 Prepare changelog for v0.13.1 2018-11-14 18:18:11 +01:00
Richard Lewis 988be62804 Add function to get currently joined room members. 2018-11-14 18:15:14 +01:00
Richard Lewis 23efd0850d Add function to get currently joined rooms. 2018-11-14 18:15:08 +01:00
David Baker 17e0f1d9ab Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:58:21 +00:00
David Baker 3c85bd55d3 Time goes forwards 2018-11-14 14:57:48 +00:00
David Baker 2298d72ab9 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:35:33 +00:00
David Baker 408407b33d Fix typo 2018-11-14 14:34:36 +00:00
David Baker ab426384e1 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:30:10 +00:00
David Baker 5bc68c0c6d Handle last received message ts being undefined 2018-11-14 14:29:03 +00:00
David Baker ebf20d5b2c Don't force more than one new session per device per hour 2018-11-14 14:20:55 +00:00
David Baker 93d9c40323 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:20:42 +00:00
Bruno Windels c6ea976d7f Merge branch 'master' into develop 2018-11-14 10:55:13 +01:00
Bruno Windels 5f24915300 Merge branch 'release-v0.13.0' 2018-11-14 10:54:52 +01:00
Bruno Windels fbe174fb64 v0.13.0 2018-11-14 10:53:42 +01:00
Richard Lewis 977d5331c0 Update CHANGELOG. 2018-11-14 10:50:05 +01:00
Richard Lewis d40d7e18f5 Update CHANGELOG. 2018-11-14 10:49:54 +01:00
Richard Lewis 11be68ad49 Refactor code to base 'login' method. 2018-11-14 10:49:44 +01:00
Richard Lewis b0d0782a72 Linting. 2018-11-14 10:49:36 +01:00
Richard Lewis dbb6d8ac71 Set access_token and user_id after login in with username and password. 2018-11-14 10:49:25 +01:00
David Baker a30845f9ce lint 2018-11-14 08:03:23 +00:00
David Baker 379f290b8b Add package-lock.json
to force base-x to version 3.0.4 because 3.0.5 breaks the build
by exporting ES6.
2018-11-13 14:49:56 +00:00
David Baker 6c413bba48 Typo + exit if session was never shared 2018-11-13 12:15:33 +00:00
David Baker e17a39d446 PR feedback 2018-11-13 12:10:26 +00:00
David Baker fcadf6ec4a Store last received message ts on olm session 2018-11-12 18:12:43 +00:00
David Baker 231fde219c Store last received message ts on olm session 2018-11-12 18:10:11 +00:00
David Baker 2774bd238b Merge pull request #781 from matrix-org/travis/block-send-if-failed
Prevent messages from being sent if other messages have failed to send
2018-11-09 10:54:38 +00:00
Travis Ralston fed67192bc Fix test: Actually resend the event after unknown devices are found
This test didn't actually test that resending would work, despite its name.
2018-11-08 17:17:30 -07:00
Travis Ralston 16db970558 Appease the linter 2018-11-08 16:51:21 -07:00
Travis Ralston c9a79bf32e Prevent messages from being sent if other messages have failed to send
Fixes https://github.com/vector-im/riot-web/issues/5408
2018-11-08 16:46:03 -07:00
David Baker d74ed508f9 Restart broken Olm sessions
* Start a new Olm sessions with a device when we get an undecryptable
   message on it.
 * Send a dummy message on that sessions such that the other end knows
   about it.
 * Re-send any outstanding keyshare requests for that device.

Also includes a unit test for megolm that isn't very related but came
out as a result anyway.

Includes https://github.com/matrix-org/matrix-js-sdk/pull/776
Fixes https://github.com/vector-im/riot-web/issues/3822
2018-11-08 19:09:28 +00:00
David Baker eafba9c7ef Merge pull request #777 from matrix-org/dbkr/olm_unit_test
A unit test for olm
2018-11-06 12:04:55 +00:00
Richard Lewis 610923af89 Merge pull request #778 from matrix-org/rxl881/loginWithPassword
Set access_token and user_id after login in with username and password.
2018-11-05 17:25:31 +00:00
Richard Lewis 23dfeb13df Update CHANGELOG. 2018-11-05 17:17:22 +00:00
Richard Lewis f4abd7d027 Update CHANGELOG. 2018-11-05 17:06:39 +00:00
Richard Lewis b716e71784 Refactor code to base 'login' method. 2018-11-05 16:40:11 +00:00
Richard Lewis 094598196a Linting. 2018-11-05 16:02:30 +00:00
David Baker db1d1c49a0 Merge pull request #779 from matrix-org/rxl881/getRooms
Add function to get currently joined rooms.
2018-11-05 12:14:12 +00:00
David Baker ff4125c11e Remove unused stuff & comments 2018-11-05 09:39:46 +00:00
Richard Lewis a0d51803ed Add function to get currently joined room members. 2018-11-05 00:08:04 +00:00
Richard Lewis 3aabd63975 Add function to get currently joined rooms. 2018-11-04 21:49:17 +00:00
Richard Lewis 394e37f9ea Set access_token and user_id after login in with username and password. 2018-11-04 19:43:18 +00:00
Travis Ralston 369b88d6f8 Merge pull request #775 from matrix-org/travis/cleanup-request
Remove the request-only stuff we don't need anymore
2018-11-02 09:39:23 -06:00
David Baker ec8b3ae515 Lint (and also name the test right) 2018-11-02 12:33:46 +00:00
David Baker c94382b46c A unit test for olm
Megolm has plenty but none for just the olm layer by itself. I
was playing with getting session re-establishment to work and a
unit test came out.
2018-11-02 12:05:13 +00:00
David Baker 2a6a67c6cc Inbound session creation counts as a received message 2018-11-01 17:08:43 +00:00
David Baker 37f0a9ad7b Try tests on node 10 2018-11-01 13:54:41 +00:00
David Baker 28540ad50a Use the last olm session that got a message
Implements https://github.com/matrix-org/matrix-doc/pull/1596
For https://github.com/vector-im/riot-web/issues/3822
Requires https://github.com/matrix-org/olm-backup/pull/77 (+release)
2018-11-01 12:59:38 +00:00
David Baker 29d92d3e81 Lint 2018-10-31 20:05:21 +00:00
David Baker 0477f354c9 Fix key forwarded count
It's exported in snake case
2018-10-31 19:45:29 +00:00
David Baker c7a0c1402c refer to getAllEndToEndInboundGroupSessions for magic numbers 2018-10-31 19:39:07 +00:00
David Baker 2af5643243 Clarify comment 2018-10-31 19:37:19 +00:00
David Baker 5e9885946f random double linebreak 2018-10-31 19:36:30 +00:00
Travis Ralston e89879d8a6 Remove the request-only stuff we don't need anymore
This was introduced in https://github.com/matrix-org/matrix-react-sdk/pull/2250 but can be pulled out due to https://github.com/matrix-org/matrix-js-sdk/pull/770. See https://github.com/vector-im/riot-web/issues/7634 for more information about the future.
2018-10-31 13:07:31 -06:00
David Baker 2f219f83db Catch exceptions from backupGroupSession() 2018-10-31 18:46:02 +00:00
David Baker 63e9f794c7 Remove unnecessary if 2018-10-31 18:37:38 +00:00
David Baker 7c0b910d7a remove unnecessary isFinite check 2018-10-31 18:36:55 +00:00
David Baker c77ecad9a5 clarify comment 2018-10-31 18:34:49 +00:00
David Baker db2897cf1e Remove spurious interlopers 2018-10-31 18:33:31 +00:00
David Baker 5c5ce0dfe3 Typo 2018-10-31 18:32:48 +00:00
David Baker 6de213483c Change getDeviceByIdentityKey() to just the 2 arg version 2018-10-31 18:31:56 +00:00
David Baker f5846b89ea More modern loop syntax 2018-10-31 18:18:41 +00:00
David Baker c5e7bedb37 Conclusion: no, it shouldn't 2018-10-31 18:08:53 +00:00
David Baker 2b46c560c7 Add crypto. prefix to keyBackupStatus event 2018-10-31 18:07:12 +00:00
David Baker c6ad0665b5 factor out duplicated test code 2018-10-31 18:03:40 +00:00
David Baker 8ab84dee16 PR feedback 1/n 2018-10-31 17:40:17 +00:00
Travis Ralston 62b2c07be2 Merge pull request #770 from matrix-org/travis/fix-request
Manually construct query strings for browser-request instances
2018-10-30 15:18:33 -06:00
Travis Ralston 2fb29ae8fd Use the right query string lib 2018-10-30 14:59:57 -06:00
Travis Ralston b57e858ad1 We can't use arrow functions, apparently. 2018-10-30 14:27:51 -06:00
Travis Ralston 054aac17aa Just use the interface provided to us rather than hack in a flag 2018-10-30 14:25:25 -06:00
Travis Ralston 68b65dd357 Because uglify-js breaks everything 2018-10-30 14:12:46 -06:00
Travis Ralston f2881126cd Manually construct query strings for browser-request instances
Because `request` just doesn't work for us in the browser, but `browser-request` is fine despite us having to do our own query strings.

Fixes https://github.com/vector-im/riot-web/issues/7620
2018-10-30 13:59:29 -06:00
Bruno Windels 11968a5888 Merge pull request #769 from matrix-org/bwindels/initcryptoll
Fix: correctly check for crypto being present
2018-10-30 16:13:32 +00:00
Bruno Windels ad279dc566 correctly check for crypto being present 2018-10-30 16:05:44 +01:00
David Baker 2814932845 lint 2018-10-30 12:36:03 +00:00
David Baker a2430dbc53 Fix DeviceList index of users by identity key
Was causing all keys to be send as unverified
2018-10-30 12:29:44 +00:00
David Baker e51d2dd36a Fix a few e2e backup bits
* Don't _maybeSendKeyBackup() as soon as we enable them: we shouldn't
   have anything to send anyway until we mark all sessions for backup,
   which we do just afterwards, so leave that to trigger the upload
   (otherwise the uploading triggered by backupAll just returns
   straight away because a backup is already in progress).
 * Pass delay & retry params to _maybeSendKeyBackup(): we want the
   all-key upload to happen straight away so pass in delay=0, and
   we also don't want to retry on a timer if the the user is waiting.
 * If we fail due to an HTTP 400 or similar, don't swallow the error.
 * Use the right indexeddb store
2018-10-30 11:45:19 +00:00
David Baker 604af1ac8c Merge branch 'master' into develop 2018-10-29 14:01:26 +00:00
David Baker 68c6393eb2 v0.12.1 2018-10-29 13:59:39 +00:00
David Baker 4cbf9c7f47 Prepare changelog for v0.12.1 2018-10-29 13:59:39 +00:00
Travis Ralston 8bb3b75b1d Merge pull request #768 from aaronraimist/lint
Update babel-eslint to 8.1.1
2018-10-26 21:13:04 -06:00
Aaron Raimist a76f0c7cb4 Update babel-eslint to 8.1.1
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2018-10-26 22:07:40 -05:00
Travis Ralston 01e31afcbd Merge pull request #764 from matrix-org/travis/permalink-routing
Support `request` in the browser and support supplying servers to try in joinRoom()
2018-10-26 14:22:51 -06:00
David Baker 3b2f2f922e Bump db version 2018-10-26 18:49:05 +01:00
Travis Ralston 64b83b3245 Merge branch 'develop' into travis/permalink-routing 2018-10-26 09:20:54 -06:00
David Baker 563e6b3cdd Fix jsdoc 2018-10-26 13:23:37 +01:00
David Baker 6518bff2ac Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-26 12:09:38 +01:00
David Baker 0e26247b53 Speed up time rather than increasing timeouts 2018-10-26 12:08:55 +01:00
David Baker e69f7dbc5f Merge pull request #767 from matrix-org/dbkr/loglevel_dep
loglevel should be a normal dependency
2018-10-26 11:26:15 +01:00
David Baker 4d0f6df89a alphabetical order 2018-10-26 11:23:05 +01:00
David Baker 6b184363a1 loglevel should be a normal dependency
rather than a dev dependency
2018-10-26 11:16:20 +01:00
David Baker b519069634 Merge pull request #766 from matrix-org/dbkr/stop_devicelist
Stop devicelist when client is stopped
2018-10-26 11:14:36 +01:00
Travis Ralston 1bd44a7427 Merge remote-tracking branch 'origin/develop' into travis/permalink-routing 2018-10-25 14:36:46 -06:00
Travis Ralston 568ff5a3f5 Appease the linter 2018-10-25 14:36:14 -06:00
David Baker a6bf40d4e2 We can always import these now 2018-10-25 19:21:29 +01:00
David Baker b3bb99d76a Stop client after backup tests 2018-10-25 19:11:43 +01:00
David Baker 243bab7036 Merge branch 'dbkr/stop_devicelist' into dbkr/e2e_backups 2018-10-25 19:03:57 +01:00
David Baker 88b39f4b67 Stop devicelist when client is stopped
To avoid the devicelist trying to save after the client has been
stopped

Hopefully will fix random test failures on node 11.
2018-10-25 19:00:03 +01:00
David Baker 5e8061f846 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-25 17:33:48 +01:00
David Baker 870e96a1df Merge pull request #743 from matrix-org/dbkr/wasm
Update to WebAssembly-powered Olm
2018-10-25 15:58:18 +01:00
David Baker 59070c2af6 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-25 15:29:41 +01:00
David Baker cec8936728 Merge pull request #763 from Ryuno-Ki/logging-lib
Logging lib. Fixes #332
2018-10-25 15:28:47 +01:00
David Baker 14071b0d31 Merge pull request #765 from matrix-org/dbkr/mock_request_stop
Use new stop() method on matrix-mock-request
2018-10-25 15:16:08 +01:00
David Baker 57173e4385 Use mock-request 1.2.2 2018-10-25 15:01:05 +01:00
David Baker 997caad985 jsdoc 2018-10-25 14:43:17 +01:00
David Baker 2b752c0c02 Use new stop() method on matrix-mock-request
To finish all pending flushes between tests. This stops the unit
tests from hanging on node 11 when run in certain combinations.

Requires https://github.com/matrix-org/matrix-mock-request/pull/6
(so will need a release of matrix-mock-request before merging)
2018-10-25 14:29:25 +01:00
Travis Ralston 2cccb8b450 Install memfs because webpack is made of fail 2018-10-24 16:57:36 -06:00
Travis Ralston 0c540ac8de Re-add the querystring options 2018-10-24 16:36:12 -06:00
Travis Ralston 6033b7b886 Update request and browserify; Use request in the browser 2018-10-24 16:36:00 -06:00
Travis Ralston b67f8d1389 Merge branch 'develop' into travis/permalink-routing 2018-10-24 16:22:18 -06:00
André Jaenisch ae645ad9f0 Use Node.js module export, since ES6 export breaks build.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:52:50 +02:00
André Jaenisch 5b72509dac Fix broken build.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:12:51 +02:00
André Jaenisch 3ce42a096b Add Apache license banner.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:06:04 +02:00
André Jaenisch 8331c2f267 Use ES6 export instead of CommonJS.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:05:21 +02:00
André Jaenisch b3c9570b0f Remove 'use strict'.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:56:29 +02:00
André Jaenisch 9d5c877df9 Set loglevel to DEBUG to remain current behaviour.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:55:54 +02:00
André Jaenisch a8e2727473 Set level of logger instead of all of them.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:55:22 +02:00
David Baker 4b9c6e6bd2 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-24 19:15:04 +01:00
David Baker d29ac088c0 retest 2018-10-24 18:55:04 +01:00
David Baker 3a316de9ef Update to Olm 3 here too 2018-10-24 17:40:58 +01:00
David Baker 40cb37e824 Update to Olm 3 2018-10-24 17:37:33 +01:00
David Baker f165b55a1d Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-10-24 17:15:36 +01:00
David Baker 84b91d4575 Update to Olm 3 2018-10-24 16:58:48 +01:00
David Baker f5832423f4 v0.12.1-rc.1 2018-10-24 11:14:59 +01:00
David Baker 73dd07aadf Prepare changelog for v0.12.1-rc.1 2018-10-24 11:14:59 +01:00
André Jaenisch 0f39a45734 Fixing module export of logger. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 01:44:10 +02:00
André Jaenisch f41060c39a Replace console.log with loglevel logger. Fixes #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:57 +02:00
André Jaenisch bbb8e12bac Create logger module. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:57 +02:00
André Jaenisch d0e1471c91 Added loglevel library. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:52 +02:00
Hubert Chathi 322ef1fd63 update backup algorithm name to agree with the proposal 2018-10-22 11:28:16 -04:00
Travis Ralston 47cb97bc60 Merge pull request #762 from aaronraimist/valid-package-json
Add repository type to package.json to make it valid
2018-10-20 21:55:48 -06:00
Aaron Raimist 8d35bea830 Add repository type to package.json to make it valid
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2018-10-20 21:38:04 -05:00
Travis Ralston d8bcc4e3f1 Initial support for specifying which servers to try in joinRoom
This has a bug when using browser-request where the query string for `server_name: [a, b]` comes out as `?server_name=a,b` instead of `?server_name=a&server_name=b`. This is due to browser-request not supporting the same qs options as request, so the qsStringifyOptions do nothing.
2018-10-19 13:34:22 -06:00
Hubert Chathi 434ac86090 properly fill out the is_verified and first_message_index fields 2018-10-19 10:51:19 -04:00
Travis Ralston 1061026f96 Merge pull request #761 from Half-Shot/hs/upload-limits
Add getMediaConfig()
2018-10-16 09:04:08 -06:00
Will Hunt e638c49160 Merge remote-tracking branch 'upstream/develop' into hs/upload-limits 2018-10-16 11:32:21 +01:00
David Baker 5d84db9fb7 Merge branch 'release-v0.12.0' 2018-10-16 10:49:14 +01:00
David Baker 874bdea634 v0.12.0 2018-10-16 10:47:33 +01:00
David Baker 68497d3a1f Prepare changelog for v0.12.0 2018-10-16 10:47:32 +01:00
David Baker b9e198c172 Oops: remove debug logging 2018-10-15 11:39:39 +01:00
Hubert Chathi 40d0a82342 remove accidental change to eslintrc 2018-10-12 15:45:48 -04:00
Hubert Chathi d49c0a1bcb more de-linting and fixing 2018-10-12 14:28:31 -04:00
Hubert Chathi 91fb7b0a7c fix unit tests for backup recovery 2018-10-12 12:03:51 -04:00
Hubert Chathi 9b12c22823 de-lint plus some minor fixes 2018-10-12 10:38:10 -04:00
Hubert Chathi 3957006fae Merge remote-tracking branch 'upstream/dbkr/e2e_backups' into e2e_backups 2018-10-11 14:01:26 -04:00
David Baker 874029dff0 oops - fix changelog format in retrospect 2018-10-11 15:38:34 +01:00
David Baker 6aff3ed407 v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker c0ae78ae82 Prepare changelog for v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker 8b22f01ecd Merge pull request #760 from matrix-org/bwindels/fixleavesinmemberlist
never replace /sync'ed memberships with OOB ones
2018-10-11 14:01:38 +01:00
Bruno Windels 2ed694b041 remove supersedes OOB logic 2018-10-11 14:32:03 +02:00
Bruno Windels a0ef6ab811 typo 2018-10-11 14:14:42 +02:00
Bruno Windels d098b39024 never replace /sync'ed memberships with OOB ones 2018-10-11 14:11:40 +02:00
David Baker 3cf23f8a5c Document breaking change 2018-10-11 12:13:25 +01:00
Hubert Chathi fc59bc2992 add localstorage support for key backups 2018-10-10 19:32:07 -04:00
Hubert Chathi da65f43983 wrap backup sending in a try, and add delays 2018-10-10 19:31:28 -04:00
Bruno Windels 72e77d237a Merge pull request #759 from matrix-org/dbkr/lazy_load_check_before_sync
Don't fail to start up if lazy load check fails
2018-10-10 18:28:47 +02:00
David Baker ecc3e18e85 typo 2018-10-10 17:27:06 +01:00
David Baker dea70af889 remove debug logging 2018-10-10 17:06:26 +01:00
David Baker 30362091e5 Don't fail to start up if lazy load check fails
Do the lazy loading check in the batch of things we do before
starting a sync rather than at client start time, so we don't fail
to start the client if we can't hit the HS to determine LL support.

Fixes https://github.com/vector-im/riot-web/issues/7455
2018-10-10 16:59:36 +01:00
David Baker ada4b6ef16 Lint 2018-10-09 15:46:12 +01:00
David Baker 59e6066579 Replace base58check with a simple parity check
base58check seems way overcomplicated for this purpose (plus the
module was exporting an es6 file, breaking the js-sdk build). A
parity check empirically detects single substitution and transposition
errors. Another option would be Luhn's algorithm.
2018-10-09 14:15:03 +01:00
David Baker 0aa3362671 Merge pull request #754 from matrix-org/dbkr/e2e_on_interslice
Make e2e work on Edge
2018-10-09 10:48:24 +01:00
David Baker 5873db7331 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-09 10:47:11 +01:00
David Baker b3fe05ec81 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-09 10:35:15 +01:00
David Baker 92fbf58b13 Merge pull request #758 from matrix-org/bwindels/betteridberrors
throw error with same name and message over idb worker boundary
2018-10-09 09:59:55 +01:00
Bruno Windels a4b2cc84c7 Merge pull request #755 from matrix-org/travis/fix-vuln-warning
Default to a room version of 1 when there is no room create event
2018-10-08 09:55:10 +02:00
David Baker 89c3f6fa0e Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-05 14:01:43 +01:00
David Baker 1395da694e Merge pull request #757 from matrix-org/dbkr/gone_away_is_the_bluebird
Silence bluebird warnings
2018-10-05 13:25:16 +01:00
David Baker 264b20535e Silence bluebird warnings 2018-10-05 12:13:05 +01:00
Bruno Windels caba350b33 throw error with same name and message over idb worker boundary
instead of string currently thrown. This allows handling error
from the main thread.
2018-10-05 12:48:12 +02:00
Travis Ralston d9fe194111 Default to a room version of 1 when there is no room create event
Fixes https://github.com/vector-im/riot-web/issues/7331

There is something to be worried about when there is no room create event, however. This should always be available, although due to cache problems or servers that don't provide the event we can't be sure of this.
2018-10-04 13:42:07 -06:00
Hubert Chathi 258adda67c retry key backups when they fail 2018-10-04 15:19:20 -04:00
David Baker 40dc13b2e2 lint try 2 2018-10-04 15:38:08 +01:00
David Baker 4cda54ca1c lint 2018-10-04 15:15:30 +01:00
David Baker 8116c5b3f7 Make e2e work on Edge
We were sucessfully opening indexeddb but any queries using compound
indicies were failing because Edge doesn't support them, so messages
were failing to decrypt with 'DataError'.

Try a dummy query at startup, so if it fails we fall back to a
different store (ie. end up using localstorage on Edge).
2018-10-04 13:49:32 +01:00
David Baker 35d584c67b Remove outdated comment 2018-10-04 13:05:45 +01:00
David Baker 9504cbcc4f Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-04 11:19:37 +01:00
Bruno Windels 1dcc5127d0 Merge pull request #750 from matrix-org/bwindels/allownonffmerge
allow non-ff merge from release branch into master
2018-10-04 10:22:02 +02:00
Bruno Windels 6790699279 Merge pull request #739 from matrix-org/ben/new-examples
add new examples, to be expanded into a post
2018-10-04 10:21:36 +02:00
David Baker 85e3d7083c Merge pull request #751 from matrix-org/dbkr/indexeddb_errors
Reject with the actual error on indexeddb error
2018-10-03 16:22:26 +01:00
David Baker 262ace1773 commit the recovery key util file 2018-10-03 10:20:57 +01:00
David Baker 7cd101d8cb Fix recovery key format 2018-10-02 19:22:10 +01:00
David Baker ce2058aea9 Merge branch 'dbkr/wasm' into dbkr/e2e_backups 2018-10-02 16:54:36 +01:00
David Baker e9b0acaa8e Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-02 16:50:37 +01:00
David Baker bd2da08c4e Reject with the actual error on indexeddb error
Rather than the event
2018-10-02 16:48:27 +01:00
Bruno Windels 0a88d419c6 allow non-ff merge from release branch into master 2018-10-01 17:21:01 +02:00
Bruno Windels 80c190db36 Merge branch 'master' into develop 2018-10-01 15:40:16 +02:00
Bruno Windels 550cf00ee7 Merge branch 'release-v0.11.1' 2018-10-01 15:36:37 +02:00
Ben Parsons fbca7951fc improvements suggested by Bruno 2018-10-01 14:29:24 +01:00
Bruno Windels 1e1358fcef v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels fd1b3329f5 Prepare changelog for v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels 9c9d8468a5 Merge pull request #744 from matrix-org/dbkr/update_mocha
Update mocha to v5
2018-09-28 12:57:49 +01:00
Bruno Windels 55ca03f100 make release compatible with latest release of hub (2.5) 2018-09-28 12:51:31 +02:00
Bruno Windels 83708725b2 check youre logged in with correct npm user when releasing 2018-09-28 11:16:00 +02:00
David Baker 6f59d62e5c Merge pull request #748 from matrix-org/bwindels/nollforguests
disable lazy loading for guests as they cant create filters
2018-09-27 19:29:54 +01:00
Bruno Windels 1c348f0cdb disable lazy loading for guests as they cant create filters 2018-09-27 18:55:21 +01:00
Bruno Windels 634596257d v0.11.1-rc.1 2018-09-27 11:47:28 +01:00
Bruno Windels 5e4973a1dc changelog 2018-09-27 11:45:33 +01:00
Bruno Windels 19f023e0ee Revert "v0.11.1-rc.1"
This reverts commit 090c15fe19.
2018-09-27 11:42:19 +01:00
Bruno Windels 090c15fe19 v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels e8b307dc4f Prepare changelog for v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels 056479d450 Revert "v0.11.1-rc.1"
This reverts commit 847d40e567.
2018-09-27 11:35:52 +01:00
Bruno Windels e5ebe2f888 Merge pull request #747 from matrix-org/bwindels/releasehubversion
make usage of hub compatible with latest version (2.5)
2018-09-27 11:30:17 +01:00
Bruno Windels e8e1b431ad make usage of hub compatible with latest version (2.5) 2018-09-27 11:28:01 +01:00
Bruno Windels 847d40e567 v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels cf6c555e6a Prepare changelog for v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels b508aa9ebc Merge pull request #746 from matrix-org/bwindels/resynconlltoggle
Detect when lazy loading has been toggled in client.startClient
2018-09-27 09:49:01 +01:00
Bruno Windels 5e7634506e Merge branch 'develop' into bwindels/resynconlltoggle 2018-09-26 18:23:07 +01:00
Bruno Windels ba39b64ced re-enable test 2018-09-26 18:00:40 +01:00
David Baker 33ad65a105 Don't assume Olm will be available from start
By doing `Olm = global.Olm` on script load, we require that Olm is
available right from the start, which isn't really necessary. As
long as it appears some time before we actually want to use it,
this is fine (we can probably assume it's not going to go away
again..?)

This means Riot doesn't need to faff about making sure olm is
loaded before starting anything else.
2018-09-26 16:39:22 +01:00
Travis Ralston fcebe89f6c Merge branch 'hs/upload-limits' into develop 2018-09-26 09:14:12 -06:00
Bruno Windels 2d5eb920b8 pass lazy loading flag into error, to format message based on it 2018-09-26 16:12:30 +01:00
Travis Ralston 26de2c86ed Merge pull request #745 from matrix-org/revert-644-hs/upload-limits
Revert "Add getMediaLimits to client"
2018-09-26 09:12:03 -06:00
Travis Ralston cba1e95d0a Revert "Add getMediaLimits to client" 2018-09-26 09:11:28 -06:00
Bruno Windels 78a5a88638 fix jsdoc lint and better naming 2018-09-26 12:49:33 +01:00
Bruno Windels b7b9c67259 fix lint 2018-09-26 12:49:26 +01:00
Bruno Windels 54bff81470 clear sync data on toggling LL,also throw spec. error and delegate clear
the sync data needs to be cleared toggling in both directions:
 not LL -> LL: you want to get rid of all the excess state in the
               sync data to get the RAM benefits
 LL -> not LL: you want to fill the sync data state again
               because getOutOfBandMembers won't be called
2018-09-26 12:39:56 +01:00
David Baker fe21972f4a Update mocha to v5
Mostly to get the non-vulnerable version of node-growl
2018-09-26 11:36:06 +01:00
Bruno Windels 58e3c72446 only store serializable options (string, boolean, number) 2018-09-26 11:34:58 +01:00
Bruno Windels 6dd5c6c317 fix existing missing this 2018-09-26 11:33:19 +01:00
Bruno Windels 4e0af3eafe don't return the IDBEvent from storeClientOptions
as it's not needed and not cloneable
2018-09-26 11:32:43 +01:00
Bruno Windels 1d0791142c all store methods should return a promise 2018-09-26 11:32:11 +01:00
Bruno Windels 2560ba2980 dont clear the store if its a brand new one 2018-09-26 10:37:52 +01:00
Bruno Windels 19be3dd852 fix lint 2018-09-26 10:13:40 +01:00
Travis Ralston 40f2a6978b Merge pull request #644 from Half-Shot/hs/upload-limits
Add getMediaLimits to client
2018-09-25 18:12:55 -06:00
Bruno Windels 1fd8c43d94 fix tests 2018-09-25 18:50:09 +01:00
David Baker 63cc3fd890 lint 2018-09-25 18:14:11 +01:00
David Baker c556ca40b1 Support Olm with WebAssembly
wasm Olm has a new interface: it now has an init method that needs
to be called and the promise it returns waited on before the Olm
module is used. Support that, and allow Crypto etc to be imported
whether Olm is enabled or not. Change whether olm is enabled to
be async since now it will be unavailable if the async module init
fails. Don't call getOlmVersion() until the Olm.init() is done.
2018-09-25 17:49:54 +01:00
Matthew Hodgson 3e32f47903 Merge pull request #742 from matrix-org/travis/build-process
Split npm start into an init and watch script
2018-09-25 17:31:23 +01:00
Travis Ralston 8f2824186a Split npm start into an init and watch script
This is to better support riot-web's build process without losing the functionality supplied by `npm start`. The watch script no longer performs an initial build and thus `start:init` has been created for this purpose.
2018-09-25 10:09:58 -06:00
Bruno Windels b0dbb20e22 fixup of in memory stores 2018-09-25 15:53:40 +01:00
Bruno Windels 0519c4c6b1 await startClient and use promises also so error gets shown 2018-09-25 15:53:14 +01:00
Bruno Windels 28184b4a29 check if lazy loading was enabled before in startClient 2018-09-25 15:32:10 +01:00
Bruno Windels 76175abea2 allow storing client options in indexeddb
so we can tell what options the sync data was created with
2018-09-25 15:30:35 +01:00
Ben Parsons d28f829b1c add new examples, to be expanded into a post 2018-09-20 13:12:50 +01:00
Bruno Windels 184c3dce15 Merge pull request #738 from matrix-org/revert-733-bwindels/roomnamealias
Revert "room name should only take canonical alias into account"
2018-09-20 12:23:38 +02:00
David Baker a08a3078da Revert "room name should only take canonical alias into account" 2018-09-20 11:20:49 +01:00
David Baker c2100d7622 Merge pull request #737 from matrix-org/bwindels/fixnamedisambiguation
fix display name disambiguation with LL
2018-09-20 10:22:38 +01:00
Bruno Windels a91fa59174 fix display name disambiguation with LL 2018-09-19 18:14:18 +02:00
Bruno Windels 574a6b68ae Merge pull request #735 from matrix-org/bwindels/fixstalerr
Introduce Room.myMembership event
2018-09-19 13:35:11 +02:00
David Baker 2f4c1dfcc4 Test all 3 code paths on backup restore 2018-09-18 17:33:47 +01:00
David Baker 1b62a21dbd Free PkEncryption/Decryption objects 2018-09-18 16:12:37 +01:00
David Baker a78825eff9 Bump to Olm 2.3.0 for PkEncryption 2018-09-18 15:06:28 +01:00
David Baker 0bad7b213e Fix lint
Remove commented code block as it's not immediately obvious it makes
sense or is the right way of suggesting a key restore.
2018-09-18 14:56:11 +01:00
David Baker e4bb37b1a8 Fix lint mostly 2018-09-18 14:53:59 +01:00
David Baker 54c443ac68 Make tests pass 2018-09-18 14:48:02 +01:00
David Baker 3af9af96ea More linting 2018-09-17 19:31:37 +01:00
David Baker f75d188131 Soe progress on linting 2018-09-17 19:25:42 +01:00
Bruno Windels fc3a00054f add test for new event 2018-09-17 19:33:36 +02:00
Bruno Windels 84e41c2ade fix tests 2018-09-17 18:28:07 +02:00
Bruno Windels 4630733b55 don't fall back anymore to member, as this is more reliable 2018-09-17 18:23:48 +02:00
Bruno Windels eb690e14e4 introduce Room.myMembership event
As you don't always have your own member with lazy loading
of members enabled, looking at the sync response section
where a room appears is the most reliable way of determining
the syncing user's membership in a room.

Before we already used this to read the
current room membership with `room.getMyMembership()`,
but we were still using the `RoomMember.membership` event
to detect when the syncing user's membership changed.

This event will help make those checks work well with LL enabled.
2018-09-17 18:20:49 +02:00
David Baker 009430e829 Add isValidRecoveryKey
Add method to check if a given string is a valid recovery key
2018-09-17 17:04:29 +01:00
David Baker 073fb73ff3 Make multi-room key restore work 2018-09-17 15:59:37 +01:00
David Baker e789747834 Check sigs on e2e backup & enable it if we can 2018-09-14 17:06:27 +01:00
David Baker 833002f846 Merge pull request #733 from matrix-org/bwindels/roomnamealias
room name should only take canonical alias into account
2018-09-14 11:38:26 +01:00
David Baker 3838fab788 WIP e2e key backup support
Continues from uhoreg's branch
2018-09-13 17:01:05 +01:00
Bruno Windels 907e9fc476 fix lint 2018-09-13 10:04:31 +02:00
Bruno Windels b829a39cd2 fix tests 2018-09-13 09:59:20 +02:00
Bruno Windels daa7af0605 room name should only take canonical alias into account 2018-09-13 09:52:21 +02:00
David Baker 47a1adc864 Merge pull request #732 from matrix-org/bwindels/fixcontextstatenotwrapped
state events from context response were not wrapped in a MatrixEvent
2018-09-11 13:27:03 +01:00
Bruno Windels 98e448acdd state events from context response were not wrapped in a MatrixEvent 2018-09-11 14:13:35 +02:00
David Baker 72bd51f26e Merge remote-tracking branch 'origin/develop' into uhoreg-e2e_backups 2018-09-11 12:02:47 +01:00
David Baker 29db856322 Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-09-11 12:00:46 +01:00
David Baker cd5cda916f Merge branch 'master' into develop 2018-09-10 11:35:25 +01:00
David Baker 33a1139772 v0.11.0 2018-09-10 11:33:51 +01:00
David Baker c5b62903f3 Prepare changelog for v0.11.0 2018-09-10 11:33:50 +01:00
David Baker 387fd16b40 v0.11.0-rc.1 2018-09-07 14:28:45 +01:00
David Baker c91b67d370 Prepare changelog for v0.11.0-rc.1 2018-09-07 14:28:44 +01:00
David Baker b809d1c263 Merge pull request #724 from matrix-org/bwindels/fastermemberinsert
Reduce amount of promises created when inserting members
2018-09-07 13:57:44 +01:00
David Baker 7bb6675abf Merge pull request #726 from matrix-org/bwindels/dontwaitforstorage
dont wait for LL members to be stored to resolve the members
2018-09-07 13:57:20 +01:00
David Baker b91bea94f4 Merge pull request #728 from matrix-org/bwindels/wrongemitargs
RoomState.members emitted with wrong argument order for OOB members
2018-09-07 13:57:09 +01:00
David Baker 96e21700bd Merge pull request #727 from matrix-org/dbkr/conn_did_fail
Only emit CATCHUP if recovering from conn error
2018-09-07 13:31:17 +01:00
David Baker 7e8f25bce3 Include 404 in connectivity success error codes 2018-09-07 13:18:45 +01:00
Bruno Windels 9e02049b05 RoomState.members emitted with wrong argument order for OOB members 2018-09-07 14:11:27 +02:00
David Baker affdfccd60 Only emit CATCHUP if recovering from conn error
Have the keepalive promise return a boolean indicating whether it
detected a connectivity failure or not. Use this to only emit
CATCHUP if there was a connectivity error, to try & suppress
the state flip-flopping back & forth between CATCHUP and ERROR
in the case where we have connectivity but the sync is returning
and error for whatever reason.
2018-09-07 11:29:44 +01:00
David Baker 402f8c27a9 Merge pull request #725 from matrix-org/dbkr/fix_sync_error_doc
Fix docstring for sync data.error
2018-09-07 09:44:28 +01:00
Bruno Windels ba4dc6c60a dont wait for LL members to be stored to resolve the members
this can easily add up to 100ms / 1000 of members
2018-09-07 10:44:06 +02:00
David Baker 6b8dd42547 Fox docstring for sync data.error
It's 'error', not 'err'
2018-09-06 19:53:52 +01:00
Bruno Windels 1511a27f4c update/remove comments 2018-09-06 18:09:46 +02:00
Bruno Windels 7ae6c147fa lint doesnt like async 2018-09-06 18:02:19 +02:00
Bruno Windels f51630eb07 dont create a promise for every inserted member but await the transaction instead 2018-09-06 18:01:20 +02:00
Bruno Windels e3586411e0 Merge pull request #723 from matrix-org/revert-721-revert-717-bwindels/fixllroompermission
Re-apply "Don't rely on members to query if syncing user can post to room"
2018-09-06 14:55:24 +02:00
David Baker a0639a32c7 Revert "Revert "Don't rely on members to query if syncing user can post to room"" 2018-09-06 11:47:40 +01:00
David Baker 759c6e77a7 Merge pull request #721 from matrix-org/revert-717-bwindels/fixllroompermission
Revert "Don't rely on members to query if syncing user can post to room"
2018-09-05 18:09:59 +01:00
David Baker 04ad3d7c3c Revert "Don't rely on members to query if syncing user can post to room" 2018-09-05 18:04:19 +01:00
Bruno Windels 49badd9a2f Merge pull request #717 from matrix-org/bwindels/fixllroompermission
Don't rely on members to query if syncing user can post to room
2018-09-05 13:07:29 +02:00
Bruno Windels 8b00083bca check power levels without relying on membership
as this might not be known for the syncing user.
instead, add a method to room which always knows the syncing user's membership
2018-09-05 12:36:53 +02:00
Bruno Windels 3d98e324b5 Merge pull request #719 from matrix-org/bwindels/fixavatars-parttrois
Fixes for room.guessDMUserId
2018-09-04 18:17:39 +02:00
Bruno Windels 768c66313f remove unneeded async keywords 2018-09-04 18:09:47 +02:00
Bruno Windels 3561fd1c05 Merge pull request #716 from matrix-org/bwindels/fixfilepanel
Fix filepanel also filtering main timeline with LL turned on.
2018-09-04 17:10:49 +02:00
Bruno Windels a6c055b6d1 Merge pull request #711 from matrix-org/bwindels/clearidbmembersonleave
Remove lazy loaded members when leaving room
2018-09-04 16:53:56 +02:00
Bruno Windels 0d24c18fed add comment to explain fix 2018-09-04 16:51:36 +02:00
Bruno Windels dd8b2a79fb Merge pull request #702 from matrix-org/bwindels/fixreconnectspinner
Fix: show spinner again while recovering from connection error
2018-09-04 16:39:38 +02:00
Bruno Windels f0095611bc add new CATCHUP state as breaking change 2018-09-04 16:38:46 +02:00
Bruno Windels a3567f0918 some tests for room.guessDMUserId() 2018-09-04 13:01:45 +02:00
Bruno Windels f0d3d0d74e remove unneeded checks, we should always have enough heroes or members 2018-09-04 13:01:15 +02:00
Bruno Windels 3e32bc0d5d check heroes is not falsy first, this would fail without LL 2018-09-04 13:00:34 +02:00
Bruno Windels 632e4aa120 pick the first member, dont need an array 2018-09-04 13:00:00 +02:00
Bruno Windels 2391ce198d this method returns userId, not member 2018-09-04 12:59:37 +02:00
David Baker e5e2bbd482 Merge branch 'master' into develop 2018-09-03 14:11:12 +01:00
David Baker 0b6632123b v0.10.9 2018-09-03 14:09:24 +01:00
David Baker b1801fc953 Prepare changelog for v0.10.9 2018-09-03 14:09:24 +01:00
Bruno Windels ca1a1c4f28 Merge pull request #714 from matrix-org/bwindels/memberlist-spinner
Add method to query LL state in client
2018-09-03 15:05:13 +02:00
Bruno Windels 3363cc4f1d shallow-clone the filter, so the timeline filter doesnt get written into it later on 2018-09-03 15:02:38 +02:00
Bruno Windels f84684982f add method to query LL state in client 2018-09-03 11:14:23 +02:00
Bruno Windels 3bed5969bf remove count logging, approach confirmed to work and be according to idb spec 2018-09-03 10:27:00 +02:00
Bruno Windels ebc162e3d8 do onLeft (which clears the LL members) as late as possible
to avoid chance that something might call loadMembersIfNeeded
on the room and load them back again.
2018-08-31 16:13:34 +02:00
Bruno Windels f30136dba3 only clear promise at the end to avoid race between load and clear members 2018-08-31 16:12:54 +02:00
Bruno Windels 9b1926f902 also clear out lazy loaded members from storage 2018-08-31 16:11:37 +02:00
David Baker d29524ba3f Merge pull request #707 from matrix-org/bwindels/llinvites
Fix: also load invited members when lazy loading members
2018-08-31 14:09:42 +01:00
Bruno Windels 7258fe4e5c clear out of band members in store when leaving room 2018-08-31 14:42:15 +02:00
Bruno Windels f8ea1702f8 store support for removing out of band members for a room 2018-08-31 14:42:15 +02:00
David Baker 7582c28c1a v0.10.9-rc.2 2018-08-31 13:40:37 +01:00
David Baker 5042eb87e7 Prepare changelog for v0.10.9-rc.2 2018-08-31 13:40:36 +01:00
David Baker e8e80732ef Merge pull request #710 from matrix-org/bwindels/avatarfixbis
Fix for "otherMember.getAvatarUrl is not a function"
2018-08-31 13:32:25 +01:00
David Baker b8744a79ae Merge pull request #708 from matrix-org/bwindels/avatarfixbis
Fix for "otherMember.getAvatarUrl is not a function"
2018-08-31 10:34:29 +01:00
Bruno Windels 414b153d28 also fallback to getting avatar from user 2018-08-31 11:05:06 +02:00
Bruno Windels 8e160dda8e make sure getAvatarFallbackMember always returns a member 2018-08-31 11:03:26 +02:00
David Baker 9b54c9b807 Merge pull request #704 from matrix-org/dbkr/discardsession
Pass through function to discard megolm session
2018-08-30 18:25:50 +01:00
Bruno Windels 1bb608cdb6 remove filter for LL members so invite members are also sent 2018-08-30 17:29:16 +02:00
Bruno Windels 1239485b30 fix test 2018-08-30 15:42:15 +02:00
Bruno Windels 0d23d047fc use CATCHUP state after ERROR before going back to SYNCING 2018-08-30 15:37:05 +02:00
Bruno Windels d837ae64ac triple = 2018-08-30 15:37:05 +02:00
Bruno Windels d72a70396a Pass through PREPARED state after error, when keepalive returns succes.
This is according to the state diagram in client.js.
This will show a spinner at the bottom of a room again
while the catchup sync is in progress,
which seems to have broken at some point.
2018-08-30 15:37:05 +02:00
David Baker 938772b86a v0.10.9-rc.1 2018-08-30 14:16:14 +01:00
David Baker 3e88593a81 Prepare changelog for v0.10.9-rc.1 2018-08-30 14:16:14 +01:00
David Baker 60c01d7869 Revert b0b0291 and a6de395
To make tests pass again
2018-08-30 12:03:53 +01:00
David Baker 1cbcc61bd6 Merge pull request #706 from matrix-org/bwindels/fixdmavatar
Fix DM avatar
2018-08-30 11:43:07 +01:00
Bruno Windels 7f5a2974ce allow self chats 2018-08-30 12:37:13 +02:00
Bruno Windels 3de3ea38b9 check heroes is present 2018-08-30 11:47:17 +02:00
Bruno Windels 3659e86d57 fix lint, actually get members as well 2018-08-30 11:41:35 +02:00
Bruno Windels c335a6b3de guess DM user id, used to patch up incorrect m.direct account data 2018-08-30 11:05:31 +02:00
Bruno Windels 267b831bc4 calculate fallback avatar for rooms with <= members but not DM 2018-08-30 11:05:05 +02:00
Bruno Windels 7ee93cb910 make sure our user is not in the summary heroes.
No reason to think it is right now, but if there is a server bug
we could end up showing your own avatar for a DM again.

Also convenience method as we add up invited + join count often
2018-08-30 11:04:32 +02:00
Matthew Hodgson ae95a49618 spell out m.new_devices no longer exist 2018-08-29 20:20:52 +01:00
David Baker 8f98504183 jsdoc 2018-08-29 18:11:53 +01:00
David Baker 1b77ee0ef4 Pass through function to discard megolm session
To make debugging crypto slightly faster
2018-08-29 18:06:45 +01:00
Matthew Hodgson a6de395cde unbreak tests from b0b0291bc7 2018-08-28 18:03:59 +01:00
Bruno Windels fcd6dd34b2 Merge pull request #699 from matrix-org/bwindels/fixlle2ememberfetch-bis
Lazy loading: avoid loading members at initial sync for e2e rooms
2018-08-28 15:36:42 +02:00
Bruno Windels a6ebfe4215 typo 2018-08-28 15:31:20 +02:00
Bruno Windels 6a9158aa62 Merge pull request #700 from matrix-org/bwindels/fixllmegolmsession
Improve setRoomEncryption guard against multiple m.room.encryption st…
2018-08-28 15:30:16 +02:00
Matthew Hodgson b0b0291bc7 hopefully fix invite_room_state as per https://github.com/vector-im/riot-web/issues/7229 2018-08-28 01:27:20 +01:00
Bruno Windels 85f1da1f10 revert unnecesary changes 2018-08-27 12:09:10 +02:00
Bruno Windels c47445ca98 no need to just add a space now 2018-08-27 12:01:22 +02:00
Bruno Windels 4e25867548 revert to async event processing
without LL, we could refresh the device list before all members have been tracked.
as promises, even resolved ones (in case of no LL), always continue async
2018-08-27 11:56:46 +02:00
Bruno Windels ad71bb30ac add comment back as we kept flag in the end 2018-08-27 11:51:29 +02:00
Bruno Windels 362bf1895d restore inhibitDeviceQuery param to avoid breaking change 2018-08-27 11:12:00 +02:00
Bruno Windels 7d00c0bd5a make LL/non-LL flow in Crypto more alike by always going through _roomDeviceTrackingState 2018-08-27 10:54:08 +02:00
Bruno Windels 5e5994f166 try and fix tests 2018-08-27 10:54:08 +02:00
Bruno Windels 7247762b60 Also support not lazy-loading members in Crypto 2018-08-27 10:54:08 +02:00
Bruno Windels 21e0c79f7d Revert "Revert "Lazy loading: don't block on setting up room crypto""
This reverts commit 5cf2ebea4f.
2018-08-27 10:54:08 +02:00
Bruno Windels 78b08bfef2 fix var declaration 2018-08-27 10:48:11 +02:00
Bruno Windels ae7e90dc2f do config comparison first to keep original error message 2018-08-27 10:39:14 +02:00
Hubert Chathi bf873bde42 split the backup version creation into two different methods 2018-08-24 22:13:13 -04:00
Hubert Chathi 017f81e430 fix some bugs 2018-08-24 16:39:22 -04:00
Bruno Windels 0028bfbfc7 fix lint 2018-08-24 18:42:48 +02:00
Bruno Windels 60c9c403bd Improve setRoomEncryption guard against multiple m.room.encryption state events
we were only bailing out when receiving a non JSON-identical m.room.encryption event.
When receiving an identical event, the algorithm in _roomEncryptors would be reset,
generating a new megolm session every time this happens (there is a LL synapse bug
where this happens on every sync).

As the _roomList is backed by indexeddb you might already have a config without the algorithm being present though,
so we first check for the room encryptor algorithm being present. If so, always bail out as setRoomEncryption was
already called for the given room.

If no algorithm is present, still check if the config is not being changed.
Also setup the roomlist and room encryption synchronously before awaiting
the indexeddb operation to store the room encryption config in roomlist.
2018-08-24 18:28:38 +02:00
David Baker ec5fff2046 Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-08-24 13:29:29 +01:00
Bruno Windels a7199a3d0d Merge pull request #698 from matrix-org/revert-696-bwindels/fixlle2ememberfetch
Revert "Lazy loading: don't block on setting up room crypto"
2018-08-23 14:10:13 +02:00
Bruno Windels 5cf2ebea4f Revert "Lazy loading: don't block on setting up room crypto" 2018-08-23 14:05:57 +02:00
David Baker 580e95605e Merge pull request #696 from matrix-org/bwindels/fixlle2ememberfetch
Lazy loading: don't block on setting up room crypto
2018-08-23 12:41:33 +01:00
David Baker 8c3d1df3cf Merge pull request #695 from matrix-org/dbkr/hide_replaced_rooms
Add getVisibleRooms()
2018-08-23 09:34:39 +01:00
David Baker 7c66f91429 Typo 2018-08-23 09:34:23 +01:00
Hubert Chathi 73e294b1bd add copyright header to backup.spec 2018-08-23 00:29:29 -04:00
Hubert Chathi e5ec479923 check that crypto is enabled 2018-08-23 00:27:30 -04:00
Hubert Chathi 75107f99b2 pass in key rather than decryption object to restoreKeyBackups 2018-08-23 00:26:21 -04:00
Hubert Chathi fb8efe368a initial draft of API for working with backup versions 2018-08-23 00:03:36 -04:00
Hubert Chathi 1faf477537 fix formatting and fix authedRequest usage 2018-08-22 23:58:59 -04:00
Bruno Windels e3d108454c fix test 2018-08-22 23:25:37 +02:00
Bruno Windels 806b40727d fix lint 2018-08-22 23:00:27 +02:00
Bruno Windels fa702efe8f fix typo 2018-08-22 19:22:50 +02:00
Bruno Windels 344e3e18ab start tracking room devices in background after finishing loading members 2018-08-22 19:13:18 +02:00
Bruno Windels aea9eaa307 Only start tracking devices in an e2e room when needed
This way we can put off loading the members
2018-08-22 18:00:38 +02:00
Bruno Windels dffe0b39b6 Merge pull request #697 from matrix-org/dbkr/joined_member_count_wrapper
Add wrapper around getJoinedMemberCount()
2018-08-22 17:44:59 +02:00
Bruno Windels 267d660527 add invite count to room as well 2018-08-22 17:42:30 +02:00
David Baker 4c3046f917 Add wrapper around getJoinedMemberCount()
On Room, because it's super confusing that Room has
getJoinedMembers() but not getJoinedMemberCount()

https://github.com/matrix-org/matrix-react-sdk/pull/2126 had assumed
that this method was on Room
2018-08-22 16:34:06 +01:00
Bruno Windels 71444b638b don't block on setting up room crypto
this will load members in case of LL and could take quite some time
The two async actions performed by onCryptoEvent is saving the crypto config
to the roomlist store and fetching the room members to track.
Both are guarded against double calls so not awaiting this should be fine.
2018-08-22 14:45:36 +02:00
David Baker 962ec7bb53 Add getVisibleRooms()
To hide rooms that have been replaced

For https://github.com/vector-im/riot-web/issues/7164
2018-08-22 11:58:44 +01:00
Bruno Windels 52fad6aec2 Merge pull request #694 from matrix-org/hs/fetch-room-event-api
Api to fetch events via /room/.../event/..
2018-08-21 14:39:14 +02:00
Will Hunt 5b830f0b6a Add fetchRoomEvent to base-apis.js 2018-08-21 11:07:48 +01:00
Will Hunt 3d24c8768f Drop fetchRoomEvent from client.js 2018-08-21 11:07:37 +01:00
Bruno Windels 977b8625f8 Merge pull request #693 from matrix-org/dbkr/room_upgrades
Support for room upgrades
2018-08-21 11:22:08 +02:00
Will Hunt df7dc04a1d Happy linter, happy developer 2018-08-20 14:16:47 +01:00
David Baker 269d3cb086 Merge branch 'master' into develop 2018-08-20 13:49:20 +01:00
David Baker 4d310cd461 v0.10.8 2018-08-20 13:44:30 +01:00
David Baker 88c5c39fcb Prepare changelog for v0.10.8 2018-08-20 13:44:29 +01:00
Will Hunt 79ca68300c Add callback 2018-08-20 12:47:26 +01:00
Will Hunt baca20b225 Add support for /rooms/$roomId/event/$eventId 2018-08-20 11:14:45 +01:00
David Baker 0e3cb1977f lint 2018-08-17 15:00:54 +01:00
David Baker 8b1fa72877 Copyright & debadgering 2018-08-17 14:59:34 +01:00
David Baker e8610a35b4 Support for room upgrades
For https://github.com/vector-im/riot-web/issues/7164
2018-08-17 14:55:12 +01:00
Bruno Windels 77e6442f73 Merge pull request #691 from matrix-org/bwindels/feature_lazyloading
Lazy loading of room members
2018-08-16 18:14:33 +02:00
David Baker eeddfd4919 v0.10.8-rc.1 2018-08-16 15:17:36 +01:00
David Baker fa16da86b3 Prepare changelog for v0.10.8-rc.1 2018-08-16 15:17:35 +01:00
Bruno Windels 372a628cab fix log whitespace 2018-08-15 12:01:26 +02:00
Bruno Windels 2f4d8c3530 check with server if it supports member lazy loading 2018-08-15 12:01:26 +02:00
Bruno Windels 482eab0e2a fix tests 2018-08-15 12:01:26 +02:00
Bruno Windels 03c63d9b12 use sync token for /members request, as synapse expects it now 2018-08-15 12:01:26 +02:00
Bruno Windels 07e87915ba fix and add tests 2018-08-15 12:01:26 +02:00
Bruno Windels 91f2bf99c0 fix lint 2018-08-15 12:01:26 +02:00
Bruno Windels 535d59db4d fixup 2018-08-15 12:01:26 +02:00
Bruno Windels 9739c3355a undo postponing tracking device keys on turning on room encryption 2018-08-15 12:01:26 +02:00
Bruno Windels 827db37eef fixup 2018-08-15 12:01:26 +02:00
Bruno Windels 0c6e47a5bc await for LL members in getEncryptionTargetUsers 2018-08-15 12:01:26 +02:00
Bruno Windels 864ea749e5 Move /members fetching to room as getEncryptionTargetMembers needs it 2018-08-15 12:01:26 +02:00
Bruno Windels 5d92ec3b7b prevent deadlock on startup
when loading the encrypted events from storage,
the code would wait for the encryption target users,
which would never come because you would only load them
when viewing the room.

This disabled starting to track the devices in the room
when the inhibitDeviceQuery is set.
2018-08-15 12:01:26 +02:00
Bruno Windels 733a3ed102 Defer encryption targeted users when OOB member loading hasn't started 2018-08-15 12:01:26 +02:00
Bruno Windels b14be026b7 pass LL flag to room, to know if we should wait for lazy members at all 2018-08-15 12:01:26 +02:00
Bruno Windels b4afe97289 spelling 2018-08-15 12:01:26 +02:00
Bruno Windels 43a7a607b2 fixup 2018-08-15 12:01:26 +02:00
Bruno Windels a8bf66d8af Make Room.getEncryptionTargetMembers async, as members might be loading 2018-08-15 12:01:26 +02:00
Bruno Windels 3616a07dbb store /members promise on room while loading members 2018-08-15 12:01:26 +02:00
Bruno Windels 6609dfd410 initial support for lazy loading when calling /context 2018-08-15 12:01:26 +02:00
Bruno Windels e3913bd397 Fix: missed call site while renaming prependStateEvents 2018-08-15 12:01:26 +02:00
Bruno Windels 8c01ed1469 add comments explaining why we ignore the put promise result 2018-08-15 12:01:26 +02:00
Bruno Windels 7aa0dcc89f PR feedback, rename method 2018-08-15 12:01:26 +02:00
Bruno Windels 5285b22a76 bump version as making startClient async is a breaking change 2018-08-15 12:01:26 +02:00
Bruno Windels 0fa49bc2cd PR feedback 2018-08-15 12:00:38 +02:00
Bruno Windels 01d8730850 cleanup, lint and docs 2018-08-15 12:00:38 +02:00
Bruno Windels 52149ce74a Move LL filter creation inside MatrixClient
As we need an option to turn lazy loading on (we can't just accept a filter,
as /messages has an incompatible filter), better only pass the option
and create the filter inside startClient
2018-08-15 12:00:38 +02:00
Bruno Windels bffc20612d Fix: member avatar was always forward looking
applying itself all the way till the next member event
when back paginating
2018-08-15 12:00:38 +02:00
Bruno Windels 2c0eb19a27 Fix sentinels changing from underneath us!
As RoomMember contains the event in a nested object (events.member),
a shallow copy was not enough to be immutable.

This solution won't copy OOB flags but that's not neccesary
for sentinels.
2018-08-15 12:00:38 +02:00
Bruno Windels 748c4737f6 remove dead code 2018-08-15 12:00:38 +02:00
Bruno Windels 769d5113f7 prepend state from /messages to appropriate timeline/room state (excluding event context for now) 2018-08-15 12:00:38 +02:00
Bruno Windels 157be6da05 centralize creating a /messages request
so we only need to add LL filter once
2018-08-15 12:00:38 +02:00
Bruno Windels 1dc4b8bb63 add option for lazy loading to startClient
we need more than just a filter, which is what is passed in now,
so have an explicit option. For now still take the filter but later on
this could be created inside MatrixClient
2018-08-15 12:00:38 +02:00
Bruno Windels f261599435 fix lint 2018-08-15 12:00:38 +02:00
Bruno Windels 2862b49057 Only return hero in getDMInviter if we were invited 2018-08-15 12:00:38 +02:00
Bruno Windels a8d0d8f33d provide method on room to help with DM detection with fallback to summary heroes/counts. 2018-08-15 12:00:38 +02:00
Bruno Windels f55a2079bf replace getMember(myId).membership with getMyMembership
This works with rooms which haven't had their members
loaded yet.
2018-08-15 12:00:38 +02:00
Bruno Windels c0f706a2a2 move userId into room 2018-08-15 12:00:38 +02:00
Bruno Windels b034f67a0f add oob member methods to stub store 2018-08-15 12:00:38 +02:00
Bruno Windels 977b9eb686 implement memory store methods 2018-08-15 12:00:38 +02:00
Bruno Windels 5e11bf735e store OOB status along with members, to avoid unneccesary fetching
for some small rooms, it is possible that calling /members would not
yield any previously unknown members, as they were all recently active.
This would be the case for most DMs.

For these rooms, we'd end up with 0 OOB members after lazy loading them,
so when getting them out of storage we need a way to distuinguist this case
from never having lazy loaded the members of the room at all.

We store a marker object in the same store and return [] or null accordingly.
This way the /members don't get fetched a second time.
2018-08-15 12:00:38 +02:00
Bruno Windels a8c73f7a4d add logging, should be useful as long as not merged into develop 2018-08-15 12:00:38 +02:00
Bruno Windels 86105611fc we dont need a separate index? 2018-08-15 12:00:38 +02:00
Bruno Windels 0364af7337 update indexeddb store to store member events, not profile information 2018-08-15 12:00:38 +02:00
Bruno Windels c618ce4625 store only out of band members 2018-08-15 12:00:38 +02:00
Bruno Windels 2b9c834476 add comment to clarify how we avoid race 2018-08-15 12:00:38 +02:00
Bruno Windels d366ec9c48 prototype how we could store ll members 2018-08-15 12:00:38 +02:00
Bruno Windels ca3981fba8 back-port infinite spinner fix as room state code changed considerably for lazy loading 2018-08-15 12:00:38 +02:00
Bruno Windels bb490faefe fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels d8f673ed51 make sure invited count cache gets reset when updating member 2018-08-15 11:59:40 +02:00
Bruno Windels 6ce7170cf4 counts from summary api should override count members manually as members might not be complete 2018-08-15 11:59:40 +02:00
Bruno Windels 1d71e7243f no need to create new array here 2018-08-15 11:59:40 +02:00
Bruno Windels cf08901d02 fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels 230a9311a0 actually need to subtract one from join+invite count as that includes the syncing user 2018-08-15 11:59:40 +02:00
Bruno Windels 576f7142c1 just need the member names actually 2018-08-15 11:59:40 +02:00
Bruno Windels 20b4285849 add some tests for room name based on room summary + fix because it was actually broken 2018-08-15 11:59:40 +02:00
Bruno Windels d67bdbf088 test accumulating summary fields 2018-08-15 11:59:40 +02:00
Bruno Windels 3a389793ff fix sync accumulator test to include summary 2018-08-15 11:59:40 +02:00
Bruno Windels f5ff5dc3e0 Fix name recalculation tests by not relying on mocking
I tried keeping the mocking but it would take too much
boilerplate code to make the tests work again, and even more
to write the tests for room name with lazy loading.

Just testing everything with a real implementation is not really
a unit test any more, but proved way easier.

It'll be somewhat annoying these tests will fail if there is
something wrong in roomstate (not room), but that's the trade-off
2018-08-15 11:59:40 +02:00
Bruno Windels 00bf5bdf69 unify member a bit access towards getMember
some tests for mock getMember, some for .members
if you use either in the code (as I did for room display name changes)
tests start playing and you play whack-a-mole switching between
both ways of accessing the members in a room.

lets start using one way so mocking becomes easier,
and besides, accessing an object internal members is not the best idea.
2018-08-15 11:59:40 +02:00
Bruno Windels 9541aa7dbf fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels e61c6b89c8 bring room name calculation in line with summary spec, while maintaining some backwards compatibility 2018-08-15 11:59:40 +02:00
Bruno Windels a5b3869e9f add invited count, only copy summary fields if present in summary
only copy any member from summary as
they are only in the response when they change.
Also accumulate them in the sync accumulator
2018-08-15 11:59:40 +02:00
Bruno Windels fbdce27db2 m.heros => m.heroes 2018-08-15 11:59:40 +02:00
Bruno Windels 148876f597 sorting should happen always 2018-08-15 11:59:40 +02:00
Bruno Windels 0cb533beca no need to recalculate name here, as recalculate already does this 2018-08-15 11:59:40 +02:00
Bruno Windels 5811ebd6f3 Support summary heroes in room name calculation
Also clean-up algorithm, and remove assumption
that we have all members as much as possible
2018-08-15 11:59:40 +02:00
Bruno Windels 8fa87f8ba5 make room summary available to Room
from either the sync accumulator or the /sync endpoint
2018-08-15 11:59:40 +02:00
Bruno Windels 21ba4f71f6 jsdoc doesn't like generic promise type annotations 2018-08-15 11:59:40 +02:00
Bruno Windels 097e7df7c9 fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels 83c6615d6e move me || syncedmembership code into room.getmymembership 2018-08-15 11:59:40 +02:00
Bruno Windels f6fafeaafb store membership from during sync, because we might not have own membership
to determine where a room should show up in the room list, we need to know
our membership type. But with lazy loading, we might not have our own member
if we weren't recently active in the room. Using getSyncedMembership can be
used to fallback if the users membership is not yet available.
2018-08-15 11:59:40 +02:00
Bruno Windels 420a88c776 remove obsolete flag 2018-08-15 11:59:40 +02:00
Bruno Windels 5fcf9481b3 fix room not having access to event mapper + tests 2018-08-15 11:59:39 +02:00
Bruno Windels 48c3dcc08a fix lint & fix and add tests 2018-08-15 11:59:39 +02:00
Bruno Windels 62333b3e2c Use /members api for lazy loading
This commit is a substantial change, as /members returns state events,
not profile information as /joined_members, and this allows to simplify
the implementation quite a bit. We can assume again all members have
a state event associated with it.

I also changed most of the naming of lazy loaded members to
out-of-band members to reflect that this is the relevant bit for most
of the code, that the members didn't come through /sync but through
another channel.

This commit also addresses the race condition between /(joined_)members
and /sync. /members returns the members at the point in the timeline
at a given event id. Members are loaded at the last event
in the live timeline, and all members that come in from sync
in the mean time are marked as superseding the out of band members,
so they won't be overwritten, even if the timeline is reset in the
mean time.

Members are also marked if they originate from an out-of-band channel
(/members) so they can be stored accordingly (future PR).

The loading status is kept in room state now, as this made resolving
the race condition easier. One consequence is that the status needs
to be shared across cloned instances of RoomState. When resetting
the timeline (and cloning the room state) while lazy loading is in
progress, one of the RoomStates could be left in progress indefinitely.
Though that is more for clarity than avoiding any actual bugs.
2018-08-15 11:59:39 +02:00
Bruno Windels df758b31b7 fix lint 2018-08-15 11:59:39 +02:00
Bruno Windels 9f08bfaa6f room lazy loading tests + fix 2018-08-15 11:59:39 +02:00
Bruno Windels 198d2c780d test that modifications to clone'd() room state dont affect the old 2018-08-15 11:59:39 +02:00
Bruno Windels ab1c0dabae make sure LL members don't needlessly get disambiguated during a clone
when cloning the state, lazy loaded members are copied over with their rawDisplayName,
which could originate from their userId if they don't have a displayname.
the displayname algorithm would assume that the displayname is explicitly set,
and see if we'd have to disambiguate. As a fix, if the display name is the same as the id, just return the id
2018-08-15 11:59:39 +02:00
Bruno Windels 0234f11914 some tests for room member + state, and some fixes to make them pass 2018-08-15 11:59:39 +02:00
Bruno Windels 79fcc9f343 only set the lazy members on the forward looking state of the live timeline
since back-paginating will also support lazy loading the state needed to display that part of the timeline, and no user interaction
is supposed to happen before the lazy loaded member are, well, loaded, applying the ll members to all timelines should not be neccessary.
2018-08-15 11:59:39 +02:00
Bruno Windels 031f722540 clarify that we only get joined members for now 2018-08-15 11:59:39 +02:00
Bruno Windels 531ccf1819 actually, comment should be 1 line higher 2018-08-15 11:59:39 +02:00
Bruno Windels 0d2ac42dc4 add comment to clarify corners cut in prototype 2018-08-15 11:59:39 +02:00
Bruno Windels 9ec6ea3bdf 2, not 3 times 2018-08-15 11:59:39 +02:00
Bruno Windels 1ce580bba3 test lazy loaded info is returned and then discarded when setting a state event 2018-08-15 11:59:39 +02:00
Bruno Windels 8ad2a94a90 make sure LL member doesn't override state event
extra safety check, as this should already not happen because of the check in RoomState
2018-08-15 11:59:39 +02:00
Bruno Windels de3f75bc83 Lazy loaded members should never take precendence over members acquired through state events 2018-08-15 11:59:39 +02:00
Bruno Windels 008d85ed32 pick joined property out of response 2018-08-15 11:59:39 +02:00
Bruno Windels 5e30aff418 more consistent naming 2018-08-15 11:59:39 +02:00
Bruno Windels 5de0d39553 move the fact that we're prototyping only with joined members up the stack to client
only MatrixClient really needs to know that for now we only load joined members, the rest of the code can be generic for other membership types as that is the eventual plan, to also support invites at least.
2018-08-15 11:59:39 +02:00
Bruno Windels d95d44dc94 move error handling to caller 2018-08-15 11:59:39 +02:00
Bruno Windels 6061deac37 use method for getting state event, less code 2018-08-15 11:59:39 +02:00
Bruno Windels ba34a766e7 fix lint 2018-08-15 11:59:39 +02:00
Bruno Windels 1c81a17298 Fix tests
getSentinelMember now does return a member (with just the userid) when there is no corresponding member yet.
With lazy loading it's perfectly possible the member is not available, and null breaks continuation in the timeline.
2018-08-15 11:59:39 +02:00
Bruno Windels 2097b31d4f handle failed /joined_members call by logging to console and reverting flag 2018-08-15 11:59:39 +02:00
Bruno Windels 2155dd0552 improve return type comment 2018-08-15 11:59:39 +02:00
Bruno Windels 8733654094 remove left-over test code, oops 2018-08-15 11:59:39 +02:00
Bruno Windels abd15748ce fix one lint warning too many 2018-08-15 11:59:39 +02:00
Bruno Windels 9a796f1383 fix lint errors 2018-08-15 11:59:39 +02:00
Bruno Windels 88f2f62945 make resetting the live timeline work with lazily loaded members
In order for the lazy loading logic not to bleed into all corners
of the JS SDK, I moved some of the state copying between timelines
over to the RoomState and EventTimeLine class.
2018-08-15 11:59:39 +02:00
Bruno Windels 30adefed07 return sentinels with userid if members haven't been loaded yet,
better than braking timeline continuation
2018-08-15 11:59:39 +02:00
Bruno Windels 20a1828fa5 make sentinels lazy loading compatible
dont just rely on member events, but just copy the member
2018-08-15 11:59:39 +02:00
Bruno Windels 809674ca2b set lazily loaded members on all RoomStates of a room
for all timelines in all timeline sets
2018-08-15 11:59:39 +02:00
Bruno Windels 0ca3475878 make method to get mxc avatar url public
so MemberInfo can use it and take lazy loading into account
2018-08-15 11:59:39 +02:00
Bruno Windels 32b741e205 use more consistent naming 2018-08-15 11:59:39 +02:00
Bruno Windels c917c4a468 return correct invite sender in case of a join 2018-08-15 11:59:39 +02:00
Bruno Windels 6c584d2b4c keep is_direct checks inside RoomMember as events.member might not be available 2018-08-15 11:59:39 +02:00
Bruno Windels 759d415d40 preserve member state event if available when lazy loading members 2018-08-15 11:59:39 +02:00
Bruno Windels 45d86fa270 emit individual events for lazily loaded members
emit individual RoomState.members/newMember events
for each lazily loaded member as batch events are not a thing.
This makes updating the memberlist work
2018-08-15 11:59:39 +02:00
Bruno Windels 2c5cad71ee prototype support for lazily loading members in matrixclient 2018-08-15 11:59:39 +02:00
Bruno Windels 2b5925b893 Support for updating members in RoomState from lazily loaded members 2018-08-15 11:59:39 +02:00
Bruno Windels f012ada2c4 add setter on RoomMember to update from lazily loaded member 2018-08-15 11:59:07 +02:00
Bruno Windels af1b26ae95 Merge pull request #689 from matrix-org/dbkr/show_room_version
Add getVersion to Room
2018-08-14 17:54:09 +02:00
David Baker f72f5b43e1 Add getVersion to Room
To get the version number of the room as per https://github.com/matrix-org/matrix-doc/issues/1425
2018-08-14 14:27:08 +01:00
Hubert Chathi d55618921b initial implementation of e2e key backup and restore 2018-08-07 23:10:55 -04:00
Bruno Windels c7e1e07262 Merge pull request #680 from matrix-org/dbkr/getsyncstatedata
Add getSyncStateData()
2018-08-06 10:51:21 +02:00
Bruno Windels 24a1bec23d Merge pull request #679 from matrix-org/dbkr/chairman_mau_pt_3_sync_error
Send sync error to listener
2018-08-03 19:06:17 +02:00
David Baker 89ad104423 Add getSyncStateData()
To get additional information about the sync state (ie. the error
object).
2018-08-03 18:00:52 +01:00
David Baker c2f3324302 Send sync error to listener
We do this in other places, but not here
2018-08-03 16:59:10 +01:00
Michael Telatynski 04a969b997 Merge pull request #675 from matrix-org/bwindels/nocrashoninvalidtags
make sure room.tags is always a valid object to avoid crashes
2018-08-01 23:03:51 +01:00
Bruno Windels 630dfa9499 make sure room.tags is always a valid object so no crashes happen later on 2018-07-31 15:33:56 +02:00
Bruno Windels 95668950c2 Merge pull request #673 from matrix-org/bwindels/infinite_spinner
Fix infinite spinner upon joining a room
2018-07-31 14:57:04 +02:00
Bruno Windels 3012501e4b update docs to clarify state when emitting newMember 2018-07-30 14:19:45 +02:00
David Baker 0e81dfb004 v0.10.7 2018-07-30 11:40:08 +01:00
David Baker 35b7f358b6 Prepare changelog for v0.10.7 2018-07-30 11:40:08 +01:00
Bruno Windels e3e48944e0 add test 2018-07-27 11:49:55 +02:00
Bruno Windels 94bbba72f5 add member to members before emitting any events 2018-07-27 11:35:19 +02:00
Michael Telatynski b34716f7e9 take into account homoglyphs when calculating similar display names
to prevent homoglyph attacks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-07-26 14:55:35 +01:00
David Baker c429ca67b9 v0.10.7-rc.1 2018-07-24 18:15:14 +01:00
David Baker bce2ba0785 Prepare changelog for v0.10.7-rc.1 2018-07-24 18:15:13 +01:00
Matthew Hodgson 2613690064 Merge pull request #666 from matrix-org/matthew/encrypt-for-invited-users
encrypt for invited users if history visibility allows.
2018-07-10 16:14:45 +01:00
David Baker 7283076bc8 Fix Users / Members mixup 2018-07-09 17:16:23 +01:00
David Baker f43d05b54e Merge branch 'master' into develop 2018-07-09 13:07:40 +01:00
David Baker c6b500bc09 v0.10.6 2018-07-09 13:06:07 +01:00
David Baker c3972015c7 Prepare changelog for v0.10.6 2018-07-09 13:06:06 +01:00
David Baker 3c18c57857 v0.10.6-rc.1 2018-07-06 15:40:55 +01:00
David Baker f562a06707 Prepare changelog for v0.10.6-rc.1 2018-07-06 15:40:54 +01:00
Michael Telatynski 7f50dd205f displayname disambiguation fixes (#662)
* fix displayname=undefined being disambiguated and strip Zero Width chars
* also strip diaritics and whitespace

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-07-06 10:42:53 +01:00
David Baker c4fe15400c Merge pull request #665 from matrix-org/luke/feature-expose-decryption-error-2
Expose event decryption error via Event.decrypted event
2018-07-05 11:02:26 +01:00
David Baker 6e3e8f7310 Merge pull request #663 from matrix-org/luke/feature-decryption-error-codes
Add decryption error codes to base.DecryptionError
2018-07-05 11:00:33 +01:00
Matthew Hodgson 5ae2c26130 lint 2018-07-05 02:01:10 +02:00
Matthew Hodgson d8d35f4022 encrypt for invited users if history visibility allows.
fixes https://github.com/vector-im/riot-web/issues/2713
2018-07-05 01:45:45 +02:00
Luke Barnard fadb4d9219 Send OLM_UNKNOWN_MESSAGE_INDEX when possible 2018-07-04 15:58:45 +01:00
Luke Barnard b63149b36a Fix error code for Olm group message decryption 2018-07-04 14:33:02 +01:00
Luke Barnard 79f92abcfa Add jsdoc for Event.decrypted error 2018-07-04 14:28:15 +01:00
Luke Barnard 0137fb468b Expose event decryption error via Event.decrypted event 2018-07-04 13:56:03 +01:00
Luke Barnard 70ef8760cc Remove spurious console.dir 2018-07-04 11:56:02 +01:00
Luke Barnard c74d2d831b Add decryption error codes to base.DecryptionError
These should roughly follow https://github.com/matrix-org/matrix-ios-sdk/blob/9732cf593206a10d2b60cd01151a759c88c1e9a2/MatrixSDK/Crypto/Algorithms/MXDecryptionResult.h#L21-L39
2018-07-04 11:54:06 +01:00
David Baker 0415f821eb v0.10.5 2018-06-29 11:32:23 +01:00
David Baker 779fe35255 Prepare changelog for v0.10.5 2018-06-29 11:32:22 +01:00
Will Hunt aec7ef6f9c getMediaLimits -> getMediaConfig 2018-06-23 14:38:39 +01:00
Will Hunt 329f09ce0a Media/limits => /config 2018-06-23 14:38:39 +01:00
Will Hunt 68c23af5ae Remove extra return 2018-06-23 14:38:39 +01:00
Will Hunt a54f30c02f Add getMediaLimits to client 2018-06-23 14:38:39 +01:00
Will Hunt fde00b1c62 getMediaLimits -> getMediaConfig 2018-06-23 12:57:29 +01:00
David Baker 11382d2cd7 v0.10.5-rc.1 2018-06-21 10:01:05 +01:00
David Baker ef31131a5d Prepare changelog for v0.10.5-rc.1 2018-06-21 10:01:05 +01:00
Will Hunt 8dd425f8ff Media/limits => /config 2018-06-20 17:24:45 +01:00
David Baker 76feabe32b Merge pull request #659 from matrix-org/t3chguy/media_fixes
fix auth header and filename=undefined
2018-06-18 13:46:59 +01:00
Michael Telatynski 7fe3e2f90a invert argument to make it positive without breaking backwards compat
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-18 12:27:25 +01:00
Michael Telatynski c0b2151929 allow omitting filename from upload entirely
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-15 18:25:20 +01:00
Michael Telatynski 5e3b1bf6b0 use Authorization header in media/v1/upload if enabled, instead of query
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-15 09:57:30 +01:00
Michael Telatynski fdf4523c2a export the Group model class (#656)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-14 13:52:13 +01:00
David Baker c6cf76f345 Merge branch 'master' into develop 2018-06-12 14:13:15 +01:00
David Baker f16e544691 v0.10.4 2018-06-12 14:11:07 +01:00
David Baker df101217fc Prepare changelog for v0.10.4 2018-06-12 14:11:07 +01:00
David Baker 5d90dc16cc Merge pull request #650 from matrix-org/t3chguy/audio_output
allow setting the output device for webrtc calls
2018-06-08 15:57:25 +01:00
David Baker bbfc1a1cd6 Merge pull request #596 from t3chguy/t3chguy/fix-docs
arguments true and false are actually invalid
2018-06-08 15:21:23 +01:00
Michael Telatynski b7208c12ac Merge pull request #643 from matrix-org/t3chguy/content-type-bugfix
fix typo where `headers` was not being used and thus sent wrong content-type
2018-06-08 12:29:35 +01:00
Michael Telatynski eaa2fdec44 Merge pull request #642 from matrix-org/t3chguy/doc_fixes
fix some documentation typos
2018-06-08 12:17:48 +01:00
David Baker 7099adfe79 v0.10.4-rc.1 2018-06-06 15:40:02 +01:00
David Baker cb17a2bcb0 Prepare changelog for v0.10.4-rc.1 2018-06-06 15:40:01 +01:00
Michael Telatynski 172044a1cb check whether notif level is undefined, because 0 is falsey (#651)
* check whether notif level is undefined, because `0` is falsey and it failed

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* improve number check for all m.room.power_levels related stuffs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-05-29 12:11:29 +01:00
Matthew Hodgson 69204d4fb3 Merge branch 'develop' into matthew/e2e_backups 2018-05-28 00:44:49 +01:00
Michael Telatynski 4b203b6b63 allow setting the output device for webrtc calls
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-05-26 16:44:27 +01:00
David Baker 67876bab4c v0.10.3 2018-05-25 15:09:24 +01:00
David Baker 6ce691e40f Prepare changelog for v0.10.3 2018-05-25 15:09:23 +01:00
David Baker 9cd44b09f9 v0.10.3-rc.1 2018-05-24 18:16:59 +01:00
David Baker 73a2704126 Prepare changelog for v0.10.3-rc.1 2018-05-24 18:16:58 +01:00
Luke Barnard a50dd785b8 (Breaking Change) Add erase option to deactivateAccount (#649)
For erasing messages etc. after deactivation.

**Breaking change: `deactivateAccount` no longer takes callback**

Also: Move /account/deactivate from PREFIX_UNSTABLE to _R0
2018-05-24 10:47:41 +01:00
Luke Barnard bafbe5cbec Emit no_consent when M_CONSENT_NOT_GIVEN received (#647) 2018-05-22 18:02:21 +01:00
Leon 9cdcbf6bf8 emit oldEventId on "updatePendingEvent"
It should fire `Room.localEchoUpdated` event and returns the oldEventId
2018-05-22 11:27:05 +08:00
Will Hunt 9596087959 Remove extra return 2018-05-03 17:57:57 +01:00
Will Hunt 6570402b95 Add getMediaLimits to client 2018-05-03 13:43:37 +01:00
David Baker 4153845346 v0.10.2 2018-04-30 13:29:26 +01:00
David Baker 548713ed98 Prepare changelog for v0.10.2 2018-04-30 13:29:25 +01:00
Michael Telatynski 1bf1ce7070 fix typo where headers was not being used and thus sent wrong mime
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-30 06:56:43 +01:00
David Baker 21dc0fbf2f v0.10.2-rc.1 2018-04-25 14:57:08 +01:00
David Baker 7b29de9698 Prepare changelog for v0.10.2-rc.1 2018-04-25 14:57:08 +01:00
Michael Telatynski 7d468ee148 Replies (#607) (and Content Helpers)
* if event has `m.relates_to` extract this into the e2e-wrapper event

* Split out helpers to make content for {HTML,text}{emote,notice,message}
So that additional content fields can be added before sending

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-23 14:26:40 +01:00
Michael Telatynski 260e7b529f fix some documentation typos
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-15 23:31:33 +01:00
Michael Telatynski b027089fc1 use constants 2018-04-15 22:16:25 +01:00
David Baker 8f318528f8 Merge branch 'master' into develop 2018-04-12 12:17:45 +01:00
David Baker 9696e70817 v0.10.1 2018-04-12 12:06:25 +01:00
David Baker 85580acbff Prepare changelog for v0.10.1 2018-04-12 12:06:24 +01:00
David Baker c45a5f7e84 Name param slightly more helpfully 2018-04-12 12:00:41 +01:00
David Baker df74b0b038 Ignore inserts of dup inbound group sessions, pt 2
Stop the error from propagating to the txn error handler and
aborting the rest of the transaction.
2018-04-12 12:00:32 +01:00
David Baker 749e5b8458 Use the name of the error, not the request 2018-04-12 12:00:23 +01:00
David Baker 1255566324 Error message formatting 2018-04-12 12:00:11 +01:00
David Baker a25da4ebf4 Ignore inserts of duplicate inbound group sessions
Rather than aborting the whole txn

This is causing e2e migration fails in the wild
2018-04-12 11:59:59 +01:00
David Baker 72835f3c5f lint 2018-04-12 11:59:46 +01:00
David Baker c4f52f6256 Fix error logging 2018-04-12 11:59:32 +01:00
David Baker 3eb9505633 Log IDB errors 2018-04-12 11:58:33 +01:00
David Baker 543a0be1e4 Merge pull request #641 from matrix-org/dbkr/ignore_duplicate_inbound_group_session_2
Ignore inserts of dup inbound group sessions, pt 2
2018-04-12 11:25:54 +01:00
David Baker 5ef21c5075 Name param slightly more helpfully 2018-04-12 11:20:45 +01:00
David Baker 5c412cc0bd Ignore inserts of dup inbound group sessions, pt 2
Stop the error from propagating to the txn error handler and
aborting the rest of the transaction.
2018-04-12 11:15:39 +01:00
David Baker 346deb4b0d Use the name of the error, not the request 2018-04-11 18:57:52 +01:00
David Baker 6f8f30b018 Error message formatting 2018-04-11 18:56:17 +01:00
David Baker b808757012 Merge pull request #639 from matrix-org/dbkr/ignore_duplicate_inbound_group_session
Ignore inserts of duplicate inbound group sessions
2018-04-11 18:31:43 +01:00
David Baker 5e491bb89a Ignore inserts of duplicate inbound group sessions
Rather than aborting the whole txn

This is causing e2e migration fails in the wild
2018-04-11 17:27:25 +01:00
David Baker fc3cb0dd45 lint 2018-04-11 16:09:28 +01:00
David Baker 0846cb04af Fix error logging 2018-04-11 16:02:56 +01:00
David Baker 9708394846 Merge pull request #638 from matrix-org/dbkr/idb_log_error
Log IDB errors
2018-04-11 15:42:14 +01:00
David Baker 92e4396602 Log IDB errors 2018-04-11 15:37:28 +01:00
Luke Barnard a5870b73a9 Merge branch 'master' into develop 2018-04-11 11:39:26 +01:00
Luke Barnard 77ff6b9088 v0.10.0 2018-04-11 11:38:08 +01:00
Luke Barnard 425e74f297 Prepare changelog for v0.10.0 2018-04-11 11:38:08 +01:00
Luke Barnard ffa184464e v0.10.0-rc.2 2018-04-09 15:29:09 +01:00
Luke Barnard eca68e9e7f Prepare changelog for v0.10.0-rc.2 2018-04-09 15:29:09 +01:00
David Baker dd1bc8ec9a Update to new join policy API (#636) 2018-04-09 14:58:13 +01:00
Luke Barnard 24dc1d6991 Add wrapped to set group is_joinable (#634)
which is exposed in the summary API under `summary.profile.is_joinable`.

If true, anyone can join the group. If false, an invite is required.
2018-04-09 14:58:06 +01:00
Luke Barnard 9f510b7eee Add wrapper for group join API (#633)
Needed for new "joinable" groups.
2018-04-09 14:57:57 +01:00
David Baker 730ca9b60b Update to new join policy API (#636) 2018-04-05 17:02:43 +01:00
Luke Barnard 1afaf903f9 Add wrapped to set group is_joinable (#634)
which is exposed in the summary API under `summary.profile.is_joinable`.

If true, anyone can join the group. If false, an invite is required.
2018-03-29 17:03:20 +01:00
Luke Barnard d53cd41aa6 Add wrapper for group join API (#633)
Needed for new "joinable" groups.
2018-03-29 12:42:16 +01:00
David Baker 3e5ea745d2 Merge pull request #632 from matrix-org/dbkr/remove_ignore_call_log
Remove not very useful but veryv spammy log line
2018-03-27 18:28:08 +01:00
David Baker c7052f7dc7 Remove not very useful but veryv spammy log line 2018-03-27 18:21:03 +01:00
Richard Lewis 56b01df85b Merge pull request #628 from matrix-org/rxl881/stickerEvent
Switch event type to m.sticker.
2018-03-22 10:16:40 +00:00
David Baker aa18eeb7d6 v0.10.0-rc.1 2018-03-19 12:08:11 +00:00
David Baker c18264c615 Prepare changelog for v0.10.0-rc.1 2018-03-19 12:08:11 +00:00
David Baker 11746290a9 Merge pull request #630 from matrix-org/dbkr/fix_repeat_state_events_on_unpeek
Fix duplicated state events in timeline from peek
2018-03-16 15:44:38 +00:00
David Baker 16c062c069 Start first incremental sync request early (#629)
* Start first incremental sync request early

So it can run while we process our sync data.
2018-03-16 15:22:06 +00:00
David Baker 64396de0dc Fix duplicated state events in timeline from peek
When joining a room we were peeking into, we duplicated all the
state events into the timeline. Put back the old behaviour of just
setting them as state events, with copious commentary on how wrong
this seems.
2018-03-15 17:35:18 +00:00
Richard Lewis 8ff78c5d60 Switch event type to m.sticker. 2018-03-12 13:56:50 +00:00
David Baker 349297e495 Merge pull request #627 from matrix-org/dbkr/indexeddb_worker_on_start
Create indexeddb worker when starting the store
2018-03-09 17:43:15 +00:00
David Baker dc3ffb3b30 Fix jsdoc 2018-03-09 17:39:02 +00:00
David Baker 5c2dfb138a Hopefully clarify _startPromise 2018-03-09 17:35:03 +00:00
David Baker 0be679de42 Make local variable look less like a global 2018-03-09 17:31:08 +00:00
David Baker 8bd68e0f10 Create indexeddb worker when starting the store
Rather than when creating it, otherwise we could potentially end
up starting workers unnecessarily.
2018-03-09 16:53:27 +00:00
David Baker df70e16b4d Merge pull request #626 from matrix-org/dbkr/fix_indexeddb_logging
Fix indexeddb logging
2018-03-09 10:29:53 +00:00
David Baker b246545da5 Merge remote-tracking branch 'origin/develop' into dbkr/fix_indexeddb_logging 2018-03-09 10:21:00 +00:00
David Baker 3280cb648f Merge pull request #625 from matrix-org/dbkr/stop_spinner_of_doom
Don't do /keys/changes on incremental sync
2018-03-09 10:19:26 +00:00
David Baker 8798bf42e6 Fix indexeddb logging
1. Fix double 'loaded' on sync data logging
2. Move the 'loaded' message into the bit where the data has
   actually loaded rather than the promise try block.
3. Add '...' to the 'loading' messages so they're easier to tell
   apart from the 'loaded' messages.
2018-03-09 10:16:32 +00:00
David Baker 5a23927e56 Move comment up 2018-03-09 10:09:36 +00:00
Matthew Hodgson beafd597dd ensure indexeddb workers are never double-connected 2018-03-09 02:18:19 +00:00
Matthew Hodgson fbc43b0d58 stupid typo 2018-03-09 00:01:14 +00:00
David Baker facfcf679d DeviceList: bring save forward if necessary
If save is called with a delay that would want the save to happen
sooner then the save we currently have scheduled, cancel the
current save and schedule a new one for the sooner time.
2018-03-08 15:35:35 +00:00
Luke Barnard 68b230a78f Add function to cancel and resend key request (#624) 2018-03-08 15:01:01 +00:00
David Baker 3d1fcc6f83 One day I'll learn to spell guaranteed 2018-03-08 14:26:48 +00:00
David Baker a0578efeb9 fix tests 2018-03-08 13:52:48 +00:00
David Baker 727ad5755e lint 2018-03-08 12:40:01 +00:00
David Baker 4f17352858 Don't do /keys/changes on incremental sync
Remove the call to /keys/changes when we do an incremental syn
where the old sync token doesn't match the one in the device list
store. To allow us to do this, always save the device list store
before saving the sync data, so we can safely assume the device
list store is at least as fresh as the sync token in the sync store.

Thread save functions through to allow this, add a delay parameter
so the sync can save the device list immediately and skip the wait,
and add a wantsSave() method so the sync can skip saving the device
list if the sync store isn't going to save anyway.

Fixes https://github.com/vector-im/riot-web/issues/6068
2018-03-08 12:33:08 +00:00
David Baker 0eb72122ce Merge pull request #623 from matrix-org/dbkr/devicelist_is_too_dirty
Don't mark devicelist dirty unnecessarily
2018-03-07 17:55:38 +00:00
David Baker 66e2b3bb70 Don't mark devicelist dirty unnecessarily
This was marking the device list as dirty even when nothing had
actually changed, causing unnecessary saves.
2018-03-07 17:32:51 +00:00
David Baker 5f12d858eb Keep a push processor and re-use it. (#622)
Because it does some nice caching stuff but that's no good if we
re-create a new one each time.
2018-03-06 19:01:30 +00:00
Luke Barnard e258d6ca8d Fix bug in crypto-store-backend; use oncomplete (#620)
instead of onsuccess for the txn to add an outgoing room key
request.
2018-03-02 12:01:21 +00:00
David Baker 2d25dedbcc Merge pull request #619 from matrix-org/dbkr/cache_joined_member_count
Cache the joined member count for a room state
2018-02-26 16:56:26 +00:00
David Baker bdf6fcb222 Fix tests 2018-02-26 16:53:17 +00:00
David Baker 7e1cea1ef6 Cache the joined member count for a room state
Pushrule evaluation needs the count of joined room members and
filtering the list for joined members takes a nontrivial amount
of time, but caching it is trivial, both code and memory wise.
2018-02-26 16:43:26 +00:00
Richard Lewis d98c803b54 Merge pull request #618 from matrix-org/rxl881/stickers
Fix JS doc
2018-02-23 15:52:42 +00:00
Matthew Hodgson 95238466b5 Merge pull request #617 from matrix-org/dbkr/fix_push_actions_frozen_matrixevents
Precompute push actions for state events
2018-02-23 14:32:11 +00:00
Matthew Hodgson 71652043a0 improve doc 2018-02-23 14:31:51 +00:00
David Baker c9cbaf254b Precompute push actions for state events
State events get froze now, so we can't write to the push actions
cache after having done so: precompute the push actions to work
around this.
2018-02-23 11:06:22 +00:00
David Baker f9cc5cbd33 Merge pull request #616 from matrix-org/luke/fix-unverified-device-blacklist
Fix bug where global "Never send to unverified..." is ignored
2018-02-23 10:11:59 +00:00
Luke Barnard bcb9405793 Instead of emitting, rely on the promise 2018-02-22 18:41:18 +00:00
Luke Barnard 30cb6f196f Fix overriding undefined per-room unverified devices setting
If the per-room setting for preventing sending keys to unverified
devices is `undefined`, it overrides the global setting (which
could be `true`).
2018-02-22 18:02:39 +00:00
Luke Barnard 856ef01632 Emit a crypto.initComplete once crypto is initialised
So that the app can call crypto-dependent functions at
the correct point in time.
2018-02-22 17:59:37 +00:00
David Baker 6f95554655 Merge pull request #615 from matrix-org/dbkr/intern_legacy_membership
Intern legacy top-level 'membership' field
2018-02-22 17:01:38 +00:00
David Baker a72f915646 Intern legacy top-level 'membership' field
There is a legacy top-level 'membership' field on events. We were
interning the normal one in 'content', but not this one, so the
legacy field was still keeping a copy of the string 'join' / 'leave'
etc in memory for every member event.

This make the interning code know about this field.
2018-02-22 16:04:35 +00:00
Matthew Hodgson 94605417f6 Merge pull request #598 from matrix-org/t3chguy/implicit_rr_redaction
Don't synthesize RR for m.room.redaction as causes the RR to go missing.
2018-02-20 18:23:06 +00:00
David Baker 1e017df128 Merge pull request #613 from matrix-org/dbkr/dates_on_demand
Make Events create Dates on demand
2018-02-20 18:08:45 +00:00
David Baker ec27bb5131 Merge pull request #612 from matrix-org/dbkr/dont_clone_events
Stop cloning events when adding to state
2018-02-20 18:08:35 +00:00
David Baker 9637fc098a comment 2018-02-20 18:01:01 +00:00
David Baker 874020ced7 Make Events create Dates on demand
My test account had 37MB (shallow) of Date objects knocking around
in memory. This gets rid of them. I can't see any appreciable
difference in the time taken to switch rooms (where now we recreate
a bunch of Dates that previously would have been cached).
2018-02-20 17:49:25 +00:00
David Baker a7beedcfb6 Unused imports 2018-02-20 17:11:40 +00:00
David Baker 4351c4dd6f Merge pull request #611 from matrix-org/dbkr/use_initialisestate
De-dup code: use the initialiseState function
2018-02-20 17:10:11 +00:00
David Baker 1ddf7fb96c Add XXX comment 2018-02-20 17:07:14 +00:00
David Baker ec5cfe4ee9 Stop cloning events when adding to state
As comment hopefully explains.

On my test account:
Before: 394657 MatrixEvents, 53MB shallow size
After: 198863 MatrixEvents, 27MB shallow size
2018-02-20 16:45:20 +00:00
Matthew Hodgson 4fed2ea7bf log event IDs of ignored calls 2018-02-20 12:38:12 +00:00
Matthew Hodgson ae14cf4740 typo 2018-02-20 12:38:12 +00:00
David Baker 8aa68b3dc1 lint 2018-02-20 11:09:32 +00:00
David Baker e810ee7750 Add test for fixed state misbehaviour 2018-02-20 11:06:33 +00:00
David Baker 9a08194597 Factor out calls to getLiveTimeline 2018-02-19 16:17:30 +00:00
David Baker c77277b60c rename variable 2018-02-19 16:07:28 +00:00
David Baker 21a324558f Comments 2018-02-19 16:05:43 +00:00
David Baker 7a31751564 Merge pull request #610 from matrix-org/dbkr/sentinels_on_demand
Create sentinel members on-demand
2018-02-16 19:01:48 +00:00
David Baker b11bacc2e2 Process state events before timeline events
Don't ignore them: its valid to send them in a non-limited sync,
they're state events that preceed the timeline events.
2018-02-16 18:52:56 +00:00
David Baker 8c02e7ba67 comment 2018-02-16 17:52:39 +00:00
David Baker 275eb8d434 De-dup code: use the initialiseState function
This should behave identically, but the code here appeared to be
identical to the code in initialiseState, so let's use it (it also
has an extra sanity check in there that we only init empty timelines).
2018-02-16 17:49:29 +00:00
David Baker 736d0df38d Handle null userID
The tests inject messages with no sender, so for now let's maintain
behaviour as it was before.
2018-02-16 14:58:19 +00:00
David Baker 89d5d41015 Merge pull request #609 from matrix-org/dbkr/doc_sentinels
Some more doc on how sentinels work
2018-02-16 12:34:34 +00:00
David Baker 0e1444c84b grammar 2018-02-16 11:57:22 +00:00
David Baker 104f8b093d Remove redundant forEach 2018-02-16 11:55:45 +00:00
David Baker 1e638c376b Create sentinel members on-demand
We only need sentinel members for things like the 'sender' field
of events, so we previously created sentinels for everyone in the
room, but a large number of them were never used.

Instead, create them on-demand and cache them.
2018-02-16 11:53:24 +00:00
David Baker a2e1a6ca8f jsdoc doesn't like that 2018-02-16 10:59:44 +00:00
David Baker 337331ff1b Some more doc on how sentinels work 2018-02-16 10:37:50 +00:00
Richard Lewis 1dfde7cd80 Fix JS doc 2018-02-08 11:17:55 +00:00
Luke Barnard 7df2bfe7bc Honour cached push rules (#606)
so that we have push rules loaded into the client after doing
a cached sync (so that the client can assume that we do have
push rules, even if it was loaded from a cached sync).

These rules will be updated once `getPushRules` is completed
prior to the first networked sync.
2018-02-08 09:35:44 +00:00
lukebarnard 4ec90a4b99 Fix incorrect self - should be this 2018-02-07 17:31:16 +00:00
Luke Barnard d4e8f9039c Set sync token before incrementally syncing (#604)
Block on syncing from cache so that the sync token
has been set prior to incremental syncing.
2018-02-07 17:16:22 +00:00
Luke Barnard 4c6c00f16d Fix NPE when loading sync from cache (#603)
* Fix NPE when loading sync from cache
2018-02-07 16:04:47 +00:00
Luke Barnard fd30b25596 Allow a mode of operation without HS connection (#601)
* Allow a mode of operation without HS connection

Instead of blocking the first sync on getting the push rules and
filter, load sync data from disc.

When the client comes online, the push rules will be acquired and
a sync cycle started. This could be immediate if the client is
already online.

This could be a breaking change for clients that get push rules
before a successful sync has been done.
2018-02-06 17:46:39 +00:00
David Baker 032b7cab5d Fix name of 1:1s other person has left (#602)
Use their name as we did before rather than 'Empty Room'

Fixes https://github.com/vector-im/riot-web/issues/6083
2018-02-06 15:43:53 +00:00
David Baker 47dfb4b8cd Merge pull request #597 from matrix-org/dbkr/e2e_rooms_indexeddb
Migrate room encryption store to crypto store
2018-02-06 10:29:29 +00:00
David Baker 51e782b671 Merge pull request #600 from matrix-org/t3chguy/fix_3pid_dm_rooms
add parameter to getIdentityServerUrl to strip the protocol for invites
2018-02-05 12:07:31 +00:00
David Baker cf195262bf inline fucntion that was only used once 2018-01-31 18:12:46 +00:00
David Baker cf72052e46 Correct return type 2018-01-31 17:51:48 +00:00
David Baker 6f50c39b2a more doc 2018-01-31 17:49:58 +00:00
David Baker b6cd826dd7 basically I just can't type 2018-01-31 17:33:35 +00:00
David Baker f1194b1fbe more comment grammar 2018-01-31 17:33:02 +00:00
David Baker c0ca85fb3a comment grammar 2018-01-31 17:32:12 +00:00
David Baker 022df1b143 Revert npm helpfully adding packages 2018-01-31 17:30:12 +00:00
David Baker fdf987f081 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_rooms_indexeddb 2018-01-29 13:40:14 +01:00
David Baker f1e874cd18 Merge pull request #594 from matrix-org/dbkr/device_tracking_indexeddb
Move Device Tracking Data to Crypto Store
2018-01-29 13:38:50 +01:00
David Baker c3bede58aa Fux lying comment 2018-01-29 13:35:03 +01:00
Matthew Hodgson 38915eb7fc fix NPE biting yannick due to broken rules 2018-01-25 18:41:36 +01:00
David Baker 470bd23b3b Add new devices as we get them 2018-01-25 16:49:30 +01:00
Michael Telatynski d007eefe2e add parameter to getIdentityServerUrl to strip the protocol for invites
use new getIdentityServerUrl param in inviteByThreePid
2018-01-25 09:53:19 +00:00
Michael Telatynski 362f442a98 Don't synthesize RR for m.room.redaction as causes the RR to go missing. 2018-01-25 00:04:24 +00:00
David Baker 8c2645c5dd Comment typo 2018-01-24 21:39:46 +01:00
David Baker 1ba0e4809e Implement correct function
in localstorage and memory stores
2018-01-22 18:46:13 +01:00
David Baker 341371b613 lint 2018-01-22 18:37:02 +01:00
David Baker d856285271 Doc param 2018-01-22 18:35:29 +01:00
David Baker e4ffc93463 Remove unused function 2018-01-22 18:34:34 +01:00
David Baker 3149958319 Missed arg 2018-01-22 18:31:17 +01:00
David Baker ac659e8df1 remove debugging 2018-01-22 18:16:11 +01:00
David Baker 81d54c7558 comment typo 2018-01-22 18:11:37 +01:00
David Baker e4de333d83 Use the right prefix function to remove prefix 2018-01-22 17:55:40 +01:00
David Baker e72096328a Actually working migration 2018-01-22 17:46:13 +01:00
David Baker 88a082a533 Hopefully working migration 2018-01-22 17:34:09 +01:00
David Baker 4fbf4f1069 Hopefully address PR feedback 2018-01-22 17:08:19 +01:00
David Baker c360dd11ed Migrate e2e rooms to crypto store
Doesn't do data migration yet
2018-01-22 15:48:53 +01:00
Michael Telatynski 95d582ccee arguments true and false are actually invalid 2018-01-19 23:00:34 +00:00
Matthew Hodgson e0c9b990e7 blindly move crypto.suggestKeyRestore over to /sync 2018-01-18 20:59:08 +00:00
David Baker 074cfb7c58 Add more comments 2018-01-18 17:32:47 +00:00
David Baker 8b649cec8d All the copyrights 2018-01-18 11:52:27 +00:00
David Baker ea6974fc89 Return null if device data isn't set
This is more normal, and the code doesn't expect to get empty
objects here which is reasonable since it never sets one.
2018-01-17 19:27:22 +00:00
David Baker fb29da4e40 Update the catchingup flag
It may change whilst processing the sync
2018-01-17 19:17:48 +00:00
David Baker 461acbcc81 More test fixing
* Change test for new storage layer
 * Always store device keys we download, even if we weren't
   tracking the user.
2018-01-17 18:49:33 +00:00
David Baker 5bab8647b6 Fix device tracking with initial /sync
* Check whether we share an e2e room with user IDs in the 'left'
   field of /keys/changes: there's no guarantee we no longer share
   any e2e rooms with these users
 * Reset everyone's tracking status on an initial sync - just
   re-fetching device lists for all users we're currently tracking
   isn't good enough since room memberships may have changed.
 * Fix typo in test
2018-01-17 16:03:46 +00:00
David Baker f2d1222de7 Merge pull request #591 from matrix-org/luke/perf-push-processor
Optimise pushprocessor
2018-01-17 11:27:50 +00:00
David Baker 585ea14a23 more lint 2018-01-17 09:58:56 +00:00
David Baker 5a0997ded5 lint 2018-01-17 09:52:01 +00:00
David Baker 0174c5674f Make tests pass, finally.
Mostly making tests aware of new storage format or making them
force it to be written. Also some bugfixes like we didn't json
encode some things in the localstorage store and we didn't
correctly check the promise when requesting device data saves.
2018-01-16 17:57:49 +00:00
lukebarnard 9de8653936 Actually check the cache 2018-01-16 09:51:30 +00:00
David Baker 27d28b8247 More test fixing
Allow localstorage store to take a localstorage impl, make TestClient
pass a cryptostore & fix True/true typo
2018-01-15 16:27:28 +00:00
David Baker 110f43a246 Make DeviceList test pass
Includes making saveIfDirty() return a promise in case you care
about when changes got saved (which the test does).
2018-01-15 15:21:39 +00:00
lukebarnard 56612751f9 Handle underride and override rules with patterns 2018-01-15 15:16:22 +00:00
lukebarnard 751fe7349a Fix case insensitivity on new RegExps 2018-01-15 14:54:51 +00:00
Matthew Hodgson fb1b554b86 initial pseudocode WIP for e2e online backups 2018-01-15 01:50:24 +00:00
David Baker 36d7d33afc Null fix & lint
Don't end up with devices / device tracking status being null
2018-01-12 16:44:51 +00:00
David Baker a94f3c720e Fix migration 2018-01-12 15:12:57 +00:00
David Baker 6c1087e429 Migrate from session store 2018-01-12 14:41:08 +00:00
David Baker 2cdb010cff Fix lint 2018-01-12 14:17:10 +00:00
David Baker 60052f59a0 Add localstorage & memory device data stores 2018-01-12 14:05:34 +00:00
David Baker 29d44f809e delay saving so we can batch multiple operations 2018-01-12 12:01:00 +00:00
David Baker 83e4aa2755 Initial attempt at device tracking -> indexeddb
* Message sending works again, but

 * Marking multiple devices known (ie. 'send anyway') is very slow
   because it writes all device info out each time
 * Support for non-indexedb stores not written yet
 * No migration
2018-01-11 18:10:19 +00:00
David Baker eeb97f5b66 Merge pull request #592 from matrix-org/dbkr/set_event_error
Set event error before emitting
2018-01-09 18:08:12 +00:00
David Baker b6f26ae6a5 Oops: new year, new company 2018-01-09 18:05:02 +00:00
Richard Lewis f40435654a Merge pull request #590 from matrix-org/rxl881/stickers
Add event type for stickers [WIP]
2018-01-09 14:49:05 +00:00
David Baker 67d471ea3d Also happy new year and all that 2018-01-09 13:24:29 +00:00
David Baker 4946c5e687 Set event error before emitting
So the event object properties are in a consistent state when we
the event is emitted.

Fixes first part of https://github.com/vector-im/riot-web/issues/5936
2018-01-09 13:23:04 +00:00
David Baker af4f05c29e Merge pull request #587 from matrix-org/dbkr/inbound_sessions_to_cryptostore
Migrate inbound sessions to cryptostore
2018-01-09 10:41:38 +00:00
David Baker 145c76095a Remove unused countEndToEndInboundGroupSessions 2018-01-09 10:37:44 +00:00
Richard Lewis 90045b6faa Fix filtering. 2018-01-08 11:18:36 +00:00
Richard Lewis aef27d811a Set sticker type on event instead of message type. 2018-01-06 00:13:24 +00:00
lukebarnard e2e5f80298 Linting 2018-01-05 20:05:12 +00:00
lukebarnard ca0ed50172 Optimise pushprocessor
by not [re]creating RegExps unnecessarily.
2018-01-05 19:56:27 +00:00
Richard Lewis d44d63c1d6 Send sticker message in to a room. 2018-01-04 21:55:20 +00:00
David Baker 8403042297 Merge pull request #588 from pafcu/displaynames
Disambiguate names if they contain an mxid
2018-01-02 17:46:04 +00:00
David Baker 1568bb014d Merge remote-tracking branch 'origin/develop' into dbkr/inbound_sessions_to_cryptostore 2018-01-02 17:12:47 +00:00
David Baker 7a069c4018 doc return 2018-01-02 17:03:57 +00:00
David Baker c856eb931f Make error message more truthful 2018-01-02 16:32:33 +00:00
David Baker b290ce795f Update comment 2018-01-02 16:21:12 +00:00
David Baker 6f3d279165 Fix doc 2018-01-02 13:43:43 +00:00
David Baker 124ab30f98 jsdoc 2018-01-02 13:42:16 +00:00
David Baker fee90bab66 Wording fix 2018-01-02 13:35:52 +00:00
David Baker e26ade0e62 Wording fix 2018-01-02 13:32:51 +00:00
David Baker c43ccb860b Always migrate inbound group sessions 2018-01-02 13:31:57 +00:00
Johannes Bornhold 81de2b3afc Add getThirdpartyUser
Signed-off-by: Johannes Bornhold <johannes@bornhold.name>
2017-12-31 22:46:18 +01:00
Johannes Bornhold 9a53fa3876 Fix typo around getThirdpartyLocation 2017-12-31 01:24:27 +01:00
Richard van der Hoff f2b7e8b038 Remove spurious linty blank line 2017-12-14 22:36:53 +00:00
Stefan Parviainen 1863f1311b Disambiguate display name if it contains a mxid
Fixes https://github.com/vector-im/riot-web/issues/1811

Signed-off-by: Stefan Parviainen <pafcu@iki.fi>
2017-12-14 21:49:13 +01:00
David Baker e5086f22d6 Merge remote-tracking branch 'origin/develop' into dbkr/inbound_sessions_to_cryptostore 2017-12-08 19:06:36 +00:00
David Baker da90a3ca78 Merge pull request #585 from matrix-org/dbkr/count_sessions_before_migrate
Check for sessions in indexeddb before migrating
2017-12-08 18:29:50 +00:00
David Baker d397c5a251 Merge pull request #586 from matrix-org/dbkr/crypto_store_migrate_warning
Emit an event for crypto store migration
2017-12-08 18:17:23 +00:00
David Baker d26b4434b8 Merge pull request #575 from matrix-org/dbkr/udd_no_auto_show
Supporting fixes For making UnknownDeviceDialog not pop up automatically
2017-12-08 16:33:18 +00:00
David Baker 7188f17f9a more linting 2017-12-07 17:14:11 +00:00
David Baker b0365f8b0e json encode before saving to localstorage 2017-12-07 17:07:06 +00:00
David Baker a1ddeea00e Fix inbound group session migration
Apparently they are parsed at a different layer
2017-12-07 16:50:18 +00:00
David Baker 1414c24caf Missed txn 2017-12-07 15:03:02 +00:00
David Baker 0748c864cd Migrate inbound sessions from session store 2017-12-07 14:57:18 +00:00
David Baker f80626a0fa lint 2017-12-07 14:32:23 +00:00
David Baker 0fffd64a7f Fix key export 2017-12-07 14:23:36 +00:00
David Baker 8da48211d9 Fix exportInboundGroupSession 2017-12-06 22:42:19 +00:00
David Baker 0dd8ffa3a0 Much tedious linting 2017-12-06 22:36:24 +00:00
David Baker 0362e61f36 Unused var 2017-12-06 22:24:57 +00:00
David Baker d92a77f695 lint 2017-12-06 22:24:19 +00:00
David Baker e6dd573e8a Fix test (needs a cryptostore now) 2017-12-06 19:42:01 +00:00
David Baker b6330c3a4f er, this isn't an object 2017-12-06 19:41:44 +00:00
David Baker 10bc714f5c lint 2017-12-06 16:04:00 +00:00
David Baker 8f57723b88 Emit an event for crypto store migration
As we may want to warn the user to not go back to an older version.
2017-12-06 15:24:29 +00:00
David Baker 3d71bee85e Check for sessions in indexeddb before migrating
Don't migrate sessions from localstorage if we already have some
in indexeddb
2017-12-06 12:43:08 +00:00
David Baker 30e00d5fa7 Add impl to localstorage & memory store 2017-12-06 12:02:05 +00:00
David Baker bc99a9d792 Merge remote-tracking branch 'origin/develop' into dbkr/inbound_sessions_to_cryptostore 2017-12-06 10:05:33 +00:00
David Baker 61df41d21f Merge pull request #584 from matrix-org/dbkr/sessions_to_cryptostore
Move sessions to the crypto store
2017-12-06 10:04:33 +00:00
David Baker 0b4ef8dcbb Migrate inbound group sessions to crypto store 2017-12-05 21:47:22 +00:00
David Baker 9d7d48b9b5 s/sessionStore/cryptoStore/ 2017-12-05 14:04:18 +00:00
David Baker fb37150dfd Doc exception handling fun 2017-12-05 13:45:37 +00:00
David Baker bd08ed898d Maybe slightly better exception handling 2017-12-05 11:11:14 +00:00
David Baker d3bc525713 More doc 2017-12-05 10:23:26 +00:00
David Baker ae27c553ac More sensible loop & logging 2017-12-05 10:13:34 +00:00
David Baker 27030ae1e9 Use _unpickleSession in _getSession 2017-12-05 10:09:54 +00:00
David Baker fd083e1e66 doc 2017-12-05 10:04:06 +00:00
David Baker 5e4149ae76 Remove unused function 2017-12-04 18:14:56 +00:00
David Baker 859d462629 Lint 2017-12-04 18:08:38 +00:00
David Baker 8bfa81df42 Catch & rethrow decryption exceptions 2017-12-04 17:55:12 +00:00
David Baker 6782d53e28 Remove prefix from sessions store keys 2017-12-04 17:36:33 +00:00
David Baker 4796721d5c Migrate sessions from sessionStore 2017-12-04 17:12:13 +00:00
David Baker 55bbc71a17 Move sessionstore -> cryptostore migration
...into a separate function
2017-12-04 16:08:52 +00:00
David Baker 5372575b24 Implement session storage in memory & localstorage 2017-12-04 15:49:59 +00:00
David Baker d995019c6e lint 2017-12-04 14:40:32 +00:00
David Baker aa70da5659 Move sessions to the crypto store
This doesn't migrate existing ones yet
2017-12-04 14:33:03 +00:00
Luke Barnard 020b293068 Merge branch 'master' into develop 2017-12-04 11:58:30 +00:00
Luke Barnard 4ad153c425 Send correct m.visibility flag in js-sdk 2017-12-01 18:01:23 +00:00
David Baker 082683bf0e Merge pull request #582 from matrix-org/dbkr/crypto_store_txn_api
Change crypto store transaction API
2017-11-30 13:45:28 +00:00
Luke Barnard 7f590af0b5 Add API wrapper for multiple device deletion API (#583) 2017-11-30 10:16:18 +00:00
David Baker ecc1c86600 Doc some more things 2017-11-29 17:40:48 +00:00
David Baker fece506cdd delint 2017-11-29 16:35:12 +00:00
David Baker f11a58e2cb Change crypto store transaction API
To allow multiple things to be fetched/stored in a single
transaction.

Currently it is still just the account that's actually in
indexeddb though.
2017-11-29 16:22:54 +00:00
David Baker 0238ecebed Fix comment 2017-11-28 11:32:34 +00:00
Richard van der Hoff 0d6ffa3935 Merge pull request #581 from matrix-org/dbkr/copyrights
Add some missed copyright notices
2017-11-27 16:04:07 +00:00
David Baker 143632e635 Add some missed copyright notices 2017-11-27 15:56:35 +00:00
David Baker 9ec33a97bb Merge pull request #579 from matrix-org/dbkr/e2e_indexeddb
Move Olm account to IndexedDB
2017-11-27 13:57:27 +00:00
David Baker 7e2c236582 Missed a s/account data/picked account/ 2017-11-27 13:46:31 +00:00
David Baker 6ebfd175bc jsdoc clarifications 2017-11-22 18:37:16 +00:00
David Baker defaa918a6 Remove unused function 2017-11-22 18:26:26 +00:00
David Baker c4e70be0a5 Better comment wording 2017-11-22 18:25:25 +00:00
David Baker 57d425fae6 Make the save function not return a promise
This was entirely unnecessary and hopefully make things a bit
simpler to understand and has fewer asyncs flying around.
2017-11-22 18:05:08 +00:00
David Baker 6024163af8 s/accountData/pickledAccount/ 2017-11-22 17:50:00 +00:00
David Baker 44b35cdb3d Lint 2017-11-22 16:53:21 +00:00
David Baker 36ff0ad019 Merge pull request #580 from matrix-org/rav/fix_crypto_error_logging
Fix logging of DecryptionErrors to be more useful
2017-11-22 16:44:09 +00:00
David Baker 9218e518f1 Add LocalStorageCryptoStore
To avoid throwing away all the data for anyone running firefox in
one of the modes where indexedDB is broken.
2017-11-22 16:41:52 +00:00
Richard van der Hoff c80bde1f60 Fix logging of DecryptionErrors to be more useful
We were relying on being able to override toString in DecryptionError, which
(a) doesn't work thanks to https://github.com/babel/babel/issues/3083, and (b)
was a bit naughty anyway. Instead, just add a detailedString property and use
that.
2017-11-22 14:42:32 +00:00
David Baker 4b7157b987 Remove unnecessary 'if' 2017-11-22 14:31:06 +00:00
David Baker a90f592224 Add comment on deprecation 2017-11-22 14:12:35 +00:00
David Baker 59f228dab7 Migrate account from session store 2017-11-22 14:07:19 +00:00
David Baker bae3f5ceb7 It's a heap, not a stack 2017-11-22 10:19:27 +00:00
David Baker a5c5da5b8a Lint 2017-11-22 10:18:53 +00:00
David Baker 7ecf313132 Use a callback function at the store layer
Rather than a promise which relies on the caller's promise handler
code being run in the same tick which is not guaranteed.
2017-11-22 10:04:27 +00:00
David Baker 313cfacfa1 Add comment 2017-11-21 18:40:25 +00:00
David Baker fb991503a9 Move OLM account to IndexedDBd
Wraps all access to the account in a transaction so any updates
done to the account must be done in the same transaction, making the
update atomic between tabs.

Doesn't do any migration from localstorage yet.
2017-11-21 18:27:40 +00:00
David Baker c31ce641a1 Merge branch 'master' into develop 2017-11-17 15:57:22 +00:00
David Baker 26e28ed687 Don't spuriously send unknown devices error
Send a sensible error message for other errors.
2017-11-17 11:48:56 +00:00
David Baker 8bf92d84db oops - didn't mean to remove that bit of doc 2017-11-16 18:12:09 +00:00
David Baker 9e2bb5b37b Allow answer to be called again after failing
* Store the answer we generate so if we fail to send it, we can
   try to send it again (doing the same again doesn't work as
   webrtc is in the wrong state).
 * Don't send ICE candidates if the call is ringing: queue them up
   so we can send them later if we manage to actually send the
   answer.
2017-11-16 16:29:45 +00:00
David Baker a48a88c312 Don't send a hangup on user media failure
We won't have sent the invite anyway. Also termainate before we
fire the error event so the call is 'ended' when the event handlers
fire (which means if they try to hang up it's also ignored)
2017-11-15 17:18:47 +00:00
David Baker 76b2fc2a6c Merge remote-tracking branch 'origin/develop' into dbkr/udd_no_auto_show 2017-11-15 12:10:06 +00:00
Luke Barnard 4438d716b9 Merge branch 'master' into develop 2017-11-15 10:57:57 +00:00
David Baker 8fcf55d761 BREAKING CHANGE: Fixes for unknown device errors
* If we can't send an invite due to unknown devices, abort the
   call.
 * Don't transition to the `invite_sent` state until the invite
   has actually sent.
 * Add a specific error code for failure due to unknown devices.
 * Don't send ICE candidate messages if the call has ended.
 * Add an `event` property to errors from `sendEvent` so that the
   caller can resend or cancel the event.
2017-11-15 10:56:57 +00:00
Matthew Hodgson a35d70e995 Merge pull request #568 from turt2live/travis/granular-settings
[BREAKING] Change the behaviour of the unverfied devices blacklist flag
2017-11-15 10:31:11 +00:00
Travis Ralston ec68000105 Merge branch 'develop' into travis/granular-settings 2017-11-14 19:19:01 -07:00
Matthew Hodgson c707d3db00 Merge pull request #557 from turt2live/travis/presence
Support set_presence=offline for syncing
2017-11-14 23:41:37 +00:00
Matthew Hodgson d3572836bd Merge pull request #556 from turt2live/travis/improved-redact-check
Consider cases where the sender may not redact their own event
2017-11-14 23:25:06 +00:00
David Baker a5dac751b0 BREAKING CHANGE: Remove send_event_error
Reverts https://github.com/matrix-org/matrix-js-sdk/pull/378

This swallowed all errors from sendEvent, breaking the ICE candidate
retrying. react-sdk no longer listens for send_event_error so I
think it's best to just remove this.
2017-11-13 17:51:59 +00:00
Travis Ralston 7a59579dcd Support sending additional options with acceptance (#570)
To be able to send things like `{visibility: { type: 'private ' }}`

Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-10 14:14:11 +00:00
Travis Ralston f24b02cae4 It helps if you use the right function
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-09 11:01:42 -07:00
Travis Ralston 995f796a5d [BREAKING] Change the behaviour of the unverfied devices blacklist flag
Previously the global flag was used as a way to completely ignore the per-room option. This commit makes the per-room and global settings be more flexible to allow users to, for example, blacklist unverified devices in all room with the exception of one or two. This is done by making the global setting a device-level default and the per-room option allowing for 3 states: true, false, and unset (use device default).

Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-08 17:47:45 -07:00
Travis Ralston 7c851faba6 Support set_presence=offline for syncing
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-10-14 14:34:37 -06:00
Travis Ralston b70c219a05 Consider cases where the sender may not redact their own event
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-10-13 20:00:39 -06:00
116 changed files with 21353 additions and 2576 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
+17 -2
View File
@@ -1,7 +1,6 @@
module.exports = {
parser: "babel-eslint",
parser: "babel-eslint", // now needed for class properties
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
ecmaFeatures: {
}
@@ -15,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", {
@@ -51,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"],
@@ -67,5 +70,17 @@ module.exports = {
"padded-blocks": ["warn"],
"no-extend-native": ["warn"],
"camelcase": ["warn"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"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
+4 -1
View File
@@ -2,6 +2,9 @@
/.jsdoc
node_modules
/.npmrc
/*.log
package-lock.json
.lock-wscript
build/Release
coverage
@@ -12,6 +15,6 @@ reports
/lib
/specbuild
# version file and tarball created by 'npm pack'
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
/matrix-js-sdk-*.tgz
-5
View File
@@ -1,5 +0,0 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
script:
- ./travis.sh
+999
View File
File diff suppressed because it is too large Load Diff
+16 -7
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,7 +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.
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
+73 -18
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
@@ -21,7 +20,11 @@ Please check [the working browser example](examples/browser) for more informatio
In Node.js
----------
``npm install matrix-js-sdk``
Ensure you have the latest LTS version of Node.js installed.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
``yarn add matrix-js-sdk``
```javascript
var sdk = require("matrix-js-sdk");
@@ -30,9 +33,61 @@ In Node.js
console.log("Public Rooms: %s", JSON.stringify(data));
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
To start the client:
```javascript
await client.startClient({initialSyncLimit: 10});
```
You can perform a call to `/sync` to get the current state of the client:
```javascript
client.once('sync', function(state, prevState, res) {
if(state === 'PREPARED') {
console.log("prepared");
} else {
console.log(state);
process.exit(1);
}
});
```
To send a message:
```javascript
var content = {
"body": "message text",
"msgtype": "m.text"
};
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
console.log(err);
});
```
To listen for message events:
```javascript
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
});
```
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) => {
client.getRoom(roomId).timeline.forEach(t => {
console.log(t.event);
});
});
```
What does this SDK do?
----------------------
@@ -231,7 +286,7 @@ This SDK uses JSDoc3 style comments. You can manually build and
host the API reference from the source files like this:
```
$ npm run gendoc
$ yarn gendoc
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
```
@@ -242,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
@@ -262,20 +317,20 @@ 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:
* ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.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 ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
--save-optional`` (if your application also works without e2e crypto enabled)
or ``--save`` (if it doesn't) to do so.
If you want to package Olm as dependency for your node.js application, you can
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.
Contributing
@@ -285,7 +340,7 @@ want to use this SDK, skip this section.*
First, you need to pull in the right build tools:
```
$ npm install
$ yarn install
```
Building
@@ -293,20 +348,20 @@ Building
To build a browser version from scratch when developing::
```
$ npm run build
$ yarn build
```
To constantly do builds when files are modified (using ``watchify``)::
```
$ npm run watch
$ yarn watch
```
To run tests (Jasmine)::
```
$ npm test
$ yarn test
```
To run linting:
```
$ npm run lint
$ yarn lint
```
+13 -1
View File
@@ -1,5 +1,17 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("browser-request"));
const request = require('browser-request');
const queryString = require('qs');
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
// we mimic `request`'s query string interface to make it all work
// as expected.
// browser-request will happily take the constructed string as the
// query string without trying to modify it further.
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
return request(opts, fn);
});
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
+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
+3 -3
View File
@@ -202,9 +202,9 @@ function printRoomList() {
dateStr = new Date(msg.getTs()).toISOString().replace(
/T/, ' ').replace(/\..+/, '');
}
var me = roomList[i].getMember(myUserId);
if (me) {
fmt = fmts[me.membership];
var myMembership = roomList[i].getMyMembership();
if (myMembership) {
fmt = fmts[myMembership];
}
var roomName = fixWidth(roomList[i].name, 25);
print(
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
npm run lint
yarn lint
+9 -7
View File
@@ -5,8 +5,8 @@ set -x
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 6 || exit $?
npm install || exit $?
nvm use 10 || exit $?
yarn install || exit $?
RC=0
@@ -18,17 +18,19 @@ function fail {
# don't use last time's test reports
rm -rf reports coverage || exit $?
npm test || fail "npm test finished with return code $?"
yarn test || fail "yarn test finished with return code $?"
npm run -s lint -- -f checkstyle > eslint.xml ||
yarn -s lint -f checkstyle > eslint.xml ||
fail "eslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
npm pack ||
fail "npm pack finished with return code $?"
# `yarn pack` doesn't seem to run scripts, however that seems okay here as we
# just built as part of `install` above.
yarn pack ||
fail "yarn pack finished with return code $?"
npm run gendoc || fail "JSDoc failed with code $?"
yarn gendoc || fail "JSDoc failed with code $?"
exit $RC
+28 -18
View File
@@ -1,24 +1,27 @@
{
"name": "matrix-js-sdk",
"version": "0.9.2",
"version": "2.3.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "babel -s -w -d lib src",
"test": "yarn test:build && yarn test:run",
"check": "yarn test:build && _mocha --recursive specbuild --colors",
"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",
"dist": "npm run build",
"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 109 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
"lint": "eslint --max-warnings 101 src spec",
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
},
"keywords": [
@@ -53,32 +56,39 @@
"babel-runtime": "^6.26.0",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.2",
"request": "^2.53.0"
"loglevel": "1.6.1",
"qs": "^6.5.2",
"request": "^2.88.0",
"unhomoglyph": "^1.0.2"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-eslint": "^7.1.1",
"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": "^14.0.0",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"eslint": "^3.13.1",
"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.0",
"mocha": "^3.2.0",
"mocha-jenkins-reporter": "^0.3.6",
"matrix-mock-request": "^1.2.3",
"mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.4.0",
"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",
"watchify": "^3.2.1"
"terser": "^4.0.0",
"watchify": "^3.11.1"
},
"browserify": {
"transform": [
+50 -15
View File
@@ -6,12 +6,26 @@
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (OSX) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
exit
fi
else
echo "hub is required: please install it"
exit
fi
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
@@ -45,7 +59,8 @@ fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
while getopts hc:xz f; do
expected_npm_user="matrixdotorg"
while getopts hc:u:xz f; do
case $f in
h)
help
@@ -60,6 +75,9 @@ while getopts hc:xz f; do
z)
skip_jsdoc=1
;;
u)
expected_npm_user="$OPTARG"
;;
esac
done
shift `expr $OPTIND - 1`
@@ -74,6 +92,14 @@ if [ -z "$skip_changelog" ]; then
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
@@ -127,14 +153,22 @@ cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_cha
set -x
# Bump package.json and build the dist
echo "npm version"
# npm version will automatically commit its modification
echo "yarn version"
# yarn version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
yarn version --no-git-tag-version --new-version "$release"
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
then
pkglock='yarn.lock'
else
pkglock=''
fi
git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
@@ -150,7 +184,7 @@ fi
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from npm link)
# (rather than whatever we're pulling in from yarn link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
@@ -161,10 +195,10 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
npm install
yarn install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
DIST_VERSION="$tag" yarn dist
popd
@@ -245,7 +279,7 @@ release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
hub release create $hubflags $assets -F "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
@@ -253,12 +287,13 @@ fi
rm "${release_text}"
rm "${latest_changes}"
# publish to npmjs
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
npm publish
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
npm run gendoc
yarn gendoc
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
@@ -281,9 +316,9 @@ fi
echo "updating master branch"
git checkout master
git pull
git merge --ff-only "$rel_branch"
git merge "$rel_branch"
# push master and docs (if generated) to github
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
+30 -12
View File
@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations 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.
@@ -25,6 +26,8 @@ import testUtils from './test-utils';
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
@@ -36,9 +39,10 @@ import Promise from 'bluebird';
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
* @param {object} options additional options to pass to the client
*/
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend,
userId, deviceId, accessToken, sessionStoreBackend, options,
) {
this.userId = userId;
this.deviceId = deviceId;
@@ -46,16 +50,24 @@ export default function TestClient(
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new testUtils.MockStorageApi();
}
this.storage = new sdk.WebStorageSessionStore(sessionStoreBackend);
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
options = Object.assign({
baseUrl: "http://" + userId + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: this.storage,
sessionStore: sessionStore,
request: this.httpBackend.requestFn,
});
}, options);
if (!options.cryptoStore) {
// expose this so the tests can get to it
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
options.cryptoStore = this.cryptoStore;
}
this.client = sdk.createClient(options);
this.deviceKeys = null;
this.oneTimeKeys = {};
@@ -71,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();
@@ -89,15 +101,17 @@ TestClient.prototype.start = function() {
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(this + ': started');
logger.log(this + ': started');
});
};
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
return this.httpBackend.stop();
};
/**
@@ -109,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);
@@ -146,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: {
@@ -172,7 +186,11 @@ TestClient.prototype.expectKeyQuery = function(response) {
this.httpBackend.when('POST', '/keys/query').respond(
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual({});
expect(content.device_keys[userId]).toEqual(
{},
"Expected key query for " + userId + ", got " +
Object.keys(content.device_keys),
);
});
return response;
});
@@ -206,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`);
});
};
+96 -62
View File
@@ -1,8 +1,26 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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 expect from 'expect';
import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
import logger from '../../src/logger';
const ROOM_ID = "!room:id";
@@ -54,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;
}
@@ -70,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.
@@ -80,17 +98,18 @@ describe("DeviceList management:", function() {
});
afterEach(function() {
aliceTestClient.stop();
return aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
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: {
@@ -103,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/',
@@ -126,6 +145,7 @@ describe("DeviceList management:", function() {
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
@@ -151,9 +171,12 @@ describe("DeviceList management:", function() {
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
aliceTestClient.client._crypto._deviceList.saveIfDirty(),
]);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
expect(data.syncToken).toEqual(1);
});
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
@@ -185,19 +208,21 @@ describe("DeviceList management:", function() {
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
chrisStat);
}
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat,
);
}
});
// now add an expectation for a query for bob's devices, and let
// it complete.
@@ -216,15 +241,18 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
bobStat);
}
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = data.trackingStatus['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error(
'Unexpected status for chris: wanted 1 or 2, got ' + bobStat,
);
}
});
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
@@ -234,16 +262,18 @@ describe("DeviceList management:", function() {
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
}).then(() => {
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
const chrisStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(data.syncToken).toEqual(3);
});
});
});
}).timeout(3000);
// https://github.com/vector-im/riot-web/issues/4983
describe("Alice should know she has stale device lists", () => {
@@ -262,13 +292,15 @@ describe("DeviceList management:", function() {
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
);
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
);
});
});
it("when Bob leaves", async function() {
@@ -297,12 +329,15 @@ describe("DeviceList management:", function() {
await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
});
it("when Alice leaves", async function() {
@@ -330,12 +365,15 @@ describe("DeviceList management:", function() {
);
await aliceTestClient.flushSync();
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
});
it("when Bob leaves whilst Alice is offline", async function() {
@@ -344,23 +382,19 @@ describe("DeviceList management:", function() {
const anotherTestClient = await createTestClient();
try {
anotherTestClient.httpBackend.when('GET', '/keys/changes').respond(
200, {
changed: [],
left: ['@bob:xyz'],
},
);
await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
const bobStat = anotherTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const bobStat = data.trackingStatus['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
} finally {
anotherTestClient.stop();
}
+39 -18
View File
@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 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.
@@ -35,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";
@@ -71,7 +73,11 @@ function expectAliQueryKeys() {
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
aliTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
expect(content.device_keys[bobUserId]).toEqual(
{},
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
@@ -90,12 +96,16 @@ 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",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
expect(content.device_keys[aliUserId]).toEqual(
{},
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
@@ -154,11 +164,15 @@ function aliDownloadsKeys() {
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return Promise.all([p1, p2]).then(function() {
const devices = aliTestClient.storage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
return Promise.all([p1, p2]).then(() => {
return aliTestClient.client._crypto._deviceList.saveIfDirty();
}).then(() => {
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
const devices = data.devices[bobUserId];
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
});
});
}
@@ -321,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);
@@ -392,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();
@@ -405,10 +419,10 @@ describe("MatrixClient crypto", function() {
});
afterEach(function() {
aliTestClient.stop();
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.stop();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", function() {
@@ -539,6 +553,7 @@ describe("MatrixClient crypto", function() {
});
it("Ali sends a message", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -549,6 +564,7 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives a message", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -559,6 +575,7 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives a message with a bogus sender", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -591,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);
};
@@ -612,6 +629,7 @@ describe("MatrixClient crypto", function() {
});
it("Ali blocks Bob's device", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -631,6 +649,7 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives two pre-key messages", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -644,6 +663,8 @@ describe("MatrixClient crypto", function() {
});
it("Bob replies to the message", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -654,13 +675,14 @@ describe("MatrixClient crypto", function() {
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
}).then(aliRecvMessage);
});
it("Ali does a key query when encryption is enabled", function() {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
@@ -691,7 +713,6 @@ describe("MatrixClient crypto", function() {
}).then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[aliUserId]: {},
[bobUserId]: {},
},
});
@@ -714,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();
@@ -726,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")
@@ -735,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
+26 -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({
@@ -30,6 +30,7 @@ describe("MatrixClient events", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("emissions", function() {
@@ -156,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,
);
@@ -301,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);
});
@@ -111,6 +112,7 @@ describe("getEventTimeline support", function() {
if (client) {
client.stopClient();
}
return httpBackend.stop();
});
it("timeline support must be enabled to work", function(done) {
@@ -226,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);
@@ -668,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");
}),
@@ -683,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);
+12 -11
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);
@@ -41,13 +41,14 @@ describe("MatrixClient", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("uploadContent", 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");
@@ -86,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" });
@@ -106,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();
@@ -159,7 +160,7 @@ describe("MatrixClient", function() {
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
const roomId = "!foo:bar";
const room = new Room(roomId);
const room = new Room(roomId, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true,
@@ -354,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) {
@@ -395,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);
+14 -17
View File
@@ -58,12 +58,13 @@ 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();
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("without opts.store", function() {
@@ -94,7 +95,7 @@ describe("MatrixClient opts", function() {
httpBackend.flush("/txn1", 1);
});
it("should be able to sync / get new events", function(done) {
it("should be able to sync / get new events", async function() {
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create",
@@ -110,20 +111,16 @@ describe("MatrixClient opts", function() {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/filter", 1);
}).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
await client.startClient();
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
});
});
@@ -131,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,
+2 -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();
@@ -36,6 +36,7 @@ describe("MatrixClient retrying", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
@@ -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({
@@ -130,6 +130,7 @@ describe("MatrixClient room timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("local echo events", function() {
+30 -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({
@@ -38,6 +38,7 @@ describe("MatrixClient syncing", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("startClient", function() {
@@ -437,6 +438,34 @@ describe("MatrixClient syncing", function() {
});
});
// XXX: This test asserts that the js-sdk obeys the spec and treats state
// events that arrive in the incremental sync as if they preceeded the
// timeline events, however this breaks peeking, so it's disabled
// (see sync.js)
xit("should correctly interpret state in incremental sync.", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomOne);
const stateAtStart = room.getLiveTimeline().getState(
EventTimeline.BACKWARDS,
);
const startRoomNameEvent = stateAtStart.getStateEvents('m.room.name', '');
expect(startRoomNameEvent.getContent().name).toEqual('Old room name');
const stateAtEnd = room.getLiveTimeline().getState(
EventTimeline.FORWARDS,
);
const endRoomNameEvent = stateAtEnd.getStateEvents('m.room.name', '');
expect(endRoomNameEvent.getContent().name).toEqual('A new room name');
});
});
xit("should update power levels for users in a room", function() {
});
+32 -20
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",
@@ -296,7 +297,7 @@ describe("megolm", function() {
});
afterEach(function() {
aliceTestClient.stop();
return aliceTestClient.stop();
});
it("Alice receives a megolm message", function() {
@@ -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);
});
});
@@ -499,6 +500,7 @@ describe("megolm", function() {
it('Alice sends a megolm message', function() {
let p2pSession;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -554,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);
@@ -581,6 +583,7 @@ describe("megolm", function() {
});
it("We shouldn't attempt to send to blocked devices", function() {
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -598,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'),
@@ -609,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, {
@@ -634,6 +637,7 @@ describe("megolm", function() {
let p2pSession;
let megolmSessionId;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -653,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'),
@@ -666,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 {};
@@ -685,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',
@@ -701,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',
@@ -789,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
@@ -809,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 {
@@ -817,8 +821,14 @@ describe("megolm", function() {
};
});
// Grab the event that we'll need to resend
const room = aliceTestClient.client.getRoom(ROOM_ID);
const pendingEvents = room.getPendingEvents();
expect(pendingEvents.length).toEqual(1);
const unsentEvent = pendingEvents[0];
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.client.resendEvent(unsentEvent, room),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
@@ -837,6 +847,7 @@ describe("megolm", function() {
let downloadPromise;
let sendPromise;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -855,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.
@@ -880,6 +891,7 @@ describe("megolm", function() {
it("Alice exports megolm keys and imports them to a new device", function() {
let messageEncrypted;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
+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);
});
});
+670
View File
@@ -0,0 +1,670 @@
/*
Copyright 2018 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.
*/
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const utils = require("../test-utils");
const AutoDiscovery = sdk.AutoDiscovery;
import expect from 'expect';
import MockHttpBackend from "matrix-mock-request";
describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});
it("should throw an error when no domain is specified", function() {
return Promise.all([
AutoDiscovery.findClientConfig(/* no args */).then(() => {
throw new Error("Expected a failure, not success with no args");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig("").then(() => {
throw new Error("Expected a failure, not success with an empty string");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(null).then(() => {
throw new Error("Expected a failure, not success with null");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(true).then(() => {
throw new Error("Expected a failure, not success with a non-string");
}, () => {
return true;
}),
]);
});
it("should return PROMPT when .well-known 404s", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "PROMPT",
error: null,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (empty string)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (no property)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (disallowed scheme)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "mxc://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HS_BASE_URL,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 404)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 500)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 200 but wrong content)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
not_matrix_versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_HOMESERVER,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
"m.homeserver", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS with the right homeserver URL", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(missing base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
not_base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
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: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(empty base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
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: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 404)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
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: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 500)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
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: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS when the identity server configuration is " +
"verifiably accurate", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "SUCCESS",
error: null,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS and preserve non-standard keys from the " +
".well-known response", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
"org.example.custom.property": {
cupcakes: "yes",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "SUCCESS",
error: null,
base_url: "https://identity.example.org",
},
"org.example.custom.property": {
cupcakes: "yes",
},
};
expect(conf).toEqual(expected);
}),
]);
});
});
+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",
);
});
+354 -8
View File
@@ -1,20 +1,366 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
let Crypto;
if (sdk.CRYPTO_ENABLED) {
Crypto = require("../../lib/crypto");
}
import '../olm-loader';
import Crypto from '../../lib/crypto';
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 TestClient from '../TestClient';
import {MatrixEvent} from '../../lib/models/event';
import Room from '../../lib/models/room';
import olmlib from '../../lib/crypto/olmlib';
import lolex from 'lolex';
const EventEmitter = require("events").EventEmitter;
const sdk = require("../..");
const Olm = global.Olm;
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
beforeEach(function(done) {
Olm.init().then(done);
});
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(2);
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
describe('Session management', function() {
const otkResponse = {
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:FLIBBLE': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally a valid signature',
},
},
},
},
},
},
};
let crypto;
let mockBaseApis;
let mockRoomList;
let fakeEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
cryptoStore.storeEndToEndDeviceData({
devices: {
'@bob:home.server': {
'BOBDEVICE': {
keys: {
'curve25519:BOBDEVICE': 'this is a key',
},
},
},
},
trackingStatus: {},
});
mockBaseApis = {
sendToDevice: expect.createSpy(),
getKeyBackupVersion: expect.createSpy(),
isGuest: expect.createSpy(),
};
mockRoomList = {};
fakeEmitter = new EventEmitter();
crypto = new Crypto(
mockBaseApis,
sessionStore,
"@alice:home.server",
"FLIBBLE",
sessionStore,
cryptoStore,
mockRoomList,
);
crypto.registerEventHandlers(fakeEmitter);
await crypto.init();
});
afterEach(async function() {
await crypto.stop();
});
it("restarts wedged Olm sessions", async function() {
const prom = new Promise((resolve) => {
mockBaseApis.claimOneTimeKeys = function() {
resolve();
return otkResponse;
};
});
fakeEmitter.emit('toDeviceEvent', {
getType: expect.createSpy().andReturn('m.room.message'),
getContent: expect.createSpy().andReturn({
msgtype: 'm.bad.encrypted',
}),
getWireContent: expect.createSpy().andReturn({
algorithm: 'm.olm.v1.curve25519-aes-sha2',
sender_key: 'this is a key',
}),
getSender: expect.createSpy().andReturn('@bob:home.server'),
});
await prom;
});
});
describe('Key requests', function() {
let aliceClient;
let bobClient;
beforeEach(async function() {
aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await aliceClient.initCrypto();
await bobClient.initCrypto();
});
afterEach(async function() {
aliceClient.stopClient();
bobClient.stopClient();
});
it(
"does not cancel keyshare requests if some messages are not decrypted",
async function() {
function awaitEvent(emitter, event) {
return new Promise((resolve, reject) => {
emitter.once(event, (result) => {
resolve(result);
});
});
}
async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent();
const key = await aliceClient._crypto._olmDevice
.getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id,
index,
);
const ksEvent = new MatrixEvent({
type: "m.forwarded_room_key",
sender: "@alice:example.com",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: eventContent.sender_key,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: eventContent.session_id,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain:
key.forwarding_curve_key_chain,
},
});
// make onRoomKeyEvent think this was an encrypted event
ksEvent._senderCurve25519Key = "akey";
return ksEvent;
}
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient._crypto.encryptEvent(event, aliceRoom);
event._clearEvent = {};
event._senderCurve25519Key = null;
event._claimedEd25519Key = null;
try {
await bobClient._crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
let eventPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
// keyshare the session key starting at the second message, so
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(events[1], 1);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
const cryptoStore = bobClient._cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
// the room key request should still be there, since we haven't
// decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toExist();
// keyshare the session key starting at the first message, so
// that it can now be decrypted
eventPromise = awaitEvent(events[0], "Event.decrypted");
ksEvent = await keyshareEventForEvent(events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
// the room key request should be gone since we've now decypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toNotExist();
},
);
it("creates a new keyshare request if we request a keyshare", async function() {
// make sure that cancelAndResend... creates a new keyshare request
// if there wasn't an already-existing one
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient._cryptoStore;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom",
session_id: "sessionid",
sender_key: "senderkey",
};
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toExist();
});
it("uses a new txnid for re-requesting keys", async function() {
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
/* return a promise and a function. When the function is called,
* the promise will be resolved.
*/
function awaitFunctionCall() {
let func;
const promise = new Promise((resolve, reject) => {
func = function(...args) {
resolve(args);
return new Promise((resolve, reject) => {
// give us some time to process the result before
// continuing
global.setTimeout(resolve, 1);
});
};
});
return {func, promise};
}
aliceClient.startClient();
const clock = lolex.install();
try {
let promise;
// make a room key request, and record the transaction ID for the
// sendToDevice call
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
clock.runToLast();
let args = await promise;
const txnId = args[2];
clock.runToLast();
// give the room key request manager time to update the state
// of the request
await Promise.resolve();
// cancel and resend the room key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
clock.runToLast();
// the first call to sendToDevice will be the cancellation
args = await promise;
// the second call to sendToDevice will be the key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
clock.runToLast();
args = await promise;
clock.runToLast();
expect(args[2]).toNotBe(txnId);
} finally {
clock.uninstall();
}
});
});
});
+46 -18
View File
@@ -1,8 +1,25 @@
/*
Copyright 2017 Vector Creations 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.
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 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';
@@ -39,14 +56,22 @@ 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();
});
afterEach(function() {
for (const dl of deviceLists) {
dl.stop();
}
});
function createTestDeviceList() {
@@ -56,7 +81,9 @@ describe('DeviceList', function() {
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
return new DeviceList(baseApis, sessionStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
deviceLists.push(dl);
return dl;
}
it("should successfully download and store device keys", function() {
@@ -72,7 +99,7 @@ describe('DeviceList', function() {
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
return prom1.then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
@@ -97,17 +124,18 @@ describe('DeviceList', function() {
dl.invalidateUserDeviceList('@test1:sw1v.org');
dl.refreshOutdatedDeviceLists();
// the first request completes
queryDefer1.resolve({
device_keys: {
'@test1:sw1v.org': {},
},
});
return prom1.then(() => {
dl.saveIfDirty().then(() => {
// the first request completes
queryDefer1.resolve({
device_keys: {
'@test1:sw1v.org': {},
},
});
return prom1;
}).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();
@@ -121,7 +149,7 @@ describe('DeviceList', function() {
// allow promise chain to complete
return prom3;
}).then(() => {
const storedKeys = sessionStore.getEndToEndDevicesForUser('@test1:sw1v.org');
const storedKeys = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
+103 -21
View File
@@ -1,34 +1,28 @@
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run megolm tests: libolm not available");
}
import '../../../olm-loader';
import expect from 'expect';
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';
// Crypto and OlmDevice won't import unless we have global.Olm
let OlmDevice;
let Crypto;
if (global.Olm) {
OlmDevice = require('../../../../lib/crypto/OlmDevice');
Crypto = require('../../../../lib/crypto');
}
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'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
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;
}
@@ -37,16 +31,18 @@ describe("MegolmDecryption", function() {
let mockCrypto;
let mockBaseApis;
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
await Olm.init();
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockBaseApis = {};
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore);
const olmDevice = new OlmDevice(cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
@@ -67,7 +63,7 @@ describe("MegolmDecryption", function() {
describe('receives some keys:', function() {
let groupSession;
beforeEach(function() {
beforeEach(async function() {
groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
@@ -96,7 +92,7 @@ describe("MegolmDecryption", function() {
},
};
return event.attemptDecryption(mockCrypto).then(() => {
await event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
});
});
@@ -264,5 +260,91 @@ describe("MegolmDecryption", function() {
// test is successful if no exception is thrown
});
});
it("re-uses sessions for sequential messages", async function() {
const mockStorage = new MockStorageApi();
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(cryptoStore);
olmDevice.verifySignature = expect.createSpy();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
},
},
},
},
},
},
}));
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
mockCrypto.downloadKeys.andReturn(Promise.resolve({
'@alice:home.server': {
aliceDevice: {
deviceId: 'aliceDevice',
isBlocked: expect.createSpy().andReturn(false),
isUnverified: expect.createSpy().andReturn(false),
getIdentityKey: expect.createSpy().andReturn(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: expect.createSpy().andReturn(''),
},
},
}));
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
const mockRoom = {
getEncryptionTargetMembers: expect.createSpy().andReturn(
[{userId: "@alice:home.server"}],
),
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
};
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
);
mockBaseApis.claimOneTimeKeys.reset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
});
});
});
+143
View File
@@ -0,0 +1,143 @@
/*
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.
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 '../../../olm-loader';
import expect from 'expect';
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 cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(cryptoStore);
return olmDevice;
}
async function setupSession(initiator, opponent) {
await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys();
const firstKey = Object.values(keys['curve25519'])[0];
const sid = await initiator.createOutboundSession(
opponent.deviceCurve25519Key, firstKey,
);
return sid;
}
describe("OlmDecryption", function() {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
return;
}
let aliceOlmDevice;
let bobOlmDevice;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
await global.Olm.init();
aliceOlmDevice = makeOlmDevice();
bobOlmDevice = makeOlmDevice();
await aliceOlmDevice.init();
await bobOlmDevice.init();
});
describe('olm', function() {
it("can decrypt messages", async function() {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
ciphertext.type,
ciphertext.body,
);
expect(result.payload).toEqual(
"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);
});
});
});
+472
View File
@@ -0,0 +1,472 @@
/*
Copyright 2018 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 '../../olm-loader';
import expect from 'expect';
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 Olm = global.Olm;
const MatrixClient = sdk.MatrixClient;
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const ENCRYPTED_EVENT = new MatrixEvent({
type: 'm.room.encrypted',
room_id: '!ROOM:ID',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
},
event_id: '$event1',
origin_server_ts: 1507753886000,
});
const KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
},
};
const BACKUP_INFO = {
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
function makeTestClient(sessionStore, cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
return new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
}
describe("MegolmBackup", function() {
if (!global.Olm) {
logger.warn('Not running megolm backup unit tests: libolm not present');
return;
}
let olmDevice;
let mockOlmLib;
let mockCrypto;
let mockStorage;
let sessionStore;
let cryptoStore;
let megolmDecryption;
beforeEach(async function() {
await Olm.init();
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = BACKUP_INFO;
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore(mockStorage);
olmDevice = new OlmDevice(cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
mockOlmLib.encryptMessageForDevice =
expect.createSpy().andReturn(Promise.resolve());
});
describe("backup", function() {
let mockBaseApis;
let realSetTimeout;
beforeEach(function() {
mockBaseApis = {};
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry.
realSetTimeout = global.setTimeout;
global.setTimeout = function(f, n) {
return realSetTimeout(f, n/100);
};
});
afterEach(function() {
global.setTimeout = realSetTimeout;
});
it('automatically calls the key back up', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
const decryptedData = {
clearEvent: {
type: 'm.room_key',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: ROOM_ID,
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
},
},
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
mockCrypto.decryptEvent = function() {
return Promise.resolve(decryptedData);
};
mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.backupGroupSession = expect.createSpy();
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
});
});
it('sends backups to the server', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqualTo(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
};
client._crypto.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(1);
});
});
});
it('retries when a backup fails', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "foobar",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqualTo(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({});
} else {
return Promise.reject(
new Error("this is an expected failure"),
);
}
};
client._crypto.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(2);
});
});
});
});
describe("restore", function() {
let client;
beforeEach(function() {
client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
});
afterEach(function() {
client.stopClient();
});
it('can restore from backup', function() {
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('can restore backup by room', function() {
client._http.authedRequest = function() {
return Promise.resolve({
rooms: {
[ROOM_ID]: {
sessions: {
[SESSION_ID]: KEY_BACKUP_DATA,
},
},
},
});
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
});
});
@@ -0,0 +1,146 @@
/*
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.
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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
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';
const Olm = global.Olm;
describe("QR code verification", function() {
if (!global.Olm) {
logger.warn('Not running device verification tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
describe("showing", function() {
it("should emit an event to show a QR code", async function() {
const qrCode = new ShowQRCode({
getUserId: () => "@alice:example.com",
deviceId: "ABCDEFG",
getDeviceEd25519Key: function() {
return "device+ed25519+key";
},
});
const spy = expect.createSpy().andCall((e) => {
qrCode.done();
});
qrCode.on("show_qr_code", spy);
await qrCode.verify();
expect(spy).toHaveBeenCalledWith({
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
});
});
});
describe("scanning", function() {
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
it("should verify when a QR code is sent", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: expect.createSpy().andReturn(device),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client);
qrCode.on("confirm_user_id", ({userId, confirm}) => {
if (userId === "@alice:example.com") {
confirm();
} else {
qrCode.cancel(new Error("Incorrect user"));
}
});
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
await qrCode.verify();
expect(client.getStoredDevice)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
expect(client.setDeviceVerified)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
});
it("should error when the user ID doesn't match", async function() {
const client = {
getStoredDevice: expect.createSpy(),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = expect.createSpy();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(client.getStoredDevice).toNotHaveBeenCalled();
expect(client.setDeviceVerified).toNotHaveBeenCalled();
});
it("should error if the key doesn't match", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: expect.createSpy().andReturn(device),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = expect.createSpy();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(client.getStoredDevice).toHaveBeenCalled();
expect(client.setDeviceVerified).toNotHaveBeenCalled();
});
});
});
@@ -0,0 +1,82 @@
/*
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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import {verificationMethods} from '../../../../lib/crypto';
import SAS from '../../../../lib/crypto/verification/SAS';
const Olm = global.Olm;
import {makeTestClients} from './util';
describe("verification request", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
it("should request and accept a verification", async function() {
const [alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
};
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.downloadKeys = () => {
return Promise.resolve();
};
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();
});
});
+276
View File
@@ -0,0 +1,276 @@
/*
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.
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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import sdk from '../../../..';
import {verificationMethods} from '../../../../lib/crypto';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import SAS from '../../../../lib/crypto/verification/SAS';
const Olm = global.Olm;
const MatrixEvent = sdk.MatrixEvent;
import {makeTestClients} from './util';
describe("SAS verification", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
it("should error on an unexpected event", async function() {
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
content: {},
}));
const spy = expect.createSpy();
await sas.verify()
.catch(spy);
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
});
describe("verification", function() {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
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);
});
});
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);
});
});
it("should send a cancellation message on error", 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.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.downloadKeys = () => {
return Promise.resolve();
};
const bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
e.mismatch();
});
resolve(verifier);
});
});
const aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
);
const aliceSpy = expect.createSpy();
const bobSpy = expect.createSpy();
await Promise.all([
aliceVerifier.verify().catch(aliceSpy),
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
]);
expect(aliceSpy).toHaveBeenCalled();
expect(bobSpy).toHaveBeenCalled();
expect(alice.setDeviceVerified)
.toNotHaveBeenCalled();
expect(bob.setDeviceVerified)
.toNotHaveBeenCalled();
});
});
+63
View File
@@ -0,0 +1,63 @@
/*
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 TestClient from '../../../TestClient';
import sdk from '../../../..';
const MatrixEvent = sdk.MatrixEvent;
export async function makeTestClients(userInfos, options) {
const clients = [];
const clientMap = {};
const sendToDevice = function(type, map) {
// console.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: msg,
});
setTimeout(
() => clientMap[userId][deviceId]
.emit("toDeviceEvent", event),
0,
);
}
}
}
}
};
for (const userInfo of userInfos) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined,
options,
)).client;
if (!(userInfo.userId in clientMap)) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = client;
client.sendToDevice = sendToDevice;
clients.push(client);
}
await Promise.all(clients.map((client) => client.initCrypto()));
return clients;
}
+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);
});
});
+38 -26
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",
@@ -132,12 +133,16 @@ describe("MatrixClient", function() {
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
"getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
@@ -181,7 +186,7 @@ describe("MatrixClient", function() {
});
});
it("should not POST /filter if a matching filter already exists", function(done) {
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -190,15 +195,19 @@ describe("MatrixClient", function() {
const filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter);
client.startClient();
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
done();
}
const syncPromise = new Promise((resolve, reject) => {
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
resolve();
} else if (state === "ERROR") {
reject(new Error("sync error"));
}
});
});
await client.startClient();
await syncPromise;
});
describe("getSyncState", function() {
@@ -206,15 +215,18 @@ describe("MatrixClient", function() {
expect(client.getSyncState()).toBe(null);
});
it("should return the same sync state as emitted sync events", function(done) {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
done();
}
it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise((resolve) => {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
resolve();
}
});
});
client.startClient();
await client.startClient();
await syncingPromise;
});
});
@@ -257,8 +269,8 @@ describe("MatrixClient", function() {
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
it("should return false if there is no request waiting", async function() {
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
@@ -342,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) {
@@ -379,7 +391,7 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> PREPARED after /sync if prev failed",
it("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
@@ -402,7 +414,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
+3
View File
@@ -24,6 +24,9 @@ describe('NotificationService', function() {
name: testDisplayName,
};
},
getJoinedMemberCount: function() {
return 0;
},
members: {},
},
};
+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);
+57 -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);
});
@@ -192,6 +192,15 @@ describe("RoomMember", function() {
});
});
describe("isOutOfBand", function() {
it("should be set by markOutOfBand", function() {
const member = new RoomMember();
expect(member.isOutOfBand()).toEqual(false);
member.markOutOfBand();
expect(member.isOutOfBand()).toEqual(true);
});
});
describe("setMembershipEvent", function() {
const joinEvent = utils.mkMembership({
event: true,
@@ -276,5 +285,52 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
it("should set 'name' to user_id if it is just whitespace", function() {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: " \u200b ",
});
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual(userA); // it should fallback because all whitespace
});
it("should disambiguate users on a fuzzy displayname match", function() {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: "Alíce\u200b", // note diacritic and zero width char
});
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).toNotEqual("Alíce"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).toNotEqual(-1);
});
});
});
+224 -18
View File
@@ -11,10 +11,13 @@ describe("RoomState", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bob:bar";
const userC = "@cleo:bar";
const userLazy = "@lazy:bar";
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
@@ -78,8 +81,8 @@ describe("RoomState", function() {
});
describe("getSentinelMember", function() {
it("should return null if there is no member", function() {
expect(state.getSentinelMember("@no-one:here")).toEqual(null);
it("should return a member with the user id as name", function() {
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here");
});
it("should return a member which doesn't change when the state is updated",
@@ -162,6 +165,7 @@ describe("RoomState", function() {
];
let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
expect(state.getMember(mem.userId)).toEqual(mem);
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet
emitCount += 1;
@@ -222,7 +226,6 @@ describe("RoomState", function() {
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
const userC = "@cleo:bar";
const memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true,
});
@@ -262,6 +265,114 @@ describe("RoomState", function() {
});
});
describe("setOutOfBandMembers", function() {
it("should add a new member", function() {
const oobMemberEvent = utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const member = state.getMember(userLazy);
expect(member.userId).toEqual(userLazy);
expect(member.isOutOfBand()).toEqual(true);
});
it("should have no effect when not in correct status", function() {
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
});
it("should emit newMember when adding a member", function() {
const userLazy = "@oob:hs";
const oobMemberEvent = utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.newMember', (_, __, member) => {
expect(member.userId).toEqual(userLazy);
eventReceived = true;
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
expect(eventReceived).toEqual(true);
});
it("should never overwrite existing members", function() {
const oobMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false);
});
it("should emit members when updating a member", function() {
const doesntExistYetUserId = "@doesntexistyet:hs";
const oobMemberEvent = utils.mkMembership({
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.members', (_, __, member) => {
expect(member.userId).toEqual(doesntExistYetUserId);
eventReceived = true;
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
expect(eventReceived).toEqual(true);
});
});
describe("clone", function() {
it("should contain same information as original", function() {
// include OOB members in copy
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
const copy = state.clone();
// check individual members
[userA, userB, userLazy].forEach((userId) => {
const member = state.getMember(userId);
const memberCopy = copy.getMember(userId);
expect(member.name).toEqual(memberCopy.name);
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
});
// check member keys
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
// check join count
expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount());
});
it("should mark old copy as not waiting for out of band anymore", function() {
state.markOutOfBandMembersStarted();
const copy = state.clone();
copy.setOutOfBandMembers([utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
})]);
// should have no effect as it should be marked in status finished just like copy
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
});
it("should return copy independent of original", function() {
const copy = state.clone();
copy.setStateEvents([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
expect(state.getJoinedMemberCount()).toEqual(2);
expect(copy.getJoinedMemberCount()).toEqual(3);
});
});
describe("setTypingEvent", function() {
it("should call setTypingEvent on each RoomMember", function() {
const typingEvent = utils.mkEvent({
@@ -284,13 +395,6 @@ describe("RoomState", function() {
});
describe("maySendStateEvent", function() {
it("should say non-joined members may not send state",
function() {
expect(state.maySendStateEvent(
'm.room.name', "@nobody:nowhere",
)).toEqual(false);
});
it("should say any member may send state with no power level event",
function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
@@ -366,15 +470,117 @@ describe("RoomState", function() {
});
});
describe("maySendEvent", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere",
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
describe("getJoinedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should update after adding joined member", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userA, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(1);
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userC, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(2);
});
});
describe("getInvitedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should update after adding invited member", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userA, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(1);
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userC, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(2);
});
});
describe("setJoinedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should, once used, override counting members from state", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userA, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(1);
state.setJoinedMemberCount(100);
expect(state.getJoinedMemberCount()).toEqual(100);
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userC, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
it("should, once used, override counting members from state, " +
"also after clone", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userA, room: roomId}),
]);
state.setJoinedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userC, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
});
describe("setInvitedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should, once used, override counting members from state", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userB, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(1);
state.setInvitedMemberCount(100);
expect(state.getInvitedMemberCount()).toEqual(100);
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userC, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
it("should, once used, override counting members from state, " +
"also after clone", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userB, room: roomId}),
]);
state.setInvitedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userC, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
});
describe("maySendEvent", function() {
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
+331 -138
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 =
@@ -67,13 +67,14 @@ describe("Room", function() {
describe("getMember", function() {
beforeEach(function() {
// clobber members property with test data
room.currentState.members = {
"@alice:bar": {
userId: userA,
roomId: roomId,
},
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": {
userId: userA,
roomId: roomId,
},
}[userId];
});
});
it("should return null if the member isn't in current state", function() {
@@ -103,7 +104,7 @@ describe("Room", function() {
user_ids: [userA],
},
});
room.addLiveEvents([typing]);
room.addEphemeralEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
});
@@ -383,22 +384,25 @@ describe("Room", function() {
});
const resetTimelineTests = function(timelineSupport) {
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "A message", event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" },
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Another New Name" },
}),
];
let events = null;
beforeEach(function() {
room = new Room(roomId, {timelineSupport: timelineSupport});
room = new Room(roomId, null, null, {timelineSupport: timelineSupport});
// set events each time to avoid resusing Event objects (which
// doesn't work because they get frozen)
events = [
utils.mkMessage({
room: roomId, user: userA, msg: "A message", event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" },
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Another New Name" },
}),
];
});
it("should copy state from previous timeline", function() {
@@ -465,7 +469,7 @@ describe("Room", function() {
describe("compareEventOrdering", function() {
beforeEach(function() {
room = new Room(roomId, {timelineSupport: true});
room = new Room(roomId, null, null, {timelineSupport: true});
});
const events = [
@@ -567,72 +571,75 @@ describe("Room", function() {
describe("hasMembershipState", function() {
it("should return true for a matching userId and membership",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true);
});
it("should return false if match membership but no match userId",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "join")).toBe(false);
});
it("should return false if match userId but no match membership",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false);
});
it("should return false if no match membership or userId",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false);
});
it("should return false if no members exist",
function() {
room.currentState.members = {};
expect(room.hasMembershipState("@foo:bar", "join")).toBe(false);
});
});
describe("recalculate", function() {
let stateLookup = {
// event.type + "$" event.state_key : MatrixEvent
};
const setJoinRule = function(rule) {
stateLookup["m.room.join_rules$"] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.join_rules", room: roomId, user: userA, content: {
join_rule: rule,
}, event: true,
});
})]);
};
const setAliases = function(aliases, stateKey) {
if (!stateKey) {
stateKey = "flibble";
}
stateLookup["m.room.aliases$" + stateKey] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
aliases: aliases,
}, event: true,
});
})]);
};
const setRoomName = function(name) {
stateLookup["m.room.name$"] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, content: {
name: name,
}, event: true,
});
})]);
};
const addMember = function(userId, state, opts) {
if (!state) {
@@ -644,56 +651,14 @@ describe("Room", function() {
opts.user = opts.user || userId;
opts.skey = userId;
opts.event = true;
stateLookup["m.room.member$" + userId] = utils.mkMembership(opts);
const event = utils.mkMembership(opts);
room.addLiveEvents([event]);
return event;
};
beforeEach(function() {
stateLookup = {};
room.currentState.getStateEvents.andCall(function(type, key) {
if (key === undefined) {
const prefix = type + "$";
const list = [];
for (const stateBlob in stateLookup) {
if (!stateLookup.hasOwnProperty(stateBlob)) {
continue;
}
if (stateBlob.indexOf(prefix) === 0) {
list.push(stateLookup[stateBlob]);
}
}
return list;
} else {
return stateLookup[type + "$" + key];
}
});
room.currentState.getMembers.andCall(function() {
const memberEvents = room.currentState.getStateEvents("m.room.member");
const members = [];
for (let i = 0; i < memberEvents.length; i++) {
members.push({
name: memberEvents[i].event.content &&
memberEvents[i].event.content.displayname ?
memberEvents[i].event.content.displayname :
memberEvents[i].getStateKey(),
userId: memberEvents[i].getStateKey(),
events: { member: memberEvents[i] },
});
}
return members;
});
room.currentState.getMember.andCall(function(userId) {
const memberEvent = room.currentState.getStateEvents(
"m.room.member", userId,
);
return {
name: memberEvent.event.content &&
memberEvent.event.content.displayname ?
memberEvent.event.content.displayname :
memberEvent.getStateKey(),
userId: memberEvent.getStateKey(),
events: { member: memberEvent },
};
});
// no mocking
room = new Room(roomId, null, userA);
});
describe("Room.recalculate => Stripped State Events", function() {
@@ -701,8 +666,8 @@ describe("Room", function() {
"room is an invite room", function() {
const roomName = "flibble";
addMember(userA, "invite");
stateLookup["m.room.member$" + userA].event.invite_room_state = [
const event = addMember(userA, "invite");
event.event.invite_room_state = [
{
type: "m.room.name",
state_key: "",
@@ -712,30 +677,108 @@ describe("Room", function() {
},
];
room.recalculate(userA);
expect(room.currentState.setStateEvents).toHaveBeenCalled();
// first call, first arg (which is an array), first element in array
const fakeEvent = room.currentState.setStateEvents.calls[0].
arguments[0][0];
expect(fakeEvent.getContent()).toEqual({
name: roomName,
});
room.recalculate();
expect(room.name).toEqual(roomName);
});
it("should not clobber state events if it isn't an invite room", function() {
addMember(userA, "join");
stateLookup["m.room.member$" + userA].event.invite_room_state = [
const event = addMember(userA, "join");
const roomName = "flibble";
setRoomName(roomName);
const roomNameToIgnore = "ignoreme";
event.event.invite_room_state = [
{
type: "m.room.name",
state_key: "",
content: {
name: "flibble",
name: roomNameToIgnore,
},
},
];
room.recalculate(userA);
expect(room.currentState.setStateEvents).toNotHaveBeenCalled();
room.recalculate();
expect(room.name).toEqual(roomName);
});
});
describe("Room.recalculate => Room Name using room summary", function() {
it("should use room heroes if available", function() {
addMember(userA, "invite");
addMember(userB);
addMember(userC);
addMember(userD);
room.setSummary({
"m.heroes": [userB, userC, userD],
});
room.recalculate();
expect(room.name).toEqual(`${userB} and 2 others`);
});
it("missing hero member state reverts to mxid", function() {
room.setSummary({
"m.heroes": [userB],
"m.joined_member_count": 2,
});
room.recalculate();
expect(room.name).toEqual(userB);
});
it("uses hero name from state", function() {
const name = "Mr B";
addMember(userA, "invite");
addMember(userB, "join", {name});
room.setSummary({
"m.heroes": [userB],
});
room.recalculate();
expect(room.name).toEqual(name);
});
it("uses counts from summary", function() {
const name = "Mr B";
addMember(userB, "join", {name});
room.setSummary({
"m.heroes": [userB],
"m.joined_member_count": 50,
"m.invited_member_count": 50,
});
room.recalculate();
expect(room.name).toEqual(`${name} and 98 others`);
});
it("relies on heroes in case of absent counts", function() {
const nameB = "Mr Bean";
const nameC = "Mel C";
addMember(userB, "join", {name: nameB});
addMember(userC, "join", {name: nameC});
room.setSummary({
"m.heroes": [userB, userC],
});
room.recalculate();
expect(room.name).toEqual(`${nameB} and ${nameC}`);
});
it("uses only heroes", function() {
const nameB = "Mr Bean";
addMember(userB, "join", {name: nameB});
addMember(userC, "join");
room.setSummary({
"m.heroes": [userB],
});
room.recalculate();
expect(room.name).toEqual(nameB);
});
it("reverts to empty room in case of self chat", function() {
room.setSummary({
"m.heroes": [],
"m.invited_member_count": 1,
});
room.recalculate();
expect(room.name).toEqual("Empty room");
});
});
@@ -748,7 +791,7 @@ describe("Room", function() {
addMember(userB);
addMember(userC);
addMember(userD);
room.recalculate(userA);
room.recalculate();
const name = room.name;
// we expect at least 1 member to be mentioned
const others = [userB, userC, userD];
@@ -769,7 +812,7 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
expect(name.indexOf(userC)).toNotEqual(-1, name);
@@ -782,7 +825,7 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
expect(name.indexOf(userC)).toNotEqual(-1, name);
@@ -794,7 +837,7 @@ describe("Room", function() {
setJoinRule("public");
addMember(userA);
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
@@ -805,7 +848,7 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA);
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
@@ -815,7 +858,7 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA, "invite", {user: userB});
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
@@ -825,7 +868,7 @@ describe("Room", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAliases([alias, "#another:one"]);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
@@ -835,7 +878,7 @@ describe("Room", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAliases([alias, "#another:one"]);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
@@ -845,7 +888,7 @@ describe("Room", function() {
const roomName = "A mighty name indeed";
setJoinRule("invite");
setRoomName(roomName);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(roomName);
});
@@ -855,25 +898,23 @@ describe("Room", function() {
const roomName = "A mighty name indeed";
setJoinRule("public");
setRoomName(roomName);
room.recalculate(userA);
const name = room.name;
expect(name).toEqual(roomName);
room.recalculate();
expect(room.name).toEqual(roomName);
});
it("should return 'Empty room' for private (invite join_rules) rooms if" +
" a room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("invite");
addMember(userA);
room.recalculate(userA);
const name = room.name;
expect(name).toEqual("Empty room");
room.recalculate();
expect(room.name).toEqual("Empty room");
});
it("should return 'Empty room' for public (public join_rules) rooms if a" +
" room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("public");
addMember(userA);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual("Empty room");
});
@@ -881,7 +922,7 @@ describe("Room", function() {
it("should return 'Empty room' if there is no name, " +
"alias or members in the room.",
function() {
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual("Empty room");
});
@@ -890,9 +931,9 @@ describe("Room", function() {
"available",
function() {
setJoinRule("invite");
addMember(userA, 'join', {name: "Alice"});
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
addMember(userB, 'join', {name: "Alice"});
addMember(userA, "invite", {user: userA});
room.recalculate();
const name = room.name;
expect(name).toEqual("Alice");
});
@@ -900,11 +941,11 @@ describe("Room", function() {
it("should return inviter mxid if display name not available",
function() {
setJoinRule("invite");
addMember(userA);
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
addMember(userB);
addMember(userA, "invite", {user: userA});
room.recalculate();
const name = room.name;
expect(name).toEqual(userA);
expect(name).toEqual(userB);
});
});
});
@@ -1151,7 +1192,7 @@ describe("Room", function() {
describe("addPendingEvent", function() {
it("should add pending events to the pendingEventList if " +
"pendingEventOrdering == 'detached'", function() {
const room = new Room(roomId, {
const room = new Room(roomId, null, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
@@ -1177,7 +1218,7 @@ describe("Room", function() {
it("should add pending events to the timeline if " +
"pendingEventOrdering == 'chronological'", function() {
room = new Room(roomId, {
room = new Room(roomId, null, userA, {
pendingEventOrdering: "chronological",
});
const eventA = utils.mkMessage({
@@ -1201,7 +1242,7 @@ describe("Room", function() {
describe("updatePendingEvent", function() {
it("should remove cancelled events from the pending list", function() {
const room = new Room(roomId, {
const room = new Room(roomId, null, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
@@ -1237,7 +1278,7 @@ describe("Room", function() {
it("should remove cancelled events from the timeline", function() {
const room = new Room(roomId);
const room = new Room(roomId, null, userA);
const eventA = utils.mkMessage({
room: roomId, user: userA, event: true,
});
@@ -1269,4 +1310,156 @@ describe("Room", function() {
expect(callCount).toEqual(1);
});
});
describe("loadMembersIfNeeded", function() {
function createClientMock(serverResponse, storageResponse = null) {
return {
getEventMapper: function() {
// events should already be MatrixEvents
return function(event) {return event;};
},
isCryptoEnabled() {
return true;
},
isRoomEncrypted: function() {
return false;
},
_http: {
serverResponse,
authedRequest: function() {
if (this.serverResponse instanceof Error) {
return Promise.reject(this.serverResponse);
} else {
return Promise.resolve({chunk: this.serverResponse});
}
},
},
store: {
storageResponse,
storedMembers: null,
getOutOfBandMembers: function() {
if (this.storageResponse instanceof Error) {
return Promise.reject(this.storageResponse);
} else {
return Promise.resolve(this.storageResponse);
}
},
setOutOfBandMembers: function(roomId, memberEvents) {
this.storedMembers = memberEvents;
return Promise.resolve();
},
getSyncToken: () => "sync_token",
},
};
}
const memberEvent = utils.mkMembership({
user: "@user_a:bar", mship: "join",
room: roomId, event: true, name: "User A",
});
it("should load members from server on first call", async function() {
const client = createClientMock([memberEvent]);
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
const storedMembers = client.store.storedMembers;
expect(storedMembers.length).toEqual(1);
expect(storedMembers[0].event_id).toEqual(memberEvent.getId());
});
it("should take members from storage if available", async function() {
const memberEvent2 = utils.mkMembership({
user: "@user_a:bar", mship: "join",
room: roomId, event: true, name: "Ms A",
});
const client = createClientMock([memberEvent2], [memberEvent]);
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
});
it("should allow retry on error", async function() {
const client = createClientMock(new Error("server says no"));
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
let hasThrown = false;
try {
await room.loadMembersIfNeeded();
} catch(err) {
hasThrown = true;
}
expect(hasThrown).toEqual(true);
client._http.serverResponse = [memberEvent];
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
});
});
describe("getMyMembership", function() {
it("should return synced membership if membership isn't available yet",
function() {
const room = new Room(roomId, null, userA);
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
});
it("should emit a Room.myMembership event on a change",
function() {
const room = new Room(roomId, null, userA);
const events = [];
room.on("Room.myMembership", (_room, membership, oldMembership) => {
events.push({membership, oldMembership});
});
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
expect(events[0]).toEqual({membership: "invite", oldMembership: null});
events.splice(0); //clear
room.updateMyMembership("invite");
expect(events.length).toEqual(0);
room.updateMyMembership("join");
expect(room.getMyMembership()).toEqual("join");
expect(events[0]).toEqual({membership: "join", oldMembership: "invite"});
});
});
describe("guessDMUserId", function() {
it("should return first hero id",
function() {
const room = new Room(roomId, null, userA);
room.setSummary({'m.heroes': [userB]});
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return first member that isn't self",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true,
})]);
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return self if only member present",
function() {
const room = new Room(roomId, null, userA);
expect(room.guessDMUserId()).toEqual(userA);
});
});
describe("maySendMessage", function() {
it("should return false if synced membership not join",
function() {
const room = new Room(roomId, null, userA);
room.updateMyMembership("invite");
expect(room.maySendMessage()).toEqual(false);
room.updateMyMembership("leave");
expect(room.maySendMessage()).toEqual(false);
room.updateMyMembership("join");
expect(room.maySendMessage()).toEqual(true);
});
});
});
+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() {
+58 -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,
});
@@ -52,6 +52,11 @@ describe("SyncAccumulator", function() {
member("bob", "join"),
],
},
summary: {
"m.heroes": undefined,
"m.joined_member_count": undefined,
"m.invited_member_count": undefined,
},
timeline: {
events: [msg("alice", "hi")],
prev_batch: "something",
@@ -318,6 +323,58 @@ describe("SyncAccumulator", function() {
},
});
});
describe("summary field", function() {
function createSyncResponseWithSummary(summary) {
return {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [],
},
summary: summary,
timeline: {
events: [],
prev_batch: "something",
},
},
},
},
};
}
it("should copy summary properties", function() {
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@alice:bar"],
"m.invited_member_count": 2,
}));
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
expect(summary["m.invited_member_count"]).toEqual(2);
expect(summary["m.heroes"]).toEqual(["@alice:bar"]);
});
it("should accumulate summary properties", function() {
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@alice:bar"],
"m.invited_member_count": 2,
}));
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@bob:bar"],
"m.joined_member_count": 5,
}));
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
expect(summary["m.invited_member_count"]).toEqual(2);
expect(summary["m.joined_member_count"]).toEqual(5);
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
});
});
});
function syncSkeleton(joinObj) {
+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);
}
}
+524
View File
@@ -0,0 +1,524 @@
/*
Copyright 2018 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.
*/
/** @module auto-discovery */
import Promise from 'bluebird';
import logger from './logger';
import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
/**
* Description for what an automatically discovered client configuration
* would look like. Although this is a class, it is recommended that it
* be treated as an interface definition rather than as a class.
*
* Additional properties than those defined here may be present, and
* should follow the Java package naming convention.
*/
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
// Dev note: this is basically a copy/paste of the .well-known response
// object as defined in the spec. It does have additional information,
// however. Overall, this exists to serve as a place for documentation
// and not functionality.
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
constructor() {
/**
* The homeserver configuration the client should use. This will
* always be present on the object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.homeserver"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
* if this is not AutoDiscovery.SUCCESS then the client should
* assume the other properties in the client config (such as
* the identity server configuration) are not valid.
*/
state: AutoDiscovery.PROMPT,
/**
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
* then this will contain a human-readable (English) message
* for what went wrong. If the state is none of those previously
* mentioned, this will be falsey.
*/
error: "Something went wrong",
/**
* The base URL clients should use to talk to the homeserver,
* particularly for the login process. May be falsey if the
* state is not AutoDiscovery.SUCCESS.
*/
base_url: "https://matrix.org",
};
/**
* The identity server configuration the client should use. This
* will always be present on teh object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.identity_server"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey.
*/
state: AutoDiscovery.PROMPT,
/**
* The base URL clients should use for interacting with the
* identity server. May be falsey if the state is not
* AutoDiscovery.SUCCESS.
*/
base_url: "https://vector.im",
};
}
}
/**
* Utilities for automatically discovery resources, such as homeservers
* for users to log in to.
*/
export class AutoDiscovery {
// Dev note: the constants defined here are related to but not
// exactly the same as those in the spec. This is to hopefully
// 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.
* @return {string}
* @constructor
*/
static get FAIL_ERROR() { return "FAIL_ERROR"; }
/**
* The auto discovery failed, however the client may still recover
* from the problem. The client is recommended to that the same
* action it would for PROMPT while also warning the user about
* what went wrong. The client may also treat this the same as
* a FAIL_ERROR state.
* @return {string}
* @constructor
*/
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
/**
* The auto discovery didn't fail but did not find anything of
* interest. The client is expected to prompt the user for more
* information, or fail if it prefers.
* @return {string}
* @constructor
*/
static get PROMPT() { return "PROMPT"; }
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
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
* and identity server URL the client would want. Additional details
* may also be discovered, and will be transparently included in the
* response object unaltered.
* @param {string} domain The homeserver domain to perform discovery
* on. For example, "matrix.org".
* @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
static async findClientConfig(domain) {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
// We use a .well-known lookup for all cases. According to the spec, we
// can do other discovery mechanisms if we want such as custom lookups
// however we won't bother with that here (mostly because the spec only
// supports .well-known right now).
//
// By using .well-known, we need to ensure we at least pull out a URL
// for the homeserver. We don't really need an identity server configuration
// but will return one anyways (with state PROMPT) to make development
// easier for clients. If we can't get a homeserver URL, all bets are
// off on the rest of the config and we'll assume it is invalid too.
// 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,
},
};
// Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const wellknown = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
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"] = {
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
};
} 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: Validate and parse the config
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
}
/**
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery.
* If valid, the URL will also be stripped of any trailing slashes.
* @param {string} url The potentially invalid URL to sanitize.
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
static _sanitizeWellKnownUrl(url) {
if (!url) return false;
try {
// We have to try and parse the URL using the NodeJS URL
// library if we're on NodeJS and use the browser's URL
// library when we're in a browser. To accomplish this, we
// try the NodeJS version first and fall back to the browser.
let parsed = null;
try {
if (NodeURL) parsed = new NodeURL(url);
else parsed = new URL(url);
} catch (e) {
parsed = new URL(url);
}
if (!parsed || !parsed.hostname) return false;
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
const port = parsed.port ? `:${parsed.port}` : "";
const path = parsed.pathname ? parsed.pathname : "";
let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
if (saferUrl.endsWith("/")) {
saferUrl = saferUrl.substring(0, saferUrl.length - 1);
}
return saferUrl;
} catch (e) {
logger.error(e);
return false;
}
}
/**
* Fetches a JSON object from a given URL, as expected by all .well-known
* related lookups. If the server gives a 404 then the `action` will be
* IGNORE. If the server returns something that isn't JSON, the `action`
* will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
*
* The returned object will be a result of the call in object form with
* the following properties:
* raw: The JSON object returned by the server.
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
* reason: Relatively human readable description of what went wrong.
* error: The actual Error, if one exists.
* @param {string} url The URL to fetch a JSON object from.
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
static async _fetchWellKnownObject(url) {
return new Promise(function(resolve, reject) {
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ 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 = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
resolve({raw: {}, action: action, reason: reason, error: err});
return;
}
try {
resolve({raw: JSON.parse(body), action: "SUCCESS"});
} catch (e) {
let reason = AutoDiscovery.ERROR_INVALID;
if (e.name === "SyntaxError") {
reason = AutoDiscovery.ERROR_INVALID_JSON;
}
resolve({
raw: {},
action: "FAIL_PROMPT",
reason: reason,
error: e,
});
}
},
);
});
}
}
+440 -92
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
@@ -89,9 +106,14 @@ MatrixBaseApis.prototype.getHomeserverUrl = function() {
/**
* Get the Identity Server URL of this client
* @param {boolean} stripProto whether or not to strip the protocol from the URL
* @return {string} Identity Server URL of this client
*/
MatrixBaseApis.prototype.getIdentityServerUrl = function() {
MatrixBaseApis.prototype.getIdentityServerUrl = function(stripProto=false) {
if (stripProto && (this.idBaseUrl.startsWith("http://") ||
this.idBaseUrl.startsWith("https://"))) {
return this.idBaseUrl.split("://")[1];
}
return this.idBaseUrl;
};
@@ -146,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
@@ -161,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 = {};
@@ -187,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
@@ -257,7 +287,18 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
utils.extend(login_data, data);
return this._http.authedRequest(
callback, "POST", "/login", undefined, login_data,
(error, response) => {
if (response && response.access_token && response.user_id) {
this._http.opts.accessToken = response.access_token;
this.credentials = {
userId: response.user_id,
};
}
if (callback) {
callback(error, response);
}
}, "POST", "/login", undefined, login_data,
);
};
@@ -293,9 +334,23 @@ MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
* @return {string} The HS URL to hit to begin the CAS login process.
*/
MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
return this._http.getUrl("/login/cas/redirect", {
return this.getSsoLoginUrl(redirectUrl, "cas");
};
/**
* @param {string} redirectUrl The URL to redirect to after the HS
* authenticates with the SSO.
* @param {string} loginType The type of SSO login we are doing (sso or cas).
* Defaults to 'sso'.
* @return {string} The HS URL to hit to begin the SSO login process.
*/
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
if (loginType === undefined) {
loginType = "sso";
}
return this._http.getUrl("/login/"+loginType+"/redirect", {
"redirectUrl": redirectUrl,
}, httpApi.PREFIX_UNSTABLE);
}, httpApi.PREFIX_R0);
};
/**
@@ -333,18 +388,28 @@ MatrixBaseApis.prototype.logout = function(callback) {
* it is up to the caller to either reset or destroy the MatrixClient after
* this method succeeds.
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @param {module:client.callback} callback Optional.
* @param {boolean} erase Optional. If set, send as `erase` attribute in the
* JSON request body, indicating whether the account should be erased. Defaults
* to false.
* @return {module:client.Promise} Resolves: On success, the empty object
*/
MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
let body = {};
if (auth) {
body = {
auth: auth,
};
MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
if (typeof(erase) === 'function') {
throw new Error(
'deactivateAccount no longer accepts a callback parameter',
);
}
return this._http.authedRequestWithPrefix(
callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE,
const body = {};
if (auth) {
body.auth = auth;
}
if (erase !== undefined) {
body.erase = erase;
}
return this._http.authedRequest(
undefined, "POST", '/account/deactivate', undefined, body,
);
};
@@ -389,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
@@ -401,6 +495,69 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) {
return this._http.authedRequest(callback, "GET", path);
};
/**
* Get an event in a room by its event id.
* @param {string} roomId
* @param {string} eventId
* @param {module:client.callback} callback Optional.
*
* @return {Promise} Resolves to an object containing the event.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.fetchRoomEvent = function(roomId, eventId, callback) {
const path = utils.encodeUri(
"/rooms/$roomId/event/$eventId", {
$roomId: roomId,
$eventId: eventId,
},
);
return this._http.authedRequest(callback, "GET", path);
};
/**
* @param {string} roomId
* @param {string} includeMembership the membership type to include in the response
* @param {string} excludeMembership the membership type to exclude from the response
* @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: dictionary of userid to profile information
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.members =
function(roomId, includeMembership, excludeMembership, atEventId, callback) {
const queryParams = {};
if (includeMembership) {
queryParams.membership = includeMembership;
}
if (excludeMembership) {
queryParams.not_membership = excludeMembership;
}
if (atEventId) {
queryParams.at = atEventId;
}
const queryString = utils.encodeParams(queryParams);
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{$roomId: roomId});
return this._http.authedRequest(callback, "GET", path);
};
/**
* Upgrades a room to a new protocol version
* @param {string} roomId
* @param {string} newVersion The target version to upgrade to
* @return {module:client.Promise} Resolves: Object with key 'replacement_room'
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) {
const path = utils.encodeUri("/rooms/$roomId/upgrade", {$roomId: roomId});
return this._http.authedRequest(
undefined, "POST", path, undefined, {new_version: newVersion},
);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group summary object
@@ -438,6 +595,27 @@ MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) {
);
};
/**
* @param {string} groupId
* @param {object} policy The join policy for the group. Must include at
* least a 'type' field which is 'open' if anyone can join the group
* the group without prior approval, or 'invite' if an invite is
* required to join.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupJoinPolicy = function(groupId, policy) {
const path = utils.encodeUri(
"/groups/$groupId/settings/m.join_policy",
{$groupId: groupId},
);
return this._http.authedRequest(
undefined, "PUT", path, undefined, {
'm.join_policy': policy,
},
);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group users list object
@@ -616,14 +794,28 @@ MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) {
/**
* @param {string} groupId
* @param {Object} opts Additional options to send alongside the acceptance.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId) {
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId, opts = null) {
const path = utils.encodeUri(
"/groups/$groupId/self/accept_invite",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, opts || {});
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.joinGroup = function(groupId) {
const path = utils.encodeUri(
"/groups/$groupId/self/join",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
@@ -748,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
@@ -813,6 +990,28 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
);
};
/**
* @return {module:client.Promise} Resolves: A list of the user's current rooms
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedRooms = function() {
const path = utils.encodeUri("/joined_rooms");
return this._http.authedRequest(undefined, "GET", path);
};
/**
* Retrieve membership info. for a room.
* @param {string} roomId ID of the room to get membership for
* @return {module:client.Promise} Resolves: A list of currently joined users
* and their profile data.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) {
const path = utils.encodeUri("/rooms/$roomId/joined_members", {
$roomId: roomId,
});
return this._http.authedRequest(undefined, "GET", path);
};
// Room Directory operations
// =========================
@@ -1020,6 +1219,10 @@ MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
@@ -1142,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);
};
/**
@@ -1177,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,
);
};
@@ -1197,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);
};
/**
@@ -1223,10 +1418,26 @@ 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);
};
/**
* Delete multiple device
*
* @param {string[]} devices IDs of the devices to delete
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
const body = {devices};
if (auth) {
body.auth = auth;
}
const path = "/delete_devices";
return this._http.authedRequest(undefined, "POST", path, undefined, body);
};
@@ -1268,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);
});
};
/**
@@ -1402,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);
};
/**
@@ -1439,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);
};
/**
@@ -1470,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);
};
/**
@@ -1492,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
@@ -1518,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;
}
};
/**
@@ -1547,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;
}
};
@@ -1613,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
@@ -1628,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') {
@@ -1646,7 +1945,7 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
* Get information on how a specific place on a third party protocol
* may be reached.
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in th
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
*/
@@ -1655,12 +1954,61 @@ 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);
};
/**
* Get information on how a specific user on a third party protocol
* may be reached.
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
const path = utils.encodeUri("/thirdparty/user/$protocol", {
$protocol: protocol,
});
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
*/
+1362 -226
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
/*
Copyright 2018 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.
*/
"use strict";
/** @module ContentHelpers */
module.exports = {
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlMessage: function(body, htmlBody) {
return {
msgtype: "m.text",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Notice event
* @param {string} body the plaintext body of the notice
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlNotice: function(body, htmlBody) {
return {
msgtype: "m.notice",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Emote event
* @param {string} body the plaintext body of the emote
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlEmote: function(body, htmlBody) {
return {
msgtype: "m.emote",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a Plaintext Message event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeTextMessage: function(body) {
return {
msgtype: "m.text",
body: body,
};
},
/**
* Generates the content for a Plaintext Notice event
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
makeNotice: function(body) {
return {
msgtype: "m.notice",
body: body,
};
},
/**
* Generates the content for a Plaintext Emote event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeEmoteMessage: function(body) {
return {
msgtype: "m.emote",
body: body,
};
},
};
+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 +
+310 -80
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations 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.
@@ -23,8 +24,10 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../logger';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/* State transition diagram for DeviceList._deviceTrackingStatus
@@ -58,31 +61,185 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) {
this._sessionStore = sessionStore;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, sessionStore, olmDevice,
);
constructor(baseApis, cryptoStore, olmDevice) {
this._cryptoStore = cryptoStore;
// userId -> {
// deviceId -> {
// [device info]
// }
// }
this._devices = {};
// map of identity keys to the user who owns it
this._userByIdentityKey = {};
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = sessionStore.getEndToEndDeviceTrackingStatus() || {};
this._deviceTrackingStatus = {}; // loaded from storage in load()
// The 'next_batch' sync token at the point the data was writen,
// ie. a token representing the point immediately after the
// moment represented by the snapshot in the db.
this._syncToken = null;
this._serialiser = new DeviceListUpdateSerialiser(
baseApis, olmDevice, this,
);
// userId -> promise
this._keyDownloadsInProgressByUser = {};
// Set whenever changes are made other than setting the sync token
this._dirty = false;
// Promise resolved when device data is saved
this._savePromise = null;
// Function that resolves the save promise
this._resolveSavePromise = null;
// The time the save is scheduled for
this._savePromiseTime = null;
// The timer used to delay the save
this._saveTimer = null;
}
/**
* Load the device tracking state from storage
*/
async load() {
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
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];
for (const device of Object.keys(userDevices)) {
const idKey = userDevices[device].keys['curve25519:'+device];
if (idKey !== undefined) {
this._userByIdentityKey[idKey] = user;
}
}
}
});
},
);
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) {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
}
// userId -> promise
this._keyDownloadsInProgressByUser = {};
this.lastKnownSyncToken = null;
stop() {
if (this._saveTimer !== null) {
clearTimeout(this._saveTimer);
}
}
/**
* Download the keys for a list of users and stores the keys in the session
* store.
* Save the device tracking state to storage, if any changes are
* pending other than updating the sync token
*
* The actual save will be delayed by a short amount of time to
* aggregate multiple writes to the database.
*
* @param {integer} delay Time in ms before which the save actually happens.
* By default, the save is delayed for a short period in order to batch
* multiple writes, but this behaviour can be disabled by passing 0.
*
* @return {Promise<bool>} true if the data was saved, false if
* it was not (eg. because no changes were pending). The promise
* will only resolve once the data is saved, so may take some time
* to resolve.
*/
async saveIfDirty(delay) {
if (!this._dirty) return Promise.resolve(false);
// Delay saves for a bit so we can aggregate multiple saves that happen
// in quick succession (eg. when a whole room's devices are marked as known)
if (delay === undefined) delay = 500;
const targetTime = Date.now + delay;
if (this._savePromiseTime && targetTime < this._savePromiseTime) {
// There's a save scheduled but for after we would like: cancel
// it & schedule one for the time we want
clearTimeout(this._saveTimer);
this._saveTimer = null;
this._savePromiseTime = null;
// (but keep the save promise since whatever called save before
// will still want to know when the save is done)
}
let savePromise = this._savePromise;
if (savePromise === null) {
savePromise = new Promise((resolve, reject) => {
this._resolveSavePromise = resolve;
});
this._savePromise = savePromise;
}
if (this._saveTimer === null) {
const resolveSavePromise = this._resolveSavePromise;
this._savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => {
logger.log('Saving device tracking data at token ' + this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewise for the dirty flag.
this._savePromiseTime = null;
this._saveTimer = null;
this._savePromise = null;
this._resolveSavePromise = null;
this._dirty = false;
this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
},
).then(() => {
resolveSavePromise();
});
}, delay);
}
return savePromise;
}
/**
* Gets the sync token last set with setSyncToken
*
* @return {string} The sync token
*/
getSyncToken() {
return this._syncToken;
}
/**
* Sets the sync token that the app will pass as the 'since' to the /sync
* endpoint next time it syncs.
* The sync token must always be set after any changes made as a result of
* data in that sync since setting the sync token to a newer one will mean
* those changed will not be synced from the server if a new client starts
* up with that data.
*
* @param {string} st The sync token
*/
setSyncToken(st) {
this._syncToken = st;
}
/**
* Ensures up to date keys for a list of users are stored in the session store,
* downloading and storing them if they're not (or if forceDownload is
* true).
* @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached.
*
@@ -98,7 +255,7 @@ export default class DeviceList {
if (this._keyDownloadsInProgressByUser[u]) {
// already a key download in progress/queued for this user; its results
// will be good enough for us.
console.log(
logger.log(
`downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`,
);
@@ -109,13 +266,13 @@ export default class DeviceList {
});
if (usersToDownload.length != 0) {
console.log("downloadKeys: downloading for", usersToDownload);
logger.log("downloadKeys: downloading for", usersToDownload);
const downloadPromise = this._doKeyDownload(usersToDownload);
promises.push(downloadPromise);
}
if (promises.length === 0) {
console.log("downloadKeys: already have all necessary keys");
logger.log("downloadKeys: already have all necessary keys");
}
return Promise.all(promises).then(() => {
@@ -152,7 +309,7 @@ export default class DeviceList {
* managed to get a list of devices for this user yet.
*/
getStoredDevicesForUser(userId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
const devs = this._devices[userId];
if (!devs) {
return null;
}
@@ -165,6 +322,18 @@ export default class DeviceList {
return res;
}
/**
* Get the stored device data for a user, in raw object form
*
* @param {string} userId the user to get data for
*
* @return {Object} deviceId->{object} devices, or undefined if
* there is no data for this user.
*/
getRawStoredDevicesForUser(userId) {
return this._devices[userId];
}
/**
* Get the stored keys for a single device
*
@@ -175,7 +344,7 @@ export default class DeviceList {
* if we don't know about this device
*/
getStoredDevice(userId, deviceId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
const devs = this._devices[userId];
if (!devs || !devs[deviceId]) {
return undefined;
}
@@ -185,13 +354,17 @@ export default class DeviceList {
/**
* Find a device by curve25519 identity key
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(userId, algorithm, senderKey) {
getDeviceByIdentityKey(algorithm, senderKey) {
const userId = this._userByIdentityKey[senderKey];
if (!userId) {
return null;
}
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
@@ -200,7 +373,7 @@ export default class DeviceList {
return null;
}
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
const devices = this._devices[userId];
if (!devices) {
return null;
}
@@ -229,6 +402,33 @@ export default class DeviceList {
return null;
}
/**
* Replaces the list of devices for a user with the given device list
*
* @param {string} u The user ID
* @param {Object} devs New device info for user
*/
storeDevicesForUser(u, devs) {
// remove previous devices from _userByIdentityKey
if (this._devices[u] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[u])) {
const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey];
}
}
this._devices[u] = devs;
// add new ones
for (const [deviceId, dev] of Object.entries(devs)) {
const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = u;
}
this._dirty = true;
}
/**
* flag the given user for device-list tracking, if they are not already.
*
@@ -250,12 +450,12 @@ export default class DeviceList {
throw new Error('userId must be a string; was '+userId);
}
if (!this._deviceTrackingStatus[userId]) {
console.log('Now tracking device list for ' + userId);
logger.log('Now tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
@@ -269,14 +469,27 @@ export default class DeviceList {
*/
stopTrackingDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log('No longer tracking device list for ' + userId);
logger.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Set all users we're currently tracking to untracked
*
* This will flag each user whose devices we are tracking as in need of an
* update.
*/
stopTrackingAllDeviceLists() {
for (const userId of Object.keys(this._deviceTrackingStatus)) {
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
}
this._dirty = true;
}
/**
* Mark the cached device list for the given user outdated.
@@ -291,23 +504,12 @@ export default class DeviceList {
*/
invalidateUserDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log("Marking device list outdated for", userId);
logger.log("Marking device list outdated for", userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Mark all tracked device lists as outdated.
*
* This will flag each user whose devices we are tracking as in need of an
* update.
*/
invalidateAllDeviceLists() {
for (const userId of Object.keys(this._deviceTrackingStatus)) {
this.invalidateUserDeviceList(userId);
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
}
@@ -318,6 +520,8 @@ export default class DeviceList {
* is no need to wait for this (it's mostly for the unit tests).
*/
refreshOutdatedDeviceLists() {
this.saveIfDirty();
const usersToDownload = [];
for (const userId of Object.keys(this._deviceTrackingStatus)) {
const stat = this._deviceTrackingStatus[userId];
@@ -326,13 +530,36 @@ export default class DeviceList {
}
}
// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
this._persistDeviceTrackingStatus();
return this._doKeyDownload(usersToDownload);
}
/**
* Set the stored device data for a user, in raw object form
* Used only by internal class DeviceListUpdateSerialiser
*
* @param {string} userId the user to get data for
*
* @param {Object} devices deviceId->{object} the new devices
*/
_setRawStoredDevicesForUser(userId, devices) {
// remove old devices from _userByIdentityKey
if (this._devices[userId] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[userId])) {
const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey];
}
}
this._devices[userId] = devices;
// add new devices into _userByIdentityKey
for (const [deviceId, dev] of Object.entries(devices)) {
const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = userId;
}
}
/**
* Fire off download update requests for the given users, and update the
@@ -352,11 +579,11 @@ export default class DeviceList {
}
const prom = this._serialiser.updateDevicesForUsers(
users, this.lastKnownSyncToken,
users, this._syncToken,
).then(() => {
finished(true);
}, (e) => {
console.error(
logger.error(
'Error downloading keys for ' + users + ":", e,
);
finished(false);
@@ -373,11 +600,13 @@ export default class DeviceList {
const finished = (success) => {
users.forEach((u) => {
this._dirty = true;
// we may have queued up another download request for this user
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) {
console.log('Another update in the queue for', u,
logger.log('Another update in the queue for', u,
'- not marking up-to-date');
return;
}
@@ -388,21 +617,17 @@ export default class DeviceList {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
console.log("Device list for", u, "now up to date");
logger.log("Device list for", u, "now up to date");
} else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
}
});
this._persistDeviceTrackingStatus();
this.saveIfDirty();
};
return prom;
}
_persistDeviceTrackingStatus() {
this._sessionStore.storeEndToEndDeviceTrackingStatus(this._deviceTrackingStatus);
}
}
/**
@@ -415,10 +640,15 @@ export default class DeviceList {
* time (and queuing other requests up).
*/
class DeviceListUpdateSerialiser {
constructor(baseApis, sessionStore, olmDevice) {
/*
* @param {object} baseApis Base API object
* @param {object} olmDevice The Olm Device
* @param {object} deviceList The device list object
*/
constructor(baseApis, olmDevice, deviceList) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
this._deviceList = deviceList; // the device list to be updated
this._downloadInProgress = false;
@@ -431,9 +661,7 @@ class DeviceListUpdateSerialiser {
// non-null indicates that we have users queued for download.
this._queuedQueryDeferred = null;
// sync token to be used for the next query: essentially the
// most recent one we know about
this._nextSyncToken = null;
this._syncToken = null; // The sync token we send with the requests
}
/**
@@ -452,15 +680,19 @@ class DeviceListUpdateSerialiser {
users.forEach((u) => {
this._keyDownloadsQueuedByUser[u] = true;
});
this._nextSyncToken = syncToken;
if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = Promise.defer();
}
// We always take the new sync token and just use the latest one we've
// been given, since it just needs to be at least as recent as the
// sync response the device invalidation message arrived in
this._syncToken = syncToken;
if (this._downloadInProgress) {
// just queue up these users
console.log('Queued key download for', users);
logger.log('Queued key download for', users);
return this._queuedQueryDeferred.promise;
}
@@ -480,12 +712,12 @@ class DeviceListUpdateSerialiser {
const deferred = this._queuedQueryDeferred;
this._queuedQueryDeferred = null;
console.log('Starting key download for', downloadUsers);
logger.log('Starting key download for', downloadUsers);
this._downloadInProgress = true;
const opts = {};
if (this._nextSyncToken) {
opts.token = this._nextSyncToken;
if (this._syncToken) {
opts.token = this._syncToken;
}
this._baseApis.downloadKeysForUsers(
@@ -507,7 +739,7 @@ class DeviceListUpdateSerialiser {
return prom;
}).done(() => {
console.log('Completed key download for ' + downloadUsers);
logger.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false;
deferred.resolve();
@@ -517,7 +749,7 @@ class DeviceListUpdateSerialiser {
this._doQueuedQueries();
}
}, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgress = false;
deferred.reject(e);
});
@@ -526,11 +758,11 @@ class DeviceListUpdateSerialiser {
}
async _processQueryResponseForUser(userId, response) {
console.log('got keys for ' + userId + ':', response);
logger.log('got keys for ' + userId + ':', response);
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
const devs = this._deviceList.getRawStoredDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
@@ -542,15 +774,13 @@ class DeviceListUpdateSerialiser {
this._olmDevice, userId, userStore, response || {},
);
// update the session store
// put the updates into thr object that will be returned as our results
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
});
this._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
this._deviceList._setRawStoredDevicesForUser(userId, storage);
}
}
@@ -566,7 +796,7 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
}
if (!(deviceId in userResult)) {
console.log("Device " + userId + ":" + deviceId +
logger.log("Device " + userId + ":" + deviceId +
" has been removed");
delete userStore[deviceId];
updated = true;
@@ -583,12 +813,12 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
// check that the user_id and device_id in the response object are
// correct
if (deviceResult.user_id !== userId) {
console.warn("Mismatched user_id " + deviceResult.user_id +
logger.warn("Mismatched user_id " + deviceResult.user_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
if (deviceResult.device_id !== deviceId) {
console.warn("Mismatched device_id " + deviceResult.device_id +
logger.warn("Mismatched device_id " + deviceResult.device_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
@@ -618,7 +848,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
const signKeyId = "ed25519:" + deviceId;
const signKey = deviceResult.keys[signKeyId];
if (!signKey) {
console.warn("Device " + userId + ":" + deviceId +
logger.warn("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
@@ -628,7 +858,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
try {
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.warn("Unable to verify signature on device " +
logger.warn("Unable to verify signature on device " +
userId + ":" + deviceId + ":" + e);
return false;
}
@@ -645,7 +875,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
console.warn("Ed25519 key for device " + userId + ":" +
logger.warn("Ed25519 key for device " + userId + ":" +
deviceId + " has changed");
return false;
}
+544 -405
View File
File diff suppressed because it is too large Load Diff
+170 -40
View File
@@ -16,6 +16,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../logger';
import utils from '../utils';
/**
@@ -35,13 +36,19 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500;
*
* The state machine looks like:
*
* |
* V (cancellation requested)
* UNSENT -----------------------------+
* | |
* | (send successful) |
* V |
* SENT |
* | (cancellation sent)
* | .-------------------------------------------------.
* | | |
* V V (cancellation requested) |
* UNSENT -----------------------------+ |
* | | |
* | | |
* | (send successful) | CANCELLATION_PENDING_AND_WILL_RESEND
* V | Λ
* SENT | |
* |-------------------------------- | --------------'
* | | (cancellation requested with intent
* | | to resend the original request)
* | |
* | (cancellation requested) |
* V |
@@ -62,6 +69,12 @@ const ROOM_KEY_REQUEST_STATES = {
/** reply received, cancellation not yet sent */
CANCELLATION_PENDING: 2,
/**
* Cancellation not yet sent and will transition to UNSENT instead of
* being deleted once the cancellation has been sent.
*/
CANCELLATION_PENDING_AND_WILL_RESEND: 3,
};
export default class OutgoingRoomKeyRequestManager {
@@ -96,7 +109,7 @@ export default class OutgoingRoomKeyRequestManager {
* Called when the client is stopped. Stops any running background processes.
*/
stop() {
console.log('stopping OutgoingRoomKeyRequestManager');
logger.log('stopping OutgoingRoomKeyRequestManager');
// stop the timer on the next run
this._clientRunning = false;
}
@@ -111,26 +124,111 @@ export default class OutgoingRoomKeyRequestManager {
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {Array<{userId: string, deviceId: string}>} recipients
* @param {boolean} resend whether to resend the key request if there is
* already one
*
* @returns {Promise} resolves when the request has been added to the
* pending list (or we have established that a similar request already
* exists)
*/
sendRoomKeyRequest(requestBody, recipients) {
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
}).then((req) => {
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
this._startTimer();
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
);
if (!req) {
await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
});
} else {
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
case ROOM_KEY_REQUEST_STATES.UNSENT:
// nothing to do here, since we're going to send a request anyways
return;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: {
// existing request is about to be cancelled. If we want to
// resend, then change the state so that it resends after
// cancelling. Otherwise, just cancel the cancellation.
const state = resend ?
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
ROOM_KEY_REQUEST_STATES.SENT;
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
},
);
break;
}
});
case ROOM_KEY_REQUEST_STATES.SENT: {
// a request has already been sent. If we don't want to
// resend, then do nothing. If we do want to, then cancel the
// existing request and send a new one.
if (resend) {
const state =
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND;
const updatedReq =
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
// need to use a new transaction ID so that
// the request gets sent
requestTxnId: this._baseApis.makeTxnId(),
},
);
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the request
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.sendRoomKeyRequest(
requestBody, recipients, resend,
);
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
try {
await this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
true,
);
} catch (e) {
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
}
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
}
break;
}
default:
throw new Error('unhandled state: ' + req.state);
}
}
// some of the branches require the timer to be started. Just start it
// all the time, because it doesn't hurt to start it.
this._startTimer();
}
/**
* Cancel room key requests, if any match the given details
* Cancel room key requests, if any match the given requestBody
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
*
@@ -147,6 +245,7 @@ export default class OutgoingRoomKeyRequestManager {
}
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
// nothing to do here
return;
@@ -158,7 +257,7 @@ export default class OutgoingRoomKeyRequestManager {
// may have seen it, so we still need to send a cancellation
// in that case :/
console.log(
logger.log(
'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody),
);
@@ -166,7 +265,7 @@ export default class OutgoingRoomKeyRequestManager {
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
);
case ROOM_KEY_REQUEST_STATES.SENT:
case ROOM_KEY_REQUEST_STATES.SENT: {
// send a cancellation.
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
@@ -181,7 +280,7 @@ export default class OutgoingRoomKeyRequestManager {
// the request cancelled. There is no point in
// sending another cancellation since the other tab
// will do it.
console.log(
logger.log(
'Tried to cancel room key request for ' +
stringifyRequestBody(requestBody) +
' but it was already cancelled in another tab',
@@ -201,20 +300,35 @@ export default class OutgoingRoomKeyRequestManager {
this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
).catch((e) => {
console.error(
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
this._startTimer();
}).done();
});
});
}
default:
throw new Error('unhandled state: ' + req.state);
}
});
}
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingSentRoomKeyRequest(userId, deviceId) {
return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT],
);
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
@@ -233,10 +347,10 @@ export default class OutgoingRoomKeyRequestManager {
}).catch((e) => {
// this should only happen if there is an indexeddb error,
// in which case we're a bit stuffed anyway.
console.warn(
logger.warn(
`error in OutgoingRoomKeyRequestManager: ${e}`,
);
}).done();
});
};
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
@@ -254,39 +368,46 @@ export default class OutgoingRoomKeyRequestManager {
return Promise.resolve();
}
console.log("Looking for queued outgoing room key requests");
logger.log("Looking for queued outgoing room key requests");
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
if (!req) {
console.log("No more outgoing room key requests");
logger.log("No more outgoing room key requests");
this._sendOutgoingRoomKeyRequestsTimer = null;
return;
}
let prom;
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
prom = this._sendOutgoingRoomKeyRequest(req);
} else { // must be a cancellation
prom = this._sendOutgoingRoomKeyRequestCancellation(req);
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.UNSENT:
prom = this._sendOutgoingRoomKeyRequest(req);
break;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
prom = this._sendOutgoingRoomKeyRequestCancellation(req);
break;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
prom = this._sendOutgoingRoomKeyRequestCancellation(req, true);
break;
}
return prom.then(() => {
// go around the loop again
return this._sendOutgoingRoomKeyRequests();
}).catch((e) => {
console.error("Error sending room key request; will retry later.", e);
logger.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
}).done();
});
});
}
// given a RoomKeyRequest, send it and update the request record
_sendOutgoingRoomKeyRequest(req) {
console.log(
logger.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` +
`(id ${req.requestId})`,
@@ -300,7 +421,7 @@ export default class OutgoingRoomKeyRequestManager {
};
return this._sendMessageToDevices(
requestMessage, req.recipients, req.requestId,
requestMessage, req.recipients, req.requestTxnId || req.requestId,
).then(() => {
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
@@ -309,9 +430,10 @@ export default class OutgoingRoomKeyRequestManager {
});
}
// given a RoomKeyRequest, cancel it and delete the request record
_sendOutgoingRoomKeyRequestCancellation(req) {
console.log(
// Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT.
_sendOutgoingRoomKeyRequestCancellation(req, andResend) {
logger.log(
`Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` +
`${stringifyRecipientList(req.recipients)} ` +
@@ -327,6 +449,14 @@ export default class OutgoingRoomKeyRequestManager {
return this._sendMessageToDevices(
requestMessage, req.recipients, req.cancellationTxnId,
).then(() => {
if (andResend) {
// We want to resend, so transition to UNSENT
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
{ state: ROOM_KEY_REQUEST_STATES.UNSENT },
);
}
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
);
+65
View File
@@ -0,0 +1,65 @@
/*
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.
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.
*/
/**
* @module crypto/RoomList
*
* Manages the list of encrypted rooms
*/
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/**
* @alias module:crypto/RoomList
*/
export default class RoomList {
constructor(cryptoStore) {
this._cryptoStore = cryptoStore;
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
this._roomEncryption = {};
}
async init() {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.getEndToEndRooms(txn, (result) => {
this._roomEncryption = result;
});
},
);
}
getRoomEncryption(roomId) {
return this._roomEncryption[roomId] || null;
}
isRoomEncrypted(roomId) {
return Boolean(this.getRoomEncryption(roomId));
}
async setRoomEncryption(roomId, roomInfo) {
// important that this happens before calling into the store
// as it prevents the Crypto::setRoomEncryption from calling
// this twice for consecutive m.room.encryption events
this._roomEncryption[roomId] = roomInfo;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
},
);
}
}
+18 -22
View File
@@ -176,34 +176,30 @@ export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
* @extends Error
*/
class DecryptionError extends Error {
constructor(msg, details) {
constructor(code, msg, details) {
super(msg);
this.code = code;
this.name = 'DecryptionError';
this.details = details;
}
/**
* override the string used when logging
*
* @returns {String}
*/
toString() {
let result = this.name + '[msg: ' + this.message;
if (this.details) {
result += ', ' +
Object.keys(this.details).map(
(k) => k + ': ' + this.details[k],
).join(', ');
}
result += ']';
return result;
this.detailedString = _detailedStringForDecryptionError(this, details);
}
}
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
function _detailedStringForDecryptionError(err, details) {
let result = err.name + '[msg: ' + err.message;
if (details) {
result += ', ' +
Object.keys(details).map(
(k) => k + ': ' + details[k],
).join(', ');
}
result += ']';
return result;
}
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
+253 -78
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 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.
@@ -22,6 +23,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../../src/logger';
const utils = require("../../utils");
const olmlib = require("../olmlib");
@@ -64,7 +66,7 @@ OutboundSessionInfo.prototype.needsRotation = function(
if (this.useCount >= rotationPeriodMsgs ||
sessionLifetime >= rotationPeriodMs
) {
console.log(
logger.log(
"Rotating megolm session after " + this.useCount +
" messages, " + sessionLifetime + "ms",
);
@@ -102,7 +104,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
}
if (!devicesInRoom.hasOwnProperty(userId)) {
console.log("Starting new session because we shared with " + userId);
logger.log("Starting new session because we shared with " + userId);
return true;
}
@@ -112,7 +114,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
}
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
console.log(
logger.log(
"Starting new session because we shared with " +
userId + ":" + deviceId,
);
@@ -142,6 +144,11 @@ function MegolmEncryption(params) {
// room).
this._setupPromise = Promise.resolve();
// Map of outbound sessions by sessions ID. Used if we need a particular
// session (the session we're currently using to send is always obtained
// using _setupPromise).
this._outboundSessions = {};
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
@@ -181,7 +188,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
console.log("Starting new megolm session because we need to rotate.");
logger.log("Starting new megolm session because we need to rotate.");
session = null;
}
@@ -191,8 +198,9 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
}
if (!session) {
console.log(`Starting new megolm session for room ${self._roomId}`);
logger.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
self._outboundSessions[session.sessionId] = session;
}
// now check if we need to share with any devices
@@ -262,6 +270,18 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
);
if (this._crypto.backupInfo) {
// don't wait for it to complete
this._crypto.backupGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [],
sessionId, key.key,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
});
}
return new OutboundSessionInfo(sessionId);
};
@@ -318,7 +338,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
continue;
}
console.log(
logger.log(
"share keys with device " + userId + ":" + deviceId,
);
@@ -407,8 +427,98 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
};
/**
* @private
* Re-shares a megolm session key with devices if the key has already been
* sent to them.
*
* @param {string} senderKey The key of the originating device for the session
* @param {string} sessionId ID of the outbound session to share
* @param {string} userId ID of the user who owns the target device
* @param {module:crypto/deviceinfo} device The target device
*/
MegolmEncryption.prototype.reshareKeyWithDevice = async function(
senderKey, sessionId, userId, device,
) {
const obSessionInfo = this._outboundSessions[sessionId];
if (!obSessionInfo) {
logger.debug("Session ID " + sessionId + " not found: not re-sharing keys");
return;
}
// The chain index of the key we previously sent this device
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
logger.debug("Session ID " + sessionId + " never shared with user " + userId);
return;
}
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sentChainIndex === undefined) {
logger.debug(
"Session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId,
);
return;
}
// get the key from the inbound session: the outbound one will already
// have been ratcheted to the next chain index.
const key = await this._olmDevice.getInboundGroupSessionKey(
this._roomId, senderKey, sessionId, sentChainIndex,
);
if (!key) {
logger.warn(
"No outbound session key found for " + sessionId + ": not re-sharing keys",
);
return;
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: {
[device.deviceId]: device,
},
},
);
const payload = {
type: "m.forwarded_room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: sessionId,
session_key: key.key,
chain_index: key.chain_index,
sender_key: senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
},
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
device,
payload,
),
await this._baseApis.sendToDevice("m.room.encrypted", {
[userId]: {
[device.deviceId]: encryptedContent,
},
});
logger.debug(
`Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`,
);
};
/**
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
@@ -440,10 +550,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
await this._encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
console.log(`Completed megolm keyshare in ${this._roomId} `
logger.log(`Completed megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
console.log(`megolm keyshare in ${this._roomId} `
logger.log(`megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
throw e;
@@ -462,7 +572,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
const self = this;
console.log(`Starting to encrypt event for ${this._roomId}`);
logger.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
// check if any of these devices are not yet known to the user.
@@ -488,6 +598,8 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
@@ -496,6 +608,16 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
});
};
/**
* Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent.
*
* This should not normally be necessary.
*/
MegolmEncryption.prototype.forceDiscardSession = function() {
this._setupPromise = this._setupPromise.then(() => null);
};
/**
* Checks the devices we're about to send to and see if any are entirely
* unknown to the user. If so, warn the user, and mark them as known to
@@ -535,46 +657,46 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
*/
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
const roomMembers = utils.map(room.getJoinedMembers(), function(u) {
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers();
const roomMembers = utils.map(members, function(u) {
return u.userId;
});
// The global value is treated as a default for when rooms don't specify a value.
let isBlacklisting = this._crypto.getGlobalBlacklistUnverifiedDevices();
if (typeof room.getBlacklistUnverifiedDevices() === 'boolean') {
isBlacklisting = room.getBlacklistUnverifiedDevices();
}
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
//
// XXX: what if the cache is stale, and the user left the room we had in
// common and then added new devices before joining this one? --Matthew
//
// yup, see https://github.com/vector-im/riot-web/issues/2305 --richvdh
return this._crypto.downloadKeys(roomMembers, false).then((devices) => {
// remove any blocked devices
for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) {
// device_lists in their /sync response. This cache should then be maintained
// using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false);
// remove any blocked devices
for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) {
continue;
}
const userDevices = devices[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
const userDevices = devices[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() &&
(room.getBlacklistUnverifiedDevices() ||
this._crypto.getGlobalBlacklistUnverifiedDevices()))
) {
delete userDevices[deviceId];
}
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
) {
delete userDevices[deviceId];
}
}
}
return devices;
});
return devices;
};
/**
@@ -614,7 +736,10 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
throw new base.DecryptionError(
"MEGOLM_MISSING_FIELDS",
"Missing fields in input",
);
}
// we add the event to the pending list *before* we start decryption.
@@ -631,11 +756,17 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
event.getId(), event.getTs(),
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._requestKeysForEvent(event);
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
}
throw new base.DecryptionError(
e.toString(), {
errorCode,
e ? e.toString() : "Unknown Error: Error is undefined", {
session: content.sender_key + '|' + content.session_id,
},
);
@@ -651,6 +782,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// scheduled, so we needn't send out the request here.)
this._requestKeysForEvent(event);
throw new base.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
"The sender's device has not sent us the keys for this message.",
{
session: content.sender_key + '|' + content.session_id,
@@ -669,6 +801,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"MEGOLM_BAD_ROOM",
"Message intended for room " + payload.room_id,
);
}
@@ -682,19 +815,9 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
};
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
const sender = event.getSender();
const wireContent = event.getWireContent();
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const recipients = [{
userId: this._userId, deviceId: '*',
}];
if (sender != this._userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
const recipients = event.getKeyRequestRecipients(this._userId);
this._crypto.requestRoomKey({
room_id: event.getRoomId(),
@@ -758,12 +881,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
!sessionId ||
!content.session_key
) {
console.error("key event is missing fields");
logger.error("key event is missing fields");
return;
}
if (!senderKey) {
console.error("key event has no sender key (not encrypted?)");
logger.error("key event has no sender key (not encrypted?)");
return;
}
@@ -780,13 +903,13 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
senderKey = content.sender_key;
if (!senderKey) {
console.error("forwarded_room_key event is missing sender_key field");
logger.error("forwarded_room_key event is missing sender_key field");
return;
}
const ed25519Key = content.sender_claimed_ed25519_key;
if (!ed25519Key) {
console.error(
logger.error(
`forwarded_room_key_event is missing sender_claimed_ed25519_key field`,
);
return;
@@ -799,24 +922,44 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
this._olmDevice.addInboundGroupSession(
logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat,
).then(() => {
// cancel any outstanding room key requests for this session
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId);
this._retryDecryption(senderKey, sessionId)
.then((success) => {
// cancel any outstanding room key requests for this session.
// Only do this if we managed to decrypt every message in the
// session, because if we didn't, we leave the other key
// requests in the hopes that someone sends us a key that
// includes an earlier index.
if (success) {
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
}
});
}).then(() => {
if (this._crypto.backupInfo) {
// don't wait for the keys to be backed up for the server
this._crypto.backupGroupSession(
content.room_id, senderKey, forwardingKeyChain,
content.session_id, content.session_key, keysClaimed,
exportFormat,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
});
}
}).catch((e) => {
console.error(`Error handling m.room_key_event: ${e}`);
logger.error(`Error handling m.room_key_event: ${e}`);
});
};
@@ -858,7 +1001,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
return null;
}
console.log(
logger.log(
"sharing keys for session " + body.sender_key + "|"
+ body.session_id + " with device "
+ userId + ":" + deviceId,
@@ -923,10 +1066,34 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
MegolmDecryption.prototype.importRoomKey = function(session) {
this._olmDevice.importInboundGroupSession(session);
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
return this._olmDevice.addInboundGroupSession(
session.room_id,
session.sender_key,
session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
).then(() => {
if (this._crypto.backupInfo) {
// don't wait for it to complete
this._crypto.backupGroupSession(
session.room_id,
session.sender_key,
session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
});
}
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
});
};
/**
@@ -935,19 +1102,27 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
* @private
* @param {String} senderKey
* @param {String} sessionId
*
* @return {Boolean} whether all messages were successfully decrypted
*/
MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId;
const pending = this._pendingEvents[k];
if (!pending) {
return;
return true;
}
delete this._pendingEvents[k];
for (const ev of pending) {
ev.attemptDecryption(this._crypto);
}
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
}
}));
return !this._pendingEvents[k];
};
base.registerAlgorithm(
+56 -42
View File
@@ -22,6 +22,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
@@ -83,60 +84,62 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
const users = utils.map(room.getJoinedMembers(), function(u) {
const members = await room.getEncryptionTargetMembers();
const users = utils.map(members, function(u) {
return u.userId;
});
const self = this;
return this._ensureSession(users).then(function() {
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
await this._ensureSession(users);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
const promises = [];
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
const promises = [];
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
),
);
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
}
return Promise.all(promises).return(encryptedContent);
});
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
),
);
}
}
return await Promise.all(promises).return(encryptedContent);
};
/**
@@ -168,11 +171,17 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
throw new base.DecryptionError(
"OLM_MISSING_CIPHERTEXT",
"Missing ciphertext",
);
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
throw new base.DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
}
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
let payloadString;
@@ -181,6 +190,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
payloadString = await this._decryptMessage(deviceKey, message);
} catch (e) {
throw new base.DecryptionError(
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
err: e,
@@ -194,12 +204,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
throw new base.DecryptionError(
"OLM_BAD_RECIPIENT",
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
throw new base.DecryptionError(
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this._olmDevice.deviceEd25519Key,
@@ -213,6 +225,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
throw new base.DecryptionError(
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
},
@@ -222,6 +235,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"OLM_BAD_ROOM",
"Message intended for room " + payload.room_id, {
reported_room: event.room_id,
},
@@ -260,7 +274,7 @@ OlmDecryption.prototype._decryptMessage = async function(
const payload = await this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
console.log(
logger.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId,
);
@@ -315,7 +329,7 @@ OlmDecryption.prototype._decryptMessage = async function(
);
}
console.log(
logger.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey,
);
+81
View File
@@ -0,0 +1,81 @@
/*
Copyright 2018 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 { randomString } from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
export async function keyForExistingBackup(backupData, password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const authData = backupData.auth_data;
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error(
"Salt and/or iterations not found: " +
"this backup cannot be restored with a passphrase",
);
}
return await deriveKey(
password, backupData.auth_data.private_key_salt,
backupData.auth_data.private_key_iterations,
);
}
export async function keyForNewBackup(password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
async function deriveKey(password, salt, iterations) {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
// TODO: Implement this for node
throw new Error("Password-based backup is not avaiable on this platform");
}
const key = await subtleCrypto.importKey(
'raw',
new TextEncoder().encode(password),
{name: 'PBKDF2'},
false,
['deriveBits'],
);
const keybits = await subtleCrypto.deriveBits(
{
name: 'PBKDF2',
salt: new TextEncoder().encode(salt),
iterations: iterations,
hash: 'SHA-512',
},
key,
global.Olm.PRIVATE_KEY_LENGTH * 8,
);
return new Uint8Array(keybits);
}
+1215 -166
View File
File diff suppressed because it is too large Load Diff
+76 -22
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,6 +24,7 @@ limitations under the License.
import Promise from 'bluebird';
const anotherjson = require('another-json');
import logger from '../logger';
const utils = require("../utils");
/**
@@ -35,6 +37,11 @@ module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* matrix algorithm tag for megolm backups
*/
module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
/**
* Encrypt an event payload for an Olm device
@@ -65,7 +72,7 @@ module.exports.encryptMessageForDevice = async function(
return;
}
console.log(
logger.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId,
);
@@ -115,19 +122,23 @@ module.exports.encryptMessageForDevice = async function(
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
* map from userid to list of devices to ensure sessions for
*
* @param {bolean} force If true, establish a new session even if one already exists.
* Optional.
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = async function(
olmDevice, baseApis, devicesByUser,
olmDevice, baseApis, devicesByUser, force,
) {
const devicesWithoutSession = [
// [userId, deviceId], ...
];
const result = {};
const resolveSession = {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
@@ -139,8 +150,37 @@ module.exports.ensureOlmSessionsForDevices = async function(
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
const sessionId = await olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
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]);
}
result[userId][deviceId] = {
@@ -154,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 = [];
@@ -176,7 +219,8 @@ module.exports.ensureOlmSessionsForDevices = async function(
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
const key = deviceInfo.getIdentityKey();
if (result[userId][deviceId].sessionId && !force) {
// we already have a result for this device
continue;
}
@@ -190,10 +234,12 @@ module.exports.ensureOlmSessionsForDevices = async function(
}
if (!oneTimeKey) {
console.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;
}
@@ -201,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;
}),
);
}
@@ -219,7 +273,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
deviceInfo.getFingerprint(),
);
} catch (e) {
console.error(
logger.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e,
);
@@ -233,12 +287,12 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
logger.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
console.log("Started new sessionid " + sid +
logger.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2018 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 bs58 from 'bs58';
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key) {
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);
let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);
return base58key.match(/.{1,4}/g).join(" ");
}
export function decodeRecoveryKey(recoverykey) {
const result = bs58.decode(recoverykey.replace(/ /g, ''));
let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}
if (
result.length !==
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1
) {
throw new Error("Incorrect length");
}
return result.slice(
OLM_RECOVERY_KEY_PREFIX.length,
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
);
}
@@ -1,7 +1,26 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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';
import logger from '../../logger';
import utils from '../../utils';
export const VERSION = 1;
export const VERSION = 7;
/**
* Implementation of a CryptoStore which is backed by an existing
@@ -21,7 +40,7 @@ export class Backend {
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (ev) => {
console.log(`versionchange for indexeddb ${this._dbName}: closing`);
logger.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.close();
};
}
@@ -47,7 +66,7 @@ export class Backend {
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
if (existing) {
// this entry matches the request - return it.
console.log(
logger.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
@@ -58,13 +77,13 @@ export class Backend {
// we got to the end of the list without finding a match
// - add the new request.
console.log(
logger.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
txn.oncomplete = () => { deferred.resolve(request); };
const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request);
txn.onsuccess = () => { deferred.resolve(request); };
});
return deferred.promise;
@@ -187,6 +206,42 @@ export class Backend {
return promiseifyTxn(txn).then(() => result);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
let stateIndex = 0;
const results = [];
function onsuccess(ev) {
const cursor = ev.target.result;
if (cursor) {
const keyReq = cursor.value;
if (keyReq.recipients.includes({userId, deviceId})) {
results.push(keyReq);
}
cursor.continue();
} else {
// try the next state in the list
stateIndex++;
if (stateIndex >= wantedStates.length) {
// no matches
return;
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
const cursorReq = store.index("state").openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => results);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -209,7 +264,7 @@ export class Backend {
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
logger.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${data.state}`,
);
@@ -247,7 +302,7 @@ export class Backend {
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
logger.warn(
`Cannot delete room key request in state ${data.state} `
+ `(expected ${expectedState})`,
);
@@ -257,16 +312,343 @@ export class Backend {
};
return promiseifyTxn(txn);
}
// Olm Account
getAccount(txn, func) {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeAccount(txn, newData) {
const objectStore = txn.objectStore("account");
objectStore.put(newData, "-");
}
// Olm Sessions
countEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
func(countReq.result);
};
}
getEndToEndSessions(deviceKey, txn, func) {
const objectStore = txn.objectStore("sessions");
const idx = objectStore.index("deviceKey");
const getReq = idx.openCursor(deviceKey);
const results = {};
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
results[cursor.value.sessionId] = {
session: cursor.value.session,
lastReceivedMessageTs: cursor.value.lastReceivedMessageTs,
};
cursor.continue();
} else {
try {
func(results);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.get([deviceKey, sessionId]);
getReq.onsuccess = function() {
try {
if (getReq.result) {
func({
session: getReq.result.session,
lastReceivedMessageTs: getReq.result.lastReceivedMessageTs,
});
} else {
func(null);
}
} catch (e) {
abortWithException(txn, e);
}
};
}
getAllEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
func(cursor.value);
cursor.continue();
} else {
try {
func(null);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
const objectStore = txn.objectStore("sessions");
objectStore.put({
deviceKey,
sessionId,
session: sessionInfo.session,
lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs,
});
}
// Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
} else {
func(null);
}
} catch (e) {
abortWithException(txn, e);
}
};
}
getAllEndToEndInboundGroupSessions(txn, func) {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
try {
func({
senderKey: cursor.value.senderCurve25519Key,
sessionId: cursor.value.sessionId,
sessionData: cursor.value.session,
});
} catch (e) {
abortWithException(txn, e);
}
cursor.continue();
} else {
try {
func(null);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const objectStore = txn.objectStore("inbound_group_sessions");
const addReq = objectStore.add({
senderCurve25519Key, sessionId, session: sessionData,
});
addReq.onerror = (ev) => {
if (addReq.error.name === 'ConstraintError') {
// This stops the error from triggering the txn's onerror
ev.stopPropagation();
// ...and this stops it from aborting the transaction
ev.preventDefault();
logger.log(
"Ignoring duplicate inbound group session: " +
senderCurve25519Key + " / " + sessionId,
);
} else {
abortWithException(txn, new Error(
"Failed to add inbound group session: " + addReq.error,
));
}
};
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const objectStore = txn.objectStore("inbound_group_sessions");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
getEndToEndDeviceData(txn, func) {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeEndToEndDeviceData(deviceData, txn) {
const objectStore = txn.objectStore("device_data");
objectStore.put(deviceData, "-");
}
storeEndToEndRoom(roomId, roomInfo, txn) {
const objectStore = txn.objectStore("rooms");
objectStore.put(roomInfo, roomId);
}
getEndToEndRooms(txn, func) {
const rooms = {};
const objectStore = txn.objectStore("rooms");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
rooms[cursor.key] = cursor.value;
cursor.continue();
} else {
try {
func(rooms);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
// session backups
getSessionsNeedingBackup(limit) {
return new Promise((resolve, reject) => {
const sessions = [];
const txn = this._db.transaction(
["sessions_needing_backup", "inbound_group_sessions"],
"readonly",
);
txn.onerror = reject;
txn.oncomplete = function() {
resolve(sessions);
};
const objectStore = txn.objectStore("sessions_needing_backup");
const sessionStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
const sessionGetReq = sessionStore.get(cursor.key);
sessionGetReq.onsuccess = function() {
sessions.push({
senderKey: sessionGetReq.result.senderCurve25519Key,
sessionId: sessionGetReq.result.sessionId,
sessionData: sessionGetReq.result.session,
});
};
if (!limit || sessions.length < limit) {
cursor.continue();
}
}
};
});
}
countSessionsNeedingBackup(txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readonly");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return new Promise((resolve, reject) => {
const req = objectStore.count();
req.onerror = reject;
req.onsuccess = () => resolve(req.result);
});
}
unmarkSessionsNeedingBackup(sessions, txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.delete([session.senderKey, session.sessionId]);
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
markSessionsNeedingBackup(sessions, txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.put({
senderCurve25519Key: session.senderKey,
sessionId: session.sessionId,
});
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
const result = func(txn);
return promise.then(() => {
return result;
});
}
}
export function upgradeDatabase(db, oldVersion) {
console.log(
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
if (oldVersion < 2) {
db.createObjectStore("account");
}
if (oldVersion < 3) {
const sessionsStore = db.createObjectStore("sessions", {
keyPath: ["deviceKey", "sessionId"],
});
sessionsStore.createIndex("deviceKey", "deviceKey");
}
if (oldVersion < 4) {
db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 5) {
db.createObjectStore("device_data");
}
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
if (oldVersion < 7) {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
// Expand as needed.
}
@@ -283,9 +665,46 @@ function createDatabase(db) {
outgoingRoomKeyRequestsStore.createIndex("state", "state");
}
/*
* Aborts a transaction with a given exception
* The transaction promise will be rejected with this exception.
*/
function abortWithException(txn, e) {
// We cheekily stick our exception onto the transaction object here
// We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception.
txn._mx_abortexception = e;
try {
txn.abort();
} catch (e) {
// sometimes we won't be able to abort the transaction
// (ie. if it's aborted or completed)
}
}
function promiseifyTxn(txn) {
return new Promise((resolve, reject) => {
txn.oncomplete = resolve;
txn.onerror = reject;
txn.oncomplete = () => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
}
resolve();
};
txn.onerror = (event) => {
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);
}
};
});
}
+348 -13
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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,8 +17,12 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../../logger';
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.
@@ -44,9 +49,13 @@ 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
* an in-memory store.
* a local storage or in-memory store.
*
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
@@ -62,7 +71,7 @@ export default class IndexedDBCryptoStore {
return;
}
console.log(`connecting to indexeddb ${this._dbName}`);
logger.log(`connecting to indexeddb ${this._dbName}`);
const req = this._indexedDB.open(
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
@@ -75,27 +84,55 @@ export default class IndexedDBCryptoStore {
};
req.onblocked = () => {
console.log(
logger.log(
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
logger.log("Error connecting to indexeddb", ev);
reject(ev.target.error);
};
req.onsuccess = (r) => {
const db = r.target.result;
console.log(`connected to indexeddb ${this._dbName}`);
logger.log(`connected to indexeddb ${this._dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
}).catch((e) => {
console.warn(
`unable to connect to indexeddb ${this._dbName}` +
`: falling back to in-memory store: ${e}`,
}).then((backend) => {
// Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
// Try a dummy query which will fail if the browser doesn't support compund keys, so
// we can fall back to a different backend.
return backend.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
return backend;
},
);
return new MemoryCryptoStore();
}).catch((e) => {
if (e.name === 'VersionError') {
logger.warn("Crypto DB is too new for us to use!", e);
// don't fall back to a different store: the user has crypto data
// in this db so we should use it or nothing at all.
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
}
logger.warn(
`unable to connect to indexeddb ${this._dbName}` +
`: falling back to localStorage store: ${e}`,
);
try {
return new LocalStorageCryptoStore(global.localStorage);
} catch (e) {
logger.warn(
`unable to open localStorage: falling back to in-memory store: ${e}`,
);
return new MemoryCryptoStore();
}
});
return this._backendPromise;
@@ -113,28 +150,29 @@ export default class IndexedDBCryptoStore {
return;
}
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 IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
logger.log("Error deleting data from indexeddb", ev);
reject(ev.target.error);
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
logger.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
}).catch((e) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that people can
// still use the app.
console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
});
}
@@ -186,6 +224,24 @@ export default class IndexedDBCryptoStore {
});
}
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
});
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -220,4 +276,283 @@ export default class IndexedDBCryptoStore {
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
});
}
// Olm Account
/*
* Get the account pickle from the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
getAccount(txn, func) {
this._backendPromise.value().getAccount(txn, func);
}
/*
* Write the account pickle to the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} newData The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backendPromise.value().storeAccount(txn, newData);
}
// Olm sessions
/**
* Returns the number of end-to-end sessions in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(int)} func Called with the count of sessions
*/
countEndToEndSessions(txn, func) {
this._backendPromise.value().countEndToEndSessions(txn, func);
}
/**
* Retrieve a specific end-to-end session between the logged-in user
* and another device.
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID of the session to retrieve
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
* @param {string} deviceKey The public key of the other device.
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
}
/**
* Retrieve all end-to-end sessions
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called one for each session with
* an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backendPromise.value().getAllEndToEndSessions(txn, func);
}
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} sessionInfo Session information object
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backendPromise.value().storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
}
// Inbound group saessions
/**
* Retrieve the end-to-end inbound group session for a given
* server key and session ID
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backendPromise.value().getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
}
/**
* Fetches all inbound group sessions in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called once for each group session
* in the store with an object having keys {senderKey, sessionId,
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func);
}
/**
* Adds an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, the session will not be added.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.value().addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
/**
* Writes an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, it will be overwritten.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.value().storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
// End-to-end device tracking
/**
* Store the state of all tracked devices
* This contains devices for each user, a tracking state for each user
* and a sync token matching the point in time the snapshot represents.
* These all need to be written out in full each time such that the snapshot
* is always consistent, so they are stored in one object.
*
* @param {Object} deviceData
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
}
/**
* Get the state of all tracked devices
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backendPromise.value().getEndToEndDeviceData(txn, func);
}
// End to End Rooms
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
* Get an object of roomId->roomInfo for all e2e rooms in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backendPromise.value().getEndToEndRooms(txn, func);
}
// session backups
/**
* Get the inbound group sessions that need to be backed up.
* @param {integer} limit The maximum number of sessions to retrieve. 0
* for no limit.
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._connect().then((backend) => {
return backend.getSessionsNeedingBackup(limit);
});
}
/**
* Count the inbound group sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._connect().then((backend) => {
return backend.countSessionsNeedingBackup(txn);
});
}
/**
* Unmark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions, txn);
});
}
/**
* Mark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.markSessionsNeedingBackup(sessions, txn);
});
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
* only be called within a callback of either this function or
* one of the store functions operating on the same transaction.
*
* @param {string} mode 'readwrite' if you need to call setter
* functions with this transaction. Otherwise, 'readonly'.
* @param {string[]} stores List IndexedDBCryptoStore.STORE_*
* options representing all types of object that will be
* accessed or written to with this transaction.
* @param {function(*)} func Function called with the
* transaction object: an opaque object that should be passed
* to store functions.
* @return {Promise} Promise that resolves with the result of the `func`
* when the transaction is complete. If the backend is
* async (ie. the indexeddb backend) any of the callback
* functions throwing an exception will cause this promise to
* reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func) {
return this._connect().then((backend) => {
return backend.doTxn(mode, stores, func);
});
}
}
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
@@ -0,0 +1,306 @@
/*
Copyright 2017, 2018 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';
import logger from '../../logger';
import MemoryCryptoStore from './memory-crypto-store.js';
/**
* Internal module. Partial localStorage backed storage for e2e.
* This is not a full crypto store, just the in-memory store with
* some things backed by localStorage. It exists because indexedDB
* is broken in Firefox private mode or set to, "will not remember
* history".
*
* @module
*/
const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) {
return KEY_ROOMS_PREFIX + roomId;
}
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class LocalStorageCryptoStore extends MemoryCryptoStore {
constructor(webStore) {
super();
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) {
let count = 0;
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count;
}
func(count);
}
_getEndToEndSessions(deviceKey, txn, func) {
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const fixedSessions = {};
// fix up any old sessions to be objects rather than just the base64 pickle
for (const [sid, val] of Object.entries(sessions || {})) {
if (typeof val === 'string') {
fixedSessions[sid] = {
session: val,
};
} else {
fixedSessions[sid] = val;
}
}
return fixedSessions;
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
const sessions = this._getEndToEndSessions(deviceKey);
func(sessions[sessionId] || {});
}
getEndToEndSessions(deviceKey, txn, func) {
func(this._getEndToEndSessions(deviceKey) || {});
}
getAllEndToEndSessions(txn, func) {
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1];
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
func(sess);
}
}
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = sessionInfo;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
));
}
getAllEndToEndInboundGroupSessions(txn, func) {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
func({
senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43),
sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44),
sessionData: getJsonItem(this.store, key),
});
}
}
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const existing = getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
);
if (!existing) {
this.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
setJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
sessionData,
);
}
getEndToEndDeviceData(txn, func) {
func(getJsonItem(
this.store, KEY_DEVICE_DATA,
));
}
storeEndToEndDeviceData(deviceData, txn) {
setJsonItem(
this.store, KEY_DEVICE_DATA, deviceData,
);
}
storeEndToEndRoom(roomId, roomInfo, txn) {
setJsonItem(
this.store, keyEndToEndRoomsPrefix(roomId), roomInfo,
);
}
getEndToEndRooms(txn, func) {
const result = {};
const prefix = keyEndToEndRoomsPrefix('');
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(prefix)) {
const roomId = key.substr(prefix.length);
result[roomId] = getJsonItem(this.store, key);
}
}
func(result);
}
getSessionsNeedingBackup(limit) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = [];
for (const session in sessionsNeedingBackup) {
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
// see getAllEndToEndInboundGroupSessions for the magic number explanations
const senderKey = session.substr(0, 43);
const sessionId = session.substr(44);
this.getEndToEndInboundGroupSession(
senderKey, sessionId, null,
(sessionData) => {
sessions.push({
senderKey: senderKey,
sessionId: sessionId,
sessionData: sessionData,
});
},
);
if (limit && session.length >= limit) {
break;
}
}
}
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId];
}
setJsonItem(
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true;
}
setJsonItem(
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
return Promise.resolve();
}
/**
* Delete all data from this store.
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
this.store.removeItem(KEY_END_TO_END_ACCOUNT);
return Promise.resolve();
}
// Olm account
getAccount(txn, func) {
const account = getJsonItem(this.store, KEY_END_TO_END_ACCOUNT);
func(account);
}
storeAccount(txn, newData) {
setJsonItem(
this.store, KEY_END_TO_END_ACCOUNT, newData,
);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
}
function getJsonItem(store, key) {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
} catch (e) {
logger.log("Error: Failed to get key %s: %s", key, e.stack || e);
logger.log(e.stack);
}
return null;
}
function setJsonItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
+169 -4
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 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,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../../logger';
import utils from '../../utils';
/**
@@ -30,6 +32,18 @@ import utils from '../../utils';
export default class MemoryCryptoStore {
constructor() {
this._outgoingRoomKeyRequests = [];
this._account = null;
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {};
// Opaque device data object
this._deviceData = null;
// roomId -> Opaque roomInfo object
this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
}
/**
@@ -60,7 +74,7 @@ export default class MemoryCryptoStore {
if (existing) {
// this entry matches the request - return it.
console.log(
logger.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
@@ -70,7 +84,7 @@ export default class MemoryCryptoStore {
// we got to the end of the list without finding a match
// - add the new request.
console.log(
logger.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
@@ -133,6 +147,19 @@ export default class MemoryCryptoStore {
return Promise.resolve(null);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
const results = [];
for (const req of this._outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state && req.recipients.includes({userId, deviceId})) {
results.push(req);
}
}
}
return Promise.resolve(results);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -152,7 +179,7 @@ export default class MemoryCryptoStore {
}
if (req.state != expectedState) {
console.warn(
logger.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${req.state}`,
);
@@ -183,7 +210,7 @@ export default class MemoryCryptoStore {
}
if (req.state != expectedState) {
console.warn(
logger.warn(
`Cannot delete room key request in state ${req.state} `
+ `(expected ${expectedState})`,
);
@@ -196,4 +223,142 @@ export default class MemoryCryptoStore {
return Promise.resolve(null);
}
// Olm Account
getAccount(txn, func) {
func(this._account);
}
storeAccount(txn, newData) {
this._account = newData;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
return Object.keys(this._sessions).length;
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
const deviceSessions = this._sessions[deviceKey] || {};
func(deviceSessions[sessionId] || null);
}
getEndToEndSessions(deviceKey, txn, func) {
func(this._sessions[deviceKey] || {});
}
getAllEndToEndSessions(txn, func) {
for (const deviceSessions of Object.values(this._sessions)) {
for (const sess of Object.values(deviceSessions)) {
func(sess);
}
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
let deviceSessions = this._sessions[deviceKey];
if (deviceSessions === undefined) {
deviceSessions = {};
this._sessions[deviceKey] = deviceSessions;
}
deviceSessions[sessionId] = sessionInfo;
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null);
}
getAllEndToEndInboundGroupSessions(txn, func) {
for (const key of Object.keys(this._inboundGroupSessions)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
func({
senderKey: key.substr(0, 43),
sessionId: key.substr(44),
sessionData: this._inboundGroupSessions[key],
});
}
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const k = senderCurve25519Key+'/'+sessionId;
if (this._inboundGroupSessions[k] === undefined) {
this._inboundGroupSessions[k] = sessionData;
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
}
// Device Data
getEndToEndDeviceData(txn, func) {
func(this._deviceData);
}
storeEndToEndDeviceData(deviceData, txn) {
this._deviceData = deviceData;
}
// E2E rooms
storeEndToEndRoom(roomId, roomInfo, txn) {
this._rooms[roomId] = roomInfo;
}
getEndToEndRooms(txn, func) {
func(this._rooms);
}
getSessionsNeedingBackup(limit) {
const sessions = [];
for (const session in this._sessionsNeedingBackup) {
if (this._inboundGroupSessions[session]) {
sessions.push({
senderKey: session.substr(0, 43),
sessionId: session.substr(44),
sessionData: this._inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
break;
}
}
}
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
delete this._sessionsNeedingBackup[sessionKey];
}
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
this._sessionsNeedingBackup[sessionKey] = true;
}
return Promise.resolve();
}
// Session key backups
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
}
+253
View File
@@ -0,0 +1,253 @@
/*
Copyright 2018 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.
*/
/**
* Base class for verification methods.
* @module crypto/verification/Base
*/
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 {
/**
* Base class for verification methods.
*
* <p>Once a verifier object is created, the verification can be started by
* calling the verify() method, which will return a promise that will
* resolve when the verification is completed, or reject if it could not
* complete.</p>
*
* <p>Subclasses must have a NAME class property.</p>
*
* @class
*
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
*
* @param {string} userId the user ID that is being verified
*
* @param {string} deviceId the device ID that is being verified
*
* @param {string} transactionId the transaction ID to be used when sending events
*
* @param {object} startEvent the m.key.verification.start event that
* initiated this verification, if any
*
* @param {object} request the key verification request object related to
* this verification, if any
*
* @param {object} parent parent verification for this verification, if any
*/
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) {
super();
this._baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
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) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
content.transaction_id = this.transactionId;
return this._baseApis.sendToDevice(type, {
[this.userId]: { [this.deviceId]: content },
});
}
_waitForEvent(type) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
this._expectedEvent = type;
return new Promise((resolve, reject) => {
this._resolveEvent = resolve;
this._rejectEvent = reject;
});
}
handleEvent(e) {
if (this._done) {
return;
} else if (e.getType() === this._expectedEvent) {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
} else {
this._expectedEvent = undefined;
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(),
);
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
reject(exception);
}
this.cancel(exception);
}
}
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 === 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();
if (e.getType() === "m.key.verification.cancel") {
content.code = content.code || "m.unknown";
content.reason = content.reason || content.body
|| "Unknown reason";
content.transaction_id = this.transactionId;
this._sendToDevice("m.key.verification.cancel", content);
} else {
this._sendToDevice("m.key.verification.cancel", {
code: "m.unknown",
reason: content.body || "Unknown reason",
transaction_id: this.transactionId,
});
}
}
} else {
this._sendToDevice("m.key.verification.cancel", {
code: "m.unknown",
reason: e.toString(),
transaction_id: this.transactionId,
});
}
}
if (this._promise !== null) {
// 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);
}
// Also emit a 'cancel' event that the app can listen for to detect cancellation
// before calling verify()
this.emit('cancel', e);
}
}
/**
* Begin the key verification
*
* @returns {Promise} Promise which resolves when the verification has
* completed.
*/
verify() {
if (this._promise) return this._promise;
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));
}
return this._promise;
}
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) {
logger.warn(`verification: Could not find device ${deviceId} to verify`);
} else {
await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
}
}
// 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);
}
}
}
+87
View File
@@ -0,0 +1,87 @@
/*
Copyright 2018 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.
*/
/**
* Error messages.
*
* @module crypto/verification/Error
*/
import {MatrixEvent} from "../../models/event";
export function newVerificationError(code, reason, extradata) {
extradata = extradata || {};
extradata.code = code;
extradata.reason = reason;
return new MatrixEvent({
type: "m.key.verification.cancel",
content: extradata,
});
}
export function errorFactory(code, reason) {
return function(extradata) {
return newVerificationError(code, reason, extradata);
};
}
/**
* The verification was cancelled by the user.
*/
export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
/**
* The verification timed out.
*/
export const newTimeoutError = errorFactory("m.timeout", "Timed out");
/**
* The transaction is unknown.
*/
export const newUnknownTransactionError = errorFactory(
"m.unknown_transaction", "Unknown transaction",
);
/**
* An unknown method was selected.
*/
export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
/**
* An unexpected message was sent.
*/
export const newUnexpectedMessageError = errorFactory(
"m.unexpected_message", "Unexpected message",
);
/**
* The key does not match.
*/
export const newKeyMismatchError = errorFactory(
"m.key_mismatch", "Key mismatch",
);
/**
* The user does not match.
*/
export const newUserMismatchError = errorFactory("m.user_error", "User mismatch");
/**
* An invalid message was sent.
*/
export const newInvalidMessageError = errorFactory(
"m.invalid_message", "Invalid message",
);
+123
View File
@@ -0,0 +1,123 @@
/*
Copyright 2018 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.
*/
/**
* QR code key verification.
* @module crypto/verification/QRCode
*/
import Base from "./Base";
import {
errorFactory,
newUserCancelledError,
newKeyMismatchError,
newUserMismatchError,
} from './Error';
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
const KEY_REGEXP = /^key_([^:]+:.+)$/;
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
/**
* @class crypto/verification/QRCode/ShowQRCode
* @extends {module:crypto/verification/Base}
*/
export class ShowQRCode extends Base {
_doVerification() {
if (!this._done) {
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
+ "&action=verify&key_ed25519%3A"
+ encodeURIComponent(this._baseApis.deviceId) + "="
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
this.emit("show_qr_code", {
url: url,
});
}
}
}
ShowQRCode.NAME = "m.qr_code.show.v1";
/**
* @class crypto/verification/QRCode/ScanQRCode
* @extends {module:crypto/verification/Base}
*/
export class ScanQRCode extends Base {
static factory(...args) {
return new ScanQRCode(...args);
}
async _doVerification() {
const code = await new Promise((resolve, reject) => {
this.emit("scan", {
done: resolve,
cancel: () => reject(newUserCancelledError()),
});
});
const match = code.match(MATRIXTO_REGEXP);
let deviceId;
const keys = {};
if (!match) {
throw newQRCodeError();
}
const userId = match[1];
const params = match[2].split("&").map(
(x) => x.split("=", 2).map(decodeURIComponent),
);
let action;
for (const [name, value] of params) {
if (name === "device") {
deviceId = value;
} else if (name === "action") {
action = value;
} else {
const keyMatch = name.match(KEY_REGEXP);
if (keyMatch) {
keys[keyMatch[1]] = value;
}
}
}
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
throw newQRCodeError();
}
if (!this.userId) {
await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: userId,
confirm: resolve,
cancel: () => reject(newUserMismatchError()),
});
});
} else if (this.userId !== userId) {
throw newUserMismatchError({
expected: this.userId,
actual: userId,
});
}
await this._verifyKeys(userId, keys, (keyId, device, key) => {
if (device.keys[keyId] !== key) {
throw newKeyMismatchError();
}
});
}
}
ScanQRCode.NAME = "m.qr_code.scan.v1";
+399
View File
@@ -0,0 +1,399 @@
/*
Copyright 2018 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.
*/
/**
* Short Authentication String (SAS) verification.
* @module crypto/verification/SAS
*/
import Base from "./Base";
import anotherjson from 'another-json';
import {
errorFactory,
newUserCancelledError,
newUnknownMethodError,
newKeyMismatchError,
newInvalidMessageError,
} from './Error';
const EVENTS = [
"m.key.verification.accept",
"m.key.verification.key",
"m.key.verification.mac",
];
let olmutil;
const newMismatchedSASError = errorFactory(
"m.mismatched_sas", "Mismatched short authentication string",
);
const newMismatchedCommitmentError = errorFactory(
"m.mismatched_commitment", "Mismatched commitment",
);
function generateDecimalSas(sasBytes) {
/**
* +--------+--------+--------+--------+--------+
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
* +--------+--------+--------+--------+--------+
* bits: 87654321 87654321 87654321 87654321 87654321
* \____________/\_____________/\____________/
* 1st number 2nd number 3rd number
*/
return [
(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000,
];
}
const emojiMapping = [
["🐶", "dog"], // 0
["🐱", "cat"], // 1
["🦁", "lion"], // 2
["🐎", "horse"], // 3
["🦄", "unicorn"], // 4
["🐷", "pig"], // 5
["🐘", "elephant"], // 6
["🐰", "rabbit"], // 7
["🐼", "panda"], // 8
["🐓", "rooster"], // 9
["🐧", "penguin"], // 10
["🐢", "turtle"], // 11
["🐟", "fish"], // 12
["🐙", "octopus"], // 13
["🦋", "butterfly"], // 14
["🌷", "flower"], // 15
["🌳", "tree"], // 16
["🌵", "cactus"], // 17
["🍄", "mushroom"], // 18
["🌏", "globe"], // 19
["🌙", "moon"], // 20
["☁️", "cloud"], // 21
["🔥", "fire"], // 22
["🍌", "banana"], // 23
["🍎", "apple"], // 24
["🍓", "strawberry"], // 25
["🌽", "corn"], // 26
["🍕", "pizza"], // 27
["🎂", "cake"], // 28
["❤️", "heart"], // 29
["🙂", "smiley"], // 30
["🤖", "robot"], // 31
["🎩", "hat"], // 32
["👓", "glasses"], // 33
["🔧", "spanner"], // 34
["🎅", "santa"], // 35
["👍", "thumbs up"], // 36
["☂️", "umbrella"], // 37
["⌛", "hourglass"], // 38
["⏰", "clock"], // 39
["🎁", "gift"], // 40
["💡", "light bulb"], // 41
["📕", "book"], // 42
["✏️", "pencil"], // 43
["📎", "paperclip"], // 44
["✂️", "scissors"], // 45
["🔒", "padlock"], // 46
["🔑", "key"], // 47
["🔨", "hammer"], // 48
["☎️", "telephone"], // 49
["🏁", "flag"], // 50
["🚂", "train"], // 51
["🚲", "bicycle"], // 52
["✈️", "aeroplane"], // 53
["🚀", "rocket"], // 54
["🏆", "trophy"], // 55
["⚽", "ball"], // 56
["🎸", "guitar"], // 57
["🎺", "trumpet"], // 58
["🔔", "bell"], // 59
["⚓️", "anchor"], // 60
["🎧", "headphones"], // 61
["📁", "folder"], // 62
["📌", "pin"], // 63
];
function generateEmojiSas(sasBytes) {
const emojis = [
// just like base64 encoding
sasBytes[0] >> 2,
(sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4,
(sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6,
sasBytes[2] & 0x3f,
sasBytes[3] >> 2,
(sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4,
(sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6,
];
return emojis.map((num) => emojiMapping[num]);
}
const sasGenerators = {
decimal: generateDecimalSas,
emoji: generateEmojiSas,
};
function generateSas(sasBytes, methods) {
const sas = {};
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}
*/
export default class SAS extends Base {
get events() {
return EVENTS;
}
async _doVerification() {
await global.Olm.init();
olmutil = olmutil || new global.Olm.Utility();
// make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]);
if (this.startEvent) {
return await this._doRespondVerification();
} else {
return await this._doSendVerification();
}
}
async _doSendVerification() {
const initialMessage = {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
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: SAS_LIST,
transaction_id: this.transactionId,
};
this._sendToDevice("m.key.verification.start", initialMessage);
let e = await this._waitForEvent("m.key.verification.accept");
let content = e.getContent();
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 olmSAS = new global.Olm.SAS();
try {
this._sendToDevice("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
e = await this._waitForEvent("m.key.verification.key");
// 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();
}
olmSAS.set_their_key(content.key);
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
async _doRespondVerification() {
let content = this.startEvent.getContent();
// 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();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._sendToDevice("m.key.verification.accept", {
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),
});
let e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
olmSAS.set_their_key(content.key);
this._sendToDevice("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod);
} finally {
olmSAS.free();
}
}
_sendMAC(olmSAS, method) {
const keyId = `ed25519:${this._baseApis.deviceId}`;
const mac = {};
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
mac[keyId] = olmSAS[macMethods[method]](
this._baseApis.getDeviceEd25519Key(),
baseInfo + keyId,
);
const keys = olmSAS[macMethods[method]](
keyId,
baseInfo + "KEY_IDS",
);
this._sendToDevice("m.key.verification.mac", { mac, keys });
}
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[macMethods[method]](
Object.keys(content.mac).sort().join(","),
baseInfo + "KEY_IDS",
)) {
throw newKeyMismatchError();
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== olmSAS[macMethods[method]](
device.keys[keyId],
baseInfo + keyId,
)) {
throw newKeyMismatchError();
}
});
}
}
SAS.NAME = "m.sas.v1";
+46
View File
@@ -0,0 +1,46 @@
// can't just do InvalidStoreError extends Error
// because of http://babeljs.io/docs/usage/caveats/#classes
export function InvalidStoreError(reason, value) {
const message = `Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.value = value;
return instance;
}
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
InvalidStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidStoreError, Error);
export function InvalidCryptoStoreError(reason) {
const message = `Crypto store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.name = 'InvalidCryptoStoreError';
return instance;
}
InvalidCryptoStoreError.TOO_NEW = "TOO_NEW";
InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
+4 -3
View File
@@ -97,11 +97,12 @@ FilterComponent.prototype._checkFields =
};
const self = this;
Object.keys(literal_keys).forEach(function(name) {
for (let n=0; n < Object.keys(literal_keys).length; n++) {
const name = Object.keys(literal_keys)[n];
const match_func = literal_keys[name];
const not_name = "not_" + name;
const disallowed_values = self[not_name];
if (disallowed_values.map(match_func)) {
if (disallowed_values.filter(match_func).length > 0) {
return false;
}
@@ -111,7 +112,7 @@ FilterComponent.prototype._checkFields =
return false;
}
}
});
}
const contains_url_filter = this.filter_json.contains_url;
if (contains_url_filter !== undefined) {
+11
View File
@@ -51,6 +51,17 @@ function Filter(userId, filterId) {
this.definition = {};
}
Filter.LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
Filter.LAZY_LOADING_SYNC_FILTER = {
room: {
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
},
};
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
+81 -17
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,
};
},
@@ -118,6 +125,10 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
@@ -152,14 +163,29 @@ module.exports.MatrixHttpApi.prototype = {
opts = {};
}
// default opts.includeFilename to true (ignoring falsey values)
const includeFilename = opts.includeFilename !== false;
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
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.
@@ -168,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 " +
@@ -181,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 " +
@@ -271,11 +297,27 @@ module.exports.MatrixHttpApi.prototype = {
});
}
});
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(fileName);
let url = this.opts.baseUrl + "/_matrix/media/r0/upload";
const queryArgs = [];
if (includeFilename && fileName) {
queryArgs.push("filename=" + encodeURIComponent(fileName));
}
if (!this.useAuthorizationHeader) {
queryArgs.push("access_token="
+ encodeURIComponent(this.opts.accessToken));
}
if (queryArgs.length > 0) {
url += "?" + queryArgs.join("&");
}
xhr.open("POST", url);
if (this.useAuthorizationHeader) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
@@ -283,13 +325,15 @@ module.exports.MatrixHttpApi.prototype = {
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
const queryParams = {
filename: fileName,
};
const queryParams = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
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,
@@ -330,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)) {
@@ -351,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(
@@ -432,7 +488,13 @@ 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",
err.message,
err.data.consent_uri,
);
}
});
@@ -721,10 +783,12 @@ module.exports.MatrixHttpApi.prototype = {
method: method,
withCredentials: false,
qs: queryParams,
qsStringifyOptions: opts.qsStringifyOptions,
useQuerystring: true,
body: data,
json: false,
timeout: localTimeoutMs,
headers: opts.headers || {},
headers: headers || {},
_matrix_opts: this.opts,
},
function(err, response, body) {
+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;
},
+36
View File
@@ -0,0 +1,36 @@
/*
Copyright 2018 André Jaenisch
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.
*/
/**
* @module logger
*/
const log = require("loglevel");
// This is to demonstrate, that you can use any namespace you want.
// Namespaces allow you to turn on/off the logging for specific parts of the
// application.
// An idea would be to control this via an environment variable (on Node.js).
// See https://www.npmjs.com/package/debug to see how this could be implemented
// Part of #332 is introducing a logging library in the first place.
const DEFAULT_NAME_SPACE = "matrix";
const logger = log.getLogger(DEFAULT_NAME_SPACE);
logger.setLevel(log.levels.DEBUG);
/**
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
* Can be tailored down to specific use cases if needed.
*/
module.exports = logger;
+29 -6
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.
@@ -16,12 +17,20 @@ limitations under the License.
*/
"use strict";
/** The {@link module:ContentHelpers} object */
module.exports.ContentHelpers = require("./content-helpers");
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
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. */
@@ -32,10 +41,14 @@ module.exports.SyncAccumulator = require("./sync-accumulator");
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
/** The {@link module:http-api.MatrixError|MatrixError} class. */
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:errors.InvalidStoreError|InvalidStoreError} class. */
module.exports.InvalidStoreError = require("./errors").InvalidStoreError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/group|Group} class. */
module.exports.Group = require("./models/group");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
@@ -61,7 +74,10 @@ module.exports.Filter = require("./filter");
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/** The {@link module:interactive-auth} class. */
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;
@@ -80,14 +96,21 @@ module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCal
/**
* Set an audio input device to use for MatrixCalls
* Set a preferred audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioOutput = require('./webrtc/call').setAudioOutput;
/**
* Set a preferred audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
/**
* Set a video input device to use for MatrixCalls
* Set a preferred video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
@@ -149,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
@@ -172,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();
+190 -43
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);
@@ -168,49 +187,19 @@ EventTimelineSet.prototype.resetLiveTimeline = function(
// if timeline support is disabled, forget about the old timelines
const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken;
let newTimeline;
const oldTimeline = this._liveTimeline;
const newTimeline = resetAllTimelines ?
oldTimeline.forkLive(EventTimeline.FORWARDS) :
oldTimeline.fork(EventTimeline.FORWARDS);
if (resetAllTimelines) {
newTimeline = new EventTimeline(this);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
} else {
newTimeline = this.addTimeline();
this._timelines.push(newTimeline);
}
const oldTimeline = this._liveTimeline;
// Collect the state events from the old timeline
const evMap = oldTimeline.getState(EventTimeline.FORWARDS).events;
const events = [];
for (const evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) {
continue;
}
for (const stateKey in evMap[evtype]) {
if (!evMap[evtype].hasOwnProperty(stateKey)) {
continue;
}
events.push(evMap[evtype][stateKey]);
}
}
// Use those events to initialise the state of the new live timeline
newTimeline.initialiseState(events);
const freshEndState = newTimeline._endState;
// Now clobber the end state of the new live timeline with that from the
// previous live timeline. It will be identical except that we'll keep
// using the same RoomMember objects for the 'live' set of members with any
// listeners still attached
newTimeline._endState = oldTimeline._endState;
// If we're not resetting all timelines, we need to fix up the old live timeline
if (!resetAllTimelines) {
// Firstly, we just stole the old timeline's end state, so it needs a new one.
// Just swap them around and give it the one we just generated for the
// new live timeline.
oldTimeline._endState = freshEndState;
if (forwardPaginationToken) {
// Now set the forward pagination token on the old live timeline
// so it can be forward-paginated.
oldTimeline.setPaginationToken(
@@ -435,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;
}
@@ -448,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);
}
};
@@ -517,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,
@@ -651,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.
*/
+61 -15
View File
@@ -20,8 +20,6 @@ limitations under the License.
*/
const RoomState = require("./room-state");
const utils = require("../utils");
const MatrixEvent = require("./event").MatrixEvent;
/**
* Construct a new EventTimeline
@@ -88,22 +86,70 @@ EventTimeline.prototype.initialiseState = function(stateEvents) {
throw new Error("Cannot initialise state after events are added");
}
// we deep-copy the events here, in case they get changed later - we don't
// want changes to the start state leaking through to the end state.
const oldStateEvents = utils.map(
utils.deepCopy(
stateEvents.map(function(mxEvent) {
return mxEvent.event;
}),
),
function(ev) {
return new MatrixEvent(ev);
});
// We previously deep copied events here and used different copies in
// the oldState and state events: this decision seems to date back
// quite a way and was apparently made to fix a bug where modifications
// made to the start state leaked through to the end state.
// This really shouldn't be possible though: the events themselves should
// not change. Duplicating the events uses a lot of extra memory,
// so we now no longer do it. To assert that they really do never change,
// freeze them! Note that we can't do this for events in general:
// although it looks like the only things preventing us are the
// 'status' flag, forwardLooking (which is only set once when adding to the
// timeline) and possibly the sender (which seems like it should never be
// reset but in practice causes a lot of the tests to break).
for (const e of stateEvents) {
Object.freeze(e);
}
this._startState.setStateEvents(oldStateEvents);
this._startState.setStateEvents(stateEvents);
this._endState.setStateEvents(stateEvents);
};
/**
* Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
* All attached listeners will keep receiving state updates from the new live timeline state.
* The end state of this timeline gets replaced with an independent copy of the current RoomState,
* and will need a new pagination token if it ever needs to paginate forwards.
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
* of the timeline.
*
* @return {EventTimeline} the new timeline
*/
EventTimeline.prototype.forkLive = function(direction) {
const forkState = this.getState(direction);
const timeline = new EventTimeline(this._eventTimelineSet);
timeline._startState = forkState.clone();
// Now clobber the end state of the new live timeline with that from the
// previous live timeline. It will be identical except that we'll keep
// using the same RoomMember objects for the 'live' set of members with any
// listeners still attached
timeline._endState = forkState;
// Firstly, we just stole the current timeline's end state, so it needs a new one.
// Make an immutable copy of the state so back pagination will get the correct sentinels.
this._endState = forkState.clone();
return timeline;
};
/**
* Creates an independent timeline, inheriting the directional state from this timeline.
*
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
* of the timeline.
*
* @return {EventTimeline} the new timeline
*/
EventTimeline.prototype.fork = function(direction) {
const forkState = this.getState(direction);
const timeline = new EventTimeline(this._eventTimelineSet);
timeline._startState = forkState.clone();
timeline._endState = forkState.clone();
return timeline;
};
/**
* Get the ID of the room for this timeline
* @return {string} room ID
@@ -230,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) {
+431 -35
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
@@ -80,24 +88,32 @@ module.exports.MatrixEvent = function MatrixEvent(
// intern the values of matrix events to force share strings and reduce the
// amount of needless string duplication. This can save moderate amounts of
// memory (~10% on a 350MB heap).
["state_key", "type", "sender", "room_id"].forEach((prop) => {
// 'membership' at the event level (rather than the content level) is a legacy
// field that Riot never otherwise looks at, but it will still take up a lot
// of space if we don't intern it.
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
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 || {};
@@ -108,8 +124,9 @@ module.exports.MatrixEvent = function MatrixEvent(
this.error = null;
this.forwardLooking = true;
this._pushActions = null;
this._date = this.event.origin_server_ts ?
new Date(this.event.origin_server_ts) : null;
this._replacingEvent = null;
this._localRedactionEvent = null;
this._isCancelled = false;
this._clearEvent = {};
@@ -204,16 +221,37 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* @return {Date} The event date, e.g. <code>new Date(1433502692297)</code>
*/
getDate: function() {
return this._date;
return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null;
},
/**
* 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();
}
},
/**
@@ -351,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(
@@ -366,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;
@@ -377,6 +415,47 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this._decryptionPromise;
},
/**
* Cancel any room key request for this event and resend another.
*
* @param {module:crypto} crypto crypto module
* @param {string} userId the user who received this event
*
* @returns {Promise} a promise that resolves when the request is queued
*/
cancelAndResendKeyRequest: function(crypto, userId) {
const wireContent = this.getWireContent();
return crypto.requestRoomKey({
algorithm: wireContent.algorithm,
room_id: this.getRoomId(),
session_id: wireContent.session_id,
sender_key: wireContent.sender_key,
}, this.getKeyRequestRecipients(userId), true);
},
/**
* Calculate the recipients for keyshare requests.
*
* @param {string} userId the user who received this event.
*
* @returns {Array} array of recipients
*/
getKeyRequestRecipients: function(userId) {
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const wireContent = this.getWireContent();
const recipients = [{
userId, deviceId: '*',
}];
const sender = this.getSender();
if (sender !== userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
return recipients;
},
_decryptionLoop: async function(crypto) {
// make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear _decryptionPromise *before*
@@ -388,6 +467,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
this._retryDecryption = false;
let res;
let err;
try {
if (!crypto) {
res = this._badEncryptedMessage("Encryption not enabled");
@@ -398,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;
@@ -406,6 +486,8 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return;
}
err = e;
// see if we have a retry queued.
//
// NB: make sure to keep this check in the same tick of the
@@ -422,17 +504,17 @@ 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.message}), but retrying`,
`${e}), but retrying`,
);
continue;
}
// decryption error, no retries queued. Warn about the error and
// set it to m.bad.encrypted.
console.warn(
`Error decrypting event (id=${this.getId()}): ${e}`,
logger.warn(
`Error decrypting event (id=${this.getId()}): ${e.detailedString}`,
);
res = this._badEncryptedMessage(e.message);
@@ -451,6 +533,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
this._decryptionPromise = null;
this._retryDecryption = false;
this._setClearData(res);
// Before we emit the event, clear the push actions so that they can be recalculated
// by relevant code. We do this because the clear event has now changed, making it
// so that existing rules can be re-run over the applicable properties. Stuff like
// highlighting when the user's name is mentioned rely on this happening. We also want
// to set the push actions before emitting so that any notification listeners don't
// pick up the wrong contents.
this.setPushActions(null);
this.emit("Event.decrypted", this, err);
return;
}
},
@@ -487,7 +580,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
decryptionResult.claimedEd25519Key || null;
this._forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this.emit("Event.decrypted", this);
},
/**
* Gets the cleartext content for this event. If the event is not encrypted,
* or encryption has not been completed, this will return null.
*
* @returns {Object} The cleartext (decrypted) content for the event
*/
getClearContent: function() {
const ev = this._clearEvent;
return ev && ev.content ? ev.content : null;
},
/**
@@ -571,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
@@ -584,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.
//
@@ -626,41 +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._date = new Date(this.event.origin_server_ts);
},
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
@@ -693,4 +1086,7 @@ const _REDACT_KEEP_CONTENT_MAP = {
*
* @param {module:models/event.MatrixEvent} event
* The matrix event which has been decrypted
* @param {module:crypto/algorithms/base.DecryptionError?} err
* The error that occured during decryption, or `undefined` if no
* error occured.
*/
+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);
}
}
}
}
+99 -13
View File
@@ -58,10 +58,27 @@ function RoomMember(roomId, userId) {
this.events = {
member: null,
};
this._isOutOfBand = false;
this._updateModifiedTime();
}
utils.inherits(RoomMember, EventEmitter);
/**
* Mark the member as coming from a channel that is not sync
*/
RoomMember.prototype.markOutOfBand = function() {
this._isOutOfBand = true;
};
/**
* @return {bool} does the member come from a channel that is not sync?
* This is used to store the member seperately
* from the sync state so it available across browser sessions.
*/
RoomMember.prototype.isOutOfBand = function() {
return this._isOutOfBand;
};
/**
* Update this room member's membership event. May fire "RoomMember.name" if
* this event updates this member's name.
@@ -75,13 +92,20 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
if (event.getType() !== "m.room.member") {
return;
}
this._isOutOfBand = false;
this.events.member = event;
const oldMembership = this.membership;
this.membership = event.getDirectionalContent().membership;
const oldName = this.name;
this.name = calculateDisplayName(this, event, roomState);
this.name = calculateDisplayName(
this.userId,
event.getDirectionalContent().displayname,
roomState);
this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
if (oldMembership !== this.membership) {
this._updateModifiedTime();
@@ -177,6 +201,44 @@ RoomMember.prototype.getLastModifiedTime = function() {
return this._modified;
};
RoomMember.prototype.isKicked = function() {
return this.membership === "leave" &&
this.events.member.getSender() !== this.events.member.getStateKey();
};
/**
* If this member was invited with the is_direct flag set, return
* the user that invited this member
* @return {string} user id of the inviter
*/
RoomMember.prototype.getDMInviter = function() {
// when not available because that room state hasn't been loaded in,
// we don't really know, but more likely to not be a direct chat
if (this.events.member) {
// TODO: persist the is_direct flag on the member as more member events
// come in caused by displayName changes.
// the is_direct flag is set on the invite member event.
// This is copied on the prev_content section of the join member event
// when the invite is accepted.
const memberEvent = this.events.member;
let memberContent = memberEvent.getContent();
let inviteSender = memberEvent.getSender();
if (memberContent.membership === "join") {
memberContent = memberEvent.getPrevContent();
inviteSender = memberEvent.getUnsigned().prev_sender;
}
if (memberContent.membership === "invite" && memberContent.is_direct) {
return inviteSender;
}
}
};
/**
* Get the avatar URL for a room member.
* @param {string} baseUrl The base homeserver URL See
@@ -187,7 +249,7 @@ RoomMember.prototype.getLastModifiedTime = 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
@@ -200,10 +262,12 @@ RoomMember.prototype.getAvatarUrl =
if (allowDefault === undefined) {
allowDefault = true;
}
if (!this.events.member && !allowDefault) {
const rawUrl = this.getMxcAvatarUrl();
if (!rawUrl && !allowDefault) {
return null;
}
const rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
const httpUrl = ContentRepo.getHttpUriForMxc(
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks,
);
@@ -216,12 +280,27 @@ RoomMember.prototype.getAvatarUrl =
}
return null;
};
/**
* get the mxc avatar url, either from a state event, or from a lazily loaded member
* @return {string} the mxc avatar url
*/
RoomMember.prototype.getMxcAvatarUrl = function() {
if(this.events.member) {
return this.events.member.getDirectionalContent().avatar_url;
} else if(this.user) {
return this.user.avatarUrl;
}
return null;
};
function calculateDisplayName(member, event, roomState) {
const displayName = event.getDirectionalContent().displayname;
const selfUserId = member.userId;
function calculateDisplayName(selfUserId, displayName, roomState) {
if (!displayName || displayName === selfUserId) {
return selfUserId;
}
if (!displayName) {
// First check if the displayname is something we consider truthy
// after stripping it of zero width characters and padding spaces
if (!utils.removeHiddenChars(displayName)) {
return selfUserId;
}
@@ -229,11 +308,18 @@ function calculateDisplayName(member, event, roomState) {
return displayName;
}
const userIds = roomState.getUserIdsWithDisplayName(displayName);
const otherUsers = userIds.filter(function(u) {
return u !== selfUserId;
});
if (otherUsers.length > 0) {
// Next check if the name contains something that look like a mxid
// If it does, it may be someone trying to impersonate someone else
// Show full mxid in this case
// Also show mxid if there are other people with the same or similar
// displayname, after hidden character removal.
let disambiguate = /@.+:.+/.test(displayName);
if (!disambiguate) {
const userIds = roomState.getUserIdsWithDisplayName(displayName);
disambiguate = userIds.some((u) => u !== selfUserId);
}
if (disambiguate) {
return displayName + " (" + selfUserId + ")";
}
return displayName;
+359 -62
View File
@@ -21,19 +21,48 @@ 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;
const OOB_STATUS_INPROGRESS = 2;
const OOB_STATUS_FINISHED = 3;
/**
* Construct room state.
*
* Room State represents the state of the room at a given point.
* It can be mutated by adding state events to it.
* There are two types of room member associated with a state event:
* normal member objects (accessed via getMember/getMembers) which mutate
* with the state to represent the current state of that room/user, eg.
* the object returned by getMember('@bob:example.com') will mutate to
* get a different display name if Bob later changes his display name
* in the room.
* There are also 'sentinel' members (accessed via getSentinelMember).
* These also represent the state of room members at the point in time
* represented by the RoomState object, but unlike objects from getMember,
* sentinel objects will always represent the room state as at the time
* getSentinelMember was called, so if Bob subsequently changes his display
* name, a room member object previously acquired with getSentinelMember
* will still have his old display name. Calling getSentinelMember again
* after the display name change will return a new RoomMember object
* with Bob's new display name.
*
* @constructor
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @param {?object} oobMemberFlags Optional. The state of loading out of bound members.
* As the timeline might get reset while they are loading, this state needs to be inherited
* and shared when the room state is cloned for the new timeline.
* This should only be passed from clone.
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
* events dictionary, keyed on the event type and then the state_key value.
* @prop {string} paginationToken The pagination token for this state.
*/
function RoomState(roomId) {
function RoomState(roomId, oobMemberFlags = undefined) {
this.roomId = roomId;
this.members = {
// userId: RoomMember
@@ -47,12 +76,79 @@ function RoomState(roomId) {
// userId: RoomMember
};
this._updateModifiedTime();
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
this._joinedMemberCount = null; // cache of the number of joined members
// joined members count from summary api
// once set, we know the server supports the summary api
// and we should only trust that
// we could also only trust that before OOB members
// are loaded but doesn't seem worth the hassle atm
this._summaryJoinedMemberCount = null;
// same for invited member count
this._invitedMemberCount = null;
this._summaryInvitedMemberCount = null;
if (!oobMemberFlags) {
oobMemberFlags = {
status: OOB_STATUS_NOTSTARTED,
};
}
this._oobMemberFlags = oobMemberFlags;
}
utils.inherits(RoomState, EventEmitter);
/**
* Returns the number of joined members in this room
* This method caches the result.
* @return {integer} The number of members in this room whose membership is 'join'
*/
RoomState.prototype.getJoinedMemberCount = function() {
if (this._summaryJoinedMemberCount !== null) {
return this._summaryJoinedMemberCount;
}
if (this._joinedMemberCount === null) {
this._joinedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'join' ? count + 1 : count;
}, 0);
}
return this._joinedMemberCount;
};
/**
* Set the joined member count explicitly (like from summary part of the sync response)
* @param {number} count the amount of joined members
*/
RoomState.prototype.setJoinedMemberCount = function(count) {
this._summaryJoinedMemberCount = count;
};
/**
* Returns the number of invited members in this room
* @return {integer} The number of members in this room whose membership is 'invite'
*/
RoomState.prototype.getInvitedMemberCount = function() {
if (this._summaryInvitedMemberCount !== null) {
return this._summaryInvitedMemberCount;
}
if (this._invitedMemberCount === null) {
this._invitedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'invite' ? count + 1 : count;
}, 0);
}
return this._invitedMemberCount;
};
/**
* Set the amount of invited members in this room
* @param {number} count the amount of invited members
*/
RoomState.prototype.setInvitedMemberCount = function(count) {
this._summaryInvitedMemberCount = count;
};
/**
* Get all RoomMembers in this room.
* @return {Array<RoomMember>} A list of RoomMembers.
@@ -61,6 +157,16 @@ RoomState.prototype.getMembers = function() {
return utils.values(this.members);
};
/**
* Get all RoomMembers in this room, excluding the user IDs provided.
* @param {Array<string>} excludedIds The user IDs to exclude.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembersExcept = function(excludedIds) {
return utils.values(this.members)
.filter((m) => !excludedIds.includes(m.userId));
};
/**
* Get a room member by their user ID.
* @param {string} userId The room member's user ID.
@@ -80,7 +186,18 @@ RoomState.prototype.getMember = function(userId) {
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getSentinelMember = function(userId) {
return this._sentinels[userId] || null;
if (!userId) return null;
let sentinel = this._sentinels[userId];
if (sentinel === undefined) {
sentinel = new RoomMember(this.roomId, userId);
const member = this.members[userId];
if (member) {
sentinel.setMembershipEvent(member.events.member, this);
}
this._sentinels[userId] = sentinel;
}
return sentinel;
};
/**
@@ -104,6 +221,67 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) {
return event ? event : null;
};
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
*/
RoomState.prototype.clone = function() {
const copy = new RoomState(this.roomId, this._oobMemberFlags);
// Ugly hack: because setStateEvents will mark
// members as susperseding future out of bound members
// if loading is in progress (through _oobMemberFlags)
// since these are not new members, we're merely copying them
// set the status to not started
// after copying, we set back the status
const status = this._oobMemberFlags.status;
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
Object.values(this.events).forEach((eventsByStateKey) => {
const eventsForType = Object.values(eventsByStateKey);
copy.setStateEvents(eventsForType);
});
// Ugly hack: see above
this._oobMemberFlags.status = status;
if (this._summaryInvitedMemberCount !== null) {
copy.setInvitedMemberCount(this.getInvitedMemberCount());
}
if (this._summaryJoinedMemberCount !== null) {
copy.setJoinedMemberCount(this.getJoinedMemberCount());
}
// copy out of band flags if needed
if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) {
// copy markOutOfBand flags
this.getMembers().forEach((member) => {
if (member.isOutOfBand()) {
const copyMember = copy.getMember(member.userId);
copyMember.markOutOfBand();
}
});
}
return copy;
};
/**
* Add previously unknown state events.
* When lazy loading members while back-paginating,
* the relevant room state for the timeline chunk at the end
* of the chunk can be set with this method.
* @param {MatrixEvent[]} events state events to prepend
*/
RoomState.prototype.setUnknownStateEvents = function(events) {
const unknownStateEvents = events.filter((event) => {
return this.events[event.getType()] === undefined ||
this.events[event.getType()][event.getStateKey()] === undefined;
});
this.setStateEvents(unknownStateEvents);
};
/**
* Add an array of one or more state MatrixEvents, overwriting
* any existing state with the same {type, stateKey} tuple. Will fire
@@ -127,10 +305,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
return;
}
if (self.events[event.getType()] === undefined) {
self.events[event.getType()] = {};
}
self.events[event.getType()][event.getStateKey()] = event;
self._setStateEvent(event);
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname,
@@ -168,28 +343,10 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
event.getPrevContent().displayname;
}
let member = self.members[userId];
if (!member) {
member = new RoomMember(event.getRoomId(), userId);
self.emit("RoomState.newMember", event, self, member);
}
// Add a new sentinel for this change. We apply the same
// operations to both sentinel and member rather than deep copying
// so we don't make assumptions about the properties of RoomMember
// (e.g. and manage to break it because deep copying doesn't do
// everything).
const sentinel = new RoomMember(event.getRoomId(), userId);
utils.forEach([member, sentinel], function(roomMember) {
roomMember.setMembershipEvent(event, self);
// this member may have a power level already, so set it.
const pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
roomMember.setPowerLevelEvent(pwrLvlEvent);
}
});
const member = self._getOrCreateMember(userId, event);
member.setMembershipEvent(event, self);
self._sentinels[userId] = sentinel;
self.members[userId] = member;
self._updateMember(member);
self.emit("RoomState.members", event, self, member);
} else if (event.getType() === "m.room.power_levels") {
const members = utils.values(self.members);
@@ -198,19 +355,146 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self.emit("RoomState.members", event, self, member);
});
// Go through the sentinel members and see if any of them would be
// affected by the new power levels. If so, replace the sentinel.
for (const userId of Object.keys(self._sentinels)) {
const oldSentinel = self._sentinels[userId];
const newSentinel = new RoomMember(event.getRoomId(), userId);
newSentinel.setMembershipEvent(oldSentinel.events.member, self);
newSentinel.setPowerLevelEvent(event);
self._sentinels[userId] = newSentinel;
}
// assume all our sentinels are now out-of-date
self._sentinels = {};
}
});
};
/**
* Looks up a member by the given userId, and if it doesn't exist,
* create it and emit the `RoomState.newMember` event.
* This method makes sure the member is added to the members dictionary
* before emitting, as this is done from setStateEvents and _setOutOfBandMember.
* @param {string} userId the id of the user to look up
* @param {MatrixEvent} event the membership event for the (new) member. Used to emit.
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @returns {RoomMember} the member, existing or newly created.
*/
RoomState.prototype._getOrCreateMember = function(userId, event) {
let member = this.members[userId];
if (!member) {
member = new RoomMember(this.roomId, userId);
// add member to members before emitting any events,
// as event handlers often lookup the member
this.members[userId] = member;
this.emit("RoomState.newMember", event, this, member);
}
return member;
};
RoomState.prototype._setStateEvent = function(event) {
if (this.events[event.getType()] === undefined) {
this.events[event.getType()] = {};
}
this.events[event.getType()][event.getStateKey()] = event;
};
RoomState.prototype._updateMember = function(member) {
// this member may have a power level already, so set it.
const pwrLvlEvent = this.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
member.setPowerLevelEvent(pwrLvlEvent);
}
// blow away the sentinel which is now outdated
delete this._sentinels[member.userId];
this.members[member.userId] = member;
this._joinedMemberCount = null;
this._invitedMemberCount = null;
};
/**
* Get the out-of-band members loading state, whether loading is needed or not.
* Note that loading might be in progress and hence isn't needed.
* @return {bool} whether or not the members of this room need to be loaded
*/
RoomState.prototype.needsOutOfBandMembers = function() {
return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED;
};
/**
* Mark this room state as waiting for out-of-band members,
* ensuring it doesn't ask for them to be requested again
* through needsOutOfBandMembers
*/
RoomState.prototype.markOutOfBandMembersStarted = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_INPROGRESS;
};
/**
* Mark this room state as having failed to fetch out-of-band members
*/
RoomState.prototype.markOutOfBandMembersFailed = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Clears the loaded out-of-band members
*/
RoomState.prototype.clearOutOfBandMembers = function() {
let count = 0;
Object.keys(this.members).forEach((userId) => {
const member = this.members[userId];
if (member.isOutOfBand()) {
++count;
delete this.members[userId];
}
});
logger.log(`LL: RoomState removed ${count} members...`);
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Sets the loaded out-of-band members.
* @param {MatrixEvent[]} stateEvents array of membership state events
*/
RoomState.prototype.setOutOfBandMembers = function(stateEvents) {
logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
logger.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
this._oobMemberFlags.status = OOB_STATUS_FINISHED;
stateEvents.forEach((e) => this._setOutOfBandMember(e));
};
/**
* Sets a single out of band member, used by both setOutOfBandMembers and clone
* @param {MatrixEvent} stateEvent membership state event
*/
RoomState.prototype._setOutOfBandMember = function(stateEvent) {
if (stateEvent.getType() !== 'm.room.member') {
return;
}
const userId = stateEvent.getStateKey();
const existingMember = this.getMember(userId);
// never replace members received as part of the sync
if (existingMember && !existingMember.isOutOfBand()) {
return;
}
const member = this._getOrCreateMember(userId, stateEvent);
member.setMembershipEvent(stateEvent, this);
// needed to know which members need to be stored seperately
// as they are not part of the sync accumulator
// this is cleared by setMembershipEvent so when it's updated through /sync
member.markOutOfBand();
_updateDisplayNameCache(this, member.userId, member.name);
this._setStateEvent(stateEvent);
this._updateMember(member);
this.emit("RoomState.members", stateEvent, this, member);
};
/**
* Set the current typing event for this room.
* @param {MatrixEvent} event The typing event
@@ -248,12 +532,12 @@ RoomState.prototype.getLastModifiedTime = function() {
};
/**
* Get user IDs with the specified display name.
* Get user IDs with the specified or similar display names.
* @param {string} displayName The display name to get user IDs from.
* @return {string[]} An array of user IDs or an empty array.
*/
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
return this._displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
};
/**
@@ -268,7 +552,11 @@ RoomState.prototype.maySendRedactionForEvent = function(mxEvent, userId) {
if (!member || member.membership === 'leave') return false;
if (mxEvent.status || mxEvent.isRedacted()) return false;
if (mxEvent.getSender() === userId) return true;
// The user may have been the sender, but they can't redact their own message
// if redactions are blocked.
const canRedact = this.maySendEvent("m.room.redaction", userId);
if (mxEvent.getSender() === userId) return canRedact;
return this._hasSufficientPowerLevelFor('redact', member.powerLevel);
};
@@ -288,7 +576,7 @@ RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
}
let requiredLevel = 50;
if (powerLevels[action] !== undefined) {
if (utils.isNumber(powerLevels[action])) {
requiredLevel = powerLevels[action];
}
@@ -360,11 +648,6 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
const member = this.getMember(userId);
if (!member || member.membership == 'leave') {
return false;
}
const power_levels_event = this.getStateEvents('m.room.power_levels', '');
let power_levels;
@@ -372,25 +655,34 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
let state_default = 0;
let events_default = 0;
let powerLevel = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
if (power_levels.state_default !== undefined) {
if (Number.isFinite(power_levels.state_default)) {
state_default = power_levels.state_default;
} else {
state_default = 50;
}
if (power_levels.events_default !== undefined) {
const userPowerLevel = power_levels.users && power_levels.users[userId];
if (Number.isFinite(userPowerLevel)) {
powerLevel = userPowerLevel;
} else if(Number.isFinite(power_levels.users_default)) {
powerLevel = power_levels.users_default;
}
if (Number.isFinite(power_levels.events_default)) {
events_default = power_levels.events_default;
}
}
let required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
if (Number.isFinite(events_levels[eventType])) {
required_level = events_levels[eventType];
}
return member.powerLevel >= required_level;
return powerLevel >= required_level;
};
/**
@@ -414,7 +706,7 @@ RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
powerLevelsEvent &&
powerLevelsEvent.getContent() &&
powerLevelsEvent.getContent().notifications &&
powerLevelsEvent.getContent().notifications[notifLevelKey]
utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
) {
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
}
@@ -453,22 +745,26 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
const existingUserIds = roomState._displayNameToUserIds[oldName] || [];
for (let i = 0; i < existingUserIds.length; i++) {
if (existingUserIds[i] === userId) {
// remove this user ID from this array
existingUserIds.splice(i, 1);
i--;
}
const strippedOldName = utils.removeHiddenChars(oldName);
const existingUserIds = roomState._displayNameToUserIds[strippedOldName];
if (existingUserIds) {
// remove this user ID from this array
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
roomState._displayNameToUserIds[strippedOldName] = filteredUserIDs;
}
roomState._displayNameToUserIds[oldName] = existingUserIds;
}
roomState._userIdsToDisplayNames[userId] = displayName;
if (!roomState._displayNameToUserIds[displayName]) {
roomState._displayNameToUserIds[displayName] = [];
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
if (strippedDisplayname) {
if (!roomState._displayNameToUserIds[strippedDisplayname]) {
roomState._displayNameToUserIds[strippedDisplayname] = [];
}
roomState._displayNameToUserIds[strippedDisplayname].push(userId);
}
roomState._displayNameToUserIds[displayName].push(userId);
}
/**
@@ -498,7 +794,8 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
/**
* Fires whenever a member is added to the members dictionary. The RoomMember
* will not be fully populated yet (e.g. no membership state).
* will not be fully populated yet (e.g. no membership state) but will already
* be available in the members dictionary.
* @event module:client~MatrixClient#"RoomState.newMember"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
+770 -132
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -39,6 +39,9 @@ limitations under the License.
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* an approximation and that the user should be seen as active 'now'
* @prop {string} _unstable_statusMessage The status message for the user, if known. This is
* different from the presenceStatusMsg in that this is not tied to
* the user's presence, and should be represented differently.
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
@@ -46,6 +49,7 @@ function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this._unstable_statusMessage = "";
this.displayName = userId;
this.rawDisplayName = userId;
this.avatarUrl = null;
@@ -179,6 +183,18 @@ User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* Manually set the user's status message.
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
* @fires module:client~MatrixClient#event:"User._unstable_statusMessage"
*/
User.prototype._unstable_updateStatusMessage = function(event) {
if (!event.getContent()) this._unstable_statusMessage = "";
else this._unstable_statusMessage = event.getContent()["status"];
this._updateModifiedTime();
this.emit("User._unstable_statusMessage", this);
};
/**
* The User class.
*/
+149 -29
View File
@@ -14,20 +14,75 @@ 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 {escapeRegExp, globToRegexp} from "./utils";
/**
* @module pushprocessor
*/
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
* @param {Object} client The Matrix client object to use
*/
function PushProcessor(client) {
const escapeRegExp = function(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const cachedGlobToRegex = {
// $glob: RegExp,
};
const matchingRuleFromKindSet = (ev, kindset, device) => {
@@ -75,7 +130,7 @@ function PushProcessor(client) {
rawrule.conditions.push({
'kind': 'event_match',
'key': 'room_id',
'pattern': tprule.rule_id,
'value': tprule.rule_id,
});
break;
case 'sender':
@@ -85,7 +140,7 @@ function PushProcessor(client) {
rawrule.conditions.push({
'kind': 'event_match',
'key': 'user_id',
'pattern': tprule.rule_id,
'value': tprule.rule_id,
});
break;
case 'content':
@@ -152,9 +207,7 @@ function PushProcessor(client) {
return false;
}
const memberCount = Object.keys(room.currentState.members).filter(function(m) {
return room.currentState.members[m].membership == 'join';
}).length;
const memberCount = room.currentState.getJoinedMemberCount();
const m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) {
@@ -183,7 +236,10 @@ function PushProcessor(client) {
};
const eventFulfillsDisplayNameCondition = function(cond, ev) {
const content = ev.getContent();
let content = ev.getContent();
if (ev.isEncrypted() && ev.getClearContent()) {
content = ev.getClearContent();
}
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
@@ -207,35 +263,39 @@ function PushProcessor(client) {
};
const eventFulfillsEventMatchCondition = function(cond, ev) {
if (!cond.key) {
return false;
}
const val = valueForDottedKey(cond.key, ev);
if (!val || typeof val != 'string') {
return false;
}
let pat;
if (cond.key == 'content.body') {
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
if (cond.value) {
return cond.value === val;
}
const regex = new RegExp(pat, 'i');
let regex;
if (cond.key == 'content.body') {
regex = createCachedRegex('(^|\\W)', cond.pattern, '(\\W|$)');
} else {
regex = createCachedRegex('^', cond.pattern, '$');
}
return !!val.match(regex);
};
const globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
});
return pat;
const createCachedRegex = function(prefix, glob, suffix) {
if (cachedGlobToRegex[glob]) {
return cachedGlobToRegex[glob];
}
cachedGlobToRegex[glob] = new RegExp(
prefix + globToRegexp(glob) + suffix,
'i', // Case insensitive
);
return cachedGlobToRegex[glob];
};
const valueForDottedKey = function(key, ev) {
@@ -304,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) {
@@ -323,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);
};
/**
@@ -372,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}
+26
View File
@@ -0,0 +1,26 @@
/*
Copyright 2018 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.
*/
export function randomString(len) {
let ret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; ++i) {
ret += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ret;
}
+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);
}
}

Some files were not shown because too many files have changed in this diff Show More