Compare commits

..

185 Commits

Author SHA1 Message Date
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
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
Š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
florianjacob 22713d8f89 correct IndexedDB store creation example 2020-08-26 14:24:13 +02:00
41 changed files with 1774 additions and 1088 deletions
+3 -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"],
@@ -33,7 +32,7 @@ module.exports = {
},
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",
+151
View File
@@ -1,3 +1,154 @@
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)
+8 -5
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "9.7.0",
"version": "10.0.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepublishOnly": "yarn build",
@@ -15,7 +15,7 @@
"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 73 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"
@@ -61,6 +61,8 @@
"devDependencies": {
"@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",
@@ -72,15 +74,16 @@
"@types/jest": "^26.0.20",
"@types/node": "12",
"@types/request": "^2.48.5",
"babel-eslint": "^10.1.0",
"@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": "^17.0.0",
"docdash": "^1.2.0",
"eslint": "7.18.0",
"eslint-config-matrix-org": "^0.2.0",
"eslint-plugin-babel": "^5.3.1",
"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.1.2",
"jest": "^26.6.3",
+58 -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,7 +99,7 @@ describe('DeviceList', function() {
}
});
function createTestDeviceList() {
function createTestDeviceList(keyDownloadChunkSize = 250) {
const baseApis = {
downloadKeysForUsers: downloadSpy,
getUserId: () => '@test1:sw1v.org',
@@ -78,7 +108,7 @@ describe('DeviceList', function() {
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;
}
@@ -150,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);
});
});
});
+3 -1
View File
@@ -193,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,
});
});
});
+5 -5
View File
@@ -30,7 +30,7 @@ export async function makeTestClients(userInfos, options) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
type: type,
content: msg,
});
@@ -49,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,
@@ -61,13 +61,13 @@ 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
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
logger.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
} else {
+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({
+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);
});
});
});
+65 -1
View File
@@ -79,6 +79,7 @@ class MockRTCPeerConnection {
return Promise.resolve();
}
close() {}
getStats() { return []; }
}
describe('Call', function() {
@@ -122,6 +123,7 @@ describe('Call', function() {
// 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',
@@ -138,7 +140,9 @@ describe('Call', function() {
});
it('should ignore candidate events from non-matching party ID', async function() {
await call.placeVoiceCall();
const callPromise = call.placeVoiceCall();
await client.httpBackend.flush();
await callPromise;
await call.onAnswerReceived({
getContent: () => {
return {
@@ -190,4 +194,64 @@ describe('Call', function() {
// 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: '',
});
});
});
+11
View File
@@ -36,6 +36,10 @@ export enum EventType {
*/
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",
@@ -53,6 +57,7 @@ export enum EventType {
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",
@@ -87,3 +92,9 @@ export enum MsgType {
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
}
+2 -2
View File
@@ -41,7 +41,7 @@ declare global {
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
}
export interface DesktopCapturerConstraints {
interface DesktopCapturerConstraints {
audio: boolean | {
mandatory: {
chromeMediaSource: string;
@@ -56,7 +56,7 @@ declare global {
};
}
export interface DesktopCapturerSource {
interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
+63 -1
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) {
@@ -1518,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
*
@@ -2374,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",
});
};
+131 -52
View File
@@ -61,6 +61,7 @@ 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 = [];
@@ -180,6 +181,11 @@ 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.
*
@@ -367,6 +373,7 @@ 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;
@@ -387,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
@@ -489,19 +500,8 @@ MatrixClient.prototype.rehydrateDevice = async function() {
return;
}
let getDeviceResult;
try {
getDeviceResult = 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());
const getDeviceResult = await this.getDehydratedDevice();
if (!getDeviceResult) {
return;
}
@@ -563,6 +563,27 @@ MatrixClient.prototype.rehydrateDevice = async function() {
}
};
/**
* 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.
@@ -2282,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
@@ -3441,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);
};
@@ -4936,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
@@ -5088,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
@@ -5200,7 +5311,7 @@ MatrixClient.prototype.stopClient = function() {
this._callEventHandler = null;
}
global.clearTimeout(this._checkTurnServersTimeoutID);
global.clearInterval(this._checkTurnServersIntervalID);
if (this._clientWellKnownIntervalID !== undefined) {
global.clearInterval(this._clientWellKnownIntervalID);
}
@@ -5407,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,
@@ -5414,41 +5528,6 @@ async function(roomId, eventId, relationType, eventType, opts = {}) {
};
};
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");
// If we get a 403, there's no point in looping forever.
if (err.httpStatus === 403) {
logger.info("TURN access unavailable for this account");
return;
}
client._checkTurnServersTimeoutID = setTimeout(function() {
checkTurnServers(client);
}, 60000);
});
}
function _reject(callback, reject, err) {
if (callback) {
callback(err);
+16 -9
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
+46 -5
View File
@@ -545,6 +545,7 @@ OlmDevice.prototype.createOutboundSession = async function(
}
});
},
logger.withPrefix("[createOutboundSession]"),
);
return newSessionId;
};
@@ -605,6 +606,7 @@ OlmDevice.prototype.createInboundSession = async function(
}
});
},
logger.withPrefix("[createInboundSession]"),
);
return result;
@@ -619,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) {
@@ -638,6 +642,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
},
);
},
log,
);
return sessionIds;
@@ -651,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) {
@@ -697,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) {
@@ -727,6 +738,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
}
});
},
log,
);
return info;
@@ -761,6 +773,7 @@ OlmDevice.prototype.encryptMessage = async function(
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[encryptMessage]"),
);
return res;
};
@@ -794,6 +807,7 @@ OlmDevice.prototype.decryptMessage = async function(
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[decryptMessage]"),
);
return payloadString;
};
@@ -825,6 +839,7 @@ OlmDevice.prototype.matchesSession = async function(
matches = sessionInfo.session.matches_inbound(ciphertext);
});
},
logger.withPrefix("[matchesSession]"),
);
return matches;
};
@@ -1033,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(
@@ -1089,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]"),
);
};
@@ -1265,6 +1288,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
},
);
},
logger.withPrefix("[decryptGroupMessage]"),
);
if (error) {
@@ -1310,6 +1334,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
},
);
},
logger.withPrefix("[hasInboundSessionKeys]"),
);
return result;
@@ -1365,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;
@@ -1396,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
// =========
+180 -40
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(
@@ -271,7 +297,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
logger.debug(`Shared keys with existing Olm sessions in ${this._roomId}`);
})(),
(async () => {
logger.debug(`Sharing keys with new Olm sessions in ${this._roomId}`);
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
@@ -285,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,
@@ -313,21 +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 with new Olm sessions in ${this._roomId}`);
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 (
@@ -335,13 +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 blocked devices in ${this._roomId}`);
logger.debug(`Notified ${blockedCount} blocked devices in ${this._roomId}`);
})(),
]);
};
@@ -369,15 +400,18 @@ 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},
);
// don't wait for it to complete
@@ -386,7 +420,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
sessionId, key.key,
);
return new OutboundSessionInfo(sessionId);
return new OutboundSessionInfo(sessionId, sharedHistory);
};
/**
@@ -667,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,
},
};
@@ -728,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(
@@ -743,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;
}
}
@@ -771,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) {
@@ -785,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] || {};
@@ -802,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}`,
);
};
/**
@@ -877,7 +931,7 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
}
logger.debug(`Ensuring outbound session in ${this._roomId}`);
await this._ensureOutboundSession(devicesInRoom, blocked, true);
await this._ensureOutboundSession(room, devicesInRoom, blocked, true);
logger.debug(`Ready to encrypt events for ${this._roomId}`);
} catch (e) {
@@ -921,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,
@@ -1346,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)
@@ -1549,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,
},
};
};
@@ -1570,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,
@@ -1578,7 +1644,7 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
session.session_key,
session.sender_claimed_keys,
true,
opts.untrusted ? { untrusted: opts.untrusted } : {},
extraSessionData,
).then(() => {
if (opts.source !== "backup") {
// don't wait for it to complete
@@ -1657,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,
);
+39 -8
View File
@@ -568,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
@@ -1300,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
@@ -1322,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();
}
@@ -1348,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 {
@@ -1360,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();
}
@@ -1374,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 {
@@ -1382,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();
}
+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) {
@@ -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",
+1 -1
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();
+32 -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,6 +36,11 @@ 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" ||
@@ -54,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;
}
+2 -1
View File
@@ -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,
@@ -142,6 +142,7 @@ export interface ICreateClientOpts {
unstableClientRelationAggregation?: boolean;
verificationMethods?: Array<any>;
forceTURN?: boolean;
iceCandidatePoolSize?: number,
supportsCallTransfer?: boolean,
fallbackICEServerAllowed?: boolean;
cryptoCallbacks?: ICryptoCallbacks;
+1 -1
View File
@@ -168,7 +168,7 @@ 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
+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) {
+157 -11
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
@@ -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);
/**
@@ -356,6 +378,31 @@ Room.prototype.getPendingEvents = function() {
return this._pendingEventList;
};
/**
* Removes a pending event for this room
*
* @param {string} eventId
* @return {boolean} True if an element was removed.
*/
Room.prototype.removePendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"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.
@@ -817,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;
}
@@ -835,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
@@ -1182,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);
}
@@ -1209,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
@@ -1246,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,
@@ -1300,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
@@ -1424,6 +1515,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
}
this.removeEvent(oldEventId);
}
this._savePendingEvents();
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
};
@@ -1822,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.
*/
@@ -1831,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.
@@ -2048,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
*/
+4
View File
@@ -268,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') {
+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
@@ -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,
+1 -1
View File
@@ -476,7 +476,7 @@ SyncApi.prototype.sync = function() {
this._running = true;
if (global.window) {
if (global.window && global.window.addEventListener) {
this._onOnlineBound = this._onOnline.bind(this);
global.window.addEventListener("online", this._onOnlineBound, false);
}
+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
+298 -149
View File
@@ -174,6 +174,11 @@ export enum CallErrorCode {
SignallingFailed = 'signalling_timeout',
}
enum ConstraintsType {
Audio = "audio",
Video = "video",
}
/**
* The version field that we set in m.call.* events
*/
@@ -201,9 +206,9 @@ export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource
}
export class CallError extends Error {
code : string;
code: string;
constructor(code : CallErrorCode, msg: string, err: Error) {
constructor(code: CallErrorCode, msg: string, err: Error) {
// Stil ldon't think there's any way to have proper nested errors
super(msg + ": " + err);
@@ -251,8 +256,6 @@ export class MatrixCall extends EventEmitter {
private localAVStream: MediaStream;
private inviteOrAnswerSent: boolean;
private waitForLocalAVStream: boolean;
// XXX: This is either the invite or answer from remote...
private msg: any;
// XXX: I don't know why this is called 'config'.
private config: MediaStreamConstraints;
private successor: MatrixCall;
@@ -284,6 +287,11 @@ export class MatrixCall extends EventEmitter {
private makingOffer: boolean;
private ignoreOffer: boolean;
// If candidates arrive before we've picked an opponent (which, in particular,
// will happen if the opponent sends candidates eagerly before the user answers
// the call) we buffer them up here so we can then add the ones from the party we pick
private remoteCandidateBuffer = new Map<string, RTCIceCandidate[]>();
constructor(opts: CallOpts) {
super();
this.roomId = opts.roomId;
@@ -291,9 +299,6 @@ export class MatrixCall extends EventEmitter {
this.type = null;
this.forceTURN = opts.forceTURN;
this.ourPartyId = this.client.deviceId;
// We compare this to null to checks the presence of a party ID:
// make sure it's null, not undefined
this.opponentPartyId = null;
// Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
@@ -328,11 +333,12 @@ export class MatrixCall extends EventEmitter {
* Place a voice call to this room.
* @throws If you have not specified a listener for 'error' events.
*/
placeVoiceCall() {
async placeVoiceCall() {
logger.debug("placeVoiceCall");
this.checkForErrorListener();
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Voice));
const constraints = getUserMediaContraints(ConstraintsType.Audio);
this.type = CallType.Voice;
await this.placeCallWithConstraints(constraints);
}
/**
@@ -343,13 +349,14 @@ export class MatrixCall extends EventEmitter {
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
async placeVideoCall(remoteVideoElement: HTMLVideoElement, localVideoElement: HTMLVideoElement) {
logger.debug("placeVideoCall");
this.checkForErrorListener();
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
this.placeCallWithConstraints(getUserMediaVideoContraints(CallType.Video));
const constraints = getUserMediaContraints(ConstraintsType.Video);
this.type = CallType.Video;
await this.placeCallWithConstraints(constraints);
}
/**
@@ -365,59 +372,42 @@ export class MatrixCall extends EventEmitter {
async placeScreenSharingCall(
remoteVideoElement: HTMLVideoElement,
localVideoElement: HTMLVideoElement,
selectDesktopCapturerSource: () => Promise<DesktopCapturerSource>,
selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>,
) {
logger.debug("placeScreenSharingCall");
this.checkForErrorListener();
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
if (window.electron?.getDesktopCapturerSources) {
// We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available...");
try {
const selectedSource = await selectDesktopCapturerSource();
const getUserMediaOptions: MediaStreamConstraints | DesktopCapturerConstraints = {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: selectedSource.id,
},
},
}
this.screenSharingStream = await window.navigator.mediaDevices.getUserMedia(getUserMediaOptions);
try {
const screenshareConstraints = await getScreenshareContraints(selectDesktopCapturerSource);
if (!screenshareConstraints) {
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
return;
}
logger.debug("Got screen stream, requesting audio stream...");
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
this.placeCallWithConstraints(audioConstraints);
} catch (err) {
this.emit(CallEvent.Error,
new CallError(
CallErrorCode.NoUserMedia,
"Failed to get screen-sharing stream: ", err,
),
);
}
} else {
/* We do not have access to the Electron desktop capturer,
* therefore we can assume we are on the web */
logger.debug("Electron desktopCapturer is not available...");
try {
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia({'audio': false});
logger.debug("Got screen stream, requesting audio stream...");
const audioConstraints = getUserMediaVideoContraints(CallType.Voice);
this.placeCallWithConstraints(audioConstraints);
} catch (err) {
this.emit(CallEvent.Error,
new CallError(
CallErrorCode.NoUserMedia,
"Failed to get screen-sharing stream: ", err,
),
);
if (window.electron?.getDesktopCapturerSources) {
// We are using Electron
logger.debug("Getting screen stream using getUserMedia()...");
this.screenSharingStream = await navigator.mediaDevices.getUserMedia(screenshareConstraints);
} else {
// We are not using Electron
logger.debug("Getting screen stream using getDisplayMedia()...");
this.screenSharingStream = await navigator.mediaDevices.getDisplayMedia(screenshareConstraints);
}
logger.debug("Got screen stream, requesting audio stream...");
const audioConstraints = getUserMediaContraints(ConstraintsType.Audio);
this.placeCallWithConstraints(audioConstraints);
} catch (err) {
this.emit(CallEvent.Error,
new CallError(
CallErrorCode.NoUserMedia,
"Failed to get screen-sharing stream: ", err,
),
);
this.terminate(CallParty.Local, CallErrorCode.NoUserMedia, false);
}
this.type = CallType.Video;
}
@@ -460,7 +450,7 @@ export class MatrixCall extends EventEmitter {
* video will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
public async setLocalVideoElement(element : HTMLVideoElement) {
public async setLocalVideoElement(element: HTMLVideoElement) {
this.localVideoElement = element;
if (element && this.localAVStream && this.type === CallType.Video) {
@@ -481,7 +471,7 @@ export class MatrixCall extends EventEmitter {
* the first received video-capable stream will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
public setRemoteVideoElement(element : HTMLVideoElement) {
public setRemoteVideoElement(element: HTMLVideoElement) {
if (element === this.remoteVideoElement) return;
element.autoplay = true;
@@ -539,11 +529,24 @@ export class MatrixCall extends EventEmitter {
* @param {MatrixEvent} event The m.call.invite event
*/
async initWithInvite(event: MatrixEvent) {
this.msg = event.getContent();
const invite = event.getContent();
this.direction = CallDirection.Inbound;
// make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers();
if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
}
this.peerConn = this.createPeerConnection();
// we must set the party ID before await-ing on anything: the call event
// handler will start giving us more call events (eg. candidates) so if
// we haven't set the party ID, we'll ignore them.
this.chooseOpponent(event);
try {
await this.peerConn.setRemoteDescription(this.msg.offer);
await this.peerConn.setRemoteDescription(invite.offer);
await this.addBufferedIceCandidates();
} catch (e) {
logger.debug("Failed to set remote description", e);
this.terminate(CallParty.Local, CallErrorCode.SetRemoteDescription, false);
@@ -562,13 +565,6 @@ export class MatrixCall extends EventEmitter {
this.type = this.remoteStream.getTracks().some(t => t.kind === 'video') ? CallType.Video : CallType.Voice;
this.setState(CallState.Ringing);
this.opponentVersion = this.msg.version;
if (this.opponentVersion !== 0) {
// ignore party ID in v0 calls: party ID isn't a thing until v1
this.opponentPartyId = this.msg.party_id || null;
}
this.opponentCaps = this.msg.capabilities || {};
this.opponentMember = event.sender;
if (event.getLocalAge()) {
setTimeout(() => {
@@ -582,7 +578,7 @@ export class MatrixCall extends EventEmitter {
}
this.emit(CallEvent.Hangup);
}
}, this.msg.lifetime - event.getLocalAge());
}, invite.lifetime - event.getLocalAge());
}
}
@@ -594,7 +590,6 @@ export class MatrixCall extends EventEmitter {
// perverse as it may seem, sometimes we want to instantiate a call with a
// hangup message (because when getting the state of the room on load, events
// come in reverse order and we want to remember that a call has been hung up)
this.msg = event.getContent();
this.setState(CallState.Ended);
}
@@ -609,7 +604,11 @@ export class MatrixCall extends EventEmitter {
logger.debug(`Answering call ${this.callId} of type ${this.type}`);
if (!this.localAVStream && !this.waitForLocalAVStream) {
const constraints = getUserMediaVideoContraints(this.type);
const constraints = getUserMediaContraints(
this.type == CallType.Video ?
ConstraintsType.Video:
ConstraintsType.Audio,
);
logger.log("Getting user media with constraints", constraints);
this.setState(CallState.WaitLocalMedia);
this.waitForLocalAVStream = true;
@@ -666,6 +665,8 @@ export class MatrixCall extends EventEmitter {
logger.debug("Ending call " + this.callId);
this.terminate(CallParty.Local, reason, !suppressEvent);
// We don't want to send hangup here if we didn't even get to sending an invite
if (this.state === CallState.WaitLocalMedia) return;
const content = {};
// Continue to send no reason for user hangups temporarily, until
// clients understand the user_hangup reason (voip v1)
@@ -839,8 +840,11 @@ export class MatrixCall extends EventEmitter {
return;
}
if (this.callHasEnded()) {
this.stopAllMedia();
return;
}
this.localAVStream = stream;
logger.info("Got local AV stream with id " + this.localAVStream.id);
this.setState(CallState.CreateOffer);
@@ -866,25 +870,22 @@ export class MatrixCall extends EventEmitter {
}
}
this.localAVStream = stream;
logger.info("Got local AV stream with id " + this.localAVStream.id);
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = this.createPeerConnection();
for (const audioTrack of stream.getAudioTracks()) {
logger.info("Adding audio track with id " + audioTrack.id);
this.peerConn.addTrack(audioTrack, stream);
}
for (const videoTrack of (this.screenSharingStream || stream).getVideoTracks()) {
logger.info("Adding audio track with id " + videoTrack.id);
logger.info("Adding video track with id " + videoTrack.id);
this.peerConn.addTrack(videoTrack, stream);
}
// Now we wait for the negotiationneeded event
};
private sendAnswer() {
private async sendAnswer() {
const answerContent = {
answer: {
sdp: this.peerConn.localDescription.sdp,
@@ -906,12 +907,12 @@ export class MatrixCall extends EventEmitter {
logger.info(`Discarding ${this.candidateSendQueue.length} candidates that will be sent in answer`);
this.candidateSendQueue = [];
this.sendVoipEvent(EventType.CallAnswer, answerContent).then(() => {
try {
await this.sendVoipEvent(EventType.CallAnswer, answerContent);
// If this isn't the first time we've tried to send the answer,
// we may have candidates queued up, so send them now.
this.inviteOrAnswerSent = true;
this.sendCandidateQueue();
}).catch((error) => {
} catch (error) {
// We've failed to answer: back to the ringing state
this.setState(CallState.Ringing);
this.client.cancelPendingEvent(error.event);
@@ -924,7 +925,11 @@ export class MatrixCall extends EventEmitter {
}
this.emit(CallEvent.Error, new CallError(code, message, error));
throw error;
});
}
// error handler re-throws so this won't happen on error, but
// we don't want the same error handling on the candidate queue
this.sendCandidateQueue();
}
private gotUserMediaForAnswer = async (stream: MediaStream) => {
@@ -988,7 +993,7 @@ export class MatrixCall extends EventEmitter {
private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent) => {
if (event.candidate) {
logger.debug(
"Got local ICE " + event.candidate.sdpMid + " candidate: " +
"Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " +
event.candidate.candidate,
);
@@ -1022,43 +1027,39 @@ export class MatrixCall extends EventEmitter {
}
};
onRemoteIceCandidatesReceived(ev: MatrixEvent) {
async onRemoteIceCandidatesReceived(ev: MatrixEvent) {
if (this.callHasEnded()) {
//debuglog("Ignoring remote ICE candidate because call has ended");
return;
}
if (!this.partyIdMatches(ev.getContent())) {
logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`,
);
return;
}
const cands = ev.getContent().candidates;
if (!cands) {
logger.info("Ignoring candidates event with no candidates!");
return;
}
for (const cand of cands) {
if (
(cand.sdpMid === null || cand.sdpMid === undefined) &&
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
) {
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
return;
}
logger.debug("Got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
try {
this.peerConn.addIceCandidate(cand);
} catch (err) {
if (!this.ignoreOffer) {
logger.info("Failed to add remore ICE candidate", err);
}
}
const fromPartyId = ev.getContent().version === 0 ? null : ev.getContent().party_id || null;
if (this.opponentPartyId === undefined) {
// we haven't picked an opponent yet so save the candidates
logger.info(`Bufferring ${cands.length} candidates until we pick an opponent`);
const bufferedCands = this.remoteCandidateBuffer.get(fromPartyId) || [];
bufferedCands.push(...cands);
this.remoteCandidateBuffer.set(fromPartyId, bufferedCands);
return;
}
if (!this.partyIdMatches(ev.getContent())) {
logger.info(
`Ignoring candidates from party ID ${ev.getContent().party_id}: ` +
`we have chosen party ID ${this.opponentPartyId}`,
);
return;
}
await this.addIceCandidates(cands);
}
/**
@@ -1066,11 +1067,14 @@ export class MatrixCall extends EventEmitter {
* @param {Object} msg
*/
async onAnswerReceived(event: MatrixEvent) {
logger.debug(`Got answer for call ID ${this.callId} from party ID ${event.getContent().party_id}`);
if (this.callHasEnded()) {
logger.debug(`Ignoring answer because call ID ${this.callId} has ended`);
return;
}
if (this.opponentPartyId !== null) {
if (this.opponentPartyId !== undefined) {
logger.info(
`Ignoring answer from party ID ${event.getContent().party_id}: ` +
`we already have an answer/reject from ${this.opponentPartyId}`,
@@ -1078,12 +1082,8 @@ export class MatrixCall extends EventEmitter {
return;
}
this.opponentVersion = event.getContent().version;
if (this.opponentVersion !== 0) {
this.opponentPartyId = event.getContent().party_id || null;
}
this.opponentCaps = event.getContent().capabilities || {};
this.opponentMember = event.sender;
this.chooseOpponent(event);
await this.addBufferedIceCandidates();
this.setState(CallState.Connecting);
@@ -1199,7 +1199,7 @@ export class MatrixCall extends EventEmitter {
}
}
private callHasEnded() : boolean {
private callHasEnded(): boolean {
// This exists as workaround to typescript trying to be clever and erroring
// when putting if (this.state === CallState.Ended) return; twice in the same
// function, even though that function is async.
@@ -1258,19 +1258,9 @@ export class MatrixCall extends EventEmitter {
try {
await this.sendVoipEvent(eventType, content);
this.sendCandidateQueue();
if (this.state === CallState.CreateOffer) {
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = null;
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout, false);
}
}, CALL_TIMEOUT_MS);
}
} catch (error) {
this.client.cancelPendingEvent(error.event);
logger.error("Failed to send invite", error);
if (error.event) this.client.cancelPendingEvent(error.event);
let code = CallErrorCode.SignallingFailed;
let message = "Signalling failed";
@@ -1285,6 +1275,22 @@ export class MatrixCall extends EventEmitter {
this.emit(CallEvent.Error, new CallError(code, message, error));
this.terminate(CallParty.Local, code, false);
// no need to carry on & send the candidate queue, but we also
// don't want to rethrow the error
return;
}
this.sendCandidateQueue();
if (this.state === CallState.CreateOffer) {
this.inviteOrAnswerSent = true;
this.setState(CallState.InviteSent);
this.inviteTimeout = setTimeout(() => {
this.inviteTimeout = null;
if (this.state === CallState.InviteSent) {
this.hangup(CallErrorCode.InviteTimeout, false);
}
}, CALL_TIMEOUT_MS);
}
};
@@ -1526,7 +1532,10 @@ export class MatrixCall extends EventEmitter {
}
}
async transfer(targetUserId: string, targetRoomId?: string) {
/*
* Transfers this call to another user
*/
async transfer(targetUserId: string) {
// Fetch the target user's global profile info: their room avatar / displayname
// could be different in whatever room we shae with them.
const profileInfo = await this.client.getProfileInfo(targetUserId);
@@ -1543,9 +1552,49 @@ export class MatrixCall extends EventEmitter {
create_call: replacementId,
} as MCallReplacesEvent;
if (targetRoomId) body.target_room = targetRoomId;
await this.sendVoipEvent(EventType.CallReplaces, body);
return this.sendVoipEvent(EventType.CallReplaces, body);
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
}
/*
* Transfers this call to the target call, effectively 'joining' the
* two calls (so the remote parties on each call are connected together).
*/
async transferToCall(transferTargetCall?: MatrixCall) {
const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId);
const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId);
const newCallId = genCallID();
const bodyToTransferTarget = {
// the replacements on each side have their own ID, and it's distinct from the
// ID of the new call (but we can use the same function to generate it)
replacement_id: genCallID(),
target_user: {
id: this.getOpponentMember().userId,
display_name: transfereeProfileInfo.display_name,
avatar_url: transfereeProfileInfo.avatar_url,
},
await_call: newCallId,
} as MCallReplacesEvent;
await transferTargetCall.sendVoipEvent(EventType.CallReplaces, bodyToTransferTarget);
const bodyToTransferee = {
replacement_id: genCallID(),
target_user: {
id: transferTargetCall.getOpponentMember().userId,
display_name: targetProfileInfo.display_name,
avatar_url: targetProfileInfo.avatar_url,
},
create_call: newCallId,
} as MCallReplacesEvent;
await this.sendVoipEvent(EventType.CallReplaces, bodyToTransferee);
await this.terminate(CallParty.Local, CallErrorCode.Replaced, true);
await transferTargetCall.terminate(CallParty.Local, CallErrorCode.Replaced, true);
}
private async terminate(hangupParty: CallParty, hangupReason: CallErrorCode, shouldEmit: boolean) {
@@ -1621,7 +1670,7 @@ export class MatrixCall extends EventEmitter {
}
}
private sendCandidateQueue() {
private async sendCandidateQueue() {
if (this.candidateSendQueue.length === 0) {
return;
}
@@ -1633,20 +1682,28 @@ export class MatrixCall extends EventEmitter {
candidates: cands,
};
logger.debug("Attempting to send " + cands.length + " candidates");
this.sendVoipEvent(EventType.CallCandidates, content).then(() => {
this.candidateSendTries = 0;
this.sendCandidateQueue();
}, (error) => {
for (let i = 0; i < cands.length; i++) {
this.candidateSendQueue.push(cands[i]);
}
try {
await this.sendVoipEvent(EventType.CallCandidates, content);
} catch (error) {
// don't retry this event: we'll send another one later as we might
// have more candidates by then.
if (error.event) this.client.cancelPendingEvent(error.event);
// put all the candidates we failed to send back in the queue
this.candidateSendQueue.push(...cands);
if (this.candidateSendTries > 5) {
logger.debug(
"Failed to send candidates on attempt " + this.candidateSendTries +
". Giving up for now.", error,
". Giving up on this call.", error,
);
this.candidateSendTries = 0;
const code = CallErrorCode.SignallingFailed;
const message = "Signalling failed";
this.emit(CallEvent.Error, new CallError(code, message, error));
this.hangup(code, false);
return;
}
@@ -1656,7 +1713,7 @@ export class MatrixCall extends EventEmitter {
setTimeout(() => {
this.sendCandidateQueue();
}, delayMs);
});
}
}
private async placeCallWithConstraints(constraints: MediaStreamConstraints) {
@@ -1666,11 +1723,18 @@ export class MatrixCall extends EventEmitter {
this.setState(CallState.WaitLocalMedia);
this.direction = CallDirection.Outbound;
this.config = constraints;
// It would be really nice if we could start gathering candidates at this point
// so the ICE agent could be gathering while we open our media devices: we already
// know the type of the call and therefore what tracks we want to send.
// Perhaps we could do this by making fake tracks now and then using replaceTrack()
// once we have the actual tracks? (Can we make fake tracks?)
// make sure we have valid turn creds. Unless something's gone wrong, it should
// poll and keep the credentials valid so this should be instant.
const haveTurnCreds = await this.client._checkTurnServers();
if (!haveTurnCreds) {
logger.warn("Failed to get TURN credentials! Proceeding with call anyway...");
}
// create the peer connection now so it can be gathering candidates while we get user
// media (assuming a candidate pool size is configured)
this.peerConn = this.createPeerConnection();
try {
const mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
this.gotUserMediaForInvite(mediaStream);
@@ -1684,6 +1748,7 @@ export class MatrixCall extends EventEmitter {
const pc = new window.RTCPeerConnection({
iceTransportPolicy: this.forceTURN ? 'relay' : undefined,
iceServers: this.turnServers,
iceCandidatePoolSize: this.client._iceCandidatePoolSize,
});
// 'connectionstatechange' would be better, but firefox doesn't implement that.
@@ -1699,9 +1764,64 @@ export class MatrixCall extends EventEmitter {
private partyIdMatches(msg): boolean {
// They must either match or both be absent (in which case opponentPartyId will be null)
const msgPartyId = msg.party_id || null;
// Also we ignore party IDs on the invite/offer if the version is 0, so we must do the same
// here and use null if the version is 0 (woe betide any opponent sending messages in the
// same call with different versions)
const msgPartyId = msg.version === 0 ? null : msg.party_id || null;
return msgPartyId === this.opponentPartyId;
}
// Commits to an opponent for the call
// ev: An invite or answer event
private chooseOpponent(ev: MatrixEvent) {
// I choo-choo-choose you
const msg = ev.getContent();
logger.debug(`Choosing party ID ${msg.party_id} for call ID ${this.callId}`);
this.opponentVersion = msg.version;
if (this.opponentVersion === 0) {
// set to null to indicate that we've chosen an opponent, but because
// they're v0 they have no party ID (even if they sent one, we're ignoring it)
this.opponentPartyId = null;
} else {
// set to their party ID, or if they're naughty and didn't send one despite
// not being v0, set it to null to indicate we picked an opponent with no
// party ID
this.opponentPartyId = msg.party_id || null;
}
this.opponentCaps = msg.capabilities || {};
this.opponentMember = ev.sender;
}
private async addBufferedIceCandidates() {
const bufferedCands = this.remoteCandidateBuffer.get(this.opponentPartyId);
if (bufferedCands) {
logger.info(`Adding ${bufferedCands.length} buffered candidates for opponent ${this.opponentPartyId}`);
await this.addIceCandidates(bufferedCands);
}
this.remoteCandidateBuffer = null;
}
private async addIceCandidates(cands: RTCIceCandidate[]) {
for (const cand of cands) {
if (
(cand.sdpMid === null || cand.sdpMid === undefined) &&
(cand.sdpMLineIndex === null || cand.sdpMLineIndex === undefined)
) {
logger.debug("Ignoring remote ICE candidate with no sdpMid or sdpMLineIndex");
continue;
}
logger.debug("Call " + this.callId + " got remote ICE " + cand.sdpMid + " candidate: " + cand.candidate);
try {
await this.peerConn.addIceCandidate(cand);
} catch (err) {
if (!this.ignoreOffer) {
logger.info("Failed to add remote ICE candidate", err);
}
}
}
}
}
function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
@@ -1710,17 +1830,19 @@ function setTracksEnabled(tracks: Array<MediaStreamTrack>, enabled: boolean) {
}
}
function getUserMediaVideoContraints(callType: CallType) {
function getUserMediaContraints(type: ConstraintsType) {
const isWebkit = !!navigator.webkitGetUserMedia;
switch (callType) {
case CallType.Voice:
switch (type) {
case ConstraintsType.Audio: {
return {
audio: {
deviceId: audioInput ? {ideal: audioInput} : undefined,
}, video: false,
},
video: false,
};
case CallType.Video:
}
case ConstraintsType.Video: {
return {
audio: {
deviceId: audioInput ? {ideal: audioInput} : undefined,
@@ -1735,6 +1857,33 @@ function getUserMediaVideoContraints(callType: CallType) {
height: isWebkit ? { exact: 360 } : { ideal: 360 },
},
};
}
}
}
async function getScreenshareContraints(selectDesktopCapturerSource?: () => Promise<DesktopCapturerSource>) {
if (window.electron?.getDesktopCapturerSources && selectDesktopCapturerSource) {
// We have access to getDesktopCapturerSources()
logger.debug("Electron getDesktopCapturerSources() is available...");
const selectedSource = await selectDesktopCapturerSource();
if (!selectedSource) return null;
return {
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: selectedSource.id,
},
},
};
} else {
// We do not have access to the Electron desktop capturer,
// therefore we can assume we are on the web
logger.debug("Electron desktopCapturer is not available...");
return {
audio: false,
video: true,
};
}
}
+2
View File
@@ -138,6 +138,8 @@ export class CallEventHandler {
);
}
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,
});
+1
View File
@@ -32,6 +32,7 @@ export interface MCallReplacesEvent {
replacement_id: string;
target_user: MCallReplacesTarget;
create_call: string;
await_call: string;
target_room: string;
}
/* eslint-enable camelcase */
+157 -730
View File
File diff suppressed because it is too large Load Diff