Compare commits

...

575 Commits

Author SHA1 Message Date
RiotRobot 340fa6c63e v10.0.0 2021-04-26 17:34:17 +01:00
RiotRobot 1177bf39a2 Prepare changelog for v10.0.0 2021-04-26 17:34:16 +01:00
RiotRobot e3ba08fbbc v10.0.0-rc.1 2021-04-21 16:40:23 +01:00
RiotRobot dd84e51161 Prepare changelog for v10.0.0-rc.1 2021-04-21 16:40:22 +01:00
Germain b435b582bd Merge pull request #1675 from matrix-org/gsouquet-powerlevels-perf
Restrict event emit for room members that had power levels changed
2021-04-20 11:57:18 +01:00
Germain Souquet d46021a05e Restrict event emit for room members that had power levels changed 2021-04-20 11:22:16 +01:00
Michael Telatynski 68206a6e19 Merge pull request #1669 from hannojg/patch-3
Fix sync with misconfigured push rules
2021-04-20 08:57:16 +01:00
Hanno J. Gödecke b13f5aebfd lint 2021-04-17 09:42:44 +02:00
Hanno J. Gödecke 338301bb5d fix: failure during sync 2021-04-17 09:34:31 +02:00
Hubert Chathi cbe9b59222 Merge pull request #1665 from uhoreg/dehydrated_device_missing_await
Add missing await
2021-04-14 10:47:09 -04:00
Hubert Chathi 276b52f0fe add missing await 2021-04-14 10:29:39 -04:00
J. Ryan Stinnett 09fac77ce0 Merge pull request #1642 from matrix-org/jryans/rework-linting
Migrate to `eslint-plugin-matrix-org`
2021-04-13 11:12:39 +01:00
J. Ryan Stinnett 102704e91a Migrate to eslint-plugin-matrix-org
This migrates to the new plugin form of our custom ESLint configs. As part of
this, some packages are de-duplicated, configs streamlined, etc.
2021-04-13 11:00:41 +01:00
Michael Telatynski c5fb351baa Merge pull request #1664 from matrix-org/t3chguy/fix/8665
Add missing event type enum for key verification done
2021-04-13 10:05:00 +01:00
Michael Telatynski 98f8d4414d Add missing event type enum for key verification done 2021-04-13 09:41:53 +01:00
Germain 1dddcd4925 Merge pull request #1663 from matrix-org/gsouquet-timeline-jumpiness 2021-04-12 15:00:49 +01:00
Germain Souquet 2666a271a5 fix tests when pendingEventsList does not exist 2021-04-12 14:51:45 +01:00
RiotRobot e277de6e3d Resetting package fields for development 2021-04-12 14:42:29 +01:00
RiotRobot daa17b3287 Merge branch 'master' into develop 2021-04-12 14:42:29 +01:00
RiotRobot c7f887131e v9.11.0 2021-04-12 14:39:33 +01:00
RiotRobot 58546b80d0 Prepare changelog for v9.11.0 2021-04-12 14:39:32 +01:00
Germain Souquet 466f749b71 fix typo 2021-04-12 14:38:27 +01:00
Germain Souquet 4c2a83c470 Add explanation for events persistence 2021-04-12 14:29:10 +01:00
Germain Souquet d534ab18c7 fix event filtering logic 2021-04-12 13:07:57 +01:00
Germain Souquet 64aaed833b Only persist encrypted events for encrypted rooms 2021-04-12 12:53:27 +01:00
Germain Souquet 2f05be599c undo changes to event#toJSON and persist encrypted events 2021-04-12 12:26:27 +01:00
Germain Souquet 7371f8dd3a Persist txnId to ensure idempotency 2021-04-12 09:55:01 +01:00
Germain Souquet 79aefe9707 Fix timeline jumpiness by setting correct txnId 2021-04-12 09:37:19 +01:00
Michael Telatynski 6bc80577ee Merge pull request #1661 from janpawellek/janpawellek-fix-addEventListener
Fix calling addEventListener if it does not exist
2021-04-12 09:15:19 +01:00
Germain 837764190f Merge pull request #1655 from matrix-org/gsouquet-persist-unsent-messages 2021-04-09 17:48:01 +01:00
Germain Souquet 61948d70e3 Merge branch 'develop' into gsouquet-persist-unsent-messages 2021-04-09 17:05:19 +01:00
Jan Pawellek 9fb0385694 Fix calling addEventListener if it does not exist
Some platforms (e.g. React Native) register global.window, but do not have global.window.addEventListener. In this case, this function should not be called.
2021-04-09 10:18:47 +02:00
RiotRobot 1c6459fe65 v9.11.0-rc.1 2021-04-07 12:55:25 +01:00
RiotRobot adaeb42416 Prepare changelog for v9.11.0-rc.1 2021-04-07 12:55:24 +01:00
J. Ryan Stinnett f1e1daa194 Merge pull request #1657 from matrix-org/jryans/cs-keys-test
Only try to cache private keys we know exist
2021-04-06 10:09:04 +01:00
J. Ryan Stinnett 401e89ef78 Only try to cache private keys we know exist
This tweaks https://github.com/matrix-org/matrix-js-sdk/pull/1649 to only try
caching private keys locally once we've confirmed they exist first. This is most
likely only an issue in tests, where we sometimes create only some but not all
keys.
2021-04-06 09:49:20 +01:00
Travis Ralston 59e0bd467c Merge pull request #1654 from SimonBrandner/terminate-screen-share
Properly terminate screen-share calls if NoUserMedia
2021-04-05 10:55:02 -06:00
David Baker 3820c15ecf Merge pull request #1652 from matrix-org/dbkr/attended_transfer
Attended transfer
2021-04-01 17:34:18 +01:00
Germain Souquet 26ef33e4f3 No this assign 2021-04-01 17:29:21 +01:00
Germain Souquet 0534a4ed1b prevent removePendingEvent being called when not in detached mode 2021-04-01 17:23:55 +01:00
Germain Souquet f29a24a915 specify TestClient when testing room model 2021-04-01 15:22:13 +01:00
Germain Souquet cecbcd941e Persist unsent messages for subsequent sessions 2021-04-01 10:59:16 +01:00
David Baker 6be99d6397 Terminate the other call too 2021-03-30 12:13:28 +01:00
J. Ryan Stinnett 4e5947af51 Merge pull request #1653 from matrix-org/jryans/cancel-security-key
Remove catch handlers in private key retrieval
2021-03-29 17:58:07 +01:00
RiotRobot 4204b2170a Resetting package fields for development 2021-03-29 13:28:09 +01:00
RiotRobot 0a5ad489b6 Merge branch 'master' into develop 2021-03-29 13:28:08 +01:00
RiotRobot 5de34a5c99 v9.10.0 2021-03-29 13:25:27 +01:00
RiotRobot 08da6b8800 Prepare changelog for v9.10.0 2021-03-29 13:25:27 +01:00
Šimon Brandner 02b283be78 Properly terminate screenshare calls if NoUserMedia
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-27 09:13:44 +01:00
J. Ryan Stinnett 10c49c0fd1 Remove catch handlers in private key retrieval
This removes some catch blocks originally added by
https://github.com/matrix-org/matrix-js-sdk/pull/1472 so that higher level
operations can handle them as needed.

Part of https://github.com/vector-im/element-web/issues/15584
2021-03-26 17:57:23 +00:00
David Baker 9ecc0f5d95 Terminate calls once we've replaced them 2021-03-26 13:52:23 +00:00
Travis Ralston 972b59b99e Merge pull request #1651 from DantrazTrev/CallErrorFix
Fixed the media fail error on caller's side
2021-03-25 19:05:36 -06:00
David Baker 34bb05bd88 WIP attended transfer 2021-03-25 19:57:20 +00:00
Hubert Chathi 37fb21f726 Merge pull request #1640 from uhoreg/room-history-key-sharing2
Add function to share megolm keys for historical messages.
2021-03-25 14:18:12 -04:00
Ayush PS b42efa4a07 Fixed lint errors 2021-03-25 23:38:18 +05:30
Ayush PS 20b20738a7 Fixed a slight error back to orignal in ScreenShareCall 2021-03-25 23:02:44 +05:30
Ayush PS b28a191c4e Fixed the media fail error on caller's side 2021-03-25 22:43:18 +05:30
Hubert Chathi f92b620434 Merge branch 'develop' into room-history-key-sharing2 2021-03-25 12:24:42 -04:00
RiotRobot ae6e2cca27 v9.10.0-rc.1 2021-03-25 12:06:34 +00:00
RiotRobot bd920eef1f Prepare changelog for v9.10.0-rc.1 2021-03-25 12:06:34 +00:00
J. Ryan Stinnett bf25cb68da Merge pull request #1649 from matrix-org/jryans/get-keys-bootstrap-only
Cache cross-signing private keys if needed on bootstrap
2021-03-24 15:56:15 +00:00
J. Ryan Stinnett 0b063f6b8b Cache cross-signing private keys if needed on bootstrap
This is a revised version of
https://github.com/matrix-org/matrix-js-sdk/pull/1472 which was previously
reverted for causing security prompts to appear on device list sync. In this
version, we only allow private key requests (which are likely to trigger user
dialogs) if we are coming from the bootstrap path.

This allows sessions that have already synced cross-signing public keys but
never got the private keys for some reason to make forward progress when e.g.
the user triggers bootstrap from security settings.
2021-03-24 11:48:45 +00:00
Travis Ralston ed6d4e5f6c Merge pull request #1647 from SimonBrandner/dont-send-hangup
Don't send m.call.hangup if m.call.invite wasn't sent either
2021-03-22 00:42:04 -06:00
Travis Ralston accfa325b5 Merge pull request #1641 from NicolaiSoeborg/fix-registerGuest
docs: registerGuest()
2021-03-21 20:37:49 -06:00
Šimon Brandner b6ef8d95cd Don't send hangup if invite wasn't sent
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-21 21:29:00 +01:00
Travis Ralston c1144e3810 Merge pull request #1639 from Johennes/feature/chunk-device-keys
Download device keys in chunks of 250
2021-03-18 13:17:15 -06:00
Johannes Marbach 8663fd402b Download device keys in chunks of 250
Depending on the number of users in the request, the server might
overload. To prevent this, the download is broken into chunks of
250 users each. Additionally, no more than 3 requests are kicked off
at the same time to avoid running into rate limiting. Responses are
processed once all chunks have been downloaded.

Fixes: #1619

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2021-03-17 20:56:25 +01:00
David Baker f34052fd31 Merge pull request #1646 from matrix-org/dbkr/more_call_fixes
More VoIP connectivity fixes
2021-03-17 15:00:09 +00:00
David Baker 27d75a269f unintentional comment 2021-03-16 19:18:45 +00:00
David Baker d208a7fc5f Remove unintentionally committed stuff 2021-03-16 19:17:04 +00:00
David Baker 702e16e3df More VoIP connectivity fixes
* Don't ignore other candidates when we see a null one (continue
   rather than return)
 * await on addICECandidate()
 * Don't add ice candidates until we've set a remote description
 * More & better logging
2021-03-16 19:13:03 +00:00
Hubert Chathi 6381018658 add jsdoc and implementation for memory crypto store 2021-03-16 13:52:05 -04:00
Travis Ralston 12050b14f0 Merge pull request #1644 from SimonBrandner/fix-optional
Make selectDesktopCapturerSource param optional
2021-03-15 21:12:03 -06:00
Hubert Chathi 1c191b2278 use new terminology and field name from MSC 2021-03-15 22:49:43 -04:00
Nicolai Søborg 2d4a4f1736 docs: registerGuest 2021-03-15 22:46:43 +00:00
RiotRobot cd38fb9b4c Resetting package fields for development 2021-03-15 14:34:56 +00:00
RiotRobot 7941b16ec4 Merge branch 'master' into develop 2021-03-15 14:34:56 +00:00
RiotRobot 3ff517e76e v9.9.0 2021-03-15 14:31:58 +00:00
RiotRobot 9559b26310 Prepare changelog for v9.9.0 2021-03-15 14:31:57 +00:00
Šimon Brandner 56ea4b8741 Make selectDesktopCapturerSource param optional
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-13 15:03:36 +01:00
Hubert Chathi a489691151 various fixes 2021-03-11 17:30:05 -05:00
Nicolai Søborg 6dabfcda6f setGuest(true) when registerGuest()
Signed-off-by: Nicolai Søborg <git@xn--sb-lka.org>
2021-03-11 22:39:51 +01:00
J. Ryan Stinnett 0b7b35f800 Merge pull request #1632 from matrix-org/matthew/rework-cross-signing-login
Expose APIs needed for reworked cross-signing login flow
2021-03-11 12:54:18 +00:00
Hubert Chathi 0bfcb5071d fix test, lint 2021-03-10 20:04:34 -05:00
Hubert Chathi ceb162eb01 initial work on room history key sharing, take 2 2021-03-10 19:51:22 -05:00
RiotRobot 0ffdf7c0f1 v9.9.0-rc.1 2021-03-10 17:21:50 +00:00
RiotRobot 13b6db8eb4 Prepare changelog for v9.9.0-rc.1 2021-03-10 17:21:49 +00:00
J. Ryan Stinnett 481acb2a1a Merge pull request #1638 from matrix-org/jryans/rm-olm-profiling
Remove detailed Olm session logging
2021-03-10 12:44:22 +00:00
J. Ryan Stinnett 683092140d Remove OTK claim timeout logging 2021-03-10 12:43:45 +00:00
J. Ryan Stinnett 1bb8c2d1a5 Remove detailed Olm session logging
Now that we understand the Olm session deadlock, we shouldn't need this detailed
per-session logging.

Fixes https://github.com/vector-im/element-web/issues/16647
2021-03-10 12:43:45 +00:00
J. Ryan Stinnett 60fd3b0786 Remove extra space in log message 2021-03-10 11:25:44 +00:00
J. Ryan Stinnett cd4abc4e9b Disable crypto transaction profiling 2021-03-10 11:05:17 +00:00
Michael Telatynski e6a21cc487 Merge pull request #1637 from matrix-org/t3chguy/spaces4.5
Add space summary suggested only param
2021-03-10 10:55:27 +00:00
David Baker 6c5fc153bf Merge pull request #1634 from matrix-org/dbkr/check_turn_interval
Check TURN servers periodically, and at start of calls
2021-03-09 17:06:02 +00:00
David Baker 07f15b41a2 Don't start the timer if voip not supported 2021-03-09 14:39:21 +00:00
David Baker 8375638d76 Fix tests
Bit of a re-organisation so a peerconnection exists when the tests
go to mock things out. placeCall methods return promises to make this
possible.
2021-03-09 14:09:55 +00:00
J. Ryan Stinnett bed7543b46 Merge pull request #1624 from robintown/invite-reasons
Support sending invite reasons
2021-03-09 11:44:04 +00:00
Travis Ralston dc55236263 Merge pull request #1636 from matrix-org/dependabot/npm_and_yarn/elliptic-6.5.4
Bump elliptic from 6.5.3 to 6.5.4
2021-03-08 18:23:50 -07:00
dependabot[bot] 5f3427c5d1 Bump elliptic from 6.5.3 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 01:23:08 +00:00
Travis Ralston 66e5af185d Merge pull request #1635 from matrix-org/travis/media-customization
Add a function to get a room's MXC URI
2021-03-08 18:23:00 -07:00
Travis Ralston 0ff611e033 Enum and linter 2021-03-08 18:16:39 -07:00
Travis Ralston 737cadaabc Add a function to get a room's MXC URI
This matches the RoomMember function of the same name.
2021-03-08 18:13:14 -07:00
Matthew Hodgson 9fb2fbaeec factor out getDehydratedDevice 2021-03-09 00:09:22 +00:00
David Baker 51e817a3a2 This is in ms, not seconds 2021-03-08 18:54:50 +00:00
David Baker 59c93b59bf Check TURN servers periodically, and at start of calls
Hopefully this should make our turn-credential checking code a bit
more robust (and possibly fix a seconds / ms mismatch).
2021-03-08 18:49:25 +00:00
David Baker c18ef051fc Merge pull request #1633 from matrix-org/dbkr/stop_streams_if_call_ended
Stop streams if the call has ended
2021-03-08 17:01:31 +00:00
David Baker 1ac5c9acbd Stop streams if the call has ended
When we get user media, don't forget to close the streams if the
call's ended by the time we got media.
2021-03-08 16:55:48 +00:00
J. Ryan Stinnett a034ca171e Merge pull request #1631 from SimonBrandner/remove-export
Remove export keyword from global.d.ts
2021-03-08 14:11:37 +00:00
Matthew Hodgson 977682d37f fix lint 2021-03-08 09:24:25 +00:00
Matthew Hodgson 0bafe263d7 fix lint 2021-03-08 05:05:14 +00:00
Matthew Hodgson 1a8fced80e Merge branch 'develop' into matthew/rework-cross-signing-login 2021-03-08 04:59:40 +00:00
Matthew Hodgson 1c4d0b5e99 expose getDevice API 2021-03-08 04:59:29 +00:00
Matthew Hodgson 844a2b457c expose getDehydratedDevice API 2021-03-08 04:59:19 +00:00
Matthew Hodgson ccf06f2216 don't cancel ourselves when selecting a self-verification partner 2021-03-08 04:58:55 +00:00
Šimon Brandner f630a9f297 Remove export
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-06 16:14:12 +01:00
Michael Telatynski 2f71c93b53 Add space summary suggested only param 2021-03-05 16:12:39 +00:00
J. Ryan Stinnett 92032a17a8 Merge pull request #1445 from florianjacob/patch-1
Fix IndexedDB store creation example
2021-03-04 16:28:05 +00:00
David Baker e531456d42 Merge pull request #1613 from SimonBrandner/constraint-cleanup
An attempt to  cleanup how constraints are handled in calls
2021-03-03 15:03:53 +00:00
Šimon Brandner f0b2d2fe4d Null-check screenshareConstraints
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-03 15:38:49 +01:00
Šimon Brandner 427500220d Remove AudioVideo ConstraintsType
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-03 15:35:02 +01:00
Šimon Brandner 32e19ead74 Merge branch 'develop' into constraint-cleanup 2021-03-03 15:30:46 +01:00
J. Ryan Stinnett d11adb6f43 Merge pull request #1628 from matrix-org/jryans/opt-display-name
Extract display name patterns to constants
2021-03-03 11:44:30 +00:00
Travis Ralston f6155a50f6 Merge pull request #1630 from matrix-org/dependabot/npm_and_yarn/pug-code-gen-2.0.3
Bump pug-code-gen from 2.0.2 to 2.0.3
2021-03-02 21:55:39 -07:00
dependabot[bot] 4efee9445d Bump pug-code-gen from 2.0.2 to 2.0.3
Bumps [pug-code-gen](https://github.com/pugjs/pug) from 2.0.2 to 2.0.3.
- [Release notes](https://github.com/pugjs/pug/releases)
- [Commits](https://github.com/pugjs/pug/compare/pug-code-gen@2.0.2...pug@2.0.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-03 03:35:37 +00:00
J. Ryan Stinnett 20746a433f Extract display name patterns to constants
This changes to constant patterns for calculating display names, which cuts out
~18% of the time spent and reduces GC pressure as well.
2021-03-02 17:52:57 +00:00
J. Ryan Stinnett 31dacc4206 Merge pull request #1627 from matrix-org/jryans/olm-session-deadlock
Avoid deadlocks when ensuring Olm sessions for devices
2021-03-02 14:35:04 +00:00
J. Ryan Stinnett 88e5c59a85 Fix lint warning on OTK result variable 2021-03-02 13:03:12 +00:00
J. Ryan Stinnett cf74920b36 Remove redundant Olm session in progress deletion
This removes extra steps that duplicated deletion of an in progress Olm session.
Resolving the promise handles removing the session from the in progress set, so
there's no need to do it again. There's also no need to delete from
`resolveSession`, as it's okay to resolve a promise multiple times.
2021-03-02 12:58:09 +00:00
J. Ryan Stinnett 972c900b58 Remove unused support for rejecting in progress Olm sessions
This removes the unused `reject` path for in progress Olm sessions to simplify
understanding the code.
2021-03-02 12:55:43 +00:00
J. Ryan Stinnett 12d5fd79f7 Avoid deadlocks when ensuring Olm sessions for devices
This reworks tracking the Olm sessions a particular task is updating to avoid
deadlocks. By ensuring we synchronously mark all sessions a task cares about as
in progress from the start, we know that no other tasks will own updating a
session in common, which avoids deadlocks across multiple tasks that might be
working on a shared set of devices.

Fixes https://github.com/vector-im/element-web/issues/16194
2021-03-02 12:50:49 +00:00
J. Ryan Stinnett a29f6979b2 Merge pull request #1626 from matrix-org/jryans/replacement-senders
Filter out edits from other senders in history
2021-03-02 12:33:21 +00:00
J. Ryan Stinnett 3a7146c77b Only log claim timeouts when a time was provided
This avoids logging immediately on various code paths (including tests) where no
timeout value is supplied.
2021-03-02 12:22:58 +00:00
J. Ryan Stinnett b178d8f629 Filter out edits from other senders in history
We currently don't support edits from other senders, but the server may not
filter them, so we filter them here on the client.
2021-03-02 12:15:27 +00:00
Šimon Brandner 0c94ee62a3 Pass in selectDesktopCapturerSource()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-02 13:00:57 +01:00
Šimon Brandner e7562898cd Add getScreenshareContraints()
This is nicer since we avoid some async functions

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-03-02 12:58:45 +01:00
RiotRobot fb73ab6878 Resetting package fields for development 2021-03-01 12:48:30 +00:00
RiotRobot 38f978791d Merge branch 'master' into develop 2021-03-01 12:48:30 +00:00
RiotRobot 5dd60de57d v9.8.0 2021-03-01 12:44:55 +00:00
RiotRobot 5efbfc2dba Prepare changelog for v9.8.0 2021-03-01 12:44:54 +00:00
J. Ryan Stinnett fcd1dbad89 Merge pull request #1618 from robintown/fix-content-helpers-export
Fix ContentHelpers export
2021-03-01 11:13:10 +00:00
J. Ryan Stinnett ad521bf4c2 Merge pull request #1621 from matrix-org/jryans/megolm-logs-2021-02-26
Add logging to in progress Olm sessions
2021-02-28 17:52:24 +00:00
J. Ryan Stinnett 8152fa44e0 Add more logging scopes to session IDs
This uses prefix chaining to correlate several scopes together.
2021-02-28 17:15:07 +00:00
J. Ryan Stinnett e217bf9e37 Enable prefixed loggers to chain 2021-02-28 17:15:07 +00:00
David Baker bfad21f811 Merge pull request #1623 from matrix-org/dbkr/ice_candidate_buffer
Don't ignore ICE candidates received before offer/answer
2021-02-27 15:11:26 +00:00
David Baker 81e68abce3 Merge pull request #1622 from matrix-org/dbkr/candidate_retries
Better handling of send failures on VoIP events
2021-02-27 15:11:00 +00:00
David Baker 7963bb352d Merge pull request #1620 from matrix-org/dbkr/log_turn_cred_expiry
Log when turn creds expire
2021-02-27 15:09:54 +00:00
Michael Telatynski 14d3882059 Merge pull request #1563 from matrix-org/t3chguy/spaces
Initial Spaces [MSC1772] support
2021-02-26 22:08:09 +00:00
Robin Townsend dede508e89 Support sending invite reasons
Added as the final argument to `invite` in order to keep backwards
compatibility.

Signed-off-by: Robin Townsend <robin@robin.town>
2021-02-26 16:46:18 -05:00
David Baker ea39b69f65 Don't ignore ICE candidates received before offer/answer
The main bug here was a race on the callee side because we await-ed
on setRemoteDescription before setting the opponent party ID, and
while we were await-ing, the callEventHandler could give us candidate
events which we'd duly ignore because we thought the party ID didn't
match.

This also meant that any candidates that arrived before the answer
would have been ignored. Save these up by party ID and then add the
ones from the party ID that we pick once the answer comes in.

Also fix the confusion on party IDs where we weren't sure whether
we hadn't picked an opponent or we'd picked an opponent without a
party ID. It's now undefined for the former and null for the latter,
as it claims to be in the comment.
2021-02-26 21:25:52 +00:00
David Baker eafecd36bc Better handling of send failures on VoIP events
Don't leave candidate message sin the queue, abort if we fail to
send the invite.
2021-02-26 18:42:05 +00:00
J. Ryan Stinnett 198c9a2507 Add logging to in progress Olm sessions
It seems like this might be where
https://github.com/vector-im/element-web/issues/16194 is deadlocking.
2021-02-26 17:27:06 +00:00
David Baker d07563013b Log when turn creds expire 2021-02-26 14:47:27 +00:00
Michael Telatynski 9e967832cd Update space summary API unstable prefix 2021-02-26 10:37:09 +00:00
Michael Telatynski bfe1987cd9 Add Spaces event types from MSC1772 2021-02-26 10:35:02 +00:00
Robin Townsend 1045538f1f Fix ContentHelpers export
This was previously exporting a promise, since it called the import
function manually but didn't await the result. However, since we have
Babel we can just use the new export … as … from syntax instead.

Signed-off-by: Robin Townsend <robin@robin.town>
2021-02-25 14:41:48 -05:00
J. Ryan Stinnett fccf08edcf Merge pull request #1617 from matrix-org/jryans/crypto-store-logging
Add logging to crypto store transactions
2021-02-25 16:58:45 +00:00
J. Ryan Stinnett f43fe366b5 Add logging to crypto store transactions
We churn through a huge number of crypto store transactions during startup,
which may be the cause of the symptoms in
https://github.com/vector-im/element-web/issues/16194.
2021-02-25 16:49:49 +00:00
Michael Telatynski 0f75f2ef9c Add base API for Space Summary MSC2946 2021-02-25 13:12:22 +00:00
Michael Telatynski 2cdc68f9c3 Merge pull request #1610 from matrix-org/t3chguy/spaces2
Room helper for getting type and checking if it is a space room
2021-02-25 11:30:49 +00:00
RiotRobot 6a7d58e22e v9.8.0-rc.1 2021-02-24 17:24:17 +00:00
RiotRobot 203829c1cd Prepare changelog for v9.8.0-rc.1 2021-02-24 17:24:16 +00:00
J. Ryan Stinnett b55e6c4ef0 Merge pull request #1615 from matrix-org/jryans/megolm-logs-2021-02-22
Optimise prefixed logger
2021-02-23 17:46:57 +00:00
J. Ryan Stinnett 8d779e8aec Optimise prefixed logger
Tweak the prefixed logger to only do the setup work the first time.
2021-02-23 16:32:10 +00:00
J. Ryan Stinnett dd1d48f688 Merge pull request #1614 from matrix-org/jryans/megolm-logs-2021-02-22
Add debug logs to encryption prep, take 3
2021-02-23 16:07:39 +00:00
J. Ryan Stinnett 8d14dc9ee3 Add debug logs to encryption prep, take 3
This continues adding more logs to work out the root cause of
https://github.com/vector-im/element-web/issues/16194.

Somehow, we're getting stuck while sharing keys with new sessions.
2021-02-23 14:22:44 +00:00
Šimon Brandner 5849ea8e63 Add AudioVideo constraint type
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 12:25:20 +01:00
Šimon Brandner 20afebf339 Set video to true
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 12:23:49 +01:00
Šimon Brandner 20eaba191e Simplifie placeScreenSharingCall()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 11:12:16 +01:00
Šimon Brandner ba58d3c544 Add screenshare type to getUserMediaContraints()
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-23 10:47:43 +01:00
David Baker a8b9d8e3ae Merge pull request #1612 from matrix-org/dbkr/jitsi_conference_captialised
Add functions for upper & lowercase random strings
2021-02-22 17:33:35 +00:00
David Baker 83d1e61b2f Add functions for upper & lowercase random strings 2021-02-22 16:47:16 +00:00
Michael Telatynski 8e0fc8d460 Room helper for getting type and checking if it is a space room 2021-02-19 14:21:22 +00:00
Michael Telatynski f547fa732f Merge pull request #1609 from matrix-org/t3chguy/spaces1
Room helpers for invite permissions and join rules
2021-02-18 18:02:31 +00:00
Michael Telatynski e24b1519a4 Merge pull request #1606 from SimonBrandner/fix-log
Fixed wording in "Adding video track with id" log
2021-02-18 18:00:28 +00:00
Michael Telatynski 3028fe9c87 Improve room documentation 2021-02-18 14:52:32 +00:00
Michael Telatynski 0b970b05b6 Wire up helpers for checking if a user can invite to a room and getting its join rule 2021-02-18 14:52:23 +00:00
Šimon Brandner f7bfb1e49e Fixed log (audio -> video)
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-17 20:51:43 +01:00
J. Ryan Stinnett 371ca009e9 Merge pull request #1605 from matrix-org/jryans/more-megolm-logging
Add more debug logs to encryption prep
2021-02-17 13:56:46 +00:00
J. Ryan Stinnett 4a0f848551 Add more debug logs to encryption prep
This continues work from https://github.com/matrix-org/matrix-js-sdk/pull/1580
and adds more logging, including specialised logging for a potential cause of
https://github.com/vector-im/element-web/issues/16194.

So far, it seems clear that something's going wrong in the "sharing keys with
new Olm session" step.
2021-02-17 13:45:26 +00:00
David Baker 5e8b7b2a62 Merge pull request #1604 from matrix-org/dbkr/ice_candidate_pool_size
Add option to set ice candidate pool size
2021-02-16 16:01:53 +00:00
David Baker 0f27b703bd Should be optional 2021-02-16 15:51:10 +00:00
David Baker 61e19c30cb Add option to set ice candidate pool size 2021-02-16 15:47:48 +00:00
RiotRobot c82bc35202 Resetting package fields for development 2021-02-16 10:58:15 +00:00
RiotRobot 65934227c3 Merge branch 'master' into develop 2021-02-16 10:58:15 +00:00
RiotRobot 7519becd43 v9.7.0 2021-02-16 10:55:42 +00:00
RiotRobot fe83c15bc6 Prepare changelog for v9.7.0 2021-02-16 10:55:41 +00:00
J. Ryan Stinnett 07e6b47fa7 Merge pull request #1601 from SimonBrandner/cancel-call-if-no-source
Cancel call if no source was selected
2021-02-11 12:29:57 +00:00
RiotRobot b026e1c2f7 v9.7.0-rc.1 2021-02-10 15:54:02 +00:00
RiotRobot f8194d9418 Prepare changelog for v9.7.0-rc.1 2021-02-10 15:54:01 +00:00
Šimon Brandner 1ecd7f274f Cancel call if no source was selected
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-10 07:49:03 +01:00
David Baker 66bf0ec7af Merge pull request #1600 from SimonBrandner/handle-undefined-peerconn
Handle undefined peerconn
2021-02-09 16:29:46 +00:00
Šimon Brandner 1b22df2b7b Handle undefined peerconn
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-09 14:42:34 +01:00
David Baker 9f993f1f67 Merge pull request #1599 from matrix-org/dbkr/reemitter_dont_throw_if_no_error_handler
ReEmitter: Don't throw if no error handler is attached
2021-02-08 19:44:03 +00:00
David Baker 975518bd88 ReEmitter: Don't throw if no error handler is attached
As hopefully explained by lengthy comment

Fixes https://github.com/matrix-org/matrix-js-sdk/issues/1569
2021-02-08 19:37:17 +00:00
David Baker 66a863456c Merge pull request #1598 from matrix-org/dbkr/reemitter_ts
Convert ReEmitter to TS
2021-02-08 19:15:08 +00:00
David Baker 91290c0d25 Actually add the test 2021-02-08 19:09:32 +00:00
David Baker 8a23e89c87 Convert ReEmitter to TS
And also add a test so I can be confident it's actually doing the
same thing.

NB. There was some logic there previously to reduce the number of
bound functions that had to be kept around, but it subsequently
started adding the source object as the last arg, at which point
there's now one bound function in memory per re-emitted event name
(plus the previous per-event-name handlers). This reduces it so it's
just one per re-emitted event name, so still could be quite a few,
but fewer than before.
2021-02-08 19:04:23 +00:00
Michael Telatynski 9e9cf85ba1 Merge pull request #1597 from rherrmann/patch-1
Fix typo in main readme
2021-02-08 17:18:29 +00:00
David Baker 3dd365bbea Merge pull request #1596 from matrix-org/dbkr/rogue_plus
Remove rogue plus character
2021-02-08 16:49:47 +00:00
Rüdiger Herrmann 33a824b980 Fix typo in main readme
Signed-off-by: Rüdiger Herrmann <ruediger.herrmann@gmx.de>
2021-02-08 17:41:48 +01:00
David Baker 8571884304 Remove rogue plus character
Apparently this is perfectly valid javascript and somehow casts
this.callId to a number... possibly it's ignoring the whitespace
and trating it as `++this.callId`?
2021-02-08 16:27:38 +00:00
David Baker 4f1067e66c Merge pull request #1595 from matrix-org/dbkr/call_id_nan
Fix call ID NaN
2021-02-08 16:18:01 +00:00
David Baker 7b5b851db0 Fix call ID NaN
We were seeing call IDs of NaN in the wild somehow... hopefully this
should make sure they're all actually strings.
2021-02-08 16:12:39 +00:00
J. Ryan Stinnett ed0be0cf84 Merge pull request #1594 from matrix-org/jryans/electron-type-merge
Fix Electron type merging
2021-02-08 15:25:14 +00:00
J. Ryan Stinnett d3775e5cb1 Fix Electron type merging
This changes to an interface for Electron types so that other layers can merge
in further APIs as needed.
2021-02-08 15:13:00 +00:00
J. Ryan Stinnett 2c8f658810 Merge pull request #1593 from SimonBrandner/fix-browser-screens-share
Fix browser screen share
2021-02-08 14:56:57 +00:00
Šimon Brandner 5c52f5f579 Fix browser screen share
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-08 15:48:30 +01:00
David Baker 0a81bb3fdc Merge pull request #1570 from SimonBrandner/fix-screen-sharing
Fix desktop Matrix screen sharing
2021-02-08 13:54:38 +00:00
J. Ryan Stinnett f33196bc51 Merge pull request #1591 from matrix-org/jryans/pos-wait
Guard against confused server retry times
2021-02-05 17:42:19 +00:00
J. Ryan Stinnett 6beb90a835 Guard against confused server retry times
If a server happens to give a negative retry time, this would be passed to
`setTimeout`, and browsers interpret negative values as `0`, meaning "as soon as
possible", so we then start looping infinitely with no delay.
2021-02-05 17:37:40 +00:00
J. Ryan Stinnett 9d45e6acd6 Merge pull request #1589 from SimonBrandner/decrypt-redaction-reason
Decrypt redaction events
2021-02-04 16:10:59 +00:00
Šimon Brandner 516c464458 Call decryptEvent recursively
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-04 16:59:26 +01:00
RiotRobot 6ad3fb16b3 Resetting package fields for development 2021-02-03 12:01:13 +00:00
RiotRobot 277fdd9b8c Merge branch 'master' into develop 2021-02-03 12:01:13 +00:00
RiotRobot 7d56993b39 v9.6.0 2021-02-03 11:58:27 +00:00
RiotRobot 4e1442fcf6 Prepare changelog for v9.6.0 2021-02-03 11:58:27 +00:00
Michael Telatynski 4777bf3e75 Merge pull request #1588 from matrix-org/t3chguy/cherrypick/1587
[Release] Fix edge cases with peeking where a room is re-peeked
2021-02-01 13:01:01 +00:00
Šimon Brandner 14cd37ec56 Decrypt redaction events
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-02-01 13:52:56 +01:00
Michael Telatynski 8bcdfd50c9 Fix edge cases with peeking where a room is re-peeked
but two Room instances are created and things get duplicated
2021-02-01 12:30:04 +00:00
Michael Telatynski 6776df8e80 Merge pull request #1587 from matrix-org/t3chguy/peeking
Fix edge cases with peeking where a room is re-peeked
2021-02-01 12:26:35 +00:00
Michael Telatynski fbec079c9b Fix edge cases with peeking where a room is re-peeked
but two Room instances are created and things get duplicated
2021-01-31 16:08:03 +00:00
RiotRobot 93f6bc3780 v9.6.0-rc.1 2021-01-29 17:20:04 +00:00
RiotRobot dde8f23cc3 Prepare changelog for v9.6.0-rc.1 2021-01-29 17:20:03 +00:00
RiotRobot 7cfbd0da95 Merge branch 'master' into develop 2021-01-26 11:42:21 +00:00
RiotRobot a27ddfaaaf v9.5.1 2021-01-26 11:39:39 +00:00
RiotRobot b53f616015 Prepare changelog for v9.5.1 2021-01-26 11:39:38 +00:00
J. Ryan Stinnett 22dc175879 Merge pull request #1585 from matrix-org/dbkr/voip-v0-release
[Release] Fix compatibility with v0 calls
2021-01-26 11:24:07 +00:00
David Baker 5f23e4699c We were using undefined here too 2021-01-26 11:17:44 +00:00
David Baker a1bd258a7b Remove unintentional commit 2021-01-26 11:17:44 +00:00
David Baker 39a9c54589 Fix compatability with v0 calls
https://github.com/matrix-org/matrix-js-sdk/pull/1567 introduced a
bug where we'd leave opponentPartyId undefined, but we compared it
to null later when testing for its presence.

Fixes https://github.com/vector-im/element-web/issues/16239
2021-01-26 11:17:44 +00:00
David Baker 5f68370e07 Merge pull request #1584 from matrix-org/dbkr/callstats
Add support for getting call stats
2021-01-26 10:53:53 +00:00
David Baker dae2de703d Add support for getting call stats
Also add a few 'public' annotations
2021-01-26 09:40:20 +00:00
David Baker fa19c40868 Merge pull request #1583 from matrix-org/dbkr/fix_v0_compat
Fix compatibility with v0 calls
2021-01-25 17:29:59 +00:00
David Baker 1df69d259a We were using undefined here too 2021-01-25 16:34:28 +00:00
David Baker 90dda0ca68 Remove unintentional commit 2021-01-25 16:13:13 +00:00
David Baker e2d138cac6 Fix compatability with v0 calls
https://github.com/matrix-org/matrix-js-sdk/pull/1567 introduced a
bug where we'd leave opponentPartyId undefined, but we compared it
to null later when testing for its presence.

Fixes https://github.com/vector-im/element-web/issues/16239
2021-01-25 16:09:39 +00:00
J. Ryan Stinnett 15f968d5f8 Merge pull request #1582 from matrix-org/jryans/upgrade-deps-2021-01
Upgrade deps 2021-01
2021-01-22 10:16:41 +00:00
Šimon Brandner 4a3b68de8f Merge branch 'develop' into fix-screen-sharing 2021-01-21 19:15:33 +01:00
David Baker f6aec7f763 Merge pull request #1581 from matrix-org/dbkr/log_the_call_id
Log the call ID when logging that we've received VoIP events
2021-01-21 17:59:49 +00:00
J. Ryan Stinnett 212b6c3a0f Resolve linting errors after upgrades 2021-01-20 13:54:45 +00:00
J. Ryan Stinnett 820256d451 Nested upgrades via yarn upgrade 2021-01-20 11:07:11 +00:00
J. Ryan Stinnett 3aba538db3 Update to latest deps 2021-01-20 11:05:17 +00:00
David Baker 4820cf8cac Log call ID here too 2021-01-19 19:28:08 +00:00
David Baker c289effba0 Log the call ID when logging that we've received VoIP events
Should make the logs a bit clearer
2021-01-19 18:11:41 +00:00
David Baker 3edccf496a Merge pull request #1579 from matrix-org/dbkr/foxes_dont_like_to_be_held
Fix extra negotiate message in Firefox
2021-01-19 17:51:35 +00:00
J. Ryan Stinnett 97b4171b3e Merge pull request #1580 from matrix-org/jryans/debug-encryption-prep
Add debug logs to encryption prep
2021-01-19 15:47:34 +00:00
J. Ryan Stinnett 4a073a7ba5 Fix lint 2021-01-19 15:36:08 +00:00
J. Ryan Stinnett 9f275d57a9 Add debug logs to encryption prep
This extra debug logs may help isolate the cause of
https://github.com/vector-im/element-web/issues/16194.

These changes also fix a related (but most likely different) failure mode: if a
failure occurred in the `encryptionPreparation` async task, we would skip trying
to prepare in all future attempts for that room. This change ensures prep
failures are logged and we resume prep attempts on the next call from the
application.
2021-01-19 15:28:28 +00:00
David Baker d23bbaeb06 Fix extra negotiate message in Firefox
Hopefully explained by the comments: Firefox sees that it's been
put on hold and tries to negotiate itself off hold again.

Fixes https://github.com/vector-im/element-web/issues/16190
2021-01-19 12:25:36 +00:00
J. Ryan Stinnett c64f7a9ec4 Merge pull request #1578 from tzyl/tzyl/get-presence-endpoint
Expose getPresence endpoint
2021-01-19 11:03:02 +00:00
RiotRobot 214a9df382 Resetting package fields for development 2021-01-18 15:06:36 +00:00
RiotRobot 90f6620f1e Merge branch 'master' into develop 2021-01-18 15:06:36 +00:00
RiotRobot 45f3a2f909 v9.5.0 2021-01-18 15:04:00 +00:00
RiotRobot 5904378170 Prepare changelog for v9.5.0 2021-01-18 15:03:59 +00:00
tzyl f6e8048d9e Expose getPresence endpoint 2021-01-18 10:17:46 +00:00
Hubert Chathi 5afca17d27 Merge pull request #1577 from uhoreg/always_queue_backup
Queue keys for backup even if backup isn't enabled yet
2021-01-15 12:28:06 -05:00
J. Ryan Stinnett 2d7f5ae279 Merge pull request #1576 from matrix-org/jryans/forbidden-turn
Stop retrying TURN access when forbidden
2021-01-15 10:06:18 +00:00
Hubert Chathi 458384d658 queue keys for backup even if backup isn't enabled yet
We may not have managed to set up the backup yet when we get keys.  So we should
unconditionally queue up the keys for backup, so that when the backup is set up,
they will be sent instead of dropped.
2021-01-14 19:55:02 -05:00
J. Ryan Stinnett 159b98132d Stop retrying TURN access when forbidden
If we're not allowed to have TURN access, there's no reason to ask in a loop.
2021-01-14 17:49:15 +00:00
Šimon Brandner 349bb2730a Update thumbnails
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-01-14 12:44:50 +01:00
Šimon Brandner c13813348d Merge branch 'develop' into fix-screen-sharing 2021-01-14 08:35:58 +01:00
Šimon Brandner 26e70d6b30 Use contextBridge
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2021-01-14 08:34:46 +01:00
David Baker f6d3b50b08 Merge pull request #1573 from matrix-org/dbkr/dtmf
Add DTMF sending support
2021-01-13 13:07:18 +00:00
RiotRobot 50ee489079 v9.5.0-rc.1 2021-01-13 12:54:37 +00:00
RiotRobot b60e5f40ab Prepare changelog for v9.5.0-rc.1 2021-01-13 12:54:36 +00:00
David Baker 5b1fdb7b37 Typo
Co-authored-by: J. Ryan Stinnett <jryans@gmail.com>
2021-01-13 11:37:30 +00:00
David Baker 65d4015300 Merge pull request #1574 from matrix-org/dbkr/dont_log_on_no_webrtc
Don't log if no WebRTC
2021-01-12 18:03:27 +00:00
David Baker b692cd109e Don't log if no WebRTC
as hopefully explained in comment
2021-01-12 17:58:35 +00:00
Will Hunt 0f90f055ba Merge pull request #1417 from matrix-org/hs/shared-rooms-api
Add _unstable_getSharedRooms
2021-01-11 22:38:56 +00:00
J. Ryan Stinnett 28d5ce288c Merge pull request #1568 from matrix-org/dependabot/npm_and_yarn/node-notifier-8.0.1
Bump node-notifier from 8.0.0 to 8.0.1
2021-01-11 13:12:59 +00:00
Šimon Brandner c701bf279f Merge branch 'develop' into fix-screen-sharing 2021-01-05 20:48:34 +01:00
David Baker 6039066e7f Merge pull request #1567 from matrix-org/dbkr/ignore_party_id_v0_3
Ignore party ID if opponent is v0
2021-01-05 17:23:15 +00:00
David Baker c9a2f8b170 Merge pull request #1566 from matrix-org/dbkr/call_transfer_2
Basic call transfer initiation support
2021-01-05 17:22:08 +00:00
David Baker b34a36d853 Rename other supportsTransfers 2021-01-05 17:11:49 +00:00
David Baker f8f76f6806 Add DTMF sending support 2021-01-04 19:58:12 +00:00
David Baker e25ae546fc Merge pull request #1572 from matrix-org/dbkr/room_version_6
Room version 6 is now a thing
2021-01-04 15:23:01 +00:00
David Baker 5b73bf3e5d Room version 6 is now a thing
MSC2788 (https://github.com/matrix-org/matrix-doc/pull/2788) etc
2021-01-04 15:16:09 +00:00
Hubert Chathi c16b093bd7 Merge pull request #1571 from Sorunome/soru/receive-real-key-later
Store keys with same index but better trust level
2020-12-30 11:13:30 -05:00
Sorunome e406f32386 Store keys with same index but better trust level 2020-12-29 17:02:01 +01:00
Šimon Brandner c4e7c149a4 Type cleanup
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2020-12-26 18:09:38 +01:00
Šimon Brandner f91edfabbb Change formatting
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2020-12-26 16:58:08 +01:00
Šimon Brandner f410004d45 Clean up
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2020-12-26 08:50:46 +01:00
Šimon Brandner 49e238d580 Get screen-sharing working, somehow
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
2020-12-26 08:32:50 +01:00
David Baker 489d188966 Add a separate file for types 2020-12-22 15:37:58 +00:00
J. Ryan Stinnett 79fb7bab0b Merge pull request #1561 from matrix-org/jryans/prepublish-only
Use TypeScript source for development, swap to build during release
2020-12-22 13:48:15 +00:00
J. Ryan Stinnett c410954bad Replace dot-json with jq equivalents 2020-12-22 13:39:42 +00:00
J. Ryan Stinnett 53a8a7d50f Remove temporary prepare script
This was as a temporary measure during the last release so that downstream
layers would still have types as they expect.
2020-12-22 11:49:59 +00:00
J. Ryan Stinnett 1c7f95c0ee Use TypeScript source for development, swap to build during release
This changes the JS SDK to point `main` to TypeScript source and remove any
indication of `typings`. For local development and CI workflows, it means many
steps can run without building first, which saves lots of time.

During release, we still build for Node and browsers as before. The release
script adjusts the `main` and `typings` fields before publishing and
distribution to point to the built output for those that use them.
2020-12-22 11:48:57 +00:00
RiotRobot 1717fcf499 Merge branch 'master' into develop 2020-12-21 17:34:12 +00:00
RiotRobot b25453cf87 v9.4.1 2020-12-21 17:31:21 +00:00
RiotRobot 07b596bf30 Prepare changelog for v9.4.1 2020-12-21 17:29:58 +00:00
RiotRobot 1166947c21 Further tweaks to get all layers building again 2020-12-21 17:29:22 +00:00
dependabot[bot] 6e7b9ca6c0 Bump node-notifier from 8.0.0 to 8.0.1
Bumps [node-notifier](https://github.com/mikaelbr/node-notifier) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/mikaelbr/node-notifier/releases)
- [Changelog](https://github.com/mikaelbr/node-notifier/blob/v8.0.1/CHANGELOG.md)
- [Commits](https://github.com/mikaelbr/node-notifier/compare/v8.0.0...v8.0.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-21 16:54:57 +00:00
RiotRobot 5b1e3537cc Merge branch 'master' into develop 2020-12-21 16:53:35 +00:00
RiotRobot 44843d418d v9.4.0 2020-12-21 16:50:47 +00:00
RiotRobot a103ffa038 Prepare changelog for v9.4.0 2020-12-21 16:50:46 +00:00
David Baker 712335789e Yes, thank you test, you've made your point 2020-12-21 16:30:16 +00:00
David Baker cdf8186f44 and also in the other direction 2020-12-21 16:30:08 +00:00
David Baker d06942d602 Ignore party ID if opponent is v0 2020-12-21 16:30:00 +00:00
RiotRobot 45ac3a60dc Revert "Remove postinstall script which also runs as a dependency"
This temporarily reverts commit 853363fdf5, which
will cause trouble for downstream layers without additional changes.
2020-12-21 16:23:16 +00:00
David Baker 4f244da3ec Makean interface for the replaces event 2020-12-21 15:42:34 +00:00
David Baker 4eefa05d3f Another typo 2020-12-21 15:20:45 +00:00
David Baker 40fb31099a Typo 2020-12-21 15:18:21 +00:00
David Baker bcd85f5397 Rename to supportsCallTransfer
to be less ambiguous
2020-12-21 15:14:59 +00:00
David Baker 89aeda45c6 Basic call transfer initiation support
Re-commit of f3ee164a7d after I accidentally
merged to develop
2020-12-21 13:54:16 +00:00
David Baker 7581e5ffdc Merge pull request #1565 from matrix-org/revert-1559-dbkr/ignore_party_id_v0
Revert "Ignore party ID if opponent is v0"
2020-12-21 13:48:40 +00:00
David Baker 7046fa3224 Revert "Ignore party ID if opponent is v0" 2020-12-21 13:48:06 +00:00
David Baker ef392785e8 Merge pull request #1559 from matrix-org/dbkr/ignore_party_id_v0
Ignore party ID if opponent is v0
2020-12-21 13:47:33 +00:00
David Baker 5bd029115c Merge pull request #1562 from matrix-org/dbkr/honour_reject_from_self
Honour a call reject event from another of our own devices
2020-12-18 17:24:32 +00:00
David Baker b6f42b25dd Honour a call reject event from another of our own devices
Fixes a bug where the call would ring again when you refreshed,
even though you'd previously rejected it.
2020-12-18 13:45:13 +00:00
RiotRobot 75dd9625a0 v9.4.0-rc.2 2020-12-16 15:53:01 +00:00
RiotRobot fd6110679e Prepare changelog for v9.4.0-rc.2 2020-12-16 15:53:00 +00:00
J. Ryan Stinnett c761019aca Merge pull request #1560 from matrix-org/jryans/rm-postinstall
Remove `postinstall` script which also runs as a dependency
2020-12-16 15:50:03 +00:00
J. Ryan Stinnett 853363fdf5 Remove postinstall script which also runs as a dependency
It seems I misunderstood the `postinstall` script and had thought it would only
run when installing the project root. Instead, it seems to run also as a
dependency as well.

This change should be fine for release, but it does mean when developing
the JS SDK, you'll need to manually build. CI pipelines will also need to be
changed to call an extra build step.
2020-12-16 15:43:06 +00:00
RiotRobot f7753f8be3 v9.4.0-rc.1 2020-12-16 14:17:25 +00:00
RiotRobot d2dfa29556 Prepare changelog for v9.4.0-rc.1 2020-12-16 14:17:25 +00:00
David Baker c0a88b7f4e Yes, thank you test, you've made your point 2020-12-15 18:06:48 +00:00
David Baker 150e5fede4 and also in the other direction 2020-12-15 16:22:11 +00:00
David Baker bb3ec322fb Ignore party ID if opponent is v0 2020-12-15 16:21:14 +00:00
David Baker f3ee164a7d Basic call transfer initiation support
Sends an m.call.replaces event and flag for whether to advertise
support.

MSC2747 (https://github.com/matrix-org/matrix-doc/pull/2747)
2020-12-15 14:51:29 +00:00
David Baker 035cb9fe08 Merge pull request #1553 from matrix-org/dbkr/line_1_2
Fixes to support line 1 / 2
2020-12-11 10:26:23 +00:00
David Baker 58d0018174 Merge remote-tracking branch 'origin/develop' into dbkr/line_1_2 2020-12-10 14:00:47 +00:00
David Baker 52ed0f8615 Merge pull request #1549 from matrix-org/dbkr/hold_ui
Add API for listening to remote hold status, advertise VoIP V1
2020-12-10 12:30:46 +00:00
David Baker 2a46513dfd remove outdated comment
(it did work - I just was not checking the flag in the right place)
2020-12-09 11:21:23 +00:00
David Baker 907567182d Mute speaker when putting a call on hold 2020-12-08 19:45:11 +00:00
David Baker 46cebcd1ca Merge pull request #1555 from matrix-org/dbkr/fix_hangup_from_other_client
A hangup from another client is still valid
2020-12-08 19:02:00 +00:00
David Baker 40198f95dc A hangup from another client is still valid
if we're in the ringing state

Fixes https://github.com/vector-im/element-web/issues/15933
2020-12-08 18:53:11 +00:00
David Baker 736b934b18 Merge remote-tracking branch 'origin/develop' into dbkr/hold_ui 2020-12-08 11:59:32 +00:00
J. Ryan Stinnett b009811ac9 Merge pull request #1554 from matrix-org/jryans/rm-temp-test-build
Remove temporary build step for tests
2020-12-08 10:48:56 +00:00
RiotRobot ff6612f9d0 Merge branch 'master' into develop 2020-12-07 12:10:31 +00:00
RiotRobot 565d446b1d v9.3.0 2020-12-07 12:07:32 +00:00
RiotRobot 044d334398 Prepare changelog for v9.3.0 2020-12-07 12:07:31 +00:00
J. Ryan Stinnett e31ef2dfb5 Remove temporary build step for tests
Now that the pipeline has been updated to build for tests, we can remove this
temporary build in the test script.

Related to https://github.com/matrix-org/pipelines/pull/113
2020-12-07 10:21:26 +00:00
J. Ryan Stinnett c20566083a Merge pull request #1552 from matrix-org/jryans/browser-prepublishonly
Move browser build steps to prepublish only
2020-12-07 10:19:07 +00:00
David Baker 9845553a5f playRemoteVideo should be with it's friend, playRemoteAudio 2020-12-04 20:03:58 +00:00
David Baker 03737546fe Remove the media operation queues
As per the comment on playRemoteVideo()
2020-12-04 20:03:11 +00:00
J. Ryan Stinnett 579cb00c98 Temporarily build browser mode for tests 2020-12-04 14:19:46 +00:00
David Baker a307950213 Merge remote-tracking branch 'origin/develop' into dbkr/hold_ui 2020-12-03 19:26:36 +00:00
David Baker 0f5c469be6 Merge remote-tracking branch 'origin/develop' into dbkr/line_1_2 2020-12-03 17:55:59 +00:00
David Baker 6f7e409e9a Some little fixes to support line 1 / 2
* Resume playing audio at the appropriate time
 * Re-emit call events (they were the exception before - all other events
   were re-emitted through the MatrixClient)
 * Fix an audio/video typo
2020-12-03 17:41:34 +00:00
J. Ryan Stinnett d707912d81 Move browser build steps to prepublish only
This speeds local development and CI runs by only running the browser build
steps at release time.
2020-12-03 14:58:28 +00:00
Michael Telatynski 97ab680f4e Merge pull request #1541 from matrix-org/t3chguy/socials
Extend getSsoLoginUrl for MSC2858
2020-12-02 15:48:40 +00:00
Michael Telatynski ea35a29bd8 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into t3chguy/socials 2020-12-02 15:18:37 +00:00
RiotRobot 63c182bc3e v9.3.0-rc.1 2020-12-02 14:20:20 +00:00
RiotRobot b1a0a12a5f Prepare changelog for v9.3.0-rc.1 2020-12-02 14:20:19 +00:00
David Baker cc242230be Update comment 2020-11-27 14:42:18 +00:00
David Baker aef9211ea8 Merge pull request #1551 from matrix-org/dbkr/user_media_error
Export CallError
2020-11-27 14:40:59 +00:00
David Baker de5d557882 Export CallError
So the types can be typed
2020-11-27 12:57:40 +00:00
J. Ryan Stinnett fa9adf199d Merge pull request #1550 from matrix-org/jryans/upgrade-deps-2020-11-25
Upgrade dependencies
2020-11-26 15:50:28 +00:00
J. Ryan Stinnett 0807066f1f Nested upgrades via yarn upgrade 2020-11-26 15:06:30 +00:00
J. Ryan Stinnett 142e79941d Upgrade to latest major version of direct deps 2020-11-26 15:04:52 +00:00
David Baker 5e9ce38a24 Add API for listening to remote hold status
And avoid isLocalOnHold() returning true whilst the remote side
is un-held.
2020-11-26 14:32:47 +00:00
J. Ryan Stinnett f42e6373c4 Merge pull request #1547 from dalcde/error
Don't log error when environment does not support WebRTC
2020-11-26 12:13:49 +00:00
Dexter Chua bc46609caa Don't log error when WebRTC not supported
This function is *always* called when a MatrixClient is created, e.g. in
an appservice. If the environment does not support WebRTC, this is not
necessarily an error; it is expected in many situations.

Fix a small typo

Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-11-26 13:55:36 +08:00
J. Ryan Stinnett cc44abe2d3 Nested upgrades via yarn upgrade 2020-11-25 17:22:20 +00:00
J. Ryan Stinnett db6398acdd Upgrade to latest minor version of direct deps 2020-11-25 17:16:22 +00:00
RiotRobot 6661bde608 Merge branch 'master' into develop 2020-11-23 16:23:01 +00:00
RiotRobot 69f6bba964 v9.2.0 2020-11-23 16:19:22 +00:00
RiotRobot 2a4e722f0f Prepare changelog for v9.2.0 2020-11-23 16:19:22 +00:00
J. Ryan Stinnett dd20828ded Merge pull request #1545 from matrix-org/jryans/fix-dehydration-method-release
[Release] Fix dehydration method name
2020-11-19 16:30:35 +00:00
J. Ryan Stinnett 5993dd588c Merge pull request #1544 from matrix-org/jryans/fix-dehydration-method
Fix dehydration method name
2020-11-19 16:24:25 +00:00
J. Ryan Stinnett 30f86e2437 Fix dehydration method name
https://github.com/matrix-org/matrix-js-sdk/pull/1537 changed some dehydration
method names, but one call site was missed.
2020-11-19 16:22:38 +00:00
J. Ryan Stinnett 3ef91e16d8 Fix dehydration method name
https://github.com/matrix-org/matrix-js-sdk/pull/1537 changed some dehydration
method names, but one call site was missed.
2020-11-19 16:16:40 +00:00
RiotRobot 45e9b3ac68 v9.2.0-rc.1 2020-11-18 15:54:10 +00:00
RiotRobot a076e3f0fc Prepare changelog for v9.2.0-rc.1 2020-11-18 15:54:09 +00:00
Michael Telatynski 99bff04ccc Update base-apis.js 2020-11-17 15:25:17 +00:00
Michael Telatynski bd906e619d Extend getSsoLoginUrl for MSC2858 2020-11-17 09:55:14 +00:00
David Baker 34882cc438 Merge pull request #1532 from matrix-org/dbkr/call_hold
Implement call holding functionality
2020-11-10 08:41:26 +00:00
RiotRobot 5ac00e3465 Merge branch 'master' into develop 2020-11-09 16:21:25 +00:00
RiotRobot 622dd065ff v9.1.0 2020-11-09 16:17:53 +00:00
RiotRobot c5c98a6ac1 Prepare changelog for v9.1.0 2020-11-09 16:17:53 +00:00
David Baker da423ed508 Add comment that the timeout probably isn't
After a fair emount of investigation, it looks like there's not
really a particularly elegant way to have an environment where
setTimeout has a return type that makes sensible assertions in both
node and the browser. Ideally we want it to be something that asserts
that we don't try to use it as a NodeJS.Timeout (ie. call the methods)
because that will break in a browser. In practice, this involves making
wrappers or redefining the timeout functions or some similar kind of
hackery, the evils of which probably offset having perfect typing.

https://matrix.to/#/!bEWtlqtDwCLFIAKAcv:matrix.org/$vpFWl7p1_8A858RAccO0gud3sNVWIDNxNELRjdqaZQ4?via=matrix.org&via=mozilla.org&via=vector.modular.im
for the discussion
2020-11-09 15:59:54 +00:00
Bruno Windels 11c4337cfc Merge pull request #1537 from matrix-org/bwindels/dehydration
Support awaitable one-time dehydration
2020-11-04 15:22:09 +00:00
Bruno Windels 458164384d add client method for one-time dehydration that can be awaited 2020-11-04 16:05:31 +01:00
Bruno Windels 13c7f55a79 split up setKey and setKeyAndQueue
as dehydrating in the background prevents use-cases where you
want to await the creation of the dehydrated device
2020-11-04 16:02:31 +01:00
Bruno Windels 4ab675863a fix typo 2020-11-04 16:00:53 +01:00
RiotRobot 5414b3b39d v9.1.0-rc.1 2020-11-04 14:03:49 +00:00
RiotRobot c54db30dc8 Prepare changelog for v9.1.0-rc.1 2020-11-04 14:03:48 +00:00
Michael Telatynski f11103bfcc Merge pull request #1534 from matrix-org/t3chguy/fix/15604
Client set profile methods update own user
2020-11-04 13:59:58 +00:00
Bruno Windels b56936003d stop dehydration timer when stopping the client 2020-11-03 10:13:19 +01:00
Travis Ralston f61604a51e Merge pull request #1535 from matrix-org/travis/fix-acl-type
Fix spelling error in the server ACL event type
2020-11-02 12:54:14 -07:00
Travis Ralston ae77f900ef Fix spelling error in the server ACL event type 2020-11-02 12:51:15 -07:00
Michael Telatynski 645842f0fd Update comments 2020-11-02 18:09:32 +00:00
Bruno Windels 39d3640973 Merge pull request #1533 from matrix-org/bwindels/dehydration-await-crypto-store
await idb operations from crypto store for dehydration
2020-11-02 17:49:45 +00:00
Michael Telatynski 66aa9c4831 pass a presence event 2020-11-02 17:39:25 +00:00
Michael Telatynski 7de0ca2048 pass args 2020-11-02 17:29:14 +00:00
Michael Telatynski 5bbc5cad9f Client setProfile methods update own user 2020-11-02 17:20:03 +00:00
Bruno Windels 0a7a80d5a8 await idb operations from crypto store for dehydration 2020-11-02 17:35:32 +01:00
David Baker 33d1a33a17 Implement call holding functionality
Using m.call.negotiate
2020-10-29 17:54:54 +00:00
David Baker 7f130949c8 Merge pull request #1531 from matrix-org/dbkr/those_cached_promises_again
Fix stuck never-sending messages
2020-10-28 20:20:26 +00:00
David Baker ba7ee37899 Fix stuck never-sending messages
Another cached promise that wasn't cleared on failure
2020-10-28 18:29:05 +00:00
RiotRobot f8863d5c24 Merge branch 'master' into develop 2020-10-28 14:15:01 +00:00
RiotRobot 1d7954c831 v9.0.1 2020-10-28 14:11:30 +00:00
RiotRobot 2bb4e91dfd Prepare changelog for v9.0.1 2020-10-28 14:11:29 +00:00
J. Ryan Stinnett abe5bf4240 Merge pull request #1530 from matrix-org/jryans/await-key-cache-release
[Release] Await key cache check to avoid prompts
2020-10-28 11:42:19 +00:00
J. Ryan Stinnett c493bf7866 Deduplicate key backup signing paths 2020-10-28 11:07:56 +00:00
J. Ryan Stinnett 06bf0f22be Await key cache check to avoid prompts
`isStoredInKeyCache` is async, so we need to await the result to know whether to
proceed. In addition, this wraps the block in a try / catch, since signing the
key backup is an optional step.

Fixes https://github.com/vector-im/element-web/issues/15530
2020-10-28 11:07:56 +00:00
J. Ryan Stinnett 02d9fe1d30 Merge pull request #1529 from matrix-org/jryans/await-key-cache
Await key cache check to avoid prompts
2020-10-27 17:34:21 +00:00
J. Ryan Stinnett c3091c5aa4 Deduplicate key backup signing paths 2020-10-27 17:17:54 +00:00
J. Ryan Stinnett 677a427f1f Await key cache check to avoid prompts
`isStoredInKeyCache` is async, so we need to await the result to know whether to
proceed. In addition, this wraps the block in a try / catch, since signing the
key backup is an optional step.

Fixes https://github.com/vector-im/element-web/issues/15530
2020-10-27 17:07:04 +00:00
RiotRobot c416dd01a7 Merge branch 'master' into develop 2020-10-26 16:45:54 +00:00
RiotRobot 662fcb426b v9.0.0 2020-10-26 16:42:34 +00:00
RiotRobot fd0fe9e225 Prepare changelog for v9.0.0 2020-10-26 16:42:33 +00:00
David Baker 0ca8613896 Merge pull request #1524 from matrix-org/dbkr/optimise_ice_candidate_sending
Improve ICE candidate batching
2020-10-22 16:29:22 +01:00
David Baker 4b1817719e Merge pull request #1527 from matrix-org/dbkr/logger_ts_try_3
Convert logger to typescript
2020-10-22 16:08:09 +01:00
David Baker 295c591e95 Merge pull request #1528 from matrix-org/dbkr/debugl_rel
Fix logger typo
2020-10-22 16:06:31 +01:00
David Baker 9df0480b78 Merge remote-tracking branch 'origin/develop' into dbkr/logger_ts_try_3 2020-10-22 16:03:12 +01:00
David Baker 5260f40451 Merge pull request #1525 from matrix-org/dbkr/debugl
Fix logger typo
2020-10-22 16:02:10 +01:00
David Baker 0cbe35e41f Fix logger typo 2020-10-22 16:00:23 +01:00
David Baker 502745271d Convert logger to typescript
Because it's annoying for the IDE to have no idea what methods are
on the logger. `loglevel` has types so it should just pass them
through.
2020-10-22 15:59:38 +01:00
David Baker 95baa3cd27 Fix logger typo 2020-10-22 15:54:55 +01:00
David Baker 8fe4a29176 Stop typescript from trying to be clever 2020-10-22 10:10:06 +01:00
David Baker 1a1a0e7324 Abort if call has ended and remove stray self(!) 2020-10-21 18:55:57 +01:00
David Baker d00d07a1c1 Improve ICE candidate batching
Hopefully send fewer ICE candidate events by obeying the batching
guidelines in MSC2476.
2020-10-21 18:23:01 +01:00
Michael Telatynski f27db16e30 Merge pull request #1523 from matrix-org/t3chguy/fix/js-online
bind online listener to window instead of document
2020-10-21 16:08:18 +01:00
RiotRobot d4e107b3cd v9.0.0-rc.1 2020-10-21 14:33:50 +01:00
RiotRobot 881b60c85f Prepare changelog for v9.0.0-rc.1 2020-10-21 14:33:50 +01:00
Michael Telatynski c5e1aade12 bind online listener to window instead of document
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-21 13:58:34 +01:00
David Baker 28198a6f40 Merge pull request #1522 from matrix-org/dbke/select_answer
Support m.call.select_answer
2020-10-21 13:33:49 +01:00
David Baker 30a01e26de Make test pass
Don't send events all the way via the mock HTTP backend: we're not
trying to test that here. This meant we weren't actually getting
into the right state because the request to send the invite never
actually returned. Now this works, we need to clear the invite timer
otherwise jest has a timer hanging around at the end of the test
(plus we should be doing it anyway).
2020-10-21 12:41:36 +01:00
David Baker 8712703f7c Support m.call.select_answer
Only send one if the answer has a party_id set

Also change some event types to the enum and rename receivedAnswer
to be more consistent
2020-10-21 11:52:58 +01:00
David Baker e3a8631faa Merge pull request #1521 from matrix-org/dbkr/dont_cache_versions_failure
Don't cache failures when fetching /versions
2020-10-20 16:35:49 +01:00
David Baker 3eab51ce79 Don't cache failures when fetching /versions
Otherwise we never recover

Fixes https://github.com/vector-im/element-web/issues/15509
2020-10-20 16:25:50 +01:00
J. Ryan Stinnett 9db0fe0795 Merge pull request #1518 from matrix-org/jryans/release-install-first
Install deps first as part of release
2020-10-20 15:45:25 +01:00
Michael Telatynski 228af037e3 Merge pull request #1517 from matrix-org/t3chguy/fix/js-1504
[Breaking] Change hasPendingEvent to return false if pending ordering !detached
2020-10-20 15:28:29 +01:00
J. Ryan Stinnett 654f250fd8 Merge pull request #1519 from matrix-org/jryans/release-disable-editor
Skip editor prompts for merges
2020-10-20 12:06:34 +01:00
J. Ryan Stinnett a8693d9d68 Skip editor prompts for merges
The merges are never edited, and even with this change they will still abort if
there are conflicts.
2020-10-20 11:47:59 +01:00
J. Ryan Stinnett f9f345e428 Move cache clean above install 2020-10-20 11:42:28 +01:00
J. Ryan Stinnett e678706414 Install deps first as part of release
This ensures we always install (without running build scripts) as the first step
of release process, as otherwise it may fail later due to mismatched types or
any number of other errors.
2020-10-20 11:20:57 +01:00
Michael Telatynski 8018259480 Change hasPendingEvent to return false if pending event ordering !detached
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-19 14:56:58 +01:00
David Baker 9f713781cd Merge pull request #1516 from matrix-org/dbkr/aint_no_party_like_a_typescript_party
Convert call test to TypeScript
2020-10-19 09:57:52 +01:00
David Baker 48d56dc5c0 Merge pull request #1512 from matrix-org/dbkr/party_in_the_usa
Support party_id
2020-10-19 09:57:30 +01:00
David Baker 701dfa09b4 Merge pull request #1510 from matrix-org/dbkr/hang_up_your_hangups
Support m.call.reject
2020-10-19 09:57:00 +01:00
David Baker d965648fd7 Convert call test to TypeScript
Typescript tests basically just appear to work, apart from needing
the jest types imported so the typescript checker knows what's what.

DConvert the webrtc test to typescript, which actually mostly just
serves to point out that we're not mocking the whole of `document`,
but oh well.
2020-10-16 18:20:03 +01:00
David Baker 08c15e7203 Merge pull request #1515 from matrix-org/dbkr/remove_specbuild
Remove specbuild from .gitignore
2020-10-16 17:56:02 +01:00
David Baker 9b9e52d0a2 Remove specbuild from .gitignore
It's no longer a thing
2020-10-16 17:47:31 +01:00
David Baker 38cc8fe7dc Merge pull request #1514 from matrix-org/dbkr/log_candidate_send_error
Log the error when we failed to send candidates
2020-10-16 16:52:05 +01:00
David Baker 590fac0fa9 Log the error when we failed to send candidates 2020-10-16 16:46:07 +01:00
David Baker 9590c8aaf0 Don't ignore hangups if we don't have a partner 2020-10-16 14:37:50 +01:00
David Baker e2b79e4e7e linty lint lint 2020-10-16 13:02:47 +01:00
David Baker 2df588f95a Support party_id
Send party_id on events and check the party_id of incoming events matches

Includes a basic test to assert that it actually does: we should
build out a decent test suite for calls as there's a lot of edge-case
functionality that can break and slip through the cracks (eg. glare).
This is a start.

Fixes https://github.com/matrix-org/matrix-js-sdk/issues/1511
2020-10-16 12:53:08 +01:00
David Baker 7c3af91b42 Support m.call.reject
Start the migration to v1 VoIP by supporting m.call.reject, which
we'll send if the caller says they're v1. Our version stays as v0
for now, until we speak the rest of v1.

Honour the default reaosn in a hangup being user_hangup.
2020-10-15 14:51:05 +01:00
David Baker 4cc4b28c47 Merge pull request #1503 from matrix-org/dbkr/call_state_machine
Fixes for call state machine
2020-10-13 10:39:10 +01:00
David Baker fad9b4c67f Merge pull request #1506 from matrix-org/dbkr/fix_event_listener_remove
Fix call event handler listener removing
2020-10-12 14:17:53 +01:00
David Baker ade3b3a021 Fix call event handler listener removing
Wrong kind of event emitter...
2020-10-12 14:12:34 +01:00
RiotRobot d8c4101fdd Merge branch 'master' into develop 2020-10-12 13:18:23 +01:00
RiotRobot a12c250f2b v8.5.0 2020-10-12 13:15:07 +01:00
RiotRobot 57eef2d832 Prepare changelog for v8.5.0 2020-10-12 13:15:06 +01:00
David Baker b67a179a54 Merge pull request #1501 from matrix-org/dbkr/set_type_based_on_tracks
Set the type of the call based on the tracks
2020-10-12 11:42:40 +01:00
David Baker 06044b39c3 Merge pull request #1499 from matrix-org/dbkr/new_age_calls
Use new local timestamp for calls
2020-10-12 11:42:28 +01:00
J. Ryan Stinnett d16cf26c5f Merge pull request #1502 from matrix-org/jryans/sso-4s-integration
Adjust types and APIs to match React SDK
2020-10-12 11:41:06 +01:00
J. Ryan Stinnett b060c5af38 Tune crypto types 2020-10-12 11:01:00 +01:00
David Baker b28bad651e Make events names an enum 2020-10-12 10:15:58 +01:00
David Baker 5c4b7a3213 Add user_hangup error code
but special case it for now & don't send it: it needs voip v1
2020-10-09 18:51:35 +01:00
David Baker 7f21c591ae Fixes for call state machine
* Set 'connecting' state before sending answer, otherwise it can
   race with ICE connecting
 * Ignore completed ice connection state: connected is what we care about
 * Null-check remotestream when stopping media
 * Comments
2020-10-09 18:45:45 +01:00
David Baker 65b24f595c Missed a reference to callList 2020-10-09 18:14:00 +01:00
David Baker 9d9c2720c2 Hopefully make if statement clearer 2020-10-09 17:43:40 +01:00
David Baker f845100062 add 'public' to public method 2020-10-09 17:43:34 +01:00
David Baker e6155f9e37 Use MatrixClient as type
and different array syntax
2020-10-09 17:43:28 +01:00
J. Ryan Stinnett e9590e9093 Adjust types and APIs to match React SDK
Various small tweaks and alignments to match React SDK as part of TypeScript
conversion.

Part of https://github.com/vector-im/element-web/issues/15350
2020-10-09 17:21:14 +01:00
David Baker 49f2d1501c Set the type of the call based on the tracks
Remove the old hack of inspecting the SDP which no longer seems to
be necessary.
2020-10-08 15:42:39 +01:00
J. Ryan Stinnett c6819e0450 Omit stack trace if rehydration fails 2020-10-08 15:11:46 +01:00
David Baker f518ea95f4 Use getLocalAge() & add grace period
Use the new local-age field for deciding whether a call is still
valid or not. Also add a grace period so we don't ring half a second
before the call becomes invalid.
2020-10-08 11:49:54 +01:00
David Baker 92c6332143 use more enums 2020-10-08 11:32:32 +01:00
David Baker 0df1a7da21 copyright header 2020-10-08 11:12:39 +01:00
David Baker 487a9c0967 Extract the call event handler out to its own class
and convert it to TypeScript
2020-10-08 11:10:20 +01:00
David Baker fb89761671 Merge pull request #1495 from matrix-org/dbkr/age_is_just_a_number
Make an accurate version of 'age' for events
2020-10-08 09:29:16 +01:00
David Baker d1d3ae074d Add tests & fix some bugs found by said tests 2020-10-07 18:29:33 +01:00
David Baker 7dedaf90c3 Add a test for the age mangling 2020-10-07 18:04:20 +01:00
David Baker 687b98a09d Don't add localTs if an event has no age 2020-10-07 17:39:19 +01:00
David Baker c6b2e9873c Merge pull request #1498 from matrix-org/dbkr/call_options_is_optional
Make 'options' parameter optional
2020-10-07 17:15:26 +01:00
David Baker 1e80491675 Make 'options' parameter optional
It's deprecated so generally nothing should be passing it
2020-10-07 17:08:21 +01:00
Michael Telatynski a727da9193 Merge pull request #1497 from matrix-org/t3chguy/fix/14804
Create a giant event type enum
2020-10-07 16:48:39 +01:00
Michael Telatynski 0b7754581a Update src/@types/event.ts
Co-authored-by: Travis Ralston <travpc@gmail.com>
2020-10-07 16:32:04 +01:00
David Baker 452e8ea385 Merge pull request #1494 from matrix-org/revert-1493-revert-1487-dbkr/tsify_call
Convert call.js to Typescript & update WebRTC APIs (re-apply)
2020-10-07 15:36:27 +01:00
David Baker a189de9a2e Add comment on mass event copying 2020-10-07 15:32:22 +01:00
David Baker 8632ca6e37 Merge remote-tracking branch 'origin/develop' into revert-1493-revert-1487-dbkr/tsify_call 2020-10-07 15:16:21 +01:00
RiotRobot 293860b6c5 v8.5.0-rc.1 2020-10-07 14:18:52 +01:00
RiotRobot 07667172cd Prepare changelog for v8.5.0-rc.1 2020-10-07 14:18:52 +01:00
J. Ryan Stinnett f5240cdac6 Merge pull request #1467 from uhoreg/fallback_keys
Add support for olm fallback keys
2020-10-07 10:24:12 +01:00
Michael Telatynski 7529d2b638 Create a giant event type enum
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-07 09:57:56 +01:00
Michael Telatynski 03fc12e888 Merge pull request #1492 from matrix-org/t3chguy/fix/10545
Fix editing local echoes not updating them in real time
2020-10-07 00:15:16 +01:00
Hubert Chathi e05a50528e make test no longer dependent on emscripten internals 2020-10-06 19:01:01 -04:00
Hubert Chathi 356ee90417 lint 2020-10-06 18:33:45 -04:00
Hubert Chathi 5dced57724 bump olm release again and fix global.d.ts to work with the olm TS definitions 2020-10-06 17:55:40 -04:00
Michael Telatynski 8ec3b88c5d Update comments
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-06 22:55:34 +01:00
Michael Telatynski d7ef128510 Merge pull request #1491 from matrix-org/t3chguy/fix/9836
Fix re-emit of Event.replaced to be on client and not room
2020-10-06 22:50:14 +01:00
Hubert Chathi bb2502409b update yarn.lock 2020-10-06 15:34:02 -04:00
Hubert Chathi aa6aab4245 Merge remote-tracking branch 'origin/develop' into fallback_keys 2020-10-06 15:22:25 -04:00
Hubert Chathi 12aab0caeb depend on newer olm 2020-10-06 15:18:18 -04:00
David Baker 2f316c558d Merge pull request #1496 from matrix-org/dbkr/space_in_log_line
Add space to log line
2020-10-06 19:16:25 +01:00
David Baker f447273b75 Add space to log line 2020-10-06 18:59:34 +01:00
David Baker c8a18d51e6 Lint (& unused variable) 2020-10-06 17:45:21 +01:00
David Baker d77af1e67a Make an accurate version of 'age' for events
We've always had 'age' in events but it's never really been an
accurate representation of the event's age because we never did
anything with it. This transforms it into a local clock timestamp
when the event arives and when it comes out of the sync store, and
changes getLocalAge() to use it.

react-sdk doesn't appear to use getLocalAge() but any 3rd party apps
that do may notice a slight change in bahaviour.
2020-10-06 15:24:09 +01:00
David Baker 81c95224d1 Revert "Revert "Convert call.js to Typescript & update WebRTC APIs"" 2020-10-06 15:21:45 +01:00
David Baker a0e66291df Merge pull request #1493 from matrix-org/revert-1487-dbkr/tsify_call
Revert "Convert call.js to Typescript & update WebRTC APIs"
2020-10-06 15:21:38 +01:00
David Baker 5733f46f4c Revert "Convert call.js to Typescript & update WebRTC APIs" 2020-10-06 15:16:32 +01:00
David Baker da2128feff Merge pull request #1487 from matrix-org/dbkr/tsify_call
Convert call.js to Typescript & update WebRTC APIs
2020-10-06 15:09:10 +01:00
Michael Telatynski d413faefdb double sync-browserify jest timeout
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-06 12:39:18 +01:00
Michael Telatynski 0d1d767d96 Fix editing local echoes not updating them in real time
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-06 12:32:50 +01:00
David Baker 5619554023 use MatrixEvent for event types 2020-10-06 11:03:05 +01:00
David Baker 53880a4bb2 This is how plurals work 2020-10-06 10:54:51 +01:00
David Baker 812ae227b6 Make type enums PascalCase 2020-10-06 10:53:42 +01:00
David Baker c6b44098ac remove space before type colon 2020-10-06 09:55:02 +01:00
David Baker 92b95a98d0 comment other snake case identifiers
and also sto mixing quotes
2020-10-06 09:52:59 +01:00
David Baker a3505ff42d Use right case for private enum field 2020-10-06 09:49:51 +01:00
Michael Telatynski 15b2e3ff1d Fix re-emit of Event.replaced to be on client and not room
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-10-06 08:23:19 +01:00
Hubert Chathi 1845d1ac55 Merge pull request #1436 from uhoreg/dehydration
Dehydrate and rehydrate devices
2020-10-05 17:19:28 -04:00
J. Ryan Stinnett 463819caa7 Merge pull request #1490 from matrix-org/jryans/key-setup-device-undefined
Keep local device after processing device list sync
2020-10-05 14:49:18 +01:00
J. Ryan Stinnett a0317d9587 Fix tests 2020-10-05 14:29:30 +01:00
J. Ryan Stinnett fa6ce0cb70 Keep local device after processing device list sync
This change ensure we always keep the local device in the device list, even if
the server claims it doesn't exist. This is important for initial startup, as it
is possible the device key upload takes a while to apply, and thus might not
appear in the first device list sync.

Fixes https://github.com/vector-im/element-web/issues/15319
2020-10-05 14:09:16 +01:00
David Baker 7471ff4b0d Make setSinkId reliable
As per comment, this seems to be the way to make setSinkId work reliably on Chrome.

Also don't make our own mediastreams if we don't need to (ie. in a normal call
our tracks should come with their owen MediaStreams).
2020-10-03 16:57:33 +01:00
Hubert Chathi 65a5bfac88 update property name 2020-10-02 15:52:09 -04:00
David Baker d2321410f8 Pass stream to addTrack too
Otherwise we end up with separate, unbundled 'streamless tracks'
which is not what we want.
2020-10-02 15:12:12 +01:00
Hubert Chathi f90b5a99cd Merge remote-tracking branch 'origin/develop' into dehydration 2020-10-01 21:55:05 -04:00
David Baker cc4656b36a Update call.js to use new WebRTC APIs
and some other related & semi-related refactoring
2020-10-01 19:15:49 +01:00
J. Ryan Stinnett 34bc63a146 Merge pull request #1489 from matrix-org/jryans/no-console
Enforce logger module via lint rules
2020-10-01 14:56:15 +01:00
J. Ryan Stinnett 09bd91a588 Enforce logger module via lint rules
This adds lint rules (and fixes various errors) to ensure we use the `logger`
intermediary module, rather than accessing the console directly.
2020-10-01 14:28:24 +01:00
David Baker bb3e082e5b Make direction public, remove didConnect
Various flags that may or may not have been part of the poorly defined
external API before. Neither were used internally. Make direction a
public export but remove didConnect.
2020-10-01 10:15:27 +01:00
Hubert Chathi 60c863f829 lint 2020-09-30 18:33:27 -04:00
Hubert Chathi 2d2a73bf52 make dehydrated device name configurable 2020-09-30 18:05:52 -04:00
Hubert Chathi 7b9f73709d use "dehydration" as the cache name 2020-09-30 17:50:37 -04:00
Hubert Chathi 1dc89f642d use logger instead of console 2020-09-30 17:46:18 -04:00
David Baker a78b59010a Convert call.js to Typescript
This does a fairly basic rewriting of the whole file to bring it up
to a vaguely modern standard of code. Nothing should have changed
fundamentally, but it probably should do: as per the global type
overrides, we use a lot of deprecated WebRTC APIs and should probably
update.
2020-09-30 18:54:48 +01:00
Hubert Chathi a2e1d94fcf more lint 2020-09-30 01:22:46 -04:00
Hubert Chathi b181c83b93 lint 2020-09-30 01:16:57 -04:00
Hubert Chathi cf7c84c4ba more linting, because GitHub is showing me outdated CI info 2020-09-30 01:10:33 -04:00
Hubert Chathi 6e5230f9f9 fix lint again 2020-09-30 01:07:35 -04:00
Hubert Chathi f03f7c0acb fix lint 2020-09-30 00:58:37 -04:00
Hubert Chathi aa9b807b82 update to latest dehydration endpoints, and simplify API 2020-09-30 00:48:07 -04:00
Travis Ralston fa9921e091 Merge pull request #1462 from orangecms/develop
extend method redactEvent with reason
2020-09-28 15:05:00 -06:00
RiotRobot a9a6b2de48 Merge branch 'master' into develop 2020-09-28 16:09:28 +01:00
RiotRobot 1ef746658f v8.4.1 2020-09-28 16:06:30 +01:00
RiotRobot fe0099b497 Prepare changelog for v8.4.1 2020-09-28 16:06:30 +01:00
J. Ryan Stinnett 7e9c4146a5 Merge pull request #1486 from matrix-org/dbkr/catch_exceptions_from_call_event_handler_rel
Catch exception from call event handler
2020-09-28 16:01:07 +01:00
David Baker 3e978c64e4 Merge pull request #1484 from matrix-org/dbkr/catch_exceptions_from_call_event_handler
Catch exception from call event handler
2020-09-28 15:56:29 +01:00
David Baker 13c5920a46 Merge pull request #1485 from matrix-org/dbkr/ignore_invalid_candidates_rel
Ignore invalid candidates
2020-09-28 15:55:00 +01:00
David Baker 3b19203fd2 Catch exception from call event handler
Otherwise they leak out ot the sync loop. It's unfortunate that
exceptions from event handlers leak out to the emitter, and this
seems a bit clumsy, but having to wrap eveery event emit in a try
block seems worse.
2020-09-28 15:48:06 +01:00
David Baker 84cd05b218 Ignore invalid candidates 2020-09-28 15:47:02 +01:00
David Baker 9b9a9642ee Merge pull request #1483 from matrix-org/dbkr/ignore_invalid_candidates
Ignore invalid candidates
2020-09-28 15:41:32 +01:00
David Baker 639d2317ed Catch exception from call event handler
Otherwise they leak out ot the sync loop. It's unfortunate that
exceptions from event handlers leak out to the emitter, and this
seems a bit clumsy, but having to wrap eveery event emit in a try
block seems worse.
2020-09-28 15:39:21 +01:00
David Baker 52379d7655 Ignore invalid candidates 2020-09-28 15:22:01 +01:00
Michael Telatynski 29827362d6 Merge pull request #1478 from matrix-org/t3chguy-patch-1
Always push docs if they are generated
2020-09-28 14:32:07 +01:00
RiotRobot f33315f610 Merge branch 'master' into develop 2020-09-28 14:23:19 +01:00
J. Ryan Stinnett f3a8306107 Merge pull request #1481 from matrix-org/jryans/guard-backup-sig
Only sign key backup with cross-signing keys when available
2020-09-28 10:21:34 +01:00
J. Ryan Stinnett 2b29e9934c Fix default key cache check 2020-09-28 10:16:20 +01:00
J. Ryan Stinnett 4b1104e463 Only sign key backup with cross-signing keys when available
This changes the key backup setup step to only sign with cross-signing keys when
both the public and private keys are already available without prompting. In
many cases down these paths, the cross-signing keys either may not exist or may
not be accessible. We always sign the key backup with your device key as well,
so there is always a route to trust the key backup even if this is skipped.

Fixes https://github.com/vector-im/element-web/issues/15230
2020-09-25 17:36:47 +01:00
Hubert Chathi 60b9ef959d don't include an empty signatures field when calculating the signature 2020-09-24 18:46:17 -04:00
Hubert Chathi 0074c2cf57 mark fallback keys as fallback 2020-09-24 18:09:07 -04:00
Hubert Chathi b0204ab54e use unstable prefix 2020-09-24 17:41:08 -04:00
J. Ryan Stinnett bc5b07aa75 Merge pull request #1479 from matrix-org/jryans/upgrade-deps-2020-09-23
Upgrade dependencies
2020-09-24 16:40:36 +01:00
Michael Telatynski cfe90dbed5 Always push docs if they are generated 2020-09-24 12:47:23 +01:00
J. Ryan Stinnett fa913655bc Upgrade dependencies 2020-09-23 17:35:24 +01:00
Daniel Maslowski aad7484c8d Add optional reason to be passed to redactEvent
Signed-off-by: Daniel Maslowski <info@orangecms.org>
2020-09-21 21:50:38 +02:00
Hubert Chathi 16dccd75c1 fix string 2020-09-18 19:20:09 -04:00
Hubert Chathi 5151b52688 Merge remote-tracking branch 'origin/develop' into dehydration 2020-09-18 19:13:22 -04:00
Hubert Chathi 492c1d77d8 style fixes 2020-09-16 17:09:33 -04:00
Hubert Chathi 3952768667 Merge remote-tracking branch 'origin/develop' into fallback_keys 2020-09-15 12:04:30 -04:00
Hubert Chathi 6331b34cf7 lint 2020-09-14 23:00:07 -04:00
Hubert Chathi 2ebe1dfa16 support olm fallback keys 2020-09-14 22:10:16 -04:00
Hubert Chathi 19f3996e09 only cross-sign dehydrated device if cross-signing is available 2020-09-03 16:06:06 -04:00
Hubert Chathi 25c2cc1768 various fixes 2020-09-03 11:45:37 -04:00
Hubert Chathi 438861ae5e separate dehydration functions to its own file and cache the key 2020-08-31 15:44:29 -04:00
florianjacob 22713d8f89 correct IndexedDB store creation example 2020-08-26 14:24:13 +02:00
Hubert Chathi ad85740ae2 allow other login functions to be used 2020-08-19 18:01:31 -04:00
Will Hunt 6ec7e3a0b7 UserId must be sane 2020-08-18 18:42:55 +01:00
Will Hunt 1a1fe759c3 fix encoding bug 2020-08-18 18:41:45 +01:00
Will Hunt a919c798f8 Update _unstable_getSharedRooms to match spec 2020-08-18 18:34:44 +01:00
Will Hunt 80fe66c481 Add /uk.half-shot.msc2666/ prefix 2020-08-18 18:34:44 +01:00
Will Hunt 4577dc8f44 s/msc2644/msc2666/ 2020-08-18 18:34:44 +01:00
Will Hunt cf0a5305e0 Throw if server does not support getSharedRooms 2020-08-18 18:34:44 +01:00
Will Hunt e69e6e1981 Update _unstable_getSharedRooms response format 2020-08-18 18:34:44 +01:00
Will Hunt 0febd99fbe Add _unstable_getSharedRooms 2020-08-18 18:34:43 +01:00
Hubert Chathi 6e8e3e4150 use newer version of dehydration proposal and add some comments 2020-08-17 18:09:23 -04:00
Hubert Chathi 8a4440c314 initial work on dehydration 2020-08-04 16:06:19 -04:00
65 changed files with 8253 additions and 4871 deletions
+7 -4
View File
@@ -1,13 +1,12 @@
module.exports = {
extends: ["matrix-org"],
plugins: [
"babel",
"matrix-org",
],
extends: ["plugin:matrix-org/javascript"],
env: {
browser: true,
node: true,
},
rules: {
"no-var": ["warn"],
"prefer-rest-params": ["warn"],
@@ -28,11 +27,15 @@ module.exports = {
"indent": "off",
"no-constant-condition": "off",
"no-async-promise-executor": "off",
// We use a `logger` intermediary module
"no-console": "error",
},
overrides: [{
"files": ["src/**/*.ts"],
"extends": ["matrix-org/ts"],
"extends": ["plugin:matrix-org/typescript"],
"rules": {
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// While we're converting to ts we make heavy use of this
"@typescript-eslint/no-explicit-any": "off",
"quotes": "off",
-1
View File
@@ -12,7 +12,6 @@ lib-cov
out
/dist
/lib
/specbuild
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
+482
View File
@@ -1,3 +1,485 @@
Changes in [10.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0) (2021-04-26)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0-rc.1...v10.0.0)
* No changes since rc.1
Changes in [10.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0-rc.1) (2021-04-21)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0...v10.0.0-rc.1)
* Restrict event emit for room members that had power levels changed
[\#1675](https://github.com/matrix-org/matrix-js-sdk/pull/1675)
* Fix sync with misconfigured push rules
[\#1669](https://github.com/matrix-org/matrix-js-sdk/pull/1669)
* Add missing await
[\#1665](https://github.com/matrix-org/matrix-js-sdk/pull/1665)
* Migrate to `eslint-plugin-matrix-org`
[\#1642](https://github.com/matrix-org/matrix-js-sdk/pull/1642)
* Add missing event type enum for key verification done
[\#1664](https://github.com/matrix-org/matrix-js-sdk/pull/1664)
* Fix timeline jumpiness by setting correct txnId
[\#1663](https://github.com/matrix-org/matrix-js-sdk/pull/1663)
* Fix calling addEventListener if it does not exist
[\#1661](https://github.com/matrix-org/matrix-js-sdk/pull/1661)
* Persist unsent messages for subsequent sessions
[\#1655](https://github.com/matrix-org/matrix-js-sdk/pull/1655)
Changes in [9.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0) (2021-04-12)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0-rc.1...v9.11.0)
* No changes since rc.1
Changes in [9.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0-rc.1) (2021-04-07)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0...v9.11.0-rc.1)
* Only try to cache private keys we know exist
[\#1657](https://github.com/matrix-org/matrix-js-sdk/pull/1657)
* Properly terminate screen-share calls if NoUserMedia
[\#1654](https://github.com/matrix-org/matrix-js-sdk/pull/1654)
* Attended transfer
[\#1652](https://github.com/matrix-org/matrix-js-sdk/pull/1652)
* Remove catch handlers in private key retrieval
[\#1653](https://github.com/matrix-org/matrix-js-sdk/pull/1653)
* Fixed the media fail error on caller's side
[\#1651](https://github.com/matrix-org/matrix-js-sdk/pull/1651)
* Add function to share megolm keys for historical messages, take 2
[\#1640](https://github.com/matrix-org/matrix-js-sdk/pull/1640)
* Cache cross-signing private keys if needed on bootstrap
[\#1649](https://github.com/matrix-org/matrix-js-sdk/pull/1649)
Changes in [9.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0) (2021-03-29)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0-rc.1...v9.10.0)
* No changes since rc.1
Changes in [9.10.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0-rc.1) (2021-03-25)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0...v9.10.0-rc.1)
* Don't send m.call.hangup if m.call.invite wasn't sent either
[\#1647](https://github.com/matrix-org/matrix-js-sdk/pull/1647)
* docs: registerGuest()
[\#1641](https://github.com/matrix-org/matrix-js-sdk/pull/1641)
* Download device keys in chunks of 250
[\#1639](https://github.com/matrix-org/matrix-js-sdk/pull/1639)
* More VoIP connectivity fixes
[\#1646](https://github.com/matrix-org/matrix-js-sdk/pull/1646)
* Make selectDesktopCapturerSource param optional
[\#1644](https://github.com/matrix-org/matrix-js-sdk/pull/1644)
* Expose APIs needed for reworked cross-signing login flow
[\#1632](https://github.com/matrix-org/matrix-js-sdk/pull/1632)
Changes in [9.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0) (2021-03-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0-rc.1...v9.9.0)
* No changes since rc.1
Changes in [9.9.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0-rc.1) (2021-03-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0...v9.9.0-rc.1)
* Remove detailed Olm session logging
[\#1638](https://github.com/matrix-org/matrix-js-sdk/pull/1638)
* Add space summary suggested only param
[\#1637](https://github.com/matrix-org/matrix-js-sdk/pull/1637)
* Check TURN servers periodically, and at start of calls
[\#1634](https://github.com/matrix-org/matrix-js-sdk/pull/1634)
* Support sending invite reasons
[\#1624](https://github.com/matrix-org/matrix-js-sdk/pull/1624)
* Bump elliptic from 6.5.3 to 6.5.4
[\#1636](https://github.com/matrix-org/matrix-js-sdk/pull/1636)
* Add a function to get a room's MXC URI
[\#1635](https://github.com/matrix-org/matrix-js-sdk/pull/1635)
* Stop streams if the call has ended
[\#1633](https://github.com/matrix-org/matrix-js-sdk/pull/1633)
* Remove export keyword from global.d.ts
[\#1631](https://github.com/matrix-org/matrix-js-sdk/pull/1631)
* Fix IndexedDB store creation example
[\#1445](https://github.com/matrix-org/matrix-js-sdk/pull/1445)
* An attempt to cleanup how constraints are handled in calls
[\#1613](https://github.com/matrix-org/matrix-js-sdk/pull/1613)
* Extract display name patterns to constants
[\#1628](https://github.com/matrix-org/matrix-js-sdk/pull/1628)
* Bump pug-code-gen from 2.0.2 to 2.0.3
[\#1630](https://github.com/matrix-org/matrix-js-sdk/pull/1630)
* Avoid deadlocks when ensuring Olm sessions for devices
[\#1627](https://github.com/matrix-org/matrix-js-sdk/pull/1627)
* Filter out edits from other senders in history
[\#1626](https://github.com/matrix-org/matrix-js-sdk/pull/1626)
* Fix ContentHelpers export
[\#1618](https://github.com/matrix-org/matrix-js-sdk/pull/1618)
* Add logging to in progress Olm sessions
[\#1621](https://github.com/matrix-org/matrix-js-sdk/pull/1621)
* Don't ignore ICE candidates received before offer/answer
[\#1623](https://github.com/matrix-org/matrix-js-sdk/pull/1623)
* Better handling of send failures on VoIP events
[\#1622](https://github.com/matrix-org/matrix-js-sdk/pull/1622)
* Log when turn creds expire
[\#1620](https://github.com/matrix-org/matrix-js-sdk/pull/1620)
* Initial Spaces [MSC1772] support
[\#1563](https://github.com/matrix-org/matrix-js-sdk/pull/1563)
* Add logging to crypto store transactions
[\#1617](https://github.com/matrix-org/matrix-js-sdk/pull/1617)
* Room helper for getting type and checking if it is a space room
[\#1610](https://github.com/matrix-org/matrix-js-sdk/pull/1610)
Changes in [9.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0) (2021-03-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0-rc.1...v9.8.0)
* No changes since rc.1
Changes in [9.8.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0-rc.1) (2021-02-24)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0...v9.8.0-rc.1)
* Optimise prefixed logger
[\#1615](https://github.com/matrix-org/matrix-js-sdk/pull/1615)
* Add debug logs to encryption prep, take 3
[\#1614](https://github.com/matrix-org/matrix-js-sdk/pull/1614)
* Add functions for upper & lowercase random strings
[\#1612](https://github.com/matrix-org/matrix-js-sdk/pull/1612)
* Room helpers for invite permissions and join rules
[\#1609](https://github.com/matrix-org/matrix-js-sdk/pull/1609)
* Fixed wording in "Adding video track with id" log
[\#1606](https://github.com/matrix-org/matrix-js-sdk/pull/1606)
* Add more debug logs to encryption prep
[\#1605](https://github.com/matrix-org/matrix-js-sdk/pull/1605)
* Add option to set ice candidate pool size
[\#1604](https://github.com/matrix-org/matrix-js-sdk/pull/1604)
* Cancel call if no source was selected
[\#1601](https://github.com/matrix-org/matrix-js-sdk/pull/1601)
Changes in [9.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0) (2021-02-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0-rc.1...v9.7.0)
* No changes since rc.1
Changes in [9.7.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0-rc.1) (2021-02-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0...v9.7.0-rc.1)
* Handle undefined peerconn
[\#1600](https://github.com/matrix-org/matrix-js-sdk/pull/1600)
* ReEmitter: Don't throw if no error handler is attached
[\#1599](https://github.com/matrix-org/matrix-js-sdk/pull/1599)
* Convert ReEmitter to TS
[\#1598](https://github.com/matrix-org/matrix-js-sdk/pull/1598)
* Fix typo in main readme
[\#1597](https://github.com/matrix-org/matrix-js-sdk/pull/1597)
* Remove rogue plus character
[\#1596](https://github.com/matrix-org/matrix-js-sdk/pull/1596)
* Fix call ID NaN
[\#1595](https://github.com/matrix-org/matrix-js-sdk/pull/1595)
* Fix Electron type merging
[\#1594](https://github.com/matrix-org/matrix-js-sdk/pull/1594)
* Fix browser screen share
[\#1593](https://github.com/matrix-org/matrix-js-sdk/pull/1593)
* Fix desktop Matrix screen sharing
[\#1570](https://github.com/matrix-org/matrix-js-sdk/pull/1570)
* Guard against confused server retry times
[\#1591](https://github.com/matrix-org/matrix-js-sdk/pull/1591)
* Decrypt redaction events
[\#1589](https://github.com/matrix-org/matrix-js-sdk/pull/1589)
* Fix edge cases with peeking where a room is re-peeked
[\#1587](https://github.com/matrix-org/matrix-js-sdk/pull/1587)
Changes in [9.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0) (2021-02-03)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0-rc.1...v9.6.0)
* [Release] Fix edge cases with peeking where a room is re-peeked
[\#1588](https://github.com/matrix-org/matrix-js-sdk/pull/1588)
Changes in [9.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0-rc.1) (2021-01-29)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.1...v9.6.0-rc.1)
* Add support for getting call stats
[\#1584](https://github.com/matrix-org/matrix-js-sdk/pull/1584)
* Fix compatibility with v0 calls
[\#1583](https://github.com/matrix-org/matrix-js-sdk/pull/1583)
* Upgrade deps 2021-01
[\#1582](https://github.com/matrix-org/matrix-js-sdk/pull/1582)
* Log the call ID when logging that we've received VoIP events
[\#1581](https://github.com/matrix-org/matrix-js-sdk/pull/1581)
* Fix extra negotiate message in Firefox
[\#1579](https://github.com/matrix-org/matrix-js-sdk/pull/1579)
* Add debug logs to encryption prep
[\#1580](https://github.com/matrix-org/matrix-js-sdk/pull/1580)
* Expose getPresence endpoint
[\#1578](https://github.com/matrix-org/matrix-js-sdk/pull/1578)
* Queue keys for backup even if backup isn't enabled yet
[\#1577](https://github.com/matrix-org/matrix-js-sdk/pull/1577)
* Stop retrying TURN access when forbidden
[\#1576](https://github.com/matrix-org/matrix-js-sdk/pull/1576)
* Add DTMF sending support
[\#1573](https://github.com/matrix-org/matrix-js-sdk/pull/1573)
Changes in [9.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.1) (2021-01-26)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0...v9.5.1)
* [Release] Fix compatibility with v0 calls
[\#1585](https://github.com/matrix-org/matrix-js-sdk/pull/1585)
Changes in [9.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0) (2021-01-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0-rc.1...v9.5.0)
* No changes since rc.1
Changes in [9.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0-rc.1) (2021-01-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.1...v9.5.0-rc.1)
* Don't log if no WebRTC
[\#1574](https://github.com/matrix-org/matrix-js-sdk/pull/1574)
* Add _unstable_getSharedRooms
[\#1417](https://github.com/matrix-org/matrix-js-sdk/pull/1417)
* Bump node-notifier from 8.0.0 to 8.0.1
[\#1568](https://github.com/matrix-org/matrix-js-sdk/pull/1568)
* Ignore party ID if opponent is v0
[\#1567](https://github.com/matrix-org/matrix-js-sdk/pull/1567)
* Basic call transfer initiation support
[\#1566](https://github.com/matrix-org/matrix-js-sdk/pull/1566)
* Room version 6 is now a thing
[\#1572](https://github.com/matrix-org/matrix-js-sdk/pull/1572)
* Store keys with same index but better trust level
[\#1571](https://github.com/matrix-org/matrix-js-sdk/pull/1571)
* Use TypeScript source for development, swap to build during release
[\#1561](https://github.com/matrix-org/matrix-js-sdk/pull/1561)
* Revert "Ignore party ID if opponent is v0"
[\#1565](https://github.com/matrix-org/matrix-js-sdk/pull/1565)
* Basic call transfer initiation support
[\#1558](https://github.com/matrix-org/matrix-js-sdk/pull/1558)
* Ignore party ID if opponent is v0
[\#1559](https://github.com/matrix-org/matrix-js-sdk/pull/1559)
* Honour a call reject event from another of our own devices
[\#1562](https://github.com/matrix-org/matrix-js-sdk/pull/1562)
Changes in [9.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.1) (2020-12-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0...v9.4.1)
* Further script tweaks to get all layers building again
Changes in [9.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0) (2020-12-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.2...v9.4.0)
* Revert `postinstall` script change, causes issues for other layers
Changes in [9.4.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0-rc.2) (2020-12-16)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.4.0-rc.1...v9.4.0-rc.2)
* Remove `postinstall` script which also runs as a dependency
[\#1560](https://github.com/matrix-org/matrix-js-sdk/pull/1560)
Changes in [9.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.4.0-rc.1) (2020-12-16)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.3.0...v9.4.0-rc.1)
* Fixes to support line 1 / 2
[\#1553](https://github.com/matrix-org/matrix-js-sdk/pull/1553)
* Add API for listening to remote hold status, advertise VoIP V1
[\#1549](https://github.com/matrix-org/matrix-js-sdk/pull/1549)
* A hangup from another client is still valid
[\#1555](https://github.com/matrix-org/matrix-js-sdk/pull/1555)
* Remove temporary build step for tests
[\#1554](https://github.com/matrix-org/matrix-js-sdk/pull/1554)
* Move browser build steps to prepublish only
[\#1552](https://github.com/matrix-org/matrix-js-sdk/pull/1552)
* Extend getSsoLoginUrl for MSC2858
[\#1541](https://github.com/matrix-org/matrix-js-sdk/pull/1541)
Changes in [9.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.3.0) (2020-12-07)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.3.0-rc.1...v9.3.0)
* No changes since rc.1
Changes in [9.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.3.0-rc.1) (2020-12-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.2.0...v9.3.0-rc.1)
* Export CallError
[\#1551](https://github.com/matrix-org/matrix-js-sdk/pull/1551)
* Upgrade dependencies
[\#1550](https://github.com/matrix-org/matrix-js-sdk/pull/1550)
* Don't log error when environment does not support WebRTC
[\#1547](https://github.com/matrix-org/matrix-js-sdk/pull/1547)
* Fix dehydration method name
[\#1544](https://github.com/matrix-org/matrix-js-sdk/pull/1544)
Changes in [9.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.2.0) (2020-11-23)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.2.0-rc.1...v9.2.0)
* [Release] Fix dehydration method name
[\#1545](https://github.com/matrix-org/matrix-js-sdk/pull/1545)
Changes in [9.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.2.0-rc.1) (2020-11-18)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.1.0...v9.2.0-rc.1)
* Implement call holding functionality
[\#1532](https://github.com/matrix-org/matrix-js-sdk/pull/1532)
* Support awaitable one-time dehydration
[\#1537](https://github.com/matrix-org/matrix-js-sdk/pull/1537)
* Client set profile methods update own user
[\#1534](https://github.com/matrix-org/matrix-js-sdk/pull/1534)
Changes in [9.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.1.0) (2020-11-09)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.1.0-rc.1...v9.1.0)
* No changes since rc.1
Changes in [9.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.1.0-rc.1) (2020-11-04)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.1...v9.1.0-rc.1)
* Fix spelling error in the server ACL event type
[\#1535](https://github.com/matrix-org/matrix-js-sdk/pull/1535)
* await idb operations from crypto store for dehydration
[\#1533](https://github.com/matrix-org/matrix-js-sdk/pull/1533)
* Fix stuck never-sending messages
[\#1531](https://github.com/matrix-org/matrix-js-sdk/pull/1531)
* Await key cache check to avoid prompts
[\#1529](https://github.com/matrix-org/matrix-js-sdk/pull/1529)
* Improve ICE candidate batching
[\#1524](https://github.com/matrix-org/matrix-js-sdk/pull/1524)
* Convert logger to typescript
[\#1527](https://github.com/matrix-org/matrix-js-sdk/pull/1527)
* Fix logger typo
[\#1525](https://github.com/matrix-org/matrix-js-sdk/pull/1525)
* bind online listener to window instead of document
[\#1523](https://github.com/matrix-org/matrix-js-sdk/pull/1523)
* Support m.call.select_answer
[\#1522](https://github.com/matrix-org/matrix-js-sdk/pull/1522)
Changes in [9.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.1) (2020-10-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.0...v9.0.1)
* [Release] Await key cache check to avoid prompts
[\#1530](https://github.com/matrix-org/matrix-js-sdk/pull/1530)
Changes in [9.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0) (2020-10-26)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.0-rc.1...v9.0.0)
* Fix logger typo
[\#1528](https://github.com/matrix-org/matrix-js-sdk/pull/1528)
Changes in [9.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0-rc.1) (2020-10-21)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0...v9.0.0-rc.1)
BREAKING CHANGES
---
* `hasPendingEvent` now returns false instead of throwing when pending ordering mode is not `detached`
All changes
---
* Don't cache failures when fetching /versions
[\#1521](https://github.com/matrix-org/matrix-js-sdk/pull/1521)
* Install deps first as part of release
[\#1518](https://github.com/matrix-org/matrix-js-sdk/pull/1518)
* [Breaking] Change hasPendingEvent to return false if pending ordering
!detached
[\#1517](https://github.com/matrix-org/matrix-js-sdk/pull/1517)
* Skip editor prompts for merges
[\#1519](https://github.com/matrix-org/matrix-js-sdk/pull/1519)
* Convert call test to TypeScript
[\#1516](https://github.com/matrix-org/matrix-js-sdk/pull/1516)
* Support party_id
[\#1512](https://github.com/matrix-org/matrix-js-sdk/pull/1512)
* Support m.call.reject
[\#1510](https://github.com/matrix-org/matrix-js-sdk/pull/1510)
* Remove specbuild from .gitignore
[\#1515](https://github.com/matrix-org/matrix-js-sdk/pull/1515)
* Log the error when we failed to send candidates
[\#1514](https://github.com/matrix-org/matrix-js-sdk/pull/1514)
* Fixes for call state machine
[\#1503](https://github.com/matrix-org/matrix-js-sdk/pull/1503)
* Fix call event handler listener removing
[\#1506](https://github.com/matrix-org/matrix-js-sdk/pull/1506)
* Set the type of the call based on the tracks
[\#1501](https://github.com/matrix-org/matrix-js-sdk/pull/1501)
* Use new local timestamp for calls
[\#1499](https://github.com/matrix-org/matrix-js-sdk/pull/1499)
* Adjust types and APIs to match React SDK
[\#1502](https://github.com/matrix-org/matrix-js-sdk/pull/1502)
* Make an accurate version of 'age' for events
[\#1495](https://github.com/matrix-org/matrix-js-sdk/pull/1495)
* Make 'options' parameter optional
[\#1498](https://github.com/matrix-org/matrix-js-sdk/pull/1498)
* Create a giant event type enum
[\#1497](https://github.com/matrix-org/matrix-js-sdk/pull/1497)
* Convert call.js to Typescript & update WebRTC APIs (re-apply)
[\#1494](https://github.com/matrix-org/matrix-js-sdk/pull/1494)
Changes in [8.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0) (2020-10-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0-rc.1...v8.5.0)
* No changes since rc.1
Changes in [8.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0-rc.1) (2020-10-07)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.1...v8.5.0-rc.1)
* Add support for olm fallback keys
[\#1467](https://github.com/matrix-org/matrix-js-sdk/pull/1467)
* Fix editing local echoes not updating them in real time
[\#1492](https://github.com/matrix-org/matrix-js-sdk/pull/1492)
* Fix re-emit of Event.replaced to be on client and not room
[\#1491](https://github.com/matrix-org/matrix-js-sdk/pull/1491)
* Add space to log line
[\#1496](https://github.com/matrix-org/matrix-js-sdk/pull/1496)
* Revert "Convert call.js to Typescript & update WebRTC APIs"
[\#1493](https://github.com/matrix-org/matrix-js-sdk/pull/1493)
* Convert call.js to Typescript & update WebRTC APIs
[\#1487](https://github.com/matrix-org/matrix-js-sdk/pull/1487)
* Dehydrate and rehydrate devices
[\#1436](https://github.com/matrix-org/matrix-js-sdk/pull/1436)
* Keep local device after processing device list sync
[\#1490](https://github.com/matrix-org/matrix-js-sdk/pull/1490)
* Enforce logger module via lint rules
[\#1489](https://github.com/matrix-org/matrix-js-sdk/pull/1489)
* Extend method redactEvent with reason
[\#1462](https://github.com/matrix-org/matrix-js-sdk/pull/1462)
* Catch exception from call event handler
[\#1484](https://github.com/matrix-org/matrix-js-sdk/pull/1484)
* Ignore invalid candidates
[\#1483](https://github.com/matrix-org/matrix-js-sdk/pull/1483)
* Always push docs if they are generated
[\#1478](https://github.com/matrix-org/matrix-js-sdk/pull/1478)
* Only sign key backup with cross-signing keys when available
[\#1481](https://github.com/matrix-org/matrix-js-sdk/pull/1481)
* Upgrade dependencies
[\#1479](https://github.com/matrix-org/matrix-js-sdk/pull/1479)
Changes in [8.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.1) (2020-09-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.0...v8.4.1)
* Catch exception from call event handler
[\#1486](https://github.com/matrix-org/matrix-js-sdk/pull/1486)
* Ignore invalid candidates
[\#1485](https://github.com/matrix-org/matrix-js-sdk/pull/1485)
Changes in [8.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.0) (2020-09-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.0-rc.1...v8.4.0)
+1 -1
View File
@@ -307,7 +307,7 @@ The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[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
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
initialise the crypto layer.
+45 -38
View File
@@ -1,20 +1,21 @@
{
"name": "matrix-js-sdk",
"version": "8.4.0",
"version": "10.0.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:compile-browser && yarn build:minify-browser && yarn build:types",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 76 src spec",
"lint:js": "eslint --max-warnings 72 src spec",
"lint:types": "tsc --noEmit",
"test": "jest spec/ --coverage --testEnvironment node",
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
@@ -27,10 +28,11 @@
"matrix-org"
],
"main": "./lib/index.js",
"typings": "./lib/index.d.ts",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
@@ -46,51 +48,56 @@
"release.sh"
],
"dependencies": {
"@babel/runtime": "^7.8.3",
"@babel/runtime": "^7.12.5",
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.2",
"loglevel": "^1.6.4",
"qs": "^6.5.2",
"request": "^2.88.0",
"unhomoglyph": "^1.0.2"
"content-type": "^1.0.4",
"loglevel": "^1.7.1",
"qs": "^6.9.6",
"request": "^2.88.2",
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.7.6",
"@babel/preset-typescript": "^7.7.4",
"@babel/register": "^7.7.4",
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@types/jest": "^26.0.20",
"@types/node": "12",
"@types/request": "^2.48.4",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"@types/request": "^2.48.5",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"babel-jest": "^26.6.3",
"babelify": "^10.0.0",
"better-docs": "^2.3.2",
"browserify": "^16.5.0",
"browserify": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "7.3.1",
"eslint-config-matrix-org": "^0.1.2",
"eslint-plugin-babel": "^5.3.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.0.0",
"jest": "^24.9.0",
"jest-localstorage-mock": "^2.4.0",
"jsdoc": "^3.6.5",
"fake-indexeddb": "^3.1.2",
"jest": "^26.6.3",
"jest-localstorage-mock": "^2.4.6",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^1.2.3",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"rimraf": "^3.0.0",
"terser": "^4.4.3",
"tsify": "^4.0.1",
"typescript": "^3.7.3"
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"rimraf": "^3.0.2",
"terser": "^5.5.1",
"tsify": "^5.0.2",
"typescript": "^4.1.3"
},
"jest": {
"testEnvironment": "node"
}
},
"typings": "./lib/index.d.ts"
}
+50 -11
View File
@@ -94,6 +94,14 @@ if [ $# -ne 1 ]; then
exit 1
fi
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
# Ensure all dependencies are updated
yarn install --ignore-scripts
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
@@ -170,6 +178,19 @@ echo "yarn version"
# manually commit the result.
yarn version --no-git-tag-version --new-version "$release"
# For the published and dist versions of the package, we copy the
# `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if
# they exist). This small bit of gymnastics allows us to use the TypeScript
# source directly for development without needing to build before linting or
# testing.
for i in main typings
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
fi
done
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
then
@@ -204,11 +225,6 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
yarn install
# We haven't tagged yet, so tell the dist script what version
# it's building
@@ -327,6 +343,7 @@ if [ -z "$skip_jsdoc" ]; then
$release index.html
git add "$release"
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
git push origin gh-pages
fi
# if it is a pre-release, leave it on the release branch for now.
@@ -339,18 +356,40 @@ fi
echo "updating master branch"
git checkout master
git pull
git merge "$rel_branch"
git merge "$rel_branch" --no-edit
# push master and docs (if generated) to github
# push master to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# finally, merge master back onto develop (if it exists)
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
git checkout develop
git pull
git merge master
git merge master --no-edit
# When merging to develop, we need revert the `main` and `typings` fields if
# we adjusted them previously.
for i in main typings
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
git push origin develop
fi
+7
View File
@@ -69,6 +69,9 @@ export function TestClient(
this.deviceKeys = null;
this.oneTimeKeys = {};
this._callEventHandler = {
calls: new Map(),
};
}
TestClient.prototype.toString = function() {
@@ -230,3 +233,7 @@ TestClient.prototype.flushSync = function() {
logger.log(`${this}: flushSync completed`);
});
};
TestClient.prototype.isFallbackICEServerAllowed = function() {
return true;
};
+1 -1
View File
@@ -99,5 +99,5 @@ describe("Browserify Test", function() {
client.once("sync.unexpectedError", reject);
}),
]);
}, 10000);
}, 20000); // additional timeout as this test can take quite a while
});
+56
View File
@@ -971,4 +971,60 @@ describe("megolm", function() {
expect(event.getContent().body).toEqual('42');
});
});
it("Alice receives an untrusted megolm key, only to receive the trusted one shortly after", function() {
const testClient = new TestClient(
"@alice:localhost", "device2", "access_token2",
);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(groupSession.session_key());
const rawEvent = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
return testClient.client.initCrypto().then(() => {
const keys = [{
room_id: ROOM_ID,
algorithm: 'm.megolm.v1.aes-sha2',
session_id: groupSession.session_id(),
session_key: inboundGroupSession.export_session(0),
sender_key: testSenderKey,
}];
return testClient.client.importRoomKeys(keys, { untrusted: true });
}).then(() => {
const event = testUtils.mkEvent({
event: true,
...rawEvent,
room: ROOM_ID,
});
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
expect(event.isKeySourceUntrusted()).toBeTruthy();
});
}).then(() => {
const event = testUtils.mkEvent({
type: 'm.room_key',
content: {
room_id: ROOM_ID,
algorithm: 'm.megolm.v1.aes-sha2',
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
},
event: true,
});
event._senderCurve25519Key = testSenderKey;
return testClient.client._crypto._onRoomKeyEvent(event);
}).then(() => {
const event = testUtils.mkEvent({
event: true,
...rawEvent,
room: ROOM_ID,
});
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
expect(event.isKeySourceUntrusted()).toBeFalsy();
});
});
});
});
+1 -1
View File
@@ -31,5 +31,5 @@ try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support: some tests will fail');
logger.log('nodejs was compiled without crypto support: some tests will fail');
}
+72
View File
@@ -0,0 +1,72 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventEmitter } from "events";
import { ReEmitter } from "../../src/ReEmitter";
const EVENTNAME = "UnknownEntry";
class EventSource extends EventEmitter {
doTheThing() {
this.emit(EVENTNAME, "foo", "bar");
}
doAnError() {
this.emit('error');
}
}
class EventTarget extends EventEmitter {
}
describe("ReEmitter", function() {
it("Re-Emits events with the same args", function() {
const src = new EventSource();
const tgt = new EventTarget();
const handler = jest.fn();
tgt.on(EVENTNAME, handler);
const reEmitter = new ReEmitter(tgt);
reEmitter.reEmit(src, [EVENTNAME]);
src.doTheThing();
// Args should be the args passed to 'emit' after the event name, and
// also the source object of the event which re-emitter adds
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
});
it("Doesn't throw if no handler for 'error' event", function() {
const src = new EventSource();
const tgt = new EventTarget();
const reEmitter = new ReEmitter(tgt);
reEmitter.reEmit(src, ['error']);
// without the workaround in ReEmitter, this would throw
src.doAnError();
const handler = jest.fn();
tgt.on('error', handler);
src.doAnError();
// Now we've attached an error handler, it should be called
expect(handler).toHaveBeenCalled();
});
});
+11 -3
View File
@@ -26,6 +26,7 @@ import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import {OlmDevice} from "../../../src/crypto/OlmDevice";
import {logger} from '../../../src/logger';
const userId = "@alice:example.com";
@@ -51,7 +52,7 @@ const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
logger.warn('Not running megolm backup unit tests: libolm not present');
return;
}
@@ -83,9 +84,16 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => testKey,
});
const [pubKey, ab] = await info.getCrossSigningKey("master", masterKeyPub);
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(ab).toEqual({a: 106712, b: 106712});
// check that the pkSigning object corresponds to the pubKey
const signature = pkSigning.sign("message");
const util = new global.Olm.Utility();
try {
util.ed25519_verify(pubKey, "message", signature);
} finally {
util.free();
}
});
it.each(types)("should request a key from the cache callback (if set)" +
+60 -2
View File
@@ -51,6 +51,36 @@ const signedDeviceList = {
},
};
const signedDeviceList2 = {
"failures": {},
"device_keys": {
"@test2:sw1v.org": {
"QJVRHWAKGH": {
"signatures": {
"@test2:sw1v.org": {
"ed25519:QJVRHWAKGH":
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
},
},
"user_id": "@test2:sw1v.org",
"keys": {
"ed25519:QJVRHWAKGH":
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
"curve25519:QJVRHWAKGH":
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
},
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2",
],
"device_id": "QJVRHWAKGH",
"unsigned": {},
},
},
},
};
describe('DeviceList', function() {
let downloadSpy;
let cryptoStore;
@@ -69,14 +99,16 @@ describe('DeviceList', function() {
}
});
function createTestDeviceList() {
function createTestDeviceList(keyDownloadChunkSize = 250) {
const baseApis = {
downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org',
deviceId: 'HGKAWHRVJQ',
};
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
deviceLists.push(dl);
return dl;
}
@@ -148,4 +180,30 @@ describe('DeviceList', function() {
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
});
});
it("should download device keys in batches", function() {
const dl = createTestDeviceList(1);
dl.startTrackingDeviceList('@test1:sw1v.org');
dl.startTrackingDeviceList('@test2:sw1v.org');
const queryDefer1 = utils.defer();
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
const queryDefer2 = utils.defer();
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
expect(downloadSpy).toBeCalledTimes(2);
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
return prom1.then(() => {
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
});
});
});
+86
View File
@@ -190,5 +190,91 @@ describe("OlmDevice", function() {
// new session and will have called claimOneTimeKeys
expect(count).toBe(2);
});
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
// This test checks whether `ensureOlmSessionsForDevices` properly
// handles multiple tasks in flight ensuring some set of devices in
// common without deadlocks.
let claimRequestCount = 0;
const baseApis = {
claimOneTimeKeys: () => {
// simulate a very slow server (.5 seconds to respond)
claimRequestCount++;
return new Promise((resolve, reject) => {
setTimeout(reject, 500);
});
},
};
const deviceBobA = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-A": "akey",
},
}, "BOB-A");
const deviceBobB = DeviceInfo.fromStorage({
keys: {
"curve25519:BOB-B": "bkey",
},
}, "BOB-B");
// There's no required ordering of devices per user, so here we
// create two different orderings so that each task reserves a
// device the other task needs before continuing.
const devicesByUserAB = {
"@bob:example.com": [
deviceBobA,
deviceBobB,
],
};
const devicesByUserBA = {
"@bob:example.com": [
deviceBobB,
deviceBobA,
],
};
function alwaysSucceed(promise) {
// swallow any exception thrown by a promise, so that
// Promise.all doesn't abort
return promise.catch(() => {});
}
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserAB,
));
// After a single tick through the first task, it should have
// claimed ownership of all devices to avoid deadlocking others.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
aliceOlmDevice, baseApis, devicesByUserBA,
));
// The second task should not have changed the ownership count, as
// it's waiting on the first task.
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
// Track the tasks, but don't await them yet.
const promises = Promise.all([
task1,
task2,
]);
await new Promise((resolve) => {
setTimeout(resolve, 200);
});
// After .2s, the first task should have made an initial claim request.
expect(claimRequestCount).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(claimRequestCount).toBe(2);
});
});
});
+5 -2
View File
@@ -22,6 +22,7 @@ import {TestClient} from '../../TestClient';
import {HttpResponse, setHttpResponses} from '../../test-utils';
import { resetCrossSigningKeys } from "./crypto-utils";
import { MatrixError } from '../../../src/http-api';
import {logger} from '../../../src/logger';
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
@@ -49,7 +50,7 @@ async function makeTestClient(userInfo, options, keys) {
describe("Cross Signing", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
logger.warn('Not running megolm backup unit tests: libolm not present');
return;
}
@@ -192,7 +193,9 @@ describe("Cross Signing", function() {
const keyChangePromise = new Promise((resolve, reject) => {
alice.once("crossSigning.keysChanged", async (e) => {
resolve(e);
await alice.checkOwnCrossSigningTrust();
await alice.checkOwnCrossSigningTrust({
allowPrivateKeyRequests: true,
});
});
});
+6 -5
View File
@@ -22,6 +22,7 @@ import {TestClient} from '../../TestClient';
import {makeTestClients} from './verification/util';
import {encryptAES} from "../../../src/crypto/aes";
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
import {logger} from '../../../src/logger';
import * as utils from "../../../src/utils";
@@ -29,7 +30,7 @@ try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support');
logger.log('nodejs was compiled without crypto support');
}
async function makeTestClient(userInfo, options) {
@@ -60,7 +61,7 @@ function sign(obj, key, userId) {
describe("Secrets", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
logger.warn('Not running megolm backup unit tests: libolm not present');
return;
}
@@ -193,7 +194,7 @@ describe("Secrets", function() {
};
resetCrossSigningKeys(alice);
const newKeyId = await alice.addSecretStorageKey(
const { keyId: newKeyId } = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
);
// we don't await on this because it waits for the event to come down the sync
@@ -225,8 +226,8 @@ describe("Secrets", function() {
],
{
cryptoCallbacks: {
onSecretRequested: e => {
expect(e.name).toBe("foo");
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
expect(secretName).toBe("foo");
return "bar";
},
},
+8 -7
View File
@@ -18,18 +18,19 @@ limitations under the License.
import {TestClient} from '../../../TestClient';
import {MatrixEvent} from "../../../../src/models/event";
import nodeCrypto from "crypto";
import {logger} from '../../../../src/logger';
export async function makeTestClients(userInfos, options) {
const clients = [];
const clientMap = {};
const sendToDevice = function(type, map) {
// console.log(this.getUserId(), "sends", type, map);
// logger.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
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
type: type,
content: msg,
});
@@ -48,9 +49,9 @@ export async function makeTestClients(userInfos, options) {
};
const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
const rawEvent = {
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
type: type,
content: content,
room_id: room,
@@ -60,14 +61,14 @@ export async function makeTestClients(userInfos, options) {
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
},
}));
setImmediate(() => {
for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
console.log("sending remote echo!!");
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
logger.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
} else {
tc.client.emit("Room.timeline", event);
+15
View File
@@ -521,4 +521,19 @@ describe("MatrixClient", function() {
xit("should be able to peek into a room using peekInRoom", function(done) {
});
});
describe("getPresence", function() {
it("should send a presence HTTP GET", function() {
httpLookups = [{
method: "GET",
path: `/presence/${encodeURIComponent(userId)}/status`,
data: {
"presence": "unavailable",
"last_active_ago": 420845,
},
}];
client.getPresence(userId);
expect(httpLookups.length).toEqual(0);
});
});
});
+2 -2
View File
@@ -46,8 +46,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 babel/no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line babel/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);
+9 -2
View File
@@ -3,6 +3,7 @@ import {EventStatus, MatrixEvent} from "../../src/models/event";
import {EventTimeline} from "../../src/models/event-timeline";
import {RoomState} from "../../src/models/room-state";
import {Room} from "../../src/models/room";
import {TestClient} from "../TestClient";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -1176,7 +1177,10 @@ describe("Room", function() {
describe("addPendingEvent", function() {
it("should add pending events to the pendingEventList if " +
"pendingEventOrdering == 'detached'", function() {
const room = new Room(roomId, null, userA, {
const client = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const room = new Room(roomId, client, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
@@ -1226,7 +1230,10 @@ describe("Room", function() {
describe("updatePendingEvent", function() {
it("should remove cancelled events from the pending list", function() {
const room = new Room(roomId, null, userA, {
const client = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const room = new Room(roomId, client, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
+66
View File
@@ -17,6 +17,39 @@ limitations under the License.
import {SyncAccumulator} from "../../src/sync-accumulator";
// The event body & unsigned object get frozen to assert that they don't get altered
// by the impl
const RES_WITH_AGE = {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
timeline: {
events: [
Object.freeze({
content: {
body: "This thing is happening right now!",
},
origin_server_ts: 123456789,
sender: "@alice:localhost",
type: "m.room.message",
unsigned: Object.freeze({
age: 50,
}),
}),
],
prev_batch: "something",
},
},
},
},
};
describe("SyncAccumulator", function() {
let sa;
@@ -368,6 +401,39 @@ describe("SyncAccumulator", function() {
expect(summary["m.joined_member_count"]).toEqual(5);
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
});
it("should return correctly adjusted age attributes", () => {
const delta = 1000;
const startingTs = 1000;
const oldDateNow = Date.now;
try {
Date.now = jest.fn();
Date.now.mockReturnValue(startingTs);
sa.accumulate(RES_WITH_AGE);
Date.now.mockReturnValue(startingTs + delta);
const output = sa.getJSON();
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual(
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta,
);
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
);
} finally {
Date.now = oldDateNow;
}
});
it("should mangle age without adding extra keys", () => {
sa.accumulate(RES_WITH_AGE);
const output = sa.getJSON();
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
);
});
});
});
+26
View File
@@ -282,4 +282,30 @@ describe("utils", function() {
expect(target.nonenumerableProp).toBe(undefined);
});
});
describe("chunkPromises", function() {
it("should execute promises in chunks", async function() {
let promiseCount = 0;
function fn1() {
return new Promise(async function(resolve, reject) {
await utils.sleep(1);
expect(promiseCount).toEqual(0);
++promiseCount;
resolve();
});
}
function fn2() {
return new Promise(function(resolve, reject) {
expect(promiseCount).toEqual(1);
++promiseCount;
resolve();
});
}
await utils.chunkPromises([fn1, fn2], 1);
expect(promiseCount).toEqual(2);
});
});
});
+257
View File
@@ -0,0 +1,257 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {TestClient} from '../../TestClient';
import {MatrixCall, CallErrorCode} from '../../../src/webrtc/call';
const DUMMY_SDP = (
"v=0\r\n" +
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
"a=ice-options:trickle\r\n" +
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
"a=setup:active\r\n" +
"a=mid:0\r\n" +
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
"a=sendrecv\r\n" +
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
"a=rtcp-mux\r\n" +
"a=rtpmap:111 opus/48000/2\r\n" +
"a=rtcp-fb:111 transport-cc\r\n" +
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
"a=rtpmap:103 ISAC/16000\r\n" +
"a=rtpmap:104 ISAC/32000\r\n" +
"a=rtpmap:9 G722/8000\r\n" +
"a=rtpmap:0 PCMU/8000\r\n" +
"a=rtpmap:8 PCMA/8000\r\n" +
"a=rtpmap:106 CN/32000\r\n" +
"a=rtpmap:105 CN/16000\r\n" +
"a=rtpmap:13 CN/8000\r\n" +
"a=rtpmap:110 telephone-event/48000\r\n" +
"a=rtpmap:112 telephone-event/32000\r\n" +
"a=rtpmap:113 telephone-event/16000\r\n" +
"a=rtpmap:126 telephone-event/8000\r\n" +
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
);
class MockRTCPeerConnection {
localDescription: RTCSessionDescription;
constructor() {
this.localDescription = {
sdp: DUMMY_SDP,
type: 'offer',
toJSON: function() {},
};
}
addEventListener() {}
createOffer() {
return Promise.resolve({});
}
setRemoteDescription() {
return Promise.resolve();
}
setLocalDescription() {
return Promise.resolve();
}
close() {}
getStats() { return []; }
}
describe('Call', function() {
let client;
let call;
let prevNavigator;
let prevDocument;
let prevWindow;
beforeEach(function() {
prevNavigator = global.navigator;
prevDocument = global.document;
prevWindow = global.window;
global.navigator = {
mediaDevices: {
// @ts-ignore Mock
getUserMedia: () => {
return {
getTracks: () => [],
getAudioTracks: () => [],
getVideoTracks: () => [],
};
},
},
};
global.window = {
// @ts-ignore Mock
RTCPeerConnection: MockRTCPeerConnection,
// @ts-ignore Mock
RTCSessionDescription: {},
// @ts-ignore Mock
RTCIceCandidate: {},
getUserMedia: {},
};
// @ts-ignore Mock
global.document = {};
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
// We just stub out sendEvent: we're not interested in testing the client's
// event sending code here
client.client.sendEvent = () => {};
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
call = new MatrixCall({
client: client.client,
roomId: '!foo:bar',
});
// call checks one of these is wired up
call.on('error', () => {});
});
afterEach(function() {
client.stop();
global.navigator = prevNavigator;
global.window = prevWindow;
global.document = prevDocument;
});
it('should ignore candidate events from non-matching party ID', async function() {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
call.peerConn.addIceCandidate = jest.fn();
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
candidates: [
{
candidate: '',
sdpMid: '',
},
],
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'some_other_party_id',
candidates: [
{
candidate: '',
sdpMid: '',
},
],
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
// Hangup to stop timers
call.hangup(CallErrorCode.UserHangup, true);
});
it('should add candidates received before answer if party ID is correct', async function() {
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
call.peerConn.addIceCandidate = jest.fn();
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
candidates: [
{
candidate: 'the_correct_candidate',
sdpMid: '',
},
],
};
},
});
call.onRemoteIceCandidatesReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'some_other_party_id',
candidates: [
{
candidate: 'the_wrong_candidate',
sdpMid: '',
},
],
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
await call.onAnswerReceived({
getContent: () => {
return {
version: 1,
call_id: call.callId,
party_id: 'the_correct_party_id',
answer: {
sdp: DUMMY_SDP,
},
};
},
});
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
expect(call.peerConn.addIceCandidate).toHaveBeenCalledWith({
candidate: 'the_correct_candidate',
sdpMid: '',
});
});
});
+100
View File
@@ -0,0 +1,100 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export enum EventType {
// Room state events
RoomCanonicalAlias = "m.room.canonical_alias",
RoomCreate = "m.room.create",
RoomJoinRules = "m.room.join_rules",
RoomMember = "m.room.member",
RoomThirdPartyInvite = "m.room.third_party_invite",
RoomPowerLevels = "m.room.power_levels",
RoomName = "m.room.name",
RoomTopic = "m.room.topic",
RoomAvatar = "m.room.avatar",
RoomPinnedEvents = "m.room.pinned_events",
RoomEncryption = "m.room.encryption",
RoomHistoryVisibility = "m.room.history_visibility",
RoomGuestAccess = "m.room.guest_access",
RoomServerAcl = "m.room.server_acl",
RoomTombstone = "m.room.tombstone",
/**
* @deprecated Should not be used.
*/
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
// Spaces MSC1772
SpaceChild = "org.matrix.msc1772.space.child",
SpaceParent = "org.matrix.msc1772.space.parent",
// Room timeline events
RoomRedaction = "m.room.redaction",
RoomMessage = "m.room.message",
RoomMessageEncrypted = "m.room.encrypted",
Sticker = "m.sticker",
CallInvite = "m.call.invite",
CallCandidates = "m.call.candidates",
CallAnswer = "m.call.answer",
CallHangup = "m.call.hangup",
CallReject = "m.call.reject",
CallSelectAnswer = "m.call.select_answer",
CallNegotiate = "m.call.negotiate",
CallReplaces = "m.call.replaces",
KeyVerificationRequest = "m.key.verification.request",
KeyVerificationStart = "m.key.verification.start",
KeyVerificationCancel = "m.key.verification.cancel",
KeyVerificationMac = "m.key.verification.mac",
KeyVerificationDone = "m.key.verification.done",
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
RoomMessageFeedback = "m.room.message.feedback",
// Room ephemeral events
Typing = "m.typing",
Receipt = "m.receipt",
Presence = "m.presence",
// Room account_data events
FullyRead = "m.fully_read",
Tag = "m.tag",
// User account_data events
PushRules = "m.push_rules",
Direct = "m.direct",
IgnoredUserList = "m.ignored_user_list",
// to_device events
RoomKey = "m.room_key",
RoomKeyRequest = "m.room_key_request",
ForwardedRoomKey = "m.forwarded_room_key",
Dummy = "m.dummy",
}
export enum MsgType {
Text = "m.text",
Emote = "m.emote",
Notice = "m.notice",
Image = "m.image",
File = "m.file",
Audio = "m.audio",
Location = "m.location",
Video = "m.video",
}
export const RoomCreateTypeField = "org.matrix.msc1772.type"; // Spaces MSC1772
export enum RoomType {
Space = "org.matrix.msc1772.space", // Spaces MSC1772
}
+60 -4
View File
@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as Olm from "olm";
// this is needed to tell TS about global.Olm
import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
export {};
@@ -22,10 +23,65 @@ declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
Olm: Olm;
}
}
interface Global {
Olm: Olm;
interface Window {
electron?: Electron;
}
interface Electron {
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
}
interface MediaDevices {
// This is experimental and types don't know about it yet
// https://github.com/microsoft/TypeScript/issues/33232
getDisplayMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
}
interface DesktopCapturerConstraints {
audio: boolean | {
mandatory: {
chromeMediaSource: string;
chromeMediaSourceId: string;
};
};
video: boolean | {
mandatory: {
chromeMediaSource: string;
chromeMediaSourceId: string;
};
};
}
interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
}
interface GetSourcesOptions {
types: Array<string>;
thumbnailSize?: {
height: number;
width: number;
};
fetchWindowIcons?: boolean;
}
interface HTMLAudioElement {
// sinkId & setSinkId are experimental and typescript doesn't know about them
sinkId: string;
setSinkId(outputId: string);
}
interface DummyInterfaceWeShouldntBeUsingThis {}
interface Navigator {
// We check for the webkit-prefixed getUserMedia to detect if we're
// on webkit: we should check if we still need to do this
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
}
}
-52
View File
@@ -1,52 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 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
*/
export class ReEmitter {
constructor(target) {
this.target = target;
// We keep one bound event handler for each event name so we know
// what event is arriving
this.boundHandlers = {};
}
_handleEvent(eventName, ...args) {
this.target.emit(eventName, ...args);
}
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 = forSource.bind(this, this.boundHandlers[eventName]);
source.on(eventName, boundHandler);
}
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 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";
export class ReEmitter {
private target: EventEmitter;
constructor(target: EventEmitter) {
this.target = target;
}
reEmit(source: EventEmitter, eventNames: string[]) {
for (const eventName of 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 = (...args) => {
// EventEmitter special cases 'error' to make the emit function throw if no
// handler is attached, which sort of makes sense for making sure that something
// handles an error, but for re-emitting, there could be a listener on the original
// source object so the test doesn't really work. We *could* try to replicate the
// same logic and throw if there is no listener on either the source or the target,
// but this behaviour is fairly undesireable for us anyway: the main place we throw
// 'error' events is for calls, where error events are usually emitted some time
// later by a different part of the code where 'emit' throwing because the app hasn't
// added an error handler isn't terribly helpful. (A better fix in retrospect may
// have been to just avoid using the event name 'error', but backwards compat...)
if (eventName === 'error' && this.target.listenerCount('error') === 0) return;
this.target.emit(eventName, ...args, source);
};
source.on(eventName, forSource);
}
}
}
+74 -5
View File
@@ -246,10 +246,25 @@ MatrixBaseApis.prototype.register = function(
/**
* Register a guest account.
* This method returns the auth info needed to create a new authenticated client,
* Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g:
* ```javascript
* const tmpClient = await sdk.createClient(MATRIX_INSTANCE);
* const { user_id, device_id, access_token } = tmpClient.registerGuest();
* const client = createClient({
* baseUrl: MATRIX_INSTANCE,
* accessToken: access_token,
* userId: user_id,
* deviceId: device_id,
* })
* client.setGuest(true);
* ```
*
* @param {Object=} opts Registration options
* @param {Object} opts.body JSON HTTP body to provide.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: JSON object that contains:
* { user_id, device_id, access_token, home_server }
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
@@ -356,15 +371,22 @@ MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
* authenticates with the SSO.
* @param {string} loginType The type of SSO login we are doing (sso or cas).
* Defaults to 'sso'.
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
* @return {string} The HS URL to hit to begin the SSO login process.
*/
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType, idpId) {
if (loginType === undefined) {
loginType = "sso";
}
return this._http.getUrl("/login/"+loginType+"/redirect", {
"redirectUrl": redirectUrl,
}, PREFIX_R0);
let prefix = PREFIX_R0;
let url = "/login/" + loginType + "/redirect";
if (idpId) {
url += "/" + idpId;
prefix = "/_matrix/client/unstable/org.matrix.msc2858";
}
return this._http.getUrl(url, { redirectUrl }, prefix);
};
/**
@@ -1511,6 +1533,21 @@ MatrixBaseApis.prototype.getDevices = function() {
);
};
/**
* Gets specific device details for the logged-in user
* @param {string} device_id device to query
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getDevice = function(device_id) {
const path = utils.encodeUri("/devices/$device_id", {
$device_id: device_id,
});
return this._http.authedRequest(
undefined, 'GET', path, undefined, undefined,
);
};
/**
* Update the given device
*
@@ -2367,3 +2404,35 @@ MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason)
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
};
/**
* Fetches or paginates a summary of a space as defined by MSC2946
* @param {string} roomId The ID of the space-room to use as the root of the summary.
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
* @param {number?} limit The maximum number of rooms to return in total.
* @param {string?} batch The opaque token to paginate a previous summary request.
* @returns {Promise} the response, with next_batch, rooms, events fields.
*/
MatrixBaseApis.prototype.getSpaceSummary = function(
roomId,
maxRoomsPerSpace,
suggestedOnly,
autoJoinOnly,
limit,
batch,
) {
const path = utils.encodeUri("/rooms/$roomId/spaces", {
$roomId: roomId,
});
return this._http.authedRequest(undefined, "POST", path, null, {
max_rooms_per_space: maxRoomsPerSpace,
suggested_only: suggestedOnly,
auto_join_only: autoJoinOnly,
limit,
batch,
}, {
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
});
};
+362 -271
View File
@@ -33,6 +33,7 @@ import {EventTimeline} from "./models/event-timeline";
import {SearchResult} from "./models/search-result";
import {StubStore} from "./store/stub";
import {createNewMatrixCall} from "./webrtc/call";
import {CallEventHandler} from './webrtc/callEventHandler';
import * as utils from './utils';
import {sleep} from './utils';
import {
@@ -55,10 +56,12 @@ import {PushProcessor} from "./pushprocessor";
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
import { User } from "./models/user";
import {AutoDiscovery} from "./autodiscovery";
import {DEHYDRATION_ALGORITHM} from "./crypto/dehydration";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
const TURN_CHECK_INTERVAL = 10 * 60 * 1000; // poll for turn credentials every 10 minutes
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
const keys = [];
@@ -178,6 +181,14 @@ function keyFromRecoverySession(session, decryptionKey) {
* @param {boolean} [opts.forceTURN]
* Optional. Whether relaying calls through a TURN server should be forced.
*
* * @param {boolean} [opts.iceCandidatePoolSize]
* Optional. Up to this many ICE candidates will be gathered when an incoming call arrives.
* Gathering does not send data to the caller, but will communicate with the configured TURN
* server. Default 0.
*
* @param {boolean} [opts.supportsCallTransfer]
* Optional. True to advertise support for call transfers to other parties on Matrix calls.
*
* @param {boolean} [opts.fallbackICEServerAllowed]
* Optional. Whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to false.
@@ -234,7 +245,14 @@ function keyFromRecoverySession(session, decryptionKey) {
* {
* keys: {
* <key name>: {
* pubkey: {UInt8Array}
* "algorithm": "m.secret_storage.v1.aes-hmac-sha2",
* "passphrase": {
* "algorithm": "m.pbkdf2",
* "iterations": 500000,
* "salt": "..."
* },
* "iv": "...",
* "mac": "..."
* }, ...
* }
* }
@@ -246,6 +264,7 @@ function keyFromRecoverySession(session, decryptionKey) {
* desired to avoid user prompts.
* Args:
* {string} keyId the ID of the new key
* {object} keyInfo Infomation about the key as above for `getSecretStorageKey`
* {Uint8Array} key the new private key
*
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
@@ -327,17 +346,15 @@ export function MatrixClient(opts) {
}
this.clientRunning = false;
this.callList = {
// callId: MatrixCall
};
// try constructing a MatrixCall to see if we are running in an environment
// which has WebRTC. If we are, listen for and handle m.call.* events.
const call = createNewMatrixCall(this);
this._supportsVoip = false;
if (call) {
setupCallEventHandler(this);
this._callEventHandler = new CallEventHandler(this);
this._supportsVoip = true;
} else {
this._callEventHandler = null;
}
this._syncingRetry = null;
this._syncApi = null;
@@ -356,6 +373,8 @@ export function MatrixClient(opts) {
this._cryptoCallbacks = opts.cryptoCallbacks || {};
this._forceTURN = opts.forceTURN || false;
this._iceCandidatePoolSize = opts.iceCandidatePoolSize === undefined ? 0 : opts.iceCandidatePoolSize;
this._supportsCallTransfer = opts.supportsCallTransfer || false;
this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
// List of which rooms have encryption enabled: separate from crypto because
@@ -375,6 +394,10 @@ export function MatrixClient(opts) {
this._clientWellKnown = undefined;
this._clientWellKnownPromise = undefined;
this._turnServers = [];
this._turnServersExpiry = 0;
this._checkTurnServersIntervalID = null;
// The SDK doesn't really provide a clean way for events to recalculate the push
// actions for themselves, so we have to kinda help them out when they are encrypted.
// We do this so that push rules are correctly executed on events in their decrypted
@@ -459,6 +482,153 @@ export function MatrixClient(opts) {
utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
/**
* Try to rehydrate a device if available. The client must have been
* initialized with a `cryptoCallback.getDehydrationKey` option, and this
* function must be called before initCrypto and startClient are called.
*
* @return {Promise} Resolves to undefined if a device could not be dehydrated, or
* to the new device ID if the dehydration was successful.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.rehydrateDevice = async function() {
if (this._crypto) {
throw new Error("Cannot rehydrate device after crypto is initialized");
}
if (!this._cryptoCallbacks.getDehydrationKey) {
return;
}
const getDeviceResult = await this.getDehydratedDevice();
if (!getDeviceResult) {
return;
}
if (!getDeviceResult.device_data || !getDeviceResult.device_id) {
logger.info("no dehydrated device found");
return;
}
const account = new global.Olm.Account();
try {
const deviceData = getDeviceResult.device_data;
if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) {
logger.warn("Wrong algorithm for dehydrated device");
return;
}
logger.log("unpickling dehydrated device");
const key = await this._cryptoCallbacks.getDehydrationKey(
deviceData,
(k) => {
// copy the key so that it doesn't get clobbered
account.unpickle(new Uint8Array(k), deviceData.account);
},
);
account.unpickle(key, deviceData.account);
logger.log("unpickled device");
const rehydrateResult = await this._http.authedRequest(
undefined,
"POST",
"/dehydrated_device/claim",
undefined,
{
device_id: getDeviceResult.device_id,
},
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
if (rehydrateResult.success === true) {
this.deviceId = getDeviceResult.device_id;
logger.info("using dehydrated device");
const pickleKey = this.pickleKey || "DEFAULT_KEY";
this._exportedOlmDeviceToImport = {
pickledAccount: account.pickle(pickleKey),
sessions: [],
pickleKey: pickleKey,
};
account.free();
return this.deviceId;
} else {
account.free();
logger.info("not using dehydrated device");
return;
}
} catch (e) {
account.free();
logger.warn("could not unpickle", e);
}
};
/**
* Get the current dehydrated device, if any
* @return {Promise} A promise of an object containing the dehydrated device
*/
MatrixClient.prototype.getDehydratedDevice = async function() {
try {
return await this._http.authedRequest(
undefined,
"GET",
"/dehydrated_device",
undefined, undefined,
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
} catch (e) {
logger.info("could not get dehydrated device", e.toString());
return;
}
};
/**
* Set the dehydration key. This will also periodically dehydrate devices to
* the server.
*
* @param {Uint8Array} key the dehydration key
* @param {object} [keyInfo] Information about the key. Primarily for
* information about how to generate the key from a passphrase.
* @param {string} [deviceDisplayName] The device display name for the
* dehydrated device.
* @return {Promise} A promise that resolves when the dehydrated device is stored.
*/
MatrixClient.prototype.setDehydrationKey = async function(
key, keyInfo = {}, deviceDisplayName = undefined,
) {
if (!(this._crypto)) {
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
return await this._crypto._dehydrationManager.setKeyAndQueueDehydration(
key, keyInfo, deviceDisplayName,
);
};
/**
* Creates a new dehydrated device (without queuing periodic dehydration)
* @param {Uint8Array} key the dehydration key
* @param {object} [keyInfo] Information about the key. Primarily for
* information about how to generate the key from a passphrase.
* @param {string} [deviceDisplayName] The device display name for the
* dehydrated device.
* @return {Promise<String>} the device id of the newly created dehydrated device
*/
MatrixClient.prototype.createDehydratedDevice = async function(
key, keyInfo = {}, deviceDisplayName = undefined,
) {
if (!(this._crypto)) {
logger.warn('not dehydrating device if crypto is not enabled');
return;
}
await this._crypto._dehydrationManager.setKey(
key, keyInfo, deviceDisplayName,
);
return await this._crypto._dehydrationManager.dehydrateDevice();
};
MatrixClient.prototype.exportDevice = async function() {
if (!(this._crypto)) {
logger.warn('not exporting device if crypto is not enabled');
@@ -551,6 +721,14 @@ MatrixClient.prototype.setForceTURN = function(forceTURN) {
this._forceTURN = forceTURN;
};
/**
* Set whether to advertise transfer support to other parties on Matrix calls.
* @param {bool} supportsCallTransfer True to advertise the 'm.call.transferee' capability
*/
MatrixClient.prototype.setSupportsCallTransfer = function(supportsCallTransfer) {
this._supportsCallTransfer = supportsCallTransfer;
};
/**
* Get the current sync state.
* @return {?string} the sync state, which may be null.
@@ -1318,7 +1496,7 @@ wrapCryptoFuncs(MatrixClient, [
* containing the key, or rejects if the key cannot be obtained.
* Returns:
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
* SecretStorage#addKey: an object with `passphrase` etc fields.
*/
/**
@@ -1333,7 +1511,9 @@ wrapCryptoFuncs(MatrixClient, [
* @param {string} [keyName] the name of the key. If not given, a random
* name will be generated.
*
* @return {string} the name of the key
* @return {object} An object with:
* keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase)
*/
/**
@@ -1893,7 +2073,7 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
*
* @param {string} password Passphrase
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<Buffer>} key backup key
* @return {Promise<Uint8Array>} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromPassword = function(
password, backupInfo,
@@ -1908,7 +2088,7 @@ MatrixClient.prototype.keyBackupKeyFromPassword = function(
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} recoveryKey The recovery key
* @return {Buffer} key backup key
* @return {Uint8Array} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) {
return decodeRecoveryKey(recoveryKey);
@@ -2054,7 +2234,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
// This is async.
this._crypto.storeSessionBackupPrivateKey(privKey)
.catch((e) => {
console.warn("Error caching session backup key:", e);
logger.warn("Error caching session backup key:", e);
}).then(cacheCompleteCallback);
if (progressCallback) {
@@ -2123,6 +2303,39 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio
);
};
/**
* Share shared-history decryption keys with the given users.
*
* @param {string} roomId the room for which keys should be shared.
* @param {array} userIds a list of users to share with. The keys will be sent to
* all of the user's current devices.
*/
MatrixClient.prototype.sendSharedHistoryKeys = async function(roomId, userIds) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const roomEncryption = this._roomList.getRoomEncryption(roomId);
if (!roomEncryption) {
// unknown room, or unencrypted room
logger.error("Unknown room. Not sharing decryption keys");
return;
}
const deviceInfos = await this._crypto.downloadKeys(userIds);
const devicesByUser = {};
for (const [userId, devices] of Object.entries(deviceInfos)) {
devicesByUser[userId] = Object.values(devices);
}
const alg = this._crypto._getRoomDecryptor(roomId, roomEncryption.algorithm);
if (alg.sendSharedHistoryInboundSessions) {
await alg.sendSharedHistoryInboundSessions(devicesByUser);
} else {
logger.warning("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
}
};
// Group ops
// =========
// Operations on groups that come down the sync stream (ie. ones the
@@ -2858,14 +3071,19 @@ function _sendEventHttpRequest(client, event) {
* @param {string} eventId
* @param {string} [txnId] transaction id. One will be made up if not
* supplied.
* @param {module:client.callback} callback Optional.
* @param {object|module:client.callback} callbackOrOpts
* Options to pass on, may contain `reason`.
* Can be callback for backwards compatibility.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback) {
MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callbackOrOpts) {
const opts = typeof(callbackOrOpts) === 'object' ? callbackOrOpts : {};
const reason = opts.reason;
const callback = typeof(callbackOrOpts) === 'function' ? callbackOrOpts : undefined;
return this._sendCompleteEvent(roomId, {
type: "m.room.redaction",
content: {},
content: { reason: reason },
redacts: eventId,
}, txnId, callback);
};
@@ -3277,11 +3495,12 @@ MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=fals
* @param {string} roomId
* @param {string} userId
* @param {module:client.callback} callback Optional.
* @param {string} reason Optional.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.invite = function(roomId, userId, callback) {
return _membershipChange(this, roomId, userId, "invite", undefined,
MatrixClient.prototype.invite = function(roomId, userId, callback, reason) {
return _membershipChange(this, roomId, userId, "invite", reason,
callback);
};
@@ -3569,25 +3788,39 @@ MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
/**
* @param {string} name
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setDisplayName = function(name, callback) {
return this.setProfileInfo(
MatrixClient.prototype.setDisplayName = async function(name, callback) {
const prom = await this.setProfileInfo(
"displayname", { displayname: name }, callback,
);
// XXX: synthesise a profile update for ourselves because Synapse is broken and won't
const user = this.getUser(this.getUserId());
if (user) {
user.displayName = name;
user.emit("User.displayName", user.events.presence, user);
}
return prom;
};
/**
* @param {string} url
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {Promise} Resolves: {} an empty object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setAvatarUrl = function(url, callback) {
return this.setProfileInfo(
MatrixClient.prototype.setAvatarUrl = async function(url, callback) {
const prom = await this.setProfileInfo(
"avatar_url", { avatar_url: url }, callback,
);
// XXX: synthesise a profile update for ourselves because Synapse is broken and won't
const user = this.getUser(this.getUserId());
if (user) {
user.avatarUrl = url;
user.emit("User.avatarUrl", user.events.presence, user);
}
return prom;
};
/**
@@ -3663,6 +3896,19 @@ MatrixClient.prototype.setPresence = function(opts, callback) {
);
};
/**
* @param {string} userId The user to get presence for
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: The presence state for this user.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getPresence = function(userId, callback) {
const path = utils.encodeUri("/presence/$userId/status", {
$userId: userId,
});
return this._http.authedRequest(callback, "GET", path, undefined, undefined);
};
/**
* Retrieve older messages from the given room and put them in the timeline.
@@ -4745,6 +4991,57 @@ MatrixClient.prototype.getTurnServers = function() {
return this._turnServers || [];
};
/**
* Get the unix timestamp (in seconds) at which the current
* TURN credentials (from getTurnServers) expire
* @return {number} The expiry timestamp, in seconds, or null if no credentials
*/
MatrixClient.prototype.getTurnServersExpiry = function() {
return this._turnServersExpiry;
};
MatrixClient.prototype._checkTurnServers = async function() {
if (!this._supportsVoip) {
return;
}
let credentialsGood = false;
const remainingTime = this._turnServersExpiry - Date.now();
if (remainingTime > TURN_CHECK_INTERVAL) {
logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
credentialsGood = true;
} else {
logger.debug("Fetching new TURN credentials");
try {
const res = await this.turnServer();
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
// map the response to a format that can be fed to RTCPeerConnection
const servers = {
urls: res.uris,
username: res.username,
credential: res.password,
};
this._turnServers = [servers];
// The TTL is in seconds but we work in ms
this._turnServersExpiry = Date.now() + (res.ttl * 1000);
credentialsGood = true;
}
} catch (err) {
logger.error("Failed to get TURN URIs", err);
// If we get a 403, there's no point in looping forever.
if (err.httpStatus === 403) {
logger.info("TURN access unavailable for this account: stopping credentials checks");
if (this._checkTurnServersIntervalID !== null) global.clearInterval(this._checkTurnServersIntervalID);
this._checkTurnServersIntervalID = null;
}
}
// otherwise, if we failed for whatever reason, try again the next time we're called.
}
return credentialsGood;
};
/**
* Set whether to allow a fallback ICE server should be used for negotiating a
* WebRTC connection if the homeserver doesn't provide any servers. Defaults to
@@ -4897,7 +5194,12 @@ MatrixClient.prototype.startClient = async function(opts) {
}
// periodically poll for turn servers if we support voip
checkTurnServers(this);
if (this._supportsVoip) {
this._checkTurnServersIntervalID = setInterval(() => {
this._checkTurnServers();
}, TURN_CHECK_INTERVAL);
this._checkTurnServers();
}
if (this._syncApi) {
// This shouldn't happen since we thought the client was not running
@@ -4965,6 +5267,26 @@ MatrixClient.prototype._storeClientOptions = function() {
return this.store.storeClientOptions(serializableOpts);
};
/**
* Gets a set of room IDs in common with another user
* @param {string} userId The userId to check.
* @return {Promise<string[]>} Resolves to a set of rooms
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype._unstable_getSharedRooms = async function(userId) {
if (!(await this.doesServerSupportUnstableFeature("uk.half-shot.msc2666"))) {
throw Error('Server does not support shared_rooms API');
}
const path = utils.encodeUri("/uk.half-shot.msc2666/user/shared_rooms/$userId", {
$userId: userId,
});
const res = await this._http.authedRequest(
undefined, "GET", path, undefined, undefined,
{prefix: PREFIX_UNSTABLE},
);
return res.joined;
};
/**
* High level helper method to stop the client from polling and allow a
* clean shutdown.
@@ -4984,7 +5306,12 @@ MatrixClient.prototype.stopClient = function() {
if (this._peekSync) {
this._peekSync.stopPeeking();
}
global.clearTimeout(this._checkTurnServersTimeoutID);
if (this._callEventHandler) {
this._callEventHandler.stop();
this._callEventHandler = null;
}
global.clearInterval(this._checkTurnServersIntervalID);
if (this._clientWellKnownIntervalID !== undefined) {
global.clearInterval(this._clientWellKnownIntervalID);
}
@@ -5008,7 +5335,12 @@ MatrixClient.prototype.getVersions = function() {
{
prefix: '',
},
);
).catch((e) => {
// Need to unset this if it fails, otherwise we'll never retry
this._serverVersionsPromise = null;
// but rethrow the exception to anything that was waiting
throw e;
});
return this._serverVersionsPromise;
};
@@ -5186,6 +5518,9 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
}));
events = events.filter(e => e.getType() === eventType);
}
if (originalEvent && relationType === "m.replace") {
events = events.filter(e => e.getSender() === originalEvent.getSender());
}
return {
originalEvent,
events,
@@ -5193,249 +5528,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
};
};
function setupCallEventHandler(client) {
const candidatesByCall = {
// callId: [Candidate]
};
// The sync code always emits one event at a time, so it will patiently
// wait for us to finish processing a call invite before delivering the
// next event, even if that next event is a hangup. We therefore accumulate
// all our call events and then process them on the 'sync' event, ie.
// each time a sync has completed. This way, we can avoid emitting incoming
// call events if we get both the invite and answer/hangup in the same sync.
// This happens quite often, eg. replaying sync from storage, catchup sync
// after loading and after we've been offline for a bit.
let callEventBuffer = [];
function evaluateEventBuffer() {
if (client.getSyncState() === "SYNCING") {
// don't process any events until they are all decrypted
if (callEventBuffer.some((e) => e.isBeingDecrypted())) return;
const ignoreCallIds = {}; // Set<String>
// inspect the buffer and mark all calls which have been answered
// or hung up before passing them to the call event handler.
for (let i = callEventBuffer.length - 1; i >= 0; i--) {
const ev = callEventBuffer[i];
if (ev.getType() === "m.call.answer" ||
ev.getType() === "m.call.hangup") {
ignoreCallIds[ev.getContent().call_id] = "yep";
}
}
// now loop through the buffer chronologically and inject them
callEventBuffer.forEach(function(e) {
if (
e.getType() === "m.call.invite" &&
ignoreCallIds[e.getContent().call_id]
) {
// This call has previously been answered or hung up: ignore it
return;
}
callEventHandler(e);
});
callEventBuffer = [];
}
}
client.on("sync", evaluateEventBuffer);
function onEvent(event) {
// any call events or ones that might be once they're decrypted
if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) {
// queue up for processing once all events from this sync have been
// processed (see above).
callEventBuffer.push(event);
}
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted.
event.once("Event.decrypted", () => {
if (event.getType().indexOf("m.call.") === -1) return;
if (callEventBuffer.includes(event)) {
// we were waiting for that event to decrypt, so recheck the buffer
evaluateEventBuffer();
} else {
// This one wasn't buffered so just run the event handler for it
// straight away
callEventHandler(event);
}
});
}
}
client.on("event", onEvent);
function callEventHandler(event) {
const content = event.getContent();
let call = content.call_id ? client.callList[content.call_id] : undefined;
let i;
//console.info("RECV %s content=%s", event.getType(), JSON.stringify(content));
if (event.getType() === "m.call.invite") {
if (event.getSender() === client.credentials.userId) {
return; // ignore invites you send
}
if (event.getAge() > content.lifetime) {
return; // expired call
}
if (call && call.state === "ended") {
return; // stale/old invite event
}
if (call) {
logger.log(
"WARN: Already have a MatrixCall with id %s but got an " +
"invite. Clobbering.",
content.call_id,
);
}
call = createNewMatrixCall(client, event.getRoomId(), {
forceTURN: client._forceTURN,
});
if (!call) {
logger.log(
"Incoming call ID " + content.call_id + " but this client " +
"doesn't support WebRTC",
);
// don't hang up the call: there could be other clients
// connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
return;
}
call.callId = content.call_id;
call._initWithInvite(event);
client.callList[call.callId] = call;
// if we stashed candidate events for that call ID, play them back now
if (candidatesByCall[call.callId]) {
for (i = 0; i < candidatesByCall[call.callId].length; i++) {
call._gotRemoteIceCandidate(
candidatesByCall[call.callId][i],
);
}
}
// Were we trying to call that user (room)?
let existingCall;
const existingCalls = utils.values(client.callList);
for (i = 0; i < existingCalls.length; ++i) {
const thisCall = existingCalls[i];
if (call.roomId === thisCall.roomId &&
thisCall.direction === 'outbound' &&
(["wait_local_media", "create_offer", "invite_sent"].indexOf(
thisCall.state) !== -1)) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
// If we've only got to wait_local_media or create_offer and
// we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison)
if (existingCall.state === 'wait_local_media' ||
existingCall.state === 'create_offer' ||
existingCall.callId > call.callId) {
logger.log(
"Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId,
);
existingCall._replacedBy(call);
call.answer();
} else {
logger.log(
"Glare detected: rejecting incoming call " + call.callId +
" and keeping outgoing call " + existingCall.callId,
);
call.hangup();
}
} else {
client.emit("Call.incoming", call);
}
} else if (event.getType() === 'm.call.answer') {
if (!call) {
return;
}
if (event.getSender() === client.credentials.userId) {
if (call.state === 'ringing') {
call._onAnsweredElsewhere(content);
}
} else {
call._receivedAnswer(content);
}
} else if (event.getType() === 'm.call.candidates') {
if (event.getSender() === client.credentials.userId) {
return;
}
if (!call) {
// store the candidates; we may get a call eventually.
if (!candidatesByCall[content.call_id]) {
candidatesByCall[content.call_id] = [];
}
candidatesByCall[content.call_id] = candidatesByCall[
content.call_id
].concat(content.candidates);
} else {
for (i = 0; i < content.candidates.length; i++) {
call._gotRemoteIceCandidate(content.candidates[i]);
}
}
} else if (event.getType() === 'm.call.hangup') {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
if (!call) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = createNewMatrixCall(client, event.getRoomId());
if (call) {
call.callId = content.call_id;
call._initWithHangup(event);
client.callList[content.call_id] = call;
}
} else {
if (call.state !== 'ended') {
call._onHangupReceived(content);
delete client.callList[content.call_id];
}
}
}
}
}
function checkTurnServers(client) {
if (!client._supportsVoip) {
return;
}
client.turnServer().then(function(res) {
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " +
res.ttl + " secs");
// map the response to a format that can be fed to
// RTCPeerConnection
const servers = {
urls: res.uris,
username: res.username,
credential: res.password,
};
client._turnServers = [servers];
// re-fetch when we're about to reach the TTL
client._checkTurnServersTimeoutID = setTimeout(() => {
checkTurnServers(client);
}, (res.ttl || (60 * 60)) * 1000 * 0.9);
}
}, function(err) {
logger.error("Failed to get TURN URIs");
client._checkTurnServersTimeoutID = setTimeout(function() {
checkTurnServers(client);
}, 60000);
});
}
function _reject(callback, reject, err) {
if (callback) {
callback(err);
@@ -5462,9 +5554,8 @@ function _PojoToMatrixEventMapper(client, options) {
}
event.attemptDecryption(client._crypto);
}
const room = client.getRoom(event.getRoomId());
if (room && !preventReEmit) {
room.reEmitter.reEmit(event, ["Event.replaced"]);
if (!preventReEmit) {
client.reEmitter.reEmit(event, ["Event.replaced"]);
}
return event;
}
+3 -3
View File
@@ -734,7 +734,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
if (baseApis.getUserId() !== userId) {
return;
}
console.log("Cross-signing: Self-verification done; requesting keys");
logger.log("Cross-signing: Self-verification done; requesting keys");
// This happens asynchronously, and we're not concerned about waiting for
// it. We return here in order to test.
return new Promise((resolve, reject) => {
@@ -748,7 +748,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
const crossSigning = new CrossSigningInfo(
original.userId,
{ getCrossSigningKey: async (type) => {
console.debug("Cross-signing: requesting secret",
logger.debug("Cross-signing: requesting secret",
type, deviceId);
const { promise } = client.requestSecret(
`m.cross_signing.${type}`, [deviceId],
@@ -811,6 +811,6 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
timeout,
]).then(resolve, reject);
}).catch((e) => {
console.warn("Cross-signing: failure while requesting keys:", e);
logger.warn("Cross-signing: failure while requesting keys:", e);
});
}
+27 -11
View File
@@ -28,7 +28,7 @@ import {DeviceInfo} from './deviceinfo';
import {CrossSigningInfo} from './CrossSigning';
import * as olmlib from './olmlib';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {defer, sleep} from '../utils';
import {chunkPromises, defer, sleep} from '../utils';
/* State transition diagram for DeviceList._deviceTrackingStatus
@@ -62,7 +62,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
* @alias module:crypto/DeviceList
*/
export class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice) {
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
super();
this._cryptoStore = cryptoStore;
@@ -98,6 +98,9 @@ export class DeviceList extends EventEmitter {
// userId -> promise
this._keyDownloadsInProgressByUser = {};
// Maximum number of user IDs per request to prevent server overload (#1619)
this._keyDownloadChunkSize = keyDownloadChunkSize;
// Set whenever changes are made other than setting the sync token
this._dirty = false;
@@ -780,13 +783,17 @@ class DeviceListUpdateSerialiser {
opts.token = this._syncToken;
}
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then(async (res) => {
const dk = res.device_keys || {};
const masterKeys = res.master_keys || {};
const ssks = res.self_signing_keys || {};
const usks = res.user_signing_keys || {};
const factories = [];
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) {
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize);
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts));
}
chunkPromises(factories, 3).then(async (responses) => {
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
// yield to other things that want to execute in between users, to
// avoid wedging the CPU
@@ -848,6 +855,7 @@ class DeviceListUpdateSerialiser {
await _updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, dkResponse || {},
this._baseApis.getUserId(), this._baseApis.deviceId,
);
// put the updates into the object that will be returned as our results
@@ -885,8 +893,9 @@ class DeviceListUpdateSerialiser {
}
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) {
async function _updateStoredDeviceKeysForUser(
_olmDevice, userId, userStore, userResult, localUserId, localDeviceId,
) {
let updated = false;
// remove any devices in the store which aren't in the response
@@ -896,6 +905,13 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
}
if (!(deviceId in userResult)) {
if (userId === localUserId && deviceId === localDeviceId) {
logger.warn(
`Local device ${deviceId} missing from sync, skipping removal`,
);
continue;
}
logger.log("Device " + userId + ":" + deviceId +
" has been removed");
delete userStore[deviceId];
+2 -2
View File
@@ -344,14 +344,14 @@ class SSSSCryptoCallbacks {
}
}
addPrivateKey(keyId, privKey) {
addPrivateKey(keyId, keyInfo, privKey) {
this._privateKeys.set(keyId, privKey);
// Also pass along to application to cache if it wishes
if (
this._delegateCryptoCallbacks &&
this._delegateCryptoCallbacks.cacheSecretStorageKey
) {
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, privKey);
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
}
}
}
+84 -8
View File
@@ -144,7 +144,7 @@ OlmDevice.prototype.init = async function(opts = {}) {
try {
if (fromExportedDevice) {
if (pickleKey) {
console.warn(
logger.warn(
'ignoring opts.pickleKey'
+ ' because opts.fromExportedDevice is present.',
);
@@ -477,6 +477,36 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
);
};
/**
* Generate a new fallback keys
*
* @return {Promise} Resolved once the account is saved back having generated the key
*/
OlmDevice.prototype.generateFallbackKey = async function() {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._getAccount(txn, (account) => {
account.generate_fallback_key();
this._storeAccount(txn, account);
});
},
);
};
OlmDevice.prototype.getFallbackKey = async function() {
let result;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._getAccount(txn, (account) => {
result = JSON.parse(account.fallback_key());
});
},
);
return result;
};
/**
* Generate a new outbound session
*
@@ -515,6 +545,7 @@ OlmDevice.prototype.createOutboundSession = async function(
}
});
},
logger.withPrefix("[createOutboundSession]"),
);
return newSessionId;
};
@@ -575,6 +606,7 @@ OlmDevice.prototype.createInboundSession = async function(
}
});
},
logger.withPrefix("[createInboundSession]"),
);
return result;
@@ -589,8 +621,10 @@ OlmDevice.prototype.createInboundSession = async function(
* @return {Promise<string[]>} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
const log = logger.withPrefix("[getSessionIdsForDevice]");
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
logger.log("waiting for olm session to be created");
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
try {
await this._sessionsInProgress[theirDeviceIdentityKey];
} catch (e) {
@@ -608,6 +642,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
},
);
},
log,
);
return sessionIds;
@@ -621,13 +656,14 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function
* that marked the session as being in-progress.
* @param {Logger} [log] A possibly customised log
* @return {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = async function(
theirDeviceIdentityKey, nowait,
theirDeviceIdentityKey, nowait, log,
) {
const sessionInfos = await this.getSessionInfoForDevice(
theirDeviceIdentityKey, nowait,
theirDeviceIdentityKey, nowait, log,
);
if (sessionInfos.length === 0) {
@@ -667,11 +703,16 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
* @param {boolean} nowait Don't wait for an in-progress session to complete.
* This should only be set to true of the calling function is the function
* that marked the session as being in-progress.
* @param {Logger} [log] A possibly customised log
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
OlmDevice.prototype.getSessionInfoForDevice = async function(
deviceIdentityKey, nowait, log = logger,
) {
log = log.withPrefix("[getSessionInfoForDevice]");
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
logger.log("waiting for olm session to be created");
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
try {
await this._sessionsInProgress[deviceIdentityKey];
} catch (e) {
@@ -697,6 +738,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
}
});
},
log,
);
return info;
@@ -731,6 +773,7 @@ OlmDevice.prototype.encryptMessage = async function(
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[encryptMessage]"),
);
return res;
};
@@ -764,6 +807,7 @@ OlmDevice.prototype.decryptMessage = async function(
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[decryptMessage]"),
);
return payloadString;
};
@@ -795,6 +839,7 @@ OlmDevice.prototype.matchesSession = async function(
matches = sessionInfo.session.matches_inbound(ciphertext);
});
},
logger.withPrefix("[matchesSession]"),
);
return matches;
};
@@ -1003,6 +1048,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => {
/* if we already have this session, consider updating it */
this._getInboundGroupSession(
@@ -1029,9 +1075,14 @@ OlmDevice.prototype.addInboundGroupSession = async function(
+ senderKey + "/" + sessionId,
);
if (existingSession.first_known_index()
<= session.first_known_index()) {
<= session.first_known_index()
&& !(existingSession.first_known_index() == session.first_known_index()
&& !extraSessionData.untrusted
&& existingSessionData.untrusted)) {
// existing session has lower index (i.e. can
// decrypt more), so keep it
// decrypt more), or they have the same index and
// the new sessions trust does not win over the old
// sessions trust, so keep it
logger.log(
`Keeping existing megolm session ${sessionId}`,
);
@@ -1054,12 +1105,19 @@ OlmDevice.prototype.addInboundGroupSession = async function(
this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
if (!existingSession && extraSessionData.sharedHistory) {
this._cryptoStore.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
} finally {
session.free();
}
},
);
},
logger.withPrefix("[addInboundGroupSession]"),
);
};
@@ -1230,6 +1288,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
},
);
},
logger.withPrefix("[decryptGroupMessage]"),
);
if (error) {
@@ -1275,6 +1334,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
},
);
},
logger.withPrefix("[hasInboundSessionKeys]"),
);
return result;
@@ -1330,10 +1390,12 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
"forwarding_curve25519_key_chain":
sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key,
"shared_history": sessionData.sharedHistory || false,
};
},
);
},
logger.withPrefix("[getInboundGroupSessionKey]"),
);
return result;
@@ -1361,10 +1423,24 @@ OlmDevice.prototype.exportInboundGroupSession = function(
"session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
"first_known_index": session.first_known_index(),
"org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
};
});
};
OlmDevice.prototype.getSharedHistoryInboundGroupSessions = async function(roomId) {
let result;
await this._cryptoStore.doTxn(
'readonly', [
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
], (txn) => {
result = this._cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
},
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
);
return result;
};
// Utilities
// =========
+21 -16
View File
@@ -81,25 +81,27 @@ export class SecretStorage extends EventEmitter {
* @param {string} [keyId] the ID of the key. If not given, a random
* ID will be generated.
*
* @return {string} the ID of the key
* @return {object} An object with:
* keyId: {string} the ID of the key
* keyInfo: {object} details about the key (iv, mac, passphrase)
*/
async addKey(algorithm, opts, keyId) {
const keyData = {algorithm};
const keyInfo = {algorithm};
if (!opts) opts = {};
if (opts.name) {
keyData.name = opts.name;
keyInfo.name = opts.name;
}
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
keyInfo.passphrase = opts.passphrase;
}
if (opts.key) {
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
keyData.iv = iv;
keyData.mac = mac;
keyInfo.iv = iv;
keyInfo.mac = mac;
}
} else {
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
@@ -116,10 +118,13 @@ export class SecretStorage extends EventEmitter {
}
await this._baseApis.setAccountData(
`m.secret_storage.key.${keyId}`, keyData,
`m.secret_storage.key.${keyId}`, keyInfo,
);
return keyId;
return {
keyId,
keyInfo,
};
}
/**
@@ -247,7 +252,7 @@ export class SecretStorage extends EventEmitter {
) {
const hasKey = await this.hasKey(keys[0]);
if (hasKey) {
console.log("Fixing up passthrough secret: " + name);
logger.log("Fixing up passthrough secret: " + name);
await this.storePassthrough(name, keys[0]);
const newData = await this._baseApis.getAccountDataFromServer(name);
return newData;
@@ -457,13 +462,13 @@ export class SecretStorage extends EventEmitter {
if (!this._cryptoCallbacks.onSecretRequested) {
return;
}
const secret = await this._cryptoCallbacks.onSecretRequested({
user_id: sender,
device_id: deviceId,
request_id: content.request_id,
name: content.name,
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
});
const secret = await this._cryptoCallbacks.onSecretRequested(
sender,
deviceId,
content.request_id,
content.name,
this._baseApis.checkDeviceTrust(sender, deviceId),
);
if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
const payload = {
+226 -70
View File
@@ -36,11 +36,27 @@ import {
import {WITHHELD_MESSAGES} from '../OlmDevice';
// determine whether the key can be shared with invitees
function isRoomSharedHistory(room) {
const visibilityEvent = room.currentState &&
room.currentState.getStateEvents("m.room.history_visibility", "");
// NOTE: if the room visibility is unset, it would normally default to
// "world_readable".
// (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5)
// But we will be paranoid here, and treat it as a situation where the room
// is not shared-history
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
return ["world_readable", "shared"].includes(visibility);
}
/**
* @private
* @constructor
*
* @param {string} sessionId
* @param {boolean} sharedHistory whether the session can be freely shared with
* other group members, according to the room history visibility settings
*
* @property {string} sessionId
* @property {Number} useCount number of times this session has been used
@@ -50,12 +66,13 @@ import {WITHHELD_MESSAGES} from '../OlmDevice';
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
function OutboundSessionInfo(sessionId, sharedHistory = false) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharedWithDevices = {};
this.blockedDevicesNotified = {};
this.sharedHistory = sharedHistory;
}
@@ -183,6 +200,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
@@ -192,7 +210,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked, singleOlmCreationPhase,
room, devicesInRoom, blocked, singleOlmCreationPhase,
) {
let session;
@@ -204,6 +222,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const prepareSession = async (oldSession) => {
session = oldSession;
const sharedHistory = isRoomSharedHistory(room);
// history visibility changed
if (session && sharedHistory !== session.sharedHistory) {
session = null;
}
// need to make a brand new session?
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
this._sessionRotationPeriodMs)
@@ -219,7 +244,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
if (!session) {
logger.log(`Starting new megolm session for room ${this._roomId}`);
session = await this._prepareNewSession();
session = await this._prepareNewSession(sharedHistory);
logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${this._roomId}`);
this._outboundSessions[session.sessionId] = session;
@@ -250,11 +275,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": this._roomId,
"session_id": session.sessionId,
"session_key": key.key,
"chain_index": key.chain_index,
"org.matrix.msc3061.shared_history": sharedHistory,
},
};
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
@@ -264,11 +290,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
await Promise.all([
(async () => {
// share keys with devices that we already have a session for
logger.debug(`Sharing keys with existing Olm sessions in ${this._roomId}`);
await this._shareKeyWithOlmSessions(
session, key, payload, olmSessions,
);
logger.debug(`Shared keys with existing Olm sessions in ${this._roomId}`);
})(),
(async () => {
logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this._roomId}`);
const errorDevices = [];
// meanwhile, establish olm sessions for devices that we don't
@@ -282,6 +311,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
session, key, payload, devicesWithoutSession, errorDevices,
singleOlmCreationPhase ? 10000 : 2000, failedServers,
);
logger.debug(`Shared keys (end phase 1) with new Olm sessions in ${this._roomId}`);
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
// perform the second phase of olm session creation if requested,
@@ -310,19 +340,24 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
}
logger.debug(`Sharing keys (start phase 2) with new Olm sessions in ${this._roomId}`);
await this._shareKeyWithDevices(
session, key, payload, retryDevices, failedDevices, 30000,
);
logger.debug(`Shared keys (end phase 2) with new Olm sessions in ${this._roomId}`);
await this._notifyFailedOlmDevices(session, key, failedDevices);
})();
} else {
await this._notifyFailedOlmDevices(session, key, errorDevices);
}
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this._roomId}`);
})(),
(async () => {
logger.debug(`Notifying blocked devices in ${this._roomId}`);
// also, notify blocked devices that they're blocked
const blockedMap = {};
let blockedCount = 0;
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
for (const [deviceId, device] of Object.entries(userBlockedDevices)) {
if (
@@ -330,12 +365,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || {};
blockedMap[userId][deviceId] = {device};
blockedMap[userId][deviceId] = { device };
blockedCount++;
}
}
}
await this._notifyBlockedDevices(session, blockedMap);
logger.debug(`Notified ${blockedCount} blocked devices in ${this._roomId}`);
})(),
]);
};
@@ -348,6 +385,11 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
// first wait for the previous share to complete
const prom = this._setupPromise.then(prepareSession);
// Ensure any failures are logged for debugging
prom.catch(e => {
logger.error(`Failed to ensure outbound session in ${this._roomId}`, e);
});
// _setupPromise resolves to `session` whether or not the share succeeds
this._setupPromise = prom.then(returnSession, returnSession);
@@ -358,30 +400,27 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
/**
* @private
*
* @param {boolean} sharedHistory
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
MegolmEncryption.prototype._prepareNewSession = async function() {
MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
const sessionId = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
await this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
key.key, {ed25519: this._olmDevice.deviceEd25519Key}, false,
{sharedHistory: sharedHistory},
);
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 megolm session", e);
});
}
// don't wait for it to complete
this._crypto.backupGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [],
sessionId, key.key,
);
return new OutboundSessionInfo(sessionId);
return new OutboundSessionInfo(sessionId, sharedHistory);
};
/**
@@ -662,14 +701,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
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,
"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,
"org.matrix.msc3061.shared_history": key.shared_history || false,
},
};
@@ -723,13 +763,18 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
MegolmEncryption.prototype._shareKeyWithDevices = async function(
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
) {
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
logger.withPrefix(`[${this._roomId}]`),
);
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
logger.debug(`Sharing keys with Olm sessions in ${this._roomId}`);
await this._shareKeyWithOlmSessions(session, key, payload, devicemap);
logger.debug(`Shared keys with Olm sessions in ${this._roomId}`);
};
MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
@@ -738,16 +783,17 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
const userDeviceMaps = this._splitDevices(devicemap);
for (let i = 0; i < userDeviceMaps.length; i++) {
const taskDetail =
`megolm keys for ${session.sessionId} ` +
`in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
try {
logger.debug(`Sharing ${taskDetail}`);
await this._encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
logger.log(`Completed megolm keyshare for ${session.sessionId} `
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
logger.debug(`Shared ${taskDetail}`);
} catch (e) {
logger.log(`megolm keyshare for ${session.sessionId} in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
logger.error(`Failed to share ${taskDetail}`);
throw e;
}
}
@@ -766,6 +812,11 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
session, key, failedDevices,
) {
logger.debug(
`Notifying ${failedDevices.length} devices we failed to ` +
`create Olm sessions in ${this._roomId}`,
);
// mark the devices that failed as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
for (const {userId, deviceInfo} of failedDevices) {
@@ -780,6 +831,10 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
await this._olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
);
logger.debug(
`Filtered down to ${filteredFailedDevices.length} error devices ` +
`in ${this._roomId}`,
);
const blockedMap = {};
for (const {userId, deviceInfo} of filteredFailedDevices) {
blockedMap[userId] = blockedMap[userId] || {};
@@ -797,6 +852,10 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
// send the notifications
await this._notifyBlockedDevices(session, blockedMap);
logger.debug(
`Notified ${filteredFailedDevices.length} devices we failed to ` +
`create Olm sessions in ${this._roomId}`,
);
};
/**
@@ -846,24 +905,41 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
// We're already preparing something, so don't do anything else.
// FIXME: check if we need to restart
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
const elapsedTime = Date.now() - this.encryptionPreparationMetadata.startTime;
logger.debug(
`Already started preparing to encrypt for ${this._roomId} ` +
`${elapsedTime} ms ago, skipping`,
);
return;
}
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
this.encryptionPreparationMetadata = {
startTime: Date.now(),
};
this.encryptionPreparation = (async () => {
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
try {
logger.debug(`Getting devices in ${this._roomId}`);
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
this._removeUnknownDevices(devicesInRoom);
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
this._removeUnknownDevices(devicesInRoom);
}
logger.debug(`Ensuring outbound session in ${this._roomId}`);
await this._ensureOutboundSession(room, devicesInRoom, blocked, true);
logger.debug(`Ready to encrypt events for ${this._roomId}`);
} catch (e) {
logger.error(`Failed to prepare to encrypt events for ${this._roomId}`, e);
} finally {
delete this.encryptionPreparationMetadata;
delete this.encryptionPreparation;
}
await this._ensureOutboundSession(devicesInRoom, blocked, true);
delete this.encryptionPreparation;
})();
};
@@ -899,7 +975,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
this._checkForUnknownDevices(devicesInRoom);
}
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
const session = await this._ensureOutboundSession(room, devicesInRoom, blocked);
const payloadJson = {
room_id: this._roomId,
type: eventType,
@@ -1324,10 +1400,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
const extraSessionData = {};
if (content["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat,
exportFormat, extraSessionData,
).then(() => {
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId)
@@ -1347,18 +1427,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
}
});
}).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 megolm session", e);
});
}
// 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) => {
logger.error(`Error handling m.room_key_event: ${e}`);
});
@@ -1533,14 +1607,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
return {
type: "m.forwarded_room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: sessionId,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
"algorithm": olmlib.MEGOLM_ALGORITHM,
"room_id": roomId,
"sender_key": senderKey,
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
"session_id": sessionId,
"session_key": key.key,
"chain_index": key.chain_index,
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
"org.matrix.msc3061.shared_history": key.shared_history || false,
},
};
};
@@ -1554,6 +1629,13 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
* @param {string} [opts.source] where the key came from
*/
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
const extraSessionData = {};
if (opts.untrusted) {
extraSessionData.untrusted = true;
}
if (session["org.matrix.msc3061.shared_history"]) {
extraSessionData.sharedHistory = true;
}
return this._olmDevice.addInboundGroupSession(
session.room_id,
session.sender_key,
@@ -1562,9 +1644,9 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
session.session_key,
session.sender_claimed_keys,
true,
opts.untrusted ? { untrusted: opts.untrusted } : {},
extraSessionData,
).then(() => {
if (this._crypto.backupInfo && opts.source !== "backup") {
if (opts.source !== "backup") {
// don't wait for it to complete
this._crypto.backupGroupSession(
session.room_id,
@@ -1641,6 +1723,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
return !this._pendingEvents[senderKey];
};
MegolmDecryption.prototype.sendSharedHistoryInboundSessions = async function(devicesByUser) {
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
);
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
const sharedHistorySessions =
await this._olmDevice.getSharedHistoryInboundGroupSessions(
this._roomId,
);
logger.log("shared-history sessions", sharedHistorySessions);
for (const [senderKey, sessionId] of sharedHistorySessions) {
const payload = await this._buildKeyForwardingMessage(
this._roomId, senderKey, sessionId,
);
const promises = [];
const contentMap = {};
for (const [userId, devices] of Object.entries(devicesByUser)) {
contentMap[userId] = {};
for (const deviceInfo of devices) {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
deviceInfo,
payload,
),
);
}
}
await Promise.all(promises);
// prune out any devices that encryptMessageForDevice could not encrypt for,
// in which case it will have just not added anything to the ciphertext object.
// There's no point sending messages to devices if we couldn't encrypt to them,
// since that's effectively a blank message.
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) {
logger.log(
"No ciphertext for device " +
userId + ":" + deviceId + ": pruning",
);
delete contentMap[userId][deviceId];
}
}
// No devices left for that user? Strip that too.
if (Object.keys(contentMap[userId]).length === 0) {
logger.log("Pruned all devices for user " + userId);
delete contentMap[userId];
}
}
// Is there anything left?
if (Object.keys(contentMap).length === 0) {
logger.log("No users left to send to: aborting");
return;
}
await this._baseApis.sendToDevice("m.room.encrypted", contentMap);
}
};
registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
);
+288
View File
@@ -0,0 +1,288 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {decodeBase64, encodeBase64} from './olmlib';
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
import {decryptAES, encryptAES} from './aes';
import anotherjson from "another-json";
import {logger} from '../logger';
// FIXME: these types should eventually go in a different file
type Signatures = Record<string, Record<string, string>>;
interface DeviceKeys {
algorithms: Array<string>;
device_id: string; // eslint-disable-line camelcase
user_id: string; // eslint-disable-line camelcase
keys: Record<string, string>;
signatures?: Signatures;
}
interface OneTimeKey {
key: string;
fallback?: boolean;
signatures?: Signatures;
}
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
const oneweek = 7 * 24 * 60 * 60 * 1000;
export class DehydrationManager {
private inProgress = false;
private timeoutId: any;
private key: Uint8Array;
private keyInfo: {[props: string]: any};
private deviceDisplayName: string;
constructor(private crypto) {
this.getDehydrationKeyFromCache();
}
async getDehydrationKeyFromCache(): Promise<void> {
return await this.crypto._cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.crypto._cryptoStore.getSecretStorePrivateKey(
txn,
async (result) => {
if (result) {
const {key, keyInfo, deviceDisplayName, time} = result;
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
this.key = decodeBase64(decrypted);
this.keyInfo = keyInfo;
this.deviceDisplayName = deviceDisplayName;
const now = Date.now();
const delay = Math.max(1, time + oneweek - now);
this.timeoutId = global.setTimeout(
this.dehydrateDevice.bind(this), delay,
);
}
},
"dehydration",
);
},
);
}
/** set the key, and queue periodic dehydration to the server in the background */
async setKeyAndQueueDehydration(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
): Promise<void> {
const matches = await this.setKey(key, keyInfo, deviceDisplayName);
if (!matches) {
// start dehydration in the background
this.dehydrateDevice();
}
}
async setKey(
key: Uint8Array, keyInfo: {[props: string]: any} = {},
deviceDisplayName: string = undefined,
): Promise<boolean> {
if (!key) {
// unsetting the key -- cancel any pending dehydration task
if (this.timeoutId) {
global.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
// clear storage
await this.crypto._cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.crypto._cryptoStore.storeSecretStorePrivateKey(
txn, "dehydration", null,
);
},
);
this.key = undefined;
this.keyInfo = undefined;
return;
}
// Check to see if it's the same key as before. If it's different,
// dehydrate a new device. If it's the same, we can keep the same
// device. (Assume that keyInfo and deviceDisplayName will be the
// same if the key is the same.)
let matches: boolean = this.key && key.length == this.key.length;
for (let i = 0; matches && i < key.length; i++) {
if (key[i] != this.key[i]) {
matches = false;
}
}
if (!matches) {
this.key = key;
this.keyInfo = keyInfo;
this.deviceDisplayName = deviceDisplayName;
}
return matches;
}
/** returns the device id of the newly created dehydrated device */
async dehydrateDevice(): Promise<string> {
if (this.inProgress) {
logger.log("Dehydration already in progress -- not starting new dehydration");
return;
}
this.inProgress = true;
if (this.timeoutId) {
global.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
try {
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
// update the crypto store with the timestamp
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
await this.crypto._cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this.crypto._cryptoStore.storeSecretStorePrivateKey(
txn, "dehydration",
{
keyInfo: this.keyInfo,
key,
deviceDisplayName: this.deviceDisplayName,
time: Date.now(),
},
);
},
);
logger.log("Attempting to dehydrate device");
logger.log("Creating account");
// create the account and all the necessary keys
const account = new global.Olm.Account();
account.create();
const e2eKeys = JSON.parse(account.identity_keys());
const maxKeys = account.max_number_of_one_time_keys();
// FIXME: generate in small batches?
account.generate_one_time_keys(maxKeys / 2);
account.generate_fallback_key();
const otks: Record<string, string> = JSON.parse(account.one_time_keys());
const fallbacks: Record<string, string> = JSON.parse(account.fallback_key());
account.mark_keys_as_published();
// dehydrate the account and store it on the server
const pickledAccount = account.pickle(new Uint8Array(this.key));
const deviceData: {[props: string]: any} = {
algorithm: DEHYDRATION_ALGORITHM,
account: pickledAccount,
};
if (this.keyInfo.passphrase) {
deviceData.passphrase = this.keyInfo.passphrase;
}
logger.log("Uploading account to server");
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
undefined,
"PUT",
"/dehydrated_device",
undefined,
{
device_data: deviceData,
initial_device_display_name: this.deviceDisplayName,
},
{
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
},
);
// send the keys to the server
const deviceId = dehydrateResult.device_id;
logger.log("Preparing device keys", deviceId);
const deviceKeys: DeviceKeys = {
algorithms: this.crypto._supportedAlgorithms,
device_id: deviceId,
user_id: this.crypto._userId,
keys: {
[`ed25519:${deviceId}`]: e2eKeys.ed25519,
[`curve25519:${deviceId}`]: e2eKeys.curve25519,
},
};
const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
deviceKeys.signatures = {
[this.crypto._userId]: {
[`ed25519:${deviceId}`]: deviceSignature,
},
};
if (this.crypto._crossSigningInfo.getId("self_signing")) {
await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing");
}
logger.log("Preparing one-time keys");
const oneTimeKeys = {};
for (const [keyId, key] of Object.entries(otks.curve25519)) {
const k: OneTimeKey = {key};
const signature = account.sign(anotherjson.stringify(k));
k.signatures = {
[this.crypto._userId]: {
[`ed25519:${deviceId}`]: signature,
},
};
oneTimeKeys[`signed_curve25519:${keyId}`] = k;
}
logger.log("Preparing fallback keys");
const fallbackKeys = {};
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
const k: OneTimeKey = {key, fallback: true};
const signature = account.sign(anotherjson.stringify(k));
k.signatures = {
[this.crypto._userId]: {
[`ed25519:${deviceId}`]: signature,
},
};
fallbackKeys[`signed_curve25519:${keyId}`] = k;
}
logger.log("Uploading keys to server");
await this.crypto._baseApis._http.authedRequest(
undefined,
"POST",
"/keys/upload/" + encodeURI(deviceId),
undefined,
{
"device_keys": deviceKeys,
"one_time_keys": oneTimeKeys,
"org.matrix.msc2732.fallback_keys": fallbackKeys,
},
);
logger.log("Done dehydrating");
// dehydrate again in a week
this.timeoutId = global.setTimeout(
this.dehydrateDevice.bind(this), oneweek,
);
return deviceId;
} finally {
this.inProgress = false;
}
}
private stop() {
if (this.timeoutId) {
global.clearTimeout(this.timeoutId);
this.timeoutId = undefined;
}
}
}
+140 -72
View File
@@ -56,6 +56,8 @@ import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDevice
import {IllegalMethod} from "./verification/IllegalMethod";
import {KeySignatureUploadError} from "../errors";
import {decryptAES, encryptAES} from './aes';
import {DehydrationManager} from './dehydration';
import { MatrixEvent } from "../models/event";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -146,7 +148,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
method,
);
} else {
console.warn(`Excluding unknown verification method ${method}`);
logger.warn(`Excluding unknown verification method ${method}`);
}
}
} else {
@@ -243,6 +245,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
baseApis, cryptoCallbacks,
);
this._dehydrationManager = new DehydrationManager(this);
// Assuming no app-supplied callback, default to getting from SSSS.
if (!cryptoCallbacks.getCrossSigningKey && cryptoCallbacks.getSecretStorageKey) {
cryptoCallbacks.getCrossSigningKey = async (type) => {
@@ -564,7 +568,9 @@ Crypto.prototype.bootstrapCrossSigning = async function({
"Cross-signing private keys not found locally, but they are available " +
"in secret storage, reading storage and caching locally",
);
await this.checkOwnCrossSigningTrust();
await this.checkOwnCrossSigningTrust({
allowPrivateKeyRequests: true,
});
}
// Assuming no app-supplied callback, default to storing new private keys in
@@ -630,7 +636,7 @@ Crypto.prototype.bootstrapCrossSigning = async function({
* containing the key, or rejects if the key cannot be obtained.
* Returns:
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
* SecretStorage#addKey: an object with `passphrase` etc fields.
*/
Crypto.prototype.bootstrapSecretStorage = async function({
createSecretStorageKey = async () => ({ }),
@@ -660,13 +666,13 @@ Crypto.prototype.bootstrapSecretStorage = async function({
opts.key = privateKey;
}
const keyId = await secretStorage.addKey(
const { keyId, keyInfo } = await secretStorage.addKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
if (privateKey) {
// make the private key available to encrypt 4S secrets
builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey);
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
}
await secretStorage.setDefaultKeyId(keyId);
@@ -679,9 +685,9 @@ Crypto.prototype.bootstrapSecretStorage = async function({
{keys: {[keyId]: keyInfo}}, "",
);
if (key) {
const keyData = key[1];
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData);
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
const privateKey = key[1];
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyInfo, privateKey);
const {iv, mac} = await SecretStorage._calculateKeyCheck(privateKey);
keyInfo.iv = iv;
keyInfo.mac = mac;
@@ -692,6 +698,26 @@ Crypto.prototype.bootstrapSecretStorage = async function({
}
};
const signKeyBackupWithCrossSigning = async (keyBackupAuthData) => {
if (
this._crossSigningInfo.getId() &&
await this._crossSigningInfo.isStoredInKeyCache("master")
) {
try {
logger.log("Adding cross-signing signature to key backup");
await this._crossSigningInfo.signObject(keyBackupAuthData, "master");
} catch (e) {
// This step is not critical (just helpful), so we catch here
// and continue if it fails.
logger.error("Signing key backup with cross-signing keys failed", e);
}
} else {
logger.warn(
"Cross-signing keys not available, skipping signature on key backup",
);
}
};
const oldSSSSKey = await this.getSecretStorageKey();
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
const storageExists = (
@@ -758,19 +784,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross-signing key so the key backup can
// be trusted via cross-signing.
if (
this._crossSigningInfo.getId() &&
this._crossSigningInfo.isStoredInKeyCache("master")
) {
logger.log("Adding cross-signing signature to key backup");
await this._crossSigningInfo.signObject(
keyBackupInfo.auth_data, "master",
);
} else {
logger.warn(
"Cross-signing keys not available, skipping signature on key backup",
);
}
await signKeyBackupWithCrossSigning(keyBackupInfo.auth_data);
builder.addSessionBackup(keyBackupInfo);
} else {
@@ -821,18 +835,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({
auth_data: info.auth_data,
};
if (
this._crossSigningInfo.getId() &&
this._crossSigningInfo.isStoredInKeyCache("master")
) {
// sign with cross-sign master key
logger.log("Adding cross-signing signature to key backup");
await this._crossSigningInfo.signObject(data.auth_data, "master");
} else {
logger.warn(
"Cross-signing keys not available, skipping signature on key backup",
);
}
// Sign with cross-signing master key
await signKeyBackupWithCrossSigning(data.auth_data);
// sign with the device fingerprint
await this._signObject(data.auth_data);
@@ -1298,13 +1302,19 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
* Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted.
*/
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
Crypto.prototype.checkOwnCrossSigningTrust = async function({
allowPrivateKeyRequests = false,
} = {}) {
const userId = this._userId;
// Before proceeding, ensure our cross-signing public keys have been
// downloaded via the device list.
await this.downloadKeys([this._userId]);
// Also check which private keys are locally cached.
const crossSigningPrivateKeys =
await this._crossSigningInfo.getCrossSigningKeysFromCache();
// If we see an update to our own master key, check it against the master
// key we have and, if it matches, mark it as verified
@@ -1320,18 +1330,26 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
const seenPubkey = newCrossSigning.getId();
const masterChanged = this._crossSigningInfo.getId() !== seenPubkey;
const masterExistsNotLocallyCached =
newCrossSigning.getId() && !crossSigningPrivateKeys.has("master");
if (masterChanged) {
logger.info("Got new master public key", seenPubkey);
}
if (
allowPrivateKeyRequests &&
(masterChanged || masterExistsNotLocallyCached)
) {
logger.info("Attempting to retrieve cross-signing master private key");
let signing = null;
// It's important for control flow that we leave any errors alone for
// higher levels to handle so that e.g. cancelling access properly
// aborts any larger operation as well.
try {
const ret = await this._crossSigningInfo.getCrossSigningKey(
'master', seenPubkey,
);
signing = ret[1];
logger.info("Got cross-signing master private key");
} catch (e) {
logger.error("Cross-signing master private key not available", e);
} finally {
if (signing) signing.free();
}
@@ -1346,10 +1364,24 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
const selfSigningChanged = oldSelfSigningId !== newCrossSigning.getId("self_signing");
const userSigningChanged = oldUserSigningId !== newCrossSigning.getId("user_signing");
const selfSigningExistsNotLocallyCached = (
newCrossSigning.getId("self_signing") &&
!crossSigningPrivateKeys.has("self_signing")
);
const userSigningExistsNotLocallyCached = (
newCrossSigning.getId("user_signing") &&
!crossSigningPrivateKeys.has("user_signing")
);
const keySignatures = {};
if (selfSigningChanged) {
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
}
if (
allowPrivateKeyRequests &&
(selfSigningChanged || selfSigningExistsNotLocallyCached)
) {
logger.info("Attempting to retrieve cross-signing self-signing private key");
let signing = null;
try {
@@ -1358,8 +1390,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
);
signing = ret[1];
logger.info("Got cross-signing self-signing private key");
} catch (e) {
logger.error("Cross-signing self-signing private key not available", e);
} finally {
if (signing) signing.free();
}
@@ -1372,6 +1402,11 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
}
if (userSigningChanged) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
}
if (
allowPrivateKeyRequests &&
(userSigningChanged || userSigningExistsNotLocallyCached)
) {
logger.info("Attempting to retrieve cross-signing user-signing private key");
let signing = null;
try {
@@ -1380,8 +1415,6 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
);
signing = ret[1];
logger.info("Got cross-signing user-signing private key");
} catch (e) {
logger.error("Cross-signing user-signing private key not available", e);
} finally {
if (signing) signing.free();
}
@@ -1751,6 +1784,7 @@ Crypto.prototype.start = function() {
Crypto.prototype.stop = function() {
this._outgoingRoomKeyRequestManager.stop();
this._deviceList.stop();
this._dehydrationManager.stop();
};
/**
@@ -1856,6 +1890,14 @@ Crypto.prototype.updateOneTimeKeyCount = function(currentCount) {
}
};
Crypto.prototype.setNeedsNewFallback = function(needsNewFallback) {
this._needsNewFallback = !!needsNewFallback;
};
Crypto.prototype.getNeedsNewFallback = function() {
return this._needsNewFallback;
};
// check if it's time to upload one-time keys, and do so if so.
function _maybeUploadOneTimeKeys(crypto) {
// frequency with which to check & upload one-time keys
@@ -1903,27 +1945,31 @@ function _maybeUploadOneTimeKeys(crypto) {
// out stale private keys that won't receive a message.
const keyLimit = Math.floor(maxOneTimeKeys / 2);
function uploadLoop(keyCount) {
if (keyLimit <= keyCount) {
// If we don't need to generate any more keys then we are done.
return Promise.resolve();
}
async function uploadLoop(keyCount) {
while (keyLimit > keyCount || crypto.getNeedsNewFallback()) {
// Ask olm to generate new one time keys, then upload them to synapse.
if (keyLimit > keyCount) {
logger.info("generating oneTimeKeys");
const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
await crypto._olmDevice.generateOneTimeKeys(keysThisLoop);
}
const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
if (crypto.getNeedsNewFallback()) {
logger.info("generating fallback key");
await crypto._olmDevice.generateFallbackKey();
}
// Ask olm to generate new one time keys, then upload them to synapse.
return crypto._olmDevice.generateOneTimeKeys(keysThisLoop).then(() => {
return _uploadOneTimeKeys(crypto);
}).then((res) => {
logger.info("calling _uploadOneTimeKeys");
const res = await _uploadOneTimeKeys(crypto);
if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) {
// if the response contains a more up to date value use this
// for the next loop
return uploadLoop(res.one_time_key_counts.signed_curve25519);
keyCount = res.one_time_key_counts.signed_curve25519;
} else {
throw new Error("response for uploading keys does not contain "
+ "one_time_key_counts.signed_curve25519");
throw new Error("response for uploading keys does not contain " +
"one_time_key_counts.signed_curve25519");
}
});
}
}
crypto._oneTimeKeyCheckInProgress = true;
@@ -1955,11 +2001,22 @@ function _maybeUploadOneTimeKeys(crypto) {
// returns a promise which resolves to the response
async function _uploadOneTimeKeys(crypto) {
const promises = [];
const fallbackJson = {};
if (crypto.getNeedsNewFallback()) {
const fallbackKeys = await crypto._olmDevice.getFallbackKey();
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
const k = { key, fallback: true };
fallbackJson["signed_curve25519:" + keyId] = k;
promises.push(crypto._signObject(k));
}
crypto.setNeedsNewFallback(false);
}
const oneTimeKeys = await crypto._olmDevice.getOneTimeKeys();
const oneTimeJson = {};
const promises = [];
for (const keyId in oneTimeKeys.curve25519) {
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
const k = {
@@ -1973,7 +2030,8 @@ async function _uploadOneTimeKeys(crypto) {
await Promise.all(promises);
const res = await crypto._baseApis.uploadKeysRequest({
one_time_keys: oneTimeJson,
"one_time_keys": oneTimeJson,
"org.matrix.msc2732.fallback_keys": fallbackJson,
});
await crypto._olmDevice.markKeysAsPublished();
@@ -2400,7 +2458,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
if (claimedKey !== device.getFingerprint()) {
logger.warn(
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
"but sender device has key " + device.getFingerprint());
" but sender device has key " + device.getFingerprint());
return null;
}
@@ -2613,7 +2671,10 @@ Crypto.prototype.trackRoomDevices = function(roomId) {
let promise = this._roomDeviceTrackingState[roomId];
if (!promise) {
promise = trackMembers();
this._roomDeviceTrackingState[roomId] = promise;
this._roomDeviceTrackingState[roomId] = promise.catch(err => {
this._roomDeviceTrackingState[roomId] = null;
throw err;
});
}
return promise;
};
@@ -2855,18 +2916,18 @@ Crypto.prototype.backupGroupSession = async function(
sessionId, sessionKey, keysClaimed,
exportFormat,
) {
if (!this.backupInfo) {
throw new Error("Key backups are not enabled");
}
await this._cryptoStore.markSessionsNeedingBackup([{
senderKey: senderKey,
sessionId: sessionId,
}]);
// don't wait for this to complete: it will delay so
// happens in the background
this.scheduleKeyBackupSend();
if (this.backupInfo) {
// don't wait for this to complete: it will delay so
// happens in the background
this.scheduleKeyBackupSend();
}
// if this.backupInfo is not set, then the keys will be backed up when
// client.enableKeyBackup is called
};
/**
@@ -2999,19 +3060,26 @@ Crypto.prototype.encryptEvent = async function(event, room) {
* finished decrypting. Rejects with an `algorithms.DecryptionError` if there
* is a problem decrypting the event.
*/
Crypto.prototype.decryptEvent = function(event) {
Crypto.prototype.decryptEvent = async function(event) {
if (event.isRedacted()) {
return Promise.resolve({
const redactionEvent = new MatrixEvent(event.getUnsigned().redacted_because);
const decryptedEvent = await this.decryptEvent(redactionEvent);
return {
clearEvent: {
room_id: event.getRoomId(),
type: "m.room.message",
content: {},
unsigned: {
redacted_because: decryptedEvent.clearEvent,
},
},
});
};
} else {
const content = event.getWireContent();
const alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm);
return await alg.decryptEvent(event);
}
const content = event.getWireContent();
const alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm);
return alg.decryptEvent(event);
};
/**
+61 -39
View File
@@ -183,18 +183,24 @@ export async function getExistingOlmSessions(
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*
* @param {Logger} [log] A possibly customised log
*
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
export async function ensureOlmSessionsForDevices(
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers, log,
) {
if (typeof force === "number") {
log = failedServers;
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
if (!log) {
log = logger;
}
const devicesWithoutSession = [
// [userId, deviceId], ...
@@ -202,6 +208,35 @@ export async function ensureOlmSessionsForDevices(
const result = {};
const resolveSession = {};
// Mark all sessions this task intends to update as in progress. It is
// important to do this for all devices this task cares about in a single
// synchronous operation, as otherwise it is possible to have deadlocks
// where multiple tasks wait indefinitely on another task to update some set
// of common devices.
for (const [, devices] of Object.entries(devicesByUser)) {
for (const deviceInfo of devices) {
const key = deviceInfo.getIdentityKey();
if (key === olmDevice.deviceCurve25519Key) {
// We don't start sessions with ourself, so there's no need to
// mark it in progress.
continue;
}
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 => {
resolveSession[key] = (...args) => {
delete olmDevice._sessionsInProgress[key];
resolve(...args);
};
});
}
}
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
result[userId] = {};
for (const deviceInfo of devices) {
@@ -216,7 +251,7 @@ export async function ensureOlmSessionsForDevices(
// new chain when this side has an active sender chain.
// If you see this message being logged in the wild, we should find
// the thing that is trying to send Olm messages to itself and fix it.
logger.info("Attempted to start session with ourself! Ignoring");
log.info("Attempted to start session with ourself! Ignoring");
// We must fill in the section in the return value though, as callers
// expect it to be there.
result[userId][deviceId] = {
@@ -226,41 +261,21 @@ export async function ensureOlmSessionsForDevices(
continue;
}
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 forWhom = `for ${key} (${userId}:${deviceId})`;
const sessionId = await olmDevice.getSessionIdForDevice(
key, resolveSession[key],
key, resolveSession[key], log,
);
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];
// in-progress, so resolve it now, which will unmark it and
// unblock anything that was waiting
resolveSession[key]();
}
if (sessionId === null || force) {
if (force) {
logger.info("Forcing new Olm session for " + userId + ":" + deviceId);
log.info(`Forcing new Olm session ${forWhom}`);
} else {
logger.info("Making new Olm session for " + userId + ":" + deviceId);
log.info(`Making new Olm session ${forWhom}`);
}
devicesWithoutSession.push([userId, deviceId]);
}
@@ -277,15 +292,18 @@ export async function ensureOlmSessionsForDevices(
const oneTimeKeyAlgorithm = "signed_curve25519";
let res;
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
try {
log.debug(`Claiming ${taskDetail}`);
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
);
log.debug(`Claimed ${taskDetail}`);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
resolver.resolve();
resolver();
}
logger.log("failed to claim one-time keys", e, devicesWithoutSession);
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
throw e;
}
@@ -293,10 +311,10 @@ export async function ensureOlmSessionsForDevices(
failedServers.push(...Object.keys(res.failures));
}
const otk_res = res.one_time_keys || {};
const otkResult = res.one_time_keys || {};
const promises = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otk_res[userId] || {};
const userRes = otkResult[userId] || {};
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
@@ -323,11 +341,12 @@ export async function ensureOlmSessionsForDevices(
}
if (!oneTimeKey) {
const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId;
logger.warn(msg);
log.warn(
`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` +
`for device ${userId}:${deviceId}`,
);
if (resolveSession[key]) {
resolveSession[key].resolve();
resolveSession[key]();
}
continue;
}
@@ -337,12 +356,12 @@ export async function ensureOlmSessionsForDevices(
olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => {
if (resolveSession[key]) {
resolveSession[key].resolve(sid);
resolveSession[key](sid);
}
result[userId][deviceId].sessionId = sid;
}, (e) => {
if (resolveSession[key]) {
resolveSession[key].resolve();
resolveSession[key]();
}
throw e;
}),
@@ -350,7 +369,10 @@ export async function ensureOlmSessionsForDevices(
}
}
taskDetail = `Olm sessions for ${promises.length} devices`;
log.debug(`Starting ${taskDetail}`);
await Promise.all(promises);
log.debug(`Started ${taskDetail}`);
return result;
}
@@ -19,7 +19,8 @@ limitations under the License.
import {logger} from '../../logger';
import * as utils from "../../utils";
export const VERSION = 9;
export const VERSION = 10;
const PROFILE_TRANSACTIONS = false;
/**
* Implementation of a CryptoStore which is backed by an existing
@@ -34,6 +35,7 @@ export class Backend {
*/
constructor(db) {
this._db = db;
this._nextTxnId = 0;
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
@@ -757,10 +759,59 @@ export class Backend {
}));
}
doTxn(mode, stores, func) {
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
if (!txn) {
txn = this._db.transaction(
"shared_history_inbound_group_sessions", "readwrite",
);
}
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
const req = objectStore.get([roomId]);
req.onsuccess = () => {
const {sessions} = req.result || {sessions: []};
sessions.push([senderKey, sessionId]);
objectStore.put({roomId, sessions});
};
}
getSharedHistoryInboundGroupSessions(roomId, txn) {
if (!txn) {
txn = this._db.transaction(
"shared_history_inbound_group_sessions", "readonly",
);
}
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
const req = objectStore.get([roomId]);
return new Promise((resolve, reject) => {
req.onsuccess = () => {
const {sessions} = req.result || {sessions: []};
resolve(sessions);
};
req.onerror = reject;
});
}
doTxn(mode, stores, func, log = logger) {
let startTime;
let description;
if (PROFILE_TRANSACTIONS) {
const txnId = this._nextTxnId++;
startTime = Date.now();
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
log.debug(`Starting ${description}`);
}
const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
const result = func(txn);
if (PROFILE_TRANSACTIONS) {
promise.then(() => {
const elapsedTime = Date.now() - startTime;
log.debug(`Finished ${description}, took ${elapsedTime} ms`);
}, () => {
const elapsedTime = Date.now() - startTime;
log.error(`Failed ${description}, took ${elapsedTime} ms`);
});
}
return promise.then(() => {
return result;
});
@@ -815,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["userId", "deviceId"],
});
}
if (oldVersion < 10) {
db.createObjectStore("shared_history_inbound_group_sessions", {
keyPath: ["roomId"],
});
}
// Expand as needed.
}
+28 -2
View File
@@ -582,6 +582,29 @@ export class IndexedDBCryptoStore {
return this._backend.markSessionsNeedingBackup(sessions, txn);
}
/**
* Add a shared-history group session for a room.
* @param {string} roomId The room that the key belongs to
* @param {string} senderKey The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn(). (optional)
*/
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
this._backend.addSharedHistoryInboundGroupSession(
roomId, senderKey, sessionId, txn,
);
}
/**
* Get the shared-history group session for a room.
* @param {string} roomId The room that the key belongs to
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
*/
getSharedHistoryInboundGroupSessions(roomId, txn) {
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn);
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
@@ -596,6 +619,7 @@ export class IndexedDBCryptoStore {
* @param {function(*)} func Function called with the
* transaction object: an opaque object that should be passed
* to store functions.
* @param {Logger} [log] A possibly customised log
* @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
@@ -603,8 +627,8 @@ export class IndexedDBCryptoStore {
* reject with that exception. On synchronous backends, the
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func) {
return this._backend.doTxn(mode, stores, func);
doTxn(mode, stores, func, log) {
return this._backend.doTxn(mode, stores, func, log);
}
}
@@ -613,6 +637,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
= 'shared_history_inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
+12
View File
@@ -51,6 +51,8 @@ export class MemoryCryptoStore {
this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
// roomId -> array of [senderKey, sessionId]
this._sharedHistoryInboundGroupSessions = {};
}
/**
@@ -467,6 +469,16 @@ export class MemoryCryptoStore {
return Promise.resolve();
}
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) {
const sessions = this._sharedHistoryInboundGroupSessions[roomId] || [];
sessions.push([senderKey, sessionId]);
this._sharedHistoryInboundGroupSessions[roomId] = sessions;
}
getSharedHistoryInboundGroupSessions(roomId) {
return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []);
}
// Session key backups
doTxn(mode, stores, func) {
+3 -2
View File
@@ -26,6 +26,7 @@ import {
newUserCancelledError,
} from './Error';
import {encodeUnpaddedBase64, decodeBase64} from "../olmlib";
import {logger} from '../../logger';
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
@@ -94,7 +95,7 @@ export class ReciprocateQRCode extends Base {
if (!targetKey) throw newKeyMismatchError();
if (keyInfo !== targetKey) {
console.error("key ID from key info does not match");
logger.error("key ID from key info does not match");
throw newKeyMismatchError();
}
for (const deviceKeyId in device.keys) {
@@ -102,7 +103,7 @@ export class ReciprocateQRCode extends Base {
const deviceTargetKey = keys[deviceKeyId];
if (!deviceTargetKey) throw newKeyMismatchError();
if (device.keys[deviceKeyId] !== deviceTargetKey) {
console.error("master key does not match");
logger.error("master key does not match");
throw newKeyMismatchError();
}
}
@@ -179,7 +179,9 @@ export class ToDeviceChannel {
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
const nonChosenDevices = this._devices.filter(
d => d !== this._deviceId && d !== this._client.getDeviceId(),
);
if (nonChosenDevices.length) {
const message = this.completeContent({
code: "m.accepted",
+2 -2
View File
@@ -271,10 +271,10 @@ MatrixHttpApi.prototype = {
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
let resp;
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(xhr.timeout_timer);
var resp;
try {
if (xhr.status === 0) {
throw new AbortError();
@@ -961,7 +961,7 @@ export async function retryNetworkOperation(maxAttempts, callback) {
try {
if (attempts > 0) {
const timeout = 1000 * Math.pow(2, attempts);
console.log(`network operation failed ${attempts} times,` +
logger.log(`network operation failed ${attempts} times,` +
` retrying in ${timeout}ms...`);
await new Promise(r => setTimeout(r, timeout));
}
+34 -3
View File
@@ -1,6 +1,6 @@
/*
Copyright 2018 André Jaenisch
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2021 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.
@@ -19,7 +19,7 @@ limitations under the License.
* @module logger
*/
import log from "loglevel";
import log, { Logger } from "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
@@ -36,15 +36,22 @@ const DEFAULT_NAMESPACE = "matrix";
// when logging so we always get the current value of console methods.
log.methodFactory = function(methodName, logLevel, loggerName) {
return function(...args) {
/* eslint-disable @babel/no-invalid-this */
if (this.prefix) {
args.unshift(this.prefix);
}
/* eslint-enable @babel/no-invalid-this */
const supportedByConsole = methodName === "error" ||
methodName === "warn" ||
methodName === "trace" ||
methodName === "info";
/* eslint-disable no-console */
if (supportedByConsole) {
return console[methodName](...args);
} else {
return console.log(...args);
}
/* eslint-enable no-console */
};
};
@@ -52,6 +59,30 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
* 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.
*/
export const logger = log.getLogger(DEFAULT_NAMESPACE);
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
logger.setLevel(log.levels.DEBUG);
interface PrefixedLogger extends Logger {
withPrefix?: (prefix: string) => PrefixedLogger;
prefix?: string;
}
function extendLogger(logger: PrefixedLogger) {
logger.withPrefix = function(prefix: string): PrefixedLogger {
const existingPrefix = this.prefix || "";
return getPrefixedLogger(existingPrefix + prefix);
};
}
extendLogger(logger);
function getPrefixedLogger(prefix): PrefixedLogger {
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`);
if (prefixLogger.prefix !== prefix) {
// Only do this setup work the first time through, as loggers are saved by name.
extendLogger(prefixLogger);
prefixLogger.prefix = prefix;
prefixLogger.setLevel(log.levels.DEBUG);
}
return prefixLogger;
}
+48 -17
View File
@@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -52,7 +52,7 @@ export * from "./store/session/webstorage";
export * from "./crypto/store/memory-crypto-store";
export * from "./crypto/store/indexeddb-crypto-store";
export * from "./content-repo";
export const ContentHelpers = import("./content-helpers");
export * as ContentHelpers from "./content-helpers";
export {
createNewMatrixCall,
setAudioOutput as setMatrixCallAudioOutput,
@@ -113,7 +113,7 @@ export function setCryptoStoreFactory(fac) {
cryptoStoreFactory = fac;
}
interface ICreateClientOpts {
export interface ICreateClientOpts {
baseUrl: string;
idBaseUrl?: string;
store?: Store;
@@ -126,6 +126,7 @@ interface ICreateClientOpts {
identityServer?: any;
localTimeoutMs?: number;
useAuthorizationHeader?: boolean;
timelineSupport?: boolean;
queryParams?: Record<string, unknown>;
deviceToImport?: {
olmDevice: {
@@ -136,26 +137,56 @@ interface ICreateClientOpts {
userId: string;
deviceId: string;
};
pickleKey?: string;
sessionStore?: any;
unstableClientRelationAggregation?: boolean;
verificationMethods?: Array<any>;
forceTURN?: boolean;
iceCandidatePoolSize?: number,
supportsCallTransfer?: boolean,
fallbackICEServerAllowed?: boolean;
cryptoCallbacks?: {
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => unknown;
shouldUpgradeDeviceVerifications?: (
users: Record<string, any>
) => Promise<Array<string>>;
getSecretStorageKey?: (
keys: {keys: Record<string, {pubkey: Uint8Array}>}, name: string
) => Promise<[string, Uint8Array] | null>;
cacheSecretStorageKey?: (keyId: string, key: Uint8Array) => unknown;
onSecretRequested?: (
name: string, userId: string, deviceId: string,
requestId: string, deviceTrust: any
) => Promise<string>;
cryptoCallbacks?: ICryptoCallbacks;
}
export interface ICryptoCallbacks {
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
shouldUpgradeDeviceVerifications?: (
users: Record<string, any>
) => Promise<string[]>;
getSecretStorageKey?: (
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
) => Promise<[string, Uint8Array] | null>;
cacheSecretStorageKey?: (
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
) => void;
onSecretRequested?: (
userId: string, deviceId: string,
requestId: string, secretName: string, deviceTrust: IDeviceTrustLevel
) => Promise<string>;
getDehydrationKey?: (
keyInfo: ISecretStorageKeyInfo,
checkFunc: (Uint8Array) => void,
) => Promise<Uint8Array>;
}
// TODO: Move this to `SecretStorage` once converted
export interface ISecretStorageKeyInfo {
passphrase?: {
algorithm: "m.pbkdf2";
iterations: number;
salt: string;
};
iv?: string;
mac?: string;
}
// TODO: Move this to `CrossSigning` once converted
export interface IDeviceTrustLevel {
isVerified(): boolean;
isCrossSigningVerified(): boolean;
isLocallyVerified(): boolean;
isTofu(): boolean;
}
/**
+1 -1
View File
@@ -806,7 +806,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
this.room,
);
isNewRelations = true;
relatesToEvent = this.findEventById(relatesToEventId);
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
}
+32 -3
View File
@@ -168,7 +168,15 @@ export const MatrixEvent = function(
/* The txnId with which this event was sent if it was during this session,
allows for a unique ID which does not change when the event comes back down sync.
*/
this._txnId = null;
this._txnId = event.txn_id || null;
/* Set an approximate timestamp for the event relative the local clock.
* This will inherently be approximate because it doesn't take into account
* the time between the server putting the 'age' field on the event as it sent
* it to us and the time we're now constructing this event, but that's better
* than assuming the local clock is in sync with the origin HS's clock.
*/
this._localTimestamp = Date.now() - this.getAge();
};
utils.inherits(MatrixEvent, EventEmitter);
@@ -311,11 +319,12 @@ utils.extend(MatrixEvent.prototype, {
/**
* Get the age of the event when this function was called.
* Relies on the local clock being in sync with the clock of the original homeserver.
* This is the 'age' field adjusted according to how long this client has
* had the event.
* @return {Number} The age of this event in milliseconds.
*/
getLocalAge: function() {
return Date.now() - this.getTs();
return Date.now() - this._localTimestamp;
},
/**
@@ -802,6 +811,24 @@ utils.extend(MatrixEvent.prototype, {
return this.getType() === "m.room.redaction";
},
/**
* Get the (decrypted, if necessary) redaction event JSON
* if event was redacted
*
* @returns {object} The redaction event JSON, or an empty object
*/
getRedactionEvent: function() {
if (!this.isRedacted()) return null;
if (this._clearEvent.unsigned) {
return this._clearEvent.unsigned.redacted_because;
} else if (this.event.unsigned.redacted_because) {
return this.event.unsigned.redacted_because;
} else {
return {};
}
},
/**
* Get the push actions, if known, for this event
*
@@ -904,6 +931,8 @@ utils.extend(MatrixEvent.prototype, {
/**
* Set an event that replaces the content of this event, through an m.replace relation.
*
* @fires module:models/event.MatrixEvent#"Event.replaced"
*
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
*/
makeReplaced(newEvent) {
+5 -4
View File
@@ -16,6 +16,7 @@ limitations under the License.
import {EventEmitter} from 'events';
import {EventStatus} from '../models/event';
import {logger} from '../logger';
/**
* A container for relation events that supports easy access to common ways of
@@ -60,7 +61,7 @@ export class Relations extends EventEmitter {
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
logger.error("Event must have relation info");
return;
}
@@ -68,7 +69,7 @@ export class Relations extends EventEmitter {
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
logger.error("Event relation info doesn't match this container");
return;
}
@@ -104,7 +105,7 @@ export class Relations extends EventEmitter {
const relation = event.getRelation();
if (!relation) {
console.error("Event must have relation info");
logger.error("Event must have relation info");
return;
}
@@ -112,7 +113,7 @@ export class Relations extends EventEmitter {
const eventType = event.getType();
if (this.relationType !== relationType || this.eventType !== eventType) {
console.error("Event relation info doesn't match this container");
logger.error("Event relation info doesn't match this container");
return;
}
+5 -2
View File
@@ -290,6 +290,9 @@ RoomMember.prototype.getMxcAvatarUrl = function() {
return null;
};
const MXID_PATTERN = /@.+:.+/;
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
function calculateDisplayName(selfUserId, displayName, roomState) {
if (!displayName || displayName === selfUserId) {
return selfUserId;
@@ -308,13 +311,13 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
// 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
let disambiguate = /@.+:.+/.test(displayName);
let disambiguate = MXID_PATTERN.test(displayName);
if (!disambiguate) {
// Also show mxid if the display name contains any LTR/RTL characters as these
// make it very difficult for us to find similar *looking* display names
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName);
disambiguate = LTR_RTL_PATTERN.test(displayName);
}
if (!disambiguate) {
+18 -1
View File
@@ -23,6 +23,7 @@ import {EventEmitter} from "events";
import {RoomMember} from "./room-member";
import {logger} from '../logger';
import * as utils from "../utils";
import {EventType} from "../@types/event";
// possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1;
@@ -350,8 +351,14 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
} else if (event.getType() === "m.room.power_levels") {
const members = utils.values(self.members);
utils.forEach(members, function(member) {
// We only propagate `RoomState.members` event if the
// power levels has been changed
// large room suffer from large re-rendering especially when not needed
const oldLastModified = member.getLastModifiedTime();
member.setPowerLevelEvent(event);
self.emit("RoomState.members", event, self, member);
if (oldLastModified !== member.getLastModifiedTime()) {
self.emit("RoomState.members", event, self, member);
}
});
// assume all our sentinels are now out-of-date
@@ -718,6 +725,16 @@ RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
return member.powerLevel >= notifLevel;
};
/**
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
RoomState.prototype.getJoinRule = function() {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
return joinRuleContent["join_rule"] || "invite";
};
function _updateThirdPartyTokenCache(roomState, memberEvent) {
if (!memberEvent.getContent().third_party_invite) {
+177 -19
View File
@@ -30,6 +30,7 @@ import {RoomMember} from "./room-member";
import {RoomSummary} from "./room-summary";
import {logger} from '../logger';
import {ReEmitter} from '../ReEmitter';
import {EventType, RoomCreateTypeField, RoomType} from "../@types/event";
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
@@ -37,8 +38,8 @@ import {ReEmitter} from '../ReEmitter';
// room versions which are considered okay for people to run without being asked
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
// return an m.room_versions capability.
const KNOWN_SAFE_ROOM_VERSION = '5';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5'];
const KNOWN_SAFE_ROOM_VERSION = '6';
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -122,6 +123,8 @@ export function Room(roomId, client, myUserId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
this._client = client;
// In some cases, we add listeners for every displayed Matrix event, so it's
// common to have quite a few more than the default limit.
this.setMaxListeners(100);
@@ -188,6 +191,18 @@ export function Room(roomId, client, myUserId, opts) {
if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList = [];
const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId));
if (serializedPendingEventList) {
JSON.parse(serializedPendingEventList)
.forEach(async serializedEvent => {
const event = new MatrixEvent(serializedEvent);
if (event.getType() === "m.room.encrypted") {
await event.attemptDecryption(this._client._crypto);
}
event.setStatus(EventStatus.NOT_SENT);
this.addPendingEvent(event, event.getTxnId());
});
}
}
// read by megolm; boolean value - null indicates "use global value"
@@ -196,7 +211,6 @@ export function Room(roomId, client, myUserId, opts) {
this._summaryHeroes = null;
// awaited by getEncryptionTargetMembers while room members are loading
this._client = client;
if (!this._opts.lazyLoadMembers) {
this._membersPromise = Promise.resolve();
} else {
@@ -204,6 +218,14 @@ export function Room(roomId, client, myUserId, opts) {
}
}
/**
* @param {string} roomId ID of the current room
* @returns {string} Storage key to retrieve pending events
*/
function pendingEventsKey(roomId) {
return `mx_pending_events_${roomId}`;
}
utils.inherits(Room, EventEmitter);
/**
@@ -357,22 +379,59 @@ Room.prototype.getPendingEvents = function() {
};
/**
* Check whether the pending event list contains a given event by ID.
* Removes a pending event for this room
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
* @param {string} eventId
* @return {boolean} True if an element was removed.
*/
Room.prototype.hasPendingEvent = function(eventId) {
Room.prototype.removePendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call hasPendingEvent with pendingEventOrdering == " +
"Cannot call removePendingEvent with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
const removed = utils.removeElement(
this._pendingEventList,
function(ev) {
return ev.getId() == eventId;
}, false,
);
this._savePendingEvents();
return removed;
};
/**
* Check whether the pending event list contains a given event by ID.
* If pending event ordering is not "detached" then this returns false.
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
*/
Room.prototype.hasPendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
return false;
}
return this._pendingEventList.some(event => event.getId() === eventId);
};
/**
* Get a specific event from the pending event list, if configured, null otherwise.
*
* @param {string} eventId The event ID to check for.
* @return {MatrixEvent}
*/
Room.prototype.getPendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
return null;
}
return this._pendingEventList.find(event => event.getId() === eventId);
};
/**
* Get the live unfiltered timeline for this room.
*
@@ -805,7 +864,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
*/
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
allowDefault) {
const roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
if (allowDefault === undefined) {
allowDefault = true;
}
@@ -823,6 +882,15 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
return null;
};
/**
* Get the mxc avatar url for the room, if one was set.
* @return {string} the mxc avatar url or falsy
*/
Room.prototype.getMxcAvatarUrl = function() {
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
return roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
};
/**
* Get the aliases this room has according to the room's state
* The aliases returned by this function may not necessarily
@@ -1170,7 +1238,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
* unique transaction id.
*/
Room.prototype.addPendingEvent = function(event, txnId) {
if (event.status !== EventStatus.SENDING) {
if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
throw new Error("addPendingEvent called on an event with status " +
event.status);
}
@@ -1197,7 +1265,7 @@ Room.prototype.addPendingEvent = function(event, txnId) {
event.setStatus(EventStatus.NOT_SENT);
}
this._pendingEventList.push(event);
this._savePendingEvents();
if (event.isRelation()) {
// For pending events, add them to the relations collection immediately.
// (The alternate case below already covers this as part of adding to
@@ -1234,6 +1302,46 @@ Room.prototype.addPendingEvent = function(event, txnId) {
this.emit("Room.localEchoUpdated", event, this, null, null);
};
/**
* Persists all pending events to local storage
*
* If the current room is encrypted only encrypted events will be persisted
* all messages that are not yet encrypted will be discarded
*
* This is because the flow of EVENT_STATUS transition is
* queued => sending => encrypting => sending => sent
*
* Steps 3 and 4 are skipped for unencrypted room.
* It is better to discard an unencrypted message rather than persisting
* it locally for everyone to read
*/
Room.prototype._savePendingEvents = function() {
if (this._pendingEventList) {
const pendingEvents = this._pendingEventList.map(event => {
return {
...event.event,
txn_id: event.getTxnId(),
};
}).filter(event => {
// Filter out the unencrypted messages if the room is encrypted
const isEventEncrypted = event.type === "m.room.encrypted";
const isRoomEncrypted = this._client.isRoomEncrypted(this.roomId);
return isEventEncrypted || !isRoomEncrypted;
});
const { store } = this._client._sessionStore;
if (this._pendingEventList.length > 0) {
store.setItem(
pendingEventsKey(this.roomId),
JSON.stringify(pendingEvents),
);
} else {
store.removeItem(pendingEventsKey(this.roomId));
}
}
};
/**
* Used to aggregate the local echo for a relation, and also
* for re-applying a relation after it's redaction has been cancelled,
@@ -1288,12 +1396,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
// if it's in the pending list, remove it
if (this._pendingEventList) {
utils.removeElement(
this._pendingEventList,
function(ev) {
return ev.getId() == oldEventId;
}, false,
);
this.removePendingEvent(oldEventId);
}
// replace the event source (this will preserve the plaintext payload if
@@ -1412,6 +1515,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
}
this.removeEvent(oldEventId);
}
this._savePendingEvents();
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
};
@@ -1810,7 +1914,7 @@ Room.prototype.getAccountData = function(type) {
/**
* Returns wheter the syncing user has permission to send a message in the room
* Returns whether the syncing user has permission to send a message in the room
* @return {boolean} true if the user should be permitted to send
* message events into the room.
*/
@@ -1819,6 +1923,51 @@ Room.prototype.maySendMessage = function() {
this.currentState.maySendEvent('m.room.message', this.myUserId);
};
/**
* Returns whether the given user has permissions to issue an invite for this room.
* @param {string} userId the ID of the Matrix user to check permissions for
* @returns {boolean} true if the user should be permitted to issue invites for this room.
*/
Room.prototype.canInvite = function(userId) {
let canInvite = this.getMyMembership() === "join";
const powerLevelsEvent = this.currentState.getStateEvents(EventType.RoomPowerLevels, "");
const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent();
const me = this.getMember(userId);
if (powerLevels && me && powerLevels.invite > me.powerLevel) {
canInvite = false;
}
return canInvite;
};
/**
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
* @returns {string} the join_rule applied to this room
*/
Room.prototype.getJoinRule = function() {
return this.currentState.getJoinRule();
};
/**
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
*/
Room.prototype.getType = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
return undefined;
}
return createEvent.getContent()[RoomCreateTypeField];
};
/**
* Returns whether the room is a space-room as defined by MSC1772.
* @returns {boolean} true if the room's type is RoomType.Space
*/
Room.prototype.isSpaceRoom = function() {
return this.getType() === RoomType.Space;
};
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
@@ -2036,3 +2185,12 @@ function memberNamesToRoomName(names, count = (names.length + 1)) {
*
* @param {EventStatus} oldStatus The previous event status.
*/
/**
* Fires when the logged in user's membership in the room is updated.
*
* @event module:models/room~Room#"Room.myMembership"
* @param {Room} room The room in which the membership has been updated
* @param {string} membership The new membership value
* @param {string} prevMembership The previous membership value
*/
+6 -1
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
import {logger} from './logger';
/**
* @module pushprocessor
@@ -267,6 +268,10 @@ export function PushProcessor(client) {
return cond.value === val;
}
if (typeof cond.pattern !== 'string') {
return false;
}
let regex;
if (cond.key == 'content.body') {
@@ -444,7 +449,7 @@ PushProcessor.rewriteDefaultRules = function(incomingRules) {
} else {
// Add the rule
const ruleId = override.rule_id;
console.warn(`Adding default global override for ${ruleId}`);
logger.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
+16 -1
View File
@@ -15,9 +15,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const DIGITS = "0123456789";
export function randomString(len: number): string {
return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
}
export function randomLowercaseString(len: number): string {
return randomStringFrom(len, LOWERCASE);
}
export function randomUppercaseString(len: number): string {
return randomStringFrom(len, UPPERCASE);
}
function randomStringFrom(len: number, chars: string): string {
let ret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; ++i) {
ret += chars.charAt(Math.floor(Math.random() * chars.length));
+1 -1
View File
@@ -164,7 +164,7 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
if (err.name === "M_LIMIT_EXCEEDED") {
const waitTime = err.data.retry_after_ms;
if (waitTime) {
if (waitTime > 0) {
return waitTime;
}
}
+3 -3
View File
@@ -226,7 +226,7 @@ LocalIndexedDBStoreBackend.prototype = {
account_data: {
events: accountData,
},
});
}, true);
});
},
@@ -416,7 +416,7 @@ LocalIndexedDBStoreBackend.prototype = {
},
syncToDatabase: function(userTuples) {
const syncData = this._syncAccumulator.getJSON();
const syncData = this._syncAccumulator.getJSON(true);
return Promise.all([
this._persistUserPresenceEvents(userTuples),
@@ -435,7 +435,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData, groupsData) {
logger.log("Persisting sync data up to ", nextBatch);
logger.log("Persisting sync data up to", nextBatch);
return utils.promiseTry(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
+3 -3
View File
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable babel/no-invalid-this */
/* eslint-disable @babel/no-invalid-this */
import {MemoryStore} from "./memory";
import * as utils from "../utils";
@@ -51,8 +51,8 @@ const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
* sync from the server is not required. This does not reduce memory usage as all
* the data is eagerly fetched when <code>startup()</code> is called.
* <pre>
* let opts = { localStorage: window.localStorage };
* let store = new IndexedDBStore();
* let opts = { indexedDB: window.indexedDB, localStorage: window.localStorage };
* let store = new IndexedDBStore(opts);
* await store.startup(); // load from indexed db
* let client = sdk.createClient({
* store: store,
+47 -12
View File
@@ -87,8 +87,8 @@ export class SyncAccumulator {
};
}
accumulate(syncResponse) {
this._accumulateRooms(syncResponse);
accumulate(syncResponse, fromDatabase) {
this._accumulateRooms(syncResponse, fromDatabase);
this._accumulateGroups(syncResponse);
this._accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
@@ -107,35 +107,36 @@ export class SyncAccumulator {
/**
* Accumulate incremental /sync room data.
* @param {Object} syncResponse the complete /sync JSON
* @param {boolean} fromDatabase True if the sync response is one saved to the database
*/
_accumulateRooms(syncResponse) {
_accumulateRooms(syncResponse, fromDatabase) {
if (!syncResponse.rooms) {
return;
}
if (syncResponse.rooms.invite) {
Object.keys(syncResponse.rooms.invite).forEach((roomId) => {
this._accumulateRoom(
roomId, "invite", syncResponse.rooms.invite[roomId],
roomId, "invite", syncResponse.rooms.invite[roomId], fromDatabase,
);
});
}
if (syncResponse.rooms.join) {
Object.keys(syncResponse.rooms.join).forEach((roomId) => {
this._accumulateRoom(
roomId, "join", syncResponse.rooms.join[roomId],
roomId, "join", syncResponse.rooms.join[roomId], fromDatabase,
);
});
}
if (syncResponse.rooms.leave) {
Object.keys(syncResponse.rooms.leave).forEach((roomId) => {
this._accumulateRoom(
roomId, "leave", syncResponse.rooms.leave[roomId],
roomId, "leave", syncResponse.rooms.leave[roomId], fromDatabase,
);
});
}
}
_accumulateRoom(roomId, category, data) {
_accumulateRoom(roomId, category, data, fromDatabase) {
// Valid /sync state transitions
// +--------+ <======+ 1: Accept an invite
// +== | INVITE | | (5) 2: Leave a room
@@ -159,7 +160,7 @@ export class SyncAccumulator {
delete this.inviteRooms[roomId];
}
// (3)
this._accumulateJoinState(roomId, data);
this._accumulateJoinState(roomId, data, fromDatabase);
break;
case "leave":
if (this.inviteRooms[roomId]) { // (4)
@@ -203,7 +204,7 @@ export class SyncAccumulator {
}
// Accumulate timeline and state events in a room.
_accumulateJoinState(roomId, data) {
_accumulateJoinState(roomId, data, fromDatabase) {
// We expect this function to be called a lot (every /sync) so we want
// this to be fast. /sync stores events in an array but we often want
// to clobber based on type/state_key. Rather than convert arrays to
@@ -337,8 +338,20 @@ export class SyncAccumulator {
setState(currentData._currentState, e);
// append the event to the timeline. The back-pagination token
// corresponds to the first event in the timeline
let transformedEvent;
if (!fromDatabase) {
transformedEvent = Object.assign({}, e);
if (transformedEvent.unsigned !== undefined) {
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
}
const age = e.unsigned ? e.unsigned.age : e.age;
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
} else {
transformedEvent = e;
}
currentData._timeline.push({
event: e,
event: transformedEvent,
token: index === 0 ? data.timeline.prev_batch : null,
});
});
@@ -405,6 +418,7 @@ export class SyncAccumulator {
* represents all room data that should be stored. This should be paired
* with the sync token which represents the most recent /sync response
* provided to accumulate().
* @param {boolean} forDatabase True to generate a sync to be saved to storage
* @return {Object} An object with a "nextBatch", "roomsData" and "accountData"
* keys.
* The "nextBatch" key is a string which represents at what point in the
@@ -414,7 +428,7 @@ export class SyncAccumulator {
* /sync response from the 'rooms' key onwards. The "accountData" key is
* a list of raw events which represent global account data.
*/
getJSON() {
getJSON(forDatabase) {
const data = {
join: {},
invite: {},
@@ -486,7 +500,28 @@ export class SyncAccumulator {
}
roomJson.timeline.prev_batch = msgData.token;
}
roomJson.timeline.events.push(msgData.event);
let transformedEvent;
if (!forDatabase && msgData.event._localTs) {
// This means we have to copy each event so we can fix it up to
// set a correct 'age' parameter whilst keeping the local timestamp
// on our stored event. If this turns out to be a bottleneck, it could
// be optimised either by doing this in the main process after the data
// has been structured-cloned to go between the worker & main process,
// or special-casing data from saved syncs to read the local timstamp
// directly rather than turning it into age to then immediately be
// transformed back again into a local timestamp.
transformedEvent = Object.assign({}, msgData.event);
if (transformedEvent.unsigned !== undefined) {
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
}
delete transformedEvent._localTs;
transformedEvent.unsigned = transformedEvent.unsigned || {};
transformedEvent.unsigned.age = Date.now() - msgData.event._localTs;
} else {
transformedEvent = msgData.event;
}
roomJson.timeline.events.push(transformedEvent);
});
// Add state data: roll back current state to the start of timeline,
+36 -25
View File
@@ -93,7 +93,7 @@ export function SyncApi(client, opts) {
};
}
this.opts = opts;
this._peekRoomId = null;
this._peekRoom = null;
this._currentSyncRequest = null;
this._syncState = null;
this._syncStateData = null; // additional data (eg. error object for failed sync)
@@ -264,17 +264,18 @@ SyncApi.prototype.syncLeftRooms = function() {
* store.
*/
SyncApi.prototype.peek = function(roomId) {
const self = this;
if (this._peekRoom && this._peekRoom.roomId === roomId) {
return Promise.resolve(this._peekRoom);
}
const client = this.client;
this._peekRoomId = roomId;
return this.client.roomInitialSync(roomId, 20).then(function(response) {
this._peekRoom = this.createRoom(roomId);
return this.client.roomInitialSync(roomId, 20).then((response) => {
// make sure things are init'd
response.messages = response.messages || {};
response.messages.chunk = response.messages.chunk || [];
response.state = response.state || [];
const peekRoom = self.createRoom(roomId);
// FIXME: Mostly duplicated from _processRoomEvents but not entirely
// because "state" in this API is at the BEGINNING of the chunk
const oldStateEvents = utils.map(
@@ -309,28 +310,28 @@ SyncApi.prototype.peek = function(roomId) {
// fire off pagination requests in response to the Room.timeline
// events.
if (response.messages.start) {
peekRoom.oldState.paginationToken = response.messages.start;
this._peekRoom.oldState.paginationToken = response.messages.start;
}
// set the state of the room to as it was after the timeline executes
peekRoom.oldState.setStateEvents(oldStateEvents);
peekRoom.currentState.setStateEvents(stateEvents);
this._peekRoom.oldState.setStateEvents(oldStateEvents);
this._peekRoom.currentState.setStateEvents(stateEvents);
self._resolveInvites(peekRoom);
peekRoom.recalculate();
this._resolveInvites(this._peekRoom);
this._peekRoom.recalculate();
// roll backwards to diverge old state. addEventsToTimeline
// will overwrite the pagination token, so make sure it overwrites
// it with the right thing.
peekRoom.addEventsToTimeline(messages.reverse(), true,
peekRoom.getLiveTimeline(),
this._peekRoom.addEventsToTimeline(messages.reverse(), true,
this._peekRoom.getLiveTimeline(),
response.messages.start);
client.store.storeRoom(peekRoom);
client.emit("Room", peekRoom);
client.store.storeRoom(this._peekRoom);
client.emit("Room", this._peekRoom);
self._peekPoll(peekRoom);
return peekRoom;
this._peekPoll(this._peekRoom);
return this._peekRoom;
});
};
@@ -339,16 +340,16 @@ SyncApi.prototype.peek = function(roomId) {
* peeked.
*/
SyncApi.prototype.stopPeeking = function() {
this._peekRoomId = null;
this._peekRoom = null;
};
/**
* Do a peek room poll.
* @param {Room} peekRoom
* @param {string} token from= token
* @param {string?} token from= token
*/
SyncApi.prototype._peekPoll = function(peekRoom, token) {
if (this._peekRoomId !== peekRoom.roomId) {
if (this._peekRoom !== peekRoom) {
debuglog("Stopped peeking in room %s", peekRoom.roomId);
return;
}
@@ -360,7 +361,7 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
timeout: 30 * 1000,
from: token,
}, undefined, 50 * 1000).then(function(res) {
if (self._peekRoomId !== peekRoom.roomId) {
if (self._peekRoom !== peekRoom) {
debuglog("Stopped peeking in room %s", peekRoom.roomId);
return;
}
@@ -475,9 +476,9 @@ SyncApi.prototype.sync = function() {
this._running = true;
if (global.document) {
if (global.window && global.window.addEventListener) {
this._onOnlineBound = this._onOnline.bind(this);
global.document.addEventListener("online", this._onOnlineBound, false);
global.window.addEventListener("online", this._onOnlineBound, false);
}
let savedSyncPromise = Promise.resolve();
@@ -643,8 +644,8 @@ SyncApi.prototype.sync = function() {
*/
SyncApi.prototype.stop = function() {
debuglog("SyncApi.stop");
if (global.document) {
global.document.removeEventListener("online", this._onOnlineBound, false);
if (global.window) {
global.window.removeEventListener("online", this._onOnlineBound, false);
this._onOnlineBound = undefined;
}
this._running = false;
@@ -1361,6 +1362,16 @@ SyncApi.prototype._processSyncResponse = async function(
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
this.opts.crypto.updateOneTimeKeyCount(currentCount);
}
if (this.opts.crypto && data["org.matrix.msc2732.device_unused_fallback_key_types"]) {
// The presence of device_unused_fallback_key_types indicates that the
// server supports fallback keys. If there's no unused
// signed_curve25519 fallback key we need a new one.
const unusedFallbackKeys = data["org.matrix.msc2732.device_unused_fallback_key_types"];
this.opts.crypto.setNeedsNewFallback(
unusedFallbackKeys instanceof Array &&
!unusedFallbackKeys.includes("signed_curve25519"),
);
}
};
/**
+9
View File
@@ -745,6 +745,15 @@ export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
// Creates and awaits all promises, running no more than `chunkSize` at the same time
export async function chunkPromises<T>(fns: (() => Promise<T>)[], chunkSize: number): Promise<T[]> {
const results: T[] = [];
for (let i = 0; i < fns.length; i += chunkSize) {
results.push(...(await Promise.all(fns.slice(i, i + chunkSize).map(fn => fn()))));
}
return results;
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
-1427
View File
File diff suppressed because it is too large Load Diff
+1965
View File
File diff suppressed because it is too large Load Diff
+276
View File
@@ -0,0 +1,276 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import MatrixEvent from '../models/event';
import {logger} from '../logger';
import { createNewMatrixCall, MatrixCall, CallErrorCode, CallState, CallDirection } from './call';
import { EventType } from '../@types/event';
import { MatrixClient } from '../client';
// Don't ring unless we'd be ringing for at least 3 seconds: the user needs some
// time to press the 'accept' button
const RING_GRACE_PERIOD = 3000;
export class CallEventHandler {
client: MatrixClient;
calls: Map<string, MatrixCall>;
callEventBuffer: MatrixEvent[];
candidateEventsByCall: Map<string, Array<MatrixEvent>>;
constructor(client: MatrixClient) {
this.client = client;
this.calls = new Map<string, MatrixCall>();
// The sync code always emits one event at a time, so it will patiently
// wait for us to finish processing a call invite before delivering the
// next event, even if that next event is a hangup. We therefore accumulate
// all our call events and then process them on the 'sync' event, ie.
// each time a sync has completed. This way, we can avoid emitting incoming
// call events if we get both the invite and answer/hangup in the same sync.
// This happens quite often, eg. replaying sync from storage, catchup sync
// after loading and after we've been offline for a bit.
this.callEventBuffer = [];
this.candidateEventsByCall = new Map<string, Array<MatrixEvent>>();
this.client.on("sync", this.evaluateEventBuffer);
this.client.on("event", this.onEvent);
}
public stop() {
this.client.removeListener("sync", this.evaluateEventBuffer);
this.client.removeListener("event", this.onEvent);
}
private evaluateEventBuffer = () => {
if (this.client.getSyncState() === "SYNCING") {
// don't process any events until they are all decrypted
if (this.callEventBuffer.some((e) => e.isBeingDecrypted())) return;
const ignoreCallIds = new Set<String>();
// inspect the buffer and mark all calls which have been answered
// or hung up before passing them to the call event handler.
for (const ev of this.callEventBuffer) {
if (ev.getType() === EventType.CallAnswer ||
ev.getType() === EventType.CallHangup) {
ignoreCallIds.add(ev.getContent().call_id);
}
}
// now loop through the buffer chronologically and inject them
for (const e of this.callEventBuffer) {
if (
e.getType() === EventType.CallInvite &&
ignoreCallIds.has(e.getContent().call_id)
) {
// This call has previously been answered or hung up: ignore it
continue;
}
try {
this.handleCallEvent(e);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
}
this.callEventBuffer = [];
}
}
private onEvent = (event: MatrixEvent) => {
// any call events or ones that might be once they're decrypted
if (event.getType().indexOf("m.call.") === 0 || event.isBeingDecrypted()) {
// queue up for processing once all events from this sync have been
// processed (see above).
this.callEventBuffer.push(event);
}
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// add an event listener for once the event is decrypted.
event.once("Event.decrypted", () => {
if (event.getType().indexOf("m.call.") === -1) return;
if (this.callEventBuffer.includes(event)) {
// we were waiting for that event to decrypt, so recheck the buffer
this.evaluateEventBuffer();
} else {
// This one wasn't buffered so just run the event handler for it
// straight away
try {
this.handleCallEvent(event);
} catch (e) {
logger.error("Caught exception handling call event", e);
}
}
});
}
}
private handleCallEvent(event: MatrixEvent) {
const content = event.getContent();
let call = content.call_id ? this.calls.get(content.call_id) : undefined;
//console.info("RECV %s content=%s", event.getType(), JSON.stringify(content));
if (event.getType() === EventType.CallInvite) {
if (event.getSender() === this.client.credentials.userId) {
return; // ignore invites you send
}
if (event.getLocalAge() > content.lifetime - RING_GRACE_PERIOD) {
return; // expired call
}
if (call && call.state === CallState.Ended) {
return; // stale/old invite event
}
if (call) {
logger.log(
`WARN: Already have a MatrixCall with id ${content.call_id} but got an ` +
`invite. Clobbering.`,
);
}
const timeUntilTurnCresExpire = this.client.getTurnServersExpiry() - Date.now();
logger.info("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
call = createNewMatrixCall(this.client, event.getRoomId(), {
forceTURN: this.client._forceTURN,
});
if (!call) {
logger.log(
"Incoming call ID " + content.call_id + " but this client " +
"doesn't support WebRTC",
);
// don't hang up the call: there could be other clients
// connected that do support WebRTC and declining the
// the call on their behalf would be really annoying.
return;
}
call.callId = content.call_id;
call.initWithInvite(event);
this.calls.set(call.callId, call);
// if we stashed candidate events for that call ID, play them back now
if (this.candidateEventsByCall.get(call.callId)) {
for (const ev of this.candidateEventsByCall.get(call.callId)) {
call.onRemoteIceCandidatesReceived(ev);
}
}
// Were we trying to call that user (room)?
let existingCall;
for (const thisCall of this.calls.values()) {
const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes(
thisCall.state,
);
if (
call.roomId === thisCall.roomId &&
thisCall.direction === CallDirection.Outbound &&
isCalling
) {
existingCall = thisCall;
break;
}
}
if (existingCall) {
// If we've only got to wait_local_media or create_offer and
// we've got an invite, pick the incoming call because we know
// we haven't sent our invite yet otherwise, pick whichever
// call has the lowest call ID (by string comparison)
if (existingCall.state === CallState.WaitLocalMedia ||
existingCall.state === CallState.CreateOffer ||
existingCall.callId > call.callId) {
logger.log(
"Glare detected: answering incoming call " + call.callId +
" and canceling outgoing call " + existingCall.callId,
);
existingCall.replacedBy(call);
call.answer();
} else {
logger.log(
"Glare detected: rejecting incoming call " + call.callId +
" and keeping outgoing call " + existingCall.callId,
);
call.hangup(CallErrorCode.Replaced, true);
}
} else {
this.client.emit("Call.incoming", call);
}
} else if (event.getType() === EventType.CallAnswer) {
if (!call) {
return;
}
if (event.getSender() === this.client.credentials.userId) {
if (call.state === CallState.Ringing) {
call.onAnsweredElsewhere(content);
}
} else {
call.onAnswerReceived(event);
}
} else if (event.getType() === EventType.CallCandidates) {
if (event.getSender() === this.client.credentials.userId) {
return;
}
if (!call) {
// store the candidates; we may get a call eventually.
if (!this.candidateEventsByCall.has(content.call_id)) {
this.candidateEventsByCall.set(content.call_id, []);
}
this.candidateEventsByCall.get(content.call_id).push(event);
} else {
call.onRemoteIceCandidatesReceived(event);
}
} else if ([EventType.CallHangup, EventType.CallReject].includes(event.getType())) {
// Note that we also observe our own hangups here so we can see
// if we've already rejected a call that would otherwise be valid
if (!call) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = createNewMatrixCall(this.client, event.getRoomId());
if (call) {
call.callId = content.call_id;
call.initWithHangup(event);
this.calls.set(content.call_id, call);
}
} else {
if (call.state !== CallState.Ended) {
if (event.getType() === EventType.CallHangup) {
call.onHangupReceived(content);
} else {
call.onRejectReceived(content);
}
this.calls.delete(content.call_id);
}
}
} else if (event.getType() === EventType.CallSelectAnswer) {
if (!call) return;
if (event.getContent().party_id === call.ourPartyId) {
// Ignore remote echo
return;
}
call.onSelectAnswerReceived(event);
} else if (event.getType() === EventType.CallNegotiate) {
if (!call) return;
if (event.getContent().party_id === call.ourPartyId) {
// Ignore remote echo
return;
}
call.onNegotiateReceived(event);
}
}
}
+38
View File
@@ -0,0 +1,38 @@
// allow camelcase as these are events type that go onto the wire
/* eslint-disable camelcase */
interface CallOfferAnswer {
type: string;
sdp: string;
}
export interface CallCapabilities {
'm.call.transferee': boolean;
}
export interface MCallAnswer {
answer: CallOfferAnswer;
capabilities: CallCapabilities;
}
export interface MCallOfferNegotiate {
offer: CallOfferAnswer;
description: CallOfferAnswer;
lifetime: number;
capabilities: CallCapabilities;
}
export interface MCallReplacesTarget {
id: string;
display_name: string;
avatar_url: string;
}
export interface MCallReplacesEvent {
replacement_id: string;
target_user: MCallReplacesTarget;
create_call: string;
await_call: string;
target_room: string;
}
/* eslint-enable camelcase */
+2656 -2706
View File
File diff suppressed because it is too large Load Diff