Compare commits

...

583 Commits

Author SHA1 Message Date
Damir Jelić b95ebe444e matrix-sdk: Bump our versions
CI / Check style (push) Failing after 32s
CI / Run clippy (push) Has been skipped
CI / linux / WASM (push) Has been skipped
CI / linux / appservice / stable / warp (push) Has been skipped
CI / macOS / appservice / stable / warp (push) Has been skipped
CI / linux / appservice / stable / actix (push) Has been skipped
CI / macOS / appservice / stable / actix (push) Has been skipped
CI / linux / features-markdown (push) Has been skipped
CI / linux / features-socks (push) Has been skipped
CI / linux / features-sso_login (push) Has been skipped
CI / linux / features-no-sled (push) Has been skipped
CI / linux / features-sled_cryptostore (push) Has been skipped
CI / linux / features-no-encryption-and-sled (push) Has been skipped
CI / linux / features-require_auth_for_profile_requests (push) Has been skipped
CI / linux / features-no-encryption (push) Has been skipped
CI / linux / features-rustls-tls (push) Has been skipped
CI / linux / beta (push) Has been skipped
CI / linux / stable (push) Has been skipped
CI / macOS / stable (push) Has been skipped
2021-06-22 14:57:46 +02:00
Damir Jelić e7c7b63b6e qrcode: Add a readme 2021-06-22 14:20:25 +02:00
Damir Jelić cba22ae3b2 Merge branch 'upgrade-deps' 2021-06-21 20:31:57 +02:00
Damir Jelić 57b2f6ad22 matrix-sdk: Switch to a release of ruma 2021-06-21 19:53:26 +02:00
Damir Jelić 8f1d8eeca2 Merge branch '244-room-history' 2021-06-21 17:33:40 +02:00
Damir Jelić 513fbd8900 crypto: Manually implement Debug for attachment encryptors/decryptors 2021-06-21 17:29:46 +02:00
Damir Jelić 17097f4d42 matrix-sdk: Upgrade our deps 2021-06-21 16:45:52 +02:00
SaurusXI 58369fe7d0 matrix-sdk: (fix) use macro for matching in are_events_visible 2021-06-21 20:14:40 +05:30
Jonas Platte 43e213fd67 matrix-sdk: Update ruma
Co-authored-by: Damir Jelić <poljar@termina.org.uk>
2021-06-21 15:45:33 +02:00
SaurusXI ae5be67322 matrix-sdk: (fix) return Ok(()) in ensure_members when returning early 2021-06-21 18:28:11 +05:30
SaurusXI b984fcca0c matrix-sdk: (fix) correct the history visibility states that allow us to view events in are_events_visible 2021-06-21 18:08:38 +05:30
Felix Häcker bdd35206e8 event_handler: Add AnySyncMessageEvent::Reaction 2021-06-20 17:04:31 +02:00
SaurusXI 0e84349d05 matrix-sdk: add event visibility check to ensure_members 2021-06-20 13:03:50 +05:30
SaurusXI 092ca90403 matrix-sdk: add method to check room's event visibility 2021-06-20 13:01:24 +05:30
Damir Jelić 0fb3dedd1c client: Fix compilation when the encryption feature is disabled 2021-06-17 12:35:37 +02:00
Damir Jelić 3cf843d24f matrix-sdk: Rework the public API for answering verifications 2021-06-17 12:17:11 +02:00
Damir Jelić baee5b2d11 crytpo: Couple more accessors for the verification request 2021-06-17 11:04:18 +02:00
Damir Jelić 34703bc0d6 crypto: Add a method to get all verification requests of a certain user 2021-06-17 11:04:18 +02:00
Damir Jelić d212e7df18 crypto: Add some more accessors to the verification requests 2021-06-17 11:04:18 +02:00
Damir Jelić f8b09d4537 crypto: Remember who started the verification request 2021-06-17 11:04:18 +02:00
Damir Jelić 5d38bc3802 crypto: Scope the verification requests behind the other user id 2021-06-17 11:04:18 +02:00
Damir Jelić 58d3b42a60 crypto: Don't allow QR code generation if we or the other can't handle it 2021-06-17 11:04:18 +02:00
Damir Jelić b7986a5153 crypto: Add a couple more accessors for the verification request 2021-06-17 11:04:18 +02:00
Damir Jelić c547f384bc crypto: Fix the method to transition from a request into a SAS verification 2021-06-17 11:04:18 +02:00
Damir Jelić 29bba0b2ca crypto: Allow accepting key request while specifying our supported methods 2021-06-17 11:04:18 +02:00
Damir Jelić 80fac4bfa4 cyrpto: Go into passive mode if someone else replies to a request 2021-06-17 11:04:18 +02:00
Damir Jelić be53913a16 crypto: Remove the redundant flow id copy 2021-06-17 11:04:18 +02:00
Damir Jelić df1fe0ebc4 crypto: Don't return a result when receiving a ready event
Ready events might be invalid but we might receive a valid one later on,
e.g. someone is trying to disrupt our verification, so just ignore
invalid ready events.
2021-06-17 11:04:18 +02:00
Damir Jelić 073b91fa62 crypto: Ignore verification requests that are sent by us 2021-06-17 11:04:18 +02:00
Damir Jelić cc0388929a crypto: Add some more accessors for the fields in the verification types 2021-06-17 11:04:17 +02:00
Damir Jelić b14d754aed crypto: Turn the content_to_request function into a constructor
Closes: #260
2021-06-17 11:04:17 +02:00
Damir Jelić 00c3921d2a crypto: Add initial support for QR code verification 2021-06-17 11:04:17 +02:00
Damir Jelić 71aba433da crypto: Add some more accessors to the sas structs 2021-06-17 11:04:17 +02:00
Damir Jelić 1c8081533d qrcode: Rename the main qrcode type 2021-06-17 11:04:17 +02:00
Damir Jelić 7f364fd615 crypto: Allow only a identity to be verified when the verification finishes
QR code based verification doesn't verify a device when users are
verifying each other, generalize the logic that marks stuff as verified
so we can verify either only a device or an user identity or both.
2021-06-17 11:04:17 +02:00
Damir Jelić ada71586ac crypto: Scope the verifications per sender 2021-06-17 11:04:17 +02:00
Damir Jelić 533a5b92b0 crypto: Ignore key verification requests that have an invalid timestamp 2021-06-17 11:04:17 +02:00
Damir Jelić c4b1d3bc44 Merge branch 'avatar_cache' 2021-06-17 10:42:57 +02:00
Julian Sparber 4cdb03e64b matrix-sdk: use media cache for avatar requests 2021-06-15 13:07:34 +02:00
Johannes Becker da4876acee appservice: Rename Appservice to AppService 2021-06-15 12:09:25 +02:00
Damir Jelić dbf8cf231d Merge branch 'matrix-sdk/feat/whoami' 2021-06-11 10:08:12 +02:00
Johannes Becker ba0cc3d45f matrix-sdk: Add Client::whoami() 2021-06-11 09:37:30 +02:00
Johannes Becker 1a5cd544e7 appservice: Introduce appservice mode on Client 2021-06-10 11:36:20 +02:00
Johannes Becker 97c7baab14 appservice: Rename example to get rid of cargo warning 2021-06-10 11:22:01 +02:00
Johannes Becker df42ef68a2 appservice: Enable warp by default 2021-06-09 22:14:41 +02:00
Damir Jelić 4a83e36195 Merge branch 'appservice/feature/warp' 2021-06-09 17:31:07 +02:00
Johannes Becker da673f1308 appservice: Temporarily remove windows from CI
because it's broken
2021-06-08 11:29:14 +02:00
Johannes Becker c634efbe09 appservice: Fixes after rebase 2021-06-08 11:18:56 +02:00
Damir Jelić 5fa2b05622 matrix-sdk: Fix some more typos 2021-06-08 11:13:23 +02:00
Johannes Becker 66551d28e4 appservice: Switch autojoin example to warp 2021-06-08 11:09:05 +02:00
Johannes Becker 7116fc1103 appservice: Switch warp to git dep so it works OOTB 2021-06-08 11:09:05 +02:00
Johannes Becker d8b23f789d appservice: Expand set_event_handler docs 2021-06-08 11:09:05 +02:00
Johannes Becker 4dacef2e2c appservice: Improve warp_filter 2021-06-08 11:09:05 +02:00
Johannes Becker d6ca3a27bb appservice: Properly scope webserver configuration 2021-06-08 11:09:05 +02:00
Johannes Becker 8d061447d6 appservice: Improve test coverage 2021-06-08 11:09:03 +02:00
Johannes Becker 38512d6a54 appservice: Add warp support 2021-06-08 11:01:20 +02:00
Johannes Becker f3bbcf553c appservice: Restructure tests 2021-06-08 10:58:07 +02:00
Jonas Platte e1d905fbc6 Temporarily remove Windows from CI
It's currently broken.
2021-06-07 19:50:14 +02:00
Jonas Platte 1168c39c20 Move ruma re-export from matrix-sdk-common to matrix-sdk 2021-06-07 19:50:14 +02:00
Jonas Platte 54063513a3 appservice: Depend on ruma directly 2021-06-07 19:50:14 +02:00
Jonas Platte 26788f83f0 sdk: Depend on ruma directly 2021-06-07 19:50:13 +02:00
Jonas Platte c705af1048 test: Depend on ruma directly 2021-06-07 18:55:56 +02:00
Jonas Platte c964589049 base: Depend on ruma directly 2021-06-07 18:55:56 +02:00
Jonas Platte 74d0ac7c77 crypto: Depend on ruma directly
… instead of using matrix_sdk_common's re-exports
2021-06-07 18:55:56 +02:00
Jonas Platte 3bac536daf Fix clippy lints
Automated via `cargo clippy --workspace --all-targets -Zunstable-options --fix`.
2021-06-07 15:51:18 +02:00
Jonas Platte e18f248dbb crypto: Add missing required-features to crypto_bench 2021-06-07 15:16:53 +02:00
Damir Jelić b6c7b317bf Merge branch 'qr-crate' 2021-06-07 14:03:29 +02:00
Damir Jelić 6f11244017 Merge branch 'typos' 2021-06-06 21:01:30 +02:00
Jonas Platte 6b685b671d Replace Arc<Box<dyn (Crypto|State)Store>> by Arc<dyn (Crypto|State)Store> 2021-06-06 18:16:25 +02:00
Jonas Platte eed2b37885 Replace Arc<Box<DeviceId>> by Arc<DeviceId> 2021-06-06 18:15:18 +02:00
Jonathan de Jong f76cb1d123 the the 2021-06-05 14:55:01 +02:00
Jonathan de Jong f36fb55727 some more typos 2021-06-05 14:50:08 +02:00
Jonathan de Jong 74a6d39b9f various typos 2021-06-05 14:35:20 +02:00
Damir Jelić 0df782e93e crypto: Fix some clippy warnings 2021-06-04 19:26:32 +02:00
Damir Jelić 7cca358399 Merge branch 'master' into verification-improvements 2021-06-04 18:37:42 +02:00
Damir Jelić 96d4566111 crypto: Move the verification cache into a separate module 2021-06-04 18:13:52 +02:00
Damir Jelić 31e00eb434 crypto: Don't panic if we get a unknown cancel code 2021-06-04 18:13:52 +02:00
Damir Jelić 612fa46359 crypto: Replace a bunch of From implementations with macros 2021-06-04 18:13:52 +02:00
Damir Jelić 0a7fb2cbc3 crytpo: Mark verification requests as cancelled and as done 2021-06-04 18:13:52 +02:00
Damir Jelić f9fb530480 crypto: Forward cancel events to the sas object 2021-06-04 18:13:52 +02:00
Damir Jelić 2ec8893273 crypto: Silence a clippy warning until we add QR code verifications 2021-06-04 15:39:56 +02:00
Damir Jelić bd5dda370d crypto: Remove the sas event enums module 2021-06-04 15:16:38 +02:00
Damir Jelić ac04b0c36e crypto: Create a enum for the verification types 2021-06-04 15:16:38 +02:00
Damir Jelić a04afac963 crypto: Fix a clippy warning 2021-06-04 15:16:38 +02:00
Damir Jelić cf98681f19 crypto: Remove some duplicate code 2021-06-04 15:16:38 +02:00
Damir Jelić cbcf673d21 crypto: Make sure we send verification done events 2021-06-04 15:16:38 +02:00
Damir Jelić 9b20b00908 crypto: Log if we get a missmatch of the flow id 2021-06-04 15:16:38 +02:00
Damir Jelić f50d0cd3a6 crypto: Test starting a to-device verification request 2021-06-04 15:16:38 +02:00
Damir Jelić 1e48b15040 crypto: Add enums so we can handle in-room and to-device verifications the same 2021-06-04 15:16:38 +02:00
Damir Jelić b52f3fb11f crypto: Remove an allocation when calculating the SAS MAC 2021-06-04 15:16:38 +02:00
Damir Jelić d877c1cf8c crypto: Move the Done state into the common verification module 2021-06-04 15:16:38 +02:00
Damir Jelić 327445c6a0 crypto: Move the logic for marking identities as verified out of the Sas struct 2021-06-04 15:16:38 +02:00
Damir Jelić 8a5a0e511e crypto: Don't await while holding a sync lock 2021-06-04 15:16:38 +02:00
Damir Jelić 12619ab8b3 crypto: Log a warning if we get a start event without being ready 2021-06-04 15:16:38 +02:00
Damir Jelić 069ef3a661 crypto: Move the SAS starting logic into the verification request struct 2021-06-04 15:16:38 +02:00
Damir Jelić 999f0899f8 crypto: Move the outgoing requests to the VerificationCache 2021-06-04 15:16:38 +02:00
Damir Jelić 681f32b0a7 crypto: Fix a couple of typos 2021-06-04 15:16:38 +02:00
Damir Jelić 0e514b755f crypto: Move the CancelContent generation out of the sas module 2021-06-04 15:16:38 +02:00
Johannes Becker c0b30cadc9 appservice: Improve API 2021-06-01 10:48:01 +02:00
Damir Jelić ee40d917d1 Merge branch 'feat/appservice-client-config' 2021-05-31 13:28:31 +02:00
Johannes Becker 3f2fecd309 appservice: Add new_with_client_config 2021-05-31 12:50:53 +02:00
Damir Jelić d58a190712 Merge branch 'media-store' 2021-05-31 10:36:20 +02:00
Damir Jelić 3c72304e36 Merge branch 'patch-1' 2021-05-31 09:39:07 +02:00
Damir Jelić 10b38ce44e matrix-sdk: Fix a bunch of typos 2021-05-31 09:35:19 +02:00
Jonas Platte 3c9f929598 Fix typo: underlaying => underlying 2021-05-30 15:01:27 +02:00
L3af d7e167498d docs: fix on_room_join_rules 2021-05-29 04:31:25 +00:00
Kévin Commaille a959116af2 sdk: Fix clippy warnings 2021-05-28 09:11:48 +02:00
Damir Jelić 63dc939081 matrix-qrcode: Modify the QR code generation so mobile clients can decode 2021-05-27 11:24:44 +02:00
Johannes Becker 2becb88c35 appservice: Add client_with_config singleton 2021-05-26 18:12:50 +02:00
Kévin Commaille 6367cdddbf sdk: Add tests for media content 2021-05-25 22:15:27 +02:00
Kévin Commaille 5e05b37d02 base: Add tests for media content storage 2021-05-25 22:10:04 +02:00
Kévin Commaille df883d3328 Add MediaEventContent trait and implement it for corresponding room events
Add helper methods in Client
2021-05-25 21:52:27 +02:00
Kévin Commaille b805670c8a sdk: Add methods for media content 2021-05-25 21:43:01 +02:00
Kévin Commaille 0c8e870bff crypto: Implement From<EncryptedFile> for EncryptionInfo 2021-05-25 21:33:38 +02:00
Kévin Commaille c318a6e847 base: Add media store 2021-05-25 21:16:28 +02:00
Damir Jelić bdd51a323a Merge branch 'read-receipts' 2021-05-25 19:25:48 +02:00
Kévin Commaille f619bbb884 base: Change receipt store tests' user ID 2021-05-25 14:20:13 +02:00
Damir Jelić 37c23f1761 matrix-qrcode: Add accessors for our keys/secrets. 2021-05-25 13:31:12 +02:00
Kévin Commaille 49c72e74f7 base: Add store tests for receipts 2021-05-25 11:57:03 +02:00
Johannes Becker 7609c7445c matrix-sdk: Allow to get Client's RequestConfig 2021-05-25 10:38:43 +02:00
Kévin Commaille a889bb3aca base: Simplify decode_key_value 2021-05-25 10:26:56 +02:00
Johannes Becker 20454e1666 appservice: Put registration into Arc 2021-05-25 10:05:51 +02:00
Johannes Becker aaa17535ac matrix_sdk: Fix typo 2021-05-25 10:05:51 +02:00
Johannes Becker bd5e112a46 appservice: Remove outdated serde_yaml dependency 2021-05-25 10:05:51 +02:00
Johannes Becker cc591cce1c appservice: Improve docs 2021-05-25 10:05:51 +02:00
Damir Jelić e058191b99 base: Correctly update the room info for invited rooms 2021-05-25 09:31:32 +02:00
Damir Jelić 300189bb37 crypto: Use the verification cache in verification requests 2021-05-24 16:41:27 +02:00
Damir Jelić d928f39f68 crypto: Add a VerificationCache struct 2021-05-24 16:41:27 +02:00
Damir Jelić 98c259dc1e crypto: Refactor the VerificationReqest struct a bit 2021-05-24 16:41:27 +02:00
Damir Jelić c174c4fda2 matrix-qrcode: Rename the crate. 2021-05-24 16:38:21 +02:00
Damir Jelić 7a5daf6ac7 Merge branch 'famous' 2021-05-24 12:47:14 +02:00
timorl ded5830deb Make client use .well-known redirects
Was supposed to fix #219, but apparently that was about something else.
2021-05-24 11:00:42 +02:00
Austin Ray 59c8652ce8 base: fix empty room name calculation
Step 3.iii of the naming algorithm[0] covers name calculation for empty
rooms; however, it isn't an else condition for steps 3.i and 3.ii. It's a
final conditional in the algorithm before returning the name. The
current implementation treats it as an else condition.

To fix this, use step 3.i and 3.ii to calculate a room name then check
if the user is alone. If alone, return "Empty room (was...)" containing
any former member names. If alone and there aren't any former members,
return "Empty room"

Fixes #133

[0] https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room
2021-05-21 19:18:11 -04:00
Austin Ray 5670700f7f base: fix room name's "and {} others" count
The current implementation uses number of invited and joined members for
"and {} others" message. This assigns rooms with 5 members the name "a,
b, c, and 5 others" suggesting 8 room members. The correct message is
"a, b, c, and 2 others". To get this, subtract number of heroes from
invited and joined member count.

Step 3.ii of the naming algorithm[0] confirms using a remaining users
count in the name.

[0] https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room
2021-05-21 18:48:19 -04:00
Austin Ray 79025e3f40 base: split calculate_room_name()
Split `calculate_room_name()` into a high-level function using Matrix
data types and low-level function containing the core logic using plain
Rust data types. Since the low-level function uses simple data types,
unit testing becomes easier.
2021-05-21 17:25:59 -04:00
Austin Ray c90e8ab483 base: use correct bound in naming algorithm
Step 3.ii of the name calculation algorithm[0] specifies the conditional
as `heroes < invited + joined - 1 AND invited + joined > 1`. However,
current implementation uses `invited + joined - 1 > 1` in the
conditional, which can trigger the conditional if the user is alone.

Correct this by using two variables representing `invited + joined` and
`invited_joined - 1` and updating the conditional. `invited + joined` is
kept as a variable since step 3.iii uses it for its conditional.

[0] https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room
2021-05-21 14:51:59 -04:00
Kévin Commaille 64b5298881 base: Add support for read receipts 2021-05-20 17:14:57 +02:00
Damir Jelić 8018b43443 qrcode: Document the qrcode crate. 2021-05-20 15:02:14 +02:00
Damir Jelić f49f5f1636 qrcode: Add some more tests 2021-05-20 14:04:40 +02:00
Damir Jelić b073323089 qrcode: Add another TryFrom implementation 2021-05-20 14:04:07 +02:00
Damir Jelić 305766955b matrix-sdk: Add a crate to generate and parse QR codes
This patch adds types and methods to parse QR codes defined in the
[spec]. It supports parsing the QR format from an image or from a byte
string, converting back to an image and bytestring is possible as well.

[spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format
2021-05-19 16:10:48 +02:00
Damir Jelić 110b8eb8dd Merge branch 'master' into sas-longer-flow 2021-05-18 09:07:50 +02:00
Damir Jelić fe17dce813 Merge branch 'ProjectMoon/master' 2021-05-18 08:53:31 +02:00
Damir Jelić c122549e0d base: Correctly get the user ids of all room members 2021-05-18 08:29:10 +02:00
projectmoon bb69901d94 Return joined members in a room from the correct Sled tree. 2021-05-17 22:28:30 +00:00
Jonas Platte cd77441d1b Upgrade ruma to 0.1.0 (crates.io release) 2021-05-17 02:57:36 +02:00
Jonas Platte 5059d8b2c6 Remove unused import 2021-05-17 02:21:18 +02:00
Jonas Platte ffea84b64a Use Instant instead of SystemTime to measure elapsed time 2021-05-15 17:23:31 +02:00
Jonas Platte 15540e84e3 Upgrade ruma 2021-05-15 17:22:32 +02:00
Johannes Becker 0bdcc0fbf9 appservice: Refactor API 2021-05-13 17:42:06 +02:00
Damir Jelić 3f57a2a9f2 Merge branch 'master' into sas-longer-flow 2021-05-13 11:26:40 +02:00
Damir Jelić 09a7858702 crypto: Initial support for the longer to-device verification flow 2021-05-13 11:15:56 +02:00
Damir Jelić ec55258be9 crypto: Handle decrypted to-device events as well
Usually only room keys and forwarded room keys are sent as encrypted
to-device events, those are specially handled to avoid accepting room
keys coming in unencrypted.

Some clients might send out other events encrypted which might lower
metadata leakage and the spec doesn't disallow it.

This patch handles decrypted events the same way as non-encrypted ones,
we're still special casing the decryption handling to avoid decryption
loops/bombs (i.e. events that are encrypted multiple times).
2021-05-13 11:08:13 +02:00
Devin Ragotzy 6b600d7e6d Replace async_trait rustfmt removed 2021-05-12 20:34:14 -04:00
Devin Ragotzy 5f09d091cb Add cargo fmt to ci using nightly 2021-05-12 15:38:59 -04:00
Devin Ragotzy 2ef0c2959c Add use_small_heuristics option and run fmt 2021-05-12 15:37:29 -04:00
Devin Ragotzy c85f4d4f0c Add rustfmt config file and run over workspace 2021-05-12 15:36:52 -04:00
Damir Jelić 4f7902d6f0 crypto: Add a method to check it the SAS flow supports emoji 2021-05-12 20:09:02 +02:00
Damir Jelić 9863bc4a1c matrix-sdk: Fix a clippy warning 2021-05-12 19:45:23 +02:00
Damir Jelić 77c2a4ed4f matrix-sdk: Bump ruma 2021-05-12 19:19:42 +02:00
Damir Jelić 4c09c6272b Merge branch 'feat/appservice' 2021-05-11 09:50:26 +02:00
Damir Jelić da57061db0 Merge branch 'tags' 2021-05-10 13:27:30 +02:00
Johannes Becker 753302394f appservice: Remove outdated error 2021-05-10 12:08:04 +02:00
Johannes Becker a2125adeee ci: Dedicated matrix-sdk-appservice pipeline 2021-05-10 09:56:33 +02:00
Johannes Becker 14bc4eb7e0 appservice: Rename verify_hs_token to hs_token_matches 2021-05-10 09:11:15 +02:00
Johannes Becker 325531d13f appservice: Compile time webserver feature check 2021-05-10 09:04:51 +02:00
Johannes Becker 87099676f9 appservice: Improve docs 2021-05-10 08:43:06 +02:00
Johannes Becker 3b24d33822 appservice: Rely on cfg-toggle in send_request 2021-05-10 07:56:00 +02:00
Johannes Becker eece920953 appservice: Initial version 2021-05-10 07:51:52 +02:00
Jonas Platte 44eff7deb7 Add a general-purpose API error variant to HttpError 2021-05-08 15:01:02 +02:00
Jonas Platte 68b74c5ea9 Rename HttpError::{FromHttpResponse => ClientApi} 2021-05-08 14:49:42 +02:00
Jonas Platte efe5b1e538 Bump ruma 2021-05-08 14:49:15 +02:00
Kévin Commaille a2ab6a9f23 base: Get the tags for a room 2021-05-07 19:11:27 +02:00
Damir Jelić 1bda3659ce sas: Allow to just get the emoji index instead of the emoji and descryption 2021-05-07 17:04:27 +02:00
Damir Jelić 80d01b23c4 sas: Return an array of seven emojis instead of a vector 2021-05-07 17:01:53 +02:00
Damir Jelić dea3d4cb68 sas: Implement a missing todo, allow accepting in-room verifications.
Technically that's not needed since we auto-accept here after we
accepted the request but we still need to remove the TODO there.
2021-05-07 16:57:52 +02:00
Kévin Commaille b8017b1fb0 bump ruma to 24154195a00390a33542603b968e94022487587c 2021-05-07 13:22:32 +02:00
Damir Jelić 8dbbacfbe6 client: Add a method to get the ed25519 key of our own device 2021-05-06 21:44:50 +02:00
Damir Jelić 43b7072609 matrix-sdk: Fix some newly introduced clippy warnings 2021-05-06 20:42:27 +02:00
Damir Jelić cad888e69b client: Remove a now unneeded workaround for UIA 2021-05-06 19:18:22 +02:00
Damir Jelić 5df9ae350c client: Require a proper Url to create a client 2021-05-06 09:58:21 +02:00
Damir Jelić d90e112c06 Merge branch 'encryption-info-v2' 2021-04-29 16:54:14 +02:00
poljar 6048a1a507 crypto: Fix a typo
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
2021-04-29 15:34:53 +02:00
poljar 233c4355d8 crypto: Use encryption info in the docstring for the type of the same name
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
2021-04-29 15:34:39 +02:00
poljar e71cabc8f0 crypto: Fix a typo.
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
2021-04-29 15:34:04 +02:00
poljar 22b333a0d9 Use as_str() to get the string event type.
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
2021-04-29 15:33:45 +02:00
Damir Jelić c720abfa87 base: Fix the wasm example 2021-04-29 12:46:21 +02:00
Damir Jelić 5d73dd7da7 room: Add methods to get members that don't do any requests
Our main methods to get members nowadays ensure that the member list is
synchronized with the server. This is nice and convenient but might not
be desirable for a couple of reasons.

Firstly it might be costly to fetch all members at once depending on
what the client is doing and the number of rooms and secondly some
clients might have a hybrid setup where not everything is running on a
tokio thread, sending out requests is only possible on a tokio thread.
2021-04-29 12:38:07 +02:00
Damir Jelić 5cf0fd2b85 room: Override the method to get a specific room member 2021-04-29 11:08:09 +02:00
Damir Jelić b3cf2c5899 base: Fix a clippy warning if the encryption feature is turned off 2021-04-29 10:46:47 +02:00
Damir Jelić 4fc21a8860 base: Store the raw versions of events in the state store
This patch changes the way we store and load the majority of events that
are in the state store. This is done so custom fields in the event
aren't lost since a deserialize/serialize cycle removes all the unknown
fields from the event.
2021-04-29 10:35:54 +02:00
Damir Jelić cff90b1480 matrix-sdk: Add encryption info to our sync events. 2021-04-29 10:35:54 +02:00
Damir Jelić 5ed0c7a7b3 Merge branch 'notifications' 2021-04-28 10:30:06 +02:00
Kévin Commaille 0e2017e537 matrix-sdk: Fix clippy warning 2021-04-27 14:37:43 +02:00
Kévin Commaille 1cc4f953b3 matrix-sdk: Small fixes 2021-04-27 13:47:07 +02:00
Kévin Commaille f8bc9f3dc9 matrix-sdk: handle overflow in active_members_count 2021-04-27 13:47:06 +02:00
Kévin Commaille 24e96df7ea matrix-sdk: Propagate store error in get_push_rules 2021-04-27 13:47:06 +02:00
Kévin Commaille c569436ba4 matrix-sdk: Add StateChanges::add_notification 2021-04-27 13:47:06 +02:00
Kévin Commaille f6c4fdde7d matrix-sdk: Implement EncodeKey for EventType 2021-04-27 13:47:06 +02:00
Kévin Commaille 3f2c5d22b6 matrix-sdk: Get notifications locally on sync 2021-04-27 13:46:56 +02:00
Jonas Platte bd02ff901f Avoid needless copies by changing http::Request<Vec<u8>> to http::Request<Bytes> 2021-04-26 17:31:27 +02:00
Johannes Becker 242d46c9a1 matrix-sdk: require_auth_for_profile_requests feature and force_auth request config
forces authentication for `get_avatar` which was previously done with
the unstable-synapse-quirks feature in ruma
2021-04-26 17:31:25 +02:00
Johannes Becker 5c882f89e8 chore: bump ruma 2021-04-26 08:05:58 +02:00
Damir Jelić ab180362c9 Merge branch 'json-sync-builder' 2021-04-23 15:38:00 +02:00
Johannes Becker 28ddb9b70b ci: clippy check without default features 2021-04-23 10:42:12 +02:00
Johannes Becker 910a45b3d5 chore: cleanup clippy warnings 2021-04-23 10:38:49 +02:00
Damir Jelić a1c0acbd0c test: Add a method to build a sync response as a JsonValue 2021-04-21 16:12:05 +02:00
Damir Jelić a7c2a645aa rooms: Override the joined_members() method so we return the correct RoomMember 2021-04-21 15:47:44 +02:00
Damir Jelić 2a5ede9e1a client: Better docs for the get_or_upload_filter() method 2021-04-21 15:08:36 +02:00
Damir Jelić 324a0aafca Merge branch 'key-share-improvements' 2021-04-21 13:47:02 +02:00
Damir Jelić bfc7434f7e crypto: Move the outbound session filter logic into the group session cache 2021-04-20 13:35:47 +02:00
poljar e15f7264dc crypto: Don't borrow inside a format unnecessarily
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
2021-04-20 12:27:56 +02:00
Damir Jelić 4a7be13961 crypto: Only send out automatic key requests if we have a verified device
Sending out automatic key requests is a bit spammy for new logins,
they'll likely have many undecryptable events upon an initial sync.

It's unlikely that anyone will respond to such a key request since keys are
shared only with verified devices between devices of the same user or if
the key owner knows that the device should have received the key.

Upon initial sync it's unlikely that we have been verified and the key
owner likely did not intend to send us the key since we just created the
new device.
2021-04-20 11:47:11 +02:00
Damir Jelić 65d84c111b Merge branch 'exhaustive-sync-events-conv' 2021-04-19 15:05:52 +02:00
Damir Jelić 78b7dcac61 crypto: Add a public method to request and re-request keys. 2021-04-19 15:00:21 +02:00
Jonas Platte 796354ce5d Ensure exhaustiveness for sync_events::Response destructuring
So the SDKs own SyncResponse type doesn't get out-of-sync.
2021-04-19 13:42:16 +02:00
Johannes Becker 95421f1713 matrix-sdk!: send_request returns Bytes
Prevents unnecessary copy
2021-04-19 12:26:10 +02:00
Jonas Platte 1578067498 Only activate the client parts of ruma-client-api
… to reduce compile times.
2021-04-19 12:23:09 +02:00
Jonas Platte 401cf282a7 Upgrade ruma dependency 2021-04-19 12:16:13 +02:00
Johannes Becker 3414a59b91 chore: bump ruma 2021-04-16 12:45:21 +02:00
Damir Jelić 8c007510cd crypto: Only load the outgoing key requests when we want to send them out 2021-04-15 19:40:24 +02:00
Damir Jelić f9d290746c crypto: Load unsent outgoing key requests when we open a store 2021-04-15 17:48:37 +02:00
Damir Jelić d4c56cc5b3 crypto: Refactor the outobund group session storing
This introduces a group session cache struct that can be shared between
components that need to access the currently active group session.
2021-04-15 15:19:21 +02:00
Damir Jelić 9e817a623b crypto: Fix an invalid assert in the crypto bench 2021-04-15 15:01:56 +02:00
Damir Jelić 02331fa325 crypto: Add specialized methods to store outgoing key requests 2021-04-15 13:28:50 +02:00
Damir Jelić 5637ca3080 crypto: Simplify the should_share_session method 2021-04-15 13:28:50 +02:00
Damir Jelić 975f9a0b41 crypto: Improve the way we decide if we honor room key requests
This improves two things, use the correct outbound session to check if
the session should be shared.

Check first if the session has been shared if there isn't a session or
it hasn't been shared check if the request is comming from our own user.
2021-04-14 14:30:53 +02:00
Damir Jelić 4713af6aac crypto: Fix a typo 2021-04-14 11:14:59 +02:00
Damir Jelić ba81c2460c crypto: Ignore key requests from ourselves 2021-04-13 17:17:09 +02:00
Damir Jelić 5132971558 crypto: Add a progress listener for key imports 2021-04-13 12:47:22 +02:00
Johannes Becker 53b1845cbe ci: test features 2021-04-12 21:05:07 +02:00
Damir Jelić 893a5109ce crypto: Remove some unneeded parenthesis 2021-04-12 19:11:03 +02:00
Damir Jelić a97b01f3ce Merge branch 'matrix-sdk/fix-no-encryption-build' 2021-04-12 19:09:32 +02:00
Johannes Becker be72c53d3e matrix-sdk: fix building without encryption feature 2021-04-12 17:45:58 +02:00
Damir Jelić b4b897dd51 crypto: Await the group session invalidation 2021-04-12 15:19:30 +02:00
Damir Jelić cb58c499b3 crypto: Store that our outbound session was invalidated 2021-04-12 13:47:38 +02:00
Kévin Commaille ebcb2024d1 Fix docs wording 2021-04-11 16:39:49 +02:00
Kévin Commaille dadd2fa68c Bump ruma to e2728a70812412aade9322f6ad832731978a4240 2021-04-11 12:04:53 +02:00
Julian Sparber b5de203499 matrix-sdk: Add RequestConfig that replaces timeout for requests
This exposes the retry behavior to the developer. This way the user can
set if a request should be retried or failed immidiatly.

This also make sure that the timeout set by the user is used for all
requests. Of-course it can't be used for uploaded and syncs with
timeout, but this doesn't change the behavior for those requests.
2021-04-07 10:35:31 +02:00
Damir Jelić 98ee4a3bca Merge branch 'fix_register_error' 2021-04-07 10:11:03 +02:00
Damir Jelić fdb1e3482e Merge branch 'bump-ruma' 2021-04-06 12:44:33 +02:00
Damir Jelić 999c99107d Merge branch 'room_member' 2021-04-06 12:20:04 +02:00
Kévin Commaille 7c34ac4e82 Bump ruma to 2f1b9f097930bf7908ca539f2ab7bb0ccf5d8b25
Use MxcUri instead of String for media URLs.

Fix wrong MXC URIs in tests.

Remove method parse_mxc no longer useful.

Apply new non-exhaustive types: CrossSigningKey, OneTimeKey and SignedKey.

Apply endpoint name change: send_state_event_for_key to send_state_event
2021-04-05 19:49:55 +02:00
Julian Sparber e72f4cee59 matrix-sdk: Add RoomMember 2021-04-02 20:39:50 +02:00
Julian Sparber 50423786f7 matrix-sdk: Fix register_error test 2021-04-02 12:13:56 +02:00
Jonas Platte 79eb07f717 Allow Result aliases to be used with two type parameters 2021-04-01 19:35:09 +02:00
Damir Jelić ff683602f2 crypto: Export the KeysExport error 2021-03-30 13:52:57 +02:00
Damir Jelić 74274e6dcb base: Allow the test target to be compiled on WASM 2021-03-30 13:05:45 +02:00
Damir Jelić 02b44ca9ba matrix-sdk: Fix or silence a bunch of new clippy warnings 2021-03-30 13:05:13 +02:00
Julian Sparber 84b187ec12 matrix-sdk: Add function to get room avatar 2021-03-25 15:01:41 +01:00
Julian Sparber d35e730052 matrix-sdk: Add function to get users avatar 2021-03-25 15:01:41 +01:00
Damir Jelić ef6e481860 Merge branch 'client-sso' 2021-03-23 16:12:24 +01:00
Kévin Commaille 8679e81555 client: Add login_with_sso 2021-03-23 15:30:40 +01:00
Kévin Commaille 6f59e895b6 client: Add login_with_token 2021-03-23 15:17:12 +01:00
Kévin Commaille 8a96b2c062 client: Add get_sso_login_url 2021-03-23 14:47:15 +01:00
Damir Jelić ce4b809072 matrix-sdk: Don't ignore the accept_with_settings() Sas example 2021-03-23 14:30:31 +01:00
Damir Jelić e92b97eff6 matrix-sdk: Fix the example for the room_send() method 2021-03-23 14:29:26 +01:00
Kévin Commaille 51d915a181 client: Add get_login_types 2021-03-23 14:27:55 +01:00
Damir Jelić 9d0085d4dd matrix-sdk: Add the Client level room send method back 2021-03-23 14:00:20 +01:00
Damir Jelić 35c7ae665d CI: Install Emsripten 2021-03-23 12:43:38 +01:00
Damir Jelić 97385255d4 CI: Change the dir take three 2021-03-23 12:31:17 +01:00
Damir Jelić cf90a18f13 CI: Change the directory take two 2021-03-23 12:26:46 +01:00
Damir Jelić a9c37ba2d0 CI: Install the WASM target for the WASM check 2021-03-23 12:11:05 +01:00
Damir Jelić 957bca1a14 CI: Add the missing runs-on definition 2021-03-23 12:02:32 +01:00
Damir Jelić f0f6012871 CI: check if the WASM example compiles 2021-03-23 11:56:43 +01:00
Damir Jelić 15d5b234ed Merge branch 'multithreaded-crypto' 2021-03-23 11:34:07 +01:00
Damir Jelić 50d7e09347 commmon: Document the executor module 2021-03-23 11:33:14 +01:00
Damir Jelić 12bf0f53a8 matrix-sdk: Fix the WASM example 2021-03-23 10:18:55 +01:00
Damir Jelić bbe812f1d9 common: Add a executor abstraction so we can spawn tasks under WASM 2021-03-23 10:18:55 +01:00
Kévin Commaille dc74bc6116 bump ruma to 92ee92ad7eb90b3c80abbd7eb116d886c79bf4fd 2021-03-18 11:40:53 +01:00
Julian Sparber 382ec01bc3 move matrix_sdk_base::EventHandler to matrix_sdk 2021-03-17 15:29:26 +01:00
Damir Jelić e9dff24ba7 Merge branch 'add_room_enum' 2021-03-17 15:08:02 +01:00
Julian Sparber 19cacb1f26 matrix-sdk: Add room::State enum
This enum contains the room in the Joined, Left and Invited state.
2021-03-17 13:08:31 +01:00
Julian Sparber 5d66ff475f matrix-sdk: Reexport matrix_sdk_base::Room as BaseRoom 2021-03-17 12:17:37 +01:00
Damir Jelić 9aad775f01 Merge branch 'lock_requests' 2021-03-16 17:01:04 +01:00
Damir Jelić ec88e28fd2 Merge branch 'fix-examples' 2021-03-16 16:54:00 +01:00
Julian Sparber 387104e6e0 matrix-sdk: wrap request locks into an Arc 2021-03-16 16:37:50 +01:00
Weihang Lo de1bf2b89f matrix-sdk: fix accidentally hidden lines in examples 2021-03-16 23:15:51 +08:00
Weihang Lo 8c1761faed matrix-sdk: hide get joined room logic 2021-03-16 21:55:15 +08:00
Weihang Lo cbc8b53da1 client: add test for room_redact 2021-03-16 21:52:15 +08:00
Weihang Lo b110ee27fa client: PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId} 2021-03-16 21:49:47 +08:00
Julian Sparber 5465a7b511 matrix-sdk: prevent frequent typing_notice requests 2021-03-16 10:50:50 +01:00
Julian Sparber 2f769726dd matrix-sdk: prevent dupplicated members requests 2021-03-15 12:32:57 +01:00
Julian Sparber 31dd031269 matrix-sdk: Move room specific methods to room structs 2021-03-15 10:54:45 +01:00
Julian Sparber 450036cf86 matrix-sdk-test: Add response for members API 2021-03-13 13:15:50 +01:00
Julian Sparber a4bac499e9 matrix-sdk-base: Add method to get all members from the store 2021-03-13 13:15:50 +01:00
Julian Sparber 2d6502247b matrix-sdk: Add method to get room as room::Common 2021-03-13 13:15:50 +01:00
Julian Sparber 88e230689e matrix-sdk: Add high-level room API 2021-03-13 13:15:50 +01:00
Damir Jelić 7c04c3a041 Merge branch 'F1rst-Unicorn/master' 2021-03-13 12:05:30 +01:00
Damir Jelić 75ac29540d crypto: Simplify counting the number of messages a to-device request has 2021-03-13 11:50:05 +01:00
Weihang Lo ea7d90de62 client: PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey} 2021-03-13 16:21:51 +08:00
Damir Jelić 880818a588 crypto: Send bigger sendToDevice requests out that carry our room keys 2021-03-12 16:33:35 +01:00
Damir Jelić e09a155cfc crypto: Fix a completely wrong application of extend()
We were merging the to-device messages using the extend() method while
our data has the shape of BTreeMap<UserId, BTreeMap<_, _>>, extending
such a map would mean that the inner BTreeMap would get dropped if both
maps contain the same UserId.

We need to extend the inner maps, those are guaranteed to contain unique
device ids.
2021-03-12 16:33:26 +01:00
Jan Veen 42c8c42150 crypto: Improve doc of SAS accept settings
Document arguments explicitly.

Adapt to changed implementation.

Provide example call.
2021-03-12 15:45:58 +01:00
Jan Veen 587c09e700 crypto: Prohibit extending verification methods
Intersect the allowed methods passed from the user with the methods
supported by the other party. If the user added new methods to the
request, the remote party would cancel the verification.
2021-03-12 14:43:59 +01:00
Jan Veen e9be23f853 crypto: Add settings to customize SAS accepting
Offer specifying settings to SAS accept() requests to limit the allowed
verification methods.
2021-03-11 21:10:26 +01:00
Damir Jelić 7465574bdc crypto: Fix a clippy warning 2021-03-11 20:06:51 +01:00
Damir Jelić 593b5e55cb crypto: Don't load the account every time we load sessions in the sled store
This removes a massive performance issue since getting sessions is part
of every message send cycle.

Namely every time we check that all our devices inside a certain room
have established 1 to 1 Olm sessions we load the account from the store.

This means we go through an account unpickling phase which contains AES
decryption every time we load sessions.

Cache instead the account info that we're going to attach to sessions
when we initially save or load the account.
2021-03-11 19:49:32 +01:00
Damir Jelić d4e847f02f benches: Add a benchmark for the missing session collecting 2021-03-11 13:30:19 +01:00
Damir Jelić a32f9187e6 benches: Fix the key claiming bench, it needs to run under tokio now 2021-03-11 13:28:22 +01:00
Damir Jelić daf313e358 crypto: Go through the user device keys in parallel 2021-03-10 14:08:45 +01:00
Damir Jelić 570bd2e358 crypto: Move the tracked users marking out of the device key handling method 2021-03-10 12:20:03 +01:00
Damir Jelić c8d4cd0a5b crypto: Calculate the device changes for a given user in parallel 2021-03-10 12:05:21 +01:00
Damir Jelić 0c5d13cb91 crypto: Remove some stale TODO comments 2021-03-10 10:03:54 +01:00
Damir Jelić aff5cddb68 crypto: Remove an unneeded import. 2021-03-10 09:58:30 +01:00
poljar 4a8c30527d crypto: Fix a typo.
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
2021-03-10 09:54:33 +01:00
Damir Jelić 560aa5b0a9 crypto: Encrypt the share group session requests in parallel. 2021-03-09 15:09:59 +01:00
Damir Jelić a8bc619dca crypto: Encrypt room keys for a room key share request in parallel 2021-03-09 14:30:28 +01:00
Damir Jelić 91c326e970 benches: Run the async benches on a tokio runtime. 2021-03-09 14:24:16 +01:00
Damir Jelić e5585b57e8 Merge branch 'room_merge' 2021-03-08 13:39:46 +01:00
Damir Jelić 61167fab15 crypto: Make restored outbound sessions wait for requests if they have some 2021-03-05 17:12:32 +01:00
Julian Sparber bc2c924c88 matrix-sdk-base: remove InvitedRoom, JoinedRoom, LeftRoom and RoomState
They are all replaced by `Room`
2021-03-05 12:19:50 +01:00
Julian Sparber 9332c55c8d matrix-sdk-base: merge StrippedRoom and Room 2021-03-05 11:31:01 +01:00
Damir Jelić c5241af675 crypto: Expose the crypto store error pulicly 2021-03-04 17:46:18 +01:00
Julian Sparber f6f382e28a matrix-sdk: Export RoomType 2021-03-03 11:35:02 +01:00
Julian Sparber 31f4a58f38 matrix-sdk-base: Export RoomType 2021-03-03 11:34:59 +01:00
Julian Sparber 780348f546 matrix-sdk-base: Remove the unused enum RoomStateType 2021-03-03 11:18:23 +01:00
Damir Jelić 93e5c34670 crypto: Add a bit more info to the room key sharing logic logging 2021-03-02 17:15:10 +01:00
Damir Jelić 6597948564 crypto: Add a TODO item for m.room_key.withheld messages 2021-03-02 16:27:24 +01:00
Damir Jelić 693a0337a2 crypto: Don't log the devices that receive an outbound session twice 2021-03-02 16:26:58 +01:00
Damir Jelić 7729e2b11f matrix-sdk: Add some custom debug implementations
This should avoid polluting the logs with sled trees and a lot of
redundant info in a device if a device or store ends up in the
structured logs.
2021-03-02 16:22:38 +01:00
Damir Jelić 00df34ed59 Merge branch 'more-benchmarks' 2021-03-02 15:03:24 +01:00
Damir Jelić 8f481dd859 client: Add a method to get all known rooms 2021-03-02 14:58:30 +01:00
Damir Jelić 123772c524 crypto: More logs for the group session sharing logic 2021-03-02 14:54:56 +01:00
Damir Jelić cb91aa76fc Merge branch 'group-session-sharing-code-refactoring' 2021-03-02 12:55:37 +01:00
Damir Jelić bb358909ef crypto: Fix a typo and improve some logs in the session sharing logic. 2021-03-02 12:54:22 +01:00
Damir Jelić 56a696d1c0 contrib: Make the failures mitmproxy script a bit smarter. 2021-03-02 12:24:23 +01:00
Denis Kasak 3f7eae8633 cargo fmt 2021-03-02 12:20:09 +01:00
Denis Kasak 2b5e1744ee More refactoring of the group session sharing code for clarity. 2021-03-02 12:20:09 +01:00
Denis Kasak df8c489304 Fix typo: visiblity -> visibility 2021-03-02 12:20:09 +01:00
Denis Kasak aa16a7e291 crypto: Refactor the group session rotation code some more for clarity. 2021-03-02 12:20:09 +01:00
Denis Kasak 70ecf269d0 Improve docstring of GroupSessionManager::collect_session_recipients. 2021-03-02 12:20:09 +01:00
Damir Jelić 83926e154b README: Add a badge that points to the latest master docs 2021-03-02 11:40:02 +01:00
Damir Jelić 42fb88a7f9 Merge branch 'duplicate-content-type-header' 2021-03-02 11:32:40 +01:00
Damir Jelić 4ccb5a1cb9 benches: Benchmark the sled store as well in the key query/claim benches 2021-03-02 11:18:10 +01:00
Damir Jelić 48903a24d2 benches: Benchmark our key sharing throughput 2021-03-02 11:17:38 +01:00
Damir Jelić e6f6665fa0 Merge branch 'master' into history-visiblity-session-share 2021-03-01 20:47:31 +01:00
Damir Jelić 2e659afd26 crypto: Make it clearer that the deleted flag can only be set to true 2021-03-01 20:34:29 +01:00
Damir Jelić 3a08f0c278 matrix-sdk: Don't set the content type ourselves
We don't need to worry about this anymore, since Ruma sets this for all
the request nowadays.
2021-03-01 20:12:38 +01:00
Damir Jelić 9893ddba74 crypto: Use Default to create some test data 2021-03-01 19:41:39 +01:00
Damir Jelić 5c0f0140e9 matrix-sdk: Fix some doc examples 2021-03-01 19:41:14 +01:00
Damir Jelić 1f5cad136e matrix-sdk: Bump Ruma 2021-03-01 19:20:07 +01:00
Damir Jelić 4c3cd29224 matrix-sdk: Don't set two content-type headers for json contents
Ruma will for some requests already set the content-type for us to
application/json, but for some it still seems to miss the header, since
the headers are kept in a map add the header only if it isn't already
there.
2021-03-01 16:37:56 +01:00
Damir Jelić ffaddb22b8 Merge branch 'docs-github-pages' 2021-03-01 13:15:43 +01:00
Julian Sparber 8ebd61dd18 CI: Publish docs via github pages
Fixes: https://github.com/matrix-org/matrix-rust-sdk/issues/154
2021-03-01 12:53:35 +01:00
Damir Jelić c8e769860b Merge branch 'benchmarks' 2021-03-01 12:30:10 +01:00
Damir Jelić e1d4fe533d crypto: Make pprof a Linux specific dependency 2021-03-01 11:46:28 +01:00
Damir Jelić 447d78567a contrib: Add a mitmproxy script to block well-known lookups
This is useful if a test server sends incorrect homeserver URLs in their
well-known data, for example the Synapse instance Complement starts up
does so.
2021-03-01 11:15:50 +01:00
Damir Jelić d07ac997f2 README: Add a badge for our docs 2021-03-01 11:15:34 +01:00
Damir Jelić fc6ff4288e benches: Add support to generate flamegraphs when we profile our benchmarks 2021-02-28 11:28:10 +01:00
Damir Jelić c64567ba9b crypto: Add a bench for the key claiming process 2021-02-27 18:40:58 +01:00
Damir Jelić 6e168051b6 crypto: Chunk out key query requests. 2021-02-26 16:48:42 +01:00
Damir Jelić 2a09e588f3 crypto: Log when we receive room keys 2021-02-17 16:01:51 +01:00
Damir Jelić 5ca40b9893 crypto: Be more forgiving when updating one-time key counts 2021-02-17 15:24:46 +01:00
Damir Jelić 6cc03d1c19 crypto: Improve the logging for deserialization failures 2021-02-17 15:23:26 +01:00
Damir Jelić 544881f11c crypto: Fix a clippy warning 2021-02-16 10:52:19 +01:00
Damir Jelić ef5d7ca579 crypto: Add missing flush calls to the sled crypto store 2021-02-16 10:29:10 +01:00
Damir Jelić 1db89741bc matrix-sdk: Re-export the EncryptionInfo struct 2021-02-16 09:42:23 +01:00
Damir Jelić c39fa6543f crypto: Expose the EncryptionInfo struct publicly 2021-02-15 15:19:48 +01:00
Damir Jelić fe11ad7e3e matrix-sdk: Remove the design doc for now
It's outdated and somewhat misleading, so remove it for now until we
have a new one with pictures and stuff.

Closes: #139
2021-02-15 09:44:48 +01:00
Cédric Barreteau b6f2c43330 Rename EventEmitter to EventHandler 2021-02-13 11:01:31 +01:00
Cédric Barreteau e3e48148f0 Rename add_event_emitter to set_event_emitter
Closes #145.
2021-02-13 10:43:42 +01:00
Damir Jelić 2811c490a0 matrix-sdk: Fix some new clippy warnings 2021-02-12 12:59:53 +01:00
Jonas Platte 2e7f862f9c Delete .travis.yml
CI has been moved to GitHub actions a while ago.
2021-02-12 12:29:50 +01:00
Damir Jelić e857172170 base: Fix a couple of typos 2021-02-10 21:32:33 +01:00
Damir Jelić b7fda1deb7 base: Fix a typo in the room members 2021-02-10 20:57:26 +01:00
Damir Jelić c34f69f8a3 crypto: Don't receive the whole sync response, only what we need.
This makes it clearer what the crypto layer is doing, this also makes it
clearer for people that will use the crypto layer over FFI that they
don't need to go through a serialize/deserialize cycle for the whole
sync response.
2021-02-10 15:42:55 +01:00
Damir Jelić e3d1de8e6c client: Fix the sync_with_callback example 2021-02-10 09:51:14 +01:00
Damir Jelić 19b78be93f base: Fix a typo 2021-02-10 09:15:25 +01:00
Damir Jelić e437aea012 Merge branch 'bump_ruma' 2021-02-09 10:56:40 +01:00
Julian Sparber 155f975262 Update ruma to rev d6aa37c848b7f682a98c25b346899e284ffc6df7
This enables the `compat` feature of ruma to increase compatipility.
2021-02-09 10:46:33 +01:00
Damir Jelić e7e43a8bf0 matrix-sdk: Use a released version of backoff 2021-02-07 17:21:50 +01:00
Damir Jelić 0289f564b4 Merge branch 'pub-exports' 2021-02-07 13:58:33 +01:00
Damir Jelić 1e67f338ac Merge branch 'request-retrying' 2021-02-07 13:52:25 +01:00
Damir Jelić 36e3039d73 matrix-sdk: Disable request retrying for wasm for now
Backoff supports the retry method for futures only for non-wasm
targets for now, thus we're going to disable it until that changes.
2021-02-07 12:53:06 +01:00
Devin Ragotzy fcd1c87765 matrix_sdk: export CustomEvent and StateChanges add docs to StateChanges 2021-02-04 15:54:20 -05:00
Damir Jelić 1799721a5f crypto: Store the history visibility with inbound group sessions
This can be useful to share the room history with new room members.
2021-02-03 16:59:34 +01:00
Damir Jelić 9e83eaf2f5 crypto: Store the history visiblity with the outbound session 2021-02-03 16:01:58 +01:00
Damir Jelić 347f79d08c base: Respect the history visiblity setting when sharing group sessions 2021-02-03 13:56:31 +01:00
Julian Sparber bdaed6237e base: make fields of UnreadNotificationsCount public 2021-02-02 21:11:51 +01:00
Damir Jelić ca7117af2b matrix-sdk: Clamp the request timeout for uploads to a sensible value 2021-02-01 21:56:15 +01:00
Damir Jelić f3d4f6aab4 matrix-sdk: Fix our HttpClient trait implementation example 2021-02-01 19:24:29 +01:00
Damir Jelić 19e9884963 matrix-sdk: Update for the latest backoff changes 2021-02-01 17:58:03 +01:00
Damir Jelić 2e2d9b33a4 contrib: Add a mitmproxy script which can be used to test out request retrying 2021-02-01 17:30:43 +01:00
Damir Jelić a551ae2bee matrix-sdk: Add sensible connection and request timeouts
This sets the default
    * connection timeout to 5s
    * request timeout to 10s
    * request timeout for syncs to the sync timeout + 10s
    * request timeout for uploads to be based on 1Mbps upload
    speed expectations
2021-02-01 17:15:29 +01:00
Damir Jelić 6a4ac8f361 matrix-sdk: Replace some unwraps with expects. 2021-01-31 21:12:00 +01:00
Damir Jelić 42ec456abf matrix-sdk: Add initial support for request retrying 2021-01-31 21:10:30 +01:00
Damir Jelić 585ca9fdf7 matrix-sdk: Split out the http errors into a sub-enum 2021-01-31 18:09:03 +01:00
Damir Jelić b66c666997 base: Expose and document the stripped room info 2021-01-28 14:59:57 +01:00
Damir Jelić 92f0523e37 base: More docs 2021-01-28 14:51:34 +01:00
Damir Jelić 58691986a9 base: Initial set of docs 2021-01-28 14:10:26 +01:00
Damir Jelić 10da61c567 crypto: Answer key reshare requests only at the originally shared message index 2021-01-28 14:07:51 +01:00
Damir Jelić bf4f32eccf crypto: Remove the sqlite store for now 2021-01-27 15:29:42 +01:00
Damir Jelić bc3ba3fab0 crypto: Add tests for the sled cryptostore 2021-01-27 15:19:32 +01:00
Damir Jelić d6c5a4d8aa crypto: Add a missing encode call in the sled store 2021-01-27 15:15:45 +01:00
Damir Jelić 81667173b6 matrix-sdk: Re-enable some more client tests 2021-01-27 14:43:53 +01:00
Damir Jelić cb26e653da base: Add a TODO explaining how redacted state needs to be healed 2021-01-27 12:39:54 +01:00
Damir Jelić 442103a37e base: Store the display names in the memory store as well 2021-01-27 11:59:30 +01:00
Damir Jelić 094ead9d7d base: Allow users to inspect the ambiguity change a member event triggers 2021-01-27 11:46:44 +01:00
Damir Jelić 55430dd3d2 base: Us and_then() instead of map() + flatten() 2021-01-26 19:28:17 +01:00
Damir Jelić b3cfa48b45 base: Allow inspecting dispaly name owners in the sate inspector 2021-01-26 14:44:37 +01:00
Damir Jelić 71a087c379 crypto: Encode our keys in the sled cryptostore as well 2021-01-26 14:22:03 +01:00
Damir Jelić fc085a7391 base: Use encoded keys for the whole sled store 2021-01-26 14:04:37 +01:00
Damir Jelić b4a916b797 base: Add a method to get all the user ids that use a certain display name 2021-01-26 13:22:06 +01:00
Damir Jelić 6cb2c8b468 crypto: Store and restore outbound group sessions 2021-01-25 17:14:13 +01:00
Damir Jelić ac6dad3f35 matrix-sdk: Bump our deps 2021-01-25 15:47:51 +01:00
Damir Jelić c1f9d3bc39 crypto: Add a bench for our key query response handling 2021-01-25 10:13:08 +01:00
Damir Jelić eb8138ca6a base: Restore stripped room infos as well 2021-01-23 17:29:43 +01:00
Damir Jelić 44974982e1 client: Add an accessor method for the device id 2021-01-23 15:59:53 +01:00
Damir Jelić 077050efb4 crypto: Add a hack so e2ee support works under WASM again 2021-01-22 18:40:08 +01:00
Damir Jelić d10b85a05d matrix-sdk: Fix our wasm command bot example 2021-01-22 18:14:08 +01:00
Damir Jelić 9c98d0227b matrix-sdk: Make the http client trait WASM compatible 2021-01-22 18:12:46 +01:00
Damir Jelić 8028c23f56 base: Feature flag the sled state store 2021-01-22 18:10:17 +01:00
Damir Jelić cb12bc1584 base: Use Instant instead of SystemTime for wasm compatibility 2021-01-22 18:07:34 +01:00
Damir Jelić b83399ba14 base: Fix a typo take two 2021-01-22 16:07:24 +01:00
Damir Jelić 5daa22250f base: Fix a typo 2021-01-22 15:45:14 +01:00
Damir Jelić c034de470b base: Allow using the same sled database for the state and cryptostore 2021-01-22 11:33:06 +01:00
Damir Jelić 9cd217fc5d matrix-sdk: Remove the proxy usage from the exmaples for now
While it's generally useful to watch what the sdk is sending
out during development using mitmproxy, users of the sdk might
wonder why the example doesn't connect.

Remove the proxy usage until we add a cli parser which can enable proxy
support with a command line switch.
2021-01-22 11:32:33 +01:00
Damir Jelić cf07fc8e8e Merge branch 'master' into new-state-store 2021-01-21 19:58:40 +01:00
Tilo Spannagel 7b8d2b5319 Add support for ruma feature flag markdown
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2021-01-21 18:22:17 +01:00
Tilo Spannagel abd62cab0d Update ruma to rev 8c109d3c0a7ec66b352dc82677d30db7cb0723eb
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2021-01-21 18:22:08 +01:00
Damir Jelić 7d45417a17 base: Add a memory-only store 2021-01-21 16:31:33 +01:00
Damir Jelić 66ecb4c1e6 base: Store room infos for left and invited rooms 2021-01-21 15:12:13 +01:00
Damir Jelić 1483c22171 crypto: Don't send out empty to-device reuqests when sharing sessions
An empty to-device request can happen if we're trying to re-share a
session with devices that are we're missing an olm session with so don't
send them out.
2021-01-21 14:04:31 +01:00
Damir Jelić ae0d810fb0 base: Avoid the Trait is not general enough issue again 2021-01-21 14:03:41 +01:00
Damir Jelić 948c811d4b client: Simplify the auto key-claiming invocation 2021-01-21 14:03:20 +01:00
Damir Jelić ef2f20eb97 crypto: Rotate the megolm session if a device gets blacklisted 2021-01-21 12:19:02 +01:00
Damir Jelić 303ac513e5 base: Remove some stale files from the old state store 2021-01-21 12:13:46 +01:00
Damir Jelić de4df4e50a base: Re-introduce a state store trait. 2021-01-21 12:08:16 +01:00
Damir Jelić 2bcc0afb91 base: Use a CSPRNG to get our randomness and handle randomness errors
Since we're going to encrypt a lot of small objects separately we're
gonna need a lot of random nonces, it doesn't help that our nonces are
24 bytes long either. So use a CSPRNG to random data faster, also don't
panic if there wasn't enough randomness.
2021-01-20 16:59:46 +01:00
Damir Jelić 0a6b0e5804 base: Properly handle crypto related errors in the sled store 2021-01-20 16:27:59 +01:00
Damir Jelić 4a06c9e82d base: Initial support for an encrypted sled store. 2021-01-20 15:57:23 +01:00
Damir Jelić 06a973a1b8 crypto: Don't use the full PBKDF rounds when testing 2021-01-20 14:10:57 +01:00
Damir Jelić 28cc5acc87 base: Add a store key struct 2021-01-20 14:10:57 +01:00
Damir Jelić 2b5ff82414 base: Move the sled store into a subfolder 2021-01-20 10:25:54 +01:00
Damir Jelić 3472c99c27 base: Split out the store module into smaller submodules 2021-01-19 16:48:37 +01:00
Damir Jelić 17f3dbb0a0 crypto: Return a deserialized ToDevice struct when we receive a sync 2021-01-19 12:59:31 +01:00
Damir Jelić 6a30514d40 base: Move the deserialized responses types into the common crate 2021-01-19 12:30:58 +01:00
Damir Jelić 4f4ba831c1 crypto: Bump the PBKDF rounds for the pickle key derivation 2021-01-19 12:05:30 +01:00
Damir Jelić b8fcc003ea base: Finish up the error handling for the new stores 2021-01-19 12:03:46 +01:00
Damir Jelić ef95d9b539 crypto: Fix a misleading comment about the outbound session rotation period 2021-01-19 10:21:12 +01:00
Damir Jelić 377b8ea75a crypto: Use consistent ordering for the group session sharing log line 2021-01-19 10:19:15 +01:00
Damir Jelić 4af9b74776 crypto: Properly clamp the rotation period of the outbound session 2021-01-18 20:46:34 +01:00
Damir Jelić d07063af2b base: Add some error handling to the state store 2021-01-18 18:07:53 +01:00
Damir Jelić e5ba0298d0 crypto: Refactor and document the share group session method a bit better 2021-01-18 15:21:54 +01:00
Damir Jelić 4eb504d000 crypto: Improve the log line when we share group sessions 2021-01-18 14:15:31 +01:00
Damir Jelić 436530e874 crypto: Fix a couple clippy warnings 2021-01-18 13:50:59 +01:00
Damir Jelić 1746690eda crypto: Add a sled cryptostore 2021-01-18 13:38:00 +01:00
Damir Jelić 629a8ee84f crypto: Add getters for the sender key in our sessions 2021-01-18 13:28:09 +01:00
Damir Jelić 5418c88775 crypto: Add some more serialize/deserialize implementations 2021-01-18 13:21:30 +01:00
Damir Jelić 14575892bd crypto: Implement serialize/deserialize for devices. 2021-01-18 13:19:13 +01:00
Damir Jelić 43a74524c5 crypto: Add a pending requests method for the outbound group session 2021-01-18 12:44:19 +01:00
Amanda Graven aadbc14dc6 Add accessor for room member avatar urls 2021-01-15 18:59:51 +01:00
Damir Jelić bab8fde0ac crypto: Change the way we share group sessions
This patch removes the need to ask if a group session needs to be shared
it also adapts the method so it re-shares sessions if new users or
devices join the group.
2021-01-15 18:04:45 +01:00
Damir Jelić 40c53f09ba base: Handle room avatar updates 2021-01-15 09:57:59 +01:00
Damir Jelić 508bf3b23d base: Include the to-device events when returning the sync response 2021-01-14 13:35:21 +01:00
Damir Jelić 43ea9a16a0 crypto: Use the chain method to get the sha hash of the content 2021-01-14 13:34:12 +01:00
Damir Jelić 3f3ae794a4 crypto: Don't log an error for the commitment calculation since it isn't one 2021-01-14 13:32:39 +01:00
Amanda Graven 9efece4f7a Remove unnecessary clones 2021-01-11 14:17:17 +01:00
Damir Jelić 077c20ed74 base: Really fix the holding on across await points issue for room names 2021-01-11 13:40:35 +01:00
Amanda Graven 6c4888a123 Don't hold lock during await in name calculation 2021-01-11 13:18:26 +01:00
Damir Jelić 643526987f Merge branch 'master' into new-state-store 2021-01-05 21:40:39 +01:00
Damir Jelić b311a31c9e matrix-sdk: Bump our tokio and reqwest versions. 2021-01-05 21:39:52 +01:00
Damir Jelić b8c6c2e07c rooms: Use unstable member sorting for the room name calculation 2021-01-05 20:26:27 +01:00
Damir Jelić cdc93ddd0f base: Refactor and fix the room name calculation for non-stripped rooms 2021-01-05 20:09:06 +01:00
Damir Jelić ccd8a4d602 Merge branch 'master' into new-state-store 2021-01-05 17:03:24 +01:00
Damir Jelić f3acf582ec base: Fix a typo. 2021-01-04 18:34:23 +01:00
Damir Jelić 22b13c369b base: Add a method to check if the room is public. 2021-01-04 18:26:53 +01:00
Damir Jelić 76ce3fecb3 client: Re-enable two additional tests 2021-01-04 18:02:36 +01:00
Damir Jelić 99c1f70c1a Merge branch 'release-0.2' into new-state-store 2021-01-04 17:44:20 +01:00
Damir Jelić c6a80dc921 Merge branch 'master' into new-state-store 2021-01-04 17:34:33 +01:00
Damir Jelić e9d22c95a4 base: Handle the join rules, history visibility and guest access 2021-01-04 15:12:02 +01:00
Damir Jelić 108d4ebffe Merge branch 'master' into new-state-store 2021-01-04 14:16:15 +01:00
Damir Jelić e66add476f base: Store the room creation content
The power level depends on a bunch of stuff, if no power level event
exists the default for a room creator is 100 while for every other user
is 0, thus we need to know the room creator.
2021-01-04 12:32:54 +01:00
Damir Jelić 4afc6b2567 base: Don't mark all the room methods as public. 2021-01-04 12:26:13 +01:00
Damir Jelić 83b850d8f9 base: Add the last missing accessors and reorder them. 2021-01-04 10:15:02 +01:00
Damir Jelić e7e1d2d3eb base: Add more accessors for the room info. 2021-01-04 10:03:16 +01:00
Damir Jelić 74998c8dd8 rooms: Add a method to get the room topic. 2021-01-03 16:52:47 +01:00
Damir Jelić 0edef38eb7 base: Fix some clippy warnings 2021-01-02 13:54:47 +01:00
Damir Jelić 807c58649d Merge branch 'crypto-improvements' into new-state-store 2021-01-02 13:49:20 +01:00
Damir Jelić bafe9a0f61 crypto: Fix a couple of clippy warnings. 2021-01-02 13:47:53 +01:00
Damir Jelić f1140fec8b Merge branch 'crypto-improvements' into new-state-store 2021-01-02 13:17:25 +01:00
Damir Jelić f9f176ccfd base: Rename the state store example. 2021-01-02 13:04:05 +01:00
Damir Jelić 16f94ecc1d base: Improve the state store example so it can run non-interactively 2021-01-02 12:58:52 +01:00
Damir Jelić b995492457 base: Add a method to get either the display name or the localpart of an user 2021-01-01 14:59:30 +01:00
Damir Jelić 0c81f3d9ae base: Add a method to get all joined members. 2021-01-01 14:58:44 +01:00
Damir Jelić c804104293 client: Add the get_x_room methods back. 2021-01-01 14:57:39 +01:00
Damir Jelić 0952205e1e base: Restore rooms and the sync token when we restore the login. 2021-01-01 14:56:06 +01:00
Damir Jelić 4d7da05b90 base: Store the sync token. 2021-01-01 14:54:52 +01:00
Damir Jelić d121a856c4 base: Remember the direct target for rooms. 2021-01-01 14:31:50 +01:00
Damir Jelić 2384069641 base: Add the normalized_power_level method back to the member. 2020-12-24 17:14:46 +01:00
Damir Jelić a29d2e39c4 base: Save profiles independently from membership events.
The sender controls the content of the membership event, since the
content contains profile data (display names, avatar urls) a sender
might incorrectly change the profile of another member inside the room.

This is allowed in the case where the sender is kicking or inviting the
member, this it will self heal once the member re-joins. Still, to
mitigate this a bit we're storing the profile data when we know that the
member sent out the content on their own.
2020-12-24 16:35:32 +01:00
Damir Jelić 0d99d8cc23 crypto: Test verification request starting up to SAS. 2020-12-24 15:22:51 +01:00
Damir Jelić e2225b2700 base: Add a state store inspector to the examples. 2020-12-23 14:53:14 +01:00
Damir Jelić 8857335a7d Merge branch 'crypto-improvements' into new-state-store 2020-12-22 16:18:46 +01:00
Damir Jelić 007e452d39 Merge branch 'master' into crypto-improvements 2020-12-22 15:53:08 +01:00
Damir Jelić 1313c3da3c client: Restore the membership based get room methods. 2020-12-22 10:47:21 +01:00
Damir Jelić 1bfb2d08a6 base: Remove the obsolete models files. 2020-12-22 10:14:16 +01:00
Damir Jelić c5709d23a5 base: Implement the last missing thing to get the emitter working again. 2020-12-22 10:09:59 +01:00
Damir Jelić e25441babc base: Create a store wrapp and move store methods under it. 2020-12-20 16:27:29 +01:00
Damir Jelić a370eb1e37 base: Re-introduce the event emitter. 2020-12-19 20:20:39 +01:00
Damir Jelić f9af880176 base: Upcast the bare rooms based on the membership state 2020-12-19 16:37:35 +01:00
Damir Jelić 7abf0c8805 store: Honor state keys for the state storage. 2020-12-19 14:44:46 +01:00
Damir Jelić b119b30939 crypto: Clippy warnings. 2020-12-18 19:26:51 +01:00
Damir Jelić 55436c6514 crypto: Add a test for verification request flows. 2020-12-18 18:23:42 +01:00
Damir Jelić ec863a928d crypto: More clippy warnings. 2020-12-18 13:57:57 +01:00
Damir Jelić 1fd8c2052e crypto: Fix a bunch of clippy warnings. 2020-12-18 13:50:02 +01:00
Damir Jelić 897c6abe92 crypto: Fix our tests now that we support in-room verifications. 2020-12-18 12:55:06 +01:00
Damir Jelić f735107caf crypto: Remove an unused argument. 2020-12-17 17:03:42 +01:00
Damir Jelić 48f43a4af1 crypto: Remove some unused imports. 2020-12-17 16:28:12 +01:00
Damir Jelić 4ad4ad1e94 crypto: Send out done events for in-room verifications. 2020-12-17 15:50:13 +01:00
Damir Jelić 79102b3390 crypto: Make the cancelations output only CancelContents. 2020-12-17 12:15:11 +01:00
Damir Jelić b6e28e2280 crypto: WIP more work on in-room verifications now up to accepting them. 2020-12-15 16:35:54 +01:00
Damir Jelić b05fed5a3b matrix-sdk: Fix our tests now that the state store is roughly done. 2020-12-15 10:23:31 +01:00
Damir Jelić b4edaffbe1 base: Rename the method to get joined/invited members. 2020-12-14 15:54:49 +01:00
Damir Jelić 45db95742a base: Add a common room info struct for normal and stripped rooms. 2020-12-14 14:53:50 +01:00
Damir Jelić 3a76cf7692 base: Restore getting the user ids when receiving a sync. 2020-12-14 13:48:29 +01:00
Damir Jelić 05b1384d16 base: Upcast member events so the state key is an user id. 2020-12-12 21:44:53 +01:00
Damir Jelić e245599913 base: Save the stripped state of invited rooms. 2020-12-11 21:17:27 +01:00
Damir Jelić b16e3b6bd8 base: Rename joined rooms as they are used for left rooms as well. 2020-12-11 16:42:38 +01:00
Damir Jelić 5105629c08 crypto: WIP handle in-room start events. 2020-12-11 16:13:58 +01:00
Damir Jelić 7570cf5ac2 crypto: WIP genrealize the sas so it can handle in-room and to-device events. 2020-12-11 15:42:49 +01:00
Damir Jelić 6f35a05311 matrix-sdk: Allow users to get a reference to the store. 2020-12-11 09:52:39 +01:00
Damir Jelić b0ac9d3320 crypto: WIP change the types of the sas sturcts to allow in-room verifications. 2020-12-10 17:49:28 +01:00
Damir Jelić 1bb5b42b1d crypto: Prepare the sas structs to handle in-room verifications. 2020-12-10 15:18:28 +01:00
Damir Jelić b9ddbb11af crypto: Move the inner sas struct into a separate module. 2020-12-10 14:07:47 +01:00
Damir Jelić a4e7dc1042 base: Correctly store the state events of rooms. 2020-12-10 10:01:53 +01:00
Damir Jelić ae33904a93 base: Rename some structs. 2020-12-09 20:22:11 +01:00
Damir Jelić a08f857e49 base: Split out the new room and member structs from the state store 2020-12-09 18:12:51 +01:00
Damir Jelić 7198b0daba crypto: WIP key verification request handling. 2020-12-09 17:18:23 +01:00
Damir Jelić 5babd71341 crypto: Copy the relates to field to the unencrypted content when encrypting 2020-12-09 17:16:03 +01:00
Damir Jelić d4ebe8cc83 Merge branch 'crypto-improvements' into new-state-store 2020-12-08 16:54:23 +01:00
Damir Jelić 6d2d48a35a base: WIP inivted rooms handling. 2020-12-08 09:52:27 +01:00
Damir Jelić 5c608ed474 base: Store main account data. 2020-12-07 16:35:00 +01:00
Damir Jelić e38f0762ee base: Store the notification counts. 2020-12-07 15:11:18 +01:00
Damir Jelić ab832da03e base: Deserialize ephemeral events. 2020-12-07 14:34:18 +01:00
Damir Jelić de61798d78 base: Store room account data. 2020-12-07 14:17:18 +01:00
Damir Jelić b36d907fac base: Add the power level event to the room member. 2020-12-06 18:11:32 +01:00
Damir Jelić 886a1c7a77 Merge branch 'crypto-improvements' into new-state-store 2020-12-05 15:01:21 +01:00
Damir Jelić 2bcdd0163b Merge branch 'crypto-improvements' into new-state-store 2020-12-04 17:02:32 +01:00
Damir Jelić f483a56f81 crypto: Remove a bunch of unneeded whitespace in a log line. 2020-12-04 17:01:15 +01:00
Damir Jelić a5131a0a73 crypto: Log both user id versions when the device keys mismatch. 2020-12-04 16:58:20 +01:00
Damir Jelić a7b31c90e0 Merge branch 'crypto-improvements' into new-state-store 2020-12-04 15:13:48 +01:00
Damir Jelić 8a842ec0a5 base: Log deserialization errors for decrypted events. 2020-12-04 15:13:37 +01:00
Damir Jelić 1b6bdc3307 crypto: Preserve the relationship info while decrypting events. 2020-12-04 15:12:46 +01:00
Damir Jelić 1733808221 Merge branch 'crypto-improvements' into new-state-store 2020-12-04 13:50:39 +01:00
Damir Jelić 8291b93356 matrix-sdk: Update ruma. 2020-12-04 13:35:56 +01:00
Damir Jelić 45442dfac8 Merge branch 'master' into new-state-store 2020-12-01 17:24:00 +01:00
Damir Jelić 27b5bf3ddd base: Add initial left rooms handling. 2020-12-01 10:23:28 +01:00
Damir Jelić 0e563a9a81 base: Refactor out the room state/timeline handling. 2020-11-30 17:25:29 +01:00
Damir Jelić 7dd834a214 base: Add some more sync response fields. 2020-11-30 15:50:47 +01:00
Damir Jelić b4d0179c18 base: Fetch the member presence when we fetch members. 2020-11-30 14:55:18 +01:00
Damir Jelić 38048a2043 base: Add presence storing. 2020-11-30 14:42:08 +01:00
Damir Jelić ac2d90e92a client: Apply room changes when fetching members. 2020-11-30 09:19:11 +01:00
Damir Jelić a8d6909c56 crypto: Use a released sqlx version. 2020-11-29 11:22:53 +01:00
Damir Jelić fcb50956bb Merge branch 'master' into new-state-store 2020-11-26 15:18:38 +01:00
Damir Jelić 43ced3d279 Merge branch 'crypto-improvements' into new-state-store 2020-11-26 14:08:17 +01:00
Damir Jelić baa5bed1c9 Merge branch 'crypto-improvements' into new-state-store 2020-11-26 14:00:18 +01:00
Damir Jelić de5f5cf00a base: A better log message for unhandled member events. 2020-11-24 10:58:33 +01:00
Damir Jelić 35069c5252 base: Turn the get member method async. 2020-11-24 10:58:14 +01:00
Damir Jelić dadcc68336 base: Use the room summary for the display name calculation if we have one. 2020-11-24 10:57:21 +01:00
Damir Jelić 64fff933af base: Store the room topic with the room summary. 2020-11-24 10:56:43 +01:00
Damir Jelić c13d04ae18 matrix-sdk: Return the members response in our get members method. 2020-11-24 10:56:07 +01:00
Damir Jelić e84d3b9950 base: Track new users we get from the room/members call. 2020-11-23 17:19:55 +01:00
Damir Jelić c1383402ed matrix-sdk: Initial support to upload filters. 2020-11-22 21:25:31 +01:00
Damir Jelić a98f23e2a7 base: Add a deserialized SyncResponse type. 2020-11-21 22:48:27 +01:00
Damir Jelić 53daf40c7c matrix-sdk: Use the latest ruma and olm-rs. 2020-11-21 16:33:12 +01:00
Damir Jelić 9edf8657d0 base: WIP lazy loading support. 2020-11-20 20:17:59 +01:00
Damir Jelić 3da1d3cf8f store: Use streams so we don't load all members at once. 2020-11-16 18:11:12 +01:00
Damir Jelić 8ed8929788 base: Fix the storing of invited and joined user ids. 2020-11-12 12:59:43 +01:00
Damir Jelić 133b230964 base: Change the way we're saving our room summary updates. 2020-11-12 11:21:37 +01:00
Damir Jelić 3a1eeb6a16 Merge branch 'crypto-improvements' into new-state-store 2020-11-11 14:43:49 +01:00
Damir Jelić 3f57ba57d0 base: WIP start to split out the steps collect changes, save changes,
apply changes.
2020-11-11 14:37:04 +01:00
Damir Jelić dc57873687 base: WIP more work on the new state store. 2020-10-25 21:03:03 +01:00
Damir Jelić 962f725d63 Merge branch 'crypto-improvements' into new-state-store 2020-10-24 20:16:59 +02:00
Damir Jelić c1e679147d base: First working version of the new state store. 2020-10-24 20:01:39 +02:00
Damir Jelić 4cc803fe27 crypto: WIP cross signing bootstrap. 2020-10-24 10:32:17 +02:00
Damir Jelić 9ce7feea1a base: Wip. 2020-10-23 09:39:08 +02:00
Damir Jelić bdf32eecc7 base: More work on the new state store. 2020-10-22 09:46:12 +02:00
Damir Jelić 5323e6e270 store: More work, add the ability to store member events. 2020-10-21 09:38:13 +02:00
Damir Jelić 045ab25fb7 base: Add initial state store based on sled. 2020-10-20 17:36:21 +02:00
Damir Jelić cd3d90df3f base: Remove a bunch of stuff and add sled. 2020-10-20 17:36:21 +02:00
Jonas Platte 92bedb4571 Upgrade ruma 2020-10-20 17:36:21 +02:00
140 changed files with 64465 additions and 14250 deletions
+161 -6
View File
@@ -20,7 +20,7 @@ jobs:
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: nightly
components: rustfmt
profile: minimal
override: true
@@ -54,6 +54,166 @@ jobs:
command: clippy
args: --all-targets -- -D warnings
- name: Clippy without default features
uses: actions-rs/cargo@v1
with:
command: clippy
# TODO: add `--all-targets` once all warnings in examples are resolved
args: --no-default-features --features native-tls,warp -- -D warnings
check-wasm:
name: linux / WASM
needs: [clippy]
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
profile: minimal
override: true
- name: Install emscripten
uses: mymindstorm/setup-emsdk@v7
- name: Check
run: |
cd matrix_sdk/examples/wasm_command_bot
cargo check --target wasm32-unknown-unknown
test-appservice:
name: ${{ matrix.name }}
needs: [clippy]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
name:
- linux / appservice / stable / actix
- macOS / appservice / stable / actix
- linux / appservice / stable / warp
- macOS / appservice / stable / warp
include:
- name: linux / appservice / stable / actix
cargo_args: --no-default-features --features actix
- name: macOS / appservice / stable / actix
os: macOS-latest
cargo_args: --no-default-features --features actix
- name: linux / appservice / stable / warp
cargo_args: --features warp
- name: macOS / appservice / stable / warp
os: macOS-latest
cargo_args: --features warp
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust || 'stable' }}
target: ${{ matrix.target }}
profile: minimal
override: true
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --manifest-path matrix_sdk_appservice/Cargo.toml ${{ matrix.cargo_args }} -- -D warnings
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path matrix_sdk_appservice/Cargo.toml ${{ matrix.cargo_args }}
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path matrix_sdk_appservice/Cargo.toml ${{ matrix.cargo_args }}
test-features:
name: ${{ matrix.name }}
needs: [clippy]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
name:
- linux / features-no-encryption
- linux / features-no-sled
- linux / features-no-encryption-and-sled
- linux / features-sled_cryptostore
- linux / features-rustls-tls
- linux / features-markdown
- linux / features-socks
- linux / features-sso_login
- linux / features-require_auth_for_profile_requests
include:
- name: linux / features-no-encryption
cargo_args: --no-default-features --features "sled_state_store, native-tls"
- name: linux / features-no-sled
cargo_args: --no-default-features --features "encryption, native-tls"
- name: linux / features-no-encryption-and-sled
cargo_args: --no-default-features --features "native-tls"
- name: linux / features-sled_cryptostore
cargo_args: --no-default-features --features "encryption, sled_cryptostore, native-tls"
- name: linux / features-rustls-tls
cargo_args: --no-default-features --features rustls-tls
- name: linux / features-require_auth_for_profile_requests
cargo_args: --no-default-features --features "require_auth_for_profile_requests, native-tls"
- name: linux / features-markdown
cargo_args: --features markdown
- name: linux / features-socks
cargo_args: --features socks
- name: linux / features-sso_login
cargo_args: --features sso_login
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust || 'stable' }}
target: ${{ matrix.target }}
profile: minimal
override: true
- name: Check
uses: actions-rs/cargo@v1
with:
command: check
args: --manifest-path matrix_sdk/Cargo.toml ${{ matrix.cargo_args }}
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path matrix_sdk/Cargo.toml ${{ matrix.cargo_args }}
test:
name: ${{ matrix.name }}
needs: [clippy]
@@ -65,7 +225,6 @@ jobs:
- linux / stable
- linux / beta
- macOS / stable
- windows / stable-x86_64-msvc
include:
- name: linux / stable
@@ -76,10 +235,6 @@ jobs:
- name: macOS / stable
os: macOS-latest
- name: windows / stable-x86_64-msvc
os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- name: Checkout
uses: actions/checkout@v1
+37
View File
@@ -0,0 +1,37 @@
name: docs
on:
push:
branches: [master]
pull_request:
jobs:
docs:
name: docs
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
- name: Build docs
uses: actions-rs/cargo@v1
env:
RUSTDOCFLAGS: "--enable-index-page -Zunstable-options"
with:
command: doc
args: --no-deps --workspace --features docs
- name: Deploy docs
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./target/doc/
+6
View File
@@ -0,0 +1,6 @@
max_width = 100
comment_width = 80
wrap_comments = true
imports_granularity = "Crate"
use_small_heuristics = "Max"
group_imports = "StdExternalCrate"
-89
View File
@@ -1,89 +0,0 @@
language: rust
rust: stable
addons:
apt:
packages:
- libssl-dev
jobs:
allow_failures:
- os: osx
name: macOS 10.15
include:
- stage: Format
os: linux
before_script:
- rustup component add rustfmt
script:
- cargo fmt --all -- --check
- stage: Clippy
os: linux
before_script:
- rustup component add clippy
script:
- cargo clippy --all-targets -- -D warnings
- stage: Test
os: linux
- os: windows
script:
- cd matrix_sdk
- cargo test --no-default-features --features "messages, native-tls"
- cd ../matrix_sdk_base
- cargo test --no-default-features --features "messages"
- os: osx
- os: linux
name: native-tls build
script:
- cd matrix_sdk
- cargo build --no-default-features --features "native-tls"
- os: linux
name: rustls-tls build
script:
- cd matrix_sdk
- cargo build --no-default-features --features "rustls-tls"
- os: osx
name: macOS 10.15
osx_image: xcode12
- os: linux
name: Coverage
before_script:
- cargo install cargo-tarpaulin
script:
- cargo tarpaulin --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml
after_success:
- bash <(curl -s https://codecov.io/bash)
- os: linux
name: wasm32-unknown-unknown
before_script:
- |
set -e
cargo install wasm-bindgen-cli
rustup target add wasm32-unknown-unknown
wget https://github.com/emscripten-core/emsdk/archive/master.zip
unzip master.zip
./emsdk-master/emsdk install latest
./emsdk-master/emsdk activate latest
script:
- |
set -e
source emsdk-master/emsdk_env.sh
cd matrix_sdk/examples/wasm_command_bot
cargo build --target wasm32-unknown-unknown
cd -
cd matrix_sdk_base
cargo test --target wasm32-unknown-unknown --no-default-features
script:
- cargo build
- cargo test
+2
View File
@@ -1,9 +1,11 @@
[workspace]
members = [
"matrix_sdk",
"matrix_qrcode",
"matrix_sdk_base",
"matrix_sdk_test",
"matrix_sdk_test_macros",
"matrix_sdk_crypto",
"matrix_sdk_common",
"matrix_sdk_appservice"
]
+2
View File
@@ -2,6 +2,8 @@
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/master.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
[![Docs - Master](https://img.shields.io/badge/docs-master-blue.svg?style=flat-square)](https://matrix-org.github.io/matrix-rust-sdk/)
[![Docs - Stable](https://img.shields.io/crates/v/matrix-sdk?color=blue&label=docs&style=flat-square)](https://docs.rs/matrix-sdk)
# matrix-rust-sdk
+61
View File
@@ -0,0 +1,61 @@
"""
A mitmproxy script that introduces certain request failures in a deterministic
way.
Used mainly for Matrix style requests.
To run execute it with mitmproxy:
>>> mitmproxy -s failures.py`
"""
import time
import json
import random
from mitmproxy import http
from mitmproxy.script import concurrent
REQUEST_COUNT = 0
def timeout(flow):
timeout = 60 if "sync" in flow.request.pretty_url else 30
time.sleep(timeout)
return None
# A map holding our failure modes.
# The keys are just descriptive names for the failure mode while the values
# hold a tuple containing a function that may or may not create a failure and
# the probability weight at which rate this failure should be triggered.
#
# The method should return an http.HTTPResponse if it should modify the
# response or None if the response should be passed as is.
FAILURES = {
"Success": (lambda x: None, 50),
"Gateway error":
(lambda _: http.HTTPResponse.make(500, b"Gateway error"), 20),
"Limit exeeded": (lambda _: http.HTTPResponse.make(
429,
json.dumps({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
"retry_after_ms": 2000
})), 20),
"Timeout error": (timeout, 10)
}
@concurrent
def request(flow):
global FAILURES
weights = [weight for (_, weight) in FAILURES.values()]
failure = random.choices(list(FAILURES), weights=weights)[0]
failure_func, _ = FAILURES[failure]
response = failure_func(flow)
if response:
flow.response = response
+37
View File
@@ -0,0 +1,37 @@
"""
A mitmproxy script that blocks and removes well known Matrix server
information.
There are two ways a Matrix server can trigger the client to reconfigure the
homeserver URL:
1. By responding to a `./well-known/matrix/client` request with a new
homeserver URL.
2. By including a new homeserver URL inside the `/login` response.
To run execute it with mitmproxy:
>>> mitmproxy -s well-known-block.py`
"""
import json
from mitmproxy import http
def request(flow):
if flow.request.path == "/.well-known/matrix/client":
flow.response = http.HTTPResponse.make(
404, # (optional) status code
b"Not found", # (optional) content
{"Content-Type": "text/html"} # (optional) headers
)
def response(flow: http.HTTPFlow):
if flow.request.path == "/_matrix/client/r0/login":
if flow.response.status_code == 200:
body = json.loads(flow.response.content)
body.pop("well_known", None)
flow.response.text = json.dumps(body)
-100
View File
@@ -1,100 +0,0 @@
# Matrix Rust SDK
## Design and Layout
#### Async Client
The highest level structure that ties the other pieces of functionality together. The client is responsible for the Request/Response cycle. It can be thought of as a thin layer atop the `BaseClient` passing requests along for the `BaseClient` to handle. A user should be able to write their own `AsyncClient` using the `BaseClient`. It knows how to
- login
- send messages
- encryption ...
- sync client state with the server
- make raw Http requests
#### Base Client/Client State Machine
In addition to Http, the `AsyncClient` passes along methods from the `BaseClient` that deal with `Room`s and `RoomMember`s. This allows the client to keep track of more complicated information that needs to be calculated in some way.
- human-readable room names
- power level?
- ignored list?
- push rulesset?
- more?
#### Crypto State Machine
Given a Matrix response the crypto machine will update its own internal state, along with encryption information. `BaseClient` and the crypto machine together keep track of when to encrypt. It knows when encryption needs to happen based on signals from the `BaseClient`. The crypto state machine is given responses that relate to encryption and can create encrypted request bodies for encryption-related requests. Basically it tells the `BaseClient` to send to-device messages out, and the `BaseClient` is responsible for notifying the crypto state machine when it sent the message so crypto can update state.
#### Client State/Room and RoomMember
The `BaseClient` is responsible for keeping state in sync through the `IncomingResponse`s of `AsyncClient` or querying the `StateStore`. By processing and then delegating incoming `RoomEvent`s, `StateEvent`s, `PresenceEvent`, `IncomingAccountData` and `EphemeralEvent`s to the correct `Room` in the base clients `HashMap<RoomId, Room>` or further to `Room`'s `RoomMember` via the members `HashMap<UserId, RoomMember>`. The `BaseClient` is also responsible for emitting the incoming events to the `EventEmitter` trait.
```rust
/// A Matrix room.
pub struct Room {
/// The unique id of the room.
pub room_id: RoomId,
/// The name of the room, clients use this to represent a room.
pub room_name: RoomName,
/// The mxid of our own user.
pub own_user_id: UserId,
/// The mxid of the room creator.
pub creator: Option<UserId>,
/// The map of room members.
pub members: HashMap<UserId, RoomMember>,
/// A list of users that are currently typing.
pub typing_users: Vec<UserId>,
/// The power level requirements for specific actions in this room
pub power_levels: Option<PowerLevels>,
// TODO when encryption events are handled we store algorithm used and rotation time.
/// A flag indicating if the room is encrypted.
pub encrypted: bool,
/// Number of unread notifications with highlight flag set.
pub unread_highlight: Option<UInt>,
/// Number of unread notifications.
pub unread_notifications: Option<UInt>,
}
```
```rust
pub struct RoomMember {
/// The unique mxid of the user.
pub user_id: UserId,
/// The human readable name of the user.
pub display_name: Option<String>,
/// The matrix url of the users avatar.
pub avatar_url: Option<String>,
/// The time, in ms, since the user interacted with the server.
pub last_active_ago: Option<UInt>,
/// If the user should be considered active.
pub currently_active: Option<bool>,
/// The unique id of the room.
pub room_id: Option<String>,
/// If the member is typing.
pub typing: Option<bool>,
/// The presence of the user, if found.
pub presence: Option<PresenceState>,
/// The presence status message, if found.
pub status_msg: Option<String>,
/// The users power level.
pub power_level: Option<Int>,
/// The normalized power level of this `RoomMember` (0-100).
pub power_level_norm: Option<Int>,
/// The `MembershipState` of this `RoomMember`.
pub membership: MembershipState,
/// The human readable name of this room member.
pub name: String,
/// The events that created the state of this room member.
pub events: Vec<Event>,
/// The `PresenceEvent`s connected to this user.
pub presence_events: Vec<PresenceEvent>,
}
```
#### State Store
The `BaseClient` also has access to a `dyn StateStore` this is an abstraction around a "database" to keep the client state without requesting a full sync from the server on startup. A default implementation that serializes/deserializes JSON to files in a specified directory can be used. The user can also implement `StateStore` to fit any storage solution they choose. The base client handles the storage automatically. There "may be/are TODO" ways for the user to interact directly. The room event handling methods signal if the state was modified; if so, we check if some room state file needs to be overwritten.
- open
- load client/rooms
- store client/room
- update ??
The state store will restore our client state in the `BaseClient` and client authors can just get the latest state that they want to present from the client object. No need to ask the state store for it, this may change if custom setups request this. `StateStore`'s main purpose is to provide load/store functionality and, internally to the crate, update the `BaseClient`.
#### Event Emitter
The consumer of this crate can implement the `EventEmitter` trait for full control over how incoming events are handled by their client. If that isn't enough, it is possible to receive every incoming response with the `AsyncClient::sync_forever` callback.
- list the methods for `EventEmitter`?
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "matrix-qrcode"
description = "Library to encode and decode QR codes for interactive verifications in Matrix land"
version = "0.1.0"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = ["decode_image"]
decode_image = ["image", "rqrr", "qrcode/image", "qrcode/svg"]
docs = ["decode_image"]
[dependencies]
base64 = "0.13.0"
byteorder = "1.4.3"
image = { version = "0.23.14", optional = true }
qrcode = { version = "0.12.0", default-features = false }
rqrr = { version = "0.3.2", optional = true }
ruma-identifiers = "0.19.3"
thiserror = "1.0.25"
+62
View File
@@ -0,0 +1,62 @@
[![Build Status](https://img.shields.io/travis/matrix-org/matrix-rust-sdk.svg?style=flat-square)](https://travis-ci.org/matrix-org/matrix-rust-sdk)
[![codecov](https://img.shields.io/codecov/c/github/matrix-org/matrix-rust-sdk/master.svg?style=flat-square)](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-yellowgreen.svg?style=flat-square)](https://opensource.org/licenses/Apache-2.0)
[![#matrix-rust-sdk](https://img.shields.io/badge/matrix-%23matrix--rust--sdk-blue?style=flat-square)](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
# matrix-qrcode
**matrix-qrcode** is a crate to easily generate and parse QR codes for
interactive verification using [QR codes] in Matrix.
[Matrix]: https://matrix.org/
[Rust]: https://www.rust-lang.org/
[QR codes]: https://spec.matrix.org/unstable/client-server-api/#qr-codes
## Usage
This is probably not the crate you are looking for, it's used internally in the
matrix-rust-sdk.
If you still want to play with QR codes, here are a couple of helpful examples.
### Decode an image
```rust
use image;
use matrix_qrcode::{QrVerificationData, DecodingError};
fn main() -> Result<(), DecodingError> {
let image = image::open("/path/to/my/image.png").unwrap();
let result = QrVerificationData::from_image(image)?;
Ok(())
}
```
### Encode into a QR code
```rust
use matrix_qrcode::{QrVerificationData, DecodingError};
use image::Luma;
fn main() -> Result<(), DecodingError> {
let data = b"MATRIX\
\x02\x02\x00\x07\
FLOW_ID\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
SHARED_SECRET";
let data = QrVerificationData::from_bytes(data)?;
let encoded = data.to_qr_code().unwrap();
let image = encoded.render::<Luma<u8>>().build();
Ok(())
}
```
## License
[Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+61
View File
@@ -0,0 +1,61 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use thiserror::Error;
/// Error type describing errors that happen while QR data is being decoded.
#[derive(Error, Debug)]
pub enum DecodingError {
/// Error decoding the QR code.
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
#[error(transparent)]
Qr(#[from] rqrr::DeQRError),
/// The QR code data is missing the mandatory Matrix header.
#[error("the decoded QR code is missing the Matrix header")]
Header,
/// The QR code data is containing an invalid, non UTF-8, flow id.
#[error(transparent)]
Utf8(#[from] std::string::FromUtf8Error),
/// The QR code data is using an unsupported or invalid verification mode.
#[error("the QR code contains an invalid verification mode: {0}")]
Mode(u8),
/// The flow id is not a valid event ID.
#[error(transparent)]
Identifier(#[from] ruma_identifiers::Error),
#[error(transparent)]
/// The QR code data does not contain all the necessary fields.
Read(#[from] std::io::Error),
/// The QR code data uses an invalid shared secret.
#[error("the QR code contains a too short shared secret, length: {0}")]
SharedSecret(usize),
/// The QR code data uses an invalid or unsupported version.
#[error("the QR code contains an invalid or unsupported version: {0}")]
Version(u8),
}
/// Error type describing errors that happen while QR data is being encoded.
#[derive(Error, Debug)]
pub enum EncodingError {
/// Error generating a QR code from the data, likely because the data
/// doesn't fit into a QR code.
#[error(transparent)]
Qr(#[from] qrcode::types::QrError),
/// Error decoding the identity keys as base64.
#[error(transparent)]
Base64(#[from] base64::DecodeError),
/// Error encoding the given flow id, the flow id is too large.
#[error("The verification flow id length can't be converted into a u16: {0}")]
FlowId(#[from] std::num::TryFromIntError),
}
+225
View File
@@ -0,0 +1,225 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! This crate implements methods to parse and generate QR codes that are used
//! for interactive verification in [Matrix](https://matrix.org/).
//!
//! It implements the QR format defined in the Matrix [spec].
//!
//! [spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format
//!
//! ```no_run
//! # use matrix_qrcode::{QrVerificationData, DecodingError};
//! # fn main() -> Result<(), DecodingError> {
//! use image;
//!
//! let image = image::open("/path/to/my/image.png").unwrap();
//! let result = QrVerificationData::from_image(image)?;
//! # Ok(())
//! # }
//! ```
#![cfg_attr(feature = "docs", feature(doc_cfg))]
#![deny(
missing_debug_implementations,
dead_code,
trivial_casts,
missing_docs,
trivial_numeric_casts,
unused_extern_crates,
unused_import_braces,
unused_qualifications
)]
mod error;
mod types;
mod utils;
pub use error::{DecodingError, EncodingError};
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
pub use image;
pub use qrcode;
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
pub use rqrr;
pub use types::{
QrVerificationData, SelfVerificationData, SelfVerificationNoMasterKey, VerificationData,
};
#[cfg(test)]
mod test {
#[cfg(feature = "decode_image")]
use std::{convert::TryFrom, io::Cursor};
#[cfg(feature = "decode_image")]
use image::{ImageFormat, Luma};
#[cfg(feature = "decode_image")]
use qrcode::QrCode;
#[cfg(feature = "decode_image")]
use crate::utils::decode_qr;
use crate::{DecodingError, QrVerificationData};
#[cfg(feature = "decode_image")]
static VERIFICATION: &[u8; 4277] = include_bytes!("../data/verification.png");
#[cfg(feature = "decode_image")]
static SELF_VERIFICATION: &[u8; 1467] = include_bytes!("../data/self-verification.png");
#[cfg(feature = "decode_image")]
static SELF_NO_MASTER: &[u8; 1775] = include_bytes!("../data/self-no-master.png");
#[test]
#[cfg(feature = "decode_image")]
fn decode_qr_test() {
let image = Cursor::new(VERIFICATION);
let image = image::load(image, ImageFormat::Png).unwrap().to_luma8();
decode_qr(image).expect("Couldn't decode the QR code");
}
#[test]
#[cfg(feature = "decode_image")]
fn decode_test() {
let image = Cursor::new(VERIFICATION);
let image = image::load(image, ImageFormat::Png).unwrap().to_luma8();
let result = QrVerificationData::try_from(image).unwrap();
assert!(matches!(result, QrVerificationData::Verification(_)));
}
#[test]
#[cfg(feature = "decode_image")]
fn decode_encode_cycle() {
let image = Cursor::new(VERIFICATION);
let image = image::load(image, ImageFormat::Png).unwrap();
let result = QrVerificationData::from_image(image).unwrap();
assert!(matches!(result, QrVerificationData::Verification(_)));
let encoded = result.to_qr_code().unwrap();
let image = encoded.render::<Luma<u8>>().build();
let second_result = QrVerificationData::try_from(image).unwrap();
assert_eq!(result, second_result);
let bytes = result.to_bytes().unwrap();
let third_result = QrVerificationData::from_bytes(bytes).unwrap();
assert_eq!(result, third_result);
}
#[test]
#[cfg(feature = "decode_image")]
fn decode_encode_cycle_self() {
let image = Cursor::new(SELF_VERIFICATION);
let image = image::load(image, ImageFormat::Png).unwrap();
let result = QrVerificationData::try_from(image).unwrap();
assert!(matches!(result, QrVerificationData::SelfVerification(_)));
let encoded = result.to_qr_code().unwrap();
let image = encoded.render::<Luma<u8>>().build();
let second_result = QrVerificationData::from_luma(image).unwrap();
assert_eq!(result, second_result);
let bytes = result.to_bytes().unwrap();
let third_result = QrVerificationData::from_bytes(bytes).unwrap();
assert_eq!(result, third_result);
}
#[test]
#[cfg(feature = "decode_image")]
fn decode_encode_cycle_self_no_master() {
let image = Cursor::new(SELF_NO_MASTER);
let image = image::load(image, ImageFormat::Png).unwrap();
let result = QrVerificationData::from_image(image).unwrap();
assert!(matches!(result, QrVerificationData::SelfVerificationNoMasterKey(_)));
let encoded = result.to_qr_code().unwrap();
let image = encoded.render::<Luma<u8>>().build();
let second_result = QrVerificationData::try_from(image).unwrap();
assert_eq!(result, second_result);
let bytes = result.to_bytes().unwrap();
let third_result = QrVerificationData::try_from(bytes).unwrap();
assert_eq!(result, third_result);
}
#[test]
#[cfg(feature = "decode_image")]
fn decode_invalid_qr() {
let qr = QrCode::new(b"NonMatrixCode").expect("Can't build a simple QR code");
let image = qr.render::<Luma<u8>>().build();
let result = QrVerificationData::try_from(image);
assert!(matches!(result, Err(DecodingError::Header)))
}
#[test]
fn decode_invalid_header() {
let data = b"NonMatrixCode";
let result = QrVerificationData::from_bytes(data);
assert!(matches!(result, Err(DecodingError::Header)))
}
#[test]
fn decode_invalid_mode() {
let data = b"MATRIX\x02\x03";
let result = QrVerificationData::from_bytes(data);
assert!(matches!(result, Err(DecodingError::Mode(3))))
}
#[test]
fn decode_invalid_version() {
let data = b"MATRIX\x01\x03";
let result = QrVerificationData::from_bytes(data);
assert!(matches!(result, Err(DecodingError::Version(1))))
}
#[test]
fn decode_missing_data() {
let data = b"MATRIX\x02\x02";
let result = QrVerificationData::from_bytes(data);
assert!(matches!(result, Err(DecodingError::Read(_))))
}
#[test]
fn decode_short_secret() {
let data = b"MATRIX\
\x02\x02\x00\x07\
FLOW_ID\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
SECRET";
let result = QrVerificationData::from_bytes(data);
assert!(matches!(result, Err(DecodingError::SharedSecret(_))))
}
#[test]
fn decode_invalid_room_id() {
let data = b"MATRIX\
\x02\x00\x00\x0f\
test:localhost\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
SECRETISLONGENOUGH";
let result = QrVerificationData::from_bytes(data);
assert!(matches!(result, Err(DecodingError::Identifier(_))))
}
}
+672
View File
@@ -0,0 +1,672 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
convert::TryFrom,
io::{Cursor, Read},
};
use byteorder::{BigEndian, ReadBytesExt};
#[cfg(feature = "decode_image")]
use image::{DynamicImage, ImageBuffer, Luma};
use qrcode::QrCode;
use ruma_identifiers::EventId;
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
use crate::utils::decode_qr;
use crate::{
error::{DecodingError, EncodingError},
utils::{base_64_encode, to_bytes, to_qr_code, HEADER, MAX_MODE, MIN_SECRET_LEN, VERSION},
};
/// An enum representing the different modes a QR verification can be in.
#[derive(Clone, Debug, PartialEq)]
pub enum QrVerificationData {
/// The QR verification is verifying another user
Verification(VerificationData),
/// The QR verification is self-verifying and the current device trusts or
/// owns the master key
SelfVerification(SelfVerificationData),
/// The QR verification is self-verifying in which the current device does
/// not yet trust the master key
SelfVerificationNoMasterKey(SelfVerificationNoMasterKey),
}
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
impl TryFrom<DynamicImage> for QrVerificationData {
type Error = DecodingError;
fn try_from(image: DynamicImage) -> Result<Self, Self::Error> {
Self::from_image(image)
}
}
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
impl TryFrom<ImageBuffer<Luma<u8>, Vec<u8>>> for QrVerificationData {
type Error = DecodingError;
fn try_from(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<Self, Self::Error> {
Self::from_luma(image)
}
}
impl TryFrom<&[u8]> for QrVerificationData {
type Error = DecodingError;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::from_bytes(value)
}
}
impl TryFrom<Vec<u8>> for QrVerificationData {
type Error = DecodingError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Self::from_bytes(value)
}
}
impl QrVerificationData {
/// Decode and parse an image of a QR code into a `QrVerificationData`
///
/// The image will be converted into a grey scale image before decoding is
/// attempted
///
/// # Arguments
///
/// * `image` - The image containing the QR code.
///
/// # Example
/// ```no_run
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// use image;
///
/// let image = image::open("/path/to/my/image.png").unwrap();
/// let result = QrVerificationData::from_image(image)?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
pub fn from_image(image: DynamicImage) -> Result<Self, DecodingError> {
let image = image.to_luma8();
Self::decode(image)
}
/// Decode and parse an grey scale image of a QR code into a
/// `QrVerificationData`
///
/// # Arguments
///
/// * `image` - The grey scale image containing the QR code.
///
/// # Example
/// ```no_run
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// use image;
///
/// let image = image::open("/path/to/my/image.png").unwrap();
/// let image = image.to_luma8();
/// let result = QrVerificationData::from_luma(image)?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "decode_image")]
#[cfg_attr(feature = "docs", doc(cfg(decode_image)))]
pub fn from_luma(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<Self, DecodingError> {
Self::decode(image)
}
/// Parse the decoded payload of a QR code in byte slice form as a
/// `QrVerificationData`
///
/// This method is useful if you would like to do your own custom QR code
/// decoding.
///
/// # Arguments
///
/// * `bytes` - The raw bytes of a decoded QR code.
///
/// # Example
/// ```
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// let data = b"MATRIX\
/// \x02\x02\x00\x07\
/// FLOW_ID\
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
/// SHARED_SECRET";
///
/// let result = QrVerificationData::from_bytes(data)?;
/// # Ok(())
/// # }
/// ```
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, DecodingError> {
Self::decode_bytes(bytes)
}
/// Encode the `QrVerificationData` into a `QrCode`.
///
/// This method turns the `QrVerificationData` into a QR code that can be
/// rendered and presented to be scanned.
///
/// The encoding can fail if the data doesn't fit into a QR code or if the
/// identity keys that should be encoded into the QR code are not valid
/// base64.
///
/// # Example
/// ```
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// let data = b"MATRIX\
/// \x02\x02\x00\x07\
/// FLOW_ID\
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
/// SHARED_SECRET";
///
/// let result = QrVerificationData::from_bytes(data)?;
/// let encoded = result.to_qr_code().unwrap();
/// # Ok(())
/// # }
/// ```
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
match self {
QrVerificationData::Verification(v) => v.to_qr_code(),
QrVerificationData::SelfVerification(v) => v.to_qr_code(),
QrVerificationData::SelfVerificationNoMasterKey(v) => v.to_qr_code(),
}
}
/// Encode the `QrVerificationData` into a vector of bytes that can be
/// encoded as a QR code.
///
/// The encoding can fail if the identity keys that should be encoded are
/// not valid base64.
///
/// # Example
/// ```
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// let data = b"MATRIX\
/// \x02\x02\x00\x07\
/// FLOW_ID\
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
/// SHARED_SECRET";
///
/// let result = QrVerificationData::from_bytes(data)?;
/// let encoded = result.to_bytes().unwrap();
///
/// assert_eq!(data.as_ref(), encoded.as_slice());
/// # Ok(())
/// # }
/// ```
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
match self {
QrVerificationData::Verification(v) => v.to_bytes(),
QrVerificationData::SelfVerification(v) => v.to_bytes(),
QrVerificationData::SelfVerificationNoMasterKey(v) => v.to_bytes(),
}
}
/// Decode the byte slice containing the decoded QR code data.
///
/// The format is defined in the [spec].
///
/// The byte slice consists of the following parts:
///
/// * the ASCII string MATRIX
/// * one byte indicating the QR code version (must be 0x02)
/// * one byte indicating the QR code verification mode. one of the
/// following
/// values:
/// * 0x00 verifying another user with cross-signing
/// * 0x01 self-verifying in which the current device does trust the
/// master key
/// * 0x02 self-verifying in which the current device does not yet trust
/// the master key
/// * the event ID or transaction_id of the associated verification request
/// event, encoded as:
/// * two bytes in network byte order (big-endian) indicating the length
/// in bytes of the ID as a UTF-8 string
/// * the ID as a UTF-8 string
/// * the first key, as 32 bytes
/// * the second key, as 32 bytes
/// * a random shared secret, as a byte string. as we do not share the
/// length of the secret, and it is not a fixed size, clients will just
/// use the remainder of binary string as the shared secret.
///
/// [spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format
fn decode_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, DecodingError> {
let mut decoded = Cursor::new(bytes);
let mut header = [0u8; 6];
let mut first_key = [0u8; 32];
let mut second_key = [0u8; 32];
decoded.read_exact(&mut header)?;
let version = decoded.read_u8()?;
let mode = decoded.read_u8()?;
if header != HEADER {
return Err(DecodingError::Header);
} else if version != VERSION {
return Err(DecodingError::Version(version));
} else if mode > MAX_MODE {
return Err(DecodingError::Mode(mode));
}
let flow_id_len = decoded.read_u16::<BigEndian>()?;
let mut flow_id = vec![0; flow_id_len.into()];
decoded.read_exact(&mut flow_id)?;
decoded.read_exact(&mut first_key)?;
decoded.read_exact(&mut second_key)?;
let mut shared_secret = Vec::new();
decoded.read_to_end(&mut shared_secret)?;
if shared_secret.len() < MIN_SECRET_LEN {
return Err(DecodingError::SharedSecret(shared_secret.len()));
}
QrVerificationData::new(mode, flow_id, first_key, second_key, shared_secret)
}
/// Decode the given image of an QR code and if we find a valid code, try to
/// decode it as a `QrVerification`.
#[cfg(feature = "decode_image")]
fn decode(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<QrVerificationData, DecodingError> {
let decoded = decode_qr(image)?;
Self::decode_bytes(decoded)
}
fn new(
mode: u8,
flow_id: Vec<u8>,
first_key: [u8; 32],
second_key: [u8; 32],
shared_secret: Vec<u8>,
) -> Result<Self, DecodingError> {
let first_key = base_64_encode(&first_key);
let second_key = base_64_encode(&second_key);
let flow_id = String::from_utf8(flow_id)?;
let shared_secret = base_64_encode(&shared_secret);
match mode {
VerificationData::QR_MODE => {
let event_id = EventId::try_from(flow_id)?;
Ok(VerificationData::new(event_id, first_key, second_key, shared_secret).into())
}
SelfVerificationData::QR_MODE => {
Ok(SelfVerificationData::new(flow_id, first_key, second_key, shared_secret).into())
}
SelfVerificationNoMasterKey::QR_MODE => {
Ok(SelfVerificationNoMasterKey::new(flow_id, first_key, second_key, shared_secret)
.into())
}
m => Err(DecodingError::Mode(m)),
}
}
/// Get the flow id for this `QrVerificationData`.
///
/// This represents the ID as a string even if it is a `EventId`.
pub fn flow_id(&self) -> &str {
match self {
QrVerificationData::Verification(v) => v.event_id.as_str(),
QrVerificationData::SelfVerification(v) => &v.transaction_id,
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.transaction_id,
}
}
/// Get the first key of this `QrVerificationData`.
pub fn first_key(&self) -> &str {
match self {
QrVerificationData::Verification(v) => &v.first_master_key,
QrVerificationData::SelfVerification(v) => &v.master_key,
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.device_key,
}
}
/// Get the second key of this `QrVerificationData`.
pub fn second_key(&self) -> &str {
match self {
QrVerificationData::Verification(v) => &v.second_master_key,
QrVerificationData::SelfVerification(v) => &v.device_key,
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.master_key,
}
}
/// Get the secret of this `QrVerificationData`.
pub fn secret(&self) -> &str {
match self {
QrVerificationData::Verification(v) => &v.shared_secret,
QrVerificationData::SelfVerification(v) => &v.shared_secret,
QrVerificationData::SelfVerificationNoMasterKey(v) => &v.shared_secret,
}
}
}
/// The non-encoded data for the first mode of QR code verification.
///
/// This mode is used for verification between two users using their master
/// cross signing keys.
#[derive(Clone, Debug, PartialEq)]
pub struct VerificationData {
event_id: EventId,
first_master_key: String,
second_master_key: String,
shared_secret: String,
}
impl VerificationData {
const QR_MODE: u8 = 0x00;
/// Create a new `VerificationData` struct that can be encoded as a QR code.
///
/// # Arguments
/// * `event_id` - The event id of the `m.key.verification.request` event
/// that initiated the verification flow this QR code should be part of.
///
/// * `first_key` - Our own cross signing master key. Needs to be encoded as
/// unpadded base64
///
/// * `second_key` - The cross signing master key of the other user.
///
/// * ` shared_secret` - A random bytestring encoded as unpadded base64,
/// needs to be at least 8 bytes long.
pub fn new(
event_id: EventId,
first_key: String,
second_key: String,
shared_secret: String,
) -> Self {
Self { event_id, first_master_key: first_key, second_master_key: second_key, shared_secret }
}
/// Encode the `VerificationData` into a vector of bytes that can be
/// encoded as a QR code.
///
/// The encoding can fail if the master keys that should be encoded are not
/// valid base64.
///
/// # Example
/// ```
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// let data = b"MATRIX\
/// \x02\x00\x00\x0f\
/// $test:localhost\
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
/// SHARED_SECRET";
///
/// let result = QrVerificationData::from_bytes(data)?;
/// if let QrVerificationData::Verification(decoded) = result {
/// let encoded = decoded.to_bytes().unwrap();
/// assert_eq!(data.as_ref(), encoded.as_slice());
/// } else {
/// panic!("Data was encoded as an incorrect mode");
/// }
/// # Ok(())
/// # }
/// ```
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
to_bytes(
Self::QR_MODE,
self.event_id.as_str(),
&self.first_master_key,
&self.second_master_key,
&self.shared_secret,
)
}
/// Encode the `VerificationData` into a `QrCode`.
///
/// This method turns the `VerificationData` into a QR code that can be
/// rendered and presented to be scanned.
///
/// The encoding can fail if the data doesn't fit into a QR code or if the
/// keys that should be encoded into the QR code are not valid base64.
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
to_qr_code(
Self::QR_MODE,
self.event_id.as_str(),
&self.first_master_key,
&self.second_master_key,
&self.shared_secret,
)
}
}
impl From<VerificationData> for QrVerificationData {
fn from(data: VerificationData) -> Self {
Self::Verification(data)
}
}
/// The non-encoded data for the second mode of QR code verification.
///
/// This mode is used for verification between two devices of the same user
/// where this device, that is creating this QR code, is trusting or owning
/// the cross signing master key.
#[derive(Clone, Debug, PartialEq)]
pub struct SelfVerificationData {
transaction_id: String,
master_key: String,
device_key: String,
shared_secret: String,
}
impl SelfVerificationData {
const QR_MODE: u8 = 0x01;
/// Create a new `SelfVerificationData` struct that can be encoded as a QR
/// code.
///
/// # Arguments
/// * `transaction_id` - The transaction id of this verification flow, the
/// transaction id was sent by the `m.key.verification.request` event
/// that initiated the verification flow this QR code should be part of.
///
/// * `master_key` - Our own cross signing master key. Needs to be encoded
/// as
/// unpadded base64
///
/// * `device_key` - The ed25519 key of the other device, encoded as
/// unpadded base64.
///
/// * ` shared_secret` - A random bytestring encoded as unpadded base64,
/// needs to be at least 8 bytes long.
pub fn new(
transaction_id: String,
master_key: String,
device_key: String,
shared_secret: String,
) -> Self {
Self { transaction_id, master_key, device_key, shared_secret }
}
/// Encode the `SelfVerificationData` into a vector of bytes that can be
/// encoded as a QR code.
///
/// The encoding can fail if the keys that should be encoded are not valid
/// base64.
///
/// # Example
/// ```
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// let data = b"MATRIX\
/// \x02\x01\x00\x06\
/// FLOWID\
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
/// SHARED_SECRET";
///
/// let result = QrVerificationData::from_bytes(data)?;
/// if let QrVerificationData::SelfVerification(decoded) = result {
/// let encoded = decoded.to_bytes().unwrap();
/// assert_eq!(data.as_ref(), encoded.as_slice());
/// } else {
/// panic!("Data was encoded as an incorrect mode");
/// }
/// # Ok(())
/// # }
/// ```
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
to_bytes(
Self::QR_MODE,
&self.transaction_id,
&self.master_key,
&self.device_key,
&self.shared_secret,
)
}
/// Encode the `SelfVerificationData` into a `QrCode`.
///
/// This method turns the `SelfVerificationData` into a QR code that can be
/// rendered and presented to be scanned.
///
/// The encoding can fail if the data doesn't fit into a QR code or if the
/// keys that should be encoded into the QR code are not valid base64.
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
to_qr_code(
Self::QR_MODE,
&self.transaction_id,
&self.master_key,
&self.device_key,
&self.shared_secret,
)
}
}
impl From<SelfVerificationData> for QrVerificationData {
fn from(data: SelfVerificationData) -> Self {
Self::SelfVerification(data)
}
}
/// The non-encoded data for the third mode of QR code verification.
///
/// This mode is used for verification between two devices of the same user
/// where this device, that is creating this QR code, is not trusting the
/// cross signing master key.
#[derive(Clone, Debug, PartialEq)]
pub struct SelfVerificationNoMasterKey {
transaction_id: String,
device_key: String,
master_key: String,
shared_secret: String,
}
impl SelfVerificationNoMasterKey {
const QR_MODE: u8 = 0x02;
/// Create a new `SelfVerificationData` struct that can be encoded as a QR
/// code.
///
/// # Arguments
/// * `transaction_id` - The transaction id of this verification flow, the
/// transaction id was sent by the `m.key.verification.request` event
/// that initiated the verification flow this QR code should be part of.
///
/// * `device_key` - The ed25519 key of our own device, encoded as unpadded
/// base64.
///
/// * `master_key` - Our own cross signing master key. Needs to be encoded
/// as
/// unpadded base64
///
/// * ` shared_secret` - A random bytestring encoded as unpadded base64,
/// needs to be at least 8 bytes long.
pub fn new(
transaction_id: String,
device_key: String,
master_key: String,
shared_secret: String,
) -> Self {
Self { transaction_id, device_key, master_key, shared_secret }
}
/// Encode the `SelfVerificationNoMasterKey` into a vector of bytes that can
/// be encoded as a QR code.
///
/// The encoding can fail if the keys that should be encoded are not valid
/// base64.
///
/// # Example
/// ```
/// # use matrix_qrcode::{QrVerificationData, DecodingError};
/// # fn main() -> Result<(), DecodingError> {
/// let data = b"MATRIX\
/// \x02\x02\x00\x06\
/// FLOWID\
/// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
/// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\
/// SHARED_SECRET";
///
/// let result = QrVerificationData::from_bytes(data)?;
/// if let QrVerificationData::SelfVerificationNoMasterKey(decoded) = result {
/// let encoded = decoded.to_bytes().unwrap();
/// assert_eq!(data.as_ref(), encoded.as_slice());
/// } else {
/// panic!("Data was encoded as an incorrect mode");
/// }
/// # Ok(())
/// # }
/// ```
pub fn to_bytes(&self) -> Result<Vec<u8>, EncodingError> {
to_bytes(
Self::QR_MODE,
&self.transaction_id,
&self.device_key,
&self.master_key,
&self.shared_secret,
)
}
/// Encode the `SelfVerificationNoMasterKey` into a `QrCode`.
///
/// This method turns the `SelfVerificationNoMasterKey` into a QR code that
/// can be rendered and presented to be scanned.
///
/// The encoding can fail if the data doesn't fit into a QR code or if the
/// keys that should be encoded into the QR code are not valid base64.
pub fn to_qr_code(&self) -> Result<QrCode, EncodingError> {
to_qr_code(
Self::QR_MODE,
&self.transaction_id,
&self.device_key,
&self.master_key,
&self.shared_secret,
)
}
}
impl From<SelfVerificationNoMasterKey> for QrVerificationData {
fn from(data: SelfVerificationNoMasterKey) -> Self {
Self::SelfVerificationNoMasterKey(data)
}
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryInto;
use base64::{decode_config, encode_config, STANDARD_NO_PAD};
#[cfg(feature = "decode_image")]
use image::{ImageBuffer, Luma};
use qrcode::{bits::Bits, EcLevel, QrCode, Version};
#[cfg(feature = "decode_image")]
use crate::error::DecodingError;
use crate::error::EncodingError;
pub(crate) const HEADER: &[u8] = b"MATRIX";
pub(crate) const VERSION: u8 = 0x2;
pub(crate) const MAX_MODE: u8 = 0x2;
pub(crate) const MIN_SECRET_LEN: usize = 8;
pub(crate) fn base_64_encode(data: &[u8]) -> String {
encode_config(data, STANDARD_NO_PAD)
}
pub(crate) fn base64_decode(data: &str) -> Result<Vec<u8>, base64::DecodeError> {
decode_config(data, STANDARD_NO_PAD)
}
pub(crate) fn to_bytes(
mode: u8,
flow_id: &str,
first_key: &str,
second_key: &str,
shared_secret: &str,
) -> Result<Vec<u8>, EncodingError> {
let flow_id_len: u16 = flow_id.len().try_into()?;
let flow_id_len = flow_id_len.to_be_bytes();
let first_key = base64_decode(first_key)?;
let second_key = base64_decode(second_key)?;
let shared_secret = base64_decode(shared_secret)?;
let data = [
HEADER,
&[VERSION],
&[mode],
flow_id_len.as_ref(),
flow_id.as_bytes(),
&first_key,
&second_key,
&shared_secret,
]
.concat();
Ok(data)
}
pub(crate) fn to_qr_code(
mode: u8,
flow_id: &str,
first_key: &str,
second_key: &str,
shared_secret: &str,
) -> Result<QrCode, EncodingError> {
let data = to_bytes(mode, flow_id, first_key, second_key, shared_secret)?;
// Mobile clients seem to have trouble decoding the QR code that gets
// generated by `QrCode::new()` it seems to add a couple of data segments
// with different data modes/types. The parsers seem to assume a single
// data type and since we start with an ASCII `MATRIX` header the rest of
// the data gets treated as a string as well.
//
// We make sure that there isn't an ECI bit set and we just push the bytes,
// this seems to help since the decoder doesn't assume an encoding and
// treats everything as raw bytes.
let mut bits = Bits::new(Version::Normal(7));
bits.push_byte_data(&data)?;
bits.push_terminator(EcLevel::L)?;
Ok(QrCode::with_bits(bits, EcLevel::L)?)
}
#[cfg(feature = "decode_image")]
pub(crate) fn decode_qr(image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Result<Vec<u8>, DecodingError> {
let mut image = rqrr::PreparedImage::prepare(image);
let grids = image.detect_grids();
let mut error = None;
for grid in grids {
let mut decoded = Vec::new();
match grid.decode_to(&mut decoded) {
Ok(_) => {
if decoded.starts_with(HEADER) {
return Ok(decoded);
}
}
Err(e) => error = Some(e),
}
}
Err(error.map(|e| e.into()).unwrap_or_else(|| DecodingError::Header))
}
+51 -29
View File
@@ -8,48 +8,72 @@ license = "Apache-2.0"
name = "matrix-sdk"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.2.0"
version = "0.3.0"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = ["encryption", "sqlite_cryptostore", "messages", "native-tls"]
default = ["encryption", "sled_cryptostore", "sled_state_store", "require_auth_for_profile_requests", "native-tls"]
messages = ["matrix-sdk-base/messages"]
encryption = ["matrix-sdk-base/encryption", "dashmap"]
sqlite_cryptostore = ["matrix-sdk-base/sqlite_cryptostore"]
unstable-synapse-quirks = ["matrix-sdk-base/unstable-synapse-quirks"]
encryption = ["matrix-sdk-base/encryption"]
sled_state_store = ["matrix-sdk-base/sled_state_store"]
sled_cryptostore = ["matrix-sdk-base/sled_cryptostore"]
markdown = ["matrix-sdk-base/markdown"]
native-tls = ["reqwest/native-tls"]
rustls-tls = ["reqwest/rustls-tls"]
socks = ["reqwest/socks"]
sso_login = ["warp", "rand", "tokio-stream"]
require_auth_for_profile_requests = []
appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper", "ruma/rand"]
docs = ["encryption", "sqlite_cryptostore", "messages"]
docs = ["encryption", "sled_cryptostore", "sled_state_store", "sso_login"]
[dependencies]
dashmap = { version = "4.0.1", optional = true }
http = "0.2.2"
serde_json = "1.0.61"
thiserror = "1.0.23"
tracing = "0.1.22"
url = "2.2.0"
zeroize = "1.2.0"
dashmap = "4.0.2"
futures = "0.3.15"
http = "0.2.4"
serde_json = "1.0.64"
thiserror = "1.0.25"
tracing = "0.1.26"
url = "2.2.2"
zeroize = "1.3.0"
mime = "0.3.16"
rand = { version = "0.8.4", optional = true }
bytes = "1.0.1"
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
matrix-sdk-common = { version = "0.3.0", path = "../matrix_sdk_common" }
[dependencies.matrix-sdk-base]
version = "0.2.0"
version = "0.3.0"
path = "../matrix_sdk_base"
default_features = false
[dependencies.reqwest]
version = "0.10.10"
version = "0.11.3"
default_features = false
[dependencies.ruma]
version = "0.2.0"
features = ["client-api-c", "compat", "unstable-pre-spec"]
[dependencies.tokio-stream]
version = "0.1.6"
features = ["net"]
optional = true
[dependencies.warp]
version = "0.3.1"
default-features = false
optional = true
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.backoff]
version = "0.3.0"
features = ["tokio"]
[dependencies.tracing-futures]
version = "0.2.4"
version = "0.2.5"
default-features = false
features = ["std", "std-future"]
@@ -57,25 +81,23 @@ features = ["std", "std-future"]
futures-timer = "3.0.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.24"
version = "1.7.1"
default-features = false
features = ["fs", "blocking"]
features = ["fs", "rt"]
[target.'cfg(target_arch = "wasm32")'.dependencies.futures-timer]
version = "3.0.2"
features = ["wasm-bindgen"]
[dev-dependencies]
async-std = { version = "1.8.0", features = ["unstable"] }
dirs = "3.0.1"
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
serde_json = "1.0.61"
tracing-subscriber = "0.2.15"
tempfile = "3.1.0"
mockito = "0.28.0"
dirs = "3.0.2"
matrix-sdk-test = { version = "0.3.0", path = "../matrix_sdk_test" }
tokio = { version = "1.7.1", default-features = false, features = ["rt-multi-thread", "macros"] }
serde_json = "1.0.64"
tracing-subscriber = "0.2.18"
tempfile = "3.2.0"
mockito = "0.30.0"
lazy_static = "1.4.0"
futures = "0.3.8"
[[example]]
name = "emoji_verification"
+17 -19
View File
@@ -1,11 +1,12 @@
use std::{env, process::exit};
use tokio::time::{delay_for, Duration};
use matrix_sdk::{
self, async_trait,
events::{room::member::MemberEventContent, StrippedStateEvent},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
room::Room,
Client, ClientConfig, EventHandler, SyncSettings,
};
use tokio::time::{sleep, Duration};
use url::Url;
struct AutoJoinBot {
@@ -19,10 +20,10 @@ impl AutoJoinBot {
}
#[async_trait]
impl EventEmitter for AutoJoinBot {
impl EventHandler for AutoJoinBot {
async fn on_stripped_state_member(
&self,
room: SyncRoom,
room: Room,
room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
@@ -30,29 +31,30 @@ impl EventEmitter for AutoJoinBot {
return;
}
if let SyncRoom::Invited(room) = room {
let room = room.read().await;
println!("Autojoining room {}", room.room_id);
if let Room::Invited(room) = room {
println!("Autojoining room {}", room.room_id());
let mut delay = 2;
while let Err(err) = self.client.join_room_by_id(&room.room_id).await {
while let Err(err) = room.accept_invitation().await {
// retry autojoin due to synapse sending invites, before the
// invited user can join for more information see
// https://github.com/matrix-org/synapse/issues/4345
eprintln!(
"Failed to join room {} ({:?}), retrying in {}s",
room.room_id, err, delay
room.room_id(),
err,
delay
);
delay_for(Duration::from_secs(delay)).await;
sleep(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
eprintln!("Can't join room {} ({:?})", room.room_id, err);
eprintln!("Can't join room {} ({:?})", room.room_id(), err);
break;
}
}
println!("Successfully joined room {}", room.room_id);
println!("Successfully joined room {}", room.room_id());
}
}
}
@@ -68,17 +70,13 @@ async fn login_and_sync(
let client_config = ClientConfig::new().store_path(home);
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
client
.login(username, password, None, Some("autojoin bot"))
.await?;
client.login(username, password, None, Some("autojoin bot")).await?;
println!("logged in as {}", username);
client
.add_event_emitter(Box::new(AutoJoinBot::new(client.clone())))
.await;
client.set_event_handler(Box::new(AutoJoinBot::new(client.clone()))).await;
client.sync(SyncSettings::default()).await;
+30 -41
View File
@@ -3,54 +3,50 @@ use std::{env, process::exit};
use matrix_sdk::{
self, async_trait,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
AnyMessageEventContent, SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings,
room::Room,
Client, ClientConfig, EventHandler, SyncSettings,
};
use url::Url;
struct CommandBot {
/// This clone of the `Client` will send requests to the server,
/// while the other keeps us in sync with the server using `sync`.
client: Client,
}
struct CommandBot;
impl CommandBot {
pub fn new(client: Client) -> Self {
Self { client }
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl EventEmitter for CommandBot {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
impl EventHandler for CommandBot {
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
if let Room::Joined(room) = room {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
content:
MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
} = event
{
msg_body.clone()
msg_body
} else {
String::new()
return;
};
if msg_body.contains("!party") {
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::text_plain(
"🎉🎊🥳 let's PARTY!! 🥳🎊🎉",
));
// we clone here to hold the lock for as little time as possible.
let room_id = room.read().await.room_id.clone();
println!("sending");
self.client
// send our message to the room we found the "!party" command in
// the last parameter is an optional Uuid which we don't care about.
.room_send(&room_id, content, None)
.await
.unwrap();
// send our message to the room we found the "!party" command in
// the last parameter is an optional Uuid which we don't care about.
room.send(content, None).await.unwrap();
println!("message sent");
}
@@ -67,36 +63,29 @@ async fn login_and_sync(
let mut home = dirs::home_dir().expect("no home directory found");
home.push("party_bot");
let store = JsonStore::open(&home)?;
let client_config = ClientConfig::new()
.proxy("http://localhost:8080")?
.disable_ssl_verification()
.state_store(Box::new(store));
let client_config = ClientConfig::new().store_path(home);
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
// create a new Client with the given homeserver url and config
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
client
.login(&username, &password, None, Some("command bot"))
.await?;
client.login(&username, &password, None, Some("command bot")).await?;
println!("logged in as {}", username);
// An initial sync to set up state and so our bot doesn't respond to old messages.
// If the `StateStore` finds saved state in the location given the initial sync will
// be skipped in favor of loading state from the store
// An initial sync to set up state and so our bot doesn't respond to old
// messages. If the `StateStore` finds saved state in the location given the
// initial sync will be skipped in favor of loading state from the store
client.sync_once(SyncSettings::default()).await.unwrap();
// add our CommandBot to be notified of incoming messages, we do this after the initial
// sync to avoid responding to messages before the bot was running.
client
.add_event_emitter(Box::new(CommandBot::new(client.clone())))
.await;
// add our CommandBot to be notified of incoming messages, we do this after the
// initial sync to avoid responding to messages before the bot was running.
client.set_event_handler(Box::new(CommandBot::new())).await;
// since we called `sync_once` before we entered our sync loop we must pass
// that sync token to `sync`
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
// this keeps state from the server streaming in to CommandBot via the EventEmitter trait
// this keeps state from the server streaming in to CommandBot via the
// EventHandler trait
client.sync(settings).await;
Ok(())
+8 -25
View File
@@ -5,14 +5,12 @@ use std::{
sync::atomic::{AtomicBool, Ordering},
};
use matrix_sdk::{
self, api::r0::uiaa::AuthData, identifiers::UserId, Client, LoopCtrl, SyncSettings,
};
use serde_json::json;
use url::Url;
use matrix_sdk::{
self, api::r0::uiaa::AuthData, identifiers::UserId, Client, ClientConfig, LoopCtrl,
SyncSettings,
};
fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> {
let mut auth_parameters = BTreeMap::new();
let identifier = json!({
@@ -23,14 +21,7 @@ fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> Aut
auth_parameters.insert("identifier".to_owned(), identifier);
auth_parameters.insert("password".to_owned(), password.to_owned().into());
// This is needed because of https://github.com/matrix-org/synapse/issues/5665
auth_parameters.insert("user".to_owned(), user.as_str().into());
AuthData::DirectRequest {
kind: "m.login.password",
auth_parameters,
session,
}
AuthData::DirectRequest { kind: "m.login.password", auth_parameters, session }
}
async fn bootstrap(client: Client, user_id: UserId, password: String) {
@@ -38,9 +29,7 @@ async fn bootstrap(client: Client, user_id: UserId, password: String) {
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("error: unable to read user input");
io::stdin().read_line(&mut input).expect("error: unable to read user input");
#[cfg(feature = "encryption")]
if let Err(e) = client.bootstrap_cross_signing(None).await {
@@ -51,7 +40,7 @@ async fn bootstrap(client: Client, user_id: UserId, password: String) {
.await
.expect("Couldn't bootstrap cross signing")
} else {
panic!("Error durign cross signing bootstrap {:#?}", e);
panic!("Error during cross-signing bootstrap {:#?}", e);
}
}
@@ -64,16 +53,10 @@ async fn login(
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.disable_ssl_verification()
.proxy("http://localhost:8080")
.unwrap();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new(homeserver_url).unwrap();
let response = client
.login(username, password, None, Some("rust-sdk"))
.await?;
let response = client.login(username, password, None, Some("rust-sdk")).await?;
let user_id = &response.user_id;
let client_ref = &client;
+104 -50
View File
@@ -1,18 +1,26 @@
use std::{env, io, process::exit};
use url::Url;
use matrix_sdk::{
self, events::AnyToDeviceEvent, identifiers::UserId, Client, ClientConfig, LoopCtrl, Sas,
SyncSettings,
use std::{
env, io,
process::exit,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
async fn wait_for_confirmation(client: Client, sas: Sas) {
use matrix_sdk::{
self,
events::{room::message::MessageType, AnySyncMessageEvent, AnySyncRoomEvent, AnyToDeviceEvent},
identifiers::UserId,
verification::{SasVerification, Verification},
Client, LoopCtrl, SyncSettings,
};
use url::Url;
async fn wait_for_confirmation(client: Client, sas: SasVerification) {
println!("Does the emoji match: {:?}", sas.emoji());
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("error: unable to read user input");
io::stdin().read_line(&mut input).expect("error: unable to read user input");
match input.trim().to_lowercase().as_ref() {
"yes" | "true" | "ok" => {
@@ -27,7 +35,7 @@ async fn wait_for_confirmation(client: Client, sas: Sas) {
}
}
fn print_result(sas: &Sas) {
fn print_result(sas: &SasVerification) {
let device = sas.other_device();
println!(
@@ -56,61 +64,52 @@ async fn login(
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.disable_ssl_verification()
.proxy("http://localhost:8080")
.unwrap();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new(homeserver_url).unwrap();
client
.login(username, password, None, Some("rust-sdk"))
.await?;
client.login(username, password, None, Some("rust-sdk")).await?;
let client_ref = &client;
let initial_sync = Arc::new(AtomicBool::from(true));
let initial_ref = &initial_sync;
client
.sync_with_callback(SyncSettings::new(), |response| async move {
let client = &client_ref;
let initial = &initial_ref;
for event in &response.to_device.events {
let e = event
.deserialize()
.expect("Can't deserialize to-device event");
match e {
for event in response.to_device.events.iter().filter_map(|e| e.deserialize().ok()) {
match event {
AnyToDeviceEvent::KeyVerificationStart(e) => {
let sas = client
.get_verification(&e.content.transaction_id)
.await
.expect("Sas object wasn't created");
println!(
"Starting verification with {} {}",
&sas.other_device().user_id(),
&sas.other_device().device_id()
);
print_devices(&e.sender, &client).await;
sas.accept().await.unwrap();
if let Some(Verification::SasV1(sas)) =
client.get_verification(&e.sender, &e.content.transaction_id).await
{
println!(
"Starting verification with {} {}",
&sas.other_device().user_id(),
&sas.other_device().device_id()
);
print_devices(&e.sender, client).await;
sas.accept().await.unwrap();
}
}
AnyToDeviceEvent::KeyVerificationKey(e) => {
let sas = client
.get_verification(&e.content.transaction_id)
.await
.expect("Sas object wasn't created");
tokio::spawn(wait_for_confirmation((*client).clone(), sas));
if let Some(Verification::SasV1(sas)) =
client.get_verification(&e.sender, &e.content.transaction_id).await
{
tokio::spawn(wait_for_confirmation((*client).clone(), sas));
}
}
AnyToDeviceEvent::KeyVerificationMac(e) => {
let sas = client
.get_verification(&e.content.transaction_id)
.await
.expect("Sas object wasn't created");
if sas.is_done() {
print_result(&sas);
print_devices(&e.sender, &client).await;
if let Some(Verification::SasV1(sas)) =
client.get_verification(&e.sender, &e.content.transaction_id).await
{
if sas.is_done() {
print_result(&sas);
print_devices(&e.sender, client).await;
}
}
}
@@ -118,6 +117,61 @@ async fn login(
}
}
if !initial.load(Ordering::SeqCst) {
for (_room_id, room_info) in response.rooms.join {
for event in
room_info.timeline.events.iter().filter_map(|e| e.event.deserialize().ok())
{
if let AnySyncRoomEvent::Message(event) = event {
match event {
AnySyncMessageEvent::RoomMessage(m) => {
if let MessageType::VerificationRequest(_) = &m.content.msgtype
{
let request = client
.get_verification_request(&m.sender, &m.event_id)
.await
.expect("Request object wasn't created");
request
.accept()
.await
.expect("Can't accept verification request");
}
}
AnySyncMessageEvent::KeyVerificationKey(e) => {
if let Some(Verification::SasV1(sas)) = client
.get_verification(
&e.sender,
e.content.relates_to.event_id.as_str(),
)
.await
{
tokio::spawn(wait_for_confirmation((*client).clone(), sas));
}
}
AnySyncMessageEvent::KeyVerificationMac(e) => {
if let Some(Verification::SasV1(sas)) = client
.get_verification(
&e.sender,
e.content.relates_to.event_id.as_str(),
)
.await
{
if sas.is_done() {
print_result(&sas);
print_devices(&e.sender, client).await;
}
}
}
_ => (),
}
}
}
}
}
initial.store(false, Ordering::SeqCst);
LoopCtrl::Continue
})
.await;
+10 -16
View File
@@ -1,14 +1,16 @@
use std::{convert::TryFrom, env, process::exit};
use url::Url;
use matrix_sdk::{
self, api::r0::profile, identifiers::UserId, Client, ClientConfig, Result as MatrixResult,
self,
api::r0::profile,
identifiers::{MxcUri, UserId},
Client, Result as MatrixResult,
};
use url::Url;
#[derive(Debug)]
struct UserProfile {
avatar_url: Option<String>,
avatar_url: Option<MxcUri>,
displayname: Option<String>,
}
@@ -21,15 +23,12 @@ async fn get_profile(client: Client, mxid: &UserId) -> MatrixResult<UserProfile>
let request = profile::get_profile::Request::new(mxid);
// Start the request using matrix_sdk::Client::send
let resp = client.send(request).await?;
let resp = client.send(request, None).await?;
// Use the response and construct a UserProfile struct.
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/struct.Response.html
// for details on the Response for this Request
let user_profile = UserProfile {
avatar_url: resp.avatar_url,
displayname: resp.displayname,
};
let user_profile = UserProfile { avatar_url: resp.avatar_url, displayname: resp.displayname };
Ok(user_profile)
}
@@ -38,15 +37,10 @@ async fn login(
username: &str,
password: &str,
) -> Result<Client, matrix_sdk::Error> {
let client_config = ClientConfig::new()
.proxy("http://localhost:8080")?
.disable_ssl_verification();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new(homeserver_url).unwrap();
client
.login(username, password, None, Some("rust-sdk"))
.await?;
client.login(username, password, None, Some("rust-sdk")).await?;
Ok(client)
}
+30 -46
View File
@@ -6,36 +6,40 @@ use std::{
process::exit,
sync::Arc,
};
use tokio::sync::Mutex;
use matrix_sdk::{
self, async_trait,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
room::Room,
Client, EventHandler, SyncSettings,
};
use tokio::sync::Mutex;
use url::Url;
struct ImageBot {
client: Client,
image: Arc<Mutex<File>>,
}
impl ImageBot {
pub fn new(client: Client, image: File) -> Self {
pub fn new(image: File) -> Self {
let image = Arc::new(Mutex::new(image));
Self { client, image }
Self { image }
}
}
#[async_trait]
impl EventEmitter for ImageBot {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
impl EventHandler for ImageBot {
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
if let Room::Joined(room) = room {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
content:
MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
} = event
{
@@ -45,15 +49,10 @@ impl EventEmitter for ImageBot {
};
if msg_body.contains("!image") {
let room_id = room.read().await.room_id.clone();
println!("sending image");
let mut image = self.image.lock().await;
self.client
.room_send_attachment(&room_id, "cat", &mime::IMAGE_JPEG, &mut *image, None)
.await
.unwrap();
room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None).await.unwrap();
image.seek(SeekFrom::Start(0)).unwrap();
@@ -69,21 +68,13 @@ async fn login_and_sync(
password: String,
image: File,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.proxy("http://localhost:8080")?
.disable_ssl_verification();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new(homeserver_url).unwrap();
client
.login(&username, &password, None, Some("command bot"))
.await?;
client.login(&username, &password, None, Some("command bot")).await?;
client.sync_once(SyncSettings::default()).await.unwrap();
client
.add_event_emitter(Box::new(ImageBot::new(client.clone(), image)))
.await;
client.set_event_handler(Box::new(ImageBot::new(image))).await;
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client.sync(settings).await;
@@ -94,26 +85,19 @@ async fn login_and_sync(
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password, image_path) = match (
env::args().nth(1),
env::args().nth(2),
env::args().nth(3),
env::args().nth(4),
) {
(Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password> <image>",
env::args().next().unwrap()
);
exit(1)
}
};
let (homeserver_url, username, password, image_path) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3), env::args().nth(4)) {
(Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password> <image>",
env::args().next().unwrap()
);
exit(1)
}
};
println!(
"helloooo {} {} {} {:#?}",
homeserver_url, username, password, image_path
);
println!("helloooo {} {} {} {:#?}", homeserver_url, username, password, image_path);
let path = PathBuf::from(image_path);
let image = File::open(path).expect("Can't open image file.");
+17 -22
View File
@@ -1,34 +1,34 @@
use std::{env, process::exit};
use url::Url;
use matrix_sdk::{
self, async_trait,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
SyncMessageEvent,
},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
room::Room,
Client, EventHandler, SyncSettings,
};
use url::Url;
struct EventCallback;
#[async_trait]
impl EventEmitter for EventCallback {
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
if let SyncRoom::Joined(room) = room {
impl EventHandler for EventCallback {
async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
if let Room::Joined(room) = room {
if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
content:
MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
sender,
..
} = event
{
let name = {
// any reads should be held for the shortest time possible to
// avoid dead locks
let room = room.read().await;
let member = room.joined_members.get(&sender).unwrap();
member.name()
};
let member = room.get_member(sender).await.unwrap().unwrap();
let name = member.display_name().unwrap_or_else(|| member.user_id().as_str());
println!("{}: {}", name, msg_body);
}
}
@@ -40,17 +40,12 @@ async fn login(
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let client_config = ClientConfig::new()
.proxy("http://localhost:8080")?
.disable_ssl_verification();
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new(homeserver_url).unwrap();
client.add_event_emitter(Box::new(EventCallback)).await;
client.set_event_handler(Box::new(EventCallback)).await;
client
.login(username, password, None, Some("rust-sdk"))
.await?;
client.login(username, password, None, Some("rust-sdk")).await?;
client.sync(SyncSettings::new()).await;
Ok(())
@@ -10,10 +10,15 @@ edition = "2018"
crate-type = ["cdylib"]
[dependencies]
matrix-sdk = { path = "../..", default-features = false, features = ["native-tls"] }
url = "2.1.1"
wasm-bindgen = { version = "0.2.62", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4.12"
web-sys = { version = "0.3.39", features = ["console"] }
url = "2.2.2"
wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4.24"
console_error_panic_hook = "0.1.6"
web-sys = { version = "0.3.51", features = ["console"] }
[dependencies.matrix-sdk]
path = "../.."
default-features = false
features = ["native-tls", "encryption"]
[workspace]
@@ -7,6 +7,4 @@ You can build the example locally with:
and then visiting http://localhost:8080 in a browser should run the example!
Note: Encryption isn't supported yet
This example is loosely based off of [this example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch), an example usage of `fetch` from `wasm-bindgen`.
This example is loosely based off of [this example](https://github.com/seanmonstar/reqwest/tree/master/examples/wasm_github_fetch), an example usage of `fetch` from `wasm-bindgen`.
@@ -5,8 +5,8 @@
},
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "1.0.1",
"text-encoding": "^0.7.0",
"html-webpack-plugin": "^3.2.0",
"text-encoding": "^0.7.0",
"webpack": "^4.29.4",
"webpack-cli": "^3.1.1",
"webpack-dev-server": "^3.1.0"
+32 -21
View File
@@ -1,13 +1,11 @@
#![type_length_limit = "1702124"]
use matrix_sdk::{
api::r0::sync::sync_events::Response as SyncResponse,
deserialized_responses::SyncResponse,
events::{
room::message::{MessageEventContent, TextMessageEventContent},
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent,
},
identifiers::RoomId,
Client, ClientConfig, LoopCtrl, SyncSettings,
Client, LoopCtrl, SyncSettings,
};
use url::Url;
use wasm_bindgen::prelude::*;
@@ -19,37 +17,49 @@ impl WasmBot {
async fn on_room_message(
&self,
room_id: &RoomId,
event: SyncMessageEvent<MessageEventContent>,
event: &SyncMessageEvent<MessageEventContent>,
) {
let msg_body = if let SyncMessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
content:
MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
} = event
{
msg_body.clone()
msg_body
} else {
return;
};
console::log_1(&format!("Received message event {:?}", &msg_body).into());
if msg_body.starts_with("!party") {
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::Text(
TextMessageEventContent::plain("🎉🎊🥳 let's PARTY with wasm!! 🥳🎊🎉".to_string()),
if msg_body.contains("!party") {
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::text_plain(
"🎉🎊🥳 let's PARTY!! 🥳🎊🎉",
));
self.0.room_send(&room_id, content, None).await.unwrap();
println!("sending");
self.0
// send our message to the room we found the "!party" command in
// the last parameter is an optional Uuid which we don't care about.
.room_send(room_id, content, None)
.await
.unwrap();
println!("message sent");
}
}
async fn on_sync_response(&self, response: SyncResponse) -> LoopCtrl {
console::log_1(&"Synced".to_string().into());
for (room_id, room) in response.rooms.join {
for event in room.timeline.events {
if let Ok(event) = event.deserialize() {
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev)) = event {
self.on_room_message(&room_id, ev).await
}
if let Ok(AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev))) = event.event.deserialize() {
self.on_room_message(&room_id, &ev).await
}
}
}
@@ -60,13 +70,14 @@ impl WasmBot {
#[wasm_bindgen]
pub async fn run() -> Result<JsValue, JsValue> {
let homeserver_url = "http://localhost:8008";
let username = "user";
let password = "password";
console_error_panic_hook::set_once();
let homeserver_url = "http://localhost:8008";
let username = "example";
let password = "wordpass";
let client_config = ClientConfig::new();
let homeserver_url = Url::parse(&homeserver_url).unwrap();
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
let client = Client::new(homeserver_url).unwrap();
client
.login(username, password, None, Some("rust-sdk-wasm"))
+2093 -1267
View File
File diff suppressed because it is too large Load Diff
+12 -26
View File
@@ -18,18 +18,15 @@ use matrix_sdk_base::crypto::{
store::CryptoStoreError, Device as BaseDevice, LocalTrust, ReadOnlyDevice,
UserDevices as BaseUserDevices,
};
use matrix_sdk_common::{
api::r0::to_device::send_event_to_device::Request as ToDeviceRequest,
identifiers::{DeviceId, DeviceIdBox},
};
use ruma::{DeviceId, DeviceIdBox};
use crate::{error::Result, http_client::HttpClient, Sas};
use crate::{error::Result, verification::SasVerification, Client};
#[derive(Clone, Debug)]
/// A device represents a E2EE capable client of an user.
pub struct Device {
pub(crate) inner: BaseDevice,
pub(crate) http_client: HttpClient,
pub(crate) client: Client,
}
impl Deref for Device {
@@ -43,7 +40,8 @@ impl Deref for Device {
impl Device {
/// Start a interactive verification with this `Device`
///
/// Returns a `Sas` object that represents the interactive verification flow.
/// Returns a `Sas` object that represents the interactive verification
/// flow.
///
/// # Example
///
@@ -64,17 +62,11 @@ impl Device {
/// let verification = device.start_verification().await.unwrap();
/// # });
/// ```
pub async fn start_verification(&self) -> Result<Sas> {
pub async fn start_verification(&self) -> Result<SasVerification> {
let (sas, request) = self.inner.start_verification().await?;
let txn_id_string = request.txn_id_string();
let request = ToDeviceRequest::new(request.event_type, &txn_id_string, request.messages);
self.client.send_to_device(&request).await?;
self.http_client.send(request).await?;
Ok(Sas {
inner: sas,
http_client: self.http_client.clone(),
})
Ok(SasVerification { inner: sas, client: self.client.clone() })
}
/// Is the device trusted.
@@ -102,16 +94,13 @@ impl Device {
#[derive(Debug)]
pub struct UserDevices {
pub(crate) inner: BaseUserDevices,
pub(crate) http_client: HttpClient,
pub(crate) client: Client,
}
impl UserDevices {
/// Get the specific device with the given device id.
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
self.inner.get(device_id).map(|d| Device {
inner: d,
http_client: self.http_client.clone(),
})
self.inner.get(device_id).map(|d| Device { inner: d, client: self.client.clone() })
}
/// Iterator over all the device ids of the user devices.
@@ -121,11 +110,8 @@ impl UserDevices {
/// Iterator over all the devices of the user devices.
pub fn devices(&self) -> impl Iterator<Item = Device> + '_ {
let client = self.http_client.clone();
let client = self.client.clone();
self.inner.devices().map(move |d| Device {
inner: d,
http_client: client.clone(),
})
self.inner.devices().map(move |d| Device { inner: d, client: client.clone() })
}
}
+97 -50
View File
@@ -14,39 +14,97 @@
//! Error conditions.
use matrix_sdk_base::Error as MatrixError;
use matrix_sdk_common::{
api::{
r0::uiaa::{UiaaInfo, UiaaResponse as UiaaError},
Error as RumaClientError,
},
FromHttpResponseError as RumaResponseError, IntoHttpError as RumaIntoHttpError, ServerError,
};
use reqwest::Error as ReqwestError;
use serde_json::Error as JsonError;
use std::io::Error as IoError;
use thiserror::Error;
use http::StatusCode;
#[cfg(feature = "encryption")]
use matrix_sdk_base::crypto::store::CryptoStoreError;
use matrix_sdk_base::crypto::{store::CryptoStoreError, DecryptorError};
use matrix_sdk_base::{Error as MatrixError, StoreError};
use reqwest::Error as ReqwestError;
use ruma::{
api::{
client::{
r0::uiaa::{UiaaInfo, UiaaResponse as UiaaError},
Error as RumaClientApiError,
},
error::{FromHttpResponseError, IntoHttpError, MatrixError as RumaApiError, ServerError},
},
identifiers::Error as IdentifierError,
};
use serde_json::Error as JsonError;
use thiserror::Error;
use url::ParseError as UrlParseError;
/// Result type of the rust-sdk.
pub type Result<T> = std::result::Result<T, Error>;
/// Internal representation of errors.
/// An HTTP error, representing either a connection error or an error while
/// converting the raw HTTP response into a Matrix response.
#[derive(Error, Debug)]
pub enum Error {
/// Queried endpoint requires authentication but was called on an anonymous client.
pub enum HttpError {
/// An error at the HTTP layer.
#[error(transparent)]
Reqwest(#[from] ReqwestError),
/// Queried endpoint requires authentication but was called on an anonymous
/// client.
#[error("the queried endpoint requires authentication but was called before logging in")]
AuthenticationRequired,
/// Client tried to force authentication but did not provide an access
/// token.
#[error("tried to force authentication but no access token was provided")]
ForcedAuthenticationWithoutAccessToken,
/// Queried endpoint is not meant for clients.
#[error("the queried endpoint is not meant for clients")]
NotClientRequest,
/// An error at the HTTP layer.
/// An error converting between ruma_*_api types and Hyper types.
#[error(transparent)]
Reqwest(#[from] ReqwestError),
Api(#[from] FromHttpResponseError<RumaApiError>),
/// An error converting between ruma_client_api types and Hyper types.
#[error(transparent)]
ClientApi(#[from] FromHttpResponseError<RumaClientApiError>),
/// An error converting between ruma_client_api types and Hyper types.
#[error(transparent)]
IntoHttp(#[from] IntoHttpError),
/// An error occurred while authenticating.
///
/// When registering or authenticating the Matrix server can send a
/// `UiaaResponse` as the error type, this is a User-Interactive
/// Authentication API response. This represents an error with
/// information about how to authenticate the user.
#[error(transparent)]
UiaaError(#[from] FromHttpResponseError<UiaaError>),
/// The server returned a status code that should be retried.
#[error("Server returned an error {0}")]
Server(StatusCode),
/// The given request can't be cloned and thus can't be retried.
#[error("The request cannot be cloned")]
UnableToCloneRequest,
/// Tried to send a request without `user_id` in the `Session`
#[error("missing user_id in session")]
UserIdRequired,
}
/// Internal representation of errors.
#[derive(Error, Debug)]
pub enum Error {
/// Error doing an HTTP request.
#[error(transparent)]
Http(#[from] HttpError),
/// Queried endpoint requires authentication but was called on an anonymous
/// client.
#[error("the queried endpoint requires authentication but was called before logging in")]
AuthenticationRequired,
/// An error de/serializing type for the `StateStore`
#[error(transparent)]
@@ -54,15 +112,7 @@ pub enum Error {
/// An IO error happened.
#[error(transparent)]
IO(#[from] IoError),
/// An error converting between ruma_client_api types and Hyper types.
#[error("can't parse the JSON response as a Matrix response")]
RumaResponse(RumaResponseError<RumaClientError>),
/// An error converting between ruma_client_api types and Hyper types.
#[error("can't convert between ruma_client_api and hyper types.")]
IntoHttp(RumaIntoHttpError),
Io(#[from] IoError),
/// An error occurred in the Matrix client library.
#[error(transparent)]
@@ -73,13 +123,22 @@ pub enum Error {
#[error(transparent)]
CryptoStoreError(#[from] CryptoStoreError),
/// An error occurred while authenticating.
///
/// When registering or authenticating the Matrix server can send a `UiaaResponse`
/// as the error type, this is a User-Interactive Authentication API response. This
/// represents an error with information about how to authenticate the user.
#[error("User-Interactive Authentication required.")]
UiaaError(RumaResponseError<UiaaError>),
/// An error occurred during decryption.
#[cfg(feature = "encryption")]
#[error(transparent)]
DecryptorError(#[from] DecryptorError),
/// An error occurred in the state store.
#[error(transparent)]
StateStore(#[from] StoreError),
/// An error encountered when trying to parse an identifier.
#[error(transparent)]
Identifier(#[from] IdentifierError),
/// An error encountered when trying to parse a url.
#[error(transparent)]
Url(#[from] UrlParseError),
}
impl Error {
@@ -95,9 +154,9 @@ impl Error {
/// This method is an convenience method to get to the info the server
/// returned on the first, failed request.
pub fn uiaa_response(&self) -> Option<&UiaaInfo> {
if let Error::UiaaError(RumaResponseError::Http(ServerError::Known(
if let Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(ServerError::Known(
UiaaError::AuthResponse(i),
))) = self
)))) = self
{
Some(i)
} else {
@@ -106,20 +165,8 @@ impl Error {
}
}
impl From<RumaResponseError<UiaaError>> for Error {
fn from(error: RumaResponseError<UiaaError>) -> Self {
Self::UiaaError(error)
}
}
impl From<RumaResponseError<RumaClientError>> for Error {
fn from(error: RumaResponseError<RumaClientError>) -> Self {
Self::RumaResponse(error)
}
}
impl From<RumaIntoHttpError> for Error {
fn from(error: RumaIntoHttpError) -> Self {
Self::IntoHttp(error)
impl From<ReqwestError> for Error {
fn from(e: ReqwestError) -> Self {
Error::Http(HttpError::Reqwest(e))
}
}
+963
View File
@@ -0,0 +1,963 @@
// Copyright 2020 Damir Jelić
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::ops::Deref;
use matrix_sdk_common::async_trait;
use ruma::{
api::client::r0::push::get_notifications::Notification,
events::{
call::{
answer::AnswerEventContent, candidates::CandidatesEventContent,
hangup::HangupEventContent, invite::InviteEventContent,
},
custom::CustomEventContent,
fully_read::FullyReadEventContent,
ignored_user_list::IgnoredUserListEventContent,
presence::PresenceEvent,
push_rules::PushRulesEventContent,
reaction::ReactionEventContent,
receipt::ReceiptEventContent,
room::{
aliases::AliasesEventContent,
avatar::AvatarEventContent,
canonical_alias::CanonicalAliasEventContent,
join_rules::JoinRulesEventContent,
member::MemberEventContent,
message::{feedback::FeedbackEventContent, MessageEventContent as MsgEventContent},
name::NameEventContent,
power_levels::PowerLevelsEventContent,
redaction::SyncRedactionEvent,
tombstone::TombstoneEventContent,
},
typing::TypingEventContent,
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncEphemeralRoomEvent, AnySyncMessageEvent, AnySyncRoomEvent, AnySyncStateEvent,
GlobalAccountDataEvent, RoomAccountDataEvent, StrippedStateEvent, SyncEphemeralRoomEvent,
SyncMessageEvent, SyncStateEvent,
},
serde::Raw,
RoomId,
};
use serde_json::value::RawValue as RawJsonValue;
use crate::{deserialized_responses::SyncResponse, room::Room, Client};
pub(crate) struct Handler {
pub(crate) inner: Box<dyn EventHandler>,
pub(crate) client: Client,
}
impl Deref for Handler {
type Target = dyn EventHandler;
fn deref(&self) -> &Self::Target {
&*self.inner
}
}
impl Handler {
fn get_room(&self, room_id: &RoomId) -> Option<Room> {
self.client.get_room(room_id)
}
pub(crate) async fn handle_sync(&self, response: &SyncResponse) {
for event in response.account_data.events.iter().filter_map(|e| e.deserialize().ok()) {
self.handle_account_data_event(&event).await;
}
for (room_id, room_info) in &response.rooms.join {
if let Some(room) = self.get_room(room_id) {
for event in room_info.ephemeral.events.iter().filter_map(|e| e.deserialize().ok())
{
self.handle_ephemeral_event(room.clone(), &event).await;
}
for event in
room_info.account_data.events.iter().filter_map(|e| e.deserialize().ok())
{
self.handle_room_account_data_event(room.clone(), &event).await;
}
for (raw_event, event) in room_info.state.events.iter().filter_map(|e| {
if let Ok(d) = e.deserialize() {
Some((e, d))
} else {
None
}
}) {
self.handle_state_event(room.clone(), &event, raw_event).await;
}
for (raw_event, event) in room_info.timeline.events.iter().filter_map(|e| {
if let Ok(d) = e.event.deserialize() {
Some((&e.event, d))
} else {
None
}
}) {
self.handle_timeline_event(room.clone(), &event, raw_event).await;
}
}
}
for (room_id, room_info) in &response.rooms.leave {
if let Some(room) = self.get_room(room_id) {
for event in
room_info.account_data.events.iter().filter_map(|e| e.deserialize().ok())
{
self.handle_room_account_data_event(room.clone(), &event).await;
}
for (raw_event, event) in room_info.state.events.iter().filter_map(|e| {
if let Ok(d) = e.deserialize() {
Some((e, d))
} else {
None
}
}) {
self.handle_state_event(room.clone(), &event, raw_event).await;
}
for (raw_event, event) in room_info.timeline.events.iter().filter_map(|e| {
if let Ok(d) = e.event.deserialize() {
Some((&e.event, d))
} else {
None
}
}) {
self.handle_timeline_event(room.clone(), &event, raw_event).await;
}
}
}
for (room_id, room_info) in &response.rooms.invite {
if let Some(room) = self.get_room(room_id) {
for event in
room_info.invite_state.events.iter().filter_map(|e| e.deserialize().ok())
{
self.handle_stripped_state_event(room.clone(), &event).await;
}
}
}
for event in response.presence.events.iter().filter_map(|e| e.deserialize().ok()) {
self.on_presence_event(&event).await;
}
for (room_id, notifications) in &response.notifications {
if let Some(room) = self.get_room(room_id) {
for notification in notifications {
self.on_room_notification(room.clone(), notification.clone()).await;
}
}
}
}
async fn handle_timeline_event(
&self,
room: Room,
event: &AnySyncRoomEvent,
raw_event: &Raw<AnySyncRoomEvent>,
) {
match event {
AnySyncRoomEvent::State(event) => match event {
AnySyncStateEvent::RoomMember(e) => self.on_room_member(room, e).await,
AnySyncStateEvent::RoomName(e) => self.on_room_name(room, e).await,
AnySyncStateEvent::RoomCanonicalAlias(e) => {
self.on_room_canonical_alias(room, e).await
}
AnySyncStateEvent::RoomAliases(e) => self.on_room_aliases(room, e).await,
AnySyncStateEvent::RoomAvatar(e) => self.on_room_avatar(room, e).await,
AnySyncStateEvent::RoomPowerLevels(e) => self.on_room_power_levels(room, e).await,
AnySyncStateEvent::RoomTombstone(e) => self.on_room_tombstone(room, e).await,
AnySyncStateEvent::RoomJoinRules(e) => self.on_room_join_rules(room, e).await,
AnySyncStateEvent::PolicyRuleRoom(_)
| AnySyncStateEvent::PolicyRuleServer(_)
| AnySyncStateEvent::PolicyRuleUser(_)
| AnySyncStateEvent::RoomCreate(_)
| AnySyncStateEvent::RoomEncryption(_)
| AnySyncStateEvent::RoomGuestAccess(_)
| AnySyncStateEvent::RoomHistoryVisibility(_)
| AnySyncStateEvent::RoomPinnedEvents(_)
| AnySyncStateEvent::RoomServerAcl(_)
| AnySyncStateEvent::RoomThirdPartyInvite(_)
| AnySyncStateEvent::RoomTopic(_)
| AnySyncStateEvent::SpaceChild(_)
| AnySyncStateEvent::SpaceParent(_) => {}
_ => {
if let Ok(e) = raw_event.deserialize_as::<SyncStateEvent<CustomEventContent>>()
{
self.on_custom_event(room, &CustomEvent::State(&e)).await;
}
}
},
AnySyncRoomEvent::Message(event) => match event {
AnySyncMessageEvent::RoomMessage(e) => self.on_room_message(room, e).await,
AnySyncMessageEvent::RoomMessageFeedback(e) => {
self.on_room_message_feedback(room, e).await
}
AnySyncMessageEvent::RoomRedaction(e) => self.on_room_redaction(room, e).await,
AnySyncMessageEvent::Reaction(e) => self.on_room_reaction(room, e).await,
AnySyncMessageEvent::CallInvite(e) => self.on_room_call_invite(room, e).await,
AnySyncMessageEvent::CallAnswer(e) => self.on_room_call_answer(room, e).await,
AnySyncMessageEvent::CallCandidates(e) => {
self.on_room_call_candidates(room, e).await
}
AnySyncMessageEvent::CallHangup(e) => self.on_room_call_hangup(room, e).await,
AnySyncMessageEvent::KeyVerificationReady(_)
| AnySyncMessageEvent::KeyVerificationStart(_)
| AnySyncMessageEvent::KeyVerificationCancel(_)
| AnySyncMessageEvent::KeyVerificationAccept(_)
| AnySyncMessageEvent::KeyVerificationKey(_)
| AnySyncMessageEvent::KeyVerificationMac(_)
| AnySyncMessageEvent::KeyVerificationDone(_)
| AnySyncMessageEvent::RoomEncrypted(_)
| AnySyncMessageEvent::Sticker(_) => {}
_ => {
if let Ok(e) =
raw_event.deserialize_as::<SyncMessageEvent<CustomEventContent>>()
{
self.on_custom_event(room, &CustomEvent::Message(&e)).await;
}
}
},
AnySyncRoomEvent::RedactedState(_event) => {}
AnySyncRoomEvent::RedactedMessage(_event) => {}
}
}
async fn handle_state_event(
&self,
room: Room,
event: &AnySyncStateEvent,
raw_event: &Raw<AnySyncStateEvent>,
) {
match event {
AnySyncStateEvent::RoomMember(member) => self.on_state_member(room, member).await,
AnySyncStateEvent::RoomName(name) => self.on_state_name(room, name).await,
AnySyncStateEvent::RoomCanonicalAlias(canonical) => {
self.on_state_canonical_alias(room, canonical).await
}
AnySyncStateEvent::RoomAliases(aliases) => self.on_state_aliases(room, aliases).await,
AnySyncStateEvent::RoomAvatar(avatar) => self.on_state_avatar(room, avatar).await,
AnySyncStateEvent::RoomPowerLevels(power) => {
self.on_state_power_levels(room, power).await
}
AnySyncStateEvent::RoomJoinRules(rules) => self.on_state_join_rules(room, rules).await,
AnySyncStateEvent::RoomTombstone(tomb) => {
// TODO make `on_state_tombstone` method
self.on_room_tombstone(room, tomb).await
}
AnySyncStateEvent::PolicyRuleRoom(_)
| AnySyncStateEvent::PolicyRuleServer(_)
| AnySyncStateEvent::PolicyRuleUser(_)
| AnySyncStateEvent::RoomCreate(_)
| AnySyncStateEvent::RoomEncryption(_)
| AnySyncStateEvent::RoomGuestAccess(_)
| AnySyncStateEvent::RoomHistoryVisibility(_)
| AnySyncStateEvent::RoomPinnedEvents(_)
| AnySyncStateEvent::RoomServerAcl(_)
| AnySyncStateEvent::RoomThirdPartyInvite(_)
| AnySyncStateEvent::RoomTopic(_)
| AnySyncStateEvent::SpaceChild(_)
| AnySyncStateEvent::SpaceParent(_) => {}
_ => {
if let Ok(e) = raw_event.deserialize_as::<SyncStateEvent<CustomEventContent>>() {
self.on_custom_event(room, &CustomEvent::State(&e)).await;
}
}
}
}
pub(crate) async fn handle_stripped_state_event(
&self,
// TODO these events are only handled in invited rooms.
room: Room,
event: &AnyStrippedStateEvent,
) {
match event {
AnyStrippedStateEvent::RoomMember(member) => {
self.on_stripped_state_member(room, member, None).await
}
AnyStrippedStateEvent::RoomName(name) => self.on_stripped_state_name(room, name).await,
AnyStrippedStateEvent::RoomCanonicalAlias(canonical) => {
self.on_stripped_state_canonical_alias(room, canonical).await
}
AnyStrippedStateEvent::RoomAliases(aliases) => {
self.on_stripped_state_aliases(room, aliases).await
}
AnyStrippedStateEvent::RoomAvatar(avatar) => {
self.on_stripped_state_avatar(room, avatar).await
}
AnyStrippedStateEvent::RoomPowerLevels(power) => {
self.on_stripped_state_power_levels(room, power).await
}
AnyStrippedStateEvent::RoomJoinRules(rules) => {
self.on_stripped_state_join_rules(room, rules).await
}
_ => {}
}
}
pub(crate) async fn handle_room_account_data_event(
&self,
room: Room,
event: &AnyRoomAccountDataEvent,
) {
if let AnyRoomAccountDataEvent::FullyRead(event) = event {
self.on_non_room_fully_read(room, event).await
}
}
pub(crate) async fn handle_account_data_event(&self, event: &AnyGlobalAccountDataEvent) {
match event {
AnyGlobalAccountDataEvent::IgnoredUserList(ignored) => {
self.on_non_room_ignored_users(ignored).await
}
AnyGlobalAccountDataEvent::PushRules(rules) => self.on_non_room_push_rules(rules).await,
_ => {}
}
}
pub(crate) async fn handle_ephemeral_event(
&self,
room: Room,
event: &AnySyncEphemeralRoomEvent,
) {
match event {
AnySyncEphemeralRoomEvent::Typing(typing) => {
self.on_non_room_typing(room, typing).await
}
AnySyncEphemeralRoomEvent::Receipt(receipt) => {
self.on_non_room_receipt(room, receipt).await
}
_ => {}
}
}
}
/// This represents the various "unrecognized" events.
#[derive(Clone, Copy, Debug)]
pub enum CustomEvent<'c> {
/// A custom basic event.
Basic(&'c GlobalAccountDataEvent<CustomEventContent>),
/// A custom basic event.
EphemeralRoom(&'c SyncEphemeralRoomEvent<CustomEventContent>),
/// A custom room event.
Message(&'c SyncMessageEvent<CustomEventContent>),
/// A custom state event.
State(&'c SyncStateEvent<CustomEventContent>),
/// A custom stripped state event.
StrippedState(&'c StrippedStateEvent<CustomEventContent>),
}
/// This trait allows any type implementing `EventHandler` to specify event
/// callbacks for each event. The `Client` calls each method when the
/// corresponding event is received.
///
/// # Examples
/// ```
/// # use std::ops::Deref;
/// # use std::sync::Arc;
/// # use std::{env, process::exit};
/// # use matrix_sdk::{
/// # async_trait,
/// # EventHandler,
/// # events::{
/// # room::message::{MessageEventContent, MessageType, TextMessageEventContent},
/// # SyncMessageEvent
/// # },
/// # locks::RwLock,
/// # room::Room,
/// # };
///
/// struct EventCallback;
///
/// #[async_trait]
/// impl EventHandler for EventCallback {
/// async fn on_room_message(&self, room: Room, event: &SyncMessageEvent<MessageEventContent>) {
/// if let Room::Joined(room) = room {
/// if let SyncMessageEvent {
/// content:
/// MessageEventContent {
/// msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
/// ..
/// },
/// sender,
/// ..
/// } = event
/// {
/// let member = room.get_member(&sender).await.unwrap().unwrap();
/// let name = member
/// .display_name()
/// .unwrap_or_else(|| member.user_id().as_str());
/// println!("{}: {}", name, msg_body);
/// }
/// }
/// }
/// }
/// ```
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventHandler: Send + Sync {
// ROOM EVENTS from `IncomingTimeline`
/// Fires when `Client` receives a `RoomEvent::RoomMember` event.
async fn on_room_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
async fn on_room_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
async fn on_room_canonical_alias(
&self,
_: Room,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
async fn on_room_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
async fn on_room_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
async fn on_room_message(&self, _: Room, _: &SyncMessageEvent<MsgEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
async fn on_room_message_feedback(&self, _: Room, _: &SyncMessageEvent<FeedbackEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::Reaction` event.
async fn on_room_reaction(&self, _: Room, _: &SyncMessageEvent<ReactionEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::CallInvite` event
async fn on_room_call_invite(&self, _: Room, _: &SyncMessageEvent<InviteEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::CallAnswer` event
async fn on_room_call_answer(&self, _: Room, _: &SyncMessageEvent<AnswerEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::CallCandidates` event
async fn on_room_call_candidates(&self, _: Room, _: &SyncMessageEvent<CandidatesEventContent>) {
}
/// Fires when `Client` receives a `RoomEvent::CallHangup` event
async fn on_room_call_hangup(&self, _: Room, _: &SyncMessageEvent<HangupEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
async fn on_room_redaction(&self, _: Room, _: &SyncRedactionEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
async fn on_room_power_levels(&self, _: Room, _: &SyncStateEvent<PowerLevelsEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomJoinRules` event.
async fn on_room_join_rules(&self, _: Room, _: &SyncStateEvent<JoinRulesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_tombstone(&self, _: Room, _: &SyncStateEvent<TombstoneEventContent>) {}
/// Fires when `Client` receives room events that trigger notifications
/// according to the push rules of the user.
async fn on_room_notification(&self, _: Room, _: Notification) {}
// `RoomEvent`s from `IncomingState`
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
async fn on_state_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomName` event.
async fn on_state_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
async fn on_state_canonical_alias(
&self,
_: Room,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
async fn on_state_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
async fn on_state_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
async fn on_state_power_levels(&self, _: Room, _: &SyncStateEvent<PowerLevelsEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
async fn on_state_join_rules(&self, _: Room, _: &SyncStateEvent<JoinRulesEventContent>) {}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: Room,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName`
/// event.
async fn on_stripped_state_name(&self, _: Room, _: &StrippedStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(
&self,
_: Room,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(
&self,
_: Room,
_: &StrippedStateEvent<AliasesEventContent>,
) {
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(&self, _: Room, _: &StrippedStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(
&self,
_: Room,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(
&self,
_: Room,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
}
// `NonRoomEvent` (this is a type alias from ruma_events)
/// Fires when `Client` receives a `NonRoomEvent::RoomPresence` event.
async fn on_non_room_presence(&self, _: Room, _: &PresenceEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomName` event.
async fn on_non_room_ignored_users(
&self,
_: &GlobalAccountDataEvent<IgnoredUserListEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::RoomCanonicalAlias` event.
async fn on_non_room_push_rules(&self, _: &GlobalAccountDataEvent<PushRulesEventContent>) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
async fn on_non_room_fully_read(
&self,
_: Room,
_: &RoomAccountDataEvent<FullyReadEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::Typing` event.
async fn on_non_room_typing(&self, _: Room, _: &SyncEphemeralRoomEvent<TypingEventContent>) {}
/// Fires when `Client` receives a `NonRoomEvent::Receipt` event.
///
/// This is always a read receipt.
async fn on_non_room_receipt(&self, _: Room, _: &SyncEphemeralRoomEvent<ReceiptEventContent>) {}
// `PresenceEvent` is a struct so there is only the one method
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
async fn on_presence_event(&self, _: &PresenceEvent) {}
/// Fires when `Client` receives a `Event::Custom` event or if
/// deserialization fails because the event was unknown to ruma.
///
/// The only guarantee this method can give about the event is that it is
/// valid JSON.
async fn on_unrecognized_event(&self, _: Room, _: &RawJsonValue) {}
/// Fires when `Client` receives a `Event::Custom` event or if
/// deserialization fails because the event was unknown to ruma.
///
/// The only guarantee this method can give about the event is that it is in
/// the shape of a valid matrix event.
async fn on_custom_event(&self, _: Room, _: &CustomEvent<'_>) {}
}
#[cfg(test)]
mod test {
use std::{sync::Arc, time::Duration};
use matrix_sdk_common::{async_trait, locks::Mutex};
use matrix_sdk_test::{async_test, test_json};
use mockito::{mock, Matcher};
use ruma::user_id;
#[cfg(target_arch = "wasm32")]
pub use wasm_bindgen_test::*;
use super::*;
#[derive(Clone)]
pub struct EvHandlerTest(Arc<Mutex<Vec<String>>>);
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventHandler for EvHandlerTest {
async fn on_room_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("member".to_string())
}
async fn on_room_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("name".to_string())
}
async fn on_room_canonical_alias(
&self,
_: Room,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("canonical".to_string())
}
async fn on_room_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("aliases".to_string())
}
async fn on_room_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("avatar".to_string())
}
async fn on_room_message(&self, _: Room, _: &SyncMessageEvent<MsgEventContent>) {
self.0.lock().await.push("message".to_string())
}
async fn on_room_message_feedback(
&self,
_: Room,
_: &SyncMessageEvent<FeedbackEventContent>,
) {
self.0.lock().await.push("feedback".to_string())
}
async fn on_room_call_invite(&self, _: Room, _: &SyncMessageEvent<InviteEventContent>) {
self.0.lock().await.push("call invite".to_string())
}
async fn on_room_call_answer(&self, _: Room, _: &SyncMessageEvent<AnswerEventContent>) {
self.0.lock().await.push("call answer".to_string())
}
async fn on_room_call_candidates(
&self,
_: Room,
_: &SyncMessageEvent<CandidatesEventContent>,
) {
self.0.lock().await.push("call candidates".to_string())
}
async fn on_room_call_hangup(&self, _: Room, _: &SyncMessageEvent<HangupEventContent>) {
self.0.lock().await.push("call hangup".to_string())
}
async fn on_room_redaction(&self, _: Room, _: &SyncRedactionEvent) {
self.0.lock().await.push("redaction".to_string())
}
async fn on_room_power_levels(&self, _: Room, _: &SyncStateEvent<PowerLevelsEventContent>) {
self.0.lock().await.push("power".to_string())
}
async fn on_room_tombstone(&self, _: Room, _: &SyncStateEvent<TombstoneEventContent>) {
self.0.lock().await.push("tombstone".to_string())
}
async fn on_state_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("state member".to_string())
}
async fn on_state_name(&self, _: Room, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("state name".to_string())
}
async fn on_state_canonical_alias(
&self,
_: Room,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("state canonical".to_string())
}
async fn on_state_aliases(&self, _: Room, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("state aliases".to_string())
}
async fn on_state_avatar(&self, _: Room, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("state avatar".to_string())
}
async fn on_state_power_levels(
&self,
_: Room,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("state power".to_string())
}
async fn on_state_join_rules(&self, _: Room, _: &SyncStateEvent<JoinRulesEventContent>) {
self.0.lock().await.push("state rules".to_string())
}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: Room,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
self.0.lock().await.push("stripped state member".to_string())
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomName` event.
async fn on_stripped_state_name(&self, _: Room, _: &StrippedStateEvent<NameEventContent>) {
self.0.lock().await.push("stripped state name".to_string())
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(
&self,
_: Room,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("stripped state canonical".to_string())
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(
&self,
_: Room,
_: &StrippedStateEvent<AliasesEventContent>,
) {
self.0.lock().await.push("stripped state aliases".to_string())
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(
&self,
_: Room,
_: &StrippedStateEvent<AvatarEventContent>,
) {
self.0.lock().await.push("stripped state avatar".to_string())
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(
&self,
_: Room,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("stripped state power".to_string())
}
/// Fires when `Client` receives a
/// `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(
&self,
_: Room,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
self.0.lock().await.push("stripped state rules".to_string())
}
async fn on_non_room_presence(&self, _: Room, _: &PresenceEvent) {
self.0.lock().await.push("presence".to_string())
}
async fn on_non_room_ignored_users(
&self,
_: &GlobalAccountDataEvent<IgnoredUserListEventContent>,
) {
self.0.lock().await.push("account ignore".to_string())
}
async fn on_non_room_push_rules(&self, _: &GlobalAccountDataEvent<PushRulesEventContent>) {
self.0.lock().await.push("account push rules".to_string())
}
async fn on_non_room_fully_read(
&self,
_: Room,
_: &RoomAccountDataEvent<FullyReadEventContent>,
) {
self.0.lock().await.push("account read".to_string())
}
async fn on_non_room_typing(
&self,
_: Room,
_: &SyncEphemeralRoomEvent<TypingEventContent>,
) {
self.0.lock().await.push("typing event".to_string())
}
async fn on_non_room_receipt(
&self,
_: Room,
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
) {
self.0.lock().await.push("receipt event".to_string())
}
async fn on_presence_event(&self, _: &PresenceEvent) {
self.0.lock().await.push("presence event".to_string())
}
async fn on_unrecognized_event(&self, _: Room, _: &RawJsonValue) {
self.0.lock().await.push("unrecognized event".to_string())
}
async fn on_custom_event(&self, _: Room, _: &CustomEvent<'_>) {
self.0.lock().await.push("custom event".to_string())
}
async fn on_room_notification(&self, _: Room, _: Notification) {
self.0.lock().await.push("notification".to_string())
}
}
use crate::{Client, Session, SyncSettings};
async fn get_client() -> Client {
let session = Session {
access_token: "1234".to_owned(),
user_id: user_id!("@example:localhost"),
device_id: "DEVICEID".into(),
};
let homeserver = url::Url::parse(&mockito::server_url()).unwrap();
let client = Client::new(homeserver).unwrap();
client.restore_login(session).await.unwrap();
client
}
async fn mock_sync(client: &Client, response: String) {
let _m = mock("GET", Matcher::Regex(r"^/_matrix/client/r0/sync\?.*$".to_string()))
.with_status(200)
.match_header("authorization", "Bearer 1234")
.with_body(response)
.create();
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
let _response = client.sync_once(sync_settings).await.unwrap();
}
#[async_test]
async fn event_handler_joined() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let handler = Box::new(EvHandlerTest(vec));
let client = get_client().await;
client.set_event_handler(handler).await;
mock_sync(&client, test_json::SYNC.to_string()).await;
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"account ignore",
"receipt event",
"account read",
"state rules",
"state member",
"state aliases",
"state power",
"state canonical",
"state member",
"state member",
"message",
"presence event",
"notification",
],
)
}
#[async_test]
async fn event_handler_invite() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let handler = Box::new(EvHandlerTest(vec));
let client = get_client().await;
client.set_event_handler(handler).await;
mock_sync(&client, test_json::INVITE_SYNC.to_string()).await;
let v = test_vec.lock().await;
assert_eq!(v.as_slice(), ["stripped state name", "stripped state member", "presence event"],)
}
#[async_test]
async fn event_handler_leave() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let handler = Box::new(EvHandlerTest(vec));
let client = get_client().await;
client.set_event_handler(handler).await;
mock_sync(&client, test_json::LEAVE_SYNC.to_string()).await;
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"account ignore",
"state rules",
"state member",
"state aliases",
"state power",
"state canonical",
"state member",
"state member",
"message",
"presence event",
"notification",
],
)
}
#[async_test]
async fn event_handler_more_events() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let handler = Box::new(EvHandlerTest(vec));
let client = get_client().await;
client.set_event_handler(handler).await;
mock_sync(&client, test_json::MORE_SYNC.to_string()).await;
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"receipt event",
"typing event",
"message",
"message", // this is a message edit event
"redaction",
"message", // this is a notice event
],
)
}
#[async_test]
async fn event_handler_voip() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let handler = Box::new(EvHandlerTest(vec));
let client = get_client().await;
client.set_event_handler(handler).await;
mock_sync(&client, test_json::VOIP_SYNC.to_string()).await;
let v = test_vec.lock().await;
assert_eq!(v.as_slice(), ["call invite", "call answer", "call candidates", "call hangup",],)
}
#[async_test]
async fn event_handler_two_syncs() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let handler = Box::new(EvHandlerTest(vec));
let client = get_client().await;
client.set_event_handler(handler).await;
mock_sync(&client, test_json::SYNC.to_string()).await;
mock_sync(&client, test_json::MORE_SYNC.to_string()).await;
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"account ignore",
"receipt event",
"account read",
"state rules",
"state member",
"state aliases",
"state power",
"state canonical",
"state member",
"state member",
"message",
"presence event",
"notification",
"receipt event",
"typing event",
"message",
"message", // this is a message edit event
"redaction",
"message", // this is a notice event
"notification",
"notification",
"notification",
],
)
}
}
+216 -69
View File
@@ -12,36 +12,49 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(all(not(target_arch = "wasm32")))]
use std::sync::atomic::{AtomicU64, Ordering};
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
use http::{HeaderValue, Method as HttpMethod, Response as HttpResponse};
#[cfg(all(not(target_arch = "wasm32")))]
use backoff::{future::retry, Error as RetryError, ExponentialBackoff};
#[cfg(all(not(target_arch = "wasm32")))]
use http::StatusCode;
use http::{HeaderValue, Response as HttpResponse};
use matrix_sdk_common::{async_trait, locks::RwLock, AsyncTraitDeps};
use reqwest::{Client, Response};
use ruma::api::{
client::r0::media::create_content, error::FromHttpResponseError, AuthScheme, IncomingResponse,
OutgoingRequest, OutgoingRequestAppserviceExt, SendAccessToken,
};
use tracing::trace;
use url::Url;
use matrix_sdk_common::{
api::r0::media::create_content, async_trait, locks::RwLock, AuthScheme, FromHttpResponseError,
};
use crate::{ClientConfig, Error, OutgoingRequest, Result, Session};
use crate::{error::HttpError, Bytes, BytesMut, ClientConfig, RequestConfig, Session};
/// Abstraction around the http layer. The allows implementors to use different
/// http libraries.
#[async_trait]
pub trait HttpSend: Sync + Send + Debug {
/// The method abstracting sending request types and receiving response types.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait HttpSend: AsyncTraitDeps {
/// The method abstracting sending request types and receiving response
/// types.
///
/// This is called by the client every time it wants to send anything to a homeserver.
/// This is called by the client every time it wants to send anything to a
/// homeserver.
///
/// # Arguments
///
/// * `request` - The http request that has been converted from a ruma `Request`.
/// * `request` - The http request that has been converted from a ruma
/// `Request`.
///
/// * `request_config` - The config used for this request.
///
/// # Examples
///
/// ```
/// use std::convert::TryFrom;
/// use matrix_sdk::{HttpSend, Result, async_trait};
/// use matrix_sdk::{HttpSend, async_trait, HttpError, RequestConfig, Bytes};
///
/// #[derive(Debug)]
/// struct Client(reqwest::Client);
@@ -50,7 +63,7 @@ pub trait HttpSend: Sync + Send + Debug {
/// async fn response_to_http_response(
/// &self,
/// mut response: reqwest::Response,
/// ) -> Result<http::Response<Vec<u8>>> {
/// ) -> Result<http::Response<Bytes>, HttpError> {
/// // Convert the reqwest response to a http one.
/// todo!()
/// }
@@ -58,7 +71,11 @@ pub trait HttpSend: Sync + Send + Debug {
///
/// #[async_trait]
/// impl HttpSend for Client {
/// async fn send_request(&self, request: http::Request<Vec<u8>>) -> Result<http::Response<Vec<u8>>> {
/// async fn send_request(
/// &self,
/// request: http::Request<Bytes>,
/// config: RequestConfig,
/// ) -> Result<http::Response<Bytes>, HttpError> {
/// Ok(self
/// .response_to_http_response(
/// self.0
@@ -71,91 +88,153 @@ pub trait HttpSend: Sync + Send + Debug {
/// ```
async fn send_request(
&self,
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Vec<u8>>>;
request: http::Request<Bytes>,
config: RequestConfig,
) -> Result<http::Response<Bytes>, HttpError>;
}
#[derive(Clone, Debug)]
pub(crate) struct HttpClient {
pub(crate) inner: Arc<dyn HttpSend>,
pub(crate) homeserver: Arc<Url>,
pub(crate) homeserver: Arc<RwLock<Url>>,
pub(crate) session: Arc<RwLock<Option<Session>>>,
pub(crate) request_config: RequestConfig,
}
impl HttpClient {
pub(crate) fn new(
inner: Arc<dyn HttpSend>,
homeserver: Arc<RwLock<Url>>,
session: Arc<RwLock<Option<Session>>>,
request_config: RequestConfig,
) -> Self {
HttpClient { inner, homeserver, session, request_config }
}
async fn send_request<Request: OutgoingRequest>(
&self,
request: Request,
session: Arc<RwLock<Option<Session>>>,
content_type: Option<HeaderValue>,
) -> Result<http::Response<Vec<u8>>> {
let mut request = {
let read_guard;
let access_token = match Request::METADATA.authentication {
config: Option<RequestConfig>,
) -> Result<http::Response<Bytes>, HttpError> {
let config = match config {
Some(config) => config,
None => self.request_config,
};
let request = if !self.request_config.assert_identity {
self.try_into_http_request(request, session, config).await?
} else {
self.try_into_http_request_with_identity_assertion(request, session, config).await?
};
self.inner.send_request(request, config).await
}
async fn try_into_http_request<Request: OutgoingRequest>(
&self,
request: Request,
session: Arc<RwLock<Option<Session>>>,
config: RequestConfig,
) -> Result<http::Request<Bytes>, HttpError> {
let read_guard;
let access_token = if config.force_auth {
read_guard = session.read().await;
if let Some(session) = read_guard.as_ref() {
SendAccessToken::Always(session.access_token.as_str())
} else {
return Err(HttpError::ForcedAuthenticationWithoutAccessToken);
}
} else {
match Request::METADATA.authentication {
AuthScheme::AccessToken => {
read_guard = session.read().await;
if let Some(session) = read_guard.as_ref() {
Some(session.access_token.as_str())
SendAccessToken::IfRequired(session.access_token.as_str())
} else {
return Err(Error::AuthenticationRequired);
return Err(HttpError::AuthenticationRequired);
}
}
AuthScheme::None => None,
_ => return Err(Error::NotClientRequest),
};
request.try_into_http_request(&self.homeserver.to_string(), access_token)?
AuthScheme::None => SendAccessToken::None,
_ => return Err(HttpError::NotClientRequest),
}
};
if let HttpMethod::POST | HttpMethod::PUT | HttpMethod::DELETE = *request.method() {
if let Some(content_type) = content_type {
request
.headers_mut()
.append(http::header::CONTENT_TYPE, content_type);
}
}
let http_request = request
.try_into_http_request::<BytesMut>(
&self.homeserver.read().await.to_string(),
access_token,
)?
.map(|body| body.freeze());
self.inner.send_request(request).await
Ok(http_request)
}
async fn try_into_http_request_with_identity_assertion<Request: OutgoingRequest>(
&self,
request: Request,
session: Arc<RwLock<Option<Session>>>,
_: RequestConfig,
) -> Result<http::Request<Bytes>, HttpError> {
let read_guard = session.read().await;
let access_token = if let Some(session) = read_guard.as_ref() {
SendAccessToken::Always(session.access_token.as_str())
} else {
return Err(HttpError::AuthenticationRequired);
};
let user_id = if let Some(session) = read_guard.as_ref() {
session.user_id.clone()
} else {
return Err(HttpError::UserIdRequired);
};
let http_request = request
.try_into_http_request_with_user_id::<BytesMut>(
&self.homeserver.read().await.to_string(),
access_token,
user_id,
)?
.map(|body| body.freeze());
Ok(http_request)
}
pub async fn upload(
&self,
request: create_content::Request<'_>,
) -> Result<create_content::Response> {
let response = self
.send_request(request, self.session.clone(), None)
.await?;
Ok(create_content::Response::try_from(response)?)
config: Option<RequestConfig>,
) -> Result<create_content::Response, HttpError> {
let response = self.send_request(request, self.session.clone(), config).await?;
Ok(create_content::Response::try_from_http_response(response)?)
}
pub async fn send<Request>(&self, request: Request) -> Result<Request::IncomingResponse>
pub async fn send<Request>(
&self,
request: Request,
config: Option<RequestConfig>,
) -> Result<Request::IncomingResponse, HttpError>
where
Request: OutgoingRequest,
Error: From<FromHttpResponseError<Request::EndpointError>>,
Request: OutgoingRequest + Debug,
HttpError: From<FromHttpResponseError<Request::EndpointError>>,
{
let content_type = HeaderValue::from_static("application/json");
let response = self
.send_request(request, self.session.clone(), Some(content_type))
.await?;
let response = self.send_request(request, self.session.clone(), config).await?;
trace!("Got response: {:?}", response);
Ok(Request::IncomingResponse::try_from(response)?)
let response = Request::IncomingResponse::try_from_http_response(response)?;
Ok(response)
}
}
/// Build a client with the specified configuration.
pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client> {
pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client, HttpError> {
let http_client = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let http_client = {
let http_client = match config.timeout {
Some(x) => http_client.timeout(x),
None => http_client,
};
let http_client = if config.disable_ssl_verification {
http_client.danger_accept_invalid_certs(true)
} else {
@@ -171,12 +250,13 @@ pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client> {
let user_agent = match &config.user_agent {
Some(a) => a.clone(),
None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", crate::VERSION)).unwrap(),
None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", crate::VERSION))
.expect("Can't construct the version header"),
};
headers.insert(reqwest::header::USER_AGENT, user_agent);
http_client.default_headers(headers)
http_client.default_headers(headers).timeout(config.request_config.timeout)
};
#[cfg(target_arch = "wasm32")]
@@ -186,11 +266,13 @@ pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client> {
Ok(http_client.build()?)
}
async fn response_to_http_response(mut response: Response) -> Result<http::Response<Vec<u8>>> {
async fn response_to_http_response(
mut response: Response,
) -> Result<http::Response<Bytes>, reqwest::Error> {
let status = response.status();
let mut http_builder = HttpResponse::builder().status(status);
let headers = http_builder.headers_mut().unwrap();
let headers = http_builder.headers_mut().expect("Can't get the response builder headers");
for (k, v) in response.headers_mut().drain() {
if let Some(key) = k {
@@ -198,20 +280,85 @@ async fn response_to_http_response(mut response: Response) -> Result<http::Respo
}
}
let body = response.bytes().await?.as_ref().to_owned();
let body = response.bytes().await?;
Ok(http_builder.body(body).unwrap())
Ok(http_builder.body(body).expect("Can't construct a response using the given body"))
}
#[async_trait]
#[cfg(any(target_arch = "wasm32"))]
async fn send_request(
client: &Client,
request: http::Request<Bytes>,
_: RequestConfig,
) -> Result<http::Response<Bytes>, HttpError> {
let request = reqwest::Request::try_from(request)?;
let response = client.execute(request).await?;
Ok(response_to_http_response(response).await?)
}
#[cfg(all(not(target_arch = "wasm32")))]
async fn send_request(
client: &Client,
request: http::Request<Bytes>,
config: RequestConfig,
) -> Result<http::Response<Bytes>, HttpError> {
let mut backoff = ExponentialBackoff::default();
let mut request = reqwest::Request::try_from(request)?;
let retry_limit = config.retry_limit;
let retry_count = AtomicU64::new(1);
*request.timeout_mut() = Some(config.timeout);
backoff.max_elapsed_time = config.retry_timeout;
let request = &request;
let retry_count = &retry_count;
let request = || async move {
let stop = if let Some(retry_limit) = retry_limit {
retry_count.fetch_add(1, Ordering::Relaxed) >= retry_limit
} else {
false
};
// Turn errors into permanent errors when the retry limit is reached
let error_type = if stop { RetryError::Permanent } else { RetryError::Transient };
let request = request.try_clone().ok_or(HttpError::UnableToCloneRequest)?;
let response =
client.execute(request).await.map_err(|e| error_type(HttpError::Reqwest(e)))?;
let status_code = response.status();
// TODO TOO_MANY_REQUESTS will have a retry timeout which we should
// use.
if !stop
&& (status_code.is_server_error() || response.status() == StatusCode::TOO_MANY_REQUESTS)
{
return Err(error_type(HttpError::Server(status_code)));
}
let response = response_to_http_response(response)
.await
.map_err(|e| RetryError::Permanent(HttpError::Reqwest(e)))?;
Ok(response)
};
let response = retry(backoff, request).await?;
Ok(response)
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl HttpSend for Client {
async fn send_request(
&self,
request: http::Request<Vec<u8>>,
) -> Result<http::Response<Vec<u8>>> {
Ok(
response_to_http_response(self.execute(reqwest::Request::try_from(request)?).await?)
.await?,
)
request: http::Request<Bytes>,
config: RequestConfig,
) -> Result<http::Response<Bytes>, HttpError> {
send_request(self, request, config).await
}
}
+53 -27
View File
@@ -17,19 +17,21 @@
//!
//! # Enabling logging
//!
//! Users of the matrix-sdk crate can enable log output by depending on the `tracing-subscriber`
//! crate and including the following line in their application (e.g. at the start of `main`):
//! Users of the matrix-sdk crate can enable log output by depending on the
//! `tracing-subscriber` crate and including the following line in their
//! application (e.g. at the start of `main`):
//!
//! ```rust
//! tracing_subscriber::fmt::init();
//! ```
//!
//! The log output is controlled via the `RUST_LOG` environment variable by setting it to one of
//! the `error`, `warn`, `info`, `debug` or `trace` levels. The output is printed to stdout.
//! The log output is controlled via the `RUST_LOG` environment variable by
//! setting it to one of the `error`, `warn`, `info`, `debug` or `trace` levels.
//! The output is printed to stdout.
//!
//! The `RUST_LOG` variable also supports a more advanced syntax for filtering log output more
//! precisely, for instance with crate-level granularity. For more information on this, check out
//! the [tracing_subscriber
//! The `RUST_LOG` variable also supports a more advanced syntax for filtering
//! log output more precisely, for instance with crate-level granularity. For
//! more information on this, check out the [tracing_subscriber
//! documentation](https://tracing.rs/tracing_subscriber/filter/struct.envfilter).
//!
//! # Crate Feature Flags
@@ -37,13 +39,20 @@
//! The following crate feature flags are available:
//!
//! * `encryption`: Enables end-to-end encryption support in the library.
//! * `sqlite_cryptostore`: Enables a SQLite based store for the encryption
//! * `sled_cryptostore`: Enables a Sled based store for the encryption
//! keys. If this is disabled and `encryption` support is enabled the keys will
//! by default be stored only in memory and thus lost after the client is
//! destroyed.
//! * `unstable-synapse-quirks`: Enables support to deal with inconsistencies
//! of Synapse in compliance with the Matrix API specification.
//! * `markdown`: Support for sending markdown formatted messages.
//! * `socks`: Enables SOCKS support in reqwest, the default HTTP client.
//! * `sso_login`: Enables SSO login with a local http server.
//! * `require_auth_for_profile_requests`: Whether to send the access token in
//! the authentication
//! header when calling endpoints that retrieve profile data. This matches the
//! synapse configuration `require_auth_for_profile_requests`. Enabled by
//! default.
//! * `appservice`: Enables low-level appservice functionality. For an
//! high-level API there's the `matrix-sdk-appservice` crate
#![deny(
missing_debug_implementations,
@@ -63,41 +72,58 @@ compile_error!("one of 'native-tls' or 'rustls-tls' features must be enabled");
#[cfg(all(feature = "native-tls", feature = "rustls-tls",))]
compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled");
#[cfg(all(feature = "sso_login", target_arch = "wasm32"))]
compile_error!("'sso_login' cannot be enabled on 'wasm32' arch");
pub use bytes::{Bytes, BytesMut};
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use matrix_sdk_base::crypto::LocalTrust;
#[cfg(not(target_arch = "wasm32"))]
pub use matrix_sdk_base::JsonStore;
pub use matrix_sdk_base::crypto::{EncryptionInfo, LocalTrust};
pub use matrix_sdk_base::{
CustomEvent, Error as BaseError, EventEmitter, Room, RoomMember, RoomState, Session,
StateStore, SyncRoom,
media, Error as BaseError, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType,
Session, StateChanges, StoreError,
};
#[cfg(feature = "messages")]
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
pub use matrix_sdk_base::{MessageQueue, PossiblyRedactedExt};
pub use matrix_sdk_common::*;
pub use reqwest;
#[cfg(feature = "appservice")]
pub use ruma::{
api::{appservice as api_appservice, IncomingRequest, OutgoingRequestAppserviceExt},
serde::{exports::serde::de::value::Error as SerdeError, urlencoded},
};
pub use ruma::{
api::{
client as api,
error::{
FromHttpRequestError, FromHttpResponseError, IntoHttpError, MatrixError, ServerError,
},
AuthScheme, EndpointError, IncomingResponse, OutgoingRequest, SendAccessToken,
},
assign, directory, encryption, events, identifiers, int, presence, push, receipt,
serde::{CanonicalJsonValue, Raw},
thirdparty, uint, Int, MilliSecondsSinceUnixEpoch, Outgoing, SecondsSinceUnixEpoch, UInt,
};
mod client;
mod error;
mod event_handler;
mod http_client;
/// High-level room API
pub mod room;
/// High-level room API
mod room_member;
#[cfg(feature = "encryption")]
mod device;
#[cfg(feature = "encryption")]
mod sas;
pub mod verification;
pub use client::{Client, ClientConfig, LoopCtrl, SyncSettings};
pub use client::{Client, ClientConfig, LoopCtrl, RequestConfig, SyncSettings};
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use device::Device;
pub use error::{Error, Result};
pub use error::{Error, HttpError, Result};
pub use event_handler::{CustomEvent, EventHandler};
pub use http_client::HttpSend;
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use sas::Sas;
pub use room_member::RoomMember;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
+337
View File
@@ -0,0 +1,337 @@
use std::{ops::Deref, sync::Arc};
use matrix_sdk_base::deserialized_responses::MembersResponse;
use matrix_sdk_common::locks::Mutex;
use ruma::{
api::client::r0::{
membership::{get_member_events, join_room_by_id, leave_room},
message::get_message_events,
},
events::room::history_visibility::HistoryVisibility,
UserId,
};
use crate::{
media::{MediaFormat, MediaRequest, MediaType},
room::RoomType,
BaseRoom, Client, Result, RoomMember,
};
/// A struct containing methods that are common for Joined, Invited and Left
/// Rooms
#[derive(Debug, Clone)]
pub struct Common {
inner: BaseRoom,
pub(crate) client: Client,
}
impl Deref for Common {
type Target = BaseRoom;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Common {
/// Create a new `room::Common`
///
/// # Arguments
/// * `client` - The client used to make requests.
///
/// * `room` - The underlying room.
pub fn new(client: Client, room: BaseRoom) -> Self {
// TODO: Make this private
Self { inner: room, client }
}
/// Leave this room.
///
/// Only invited and joined rooms can be left
pub(crate) async fn leave(&self) -> Result<()> {
let request = leave_room::Request::new(self.inner.room_id());
let _response = self.client.send(request, None).await?;
Ok(())
}
/// Join this room.
///
/// Only invited and left rooms can be joined via this method
pub(crate) async fn join(&self) -> Result<()> {
let request = join_room_by_id::Request::new(self.inner.room_id());
let _response = self.client.send(request, None).await?;
Ok(())
}
/// Gets the avatar of this room, if set.
///
/// Returns the avatar.
/// If a thumbnail is requested no guarantee on the size of the image is
/// given.
///
/// # Arguments
///
/// * `format` - The desired format of the avatar.
///
/// # Example
/// ```no_run
/// # use futures::executor::block_on;
/// # use matrix_sdk::Client;
/// # use matrix_sdk::identifiers::room_id;
/// # use matrix_sdk::media::MediaFormat;
/// # use url::Url;
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # block_on(async {
/// # let user = "example";
/// let client = Client::new(homeserver).unwrap();
/// client.login(user, "password", None, None).await.unwrap();
/// let room_id = room_id!("!roomid:example.com");
/// let room = client
/// .get_joined_room(&room_id)
/// .unwrap();
/// if let Some(avatar) = room.avatar(MediaFormat::File).await.unwrap() {
/// std::fs::write("avatar.png", avatar);
/// }
/// # })
/// ```
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
if let Some(url) = self.avatar_url() {
let request = MediaRequest { media_type: MediaType::Uri(url.clone()), format };
Ok(Some(self.client.get_media_content(&request, true).await?))
} else {
Ok(None)
}
}
/// Sends a request to `/_matrix/client/r0/rooms/{room_id}/messages` and
/// returns a `get_message_events::Response` that contains a chunk of
/// room and state events (`AnyRoomEvent` and `AnyStateEvent`).
///
/// # Arguments
///
/// * `request` - The easiest way to create this request is using the
/// `get_message_events::Request` itself.
///
/// # Examples
/// ```no_run
/// # use std::convert::TryFrom;
/// use matrix_sdk::Client;
/// # use matrix_sdk::identifiers::room_id;
/// # use matrix_sdk::api::r0::filter::RoomEventFilter;
/// # use matrix_sdk::api::r0::message::get_message_events::Request as MessagesRequest;
/// # use url::Url;
///
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// let room_id = room_id!("!roomid:example.com");
/// let request = MessagesRequest::backward(&room_id, "t47429-4392820_219380_26003_2265");
///
/// let mut client = Client::new(homeserver).unwrap();
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
/// # use futures::executor::block_on;
/// # block_on(async {
/// assert!(room.messages(request).await.is_ok());
/// # });
/// ```
pub async fn messages(
&self,
request: impl Into<get_message_events::Request<'_>>,
) -> Result<get_message_events::Response> {
let request = request.into();
self.client.send(request, None).await
}
pub(crate) async fn request_members(&self) -> Result<Option<MembersResponse>> {
#[allow(clippy::map_clone)]
if let Some(mutex) =
self.client.members_request_locks.get(self.inner.room_id()).map(|m| m.clone())
{
mutex.lock().await;
Ok(None)
} else {
let mutex = Arc::new(Mutex::new(()));
self.client.members_request_locks.insert(self.inner.room_id().clone(), mutex.clone());
let _guard = mutex.lock().await;
let request = get_member_events::Request::new(self.inner.room_id());
let response = self.client.send(request, None).await?;
let response =
self.client.base_client.receive_members(self.inner.room_id(), &response).await?;
self.client.members_request_locks.remove(self.inner.room_id());
Ok(Some(response))
}
}
async fn ensure_members(&self) -> Result<()> {
if !self.are_events_visible() {
return Ok(());
}
if !self.are_members_synced() {
self.request_members().await?;
}
Ok(())
}
fn are_events_visible(&self) -> bool {
if let RoomType::Invited = self.inner.room_type() {
return matches!(
self.inner.history_visibility(),
HistoryVisibility::WorldReadable | HistoryVisibility::Invited
);
}
true
}
/// Sync the member list with the server.
///
/// This method will de-duplicate requests if it is called multiple times in
/// quick succession, in that case the return value will be `None`.
pub async fn sync_members(&self) -> Result<Option<MembersResponse>> {
self.request_members().await
}
/// Get active members for this room, includes invited, joined members.
///
/// *Note*: This method will fetch the members from the homeserver if the
/// member list isn't synchronized due to member lazy loading. Because of
/// that, it might panic if it isn't run on a tokio thread.
///
/// Use [active_members_no_sync()](#method.active_members_no_sync) if you
/// want a method that doesn't do any requests.
pub async fn active_members(&self) -> Result<Vec<RoomMember>> {
self.ensure_members().await?;
self.active_members_no_sync().await
}
/// Get active members for this room, includes invited, joined members.
///
/// *Note*: This method will fetch the members from the homeserver if the
/// member list isn't synchronized due to member lazy loading. Because of
/// that, it might panic if it isn't run on a tokio thread.
///
/// Use [active_members()](#method.active_members) if you want to ensure to
/// always get the full member list.
pub async fn active_members_no_sync(&self) -> Result<Vec<RoomMember>> {
Ok(self
.inner
.active_members()
.await?
.into_iter()
.map(|member| RoomMember::new(self.client.clone(), member))
.collect())
}
/// Get all the joined members of this room.
///
/// *Note*: This method will fetch the members from the homeserver if the
/// member list isn't synchronized due to member lazy loading. Because of
/// that it might panic if it isn't run on a tokio thread.
///
/// Use [joined_members_no_sync()](#method.joined_members_no_sync) if you
/// want a method that doesn't do any requests.
pub async fn joined_members(&self) -> Result<Vec<RoomMember>> {
self.ensure_members().await?;
self.joined_members_no_sync().await
}
/// Get all the joined members of this room.
///
/// *Note*: This method will not fetch the members from the homeserver if
/// the member list isn't synchronized due to member lazy loading. Thus,
/// members could be missing from the list.
///
/// Use [joined_members()](#method.joined_members) if you want to ensure to
/// always get the full member list.
pub async fn joined_members_no_sync(&self) -> Result<Vec<RoomMember>> {
Ok(self
.inner
.joined_members()
.await?
.into_iter()
.map(|member| RoomMember::new(self.client.clone(), member))
.collect())
}
/// Get a specific member of this room.
///
/// *Note*: This method will fetch the members from the homeserver if the
/// member list isn't synchronized due to member lazy loading. Because of
/// that it might panic if it isn't run on a tokio thread.
///
/// Use [get_member_no_sync()](#method.get_member_no_sync) if you want a
/// method that doesn't do any requests.
///
/// # Arguments
///
/// * `user_id` - The ID of the user that should be fetched out of the
/// store.
pub async fn get_member(&self, user_id: &UserId) -> Result<Option<RoomMember>> {
self.ensure_members().await?;
self.get_member_no_sync(user_id).await
}
/// Get a specific member of this room.
///
/// *Note*: This method will not fetch the members from the homeserver if
/// the member list isn't synchronized due to member lazy loading. Thus,
/// members could be missing.
///
/// Use [get_member()](#method.get_member) if you want to ensure to always
/// have the full member list to chose from.
///
/// # Arguments
///
/// * `user_id` - The ID of the user that should be fetched out of the
/// store.
pub async fn get_member_no_sync(&self, user_id: &UserId) -> Result<Option<RoomMember>> {
Ok(self
.inner
.get_member(user_id)
.await?
.map(|member| RoomMember::new(self.client.clone(), member)))
}
/// Get all members for this room, includes invited, joined and left
/// members.
///
/// *Note*: This method will fetch the members from the homeserver if the
/// member list isn't synchronized due to member lazy loading. Because of
/// that it might panic if it isn't run on a tokio thread.
///
/// Use [members_no_sync()](#method.members_no_sync) if you want a
/// method that doesn't do any requests.
pub async fn members(&self) -> Result<Vec<RoomMember>> {
self.ensure_members().await?;
self.members_no_sync().await
}
/// Get all members for this room, includes invited, joined and left
/// members.
///
/// *Note*: This method will not fetch the members from the homeserver if
/// the member list isn't synchronized due to member lazy loading. Thus,
/// members could be missing.
///
/// Use [members()](#method.members) if you want to ensure to always get
/// the full member list.
pub async fn members_no_sync(&self) -> Result<Vec<RoomMember>> {
Ok(self
.inner
.members()
.await?
.into_iter()
.map(|member| RoomMember::new(self.client.clone(), member))
.collect())
}
}
+49
View File
@@ -0,0 +1,49 @@
use std::ops::Deref;
use crate::{room::Common, BaseRoom, Client, Result, RoomType};
/// A room in the invited state.
///
/// This struct contains all methods specific to a `Room` with type
/// `RoomType::Invited`. Operations may fail once the underlying `Room` changes
/// `RoomType`.
#[derive(Debug, Clone)]
pub struct Invited {
pub(crate) inner: Common,
}
impl Invited {
/// Create a new `room::Invited` if the underlying `Room` has type
/// `RoomType::Invited`.
///
/// # Arguments
/// * `client` - The client used to make requests.
///
/// * `room` - The underlying room.
pub fn new(client: Client, room: BaseRoom) -> Option<Self> {
// TODO: Make this private
if room.room_type() == RoomType::Invited {
Some(Self { inner: Common::new(client, room) })
} else {
None
}
}
/// Reject the invitation.
pub async fn reject_invitation(&self) -> Result<()> {
self.inner.leave().await
}
/// Accept the invitation.
pub async fn accept_invitation(&self) -> Result<()> {
self.inner.join().await
}
}
impl Deref for Invited {
type Target = Common;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
+617
View File
@@ -0,0 +1,617 @@
#[cfg(feature = "encryption")]
use std::sync::Arc;
use std::{io::Read, ops::Deref};
#[cfg(feature = "encryption")]
use matrix_sdk_base::crypto::AttachmentEncryptor;
#[cfg(feature = "encryption")]
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_common::{
instant::{Duration, Instant},
uuid::Uuid,
};
use mime::{self, Mime};
#[cfg(feature = "encryption")]
use ruma::events::room::EncryptedFileInit;
use ruma::{
api::client::r0::{
membership::{
ban_user,
invite_user::{self, InvitationRecipient},
kick_user, Invite3pid,
},
message::send_message_event,
read_marker::set_read_marker,
receipt::create_receipt,
redact::redact_event,
state::send_state_event,
typing::create_typing_event::{Request as TypingRequest, Typing},
},
assign,
events::{
room::{
message::{
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
MessageEventContent, MessageType, VideoMessageEventContent,
},
EncryptedFile,
},
AnyMessageEventContent, AnyStateEventContent,
},
identifiers::{EventId, UserId},
receipt::ReceiptType,
};
#[cfg(feature = "encryption")]
use tracing::instrument;
use crate::{room::Common, BaseRoom, Client, Result, RoomType};
const TYPING_NOTICE_TIMEOUT: Duration = Duration::from_secs(4);
const TYPING_NOTICE_RESEND_TIMEOUT: Duration = Duration::from_secs(3);
/// A room in the joined state.
///
/// The `JoinedRoom` contains all methods specific to a `Room` with type
/// `RoomType::Joined`. Operations may fail once the underlying `Room` changes
/// `RoomType`.
#[derive(Debug, Clone)]
pub struct Joined {
pub(crate) inner: Common,
}
impl Deref for Joined {
type Target = Common;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Joined {
/// Create a new `room::Joined` if the underlying `BaseRoom` has type
/// `RoomType::Joined`.
///
/// # Arguments
/// * `client` - The client used to make requests.
///
/// * `room` - The underlying room.
pub fn new(client: Client, room: BaseRoom) -> Option<Self> {
// TODO: Make this private
if room.room_type() == RoomType::Joined {
Some(Self { inner: Common::new(client, room) })
} else {
None
}
}
/// Leave this room.
pub async fn leave(&self) -> Result<()> {
self.inner.leave().await
}
/// Ban the user with `UserId` from this room.
///
/// # Arguments
///
/// * `user_id` - The user to ban with `UserId`.
///
/// * `reason` - The reason for banning this user.
pub async fn ban_user(&self, user_id: &UserId, reason: Option<&str>) -> Result<()> {
let request = assign!(ban_user::Request::new(self.inner.room_id(), user_id), { reason });
self.client.send(request, None).await?;
Ok(())
}
/// Kick a user out of this room.
///
/// # Arguments
///
/// * `user_id` - The `UserId` of the user that should be kicked out of the
/// room.
///
/// * `reason` - Optional reason why the room member is being kicked out.
pub async fn kick_user(&self, user_id: &UserId, reason: Option<&str>) -> Result<()> {
let request = assign!(kick_user::Request::new(self.inner.room_id(), user_id), { reason });
self.client.send(request, None).await?;
Ok(())
}
/// Invite the specified user by `UserId` to this room.
///
/// # Arguments
///
/// * `user_id` - The `UserId` of the user to invite to the room.
pub async fn invite_user_by_id(&self, user_id: &UserId) -> Result<()> {
let recipient = InvitationRecipient::UserId { user_id };
let request = invite_user::Request::new(self.inner.room_id(), recipient);
self.client.send(request, None).await?;
Ok(())
}
/// Invite the specified user by third party id to this room.
///
/// # Arguments
///
/// * `invite_id` - A third party id of a user to invite to the room.
pub async fn invite_user_by_3pid(&self, invite_id: Invite3pid<'_>) -> Result<()> {
let recipient = InvitationRecipient::ThirdPartyId(invite_id);
let request = invite_user::Request::new(self.inner.room_id(), recipient);
self.client.send(request, None).await?;
Ok(())
}
/// Activate typing notice for this room.
///
/// The typing notice remains active for 4s. It can be deactivate at any
/// point by setting typing to `false`. If this method is called while
/// the typing notice is active nothing will happen. This method can be
/// called on every key stroke, since it will do nothing while typing is
/// active.
///
/// # Arguments
///
/// * `typing` - Whether the user is typing or has stopped typing.
///
/// # Examples
///
/// ```no_run
/// use std::time::Duration;
/// use matrix_sdk::api::r0::typing::create_typing_event::Typing;
/// # use matrix_sdk::{
/// # Client, SyncSettings,
/// # identifiers::room_id,
/// # };
/// # use futures::executor::block_on;
/// # use url::Url;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// # let room_id = room_id!("!test:localhost");
/// # let room = client
/// # .get_joined_room(&room_id!("!SVkFJHzfwvuaIEawgC:localhost"))
/// # .unwrap();
///
/// room
/// .typing_notice(true)
/// .await
/// .expect("Can't get devices from server");
/// # });
/// ```
pub async fn typing_notice(&self, typing: bool) -> Result<()> {
// Only send a request to the homeserver if the old timeout has elapsed
// or the typing notice changed state within the
// TYPING_NOTICE_TIMEOUT
let send =
if let Some(typing_time) = self.client.typing_notice_times.get(self.inner.room_id()) {
if typing_time.elapsed() > TYPING_NOTICE_RESEND_TIMEOUT {
// We always reactivate the typing notice if typing is true or
// we may need to deactivate it if it's
// currently active if typing is false
typing || typing_time.elapsed() <= TYPING_NOTICE_TIMEOUT
} else {
// Only send a request when we need to deactivate typing
!typing
}
} else {
// Typing notice is currently deactivated, therefore, send a request
// only when it's about to be activated
typing
};
if send {
let typing = if typing {
self.client
.typing_notice_times
.insert(self.inner.room_id().clone(), Instant::now());
Typing::Yes(TYPING_NOTICE_TIMEOUT)
} else {
self.client.typing_notice_times.remove(self.inner.room_id());
Typing::No
};
let request =
TypingRequest::new(self.inner.own_user_id(), self.inner.room_id(), typing);
self.client.send(request, None).await?;
}
Ok(())
}
/// Send a request to notify this room that the user has read specific
/// event.
///
/// # Arguments
///
/// * `event_id` - The `EventId` specifies the event to set the read receipt
/// on.
pub async fn read_receipt(&self, event_id: &EventId) -> Result<()> {
let request =
create_receipt::Request::new(self.inner.room_id(), ReceiptType::Read, event_id);
self.client.send(request, None).await?;
Ok(())
}
/// Send a request to notify this room that the user has read up to specific
/// event.
///
/// # Arguments
///
/// * fully_read - The `EventId` of the event the user has read to.
///
/// * read_receipt - An `EventId` to specify the event to set the read
/// receipt on.
pub async fn read_marker(
&self,
fully_read: &EventId,
read_receipt: Option<&EventId>,
) -> Result<()> {
let request = assign!(set_read_marker::Request::new(self.inner.room_id(), fully_read), {
read_receipt
});
self.client.send(request, None).await?;
Ok(())
}
/// Share a group session for the given room.
///
/// This will create Olm sessions with all the users/device pairs in the
/// room if necessary and share a group session with them.
///
/// Does nothing if no group session needs to be shared.
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
async fn preshare_group_session(&self) -> Result<()> {
// TODO expose this publicly so people can pre-share a group session if
// e.g. a user starts to type a message for a room.
#[allow(clippy::map_clone)]
if let Some(mutex) =
self.client.group_session_locks.get(self.inner.room_id()).map(|m| m.clone())
{
// If a group session share request is already going on,
// await the release of the lock.
mutex.lock().await;
} else {
// Otherwise create a new lock and share the group
// session.
let mutex = Arc::new(Mutex::new(()));
self.client.group_session_locks.insert(self.inner.room_id().clone(), mutex.clone());
let _guard = mutex.lock().await;
{
let joined = self.client.store().get_joined_user_ids(self.inner.room_id()).await?;
let invited =
self.client.store().get_invited_user_ids(self.inner.room_id()).await?;
let members = joined.iter().chain(&invited);
self.client.claim_one_time_keys(members).await?;
};
let response = self.share_group_session().await;
self.client.group_session_locks.remove(self.inner.room_id());
// If one of the responses failed invalidate the group
// session as using it would end up in undecryptable
// messages.
if let Err(r) = response {
self.client.base_client.invalidate_group_session(self.inner.room_id()).await?;
return Err(r);
}
}
Ok(())
}
/// Share a group session for a room.
///
/// # Panics
///
/// Panics if the client isn't logged in.
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
#[instrument]
async fn share_group_session(&self) -> Result<()> {
let mut requests =
self.client.base_client.share_group_session(self.inner.room_id()).await?;
for request in requests.drain(..) {
let response = self.client.send_to_device(&request).await?;
self.client.base_client.mark_request_as_sent(&request.txn_id, &response).await?;
}
Ok(())
}
/// Send a room message to this room.
///
/// Returns the parsed response from the server.
///
/// If the encryption feature is enabled this method will transparently
/// encrypt the room message if this room is encrypted.
///
/// # Arguments
///
/// * `content` - The content of the message event.
///
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
/// held in its unsigned field as `transaction_id`. If not given one is
/// created for the message.
///
/// # Example
/// ```no_run
/// # use std::sync::{Arc, RwLock};
/// # use matrix_sdk::{Client, SyncSettings};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # use matrix_sdk::identifiers::room_id;
/// # use std::convert::TryFrom;
/// use matrix_sdk::events::{
/// AnyMessageEventContent,
/// room::message::{MessageEventContent, TextMessageEventContent},
/// };
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// # let room_id = room_id!("!test:localhost");
/// use matrix_sdk_common::uuid::Uuid;
///
/// let content = AnyMessageEventContent::RoomMessage(
/// MessageEventContent::text_plain("Hello world")
/// );
///
/// let txn_id = Uuid::new_v4();
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
/// room.send(content, Some(txn_id)).await.unwrap();
/// # })
/// ```
pub async fn send(
&self,
content: impl Into<AnyMessageEventContent>,
txn_id: Option<Uuid>,
) -> Result<send_message_event::Response> {
#[cfg(not(feature = "encryption"))]
let content: AnyMessageEventContent = content.into();
#[cfg(feature = "encryption")]
let content = if self.is_encrypted() {
if !self.are_members_synced() {
self.request_members().await?;
// TODO query keys here?
}
self.preshare_group_session().await?;
AnyMessageEventContent::RoomEncrypted(
self.client.base_client.encrypt(self.inner.room_id(), content).await?,
)
} else {
content.into()
};
let txn_id = txn_id.unwrap_or_else(Uuid::new_v4).to_string();
let request = send_message_event::Request::new(self.inner.room_id(), &txn_id, &content);
let response = self.client.send(request, None).await?;
Ok(response)
}
/// Send an attachment to this room.
///
/// This will upload the given data that the reader produces using the
/// [`upload()`](#method.upload) method and post an event to the given room.
/// If the room is encrypted and the encryption feature is enabled the
/// upload will be encrypted.
///
/// This is a convenience method that calls the
/// [`Client::upload()`](#Client::method.upload) and afterwards the
/// [`send()`](#method.send).
///
/// # Arguments
/// * `body` - A textual representation of the media that is going to be
/// uploaded. Usually the file name.
///
/// * `content_type` - The type of the media, this will be used as the
/// content-type header.
///
/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the
/// media.
///
/// * `txn_id` - A unique `Uuid` that can be attached to a `MessageEvent`
/// held in its unsigned field as `transaction_id`. If not given one is
/// created for the message.
///
/// # Examples
///
/// ```no_run
/// # use std::{path::PathBuf, fs::File, io::Read};
/// # use matrix_sdk::{Client, identifiers::room_id};
/// # use url::Url;
/// # use mime;
/// # use futures::executor::block_on;
/// # block_on(async {
/// # let homeserver = Url::parse("http://localhost:8080").unwrap();
/// # let mut client = Client::new(homeserver).unwrap();
/// # let room_id = room_id!("!test:localhost");
/// let path = PathBuf::from("/home/example/my-cat.jpg");
/// let mut image = File::open(path).unwrap();
///
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
/// room.send_attachment("My favorite cat", &mime::IMAGE_JPEG, &mut image, None)
/// .await
/// .expect("Can't upload my cat.");
/// # });
/// ```
pub async fn send_attachment<R: Read>(
&self,
body: &str,
content_type: &Mime,
mut reader: &mut R,
txn_id: Option<Uuid>,
) -> Result<send_message_event::Response> {
let (response, encrypted_file) = if self.is_encrypted() {
#[cfg(feature = "encryption")]
let mut reader = AttachmentEncryptor::new(reader);
#[cfg(feature = "encryption")]
let content_type = mime::APPLICATION_OCTET_STREAM;
let response = self.client.upload(&content_type, &mut reader).await?;
#[cfg(feature = "encryption")]
let keys: Option<Box<EncryptedFile>> = {
let keys = reader.finish();
Some(Box::new(
EncryptedFileInit {
url: response.content_uri.clone(),
key: keys.web_key,
iv: keys.iv,
hashes: keys.hashes,
v: keys.version,
}
.into(),
))
};
#[cfg(not(feature = "encryption"))]
let keys: Option<Box<EncryptedFile>> = None;
(response, keys)
} else {
let response = self.client.upload(content_type, &mut reader).await?;
(response, None)
};
let url = response.content_uri;
let content = match content_type.type_() {
mime::IMAGE => {
// TODO create a thumbnail using the image crate?.
MessageType::Image(assign!(
ImageMessageEventContent::plain(body.to_owned(), url, None),
{ file: encrypted_file }
))
}
mime::AUDIO => MessageType::Audio(assign!(
AudioMessageEventContent::plain(body.to_owned(), url, None),
{ file: encrypted_file }
)),
mime::VIDEO => MessageType::Video(assign!(
VideoMessageEventContent::plain(body.to_owned(), url, None),
{ file: encrypted_file }
)),
_ => MessageType::File(assign!(
FileMessageEventContent::plain(body.to_owned(), url, None),
{ file: encrypted_file }
)),
};
self.send(AnyMessageEventContent::RoomMessage(MessageEventContent::new(content)), txn_id)
.await
}
/// Send a room state event to the homeserver.
///
/// Returns the parsed response from the server.
///
/// # Arguments
///
/// * `room_id` - The id of the room that should receive the message.
///
/// * `content` - The content of the state event.
///
/// * `state_key` - A unique key which defines the overwriting semantics for
/// this piece of room state. This value is often a zero-length string.
///
/// # Example
///
/// ```no_run
/// use matrix_sdk::{
/// events::{
/// AnyStateEventContent,
/// room::member::{MemberEventContent, MembershipState},
/// },
/// identifiers::mxc_uri,
/// assign,
/// };
/// # futures::executor::block_on(async {
/// # let homeserver = url::Url::parse("http://localhost:8080").unwrap();
/// # let mut client = matrix_sdk::Client::new(homeserver).unwrap();
/// # let room_id = matrix_sdk::identifiers::room_id!("!test:localhost");
///
/// let avatar_url = mxc_uri!("mxc://example.org/avatar");
/// let member_event = assign!(MemberEventContent::new(MembershipState::Join), {
/// avatar_url: Some(avatar_url),
/// });
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
///
/// let content = AnyStateEventContent::RoomMember(member_event);
/// room.send_state_event(content, "").await.unwrap();
/// # })
/// ```
pub async fn send_state_event(
&self,
content: impl Into<AnyStateEventContent>,
state_key: &str,
) -> Result<send_state_event::Response> {
let content = content.into();
let request = send_state_event::Request::new(self.inner.room_id(), state_key, &content);
self.client.send(request, None).await
}
/// Strips all information out of an event of the room.
///
/// Returns the [`redact_event::Response`] from the server.
///
/// This cannot be undone. Users may redact their own events, and any user
/// with a power level greater than or equal to the redact power level of
/// the room may redact events there.
///
/// # Arguments
///
/// * `event_id` - The ID of the event to redact
///
/// * `reason` - The reason for the event being redacted.
///
/// * `txn_id` - A unique [`Uuid`] that can be attached to this event as
/// its transaction ID. If not given one is created for the message.
///
/// # Example
///
/// ```no_run
/// # futures::executor::block_on(async {
/// # let homeserver = url::Url::parse("http://localhost:8080").unwrap();
/// # let mut client = matrix_sdk::Client::new(homeserver).unwrap();
/// # let room_id = matrix_sdk::identifiers::room_id!("!test:localhost");
/// # let room = client
/// # .get_joined_room(&room_id)
/// # .unwrap();
/// let event_id = matrix_sdk::identifiers::event_id!("$xxxxxx:example.org");
/// let reason = Some("Indecent material");
/// room.redact(&event_id, reason, None).await.unwrap();
/// # })
/// ```
pub async fn redact(
&self,
event_id: &EventId,
reason: Option<&str>,
txn_id: Option<Uuid>,
) -> Result<redact_event::Response> {
let txn_id = txn_id.unwrap_or_else(Uuid::new_v4).to_string();
let request =
assign!(redact_event::Request::new(self.inner.room_id(), event_id, &txn_id), {
reason
});
self.client.send(request, None).await
}
}
+56
View File
@@ -0,0 +1,56 @@
use std::ops::Deref;
use ruma::api::client::r0::membership::forget_room;
use crate::{room::Common, BaseRoom, Client, Result, RoomType};
/// A room in the left state.
///
/// This struct contains all methods specific to a `Room` with type
/// `RoomType::Left`. Operations may fail once the underlying `Room` changes
/// `RoomType`.
#[derive(Debug, Clone)]
pub struct Left {
pub(crate) inner: Common,
}
impl Left {
/// Create a new `room::Left` if the underlying `Room` has type
/// `RoomType::Left`.
///
/// # Arguments
/// * `client` - The client used to make requests.
///
/// * `room` - The underlying room.
pub fn new(client: Client, room: BaseRoom) -> Option<Self> {
// TODO: Make this private
if room.room_type() == RoomType::Left {
Some(Self { inner: Common::new(client, room) })
} else {
None
}
}
/// Join this room.
pub async fn join(&self) -> Result<()> {
self.inner.join().await
}
/// Forget this room.
///
/// This communicates to the homeserver that it should forget the room.
pub async fn forget(&self) -> Result<()> {
let request = forget_room::Request::new(self.inner.room_id());
let _response = self.client.send(request, None).await?;
Ok(())
}
}
impl Deref for Left {
type Target = Common;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
+76
View File
@@ -0,0 +1,76 @@
use std::ops::Deref;
use crate::RoomType;
mod common;
mod invited;
mod joined;
mod left;
pub use self::{common::Common, invited::Invited, joined::Joined, left::Left};
/// An enum that abstracts over the different states a room can be in.
#[derive(Debug, Clone)]
pub enum Room {
/// The room in the `join` state.
Joined(Joined),
/// The room in the `left` state.
Left(Left),
/// The room in the `invited` state.
Invited(Invited),
}
impl Deref for Room {
type Target = Common;
fn deref(&self) -> &Self::Target {
match self {
Self::Joined(room) => &*room,
Self::Left(room) => &*room,
Self::Invited(room) => &*room,
}
}
}
impl From<Common> for Room {
fn from(room: Common) -> Self {
match room.room_type() {
RoomType::Joined => Self::Joined(Joined { inner: room }),
RoomType::Left => Self::Left(Left { inner: room }),
RoomType::Invited => Self::Invited(Invited { inner: room }),
}
}
}
impl From<Joined> for Room {
fn from(room: Joined) -> Self {
let room = (*room).clone();
match room.room_type() {
RoomType::Joined => Self::Joined(Joined { inner: room }),
RoomType::Left => Self::Left(Left { inner: room }),
RoomType::Invited => Self::Invited(Invited { inner: room }),
}
}
}
impl From<Left> for Room {
fn from(room: Left) -> Self {
let room = (*room).clone();
match room.room_type() {
RoomType::Joined => Self::Joined(Joined { inner: room }),
RoomType::Left => Self::Left(Left { inner: room }),
RoomType::Invited => Self::Invited(Invited { inner: room }),
}
}
}
impl From<Invited> for Room {
fn from(room: Invited) -> Self {
let room = (*room).clone();
match room.room_type() {
RoomType::Joined => Self::Joined(Joined { inner: room }),
RoomType::Left => Self::Left(Left { inner: room }),
RoomType::Invited => Self::Invited(Invited { inner: room }),
}
}
}
+1
View File
@@ -0,0 +1 @@
+70
View File
@@ -0,0 +1,70 @@
use std::ops::Deref;
use crate::{
media::{MediaFormat, MediaRequest, MediaType},
BaseRoomMember, Client, Result,
};
/// The high-level `RoomMember` representation
#[derive(Debug, Clone)]
pub struct RoomMember {
inner: BaseRoomMember,
pub(crate) client: Client,
}
impl Deref for RoomMember {
type Target = BaseRoomMember;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl RoomMember {
pub(crate) fn new(client: Client, member: BaseRoomMember) -> Self {
Self { inner: member, client }
}
/// Gets the avatar of this member, if set.
///
/// Returns the avatar.
/// If a thumbnail is requested no guarantee on the size of the image is
/// given.
///
/// # Arguments
///
/// * `format` - The desired format of the avatar.
///
/// # Example
/// ```no_run
/// # use futures::executor::block_on;
/// # use matrix_sdk::Client;
/// # use matrix_sdk::identifiers::room_id;
/// # use matrix_sdk::RoomMember;
/// # use matrix_sdk::media::MediaFormat;
/// # use url::Url;
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # block_on(async {
/// # let user = "example";
/// let client = Client::new(homeserver).unwrap();
/// client.login(user, "password", None, None).await.unwrap();
/// let room_id = room_id!("!roomid:example.com");
/// let room = client
/// .get_joined_room(&room_id)
/// .unwrap();
/// let members = room.members().await.unwrap();
/// let member = members.first().unwrap();
/// if let Some(avatar) = member.avatar(MediaFormat::File).await.unwrap() {
/// std::fs::write("avatar.png", avatar);
/// }
/// # })
/// ```
pub async fn avatar(&self, format: MediaFormat) -> Result<Option<Vec<u8>>> {
if let Some(url) = self.avatar_url() {
let request = MediaRequest { media_type: MediaType::Uri(url.clone()), format };
Ok(Some(self.client.get_media_content(&request, true).await?))
} else {
Ok(None)
}
}
}
-92
View File
@@ -1,92 +0,0 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_base::crypto::{ReadOnlyDevice, Sas as BaseSas};
use matrix_sdk_common::api::r0::to_device::send_event_to_device::Request as ToDeviceRequest;
use crate::{error::Result, http_client::HttpClient};
/// An object controling the interactive verification flow.
#[derive(Debug, Clone)]
pub struct Sas {
pub(crate) inner: BaseSas,
pub(crate) http_client: HttpClient,
}
impl Sas {
/// Accept the interactive verification flow.
pub async fn accept(&self) -> Result<()> {
if let Some(req) = self.inner.accept() {
let txn_id_string = req.txn_id_string();
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
self.http_client.send(request).await?;
}
Ok(())
}
/// Confirm that the short auth strings match on both sides.
pub async fn confirm(&self) -> Result<()> {
let (to_device, signature) = self.inner.confirm().await?;
if let Some(req) = to_device {
let txn_id_string = req.txn_id_string();
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
self.http_client.send(request).await?;
}
if let Some(s) = signature {
self.http_client.send(s).await?;
}
Ok(())
}
/// Cancel the interactive verification flow.
pub async fn cancel(&self) -> Result<()> {
if let Some(req) = self.inner.cancel() {
let txn_id_string = req.txn_id_string();
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
self.http_client.send(request).await?;
}
Ok(())
}
/// Get the emoji version of the short auth string.
pub fn emoji(&self) -> Option<Vec<(&'static str, &'static str)>> {
self.inner.emoji()
}
/// Get the decimal version of the short auth string.
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
self.inner.decimals()
}
/// Is the verification process done.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Is the verification process canceled.
pub fn is_canceled(&self) -> bool {
self.inner.is_canceled()
}
/// Get the other users device that we're veryfying.
pub fn other_device(&self) -> ReadOnlyDevice {
self.inner.other_device()
}
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Interactive verification for E2EE capable users and devices in Matrix.
//!
//! The SDK supports interactive verification of devices and users, this module
//! contains types that model and support different verification flows.
//!
//! A verification flow usually starts its life as a [VerificationRequest], the
//! request can then be accepted, or it needs to be accepted by the other side
//! of the verification flow.
//!
//! Once both sides have agreed to pereform the verification, and the
//! [VerificationRequest::is_ready()] method returns true, the verification can
//! transition into one of the supported verification flows:
//!
//! * [SasVerification] - Interactive verification using a short authentication
//! string.
//! * [QrVerification] - Interactive verification using QR codes.
mod qrcode;
mod requests;
mod sas;
pub use qrcode::QrVerification;
pub use requests::VerificationRequest;
pub use sas::SasVerification;
/// An enum over the different verification types the SDK supports.
#[derive(Debug, Clone)]
pub enum Verification {
/// The `m.sas.v1` verification variant.
SasV1(SasVerification),
/// The `m.qr_code.*.v1` verification variant.
QrV1(QrVerification),
}
impl Verification {
/// Try to deconstruct this verification enum into a SAS verification.
pub fn sas(self) -> Option<SasVerification> {
if let Verification::SasV1(sas) = self {
Some(sas)
} else {
None
}
}
/// Try to deconstruct this verification enum into a QR code verification.
pub fn qr(self) -> Option<QrVerification> {
if let Verification::QrV1(qr) = self {
Some(qr)
} else {
None
}
}
/// Has this verification finished.
pub fn is_done(&self) -> bool {
match self {
Verification::SasV1(s) => s.is_done(),
Verification::QrV1(qr) => qr.is_done(),
}
}
/// Has the verification been cancelled.
pub fn is_cancelled(&self) -> bool {
match self {
Verification::SasV1(s) => s.is_cancelled(),
Verification::QrV1(qr) => qr.is_cancelled(),
}
}
/// Get our own user id.
pub fn own_user_id(&self) -> &ruma::UserId {
match self {
Verification::SasV1(v) => v.own_user_id(),
Verification::QrV1(v) => v.own_user_id(),
}
}
/// Get the user id of the other user participating in this verification
/// flow.
pub fn other_user_id(&self) -> &ruma::UserId {
match self {
Verification::SasV1(v) => v.inner.other_user_id(),
Verification::QrV1(v) => v.inner.other_user_id(),
}
}
/// Is this a verification that is veryfying one of our own devices.
pub fn is_self_verification(&self) -> bool {
match self {
Verification::SasV1(v) => v.is_self_verification(),
Verification::QrV1(v) => v.is_self_verification(),
}
}
}
impl From<SasVerification> for Verification {
fn from(sas: SasVerification) -> Self {
Self::SasV1(sas)
}
}
impl From<QrVerification> for Verification {
fn from(qr: QrVerification) -> Self {
Self::QrV1(qr)
}
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_base::crypto::{
matrix_qrcode::{qrcode::QrCode, EncodingError},
QrVerification as BaseQrVerification,
};
use ruma::UserId;
use crate::{Client, Result};
/// An object controlling QR code style key verification flows.
#[derive(Debug, Clone)]
pub struct QrVerification {
pub(crate) inner: BaseQrVerification,
pub(crate) client: Client,
}
impl QrVerification {
/// Get our own user id.
pub fn own_user_id(&self) -> &UserId {
self.inner.user_id()
}
/// Is this a verification that is veryfying one of our own devices.
pub fn is_self_verification(&self) -> bool {
self.inner.is_self_verification()
}
/// Has this verification finished.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Has the verification been cancelled.
pub fn is_cancelled(&self) -> bool {
self.inner.is_cancelled()
}
/// Generate a QR code object that is representing this verification flow.
///
/// The `QrCode` can then be rendered as an image or as an unicode string.
///
/// The [`to_bytes()`](#method.to_bytes) method can be used to instead
/// output the raw bytes that should be encoded as a QR code.
pub fn to_qr_code(&self) -> std::result::Result<QrCode, EncodingError> {
self.inner.to_qr_code()
}
/// Generate a the raw bytes that should be encoded as a QR code is
/// representing this verification flow.
///
/// The [`to_qr_code()`](#method.to_qr_code) method can be used to instead
/// output a `QrCode` object that can be rendered.
pub fn to_bytes(&self) -> std::result::Result<Vec<u8>, EncodingError> {
self.inner.to_bytes()
}
/// Confirm that the other side has scanned our QR code.
pub async fn confirm(&self) -> Result<()> {
if let Some(request) = self.inner.confirm_scanning() {
self.client.send_verification_request(request).await?;
}
Ok(())
}
/// Abort the verification flow and notify the other side that we did so.
pub async fn cancel(&self) -> Result<()> {
if let Some(request) = self.inner.cancel() {
self.client.send_verification_request(request).await?;
}
Ok(())
}
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_base::crypto::VerificationRequest as BaseVerificationRequest;
use ruma::events::key::verification::VerificationMethod;
use super::{QrVerification, SasVerification};
use crate::{Client, Result};
/// An object controlling the interactive verification flow.
#[derive(Debug, Clone)]
pub struct VerificationRequest {
pub(crate) inner: BaseVerificationRequest,
pub(crate) client: Client,
}
impl VerificationRequest {
/// Has this verification finished.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Has the verification been cancelled.
pub fn is_cancelled(&self) -> bool {
self.inner.is_cancelled()
}
/// Get our own user id.
pub fn own_user_id(&self) -> &ruma::UserId {
self.inner.own_user_id()
}
/// Has the verification request been answered by another device.
pub fn is_passive(&self) -> bool {
self.inner.is_passive()
}
/// Is the verification request ready to start a verification flow.
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
/// Get the user id of the other user participating in this verification
/// flow.
pub fn other_user_id(&self) -> &ruma::UserId {
self.inner.other_user()
}
/// Is this a verification that is veryfying one of our own devices.
pub fn is_self_verification(&self) -> bool {
self.inner.is_self_verification()
}
/// Get the supported verification methods of the other side.
///
/// Will be present only if the other side requested the verification or if
/// we're in the ready state.
pub fn their_supported_methods(&self) -> Option<Vec<VerificationMethod>> {
self.inner.their_supported_methods()
}
/// Accept the verification request.
///
/// This method will accept the request and signal that it supports the
/// `m.sas.v1`, the `m.qr_code.show.v1`, and `m.reciprocate.v1` method.
///
/// If QR code scanning should be supported or QR code showing shouldn't be
/// supported the [`accept_with_methods()`] method should be used instead.
///
/// [`accept_with_methods()`]: #method.accept_with_methods
pub async fn accept(&self) -> Result<()> {
if let Some(request) = self.inner.accept() {
self.client.send_verification_request(request).await?;
}
Ok(())
}
/// Accept the verification request signaling that our client supports the
/// given verification methods.
///
/// # Arguments
///
/// * `methods` - The methods that we should advertise as supported by us.
pub async fn accept_with_methods(&self, methods: Vec<VerificationMethod>) -> Result<()> {
if let Some(request) = self.inner.accept_with_methods(methods) {
self.client.send_verification_request(request).await?;
}
Ok(())
}
/// Generate a QR code
pub async fn generate_qr_code(&self) -> Result<Option<QrVerification>> {
Ok(self
.inner
.generate_qr_code()
.await?
.map(|qr| QrVerification { inner: qr, client: self.client.clone() }))
}
/// Transition from this verification request into a SAS verification flow.
pub async fn start_sas(&self) -> Result<Option<SasVerification>> {
if let Some((sas, request)) = self.inner.start_sas().await? {
self.client.send_verification_request(request).await?;
Ok(Some(SasVerification { inner: sas, client: self.client.clone() }))
} else {
Ok(None)
}
}
/// Cancel the verification request
pub async fn cancel(&self) -> Result<()> {
if let Some(request) = self.inner.cancel() {
self.client.send_verification_request(request).await?;
}
Ok(())
}
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_base::crypto::{AcceptSettings, ReadOnlyDevice, Sas as BaseSas};
use ruma::UserId;
use crate::{error::Result, Client};
/// An object controlling the interactive verification flow.
#[derive(Debug, Clone)]
pub struct SasVerification {
pub(crate) inner: BaseSas,
pub(crate) client: Client,
}
impl SasVerification {
/// Accept the interactive verification flow.
pub async fn accept(&self) -> Result<()> {
self.accept_with_settings(Default::default()).await
}
/// Accept the interactive verification flow with specific settings.
///
/// # Arguments
///
/// * `settings` - specific customizations to the verification flow.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::Client;
/// # use futures::executor::block_on;
/// # use url::Url;
/// # use ruma::identifiers::user_id;
/// use matrix_sdk::verification::SasVerification;
/// use matrix_sdk_base::crypto::AcceptSettings;
/// use matrix_sdk::events::key::verification::ShortAuthenticationString;
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # let client = Client::new(homeserver).unwrap();
/// # let flow_id = "someID";
/// # let user_id = user_id!("@alice:example");
/// # block_on(async {
/// let sas = client
/// .get_verification(&user_id, flow_id)
/// .await
/// .unwrap()
/// .sas()
/// .unwrap();
///
/// let only_decimal = AcceptSettings::with_allowed_methods(
/// vec![ShortAuthenticationString::Decimal]
/// );
/// sas.accept_with_settings(only_decimal).await.unwrap();
/// # });
/// ```
pub async fn accept_with_settings(&self, settings: AcceptSettings) -> Result<()> {
if let Some(request) = self.inner.accept_with_settings(settings) {
self.client.send_verification_request(request).await?;
}
Ok(())
}
/// Confirm that the short auth strings match on both sides.
pub async fn confirm(&self) -> Result<()> {
let (request, signature) = self.inner.confirm().await?;
if let Some(request) = request {
self.client.send_verification_request(request).await?;
}
if let Some(s) = signature {
self.client.send(s, None).await?;
}
Ok(())
}
/// Cancel the interactive verification flow.
pub async fn cancel(&self) -> Result<()> {
if let Some(request) = self.inner.cancel() {
self.client.send_verification_request(request).await?;
}
Ok(())
}
/// Get the emoji version of the short auth string.
pub fn emoji(&self) -> Option<[(&'static str, &'static str); 7]> {
self.inner.emoji()
}
/// Get the decimal version of the short auth string.
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
self.inner.decimals()
}
/// Does this verification flow support emoji for the short authentication
/// string.
pub fn supports_emoji(&self) -> bool {
self.inner.supports_emoji()
}
/// Is the verification process done.
pub fn is_done(&self) -> bool {
self.inner.is_done()
}
/// Are we in a state where we can show the short auth string.
pub fn can_be_presented(&self) -> bool {
self.inner.can_be_presented()
}
/// Is the verification process canceled.
pub fn is_cancelled(&self) -> bool {
self.inner.is_cancelled()
}
/// Get the other users device that we're verifying.
pub fn other_device(&self) -> &ReadOnlyDevice {
self.inner.other_device()
}
/// Did this verification flow start from a verification request.
pub fn started_from_request(&self) -> bool {
self.inner.started_from_request()
}
/// Is this a verification that is veryfying one of our own devices.
pub fn is_self_verification(&self) -> bool {
self.inner.is_self_verification()
}
/// Get our own user id.
pub fn own_user_id(&self) -> &UserId {
self.inner.user_id()
}
}
+48
View File
@@ -0,0 +1,48 @@
[package]
authors = ["Johannes Becker <j.becker@famedly.com>"]
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio", "appservice"]
license = "Apache-2.0"
name = "matrix-sdk-appservice"
version = "0.1.0"
[features]
default = ["warp"]
actix = ["actix-rt", "actix-web"]
docs = ["actix", "warp"]
[dependencies]
actix-rt = { version = "2", optional = true }
actix-web = { version = "4.0.0-beta.6", optional = true }
dashmap = "4"
futures = "0.3"
futures-util = "0.3"
http = "0.2"
regex = "1"
serde = "1"
serde_json = "1"
serde_yaml = "0.8"
thiserror = "1.0"
tracing = "0.1"
url = "2"
warp = { git = "https://github.com/seanmonstar/warp.git", rev = "629405", optional = true, default-features = false }
matrix-sdk = { version = "0.3", path = "../matrix_sdk", default-features = false, features = ["appservice", "native-tls"] }
[dependencies.ruma]
version = "0.2.0"
features = ["client-api-c", "appservice-api-s", "unstable-pre-spec"]
[dev-dependencies]
env_logger = "0.8"
mockito = "0.30"
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }
tracing-subscriber = "0.2"
matrix-sdk-test = { version = "0.3", path = "../matrix_sdk_test", features = ["appservice"] }
[[example]]
name = "appservice_autojoin"
required-features = ["warp"]
@@ -0,0 +1,75 @@
use std::{convert::TryFrom, env};
use matrix_sdk_appservice::{
matrix_sdk::{
async_trait,
events::{
room::member::{MemberEventContent, MembershipState},
SyncStateEvent,
},
identifiers::UserId,
room::Room,
EventHandler,
},
AppService, AppServiceRegistration,
};
use tracing::{error, trace};
struct AppServiceEventHandler {
appservice: AppService,
}
impl AppServiceEventHandler {
pub fn new(appservice: AppService) -> Self {
Self { appservice }
}
pub async fn handle_room_member(
&self,
room: Room,
event: &SyncStateEvent<MemberEventContent>,
) -> Result<(), Box<dyn std::error::Error>> {
if !self.appservice.user_id_is_in_namespace(&event.state_key)? {
trace!("not an appservice user: {}", event.state_key);
} else if let MembershipState::Invite = event.content.membership {
let user_id = UserId::try_from(event.state_key.clone())?;
let appservice = self.appservice.clone();
appservice.register_virtual_user(user_id.localpart()).await?;
let client = appservice.virtual_user_client(user_id.localpart()).await?;
client.join_room_by_id(room.room_id()).await?;
}
Ok(())
}
}
#[async_trait]
impl EventHandler for AppServiceEventHandler {
async fn on_room_member(&self, room: Room, event: &SyncStateEvent<MemberEventContent>) {
match self.handle_room_member(room, event).await {
Ok(_) => (),
Err(error) => error!("{:?}", error),
}
}
}
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("RUST_LOG", "matrix_sdk=debug,matrix_sdk_appservice=debug");
tracing_subscriber::fmt::init();
let homeserver_url = "http://localhost:8008";
let server_name = "localhost";
let registration = AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?;
let mut appservice = AppService::new(homeserver_url, server_name, registration).await?;
appservice.set_event_handler(Box::new(AppServiceEventHandler::new(appservice.clone()))).await?;
let (host, port) = appservice.registration().get_host_and_port()?;
appservice.run(host, port).await?;
Ok(())
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright 2021 Famedly GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("missing access token")]
MissingAccessToken,
#[error("missing host on registration url")]
MissingRegistrationHost,
#[error("http request builder error")]
UnknownHttpRequestBuilder,
#[error("no port found")]
MissingRegistrationPort,
#[error("no client for localpart found")]
NoClientForLocalpart,
#[error("could not convert host:port to socket addr")]
HostPortToSocketAddrs,
#[error(transparent)]
HttpRequest(#[from] ruma::api::error::FromHttpRequestError),
#[error(transparent)]
Identifier(#[from] ruma::identifiers::Error),
#[error(transparent)]
Http(#[from] http::Error),
#[error(transparent)]
Url(#[from] url::ParseError),
#[error(transparent)]
Serde(#[from] serde::de::value::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
InvalidUri(#[from] http::uri::InvalidUri),
#[error(transparent)]
Matrix(#[from] matrix_sdk::Error),
#[error(transparent)]
Regex(#[from] regex::Error),
#[error(transparent)]
SerdeYaml(#[from] serde_yaml::Error),
#[error(transparent)]
SerdeJson(#[from] serde_json::Error),
#[cfg(feature = "warp")]
#[error("warp rejection: {0}")]
WarpRejection(String),
#[cfg(feature = "actix")]
#[error(transparent)]
Actix(#[from] actix_web::Error),
#[cfg(feature = "actix")]
#[error(transparent)]
ActixPayload(#[from] actix_web::error::PayloadError),
}
#[cfg(feature = "actix")]
impl actix_web::error::ResponseError for Error {}
#[cfg(feature = "warp")]
impl warp::reject::Reject for Error {}
#[cfg(feature = "warp")]
impl From<warp::Rejection> for Error {
fn from(rejection: warp::Rejection) -> Self {
Self::WarpRejection(format!("{:?}", rejection))
}
}
+523
View File
@@ -0,0 +1,523 @@
// Copyright 2021 Famedly GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Matrix [Application Service] library
//!
//! The appservice crate aims to provide a batteries-included experience by
//! being a thin wrapper around the [`matrix_sdk`]. That means that we
//!
//! * ship with functionality to configure your webserver crate or simply run
//! the webserver for you
//! * receive and validate requests from the homeserver correctly
//! * allow calling the homeserver with proper virtual user identity assertion
//! * have consistent room state by leveraging matrix-sdk's state store
//! * provide E2EE support by leveraging matrix-sdk's crypto store
//!
//! # Status
//!
//! The crate is in an experimental state. Follow
//! [matrix-org/matrix-rust-sdk#228] for progress.
//!
//! # Quickstart
//!
//! ```no_run
//! # async {
//! #
//! # use matrix_sdk::{async_trait, EventHandler};
//! #
//! # struct MyEventHandler;
//! #
//! # #[async_trait]
//! # impl EventHandler for MyEventHandler {}
//! #
//! use matrix_sdk_appservice::{AppService, AppServiceRegistration};
//!
//! let homeserver_url = "http://127.0.0.1:8008";
//! let server_name = "localhost";
//! let registration = AppServiceRegistration::try_from_yaml_str(
//! r"
//! id: appservice
//! url: http://127.0.0.1:9009
//! as_token: as_token
//! hs_token: hs_token
//! sender_localpart: _appservice
//! namespaces:
//! users:
//! - exclusive: true
//! regex: '@_appservice_.*'
//! ")?;
//!
//! let mut appservice = AppService::new(homeserver_url, server_name, registration).await?;
//! appservice.set_event_handler(Box::new(MyEventHandler)).await?;
//!
//! let (host, port) = appservice.registration().get_host_and_port()?;
//! appservice.run(host, port).await?;
//! #
//! # Ok::<(), Box<dyn std::error::Error + 'static>>(())
//! # };
//! ```
//!
//! Check the [examples directory] for fully working examples.
//!
//! [Application Service]: https://matrix.org/docs/spec/application_service/r0.1.2
//! [matrix-org/matrix-rust-sdk#228]: https://github.com/matrix-org/matrix-rust-sdk/issues/228
//! [examples directory]: https://github.com/matrix-org/matrix-rust-sdk/tree/master/matrix_sdk_appservice/examples
#[cfg(not(any(feature = "actix", feature = "warp")))]
compile_error!("one webserver feature must be enabled. available ones: `actix`, `warp`");
use std::{
convert::{TryFrom, TryInto},
fs::File,
ops::Deref,
path::PathBuf,
sync::Arc,
};
use dashmap::DashMap;
pub use error::Error;
use http::{uri::PathAndQuery, Uri};
pub use matrix_sdk;
use matrix_sdk::{reqwest::Url, Bytes, Client, ClientConfig, EventHandler, HttpError, Session};
use regex::Regex;
#[doc(inline)]
pub use ruma::api::{appservice as api, appservice::Registration};
use ruma::{
api::{
client::{
error::ErrorKind,
r0::{account::register, uiaa::UiaaResponse},
},
error::{FromHttpResponseError, ServerError},
},
assign, identifiers, DeviceId, ServerNameBox, UserId,
};
use tracing::{info, warn};
mod error;
mod webserver;
pub type Result<T> = std::result::Result<T, Error>;
pub type Host = String;
pub type Port = u16;
/// AppService Registration
///
/// Wrapper around [`Registration`]
#[derive(Debug, Clone)]
pub struct AppServiceRegistration {
inner: Registration,
}
impl AppServiceRegistration {
/// Try to load registration from yaml string
///
/// See the fields of [`Registration`] for the required format
pub fn try_from_yaml_str(value: impl AsRef<str>) -> Result<Self> {
Ok(Self { inner: serde_yaml::from_str(value.as_ref())? })
}
/// Try to load registration from yaml file
///
/// See the fields of [`Registration`] for the required format
pub fn try_from_yaml_file(path: impl Into<PathBuf>) -> Result<Self> {
let file = File::open(path.into())?;
Ok(Self { inner: serde_yaml::from_reader(file)? })
}
/// Get the host and port from the registration URL
///
/// If no port is found it falls back to scheme defaults: 80 for http and
/// 443 for https
pub fn get_host_and_port(&self) -> Result<(Host, Port)> {
let uri = Uri::try_from(&self.inner.url)?;
let host = uri.host().ok_or(Error::MissingRegistrationHost)?.to_owned();
let port = match uri.port() {
Some(port) => Ok(port.as_u16()),
None => match uri.scheme_str() {
Some("http") => Ok(80),
Some("https") => Ok(443),
_ => Err(Error::MissingRegistrationPort),
},
}?;
Ok((host, port))
}
}
impl From<Registration> for AppServiceRegistration {
fn from(value: Registration) -> Self {
Self { inner: value }
}
}
impl Deref for AppServiceRegistration {
type Target = Registration;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
type Localpart = String;
/// The `localpart` of the user associated with the application service via
/// `sender_localpart` in [`AppServiceRegistration`].
///
/// Dummy type for shared documentation
#[allow(dead_code)]
pub type MainUser = ();
/// The application service may specify the virtual user to act as through use
/// of a user_id query string parameter on the request. The user specified in
/// the query string must be covered by one of the [`AppServiceRegistration`]'s
/// `users` namespaces.
///
/// Dummy type for shared documentation
pub type VirtualUser = ();
/// AppService
#[derive(Debug, Clone)]
pub struct AppService {
homeserver_url: Url,
server_name: ServerNameBox,
registration: Arc<AppServiceRegistration>,
clients: Arc<DashMap<Localpart, Client>>,
}
impl AppService {
/// Create new AppService
///
/// Also creates and caches a [`Client`] for the [`MainUser`].
/// The default [`ClientConfig`] is used, if you want to customize it
/// use [`Self::new_with_config()`] instead.
///
/// # Arguments
///
/// * `homeserver_url` - The homeserver that the client should connect to.
/// * `server_name` - The server name to use when constructing user ids from
/// the localpart.
/// * `registration` - The [AppService Registration] to use when interacting
/// with the homeserver.
///
/// [AppService Registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
pub async fn new(
homeserver_url: impl TryInto<Url, Error = url::ParseError>,
server_name: impl TryInto<ServerNameBox, Error = identifiers::Error>,
registration: AppServiceRegistration,
) -> Result<Self> {
let appservice = Self::new_with_config(
homeserver_url,
server_name,
registration,
ClientConfig::default(),
)
.await?;
Ok(appservice)
}
/// Same as [`Self::new()`] but lets you provide a [`ClientConfig`] for the
/// [`Client`]
pub async fn new_with_config(
homeserver_url: impl TryInto<Url, Error = url::ParseError>,
server_name: impl TryInto<ServerNameBox, Error = identifiers::Error>,
registration: AppServiceRegistration,
client_config: ClientConfig,
) -> Result<Self> {
let homeserver_url = homeserver_url.try_into()?;
let server_name = server_name.try_into()?;
let registration = Arc::new(registration);
let clients = Arc::new(DashMap::new());
let sender_localpart = registration.sender_localpart.clone();
let appservice = AppService { homeserver_url, server_name, registration, clients };
// we create and cache the [`MainUser`] by default
appservice.create_and_cache_client(&sender_localpart, client_config).await?;
Ok(appservice)
}
/// Create a [`Client`] for the given [`VirtualUser`]'s `localpart`
///
/// Will create and return a [`Client`] that's configured to [assert the
/// identity] on all outgoing homeserver requests if `localpart` is
/// given.
///
/// This method is a singleton that saves the client internally for re-use
/// based on the `localpart`. The cached [`Client`] can be retrieved either
/// by calling this method again or by calling [`Self::get_cached_client()`]
/// which is non-async convenience wrapper.
///
/// Note that if you want to do actions like joining rooms with a virtual
/// user it needs to be registered first. `Self::register_virtual_user()`
/// can be used for that purpose.
///
/// # Arguments
///
/// * `localpart` - The localpart of the user we want assert our identity to
///
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
/// [assert the identity]: https://matrix.org/docs/spec/application_service/r0.1.2#identity-assertion
pub async fn virtual_user_client(&self, localpart: impl AsRef<str>) -> Result<Client> {
let client =
self.virtual_user_client_with_config(localpart, ClientConfig::default()).await?;
Ok(client)
}
/// Same as [`Self::virtual_user_client()`] but with the ability to pass in
/// a [`ClientConfig`]
///
/// Since this method is a singleton follow-up calls with different
/// [`ClientConfig`]s will be ignored.
pub async fn virtual_user_client_with_config(
&self,
localpart: impl AsRef<str>,
config: ClientConfig,
) -> Result<Client> {
// TODO: check if localpart is covered by namespace?
let localpart = localpart.as_ref();
let client = if let Some(client) = self.clients.get(localpart) {
client.clone()
} else {
self.create_and_cache_client(localpart, config).await?
};
Ok(client)
}
async fn create_and_cache_client(
&self,
localpart: &str,
config: ClientConfig,
) -> Result<Client> {
let user_id = UserId::parse_with_server_name(localpart, &self.server_name)?;
// The `as_token` in the `Session` maps to the [`MainUser`]
// (`sender_localpart`) by default, so we don't need to assert identity
// in that case
let config = if localpart != self.registration.sender_localpart {
let request_config = config.get_request_config().assert_identity();
config.request_config(request_config)
} else {
config
};
let client =
Client::new_with_config(self.homeserver_url.clone(), config.appservice_mode())?;
let session = Session {
access_token: self.registration.as_token.clone(),
user_id: user_id.clone(),
// TODO: expose & proper E2EE
device_id: DeviceId::new(),
};
client.restore_login(session).await?;
self.clients.insert(localpart.to_owned(), client.clone());
Ok(client)
}
/// Get cached [`Client`]
///
/// Will return the client for the given `localpart` if previously
/// constructed with [`Self::virtual_user_client()`] or
/// [`Self::virtual_user_client_with_config()`].
///
/// If no `localpart` is given it assumes the [`MainUser`]'s `localpart`. If
/// no client for `localpart` is found it will return an Error.
pub fn get_cached_client(&self, localpart: Option<&str>) -> Result<Client> {
let localpart = localpart.unwrap_or_else(|| self.registration.sender_localpart.as_ref());
let entry = self.clients.get(localpart).ok_or(Error::NoClientForLocalpart)?;
Ok(entry.value().clone())
}
/// Convenience wrapper around [`Client::set_event_handler()`] that attaches
/// the event handler to the [`MainUser`]'s [`Client`]
///
/// Note that the event handler in the [`AppService`] context only triggers
/// [`join` room `timeline` events], so no state events or events from the
/// `invite`, `knock` or `leave` scope. The rationale behind that is
/// that incoming AppService transactions from the homeserver are not
/// necessarily bound to a specific user but can cover a multitude of
/// namespaces, and as such the AppService basically only "observes
/// joined rooms". Also currently homeservers only push PDUs to appservices,
/// no EDUs. There's the open [MSC2409] regarding supporting EDUs in the
/// future, though it seems to be planned to put EDUs into a different
/// JSON key than `events` to stay backwards compatible.
///
/// [`join` room `timeline` events]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientr0sync
/// [MSC2409]: https://github.com/matrix-org/matrix-doc/pull/2409
pub async fn set_event_handler(&mut self, handler: Box<dyn EventHandler>) -> Result<()> {
let client = self.get_cached_client(None)?;
client.set_event_handler(handler).await;
Ok(())
}
/// Register a virtual user by sending a [`register::Request`] to the
/// homeserver
///
/// # Arguments
///
/// * `localpart` - The localpart of the user to register. Must be covered
/// by the namespaces in the [`Registration`] in order to succeed.
pub async fn register_virtual_user(&self, localpart: impl AsRef<str>) -> Result<()> {
let request = assign!(register::Request::new(), {
username: Some(localpart.as_ref()),
login_type: Some(&register::LoginType::ApplicationService),
});
let client = self.get_cached_client(None)?;
match client.register(request).await {
Ok(_) => (),
Err(error) => match error {
matrix_sdk::Error::Http(HttpError::UiaaError(FromHttpResponseError::Http(
ServerError::Known(UiaaResponse::MatrixError(ref matrix_error)),
))) => {
match matrix_error.kind {
ErrorKind::UserInUse => {
// TODO: persist the fact that we registered that user
warn!("{}", matrix_error.message);
}
_ => return Err(error.into()),
}
}
_ => return Err(error.into()),
},
}
Ok(())
}
/// Get the AppService [registration]
///
/// [registration]: https://matrix.org/docs/spec/application_service/r0.1.2#registration
pub fn registration(&self) -> &AppServiceRegistration {
&self.registration
}
/// Compare the given `hs_token` against `registration.hs_token`
///
/// Returns `true` if the tokens match, `false` otherwise.
pub fn compare_hs_token(&self, hs_token: impl AsRef<str>) -> bool {
self.registration.hs_token == hs_token.as_ref()
}
/// Check if given `user_id` is in any of the [`AppServiceRegistration`]'s
/// `users` namespaces
pub fn user_id_is_in_namespace(&self, user_id: impl AsRef<str>) -> Result<bool> {
for user in &self.registration.namespaces.users {
// TODO: precompile on AppService construction
let re = Regex::new(&user.regex)?;
if re.is_match(user_id.as_ref()) {
return Ok(true);
}
}
Ok(false)
}
/// Returns a closure to be used with [`actix_web::App::configure()`]
///
/// Note that if you handle any of the [application-service-specific
/// routes], including the legacy routes, you will break the appservice
/// functionality.
///
/// [application-service-specific routes]: https://spec.matrix.org/unstable/application-service-api/#legacy-routes
#[cfg(feature = "actix")]
#[cfg_attr(docs, doc(cfg(feature = "actix")))]
pub fn actix_configure(&self) -> impl FnOnce(&mut actix_web::web::ServiceConfig) {
let appservice = self.clone();
move |config| {
config.data(appservice);
webserver::actix::configure(config);
}
}
/// Returns a [`warp::Filter`] to be used as [`warp::serve()`] route
///
/// Note that if you handle any of the [application-service-specific
/// routes], including the legacy routes, you will break the appservice
/// functionality.
///
/// [application-service-specific routes]: https://spec.matrix.org/unstable/application-service-api/#legacy-routes
#[cfg(feature = "warp")]
#[cfg_attr(docs, doc(cfg(feature = "warp")))]
pub fn warp_filter(&self) -> warp::filters::BoxedFilter<(impl warp::Reply,)> {
webserver::warp::warp_filter(self.clone())
}
/// Convenience method that runs an http server depending on the selected
/// server feature
///
/// This is a blocking call that tries to listen on the provided host and
/// port
pub async fn run(&self, host: impl Into<String>, port: impl Into<u16>) -> Result<()> {
let host = host.into();
let port = port.into();
info!("Starting AppService on {}:{}", &host, &port);
#[cfg(feature = "actix")]
{
webserver::actix::run_server(self.clone(), host, port).await?;
Ok(())
}
#[cfg(feature = "warp")]
{
webserver::warp::run_server(self.clone(), host, port).await?;
Ok(())
}
#[cfg(not(any(feature = "actix", feature = "warp",)))]
unreachable!()
}
}
/// Transforms [legacy routes] to the correct route so ruma can parse them
/// properly
///
/// [legacy routes]: https://matrix.org/docs/spec/application_service/r0.1.2#legacy-routes
pub(crate) fn transform_legacy_route(
mut request: http::Request<Bytes>,
) -> Result<http::Request<Bytes>> {
let uri = request.uri().to_owned();
if !uri.path().starts_with("/_matrix/app/v1") {
// rename legacy routes
let mut parts = uri.into_parts();
let path_and_query = match parts.path_and_query {
Some(path_and_query) => format!("/_matrix/app/v1{}", path_and_query),
None => "/_matrix/app/v1".to_owned(),
};
parts.path_and_query =
Some(PathAndQuery::try_from(path_and_query).map_err(http::Error::from)?);
let uri = parts.try_into().map_err(http::Error::from)?;
*request.uri_mut() = uri;
}
Ok(request)
}
@@ -0,0 +1,149 @@
// Copyright 2021 Famedly GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::pin::Pin;
pub use actix_web::Scope;
use actix_web::{
dev::Payload,
error::PayloadError,
get, put,
web::{self, BytesMut, Data},
App, FromRequest, HttpRequest, HttpResponse, HttpServer,
};
use futures::Future;
use futures_util::TryStreamExt;
use ruma::api::appservice as api;
use crate::{error::Error, AppService};
pub async fn run_server(
appservice: AppService,
host: impl Into<String>,
port: impl Into<u16>,
) -> Result<(), Error> {
HttpServer::new(move || App::new().configure(appservice.actix_configure()))
.bind((host.into(), port.into()))?
.run()
.await?;
Ok(())
}
pub fn configure(config: &mut actix_web::web::ServiceConfig) {
// also handles legacy routes
config.service(push_transactions).service(query_user_id).service(query_room_alias).service(
web::scope("/_matrix/app/v1")
.service(push_transactions)
.service(query_user_id)
.service(query_room_alias),
);
}
#[tracing::instrument]
#[put("/transactions/{txn_id}")]
async fn push_transactions(
request: IncomingRequest<api::event::push_events::v1::IncomingRequest>,
appservice: Data<AppService>,
) -> Result<HttpResponse, Error> {
if !appservice.compare_hs_token(request.access_token) {
return Ok(HttpResponse::Unauthorized().finish());
}
appservice.get_cached_client(None)?.receive_transaction(request.incoming).await?;
Ok(HttpResponse::Ok().json("{}"))
}
#[tracing::instrument]
#[get("/users/{user_id}")]
async fn query_user_id(
request: IncomingRequest<api::query::query_user_id::v1::IncomingRequest>,
appservice: Data<AppService>,
) -> Result<HttpResponse, Error> {
if !appservice.compare_hs_token(request.access_token) {
return Ok(HttpResponse::Unauthorized().finish());
}
Ok(HttpResponse::Ok().json("{}"))
}
#[tracing::instrument]
#[get("/rooms/{room_alias}")]
async fn query_room_alias(
request: IncomingRequest<api::query::query_room_alias::v1::IncomingRequest>,
appservice: Data<AppService>,
) -> Result<HttpResponse, Error> {
if !appservice.compare_hs_token(request.access_token) {
return Ok(HttpResponse::Unauthorized().finish());
}
Ok(HttpResponse::Ok().json("{}"))
}
#[derive(Debug)]
pub struct IncomingRequest<T> {
access_token: String,
incoming: T,
}
impl<T: ruma::api::IncomingRequest> FromRequest for IncomingRequest<T> {
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
type Config = ();
fn from_request(request: &HttpRequest, payload: &mut Payload) -> Self::Future {
let request = request.to_owned();
let payload = payload.take();
Box::pin(async move {
let mut builder =
http::request::Builder::new().method(request.method()).uri(request.uri());
let headers = builder.headers_mut().ok_or(Error::UnknownHttpRequestBuilder)?;
for (key, value) in request.headers().iter() {
headers.append(key, value.to_owned());
}
let bytes = payload
.try_fold(BytesMut::new(), |mut body, chunk| async move {
body.extend_from_slice(&chunk);
Ok::<_, PayloadError>(body)
})
.await?
.into();
let access_token = match request.uri().query() {
Some(query) => {
let query: Vec<(String, String)> = ruma::serde::urlencoded::from_str(query)?;
query.into_iter().find(|(key, _)| key == "access_token").map(|(_, value)| value)
}
None => None,
};
let access_token = match access_token {
Some(access_token) => access_token,
None => return Err(Error::MissingAccessToken),
};
let request = builder.body(bytes)?;
let request = crate::transform_legacy_route(request)?;
Ok(IncomingRequest {
access_token,
incoming: ruma::api::IncomingRequest::try_from_http_request(request)?,
})
})
}
}
@@ -0,0 +1,4 @@
#[cfg(feature = "actix")]
pub mod actix;
#[cfg(feature = "warp")]
pub mod warp;
+210
View File
@@ -0,0 +1,210 @@
// Copyright 2021 Famedly GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{net::ToSocketAddrs, result::Result as StdResult};
use futures::TryFutureExt;
use matrix_sdk::Bytes;
use serde::Serialize;
use warp::{filters::BoxedFilter, path::FullPath, Filter, Rejection, Reply};
use crate::{AppService, Error, Result};
pub async fn run_server(
appservice: AppService,
host: impl Into<String>,
port: impl Into<u16>,
) -> Result<()> {
let routes = warp_filter(appservice);
let mut addr = format!("{}:{}", host.into(), port.into()).to_socket_addrs()?;
if let Some(addr) = addr.next() {
warp::serve(routes).run(addr).await;
Ok(())
} else {
Err(Error::HostPortToSocketAddrs)
}
}
pub fn warp_filter(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
// TODO: try to use a struct instead of needlessly cloning appservice multiple
// times on every request
warp::any()
.and(filters::transactions(appservice.clone()))
.or(filters::users(appservice.clone()))
.or(filters::rooms(appservice))
.recover(handle_rejection)
.boxed()
}
mod filters {
use super::*;
pub fn users(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
warp::get()
.and(
warp::path!("_matrix" / "app" / "v1" / "users" / String)
// legacy route
.or(warp::path!("users" / String))
.unify(),
)
.and(warp::path::end())
.and(common(appservice))
.and_then(handlers::user)
.boxed()
}
pub fn rooms(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
warp::get()
.and(
warp::path!("_matrix" / "app" / "v1" / "rooms" / String)
// legacy route
.or(warp::path!("rooms" / String))
.unify(),
)
.and(warp::path::end())
.and(common(appservice))
.and_then(handlers::room)
.boxed()
}
pub fn transactions(appservice: AppService) -> BoxedFilter<(impl Reply,)> {
warp::put()
.and(
warp::path!("_matrix" / "app" / "v1" / "transactions" / String)
// legacy route
.or(warp::path!("transactions" / String))
.unify(),
)
.and(warp::path::end())
.and(common(appservice))
.and_then(handlers::transaction)
.boxed()
}
fn common(appservice: AppService) -> BoxedFilter<(AppService, http::Request<Bytes>)> {
warp::any()
.and(filters::valid_access_token(appservice.registration().hs_token.clone()))
.map(move || appservice.clone())
.and(http_request().and_then(|request| async move {
let request = crate::transform_legacy_route(request).map_err(Error::from)?;
Ok::<http::Request<Bytes>, Rejection>(request)
}))
.boxed()
}
pub fn valid_access_token(token: String) -> BoxedFilter<()> {
warp::any()
.map(move || token.clone())
.and(warp::query::raw())
.and_then(|token: String, query: String| async move {
let query: Vec<(String, String)> =
matrix_sdk::urlencoded::from_str(&query).map_err(Error::from)?;
if query.into_iter().any(|(key, value)| key == "access_token" && value == token) {
Ok::<(), Rejection>(())
} else {
Err(warp::reject::custom(Unauthorized))
}
})
.untuple_one()
.boxed()
}
pub fn http_request() -> impl Filter<Extract = (http::Request<Bytes>,), Error = Rejection> + Copy
{
// TODO: extract `hyper::Request` instead
// blocked by https://github.com/seanmonstar/warp/issues/139
warp::any()
.and(warp::method())
.and(warp::filters::path::full())
.and(warp::filters::query::raw())
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and_then(|method, path: FullPath, query, headers, bytes| async move {
let uri = http::uri::Builder::new()
.path_and_query(format!("{}?{}", path.as_str(), query))
.build()
.map_err(Error::from)?;
let mut request = http::Request::builder()
.method(method)
.uri(uri)
.body(bytes)
.map_err(Error::from)?;
*request.headers_mut() = headers;
Ok::<http::Request<Bytes>, Rejection>(request)
})
}
}
mod handlers {
use super::*;
pub async fn user(
_user_id: String,
_appservice: AppService,
_request: http::Request<Bytes>,
) -> StdResult<impl warp::Reply, Rejection> {
Ok(warp::reply::json(&String::from("{}")))
}
pub async fn room(
_room_id: String,
_appservice: AppService,
_request: http::Request<Bytes>,
) -> StdResult<impl warp::Reply, Rejection> {
Ok(warp::reply::json(&String::from("{}")))
}
pub async fn transaction(
_txn_id: String,
appservice: AppService,
request: http::Request<Bytes>,
) -> StdResult<impl warp::Reply, Rejection> {
let incoming_transaction: matrix_sdk::api_appservice::event::push_events::v1::IncomingRequest =
matrix_sdk::IncomingRequest::try_from_http_request(request).map_err(Error::from)?;
let client = appservice.get_cached_client(None)?;
client.receive_transaction(incoming_transaction).map_err(Error::from).await?;
Ok(warp::reply::json(&String::from("{}")))
}
}
#[derive(Debug)]
struct Unauthorized;
impl warp::reject::Reject for Unauthorized {}
#[derive(Serialize)]
struct ErrorMessage {
code: u16,
message: String,
}
pub async fn handle_rejection(err: Rejection) -> std::result::Result<impl Reply, Rejection> {
if err.find::<Unauthorized>().is_some() || err.find::<warp::reject::InvalidQuery>().is_some() {
let code = http::StatusCode::UNAUTHORIZED;
let message = "UNAUTHORIZED";
let json =
warp::reply::json(&ErrorMessage { code: code.as_u16(), message: message.into() });
Ok(warp::reply::with_status(json, code))
} else {
Err(err)
}
}
@@ -0,0 +1,13 @@
id: appservice
url: http://localhost:9009
as_token: as_token
hs_token: hs_token
sender_localpart: _appservice
namespaces:
aliases: []
rooms: []
users:
- exclusive: true
regex: '@_appservice_.*'
rate_limited: false
protocols: []
+382
View File
@@ -0,0 +1,382 @@
use std::sync::{Arc, Mutex};
#[cfg(feature = "actix")]
use actix_web::{test as actix_test, App as ActixApp, HttpResponse};
use matrix_sdk::{
api_appservice::Registration,
async_trait,
events::{room::member::MemberEventContent, SyncStateEvent},
room::Room,
ClientConfig, EventHandler, RequestConfig,
};
use matrix_sdk_appservice::*;
use matrix_sdk_test::{appservice::TransactionBuilder, async_test, EventsJson};
use serde_json::json;
#[cfg(feature = "warp")]
use warp::{Filter, Reply};
fn registration_string() -> String {
include_str!("../tests/registration.yaml").to_owned()
}
async fn appservice(registration: Option<Registration>) -> Result<AppService> {
// env::set_var(
// "RUST_LOG",
// "mockito=debug,matrix_sdk=debug,ruma=debug,actix_web=debug,warp=debug",
// );
let _ = tracing_subscriber::fmt::try_init();
let registration = match registration {
Some(registration) => registration.into(),
None => AppServiceRegistration::try_from_yaml_str(registration_string()).unwrap(),
};
let homeserver_url = mockito::server_url();
let server_name = "localhost";
let client_config =
ClientConfig::default().request_config(RequestConfig::default().disable_retry());
Ok(AppService::new_with_config(
homeserver_url.as_ref(),
server_name,
registration,
client_config,
)
.await?)
}
#[async_test]
async fn test_register_virtual_user() -> Result<()> {
let appservice = appservice(None).await?;
let localpart = "someone";
let _mock = mockito::mock("POST", "/_matrix/client/r0/register")
.match_query(mockito::Matcher::Missing)
.match_header(
"authorization",
mockito::Matcher::Exact(format!("Bearer {}", appservice.registration().as_token)),
)
.match_body(mockito::Matcher::Json(json!({
"username": localpart.to_owned(),
"type": "m.login.application_service"
})))
.with_body(format!(
r#"{{
"access_token": "abc123",
"device_id": "GHTYAJCE",
"user_id": "@{localpart}:localhost"
}}"#,
localpart = localpart
))
.create();
appservice.register_virtual_user(localpart).await?;
Ok(())
}
#[async_test]
async fn test_put_transaction() -> Result<()> {
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
let mut transaction_builder = TransactionBuilder::new();
transaction_builder.add_room_event(EventsJson::Member);
let transaction = transaction_builder.build_json_transaction();
let appservice = appservice(None).await?;
#[cfg(feature = "warp")]
let status = warp::test::request()
.method("PUT")
.path(uri)
.json(&transaction)
.filter(&appservice.warp_filter())
.await
.unwrap()
.into_response()
.status();
#[cfg(feature = "actix")]
let status = {
let app =
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
actix_test::call_service(&app, req).await.status()
};
assert_eq!(status, 200);
Ok(())
}
#[async_test]
async fn test_get_user() -> Result<()> {
let appservice = appservice(None).await?;
let uri = "/_matrix/app/v1/users/%40_botty_1%3Adev.famedly.local?access_token=hs_token";
#[cfg(feature = "warp")]
let status = warp::test::request()
.method("GET")
.path(uri)
.filter(&appservice.warp_filter())
.await
.unwrap()
.into_response()
.status();
#[cfg(feature = "actix")]
let status = {
let app =
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
let req = actix_test::TestRequest::get().uri(uri).to_request();
actix_test::call_service(&app, req).await.status()
};
assert_eq!(status, 200);
Ok(())
}
#[async_test]
async fn test_get_room() -> Result<()> {
let appservice = appservice(None).await?;
let uri = "/_matrix/app/v1/rooms/%23magicforest%3Aexample.com?access_token=hs_token";
#[cfg(feature = "warp")]
let status = warp::test::request()
.method("GET")
.path(uri)
.filter(&appservice.warp_filter())
.await
.unwrap()
.into_response()
.status();
#[cfg(feature = "actix")]
let status = {
let app =
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
let req = actix_test::TestRequest::get().uri(uri).to_request();
actix_test::call_service(&app, req).await.status()
};
assert_eq!(status, 200);
Ok(())
}
#[async_test]
async fn test_invalid_access_token() -> Result<()> {
let uri = "/_matrix/app/v1/transactions/1?access_token=invalid_token";
let mut transaction_builder = TransactionBuilder::new();
let transaction =
transaction_builder.add_room_event(EventsJson::Member).build_json_transaction();
let appservice = appservice(None).await?;
#[cfg(feature = "warp")]
let status = warp::test::request()
.method("PUT")
.path(uri)
.json(&transaction)
.filter(&appservice.warp_filter())
.await
.unwrap()
.into_response()
.status();
#[cfg(feature = "actix")]
let status = {
let app =
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
actix_test::call_service(&app, req).await.status()
};
assert_eq!(status, 401);
Ok(())
}
#[async_test]
async fn test_no_access_token() -> Result<()> {
let uri = "/_matrix/app/v1/transactions/1";
let mut transaction_builder = TransactionBuilder::new();
transaction_builder.add_room_event(EventsJson::Member);
let transaction = transaction_builder.build_json_transaction();
let appservice = appservice(None).await?;
#[cfg(feature = "warp")]
{
let status = warp::test::request()
.method("PUT")
.path(uri)
.json(&transaction)
.filter(&appservice.warp_filter())
.await
.unwrap()
.into_response()
.status();
assert_eq!(status, 401);
}
#[cfg(feature = "actix")]
{
let app =
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
let resp = actix_test::call_service(&app, req).await;
// TODO: this should actually return a 401 but is 500 because something in the
// extractor fails
assert_eq!(resp.status(), 500);
}
Ok(())
}
#[async_test]
async fn test_event_handler() -> Result<()> {
let mut appservice = appservice(None).await?;
#[derive(Clone)]
struct Example {
pub on_state_member: Arc<Mutex<bool>>,
}
impl Example {
pub fn new() -> Self {
#[allow(clippy::mutex_atomic)]
Self { on_state_member: Arc::new(Mutex::new(false)) }
}
}
#[async_trait]
impl EventHandler for Example {
async fn on_room_member(&self, _: Room, _: &SyncStateEvent<MemberEventContent>) {
let on_state_member = self.on_state_member.clone();
*on_state_member.lock().unwrap() = true;
}
}
let example = Example::new();
appservice.set_event_handler(Box::new(example.clone())).await?;
let uri = "/_matrix/app/v1/transactions/1?access_token=hs_token";
let mut transaction_builder = TransactionBuilder::new();
transaction_builder.add_room_event(EventsJson::Member);
let transaction = transaction_builder.build_json_transaction();
#[cfg(feature = "warp")]
warp::test::request()
.method("PUT")
.path(uri)
.json(&transaction)
.filter(&appservice.warp_filter())
.await
.unwrap();
#[cfg(feature = "actix")]
{
let app =
actix_test::init_service(ActixApp::new().configure(appservice.actix_configure())).await;
let req = actix_test::TestRequest::put().uri(uri).set_json(&transaction).to_request();
actix_test::call_service(&app, req).await;
};
let on_room_member_called = *example.on_state_member.lock().unwrap();
assert!(on_room_member_called);
Ok(())
}
#[async_test]
async fn test_unrelated_path() -> Result<()> {
let appservice = appservice(None).await?;
#[cfg(feature = "warp")]
let status = {
let consumer_filter = warp::any()
.and(appservice.warp_filter())
.or(warp::get().and(warp::path("unrelated").map(warp::reply)));
let response = warp::test::request()
.method("GET")
.path("/unrelated")
.filter(&consumer_filter)
.await?
.into_response();
response.status()
};
#[cfg(feature = "actix")]
let status = {
let app = actix_test::init_service(
ActixApp::new()
.configure(appservice.actix_configure())
.route("/unrelated", actix_web::web::get().to(HttpResponse::Ok)),
)
.await;
let req = actix_test::TestRequest::get().uri("/unrelated").to_request();
actix_test::call_service(&app, req).await.status()
};
assert_eq!(status, 200);
Ok(())
}
mod registration {
use super::*;
#[test]
fn test_registration() -> Result<()> {
let registration: Registration = serde_yaml::from_str(&registration_string())?;
let registration: AppServiceRegistration = registration.into();
assert_eq!(registration.id, "appservice");
Ok(())
}
#[test]
fn test_registration_from_yaml_file() -> Result<()> {
let registration = AppServiceRegistration::try_from_yaml_file("./tests/registration.yaml")?;
assert_eq!(registration.id, "appservice");
Ok(())
}
#[test]
fn test_registration_from_yaml_str() -> Result<()> {
let registration = AppServiceRegistration::try_from_yaml_str(registration_string())?;
assert_eq!(registration.id, "appservice");
Ok(())
}
}
+36 -22
View File
@@ -8,48 +8,62 @@ license = "Apache-2.0"
name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.2.0"
version = "0.3.0"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = ["encryption", "sqlite_cryptostore", "messages"]
messages = []
default = []
encryption = ["matrix-sdk-crypto"]
sqlite_cryptostore = ["matrix-sdk-crypto/sqlite_cryptostore"]
unstable-synapse-quirks = ["matrix-sdk-common/unstable-synapse-quirks"]
sled_state_store = ["sled", "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305"]
sled_cryptostore = ["matrix-sdk-crypto/sled_cryptostore"]
markdown = ["ruma/markdown"]
docs = ["encryption", "sqlite_cryptostore", "messages"]
docs = ["encryption", "sled_cryptostore"]
[dependencies]
serde = "1.0.118"
serde_json = "1.0.61"
zeroize = "1.2.0"
tracing = "0.1.22"
dashmap = "4.0.2"
lru = "0.6.5"
ruma = { version = "0.2.0", features = ["client-api-c", "unstable-pre-spec"] }
serde = { version = "1.0.126", features = ["rc"] }
serde_json = "1.0.64"
tracing = "0.1.26"
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
matrix-sdk-crypto = { version = "0.2.0", path = "../matrix_sdk_crypto", optional = true }
matrix-sdk-common = { version = "0.3.0", path = "../matrix_sdk_common" }
matrix-sdk-crypto = { version = "0.3.0", path = "../matrix_sdk_crypto", optional = true }
# Misc dependencies
thiserror = "1.0.23"
thiserror = "1.0.25"
futures = "0.3.15"
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
# Deps for the sled state store
sled = { version = "0.34.6", optional = true }
chacha20poly1305 = { version = "0.8.0", optional = true }
pbkdf2 = { version = "0.8.0", default-features = false, optional = true }
hmac = { version = "0.11.0", optional = true }
sha2 = { version = "0.9.5", optional = true }
rand = { version = "0.8.4", optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.24"
version = "1.7.1"
default-features = false
features = ["sync", "fs"]
[dev-dependencies]
futures = "0.3.8"
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
http = "0.2.2"
tracing-subscriber = "0.2.15"
tempfile = "3.1.0"
mockito = "0.28.0"
matrix-sdk-test = { version = "0.3.0", path = "../matrix_sdk_test" }
http = "0.2.4"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
tokio = { version = "1.7.1", default-features = false, features = ["rt-multi-thread", "macros"] }
tempfile = "3.2.0"
rustyline = "8.2.0"
rustyline-derive = "0.4.0"
atty = "0.2.14"
clap = "2.33.3"
syntect = "4.5.0"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.19"
wasm-bindgen-test = "0.3.24"
+390
View File
@@ -0,0 +1,390 @@
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
#[cfg(not(target_arch = "wasm32"))]
use atty::Stream;
#[cfg(not(target_arch = "wasm32"))]
use clap::{App as Argparse, AppSettings as ArgParseSettings, Arg, ArgMatches, SubCommand};
use futures::executor::block_on;
use matrix_sdk_base::{RoomInfo, Store};
use ruma::{
events::EventType,
identifiers::{RoomId, UserId},
};
#[cfg(not(target_arch = "wasm32"))]
use rustyline::{
completion::{Completer, Pair},
error::ReadlineError,
highlight::{Highlighter, MatchingBracketHighlighter},
hint::{Hinter, HistoryHinter},
validate::{MatchingBracketValidator, Validator},
CompletionType, Config, Context, EditMode, Editor, OutputStreamType,
};
#[cfg(not(target_arch = "wasm32"))]
use rustyline_derive::Helper;
use serde::Serialize;
#[cfg(not(target_arch = "wasm32"))]
use syntect::{
dumps::from_binary,
easy::HighlightLines,
highlighting::{Style, ThemeSet},
parsing::SyntaxSet,
util::{as_24_bit_terminal_escaped, LinesWithEndings},
};
#[derive(Clone)]
#[cfg(not(target_arch = "wasm32"))]
struct Inspector {
store: Store,
printer: Printer,
}
#[derive(Helper)]
#[cfg(not(target_arch = "wasm32"))]
struct InspectorHelper {
store: Store,
_highlighter: MatchingBracketHighlighter,
_validator: MatchingBracketValidator,
_hinter: HistoryHinter,
}
#[cfg(not(target_arch = "wasm32"))]
impl InspectorHelper {
const EVENT_TYPES: &'static [&'static str] = &[
"m.room.aliases",
"m.room.avatar",
"m.room.canonical_alias",
"m.room.create",
"m.room.encryption",
"m.room.guest_access",
"m.room.history_visibility",
"m.room.join_rules",
"m.room.name",
"m.room.power_levels",
"m.room.tombstone",
"m.room.topic",
];
fn new(store: Store) -> Self {
Self {
store,
_highlighter: MatchingBracketHighlighter::new(),
_validator: MatchingBracketValidator::new(),
_hinter: HistoryHinter {},
}
}
fn complete_event_types(&self, arg: Option<&&str>) -> Vec<Pair> {
Self::EVENT_TYPES
.iter()
.map(|t| Pair { display: t.to_string(), replacement: format!("{} ", t) })
.filter(|r| if let Some(arg) = arg { r.replacement.starts_with(arg) } else { true })
.collect()
}
fn complete_rooms(&self, arg: Option<&&str>) -> Vec<Pair> {
let rooms: Vec<RoomInfo> = block_on(async { self.store.get_room_infos().await.unwrap() });
rooms
.into_iter()
.map(|r| Pair {
display: r.room_id.to_string(),
replacement: format!("{} ", r.room_id.to_string()),
})
.filter(|r| if let Some(arg) = arg { r.replacement.starts_with(arg) } else { true })
.collect()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Completer for InspectorHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_: &Context<'_>,
) -> Result<(usize, Vec<Pair>), ReadlineError> {
let args: Vec<&str> = line.split_ascii_whitespace().collect();
let commands = vec![
("get-state", "get a state event in the given room"),
("get-profiles", "get all the stored profiles in the given room"),
("list-rooms", "list all rooms"),
("get-members", "get all the membership events in the given room"),
]
.iter()
.map(|(r, d)| Pair {
display: format!("{} ({})", r, d),
replacement: format!("{} ", r.to_string()),
})
.collect();
if args.is_empty() {
Ok((pos, commands))
} else if args.len() == 1 {
if (args[0] == "get-state" || args[0] == "get-members" || args[0] == "get-profiles")
&& line.ends_with(' ')
{
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
} else {
Ok((
0,
commands.into_iter().filter(|c| c.replacement.starts_with(args[0])).collect(),
))
}
} else if args.len() == 2 {
if args[0] == "get-state" {
if line.ends_with(' ') {
Ok((args[0].len() + args[1].len() + 2, self.complete_event_types(args.get(2))))
} else {
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
}
} else if args[0] == "get-members" || args[0] == "get-profiles" {
Ok((args[0].len() + 1, self.complete_rooms(args.get(1))))
} else {
Ok((pos, vec![]))
}
} else if args.len() == 3 {
if args[0] == "get-state" {
Ok((args[0].len() + args[1].len() + 2, self.complete_event_types(args.get(2))))
} else {
Ok((pos, vec![]))
}
} else {
Ok((pos, vec![]))
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Hinter for InspectorHelper {
type Hint = String;
}
#[cfg(not(target_arch = "wasm32"))]
impl Highlighter for InspectorHelper {}
#[cfg(not(target_arch = "wasm32"))]
impl Validator for InspectorHelper {}
#[derive(Clone, Debug)]
#[cfg(not(target_arch = "wasm32"))]
struct Printer {
ps: Arc<SyntaxSet>,
ts: Arc<ThemeSet>,
json: bool,
color: bool,
}
#[cfg(not(target_arch = "wasm32"))]
impl Printer {
fn new(json: bool, color: bool) -> Self {
let syntax_set: SyntaxSet = from_binary(include_bytes!("./syntaxes.bin"));
let themes: ThemeSet = from_binary(include_bytes!("./themes.bin"));
Self { ps: syntax_set.into(), ts: themes.into(), json, color }
}
fn pretty_print_struct<T: Debug + Serialize>(&self, data: &T) {
let data = if self.json {
serde_json::to_string_pretty(data).expect("Can't serialize struct")
} else {
format!("{:#?}", data)
};
let syntax = if self.json {
self.ps.find_syntax_by_extension("rs").expect("Can't find rust syntax extension")
} else {
self.ps.find_syntax_by_extension("json").expect("Can't find json syntax extension")
};
if self.color {
let mut h = HighlightLines::new(syntax, &self.ts.themes["Forest Night"]);
for line in LinesWithEndings::from(&data) {
let ranges: Vec<(Style, &str)> = h.highlight(line, &self.ps);
let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
print!("{}", escaped);
}
// Clear the formatting
println!("\x1b[0m");
} else {
println!("{}", data);
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Inspector {
fn new(database_path: &str, json: bool, color: bool) -> Self {
let printer = Printer::new(json, color);
let (store, _) = Store::open_default(database_path, None).unwrap();
Self { store, printer }
}
async fn run(&self, matches: ArgMatches<'_>) {
match matches.subcommand() {
("get-profiles", args) => {
let args = args.expect("No args provided for get-state");
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
self.get_profiles(room_id).await;
}
("get-members", args) => {
let args = args.expect("No args provided for get-state");
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
self.get_members(room_id).await;
}
("list-rooms", _) => self.list_rooms().await,
("get-display-names", args) => {
let args = args.expect("No args provided for get-state");
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
let display_name = args.value_of("display-name").unwrap().to_string();
self.get_display_name_owners(room_id, display_name).await;
}
("get-state", args) => {
let args = args.expect("No args provided for get-state");
let room_id = RoomId::try_from(args.value_of("room-id").unwrap()).unwrap();
let event_type = EventType::try_from(args.value_of("event-type").unwrap()).unwrap();
self.get_state(room_id, event_type).await;
}
_ => unreachable!(),
}
}
async fn list_rooms(&self) {
let rooms: Vec<RoomInfo> = self.store.get_room_infos().await.unwrap();
self.printer.pretty_print_struct(&rooms);
}
async fn get_display_name_owners(&self, room_id: RoomId, display_name: String) {
let users = self.store.get_users_with_display_name(&room_id, &display_name).await.unwrap();
self.printer.pretty_print_struct(&users);
}
async fn get_profiles(&self, room_id: RoomId) {
let joined: Vec<UserId> = self.store.get_joined_user_ids(&room_id).await.unwrap();
for member in joined {
let event = self.store.get_profile(&room_id, &member).await.unwrap();
self.printer.pretty_print_struct(&event);
}
}
async fn get_members(&self, room_id: RoomId) {
let joined: Vec<UserId> = self.store.get_joined_user_ids(&room_id).await.unwrap();
for member in joined {
let event = self.store.get_member_event(&room_id, &member).await.unwrap();
self.printer.pretty_print_struct(&event);
}
}
async fn get_state(&self, room_id: RoomId, event_type: EventType) {
self.printer.pretty_print_struct(
&self.store.get_state_event(&room_id, event_type, "").await.unwrap(),
);
}
fn subcommands() -> Vec<Argparse<'static, 'static>> {
vec![
SubCommand::with_name("list-rooms"),
SubCommand::with_name("get-members").arg(
Arg::with_name("room-id").required(true).validator(|r| {
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
}),
),
SubCommand::with_name("get-profiles").arg(
Arg::with_name("room-id").required(true).validator(|r| {
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
}),
),
SubCommand::with_name("get-display-names")
.arg(Arg::with_name("room-id").required(true).validator(|r| {
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
}))
.arg(Arg::with_name("display-name").required(true)),
SubCommand::with_name("get-state")
.arg(Arg::with_name("room-id").required(true).validator(|r| {
RoomId::try_from(r).map(|_| ()).map_err(|_| "Invalid room id given".to_owned())
}))
.arg(Arg::with_name("event-type").required(true).validator(|e| {
EventType::try_from(e).map(|_| ()).map_err(|_| "Invalid event type".to_string())
})),
]
}
async fn parse_and_run(&self, input: &str) {
let argparse = Argparse::new("state-inspector")
.global_setting(ArgParseSettings::DisableHelpFlags)
.global_setting(ArgParseSettings::DisableVersion)
.global_setting(ArgParseSettings::VersionlessSubcommands)
.global_setting(ArgParseSettings::NoBinaryName)
.setting(ArgParseSettings::SubcommandRequiredElseHelp)
.subcommands(Inspector::subcommands());
match argparse.get_matches_from_safe(input.split_ascii_whitespace()) {
Ok(m) => {
self.run(m).await;
}
Err(e) => {
println!("{}", e);
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn main() {
let argparse = Argparse::new("state-inspector")
.global_setting(ArgParseSettings::DisableVersion)
.global_setting(ArgParseSettings::VersionlessSubcommands)
.arg(Arg::with_name("database").required(true))
.arg(
Arg::with_name("json")
.long("json")
.help("set the output to raw json instead of Rust structs")
.global(true)
.takes_value(false),
)
.subcommands(Inspector::subcommands());
let matches = argparse.get_matches();
let database_path = matches.args.get("database").expect("No database path");
let json = matches.is_present("json");
let color = atty::is(Stream::Stdout);
let inspector = Inspector::new(&database_path.vals[0].to_string_lossy(), json, color);
if matches.subcommand.is_none() {
let config = Config::builder()
.history_ignore_space(true)
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.output_stream(OutputStreamType::Stdout)
.build();
let helper = InspectorHelper::new(inspector.store.clone());
let mut rl = Editor::<InspectorHelper>::with_config(config);
rl.set_helper(Some(helper));
while let Ok(input) = rl.readline(">> ") {
rl.add_history_entry(input.as_str());
block_on(inspector.parse_and_run(input.as_str()));
}
} else {
block_on(inspector.run(matches));
}
}
#[cfg(target_arch = "wasm32")]
fn main() {
panic!("This example doesn't run on WASM");
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+14 -7
View File
@@ -15,27 +15,28 @@
//! Error conditions.
use serde_json::Error as JsonError;
use std::io::Error as IoError;
use thiserror::Error;
#[cfg(feature = "encryption")]
use matrix_sdk_crypto::{MegolmError, OlmError};
use matrix_sdk_crypto::{CryptoStoreError, MegolmError, OlmError};
use serde_json::Error as JsonError;
use thiserror::Error;
/// Result type of the rust-sdk.
pub type Result<T> = std::result::Result<T, Error>;
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Internal representation of errors.
#[derive(Error, Debug)]
pub enum Error {
/// Queried endpoint requires authentication but was called on an anonymous client.
/// Queried endpoint requires authentication but was called on an anonymous
/// client.
#[error("the queried endpoint requires authentication but was called before logging in")]
AuthenticationRequired,
/// A generic error returned when the state store fails not due to
/// IO or (de)serialization.
#[error("state store: {0}")]
StateStore(String),
#[error(transparent)]
StateStore(#[from] crate::store::StoreError),
/// An error when (de)serializing JSON.
#[error(transparent)]
@@ -45,6 +46,12 @@ pub enum Error {
#[error(transparent)]
IoError(#[from] IoError),
/// An error occurred in the crypto store.
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
#[error(transparent)]
CryptoStore(#[from] CryptoStoreError),
/// An error occurred during a E2EE operation.
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
-661
View File
@@ -1,661 +0,0 @@
// Copyright 2020 Damir Jelić
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use matrix_sdk_common::locks::RwLock;
use serde_json::value::RawValue as RawJsonValue;
use crate::{
events::{
call::{
answer::AnswerEventContent, candidates::CandidatesEventContent,
hangup::HangupEventContent, invite::InviteEventContent,
},
custom::CustomEventContent,
fully_read::FullyReadEventContent,
ignored_user_list::IgnoredUserListEventContent,
presence::PresenceEvent,
push_rules::PushRulesEventContent,
receipt::ReceiptEventContent,
room::{
aliases::AliasesEventContent,
avatar::AvatarEventContent,
canonical_alias::CanonicalAliasEventContent,
join_rules::JoinRulesEventContent,
member::MemberEventContent,
message::{feedback::FeedbackEventContent, MessageEventContent as MsgEventContent},
name::NameEventContent,
power_levels::PowerLevelsEventContent,
redaction::SyncRedactionEvent,
tombstone::TombstoneEventContent,
},
typing::TypingEventContent,
BasicEvent, StrippedStateEvent, SyncEphemeralRoomEvent, SyncMessageEvent, SyncStateEvent,
},
Room, RoomState,
};
use matrix_sdk_common::async_trait;
/// Type alias for `RoomState` enum when passed to `EventEmitter` methods.
pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
/// This represents the various "unrecognized" events.
#[derive(Clone, Copy, Debug)]
pub enum CustomEvent<'c> {
/// A custom basic event.
Basic(&'c BasicEvent<CustomEventContent>),
/// A custom basic event.
EphemeralRoom(&'c SyncEphemeralRoomEvent<CustomEventContent>),
/// A custom room event.
Message(&'c SyncMessageEvent<CustomEventContent>),
/// A custom state event.
State(&'c SyncStateEvent<CustomEventContent>),
/// A custom stripped state event.
StrippedState(&'c StrippedStateEvent<CustomEventContent>),
}
/// This trait allows any type implementing `EventEmitter` to specify event callbacks for each event.
/// The `Client` calls each method when the corresponding event is received.
///
/// # Examples
/// ```
/// # use std::ops::Deref;
/// # use std::sync::Arc;
/// # use std::{env, process::exit};
/// # use matrix_sdk_base::{
/// # self,
/// # events::{
/// # room::message::{MessageEventContent, TextMessageEventContent},
/// # SyncMessageEvent
/// # },
/// # EventEmitter, SyncRoom
/// # };
/// # use matrix_sdk_common::{async_trait, locks::RwLock};
///
/// struct EventCallback;
///
/// #[async_trait]
/// impl EventEmitter for EventCallback {
/// async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
/// if let SyncRoom::Joined(room) = room {
/// if let SyncMessageEvent {
/// content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
/// sender,
/// ..
/// } = event
/// {
/// let name = {
/// let room = room.read().await;
/// let member = room.joined_members.get(&sender).unwrap();
/// member
/// .display_name
/// .as_ref()
/// .map(ToString::to_string)
/// .unwrap_or(sender.to_string())
/// };
/// println!("{}: {}", name, msg_body);
/// }
/// }
/// }
/// }
/// ```
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait EventEmitter: Send + Sync {
// ROOM EVENTS from `IncomingTimeline`
/// Fires when `Client` receives a `RoomEvent::RoomMember` event.
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
async fn on_room_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
async fn on_room_message_feedback(
&self,
_: SyncRoom,
_: &SyncMessageEvent<FeedbackEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::CallInvite` event
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::CallAnswer` event
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::CallCandidates` event
async fn on_room_call_candidates(
&self,
_: SyncRoom,
_: &SyncMessageEvent<CandidatesEventContent>,
) {
}
/// Fires when `Client` receives a `RoomEvent::CallHangup` event
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
async fn on_room_power_levels(&self, _: SyncRoom, _: &SyncStateEvent<PowerLevelsEventContent>) {
}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {}
// `RoomEvent`s from `IncomingState`
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomName` event.
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
async fn on_state_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
async fn on_state_power_levels(
&self,
_: SyncRoom,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
}
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
async fn on_state_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedStateEvent<NameEventContent>) {}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(
&self,
_: SyncRoom,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AliasesEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AvatarEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(
&self,
_: SyncRoom,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(
&self,
_: SyncRoom,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
}
// `NonRoomEvent` (this is a type alias from ruma_events)
/// Fires when `Client` receives a `NonRoomEvent::RoomPresence` event.
async fn on_non_room_presence(&self, _: SyncRoom, _: &PresenceEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomName` event.
async fn on_non_room_ignored_users(
&self,
_: SyncRoom,
_: &BasicEvent<IgnoredUserListEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::RoomCanonicalAlias` event.
async fn on_non_room_push_rules(&self, _: SyncRoom, _: &BasicEvent<PushRulesEventContent>) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
async fn on_non_room_fully_read(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<FullyReadEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::Typing` event.
async fn on_non_room_typing(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<TypingEventContent>,
) {
}
/// Fires when `Client` receives a `NonRoomEvent::Receipt` event.
///
/// This is always a read receipt.
async fn on_non_room_receipt(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
) {
}
// `PresenceEvent` is a struct so there is only the one method
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {}
/// Fires when `Client` receives a `Event::Custom` event or if deserialization fails
/// because the event was unknown to ruma.
///
/// The only guarantee this method can give about the event is that it is valid JSON.
async fn on_unrecognized_event(&self, _: SyncRoom, _: &RawJsonValue) {}
/// Fires when `Client` receives a `Event::Custom` event or if deserialization fails
/// because the event was unknown to ruma.
///
/// The only guarantee this method can give about the event is that it is in the
/// shape of a valid matrix event.
async fn on_custom_event(&self, _: SyncRoom, _: &CustomEvent<'_>) {}
}
#[cfg(test)]
mod test {
use super::*;
use matrix_sdk_common::{async_trait, locks::Mutex};
use matrix_sdk_test::{async_test, sync_response, SyncResponseFile};
use std::sync::Arc;
#[cfg(target_arch = "wasm32")]
pub use wasm_bindgen_test::*;
#[derive(Clone)]
pub struct EvEmitterTest(Arc<Mutex<Vec<String>>>);
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl EventEmitter for EvEmitterTest {
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("member".to_string())
}
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("name".to_string())
}
async fn on_room_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("canonical".to_string())
}
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("aliases".to_string())
}
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("avatar".to_string())
}
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {
self.0.lock().await.push("message".to_string())
}
async fn on_room_message_feedback(
&self,
_: SyncRoom,
_: &SyncMessageEvent<FeedbackEventContent>,
) {
self.0.lock().await.push("feedback".to_string())
}
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {
self.0.lock().await.push("call invite".to_string())
}
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {
self.0.lock().await.push("call answer".to_string())
}
async fn on_room_call_candidates(
&self,
_: SyncRoom,
_: &SyncMessageEvent<CandidatesEventContent>,
) {
self.0.lock().await.push("call candidates".to_string())
}
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {
self.0.lock().await.push("call hangup".to_string())
}
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {
self.0.lock().await.push("redaction".to_string())
}
async fn on_room_power_levels(
&self,
_: SyncRoom,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("power".to_string())
}
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {
self.0.lock().await.push("tombstone".to_string())
}
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
self.0.lock().await.push("state member".to_string())
}
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
self.0.lock().await.push("state name".to_string())
}
async fn on_state_canonical_alias(
&self,
_: SyncRoom,
_: &SyncStateEvent<CanonicalAliasEventContent>,
) {
self.0.lock().await.push("state canonical".to_string())
}
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
self.0.lock().await.push("state aliases".to_string())
}
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
self.0.lock().await.push("state avatar".to_string())
}
async fn on_state_power_levels(
&self,
_: SyncRoom,
_: &SyncStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("state power".to_string())
}
async fn on_state_join_rules(
&self,
_: SyncRoom,
_: &SyncStateEvent<JoinRulesEventContent>,
) {
self.0.lock().await.push("state rules".to_string())
}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
self.0
.lock()
.await
.push("stripped state member".to_string())
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
async fn on_stripped_state_name(
&self,
_: SyncRoom,
_: &StrippedStateEvent<NameEventContent>,
) {
self.0.lock().await.push("stripped state name".to_string())
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(
&self,
_: SyncRoom,
_: &StrippedStateEvent<CanonicalAliasEventContent>,
) {
self.0
.lock()
.await
.push("stripped state canonical".to_string())
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AliasesEventContent>,
) {
self.0
.lock()
.await
.push("stripped state aliases".to_string())
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(
&self,
_: SyncRoom,
_: &StrippedStateEvent<AvatarEventContent>,
) {
self.0
.lock()
.await
.push("stripped state avatar".to_string())
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(
&self,
_: SyncRoom,
_: &StrippedStateEvent<PowerLevelsEventContent>,
) {
self.0.lock().await.push("stripped state power".to_string())
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(
&self,
_: SyncRoom,
_: &StrippedStateEvent<JoinRulesEventContent>,
) {
self.0.lock().await.push("stripped state rules".to_string())
}
async fn on_non_room_presence(&self, _: SyncRoom, _: &PresenceEvent) {
self.0.lock().await.push("presence".to_string())
}
async fn on_non_room_ignored_users(
&self,
_: SyncRoom,
_: &BasicEvent<IgnoredUserListEventContent>,
) {
self.0.lock().await.push("account ignore".to_string())
}
async fn on_non_room_push_rules(&self, _: SyncRoom, _: &BasicEvent<PushRulesEventContent>) {
self.0.lock().await.push("account push rules".to_string())
}
async fn on_non_room_fully_read(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<FullyReadEventContent>,
) {
self.0.lock().await.push("account read".to_string())
}
async fn on_non_room_typing(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<TypingEventContent>,
) {
self.0.lock().await.push("typing event".to_string())
}
async fn on_non_room_receipt(
&self,
_: SyncRoom,
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
) {
self.0.lock().await.push("receipt event".to_string())
}
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {
self.0.lock().await.push("presence event".to_string())
}
async fn on_unrecognized_event(&self, _: SyncRoom, _: &RawJsonValue) {
self.0.lock().await.push("unrecognized event".to_string())
}
async fn on_custom_event(&self, _: SyncRoom, _: &CustomEvent<'_>) {
self.0.lock().await.push("custom event".to_string())
}
}
use crate::{identifiers::user_id, BaseClient, Session};
async fn get_client() -> BaseClient {
let session = Session {
access_token: "1234".to_owned(),
user_id: user_id!("@example:example.com"),
device_id: "DEVICEID".into(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
client
}
#[async_test]
async fn event_emitter_joined() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let emitter = Box::new(EvEmitterTest(vec));
let client = get_client().await;
client.add_event_emitter(emitter).await;
let mut response = sync_response(SyncResponseFile::Default);
client.receive_sync_response(&mut response).await.unwrap();
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"state rules",
"state member",
"state aliases",
"state power",
"state canonical",
"state member",
"state member",
"message",
"account ignore",
"presence event",
"receipt event",
"account read",
],
)
}
#[async_test]
async fn event_emitter_invite() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let emitter = Box::new(EvEmitterTest(vec));
let client = get_client().await;
client.add_event_emitter(emitter).await;
let mut response = sync_response(SyncResponseFile::Invite);
client.receive_sync_response(&mut response).await.unwrap();
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
["stripped state name", "stripped state member"],
)
}
#[async_test]
async fn event_emitter_leave() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let emitter = Box::new(EvEmitterTest(vec));
let client = get_client().await;
client.add_event_emitter(emitter).await;
let mut response = sync_response(SyncResponseFile::Leave);
client.receive_sync_response(&mut response).await.unwrap();
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"state rules",
"state member",
"state aliases",
"state power",
"state canonical",
"state member",
"state member",
"message"
],
)
}
#[async_test]
async fn event_emitter_more_events() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let emitter = Box::new(EvEmitterTest(vec));
let client = get_client().await;
client.add_event_emitter(emitter).await;
let mut response = sync_response(SyncResponseFile::All);
client.receive_sync_response(&mut response).await.unwrap();
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"message",
"message", // this is a message edit event
"redaction",
"unrecognized event",
// "unrecognized event", this is actually a redacted "m.room.messages" event
// the ephemeral room events are looped over after the room events
"receipt event",
"typing event"
],
)
}
#[async_test]
async fn event_emitter_voip() {
let vec = Arc::new(Mutex::new(Vec::new()));
let test_vec = Arc::clone(&vec);
let emitter = Box::new(EvEmitterTest(vec));
let client = get_client().await;
client.add_event_emitter(emitter).await;
let mut response = sync_response(SyncResponseFile::Voip);
client.receive_sync_response(&mut response).await.unwrap();
let v = test_vec.lock().await;
assert_eq!(
v.as_slice(),
[
"call invite",
"call answer",
"call candidates",
"call hangup",
],
)
}
}
+10 -21
View File
@@ -20,15 +20,13 @@
//! The following crate feature flags are available:
//!
//! * `encryption`: Enables end-to-end encryption support in the library.
//! * `sqlite_cryptostore`: Enables a SQLite based store for the encryption
//! * `sled_cryptostore`: Enables a Sled based store for the encryption
//! keys. If this is disabled and `encryption` support is enabled the keys will
//! by default be stored only in memory and thus lost after the client is
//! destroyed.
//! * `unstable-synapse-quirks`: Enables support to deal with inconsistencies
//! of Synapse in compliance with the Matrix API specification.
//! * `markdown`: Support for sending markdown formatted messages.
#![deny(
missing_debug_implementations,
dead_code,
missing_docs,
trivial_casts,
trivial_numeric_casts,
@@ -38,32 +36,23 @@
)]
#![cfg_attr(feature = "docs", feature(doc_cfg))]
pub use matrix_sdk_common::*;
pub use crate::{
error::{Error, Result},
session::Session,
};
pub use matrix_sdk_common::*;
mod client;
mod error;
mod event_emitter;
mod models;
pub mod media;
mod rooms;
mod session;
mod state;
pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType};
pub use event_emitter::{CustomEvent, EventEmitter, SyncRoom};
pub use models::{Room, RoomMember};
pub use state::{AllRooms, ClientState};
mod store;
pub use client::{BaseClient, BaseClientConfig};
#[cfg(feature = "encryption")]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use matrix_sdk_crypto as crypto;
#[cfg(feature = "messages")]
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
pub use models::{MessageQueue, PossiblyRedactedExt};
#[cfg(not(target_arch = "wasm32"))]
pub use state::JsonStore;
pub use state::StateStore;
pub use rooms::{Room, RoomInfo, RoomMember, RoomType};
pub use store::{StateChanges, StateStore, Store, StoreError};
+215
View File
@@ -0,0 +1,215 @@
//! Common types for [media content](https://matrix.org/docs/spec/client_server/r0.6.1#id66).
use ruma::{
api::client::r0::media::get_content_thumbnail::Method,
events::{
room::{
message::{
AudioMessageEventContent, FileMessageEventContent, ImageMessageEventContent,
LocationMessageEventContent, VideoMessageEventContent,
},
EncryptedFile,
},
sticker::StickerEventContent,
},
MxcUri, UInt,
};
const UNIQUE_SEPARATOR: &str = "_";
/// A trait to uniquely identify values of the same type.
pub trait UniqueKey {
/// A string that uniquely identifies `Self` compared to other values of
/// the same type.
fn unique_key(&self) -> String;
}
/// The requested format of a media file.
#[derive(Clone, Debug)]
pub enum MediaFormat {
/// The file that was uploaded.
File,
/// A thumbnail of the file that was uploaded.
Thumbnail(MediaThumbnailSize),
}
impl UniqueKey for MediaFormat {
fn unique_key(&self) -> String {
match self {
Self::File => "file".into(),
Self::Thumbnail(size) => size.unique_key(),
}
}
}
/// The requested size of a media thumbnail.
#[derive(Clone, Debug)]
pub struct MediaThumbnailSize {
/// The desired resizing method.
pub method: Method,
/// The desired width of the thumbnail. The actual thumbnail may not match
/// the size specified.
pub width: UInt,
/// The desired height of the thumbnail. The actual thumbnail may not match
/// the size specified.
pub height: UInt,
}
impl UniqueKey for MediaThumbnailSize {
fn unique_key(&self) -> String {
format!("{}{}{}x{}", self.method, UNIQUE_SEPARATOR, self.width, self.height)
}
}
/// A request for media data.
#[derive(Clone, Debug)]
pub enum MediaType {
/// A media content URI.
Uri(MxcUri),
/// An encrypted media content.
Encrypted(Box<EncryptedFile>),
}
impl UniqueKey for MediaType {
fn unique_key(&self) -> String {
match self {
Self::Uri(uri) => uri.to_string(),
Self::Encrypted(file) => file.url.to_string(),
}
}
}
/// A request for media data.
#[derive(Clone, Debug)]
pub struct MediaRequest {
/// The type of the media file.
pub media_type: MediaType,
/// The requested format of the media data.
pub format: MediaFormat,
}
impl UniqueKey for MediaRequest {
fn unique_key(&self) -> String {
format!("{}{}{}", self.media_type.unique_key(), UNIQUE_SEPARATOR, self.format.unique_key())
}
}
/// Trait for media event content.
pub trait MediaEventContent {
/// Get the type of the file for `Self`.
///
/// Returns `None` if `Self` has no file.
fn file(&self) -> Option<MediaType>;
/// Get the type of the thumbnail for `Self`.
///
/// Returns `None` if `Self` has no thumbnail.
fn thumbnail(&self) -> Option<MediaType>;
}
impl MediaEventContent for StickerEventContent {
fn file(&self) -> Option<MediaType> {
Some(MediaType::Uri(self.url.clone()))
}
fn thumbnail(&self) -> Option<MediaType> {
None
}
}
impl MediaEventContent for AudioMessageEventContent {
fn file(&self) -> Option<MediaType> {
self.url
.as_ref()
.map(|uri| MediaType::Uri(uri.clone()))
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
}
fn thumbnail(&self) -> Option<MediaType> {
None
}
}
impl MediaEventContent for FileMessageEventContent {
fn file(&self) -> Option<MediaType> {
self.url
.as_ref()
.map(|uri| MediaType::Uri(uri.clone()))
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
}
fn thumbnail(&self) -> Option<MediaType> {
self.info.as_ref().and_then(|info| {
if let Some(uri) = info.thumbnail_url.as_ref() {
Some(MediaType::Uri(uri.clone()))
} else {
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
}
})
}
}
impl MediaEventContent for ImageMessageEventContent {
fn file(&self) -> Option<MediaType> {
self.url
.as_ref()
.map(|uri| MediaType::Uri(uri.clone()))
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
}
fn thumbnail(&self) -> Option<MediaType> {
self.info
.as_ref()
.and_then(|info| {
if let Some(uri) = info.thumbnail_url.as_ref() {
Some(MediaType::Uri(uri.clone()))
} else {
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
}
})
.or_else(|| self.url.as_ref().map(|uri| MediaType::Uri(uri.clone())))
}
}
impl MediaEventContent for VideoMessageEventContent {
fn file(&self) -> Option<MediaType> {
self.url
.as_ref()
.map(|uri| MediaType::Uri(uri.clone()))
.or_else(|| self.file.as_ref().map(|e| MediaType::Encrypted(e.clone())))
}
fn thumbnail(&self) -> Option<MediaType> {
self.info
.as_ref()
.and_then(|info| {
if let Some(uri) = info.thumbnail_url.as_ref() {
Some(MediaType::Uri(uri.clone()))
} else {
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
}
})
.or_else(|| self.url.as_ref().map(|uri| MediaType::Uri(uri.clone())))
}
}
impl MediaEventContent for LocationMessageEventContent {
fn file(&self) -> Option<MediaType> {
None
}
fn thumbnail(&self) -> Option<MediaType> {
self.info.as_ref().and_then(|info| {
if let Some(uri) = info.thumbnail_url.as_ref() {
Some(MediaType::Uri(uri.clone()))
} else {
info.thumbnail_file.as_ref().map(|file| MediaType::Encrypted(file.clone()))
}
})
}
}
-274
View File
@@ -1,274 +0,0 @@
//! A queue that holds at most ten of the most recent messages.
//!
//! The `Room` struct optionally holds a `MessageQueue` if the "messages"
//! feature is enabled.
use std::{time::SystemTime, vec::IntoIter};
use matrix_sdk_common::{
events::AnyPossiblyRedactedSyncMessageEvent,
identifiers::{EventId, UserId},
};
use serde::{de, ser, Serialize};
/// Exposes some of the field access methods found in the event held by
/// `AnyPossiblyRedacted*` enums.
///
/// This is just an extension trait to ease the use of certain event enums.
pub trait PossiblyRedactedExt {
/// Access the redacted or full event's `event_id` field.
fn event_id(&self) -> &EventId;
/// Access the redacted or full event's `origin_server_ts` field.
fn origin_server_ts(&self) -> &SystemTime;
/// Access the redacted or full event's `sender` field.
fn sender(&self) -> &UserId;
}
impl PossiblyRedactedExt for AnyPossiblyRedactedSyncMessageEvent {
/// Access the underlying event's `event_id`.
fn event_id(&self) -> &EventId {
match self {
Self::Regular(e) => e.event_id(),
Self::Redacted(e) => e.event_id(),
}
}
/// Access the underlying event's `origin_server_ts`.
fn origin_server_ts(&self) -> &SystemTime {
match self {
Self::Regular(e) => e.origin_server_ts(),
Self::Redacted(e) => e.origin_server_ts(),
}
}
/// Access the underlying event's `sender`.
fn sender(&self) -> &UserId {
match self {
Self::Regular(e) => e.sender(),
Self::Redacted(e) => e.sender(),
}
}
}
const MESSAGE_QUEUE_CAP: usize = 35;
/// A queue that holds the 35 most recent messages received from the server.
#[derive(Clone, Debug, Default)]
pub struct MessageQueue {
pub(crate) msgs: Vec<AnyPossiblyRedactedSyncMessageEvent>,
}
impl MessageQueue {
/// Create a new empty `MessageQueue`.
pub fn new() -> Self {
Self {
msgs: Vec::with_capacity(45),
}
}
/// Inserts a `MessageEvent` into `MessageQueue`, sorted by by `origin_server_ts`.
///
/// Removes the oldest element in the queue if there are more than 10 elements.
pub fn push(&mut self, msg: AnyPossiblyRedactedSyncMessageEvent) -> bool {
// only push new messages into the queue
if let Some(latest) = self.msgs.last() {
if msg.origin_server_ts() < latest.origin_server_ts() && self.msgs.len() >= 10 {
return false;
}
}
if self.msgs.iter().all(|old| old.event_id() != msg.event_id()) {
self.msgs.push(msg)
}
if self.msgs.len() > MESSAGE_QUEUE_CAP {
self.msgs.pop();
}
true
}
/// Iterate over the messages in the queue.
pub fn iter(&self) -> impl Iterator<Item = &AnyPossiblyRedactedSyncMessageEvent> {
self.msgs.iter()
}
/// Iterate over each message mutably.
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AnyPossiblyRedactedSyncMessageEvent> {
self.msgs.iter_mut()
}
}
impl PartialEq for MessageQueue {
fn eq(&self, other: &MessageQueue) -> bool {
self.msgs
.iter()
.zip(other.msgs.iter())
.all(|(a, b)| a.event_id() == b.event_id())
}
}
impl IntoIterator for MessageQueue {
type Item = AnyPossiblyRedactedSyncMessageEvent;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.msgs.into_iter()
}
}
pub(crate) mod ser_deser {
use std::fmt;
use super::*;
struct MessageQueueDeserializer;
impl<'de> de::Visitor<'de> for MessageQueueDeserializer {
type Value = MessageQueue;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an array of message events")
}
fn visit_seq<S>(self, mut access: S) -> Result<Self::Value, S::Error>
where
S: de::SeqAccess<'de>,
{
let mut msgs = Vec::with_capacity(access.size_hint().unwrap_or(0));
while let Some(msg) = access.next_element::<AnyPossiblyRedactedSyncMessageEvent>()? {
msgs.push(msg);
}
Ok(MessageQueue { msgs })
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<MessageQueue, D::Error>
where
D: de::Deserializer<'de>,
{
deserializer.deserialize_seq(MessageQueueDeserializer)
}
pub fn serialize<S>(msgs: &MessageQueue, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
msgs.msgs.serialize(serializer)
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use matrix_sdk_common::{
events::{AnyPossiblyRedactedSyncMessageEvent, AnySyncMessageEvent},
identifiers::{room_id, user_id, RoomId},
};
use matrix_sdk_test::test_json;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use super::*;
use crate::Room;
#[test]
fn serialize() {
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let mut room = Room::new(&id, &user);
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
);
let mut msgs = MessageQueue::new();
msgs.push(msg.clone());
room.messages = msgs;
let mut joined_rooms = HashMap::new();
joined_rooms.insert(id, room);
assert_eq!(
serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"direct_target": null,
"joined_members": {},
"invited_members": {},
"messages": [ msg ],
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}),
serde_json::to_value(&joined_rooms).unwrap()
);
}
#[test]
fn deserialize() {
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let mut room = Room::new(&id, &user);
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
);
let mut msgs = MessageQueue::new();
msgs.push(msg.clone());
room.messages = msgs;
let mut joined_rooms = HashMap::new();
joined_rooms.insert(id, room);
let json = serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"direct_target": null,
"joined_members": {},
"invited_members": {},
"messages": [ msg ],
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
});
assert_eq!(
joined_rooms,
serde_json::from_value::<HashMap<RoomId, Room>>(json).unwrap()
);
}
}
-10
View File
@@ -1,10 +0,0 @@
#[cfg(feature = "messages")]
mod message;
mod room;
mod room_member;
#[cfg(feature = "messages")]
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
pub use message::{MessageQueue, PossiblyRedactedExt};
pub use room::{Room, RoomName};
pub use room_member::RoomMember;
File diff suppressed because it is too large Load Diff
-291
View File
@@ -1,291 +0,0 @@
// Copyright 2020 Damir Jelić
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryFrom;
use matrix_sdk_common::{
events::{presence::PresenceEvent, room::member::MemberEventContent, SyncStateEvent},
identifiers::{RoomId, UserId},
presence::PresenceState,
Int, UInt,
};
use serde::{Deserialize, Serialize};
// Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob.
#[derive(Debug, Serialize, Deserialize, Clone)]
/// A Matrix room member.
pub struct RoomMember {
/// The unique MXID of the user.
pub user_id: UserId,
/// The human readable name of the user.
pub display_name: Option<String>,
/// Whether the member's display name is ambiguous due to being shared with
/// other members.
pub display_name_ambiguous: bool,
/// The matrix url of the users avatar.
pub avatar_url: Option<String>,
/// The time, in ms, since the user interacted with the server.
pub last_active_ago: Option<UInt>,
/// If the user should be considered active.
pub currently_active: Option<bool>,
/// The unique id of the room.
pub room_id: RoomId,
/// If the member is typing.
pub typing: Option<bool>,
/// The presence of the user, if found.
pub presence: Option<PresenceState>,
/// The presence status message, if found.
pub status_msg: Option<String>,
/// The users power level.
pub power_level: Option<Int>,
/// The normalized power level of this `RoomMember` (0-100).
pub power_level_norm: Option<Int>,
/// The human readable name of this room member.
pub name: String,
// FIXME: The docstring below is currently a lie since we only store the initial event that
// creates the member (the one we pass to RoomMember::new).
//
// The intent of this field is to keep the last (or last few?) state events related to the room
// member cached so we can quickly go back to the previous one in case some of them get
// redacted. Keeping all state for each room member is probably too much.
//
// Needs design.
/// The events that created the state of this room member.
pub events: Vec<SyncStateEvent<MemberEventContent>>,
/// The `PresenceEvent`s connected to this user.
pub presence_events: Vec<PresenceEvent>,
}
impl PartialEq for RoomMember {
fn eq(&self, other: &RoomMember) -> bool {
// TODO check everything but events and presence_events they don't impl PartialEq
self.room_id == other.room_id
&& self.user_id == other.user_id
&& self.name == other.name
&& self.display_name == other.display_name
&& self.display_name_ambiguous == other.display_name_ambiguous
&& self.avatar_url == other.avatar_url
&& self.last_active_ago == other.last_active_ago
}
}
impl RoomMember {
/// Create a new room member.
///
/// # Arguments
///
/// * `event` - event associated with a member joining, leaving or getting
/// invited to a room.
///
/// * `room_id` - The unique id of the room this member is part of.
pub fn new(event: &SyncStateEvent<MemberEventContent>, room_id: &RoomId) -> Self {
Self {
name: event.state_key.clone(),
room_id: room_id.clone(),
user_id: UserId::try_from(event.state_key.as_str()).unwrap(),
display_name: event.content.displayname.clone(),
display_name_ambiguous: false,
avatar_url: event.content.avatar_url.clone(),
presence: None,
status_msg: None,
last_active_ago: None,
currently_active: None,
typing: None,
power_level: None,
power_level_norm: None,
presence_events: Vec::default(),
events: vec![event.clone()],
}
}
/// Returns the most ergonomic (but potentially ambiguous/non-unique) name
/// available for the member.
///
/// This is the member's display name if it is set, otherwise their MXID.
pub fn name(&self) -> String {
self.display_name
.clone()
.unwrap_or_else(|| format!("{}", self.user_id))
}
/// Returns a name for the member which is guaranteed to be unique, but not
/// necessarily the most ergonomic.
///
/// This is either a name in the format "DISPLAY_NAME (MXID)" if the
/// member's display name is set, or simply "MXID" if not.
pub fn unique_name(&self) -> String {
self.display_name
.clone()
.map(|d| format!("{} ({})", d, self.user_id))
.unwrap_or_else(|| format!("{}", self.user_id))
}
/// Get the disambiguated display name for the member which is as ergonomic
/// as possible while still guaranteeing it is unique.
///
/// If the member's display name is currently ambiguous (i.e. shared by
/// other room members), this method will return the same result as
/// `RoomMember::unique_name`. Otherwise, this method will return the same
/// result as `RoomMember::name`.
///
/// This is usually the name you want when showing room messages from the
/// member or when showing the member in the member list.
///
/// **Warning**: When displaying a room member's display name, clients
/// *must* use a disambiguated name, so they *must not* use
/// `RoomMember::display_name` directly. Clients *should* use this method to
/// obtain the name, but an acceptable alternative is to use
/// `RoomMember::unique_name` in certain situations.
pub fn disambiguated_name(&self) -> String {
if self.display_name_ambiguous {
self.unique_name()
} else {
self.name()
}
}
}
#[cfg(test)]
mod test {
use matrix_sdk_test::{async_test, EventBuilder, EventsJson};
use crate::{
identifiers::{room_id, user_id, RoomId},
int, BaseClient, Session,
};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
async fn get_client() -> BaseClient {
let session = Session {
access_token: "1234".to_owned(),
user_id: user_id!("@example:localhost"),
device_id: "DEVICEID".into(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
client
}
// TODO: Move this to EventBuilder since it's a magic room ID used in EventBuilder's example
// events.
fn test_room_id() -> RoomId {
room_id!("!SVkFJHzfwvuaIEawgC:localhost")
}
#[async_test]
async fn room_member_events() {
let client = get_client().await;
let room_id = test_room_id();
let mut response = EventBuilder::default()
.add_room_event(EventsJson::Member)
.add_room_event(EventsJson::PowerLevels)
.build_sync_response();
client.receive_sync_response(&mut response).await.unwrap();
let room = client.get_joined_room(&room_id).await.unwrap();
let room = room.read().await;
let member = room
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.power_level, Some(int!(100)));
}
#[async_test]
async fn room_member_display_name_change() {
let client = get_client().await;
let room_id = test_room_id();
let mut builder = EventBuilder::default();
let mut initial_response = builder
.add_room_event(EventsJson::Member)
.build_sync_response();
let mut name_change_response = builder
.add_room_event(EventsJson::MemberNameChange)
.build_sync_response();
client
.receive_sync_response(&mut initial_response)
.await
.unwrap();
let room = client.get_joined_room(&room_id).await.unwrap();
// Initially, the display name is "example".
{
let room = room.read().await;
let member = room
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.display_name.as_ref().unwrap(), "example");
}
client
.receive_sync_response(&mut name_change_response)
.await
.unwrap();
// Afterwards, the display name is "changed".
{
let room = room.read().await;
let member = room
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.display_name.as_ref().unwrap(), "changed");
}
}
#[async_test]
async fn member_presence_events() {
let client = get_client().await;
let room_id = test_room_id();
let mut response = EventBuilder::default()
.add_room_event(EventsJson::Member)
.add_room_event(EventsJson::PowerLevels)
.add_presence_event(EventsJson::Presence)
.build_sync_response();
client.receive_sync_response(&mut response).await.unwrap();
let room = client.get_joined_room(&room_id).await.unwrap();
let room = room.read().await;
let member = room
.joined_members
.get(&user_id!("@example:localhost"))
.unwrap();
assert_eq!(member.power_level, Some(int!(100)));
assert!(member.avatar_url.is_none());
assert_eq!(member.last_active_ago, None);
assert_eq!(member.presence, None);
}
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::sync::Arc;
use ruma::{
events::{
presence::PresenceEvent,
room::{member::MemberEventContent, power_levels::PowerLevelsEventContent},
SyncStateEvent,
},
MxcUri, UserId,
};
use crate::deserialized_responses::MemberEvent;
/// A member of a room.
#[derive(Clone, Debug)]
pub struct RoomMember {
pub(crate) event: Arc<MemberEvent>,
pub(crate) profile: Arc<Option<MemberEventContent>>,
pub(crate) presence: Arc<Option<PresenceEvent>>,
pub(crate) power_levels: Arc<Option<SyncStateEvent<PowerLevelsEventContent>>>,
pub(crate) max_power_level: i64,
pub(crate) is_room_creator: bool,
pub(crate) display_name_ambiguous: bool,
}
impl RoomMember {
/// Get the unique user id of this member.
pub fn user_id(&self) -> &UserId {
&self.event.state_key
}
/// Get the display name of the member if there is one.
pub fn display_name(&self) -> Option<&str> {
if let Some(p) = self.profile.as_ref() {
p.displayname.as_deref()
} else {
self.event.content.displayname.as_deref()
}
}
/// Get the name of the member.
///
/// This returns either the display name or the local part of the user id if
/// the member didn't set a display name.
pub fn name(&self) -> &str {
if let Some(d) = self.display_name() {
d
} else {
self.user_id().localpart()
}
}
/// Get the avatar url of the member, if there is one.
pub fn avatar_url(&self) -> Option<&MxcUri> {
match self.profile.as_ref() {
Some(p) => p.avatar_url.as_ref(),
None => self.event.content.avatar_url.as_ref(),
}
}
/// Get the normalized power level of this member.
///
/// The normalized power level depends on the maximum power level that can
/// be found in a certain room, it's always in the range of 0-100.
pub fn normalized_power_level(&self) -> i64 {
if self.max_power_level > 0 {
(self.power_level() * 100) / self.max_power_level
} else {
self.power_level()
}
}
/// Get the power level of this member.
pub fn power_level(&self) -> i64 {
self.power_levels
.as_ref()
.as_ref()
.map(|e| {
e.content
.users
.get(self.user_id())
.map(|p| (*p).into())
.unwrap_or_else(|| e.content.users_default.into())
})
.unwrap_or_else(|| if self.is_room_creator { 100 } else { 0 })
}
/// Is the name that the member uses ambiguous in the room.
///
/// A name is considered to be ambiguous if at least one other member shares
/// the same name.
pub fn name_ambiguous(&self) -> bool {
self.display_name_ambiguous
}
}
+227
View File
@@ -0,0 +1,227 @@
mod members;
mod normal;
use std::cmp::max;
pub use members::RoomMember;
pub use normal::{Room, RoomInfo, RoomType};
use ruma::{
events::{
room::{
create::CreateEventContent, encryption::EncryptionEventContent,
guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule,
tombstone::TombstoneEventContent,
},
AnyStateEventContent,
},
MxcUri, RoomAliasId, UserId,
};
use serde::{Deserialize, Serialize};
/// A base room info struct that is the backbone of normal as well as stripped
/// rooms. Holds all the state events that are important to present a room to
/// users.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BaseRoomInfo {
/// The avatar URL of this room.
pub avatar_url: Option<MxcUri>,
/// The canonical alias of this room.
pub canonical_alias: Option<RoomAliasId>,
/// The `m.room.create` event content of this room.
pub create: Option<CreateEventContent>,
/// The user id this room is sharing the direct message with, if the room is
/// a direct message.
pub dm_target: Option<UserId>,
/// The `m.room.encryption` event content that enabled E2EE in this room.
pub encryption: Option<EncryptionEventContent>,
/// The guest access policy of this room.
pub guest_access: GuestAccess,
/// The history visibility policy of this room.
pub history_visibility: HistoryVisibility,
/// The join rule policy of this room.
pub join_rule: JoinRule,
/// The maximal power level that can be found in this room.
pub max_power_level: i64,
/// The `m.room.name` of this room.
pub name: Option<String>,
/// The `m.room.tombstone` event content of this room.
pub tombstone: Option<TombstoneEventContent>,
/// The topic of this room.
pub topic: Option<String>,
}
impl BaseRoomInfo {
/// Create a new, empty base room info.
pub fn new() -> Self {
Self::default()
}
pub(crate) fn calculate_room_name(
&self,
joined_member_count: u64,
invited_member_count: u64,
heroes: Vec<RoomMember>,
) -> String {
calculate_room_name(
joined_member_count,
invited_member_count,
heroes.iter().take(3).map(|mem| mem.name()).collect::<Vec<&str>>(),
)
}
/// Handle a state event for this room and update our info accordingly.
///
/// Returns true if the event modified the info, false otherwise.
pub fn handle_state_event(&mut self, content: &AnyStateEventContent) -> bool {
match content {
AnyStateEventContent::RoomEncryption(encryption) => {
self.encryption = Some(encryption.clone());
true
}
AnyStateEventContent::RoomAvatar(a) => {
self.avatar_url = a.url.clone();
true
}
AnyStateEventContent::RoomName(n) => {
self.name = n.name().map(|n| n.to_string());
true
}
AnyStateEventContent::RoomCreate(c) => {
if self.create.is_none() {
self.create = Some(c.clone());
true
} else {
false
}
}
AnyStateEventContent::RoomHistoryVisibility(h) => {
self.history_visibility = h.history_visibility.clone();
true
}
AnyStateEventContent::RoomGuestAccess(g) => {
self.guest_access = g.guest_access.clone();
true
}
AnyStateEventContent::RoomJoinRules(c) => {
self.join_rule = c.join_rule.clone();
true
}
AnyStateEventContent::RoomCanonicalAlias(a) => {
self.canonical_alias = a.alias.clone();
true
}
AnyStateEventContent::RoomTopic(t) => {
self.topic = Some(t.topic.clone());
true
}
AnyStateEventContent::RoomTombstone(t) => {
self.tombstone = Some(t.clone());
true
}
AnyStateEventContent::RoomPowerLevels(p) => {
let max_power_level =
p.users.values().fold(self.max_power_level, |acc, p| max(acc, (*p).into()));
self.max_power_level = max_power_level;
true
}
_ => false,
}
}
}
impl Default for BaseRoomInfo {
fn default() -> Self {
Self {
avatar_url: None,
canonical_alias: None,
create: None,
dm_target: None,
encryption: None,
guest_access: GuestAccess::CanJoin,
history_visibility: HistoryVisibility::WorldReadable,
join_rule: JoinRule::Public,
max_power_level: 100,
name: None,
tombstone: None,
topic: None,
}
}
}
/// Calculate room name according to step 3 of the [naming algorithm.][spec]
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
fn calculate_room_name(
joined_member_count: u64,
invited_member_count: u64,
heroes: Vec<&str>,
) -> String {
let heroes_count = heroes.len() as u64;
let invited_joined = invited_member_count + joined_member_count;
let invited_joined_minus_one = invited_joined.saturating_sub(1);
let names = if heroes_count >= invited_joined_minus_one {
let mut names = heroes;
// stabilize ordering
names.sort_unstable();
names.join(", ")
} else if heroes_count < invited_joined_minus_one && invited_joined > 1 {
let mut names = heroes;
names.sort_unstable();
// TODO: What length does the spec want us to use here and in
// the `else`?
format!("{}, and {} others", names.join(", "), (invited_joined - heroes_count))
} else {
"".to_string()
};
// User is alone.
if invited_joined <= 1 {
if names.is_empty() {
"Empty room".to_string()
} else {
format!("Empty room (was {})", names)
}
} else {
names
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_room_name() {
let mut actual = calculate_room_name(2, 0, vec!["a"]);
assert_eq!("a", actual);
actual = calculate_room_name(3, 0, vec!["a", "b"]);
assert_eq!("a, b", actual);
actual = calculate_room_name(4, 0, vec!["a", "b", "c"]);
assert_eq!("a, b, c", actual);
actual = calculate_room_name(5, 0, vec!["a", "b", "c"]);
assert_eq!("a, b, c, and 2 others", actual);
actual = calculate_room_name(0, 0, vec![]);
assert_eq!("Empty room", actual);
actual = calculate_room_name(1, 0, vec![]);
assert_eq!("Empty room", actual);
actual = calculate_room_name(0, 1, vec![]);
assert_eq!("Empty room", actual);
actual = calculate_room_name(1, 0, vec!["a"]);
assert_eq!("Empty room (was a)", actual);
actual = calculate_room_name(1, 0, vec!["a", "b"]);
assert_eq!("Empty room (was a, b)", actual);
actual = calculate_room_name(1, 0, vec!["a", "b", "c"]);
assert_eq!("Empty room (was a, b, c)", actual);
}
}
+565
View File
@@ -0,0 +1,565 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
convert::TryFrom,
sync::{Arc, RwLock as SyncRwLock},
};
use futures::{
future,
stream::{self, StreamExt},
};
use ruma::{
api::client::r0::sync::sync_events::RoomSummary as RumaSummary,
events::{
receipt::Receipt,
room::{
create::CreateEventContent, encryption::EncryptionEventContent,
guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule,
tombstone::TombstoneEventContent,
},
tag::Tags,
AnyRoomAccountDataEvent, AnyStateEventContent, AnySyncStateEvent, EventType,
},
receipt::ReceiptType,
EventId, MxcUri, RoomAliasId, RoomId, UserId,
};
use serde::{Deserialize, Serialize};
use tracing::info;
use super::{BaseRoomInfo, RoomMember};
use crate::{
deserialized_responses::UnreadNotificationsCount,
store::{Result as StoreResult, StateStore},
};
/// The underlying room data structure collecting state for joined, left and
/// invited rooms.
#[derive(Debug, Clone)]
pub struct Room {
room_id: Arc<RoomId>,
own_user_id: Arc<UserId>,
inner: Arc<SyncRwLock<RoomInfo>>,
store: Arc<dyn StateStore>,
}
/// The room summary containing member counts and members that should be used to
/// calculate the room display name.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RoomSummary {
/// The heroes of the room, members that should be used for the room display
/// name.
heroes: Vec<String>,
/// The number of members that are considered to be joined to the room.
joined_member_count: u64,
/// The number of members that are considered to be invited to the room.
invited_member_count: u64,
}
/// Enum keeping track in which state the room is, e.g. if our own user is
/// joined, invited, or has left the room.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum RoomType {
/// The room is in a joined state.
Joined,
/// The room is in a left state.
Left,
/// The room is in a invited state.
Invited,
}
impl Room {
pub(crate) fn new(
own_user_id: &UserId,
store: Arc<dyn StateStore>,
room_id: &RoomId,
room_type: RoomType,
) -> Self {
let room_id = Arc::new(room_id.clone());
let room_info = RoomInfo {
room_id,
room_type,
notification_counts: Default::default(),
summary: Default::default(),
members_synced: false,
last_prev_batch: None,
base_info: BaseRoomInfo::new(),
};
Self::restore(own_user_id, store, room_info)
}
pub(crate) fn restore(
own_user_id: &UserId,
store: Arc<dyn StateStore>,
room_info: RoomInfo,
) -> Self {
Self {
own_user_id: Arc::new(own_user_id.clone()),
room_id: room_info.room_id.clone(),
store,
inner: Arc::new(SyncRwLock::new(room_info)),
}
}
/// Get the unique room id of the room.
pub fn room_id(&self) -> &RoomId {
&self.room_id
}
/// Get our own user id.
pub fn own_user_id(&self) -> &UserId {
&self.own_user_id
}
/// Get the type of the room.
pub fn room_type(&self) -> RoomType {
self.inner.read().unwrap().room_type
}
/// Get the unread notification counts.
pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
self.inner.read().unwrap().notification_counts
}
/// Check if the room has it's members fully synced.
///
/// Members might be missing if lazy member loading was enabled for the
/// sync.
///
/// Returns true if no members are missing, false otherwise.
pub fn are_members_synced(&self) -> bool {
self.inner.read().unwrap().members_synced
}
/// Get the `prev_batch` token that was received from the last sync. May be
/// `None` if the last sync contained the full room history.
pub fn last_prev_batch(&self) -> Option<String> {
self.inner.read().unwrap().last_prev_batch.clone()
}
/// Get the avatar url of this room.
pub fn avatar_url(&self) -> Option<MxcUri> {
self.inner.read().unwrap().base_info.avatar_url.clone()
}
/// Get the canonical alias of this room.
pub fn canonical_alias(&self) -> Option<RoomAliasId> {
self.inner.read().unwrap().base_info.canonical_alias.clone()
}
/// Get the `m.room.create` content of this room.
///
/// This usually isn't optional but some servers might not send an
/// `m.room.create` event as the first event for a given room, thus this can
/// be optional.
pub fn create_content(&self) -> Option<CreateEventContent> {
self.inner.read().unwrap().base_info.create.clone()
}
/// Is this room considered a direct message.
pub fn is_direct(&self) -> bool {
self.inner.read().unwrap().base_info.dm_target.is_some()
}
/// If this room is a direct message, get the member that we're sharing the
/// room with.
///
/// *Note*: The member list might have been modified in the meantime and
/// the target might not even be in the room anymore. This setting should
/// only be considered as guidance.
pub fn direct_target(&self) -> Option<UserId> {
self.inner.read().unwrap().base_info.dm_target.clone()
}
/// Is the room encrypted.
pub fn is_encrypted(&self) -> bool {
self.inner.read().unwrap().is_encrypted()
}
/// Get the `m.room.encryption` content that enabled end to end encryption
/// in the room.
pub fn encryption_settings(&self) -> Option<EncryptionEventContent> {
self.inner.read().unwrap().base_info.encryption.clone()
}
/// Get the guest access policy of this room.
pub fn guest_access(&self) -> GuestAccess {
self.inner.read().unwrap().base_info.guest_access.clone()
}
/// Get the history visibility policy of this room.
pub fn history_visibility(&self) -> HistoryVisibility {
self.inner.read().unwrap().base_info.history_visibility.clone()
}
/// Is the room considered to be public.
pub fn is_public(&self) -> bool {
matches!(self.join_rule(), JoinRule::Public)
}
/// Get the join rule policy of this room.
pub fn join_rule(&self) -> JoinRule {
self.inner.read().unwrap().base_info.join_rule.clone()
}
/// Get the maximum power level that this room contains.
///
/// This is useful if one wishes to normalize the power levels, e.g. from
/// 0-100 where 100 would be the max power level.
pub fn max_power_level(&self) -> i64 {
self.inner.read().unwrap().base_info.max_power_level
}
/// Get the `m.room.name` of this room.
pub fn name(&self) -> Option<String> {
self.inner.read().unwrap().base_info.name.clone()
}
/// Has the room been tombstoned.
pub fn is_tombstoned(&self) -> bool {
self.inner.read().unwrap().base_info.tombstone.is_some()
}
/// Get the `m.room.tombstone` content of this room if there is one.
pub fn tombstone(&self) -> Option<TombstoneEventContent> {
self.inner.read().unwrap().base_info.tombstone.clone()
}
/// Get the topic of the room.
pub fn topic(&self) -> Option<String> {
self.inner.read().unwrap().base_info.topic.clone()
}
/// Calculate the canonical display name of the room, taking into account
/// its name, aliases and members.
///
/// The display name is calculated according to [this algorithm][spec].
///
/// [spec]: <https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room>
pub async fn display_name(&self) -> StoreResult<String> {
self.calculate_name().await
}
/// Get the list of users ids that are considered to be joined members of
/// this room.
pub async fn joined_user_ids(&self) -> StoreResult<Vec<UserId>> {
self.store.get_joined_user_ids(self.room_id()).await
}
/// Get the all `RoomMember`s of this room that are known to the store.
pub async fn members(&self) -> StoreResult<Vec<RoomMember>> {
let user_ids = self.store.get_user_ids(self.room_id()).await?;
let mut members = Vec::new();
for u in user_ids {
let m = self.get_member(&u).await?;
if let Some(member) = m {
members.push(member);
}
}
Ok(members)
}
/// Get the list of `RoomMember`s that are considered to be joined members
/// of this room.
pub async fn joined_members(&self) -> StoreResult<Vec<RoomMember>> {
let joined = self.store.get_joined_user_ids(self.room_id()).await?;
let mut members = Vec::new();
for u in joined {
let m = self.get_member(&u).await?;
if let Some(member) = m {
members.push(member);
}
}
Ok(members)
}
/// Get the list of `RoomMember`s that are considered to be joined or
/// invited members of this room.
pub async fn active_members(&self) -> StoreResult<Vec<RoomMember>> {
let joined = self.store.get_joined_user_ids(self.room_id()).await?;
let invited = self.store.get_invited_user_ids(self.room_id()).await?;
let mut members = Vec::new();
for u in joined.iter().chain(&invited) {
let m = self.get_member(u).await?;
if let Some(member) = m {
members.push(member);
}
}
Ok(members)
}
async fn calculate_name(&self) -> StoreResult<String> {
let summary = {
let inner = self.inner.read().unwrap();
if let Some(name) = &inner.base_info.name {
let name = name.trim();
return Ok(name.to_string());
} else if let Some(alias) = &inner.base_info.canonical_alias {
let alias = alias.alias().trim();
return Ok(alias.to_string());
}
inner.summary.clone()
};
// TODO what should we do here? We have correct counts only if lazy
// loading is used.
let joined = summary.joined_member_count;
let invited = summary.invited_member_count;
let heroes_count = summary.heroes.len() as u64;
let is_own_member = |m: &RoomMember| m.user_id() == &*self.own_user_id;
let is_own_user_id = |u: &str| u == self.own_user_id().as_str();
let members: Vec<RoomMember> = if summary.heroes.is_empty() {
self.active_members().await?.into_iter().filter(|u| !is_own_member(u)).take(5).collect()
} else {
let members: Vec<_> = stream::iter(summary.heroes.iter())
.filter(|u| future::ready(!is_own_user_id(u)))
.filter_map(|u| async move {
let user_id = UserId::try_from(u.as_str()).ok()?;
self.get_member(&user_id).await.transpose()
})
.collect()
.await;
let members: StoreResult<Vec<_>> = members.into_iter().collect();
members?
};
info!(
"Calculating name for {}, own user {} hero count {} heroes {:#?}",
self.room_id(),
self.own_user_id,
heroes_count,
summary.heroes
);
let inner = self.inner.read().unwrap();
Ok(inner.base_info.calculate_room_name(joined, invited, members))
}
pub(crate) fn clone_info(&self) -> RoomInfo {
(*self.inner.read().unwrap()).clone()
}
pub(crate) fn update_summary(&self, summary: RoomInfo) {
let mut inner = self.inner.write().unwrap();
*inner = summary;
}
/// Get the `RoomMember` with the given `user_id`.
///
/// Returns `None` if the member was never part of this room, otherwise
/// return a `RoomMember` that can be in a joined, invited, left, banned
/// state.
pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
let member_event =
if let Some(m) = self.store.get_member_event(self.room_id(), user_id).await? {
m
} else {
return Ok(None);
};
let presence =
self.store.get_presence_event(user_id).await?.and_then(|e| e.deserialize().ok());
let profile = self.store.get_profile(self.room_id(), user_id).await?;
let max_power_level = self.max_power_level();
let is_room_creator = self
.inner
.read()
.unwrap()
.base_info
.create
.as_ref()
.map(|c| &c.creator == user_id)
.unwrap_or(false);
let power =
self.store
.get_state_event(self.room_id(), EventType::RoomPowerLevels, "")
.await?
.and_then(|e| e.deserialize().ok())
.and_then(|e| {
if let AnySyncStateEvent::RoomPowerLevels(e) = e {
Some(e)
} else {
None
}
});
let ambiguous = self
.store
.get_users_with_display_name(
self.room_id(),
member_event.content.displayname.as_deref().unwrap_or_else(|| user_id.localpart()),
)
.await?
.len()
> 1;
Ok(Some(RoomMember {
event: member_event.into(),
profile: profile.into(),
presence: presence.into(),
power_levels: power.into(),
max_power_level,
is_room_creator,
display_name_ambiguous: ambiguous,
}))
}
/// Get the `Tags` for this room.
pub async fn tags(&self) -> StoreResult<Option<Tags>> {
if let Some(AnyRoomAccountDataEvent::Tag(event)) = self
.store
.get_room_account_data_event(self.room_id(), EventType::Tag)
.await?
.and_then(|r| r.deserialize().ok())
{
Ok(Some(event.content.tags))
} else {
Ok(None)
}
}
/// Get the read receipt as a `EventId` and `Receipt` tuple for the given
/// `user_id` in this room.
pub async fn user_read_receipt(
&self,
user_id: &UserId,
) -> StoreResult<Option<(EventId, Receipt)>> {
self.store.get_user_room_receipt_event(self.room_id(), ReceiptType::Read, user_id).await
}
/// Get the read receipts as a list of `UserId` and `Receipt` tuples for the
/// given `event_id` in this room.
pub async fn event_read_receipts(
&self,
event_id: &EventId,
) -> StoreResult<Vec<(UserId, Receipt)>> {
self.store.get_event_room_receipt_events(self.room_id(), ReceiptType::Read, event_id).await
}
}
/// The underlying pure data structure for joined and left rooms.
///
/// Holds all the info needed to persist a room into the state store.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RoomInfo {
/// The unique room id of the room.
pub room_id: Arc<RoomId>,
/// The type of the room.
pub room_type: RoomType,
/// The unread notifications counts.
pub notification_counts: UnreadNotificationsCount,
/// The summary of this room.
pub summary: RoomSummary,
/// Flag remembering if the room members are synced.
pub members_synced: bool,
/// The prev batch of this room we received during the last sync.
pub last_prev_batch: Option<String>,
/// Base room info which holds some basic event contents important for the
/// room state.
pub base_info: BaseRoomInfo,
}
impl RoomInfo {
pub(crate) fn mark_as_joined(&mut self) {
self.room_type = RoomType::Joined;
}
pub(crate) fn mark_as_left(&mut self) {
self.room_type = RoomType::Left;
}
pub(crate) fn mark_as_invited(&mut self) {
self.room_type = RoomType::Invited;
}
pub(crate) fn mark_members_synced(&mut self) {
self.members_synced = true;
}
pub(crate) fn mark_members_missing(&mut self) {
self.members_synced = false;
}
pub(crate) fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
if self.last_prev_batch.as_deref() != prev_batch {
self.last_prev_batch = prev_batch.map(|p| p.to_string());
true
} else {
false
}
}
pub(crate) fn is_encrypted(&self) -> bool {
self.base_info.encryption.is_some()
}
pub(crate) fn handle_state_event(&mut self, event: &AnyStateEventContent) -> bool {
self.base_info.handle_state_event(event)
}
pub(crate) fn update_notification_count(
&mut self,
notification_counts: UnreadNotificationsCount,
) {
self.notification_counts = notification_counts;
}
pub(crate) fn update_summary(&mut self, summary: &RumaSummary) -> bool {
let mut changed = false;
if !summary.is_empty() {
if !summary.heroes.is_empty() {
self.summary.heroes = summary.heroes.clone();
changed = true;
}
if let Some(joined) = summary.joined_member_count {
self.summary.joined_member_count = joined.into();
changed = true;
}
if let Some(invited) = summary.invited_member_count {
self.summary.invited_member_count = invited.into();
changed = true;
}
}
changed
}
/// The number of active members (invited + joined) in the room.
///
/// The return value is saturated at `u64::MAX`.
pub fn active_members_count(&self) -> u64 {
self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
}
}
+1 -2
View File
@@ -15,10 +15,9 @@
//! User sessions.
use ruma::{DeviceId, UserId};
use serde::{Deserialize, Serialize};
use matrix_sdk_common::identifiers::{DeviceId, UserId};
/// A user session, containing an access token and information about the
/// associated user account.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
-361
View File
@@ -1,361 +0,0 @@
use std::{
collections::HashMap,
fmt, fs,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use matrix_sdk_common::{async_trait, identifiers::RoomId, locks::RwLock};
use tokio::{fs as async_fs, io::AsyncWriteExt};
use super::{AllRooms, ClientState, StateStore};
use crate::{Error, Result, Room, RoomState, Session};
/// A default `StateStore` implementation that serializes state as json
/// and saves it to disk.
///
/// When logged in the `JsonStore` appends the user_id to its folder path,
/// so all files are saved in `my_client/user_id_localpart/*`.
pub struct JsonStore {
path: Arc<RwLock<PathBuf>>,
user_path_set: AtomicBool,
}
impl JsonStore {
/// Create a `JsonStore` to store the client and room state.
///
/// Checks if the provided path exists and creates the directories if not.
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let p = path.as_ref();
if !p.exists() {
fs::create_dir_all(p)?;
}
Ok(Self {
path: Arc::new(RwLock::new(p.to_path_buf())),
user_path_set: AtomicBool::new(false),
})
}
/// Build a path for a file where the Room state to be stored in.
async fn build_room_path(&self, room_state: &str, room_id: &RoomId) -> PathBuf {
let mut path = self.path.read().await.clone();
path.push("rooms");
path.push(room_state);
path.push(JsonStore::sanitize_room_id(room_id));
path.set_extension("json");
path
}
/// Build a path for the file where the Client state to be stored in.
async fn build_client_path(&self) -> PathBuf {
let mut path = self.path.read().await.clone();
path.push("client");
path.set_extension("json");
path
}
/// Replace common characters that can't be used in a file name with an
/// underscore.
fn sanitize_room_id(room_id: &RoomId) -> String {
room_id.as_str().replace(
&['.', ':', '<', '>', '"', '/', '\\', '|', '?', '*'][..],
"_",
)
}
}
impl fmt::Debug for JsonStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JsonStore")
.field("path", &self.path)
.finish()
}
}
#[async_trait]
impl StateStore for JsonStore {
async fn load_client_state(&self, sess: &Session) -> Result<Option<ClientState>> {
if !self.user_path_set.load(Ordering::SeqCst) {
self.user_path_set.swap(true, Ordering::SeqCst);
self.path.write().await.push(sess.user_id.localpart())
}
let path = self.build_client_path().await;
let json = async_fs::read_to_string(path)
.await
.map_or(String::default(), |s| s);
if json.is_empty() {
Ok(None)
} else {
serde_json::from_str(&json).map(Some).map_err(Error::from)
}
}
async fn load_all_rooms(&self) -> Result<AllRooms> {
let mut path = self.path.read().await.clone();
path.push("rooms");
let mut joined = HashMap::new();
let mut left = HashMap::new();
let mut invited = HashMap::new();
for room_state_type in &["joined", "invited", "left"] {
path.push(room_state_type);
// don't load rooms that aren't saved yet
if !path.exists() {
path.pop();
continue;
}
for file in fs::read_dir(&path)? {
let file = file?.path();
if file.is_dir() {
continue;
}
let json = async_fs::read_to_string(&file).await?;
let room = serde_json::from_str::<Room>(&json).map_err(Error::from)?;
let room_id = room.room_id.clone();
match *room_state_type {
"joined" => joined.insert(room_id, room),
"invited" => invited.insert(room_id, room),
"left" => left.insert(room_id, room),
_ => unreachable!("an array with 3 const elements was altered in JsonStore"),
};
}
path.pop();
}
Ok(AllRooms {
joined,
left,
invited,
})
}
async fn store_client_state(&self, state: ClientState) -> Result<()> {
let path = self.build_client_path().await;
if !path.exists() {
let mut dir = path.clone();
dir.pop();
async_fs::create_dir_all(dir).await?;
}
let json = serde_json::to_string(&state).map_err(Error::from)?;
let mut file = async_fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.await?;
file.write_all(json.as_bytes()).await.map_err(Error::from)
}
async fn store_room_state(&self, room: RoomState<&Room>) -> Result<()> {
let (room, room_state) = match room {
RoomState::Joined(room) => (room, "joined"),
RoomState::Invited(room) => (room, "invited"),
RoomState::Left(room) => (room, "left"),
};
if !self.user_path_set.load(Ordering::SeqCst) {
self.user_path_set.swap(true, Ordering::SeqCst);
self.path.write().await.push(room.own_user_id.localpart())
}
let path = self.build_room_path(room_state, &room.room_id).await;
if !path.exists() {
let mut dir = path.clone();
dir.pop();
async_fs::create_dir_all(dir).await?;
}
let json = serde_json::to_string(&room).map_err(Error::from)?;
let mut file = async_fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.await?;
file.write_all(json.as_bytes()).await.map_err(Error::from)
}
async fn delete_room_state(&self, room: RoomState<&RoomId>) -> Result<()> {
let (room_id, room_state) = match &room {
RoomState::Joined(id) => (id, "joined"),
RoomState::Invited(id) => (id, "invited"),
RoomState::Left(id) => (id, "left"),
};
if !self.user_path_set.load(Ordering::SeqCst) {
return Err(Error::StateStore("path for JsonStore not set".into()));
}
let path = self.build_room_path(room_state, room_id).await;
if !path.exists() {
return Err(Error::StateStore(format!("file {:?} not found", path)));
}
tokio::fs::remove_file(path).await.map_err(Error::from)
}
}
#[cfg(test)]
mod test {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
use crate::{
identifiers::{room_id, user_id},
push::Ruleset,
Session,
};
#[tokio::test]
async fn test_store_client_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let user = user_id!("@example:example.com");
let sess = Session {
access_token: "32nj9zu034btz90".to_string(),
user_id: user.clone(),
device_id: "Tester".into(),
};
let state = ClientState {
sync_token: Some("hello".into()),
ignored_users: vec![user],
push_ruleset: None::<Ruleset>,
};
let mut path_with_user = PathBuf::from(path);
path_with_user.push(sess.user_id.localpart());
// we have to set the path since `JsonStore::store_client_state()` doesn't append to the path
let store = JsonStore::open(path_with_user).unwrap();
store.store_client_state(state.clone()).await.unwrap();
// the newly loaded store sets it own user_id local part when `load_client_state`
let store = JsonStore::open(path).unwrap();
let loaded = store.load_client_state(&sess).await.unwrap();
assert_eq!(loaded, Some(state));
}
#[tokio::test]
async fn test_store_load_joined_room_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
.store_room_state(RoomState::Joined(&room))
.await
.unwrap();
let AllRooms { joined, .. } = store.load_all_rooms().await.unwrap();
assert_eq!(joined.get(&id), Some(&Room::new(&id, &user)));
}
#[tokio::test]
async fn test_store_load_left_room_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
.store_room_state(RoomState::Left(&room))
.await
.unwrap();
let AllRooms { left, .. } = store.load_all_rooms().await.unwrap();
assert_eq!(left.get(&id), Some(&Room::new(&id, &user)));
}
#[tokio::test]
async fn test_store_load_invited_room_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
.store_room_state(RoomState::Invited(&room))
.await
.unwrap();
let AllRooms { invited, .. } = store.load_all_rooms().await.unwrap();
assert_eq!(invited.get(&id), Some(&Room::new(&id, &user)));
}
#[tokio::test]
async fn test_store_load_join_leave_room_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
.store_room_state(RoomState::Joined(&room))
.await
.unwrap();
assert!(store
.delete_room_state(RoomState::Joined(&id))
.await
.is_ok());
let AllRooms { joined, .. } = store.load_all_rooms().await.unwrap();
// test that we have removed the correct room
assert!(joined.is_empty());
}
#[tokio::test]
async fn test_store_load_invite_join_room_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let store = JsonStore::open(path).unwrap();
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
store
.store_room_state(RoomState::Invited(&room))
.await
.unwrap();
assert!(store
.delete_room_state(RoomState::Invited(&id))
.await
.is_ok());
let AllRooms { invited, .. } = store.load_all_rooms().await.unwrap();
// test that we have removed the correct room
assert!(invited.is_empty());
}
}
-229
View File
@@ -1,229 +0,0 @@
// Copyright 2020 Devin Ragotzy
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::HashMap;
use matrix_sdk_common::{
async_trait,
identifiers::{RoomId, UserId},
push::Ruleset,
AsyncTraitDeps,
};
use serde::{Deserialize, Serialize};
#[cfg(not(target_arch = "wasm32"))]
mod json_store;
#[cfg(not(target_arch = "wasm32"))]
pub use json_store::JsonStore;
use crate::{
client::{BaseClient, Token},
Result, Room, RoomState, Session,
};
/// `ClientState` holds all the information to restore a `BaseClient`
/// except the `access_token` as the default store is not secure.
///
/// When implementing `StateStore` for something other than the filesystem
/// implement `From<ClientState> for YourDbType` this allows for easy conversion
/// when needed in `StateStore::load/store_client_state`
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ClientState {
/// The current sync token that should be used for the next sync call.
pub sync_token: Option<Token>,
/// A list of ignored users.
pub ignored_users: Vec<UserId>,
/// The push ruleset for the logged in user.
pub push_ruleset: Option<Ruleset>,
}
impl PartialEq for ClientState {
fn eq(&self, other: &Self) -> bool {
self.sync_token == other.sync_token && self.ignored_users == other.ignored_users
}
}
impl ClientState {
/// Create a JSON serialize-able `ClientState`.
///
/// This enables non sensitive information to be saved by `JsonStore`.
#[allow(clippy::eval_order_dependence)]
// TODO is this ok ^^^?? https://github.com/rust-lang/rust-clippy/issues/4637
pub async fn from_base_client(client: &BaseClient) -> ClientState {
let BaseClient {
sync_token,
ignored_users,
push_ruleset,
..
} = client;
Self {
sync_token: sync_token.read().await.clone(),
ignored_users: ignored_users.read().await.clone(),
push_ruleset: push_ruleset.read().await.clone(),
}
}
}
/// `JsonStore::load_all_rooms` returns `AllRooms`.
///
/// `AllRooms` is made of the `joined`, `invited` and `left` room maps.
#[derive(Debug)]
pub struct AllRooms {
/// The joined room mapping of `RoomId` to `Room`.
pub joined: HashMap<RoomId, Room>,
/// The invited room mapping of `RoomId` to `Room`.
pub invited: HashMap<RoomId, Room>,
/// The left room mapping of `RoomId` to `Room`.
pub left: HashMap<RoomId, Room>,
}
/// Abstraction around the data store to avoid unnecessary request on client initialization.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStore: AsyncTraitDeps {
/// Loads the state of `BaseClient` through `ClientState` type.
///
/// An `Option::None` should be returned only if the `StateStore` tries to
/// load but no state has been stored.
async fn load_client_state(&self, _: &Session) -> Result<Option<ClientState>>;
/// Load the state of all `Room`s.
///
/// This will be mapped over in the client in order to store `Room`s in an async safe way.
async fn load_all_rooms(&self) -> Result<AllRooms>;
/// Save the current state of the `BaseClient` using the `StateStore::Store` type.
async fn store_client_state(&self, _: ClientState) -> Result<()>;
/// Save the state a single `Room`.
async fn store_room_state(&self, _: RoomState<&Room>) -> Result<()>;
/// Remove state for a room.
///
/// This is used when a user leaves a room or rejects an invitation.
async fn delete_room_state(&self, _room: RoomState<&RoomId>) -> Result<()>;
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
use crate::identifiers::{room_id, user_id};
#[test]
fn serialize() {
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
let state = ClientState {
sync_token: Some("hello".into()),
ignored_users: vec![user],
push_ruleset: None,
};
assert_eq!(
r#"{"sync_token":"hello","ignored_users":["@example:example.com"],"push_ruleset":null}"#,
serde_json::to_string(&state).unwrap()
);
let mut joined_rooms = HashMap::new();
joined_rooms.insert(id, room);
#[cfg(not(feature = "messages"))]
assert_eq!(
serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"direct_target": null,
"joined_members": {},
"invited_members": {},
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}),
serde_json::to_value(&joined_rooms).unwrap()
);
#[cfg(feature = "messages")]
assert_eq!(
serde_json::json!({
"!roomid:example.com": {
"room_id": "!roomid:example.com",
"room_name": {
"name": null,
"canonical_alias": null,
"aliases": [],
"heroes": [],
"joined_member_count": null,
"invited_member_count": null
},
"own_user_id": "@example:example.com",
"creator": null,
"direct_target": null,
"joined_members": {},
"invited_members": {},
"messages": [],
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}),
serde_json::to_value(&joined_rooms).unwrap()
);
}
#[test]
fn deserialize() {
let id = room_id!("!roomid:example.com");
let user = user_id!("@example:example.com");
let room = Room::new(&id, &user);
let state = ClientState {
sync_token: Some("hello".into()),
ignored_users: vec![user],
push_ruleset: None,
};
let json = serde_json::to_string(&state).unwrap();
assert_eq!(state, serde_json::from_str(&json).unwrap());
let mut joined_rooms = HashMap::new();
joined_rooms.insert(id, room);
let json = serde_json::to_string(&joined_rooms).unwrap();
assert_eq!(joined_rooms, serde_json::from_str(&json).unwrap());
}
}
+244
View File
@@ -0,0 +1,244 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::collections::{BTreeMap, BTreeSet};
use matrix_sdk_common::deserialized_responses::{AmbiguityChange, MemberEvent};
use ruma::{events::room::member::MembershipState, EventId, RoomId, UserId};
use tracing::trace;
use super::{Result, StateChanges};
use crate::Store;
#[derive(Clone, Debug)]
pub struct AmbiguityCache {
pub store: Store,
pub cache: BTreeMap<RoomId, BTreeMap<String, BTreeSet<UserId>>>,
pub changes: BTreeMap<RoomId, BTreeMap<EventId, AmbiguityChange>>,
}
#[derive(Clone, Debug)]
struct AmbiguityMap {
display_name: String,
users: BTreeSet<UserId>,
}
impl AmbiguityMap {
fn remove(&mut self, user_id: &UserId) -> Option<UserId> {
self.users.remove(user_id);
if self.user_count() == 1 {
self.users.iter().next().cloned()
} else {
None
}
}
fn add(&mut self, user_id: UserId) -> Option<UserId> {
let ambiguous_user =
if self.user_count() == 1 { self.users.iter().next().cloned() } else { None };
self.users.insert(user_id);
ambiguous_user
}
fn user_count(&self) -> usize {
self.users.len()
}
fn is_ambiguous(&self) -> bool {
self.user_count() > 1
}
}
impl AmbiguityCache {
pub fn new(store: Store) -> Self {
Self { store, cache: BTreeMap::new(), changes: BTreeMap::new() }
}
pub async fn handle_event(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &MemberEvent,
) -> Result<()> {
// Synapse seems to have a bug where it puts the same event into the
// state and the timeline sometimes.
//
// Since our state, e.g. the old display name, already ended up inside
// the state changes and we're pulling stuff out of the cache if it's
// there calculating this twice for the same event will result in an
// incorrect AmbiguityChange overwriting the correct one. In other
// words, this method is not idempotent so we make it by ignoring
// duplicate events.
if self
.changes
.get(room_id)
.map(|c| c.contains_key(&member_event.event_id))
.unwrap_or(false)
{
return Ok(());
}
let (mut old_map, mut new_map) = self.get(changes, room_id, member_event).await?;
let display_names_same = match (&old_map, &new_map) {
(Some(a), Some(b)) => a.display_name == b.display_name,
_ => false,
};
if display_names_same {
return Ok(());
}
let disambiguated_member = old_map.as_mut().and_then(|o| o.remove(&member_event.state_key));
let ambiguated_member =
new_map.as_mut().and_then(|n| n.add(member_event.state_key.clone()));
let ambiguous = new_map.as_ref().map(|n| n.is_ambiguous()).unwrap_or(false);
self.update(room_id, old_map, new_map);
let change = AmbiguityChange {
disambiguated_member,
ambiguated_member,
member_ambiguous: ambiguous,
};
trace!("Handling display name ambiguity for {}: {:#?}", member_event.state_key, change);
self.add_change(room_id, member_event.event_id.clone(), change);
Ok(())
}
fn update(
&mut self,
room_id: &RoomId,
old_map: Option<AmbiguityMap>,
new_map: Option<AmbiguityMap>,
) {
let entry = self.cache.entry(room_id.clone()).or_insert_with(BTreeMap::new);
if let Some(old) = old_map {
entry.insert(old.display_name, old.users);
}
if let Some(new) = new_map {
entry.insert(new.display_name, new.users);
}
}
fn add_change(&mut self, room_id: &RoomId, event_id: EventId, change: AmbiguityChange) {
self.changes.entry(room_id.clone()).or_insert_with(BTreeMap::new).insert(event_id, change);
}
async fn get(
&mut self,
changes: &StateChanges,
room_id: &RoomId,
member_event: &MemberEvent,
) -> Result<(Option<AmbiguityMap>, Option<AmbiguityMap>)> {
use MembershipState::*;
let old_event = if let Some(m) =
changes.members.get(room_id).and_then(|m| m.get(&member_event.state_key))
{
Some(m.clone())
} else {
self.store.get_member_event(room_id, &member_event.state_key).await?
};
let old_display_name = if let Some(event) = old_event {
if matches!(event.content.membership, Join | Invite) {
let display_name = if let Some(d) = changes
.profiles
.get(room_id)
.and_then(|p| p.get(&member_event.state_key))
.and_then(|p| p.displayname.as_deref())
{
Some(d.to_string())
} else if let Some(d) = self
.store
.get_profile(room_id, &member_event.state_key)
.await?
.and_then(|c| c.displayname)
{
Some(d)
} else {
event.content.displayname.clone()
};
Some(display_name.unwrap_or_else(|| event.state_key.localpart().to_string()))
} else {
None
}
} else {
None
};
let old_map = if let Some(old_name) = old_display_name.as_deref() {
let old_display_name_map = if let Some(u) =
self.cache.entry(room_id.clone()).or_insert_with(BTreeMap::new).get(old_name)
{
u.clone()
} else {
self.store.get_users_with_display_name(room_id, old_name).await?
};
Some(AmbiguityMap { display_name: old_name.to_string(), users: old_display_name_map })
} else {
None
};
let new_map = if matches!(member_event.content.membership, Join | Invite) {
let new = member_event
.content
.displayname
.as_deref()
.unwrap_or_else(|| member_event.state_key.localpart());
// We don't allow other users to set the display name, so if we
// have a more trusted version of the display
// name use that.
let new_display_name = if member_event.sender.as_str() == member_event.state_key {
new
} else if let Some(old) = old_display_name.as_deref() {
old
} else {
new
};
let new_display_name_map = if let Some(u) = self
.cache
.entry(room_id.clone())
.or_insert_with(BTreeMap::new)
.get(new_display_name)
{
u.clone()
} else {
self.store.get_users_with_display_name(room_id, new_display_name).await?
};
Some(AmbiguityMap {
display_name: new_display_name.to_string(),
users: new_display_name_map,
})
} else {
None
};
Ok((old_map, new_map))
}
}
+711
View File
@@ -0,0 +1,711 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{
collections::BTreeSet,
sync::{Arc, RwLock},
};
use dashmap::{DashMap, DashSet};
use lru::LruCache;
use matrix_sdk_common::{async_trait, instant::Instant, locks::Mutex};
use ruma::{
events::{
presence::PresenceEvent,
receipt::Receipt,
room::member::{MemberEventContent, MembershipState},
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncStateEvent, EventType,
},
identifiers::{EventId, MxcUri, RoomId, UserId},
receipt::ReceiptType,
serde::Raw,
};
use tracing::info;
use super::{Result, RoomInfo, StateChanges, StateStore};
use crate::{
deserialized_responses::{MemberEvent, StrippedMemberEvent},
media::{MediaRequest, UniqueKey},
};
#[derive(Debug, Clone)]
pub struct MemoryStore {
sync_token: Arc<RwLock<Option<String>>>,
filters: Arc<DashMap<String, String>>,
account_data: Arc<DashMap<String, Raw<AnyGlobalAccountDataEvent>>>,
members: Arc<DashMap<RoomId, DashMap<UserId, MemberEvent>>>,
profiles: Arc<DashMap<RoomId, DashMap<UserId, MemberEventContent>>>,
display_names: Arc<DashMap<RoomId, DashMap<String, BTreeSet<UserId>>>>,
joined_user_ids: Arc<DashMap<RoomId, DashSet<UserId>>>,
invited_user_ids: Arc<DashMap<RoomId, DashSet<UserId>>>,
room_info: Arc<DashMap<RoomId, RoomInfo>>,
#[allow(clippy::type_complexity)]
room_state: Arc<DashMap<RoomId, DashMap<String, DashMap<String, Raw<AnySyncStateEvent>>>>>,
room_account_data: Arc<DashMap<RoomId, DashMap<String, Raw<AnyRoomAccountDataEvent>>>>,
stripped_room_info: Arc<DashMap<RoomId, RoomInfo>>,
#[allow(clippy::type_complexity)]
stripped_room_state:
Arc<DashMap<RoomId, DashMap<String, DashMap<String, Raw<AnyStrippedStateEvent>>>>>,
stripped_members: Arc<DashMap<RoomId, DashMap<UserId, StrippedMemberEvent>>>,
presence: Arc<DashMap<UserId, Raw<PresenceEvent>>>,
#[allow(clippy::type_complexity)]
room_user_receipts: Arc<DashMap<RoomId, DashMap<String, DashMap<UserId, (EventId, Receipt)>>>>,
#[allow(clippy::type_complexity)]
room_event_receipts:
Arc<DashMap<RoomId, DashMap<String, DashMap<EventId, DashMap<UserId, Receipt>>>>>,
media: Arc<Mutex<LruCache<String, Vec<u8>>>>,
}
impl MemoryStore {
#[cfg(not(feature = "sled_state_store"))]
pub fn new() -> Self {
Self {
sync_token: Arc::new(RwLock::new(None)),
filters: DashMap::new().into(),
account_data: DashMap::new().into(),
members: DashMap::new().into(),
profiles: DashMap::new().into(),
display_names: DashMap::new().into(),
joined_user_ids: DashMap::new().into(),
invited_user_ids: DashMap::new().into(),
room_info: DashMap::new().into(),
room_state: DashMap::new().into(),
room_account_data: DashMap::new().into(),
stripped_room_info: DashMap::new().into(),
stripped_room_state: DashMap::new().into(),
stripped_members: DashMap::new().into(),
presence: DashMap::new().into(),
room_user_receipts: DashMap::new().into(),
room_event_receipts: DashMap::new().into(),
media: Arc::new(Mutex::new(LruCache::new(100))),
}
}
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> {
self.filters.insert(filter_name.to_string(), filter_id.to_string());
Ok(())
}
async fn get_filter(&self, filter_name: &str) -> Result<Option<String>> {
Ok(self.filters.get(filter_name).map(|f| f.to_string()))
}
async fn get_sync_token(&self) -> Result<Option<String>> {
Ok(self.sync_token.read().unwrap().clone())
}
async fn save_changes(&self, changes: &StateChanges) -> Result<()> {
let now = Instant::now();
if let Some(s) = &changes.sync_token {
*self.sync_token.write().unwrap() = Some(s.to_owned());
}
for (room, events) in &changes.members {
for event in events.values() {
match event.content.membership {
MembershipState::Join => {
self.joined_user_ids
.entry(room.clone())
.or_insert_with(DashSet::new)
.insert(event.state_key.clone());
self.invited_user_ids
.entry(room.clone())
.or_insert_with(DashSet::new)
.remove(&event.state_key);
}
MembershipState::Invite => {
self.invited_user_ids
.entry(room.clone())
.or_insert_with(DashSet::new)
.insert(event.state_key.clone());
self.joined_user_ids
.entry(room.clone())
.or_insert_with(DashSet::new)
.remove(&event.state_key);
}
_ => {
self.joined_user_ids
.entry(room.clone())
.or_insert_with(DashSet::new)
.remove(&event.state_key);
self.invited_user_ids
.entry(room.clone())
.or_insert_with(DashSet::new)
.remove(&event.state_key);
}
}
self.members
.entry(room.clone())
.or_insert_with(DashMap::new)
.insert(event.state_key.clone(), event.clone());
}
}
for (room, users) in &changes.profiles {
for (user_id, profile) in users {
self.profiles
.entry(room.clone())
.or_insert_with(DashMap::new)
.insert(user_id.clone(), profile.clone());
}
}
for (room, map) in &changes.ambiguity_maps {
for (display_name, display_names) in map {
self.display_names
.entry(room.clone())
.or_insert_with(DashMap::new)
.insert(display_name.clone(), display_names.clone());
}
}
for (event_type, event) in &changes.account_data {
self.account_data.insert(event_type.to_string(), event.clone());
}
for (room, events) in &changes.room_account_data {
for (event_type, event) in events {
self.room_account_data
.entry(room.clone())
.or_insert_with(DashMap::new)
.insert(event_type.to_string(), event.clone());
}
}
for (room, event_types) in &changes.state {
for (event_type, events) in event_types {
for (state_key, event) in events {
self.room_state
.entry(room.clone())
.or_insert_with(DashMap::new)
.entry(event_type.to_owned())
.or_insert_with(DashMap::new)
.insert(state_key.to_owned(), event.clone());
}
}
}
for (room_id, room_info) in &changes.room_infos {
self.room_info.insert(room_id.clone(), room_info.clone());
}
for (sender, event) in &changes.presence {
self.presence.insert(sender.clone(), event.clone());
}
for (room_id, info) in &changes.invited_room_info {
self.stripped_room_info.insert(room_id.clone(), info.clone());
}
for (room, events) in &changes.stripped_members {
for event in events.values() {
self.stripped_members
.entry(room.clone())
.or_insert_with(DashMap::new)
.insert(event.state_key.clone(), event.clone());
}
}
for (room, event_types) in &changes.stripped_state {
for (event_type, events) in event_types {
for (state_key, event) in events {
self.stripped_room_state
.entry(room.clone())
.or_insert_with(DashMap::new)
.entry(event_type.to_owned())
.or_insert_with(DashMap::new)
.insert(state_key.to_owned(), event.clone());
}
}
}
for (room, content) in &changes.receipts {
for (event_id, receipts) in &content.0 {
for (receipt_type, receipts) in receipts {
for (user_id, receipt) in receipts {
// Add the receipt to the room user receipts
if let Some((old_event, _)) = self
.room_user_receipts
.entry(room.clone())
.or_insert_with(DashMap::new)
.entry(receipt_type.to_string())
.or_insert_with(DashMap::new)
.insert(user_id.clone(), (event_id.clone(), receipt.clone()))
{
// Remove the old receipt from the room event receipts
if let Some(receipt_map) = self.room_event_receipts.get(room) {
if let Some(event_map) = receipt_map.get(receipt_type.as_ref()) {
if let Some(user_map) = event_map.get_mut(&old_event) {
user_map.remove(user_id);
}
}
}
}
// Add the receipt to the room event receipts
self.room_event_receipts
.entry(room.clone())
.or_insert_with(DashMap::new)
.entry(receipt_type.to_string())
.or_insert_with(DashMap::new)
.entry(event_id.clone())
.or_insert_with(DashMap::new)
.insert(user_id.clone(), receipt.clone());
}
}
}
}
info!("Saved changes in {:?}", now.elapsed());
Ok(())
}
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>> {
#[allow(clippy::map_clone)]
Ok(self.presence.get(user_id).map(|p| p.clone()))
}
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>> {
#[allow(clippy::map_clone)]
Ok(self.room_state.get(room_id).and_then(|e| {
e.get(event_type.as_ref()).and_then(|s| s.get(state_key).map(|e| e.clone()))
}))
}
async fn get_profile(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Result<Option<MemberEventContent>> {
#[allow(clippy::map_clone)]
Ok(self.profiles.get(room_id).and_then(|p| p.get(user_id).map(|p| p.clone())))
}
async fn get_member_event(
&self,
room_id: &RoomId,
state_key: &UserId,
) -> Result<Option<MemberEvent>> {
#[allow(clippy::map_clone)]
Ok(self.members.get(room_id).and_then(|m| m.get(state_key).map(|m| m.clone())))
}
fn get_user_ids(&self, room_id: &RoomId) -> Vec<UserId> {
#[allow(clippy::map_clone)]
self.members
.get(room_id)
.map(|u| u.iter().map(|u| u.key().clone()).collect())
.unwrap_or_default()
}
fn get_invited_user_ids(&self, room_id: &RoomId) -> Vec<UserId> {
#[allow(clippy::map_clone)]
self.invited_user_ids
.get(room_id)
.map(|u| u.iter().map(|u| u.clone()).collect())
.unwrap_or_default()
}
fn get_joined_user_ids(&self, room_id: &RoomId) -> Vec<UserId> {
#[allow(clippy::map_clone)]
self.joined_user_ids
.get(room_id)
.map(|u| u.iter().map(|u| u.clone()).collect())
.unwrap_or_default()
}
fn get_room_infos(&self) -> Vec<RoomInfo> {
#[allow(clippy::map_clone)]
self.room_info.iter().map(|r| r.clone()).collect()
}
fn get_stripped_room_infos(&self) -> Vec<RoomInfo> {
#[allow(clippy::map_clone)]
self.stripped_room_info.iter().map(|r| r.clone()).collect()
}
async fn get_account_data_event(
&self,
event_type: EventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
Ok(self.account_data.get(event_type.as_ref()).map(|e| e.clone()))
}
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
Ok(self
.room_account_data
.get(room_id)
.and_then(|m| m.get(event_type.as_ref()).map(|e| e.clone())))
}
async fn get_user_room_receipt_event(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
user_id: &UserId,
) -> Result<Option<(EventId, Receipt)>> {
Ok(self.room_user_receipts.get(room_id).and_then(|m| {
m.get(receipt_type.as_ref()).and_then(|m| m.get(user_id).map(|r| r.clone()))
}))
}
async fn get_event_room_receipt_events(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
event_id: &EventId,
) -> Result<Vec<(UserId, Receipt)>> {
Ok(self
.room_event_receipts
.get(room_id)
.and_then(|m| {
m.get(receipt_type.as_ref()).and_then(|m| {
m.get(event_id)
.map(|m| m.iter().map(|r| (r.key().clone(), r.value().clone())).collect())
})
})
.unwrap_or_else(Vec::new))
}
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
self.media.lock().await.put(request.unique_key(), data);
Ok(())
}
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
Ok(self.media.lock().await.get(&request.unique_key()).cloned())
}
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
self.media.lock().await.pop(&request.unique_key());
Ok(())
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
let mut media_store = self.media.lock().await;
let keys: Vec<String> = media_store
.iter()
.filter_map(
|(key, _)| if key.starts_with(&uri.to_string()) { Some(key.clone()) } else { None },
)
.collect();
for key in keys {
media_store.pop(&key);
}
Ok(())
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl StateStore for MemoryStore {
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()> {
self.save_filter(filter_name, filter_id).await
}
async fn save_changes(&self, changes: &StateChanges) -> Result<()> {
self.save_changes(changes).await
}
async fn get_filter(&self, filter_id: &str) -> Result<Option<String>> {
self.get_filter(filter_id).await
}
async fn get_sync_token(&self) -> Result<Option<String>> {
self.get_sync_token().await
}
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>> {
self.get_presence_event(user_id).await
}
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>> {
self.get_state_event(room_id, event_type, state_key).await
}
async fn get_profile(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Result<Option<MemberEventContent>> {
self.get_profile(room_id, user_id).await
}
async fn get_member_event(
&self,
room_id: &RoomId,
state_key: &UserId,
) -> Result<Option<MemberEvent>> {
self.get_member_event(room_id, state_key).await
}
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>> {
Ok(self.get_user_ids(room_id))
}
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>> {
Ok(self.get_invited_user_ids(room_id))
}
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>> {
Ok(self.get_joined_user_ids(room_id))
}
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>> {
Ok(self.get_room_infos())
}
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>> {
Ok(self.get_stripped_room_infos())
}
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
) -> Result<BTreeSet<UserId>> {
#[allow(clippy::map_clone)]
Ok(self
.display_names
.get(room_id)
.and_then(|d| d.get(display_name).map(|d| d.clone()))
.unwrap_or_default())
}
async fn get_account_data_event(
&self,
event_type: EventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>> {
self.get_account_data_event(event_type).await
}
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>> {
self.get_room_account_data_event(room_id, event_type).await
}
async fn get_user_room_receipt_event(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
user_id: &UserId,
) -> Result<Option<(EventId, Receipt)>> {
self.get_user_room_receipt_event(room_id, receipt_type, user_id).await
}
async fn get_event_room_receipt_events(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
event_id: &EventId,
) -> Result<Vec<(UserId, Receipt)>> {
self.get_event_room_receipt_events(room_id, receipt_type, event_id).await
}
async fn add_media_content(&self, request: &MediaRequest, data: Vec<u8>) -> Result<()> {
self.add_media_content(request, data).await
}
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>> {
self.get_media_content(request).await
}
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()> {
self.remove_media_content(request).await
}
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()> {
self.remove_media_content_for_uri(uri).await
}
}
#[cfg(test)]
#[cfg(not(feature = "sled_state_store"))]
mod test {
use matrix_sdk_test::async_test;
use ruma::{
api::client::r0::media::get_content_thumbnail::Method,
identifiers::{event_id, mxc_uri, room_id, user_id, UserId},
receipt::ReceiptType,
uint,
};
use serde_json::json;
use super::{MemoryStore, StateChanges};
use crate::media::{MediaFormat, MediaRequest, MediaThumbnailSize, MediaType};
fn user_id() -> UserId {
user_id!("@example:localhost")
}
#[async_test]
async fn test_receipts_saving() {
let store = MemoryStore::new();
let room_id = room_id!("!test:localhost");
let first_event_id = event_id!("$1435641916114394fHBLK:matrix.org");
let second_event_id = event_id!("$fHBLK1435641916114394:matrix.org");
let first_receipt_event = serde_json::from_value(json!({
first_event_id.clone(): {
"m.read": {
user_id(): {
"ts": 1436451550453u64
}
}
}
}))
.unwrap();
let second_receipt_event = serde_json::from_value(json!({
second_event_id.clone(): {
"m.read": {
user_id(): {
"ts": 1436451551453u64
}
}
}
}))
.unwrap();
assert!(store
.get_user_room_receipt_event(&room_id, ReceiptType::Read, &user_id())
.await
.unwrap()
.is_none());
assert!(store
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &first_event_id)
.await
.unwrap()
.is_empty());
assert!(store
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &second_event_id)
.await
.unwrap()
.is_empty());
let mut changes = StateChanges::default();
changes.add_receipts(&room_id, first_receipt_event);
store.save_changes(&changes).await.unwrap();
assert!(store
.get_user_room_receipt_event(&room_id, ReceiptType::Read, &user_id())
.await
.unwrap()
.is_some(),);
assert_eq!(
store
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &first_event_id)
.await
.unwrap()
.len(),
1
);
assert!(store
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &second_event_id)
.await
.unwrap()
.is_empty());
let mut changes = StateChanges::default();
changes.add_receipts(&room_id, second_receipt_event);
store.save_changes(&changes).await.unwrap();
assert!(store
.get_user_room_receipt_event(&room_id, ReceiptType::Read, &user_id())
.await
.unwrap()
.is_some());
assert!(store
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &first_event_id)
.await
.unwrap()
.is_empty());
assert_eq!(
store
.get_event_room_receipt_events(&room_id, ReceiptType::Read, &second_event_id)
.await
.unwrap()
.len(),
1
);
}
#[async_test]
async fn test_media_content() {
let store = MemoryStore::new();
let uri = mxc_uri!("mxc://localhost/media");
let content: Vec<u8> = "somebinarydata".into();
let request_file =
MediaRequest { media_type: MediaType::Uri(uri.clone()), format: MediaFormat::File };
let request_thumbnail = MediaRequest {
media_type: MediaType::Uri(uri.clone()),
format: MediaFormat::Thumbnail(MediaThumbnailSize {
method: Method::Crop,
width: uint!(100),
height: uint!(100),
}),
};
assert!(store.get_media_content(&request_file).await.unwrap().is_none());
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_none());
store.add_media_content(&request_file, content.clone()).await.unwrap();
assert!(store.get_media_content(&request_file).await.unwrap().is_some());
store.remove_media_content(&request_file).await.unwrap();
assert!(store.get_media_content(&request_file).await.unwrap().is_none());
store.add_media_content(&request_file, content.clone()).await.unwrap();
assert!(store.get_media_content(&request_file).await.unwrap().is_some());
store.add_media_content(&request_thumbnail, content.clone()).await.unwrap();
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_some());
store.remove_media_content_for_uri(&uri).await.unwrap();
assert!(store.get_media_content(&request_file).await.unwrap().is_none());
assert!(store.get_media_content(&request_thumbnail).await.unwrap().is_none());
}
}
+540
View File
@@ -0,0 +1,540 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#[cfg(feature = "sled_state_store")]
use std::path::Path;
use std::{
collections::{BTreeMap, BTreeSet},
ops::Deref,
sync::Arc,
};
use dashmap::DashMap;
use matrix_sdk_common::{async_trait, locks::RwLock, AsyncTraitDeps};
use ruma::{
api::client::r0::push::get_notifications::Notification,
events::{
presence::PresenceEvent,
receipt::{Receipt, ReceiptEventContent},
room::member::MemberEventContent,
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
AnySyncStateEvent, EventContent, EventType,
},
receipt::ReceiptType,
serde::Raw,
EventId, MxcUri, RoomId, UserId,
};
#[cfg(feature = "sled_state_store")]
use sled::Db;
use crate::{
deserialized_responses::{MemberEvent, StrippedMemberEvent},
media::MediaRequest,
rooms::{RoomInfo, RoomType},
Room, Session,
};
pub(crate) mod ambiguity_map;
mod memory_store;
#[cfg(feature = "sled_state_store")]
mod sled_store;
#[cfg(not(feature = "sled_state_store"))]
use self::memory_store::MemoryStore;
#[cfg(feature = "sled_state_store")]
use self::sled_store::SledStore;
/// State store specific error type.
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
/// An error happened in the underlying sled database.
#[cfg(feature = "sled_state_store")]
#[error(transparent)]
Sled(#[from] sled::Error),
/// An error happened while serializing or deserializing some data.
#[error(transparent)]
Json(#[from] serde_json::Error),
/// An error happened while deserializing a Matrix identifier, e.g. an user
/// id.
#[error(transparent)]
Identifier(#[from] ruma::identifiers::Error),
/// The store is locked with a passphrase and an incorrect passphrase was
/// given.
#[error("The store failed to be unlocked")]
StoreLocked,
/// An unencrypted store was tried to be unlocked with a passphrase.
#[error("The store is not encrypted but was tried to be opened with a passphrase")]
UnencryptedStore,
/// The store failed to encrypt or decrypt some data.
#[error("Error encrypting or decrypting data from the store: {0}")]
Encryption(String),
}
/// A `StateStore` specific result type.
pub type Result<T, E = StoreError> = std::result::Result<T, E>;
/// An abstract state store trait that can be used to implement different stores
/// for the SDK.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait StateStore: AsyncTraitDeps {
/// Save the given filter id under the given name.
///
/// # Arguments
///
/// * `filter_name` - The name that should be used to store the filter id.
///
/// * `filter_id` - The filter id that should be stored in the state store.
async fn save_filter(&self, filter_name: &str, filter_id: &str) -> Result<()>;
/// Save the set of state changes in the store.
async fn save_changes(&self, changes: &StateChanges) -> Result<()>;
/// Get the filter id that was stored under the given filter name.
///
/// # Arguments
///
/// * `filter_name` - The name that was used to store the filter id.
async fn get_filter(&self, filter_name: &str) -> Result<Option<String>>;
/// Get the last stored sync token.
async fn get_sync_token(&self) -> Result<Option<String>>;
/// Get the stored presence event for the given user.
///
/// # Arguments
///
/// * `user_id` - The id of the user for which we wish to fetch the presence
/// event for.
async fn get_presence_event(&self, user_id: &UserId) -> Result<Option<Raw<PresenceEvent>>>;
/// Get a state event out of the state store.
///
/// # Arguments
///
/// * `room_id` - The id of the room the state event was received for.
///
/// * `event_type` - The event type of the state event.
async fn get_state_event(
&self,
room_id: &RoomId,
event_type: EventType,
state_key: &str,
) -> Result<Option<Raw<AnySyncStateEvent>>>;
/// Get the current profile for the given user in the given room.
///
/// # Arguments
///
/// * `room_id` - The room id the profile is used in.
///
/// * `user_id` - The id of the user the profile belongs to.
async fn get_profile(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Result<Option<MemberEventContent>>;
/// Get a raw `MemberEvent` for the given state key in the given room id.
///
/// # Arguments
///
/// * `room_id` - The room id the member event belongs to.
///
/// * `state_key` - The user id that the member event defines the state for.
async fn get_member_event(
&self,
room_id: &RoomId,
state_key: &UserId,
) -> Result<Option<MemberEvent>>;
/// Get all the user ids of members for a given room.
async fn get_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>>;
/// Get all the user ids of members that are in the invited state for a
/// given room.
async fn get_invited_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>>;
/// Get all the user ids of members that are in the joined state for a
/// given room.
async fn get_joined_user_ids(&self, room_id: &RoomId) -> Result<Vec<UserId>>;
/// Get all the pure `RoomInfo`s the store knows about.
async fn get_room_infos(&self) -> Result<Vec<RoomInfo>>;
/// Get all the pure `RoomInfo`s the store knows about.
async fn get_stripped_room_infos(&self) -> Result<Vec<RoomInfo>>;
/// Get all the users that use the given display name in the given room.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the display name users should
/// be fetched for.
///
/// * `display_name` - The display name that the users use.
async fn get_users_with_display_name(
&self,
room_id: &RoomId,
display_name: &str,
) -> Result<BTreeSet<UserId>>;
/// Get an event out of the account data store.
///
/// # Arguments
///
/// * `event_type` - The event type of the account data event.
async fn get_account_data_event(
&self,
event_type: EventType,
) -> Result<Option<Raw<AnyGlobalAccountDataEvent>>>;
/// Get an event out of the room account data store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the room account data event
/// should
/// be fetched.
///
/// * `event_type` - The event type of the room account data event.
async fn get_room_account_data_event(
&self,
room_id: &RoomId,
event_type: EventType,
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>>;
/// Get an event out of the user room receipt store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the receipt should be
/// fetched.
///
/// * `receipt_type` - The type of the receipt.
///
/// * `user_id` - The id of the user for who the receipt should be fetched.
async fn get_user_room_receipt_event(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
user_id: &UserId,
) -> Result<Option<(EventId, Receipt)>>;
/// Get events out of the event room receipt store.
///
/// # Arguments
///
/// * `room_id` - The id of the room for which the receipts should be
/// fetched.
///
/// * `receipt_type` - The type of the receipts.
///
/// * `event_id` - The id of the event for which the receipts should be
/// fetched.
async fn get_event_room_receipt_events(
&self,
room_id: &RoomId,
receipt_type: ReceiptType,
event_id: &EventId,
) -> Result<Vec<(UserId, Receipt)>>;
/// Add a media file's content in the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
///
/// * `content` - The content of the file.
async fn add_media_content(&self, request: &MediaRequest, content: Vec<u8>) -> Result<()>;
/// Get a media file's content out of the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn get_media_content(&self, request: &MediaRequest) -> Result<Option<Vec<u8>>>;
/// Removes a media file's content from the media store.
///
/// # Arguments
///
/// * `request` - The `MediaRequest` of the file.
async fn remove_media_content(&self, request: &MediaRequest) -> Result<()>;
/// Removes all the media files' content associated to an `MxcUri` from the
/// media store.
///
/// # Arguments
///
/// * `uri` - The `MxcUri` of the media files.
async fn remove_media_content_for_uri(&self, uri: &MxcUri) -> Result<()>;
}
/// A state store wrapper for the SDK.
///
/// This adds additional higher level store functionality on top of a
/// `StateStore` implementation.
#[derive(Debug, Clone)]
pub struct Store {
inner: Arc<dyn StateStore>,
pub(crate) session: Arc<RwLock<Option<Session>>>,
pub(crate) sync_token: Arc<RwLock<Option<String>>>,
rooms: Arc<DashMap<RoomId, Room>>,
stripped_rooms: Arc<DashMap<RoomId, Room>>,
}
impl Store {
fn new(inner: Box<dyn StateStore>) -> Self {
let session = Arc::new(RwLock::new(None));
let sync_token = Arc::new(RwLock::new(None));
Self {
inner: inner.into(),
session,
sync_token,
rooms: DashMap::new().into(),
stripped_rooms: DashMap::new().into(),
}
}
pub(crate) async fn restore_session(&self, session: Session) -> Result<()> {
for info in self.inner.get_room_infos().await? {
let room = Room::restore(&session.user_id, self.inner.clone(), info);
self.rooms.insert(room.room_id().to_owned(), room);
}
for info in self.inner.get_stripped_room_infos().await? {
let room = Room::restore(&session.user_id, self.inner.clone(), info);
self.stripped_rooms.insert(room.room_id().to_owned(), room);
}
let token = self.get_sync_token().await?;
*self.sync_token.write().await = token;
*self.session.write().await = Some(session);
Ok(())
}
#[cfg(not(feature = "sled_state_store"))]
pub(crate) fn open_memory_store() -> Self {
let inner = Box::new(MemoryStore::new());
Self::new(inner)
}
/// Open the default Sled store.
///
/// # Arguments
///
/// * `path` - The path where the store should reside in.
///
/// * `passphrase` - A passphrase that should be used to encrypt the state
/// store.
#[cfg(feature = "sled_state_store")]
pub fn open_default(path: impl AsRef<Path>, passphrase: Option<&str>) -> Result<(Self, Db)> {
let inner = if let Some(passphrase) = passphrase {
SledStore::open_with_passphrase(path, passphrase)?
} else {
SledStore::open_with_path(path)?
};
Ok((Self::new(Box::new(inner.clone())), inner.inner))
}
#[cfg(feature = "sled_state_store")]
pub(crate) fn open_temporary() -> Result<(Self, Db)> {
let inner = SledStore::open()?;
Ok((Self::new(Box::new(inner.clone())), inner.inner))
}
/// Get all the rooms this store knows about.
pub fn get_rooms(&self) -> Vec<Room> {
self.rooms.iter().filter_map(|r| self.get_room(r.key())).collect()
}
/// Get the room with the given room id.
pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
self.rooms
.get(room_id)
.and_then(|r| match r.room_type() {
RoomType::Joined => Some(r.clone()),
RoomType::Left => Some(r.clone()),
RoomType::Invited => self.get_stripped_room(room_id),
})
.or_else(|| self.get_stripped_room(room_id))
}
fn get_stripped_room(&self, room_id: &RoomId) -> Option<Room> {
self.stripped_rooms.get(room_id).map(|r| r.clone())
}
pub(crate) async fn get_or_create_stripped_room(&self, room_id: &RoomId) -> Room {
let session = self.session.read().await;
let user_id = &session.as_ref().expect("Creating room while not being logged in").user_id;
self.stripped_rooms
.entry(room_id.clone())
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, RoomType::Invited))
.clone()
}
pub(crate) async fn get_or_create_room(&self, room_id: &RoomId, room_type: RoomType) -> Room {
let session = self.session.read().await;
let user_id = &session.as_ref().expect("Creating room while not being logged in").user_id;
self.rooms
.entry(room_id.clone())
.or_insert_with(|| Room::new(user_id, self.inner.clone(), room_id, room_type))
.clone()
}
}
impl Deref for Store {
type Target = dyn StateStore;
fn deref(&self) -> &Self::Target {
&*self.inner
}
}
/// Store state changes and pass them to the StateStore.
#[derive(Debug, Default)]
pub struct StateChanges {
/// The sync token that relates to this update.
pub sync_token: Option<String>,
/// A user session, containing an access token and information about the
/// associated user account.
pub session: Option<Session>,
/// A mapping of event type string to `AnyBasicEvent`.
pub account_data: BTreeMap<String, Raw<AnyGlobalAccountDataEvent>>,
/// A mapping of `UserId` to `PresenceEvent`.
pub presence: BTreeMap<UserId, Raw<PresenceEvent>>,
/// A mapping of `RoomId` to a map of users and their `MemberEvent`.
pub members: BTreeMap<RoomId, BTreeMap<UserId, MemberEvent>>,
/// A mapping of `RoomId` to a map of users and their `MemberEventContent`.
pub profiles: BTreeMap<RoomId, BTreeMap<UserId, MemberEventContent>>,
/// A mapping of `RoomId` to a map of event type string to a state key and
/// `AnySyncStateEvent`.
pub state: BTreeMap<RoomId, BTreeMap<String, BTreeMap<String, Raw<AnySyncStateEvent>>>>,
/// A mapping of `RoomId` to a map of event type string to `AnyBasicEvent`.
pub room_account_data: BTreeMap<RoomId, BTreeMap<String, Raw<AnyRoomAccountDataEvent>>>,
/// A map of `RoomId` to `RoomInfo`.
pub room_infos: BTreeMap<RoomId, RoomInfo>,
/// A map of `RoomId` to `ReceiptEventContent`.
pub receipts: BTreeMap<RoomId, ReceiptEventContent>,
/// A mapping of `RoomId` to a map of event type to a map of state key to
/// `AnyStrippedStateEvent`.
pub stripped_state:
BTreeMap<RoomId, BTreeMap<String, BTreeMap<String, Raw<AnyStrippedStateEvent>>>>,
/// A mapping of `RoomId` to a map of users and their `StrippedMemberEvent`.
pub stripped_members: BTreeMap<RoomId, BTreeMap<UserId, StrippedMemberEvent>>,
/// A map of `RoomId` to `RoomInfo`.
pub invited_room_info: BTreeMap<RoomId, RoomInfo>,
/// A map from room id to a map of a display name and a set of user ids that
/// share that display name in the given room.
pub ambiguity_maps: BTreeMap<RoomId, BTreeMap<String, BTreeSet<UserId>>>,
/// A map of `RoomId` to a vector of `Notification`s
pub notifications: BTreeMap<RoomId, Vec<Notification>>,
}
impl StateChanges {
/// Create a new `StateChanges` struct with the given sync_token.
pub fn new(sync_token: String) -> Self {
Self { sync_token: Some(sync_token), ..Default::default() }
}
/// Update the `StateChanges` struct with the given `PresenceEvent`.
pub fn add_presence_event(&mut self, event: PresenceEvent, raw_event: Raw<PresenceEvent>) {
self.presence.insert(event.sender, raw_event);
}
/// Update the `StateChanges` struct with the given `RoomInfo`.
pub fn add_room(&mut self, room: RoomInfo) {
self.room_infos.insert(room.room_id.as_ref().to_owned(), room);
}
/// Update the `StateChanges` struct with the given `RoomInfo`.
pub fn add_stripped_room(&mut self, room: RoomInfo) {
self.room_infos.insert(room.room_id.as_ref().to_owned(), room);
}
/// Update the `StateChanges` struct with the given `AnyBasicEvent`.
pub fn add_account_data(
&mut self,
event: AnyGlobalAccountDataEvent,
raw_event: Raw<AnyGlobalAccountDataEvent>,
) {
self.account_data.insert(event.content().event_type().to_owned(), raw_event);
}
/// Update the `StateChanges` struct with the given room with a new
/// `AnyBasicEvent`.
pub fn add_room_account_data(
&mut self,
room_id: &RoomId,
event: AnyRoomAccountDataEvent,
raw_event: Raw<AnyRoomAccountDataEvent>,
) {
self.room_account_data
.entry(room_id.to_owned())
.or_insert_with(BTreeMap::new)
.insert(event.content().event_type().to_owned(), raw_event);
}
/// Update the `StateChanges` struct with the given room with a new
/// `StrippedMemberEvent`.
pub fn add_stripped_member(&mut self, room_id: &RoomId, event: StrippedMemberEvent) {
let user_id = event.state_key.clone();
self.stripped_members
.entry(room_id.to_owned())
.or_insert_with(BTreeMap::new)
.insert(user_id, event);
}
/// Update the `StateChanges` struct with the given room with a new
/// `AnySyncStateEvent`.
pub fn add_state_event(
&mut self,
room_id: &RoomId,
event: AnySyncStateEvent,
raw_event: Raw<AnySyncStateEvent>,
) {
self.state
.entry(room_id.to_owned())
.or_insert_with(BTreeMap::new)
.entry(event.content().event_type().to_string())
.or_insert_with(BTreeMap::new)
.insert(event.state_key().to_string(), raw_event);
}
/// Update the `StateChanges` struct with the given room with a new
/// `Notification`.
pub fn add_notification(&mut self, room_id: &RoomId, notification: Notification) {
self.notifications.entry(room_id.to_owned()).or_insert_with(Vec::new).push(notification);
}
/// Update the `StateChanges` struct with the given room with a new
/// `Receipts`.
pub fn add_receipts(&mut self, room_id: &RoomId, event: ReceiptEventContent) {
self.receipts.insert(room_id.to_owned(), event);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,286 @@
// Copyright 2020 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::convert::TryFrom;
use chacha20poly1305::{
aead::{Aead, Error as EncryptionError, NewAead},
ChaCha20Poly1305, Key, Nonce, XChaCha20Poly1305, XNonce,
};
use hmac::Hmac;
use pbkdf2::pbkdf2;
use rand::{thread_rng, Error as RngError, Fill};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use zeroize::{Zeroize, Zeroizing};
use crate::StoreError;
const VERSION: u8 = 1;
const KEY_SIZE: usize = 32;
const NONCE_SIZE: usize = 12;
const XNONCE_SIZE: usize = 24;
const KDF_SALT_SIZE: usize = 32;
#[cfg(not(test))]
const KDF_ROUNDS: u32 = 200_000;
#[cfg(test)]
const KDF_ROUNDS: u32 = 1000;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Serialization(#[from] serde_json::Error),
#[error("Error encrypting or decrypting an event {0}")]
Encryption(String),
#[error("Error generating enough random data for a cryptographic operation")]
Random(#[from] RngError),
}
#[allow(clippy::from_over_into)]
impl Into<StoreError> for Error {
fn into(self) -> StoreError {
match self {
Error::Serialization(e) => StoreError::Json(e),
Error::Encryption(e) => StoreError::Encryption(e),
Error::Random(_) => StoreError::Encryption(self.to_string()),
}
}
}
impl From<EncryptionError> for Error {
fn from(e: EncryptionError) -> Self {
Error::Encryption(e.to_string())
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct EncryptedEvent {
version: u8,
ciphertext: Vec<u8>,
nonce: Vec<u8>,
}
/// Version specific info for the key derivation method that is used.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum KdfInfo {
Pbkdf2ToChaCha20Poly1305 {
/// The number of PBKDF rounds that were used when deriving the store
/// key.
rounds: u32,
/// The salt that was used when the passphrase was expanded into a store
/// key.
kdf_salt: Vec<u8>,
},
}
/// Version specific info for encryption method that is used to encrypt our
/// store key.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub enum CipherTextInfo {
ChaCha20Poly1305 {
/// The nonce that was used to encrypt the ciphertext.
nonce: Vec<u8>,
/// The encrypted store key.
ciphertext: Vec<u8>,
},
}
/// An encrypted version of our store key, this can be safely stored in a
/// database.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct EncryptedStoreKey {
/// Info about the key derivation method that was used to expand the
/// passphrase into an encryption key.
pub kdf_info: KdfInfo,
/// The ciphertext with it's accompanying additional data that is needed to
/// decrypt the store key.
pub ciphertext_info: CipherTextInfo,
}
/// A store key that can be used to encrypt entries in the store.
#[derive(Debug, Zeroize, PartialEq)]
pub struct StoreKey {
inner: Vec<u8>,
}
impl TryFrom<Vec<u8>> for StoreKey {
type Error = ();
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.len() != KEY_SIZE {
Err(())
} else {
Ok(Self { inner: value })
}
}
}
impl StoreKey {
/// Generate a new random store key.
pub fn new() -> Result<Self, Error> {
let mut key = vec![0u8; KEY_SIZE];
let mut rng = thread_rng();
key.try_fill(&mut rng)?;
Ok(Self { inner: key })
}
/// Expand the given passphrase into a KEY_SIZE long key.
fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Zeroizing<Vec<u8>> {
let mut key = Zeroizing::from(vec![0u8; KEY_SIZE]);
pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), salt, rounds, &mut *key);
key
}
/// Get the store key.
fn key(&self) -> &Key {
Key::from_slice(&self.inner)
}
/// Encrypt and export our store key using the given passphrase.
///
/// # Arguments
///
/// * `passphrase` - The passphrase that should be used to encrypt the
/// store key.
pub fn export(&self, passphrase: &str) -> Result<EncryptedStoreKey, Error> {
let mut rng = thread_rng();
let mut salt = vec![0u8; KDF_SALT_SIZE];
salt.try_fill(&mut rng)?;
let key = StoreKey::expand_key(passphrase, &salt, KDF_ROUNDS);
let key = Key::from_slice(key.as_ref());
let cipher = ChaCha20Poly1305::new(key);
let mut nonce = vec![0u8; NONCE_SIZE];
nonce.try_fill(&mut rng)?;
let ciphertext =
cipher.encrypt(Nonce::from_slice(nonce.as_ref()), self.inner.as_slice())?;
Ok(EncryptedStoreKey {
kdf_info: KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds: KDF_ROUNDS, kdf_salt: salt },
ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext },
})
}
fn get_nonce() -> Result<Vec<u8>, RngError> {
let mut nonce = vec![0u8; XNONCE_SIZE];
let mut rng = thread_rng();
nonce.try_fill(&mut rng)?;
Ok(nonce)
}
pub fn encrypt(&self, event: &impl Serialize) -> Result<EncryptedEvent, Error> {
let event = serde_json::to_vec(event)?;
let nonce = StoreKey::get_nonce()?;
let cipher = XChaCha20Poly1305::new(self.key());
let xnonce = XNonce::from_slice(&nonce);
let ciphertext = cipher.encrypt(xnonce, event.as_ref())?;
Ok(EncryptedEvent { version: VERSION, ciphertext, nonce })
}
pub fn decrypt<T: for<'b> Deserialize<'b>>(&self, event: EncryptedEvent) -> Result<T, Error> {
if event.version != VERSION {
return Err(Error::Encryption(
"Error decrypting: Unknown ciphertext version".to_string(),
));
}
let cipher = XChaCha20Poly1305::new(self.key());
let nonce = XNonce::from_slice(&event.nonce);
let plaintext = cipher.decrypt(nonce, event.ciphertext.as_ref())?;
Ok(serde_json::from_slice(&plaintext)?)
}
/// Restore a store key from an encrypted export.
///
/// # Arguments
///
/// * `passphrase` - The passphrase that should be used to encrypt the
/// store key.
///
/// * `encrypted` - The exported and encrypted version of the store key.
pub fn import(passphrase: &str, encrypted: EncryptedStoreKey) -> Result<Self, EncryptionError> {
let key = match encrypted.kdf_info {
KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, kdf_salt } => {
Self::expand_key(passphrase, &kdf_salt, rounds)
}
};
let key = Key::from_slice(key.as_ref());
let decrypted = match encrypted.ciphertext_info {
CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => {
let cipher = ChaCha20Poly1305::new(key);
let nonce = Nonce::from_slice(&nonce);
cipher.decrypt(nonce, ciphertext.as_ref())?
}
};
Ok(Self { inner: decrypted })
}
}
#[cfg(test)]
mod test {
use serde_json::{json, Value};
use super::StoreKey;
#[test]
fn generating() {
StoreKey::new().unwrap();
}
#[test]
fn encrypting() {
let passphrase = "it's a secret to everybody";
let store_key = StoreKey::new().unwrap();
let encrypted = store_key.export(passphrase).unwrap();
let decrypted = StoreKey::import(passphrase, encrypted).unwrap();
assert_eq!(store_key, decrypted);
}
#[test]
fn encrypting_events() {
let event = json!({
"content": {
"body": "Bee Gees - Stayin' Alive",
"info": {
"duration": 2140786,
"mimetype": "audio/mpeg",
"size": 1563685
},
"msgtype": "m.audio",
"url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
},
});
let store_key = StoreKey::new().unwrap();
let encrypted = store_key.encrypt(&event).unwrap();
let decrypted: Value = store_key.decrypt(encrypted).unwrap();
assert_eq!(event, decrypted);
}
}
+10 -13
View File
@@ -8,27 +8,24 @@ license = "Apache-2.0"
name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.2.0"
[features]
unstable-synapse-quirks = ["ruma/unstable-synapse-quirks"]
version = "0.3.0"
[dependencies]
async-trait = "0.1.50"
instant = { version = "0.1.9", features = ["wasm-bindgen", "now"] }
async-trait = "0.1.42"
[dependencies.ruma]
version = "0.0.2"
features = ["client-api", "unstable-pre-spec"]
ruma = { version = "0.2.0", features = ["client-api-c"] }
serde = "1.0.126"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
uuid = { version = "0.8.1", default-features = false, features = ["v4", "serde"] }
uuid = { version = "0.8.2", default-features = false, features = ["v4", "serde"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.24"
version = "1.7.1"
default-features = false
features = ["sync"]
features = ["rt", "sync"]
[target.'cfg(target_arch = "wasm32")'.dependencies]
futures = "0.3.15"
futures-locks = { version = "0.6.0", default-features = false }
uuid = { version = "0.8.1", default-features = false, features = ["v4", "wasm-bindgen"] }
wasm-bindgen-futures = "0.4.24"
uuid = { version = "0.8.2", default-features = false, features = ["v4", "wasm-bindgen"] }
@@ -0,0 +1,341 @@
use std::{collections::BTreeMap, convert::TryFrom};
use ruma::{
api::client::r0::{
push::get_notifications::Notification,
sync::sync_events::{
DeviceLists, Ephemeral, GlobalAccountData, InvitedRoom, Presence, RoomAccountData,
State, ToDevice, UnreadNotificationsCount as RumaUnreadNotificationsCount,
},
},
events::{
room::member::MemberEventContent, AnySyncRoomEvent, StateEvent, StrippedStateEvent,
SyncStateEvent, Unsigned,
},
identifiers::{DeviceKeyAlgorithm, EventId, RoomId, UserId},
serde::Raw,
DeviceIdBox, MilliSecondsSinceUnixEpoch,
};
use serde::{Deserialize, Serialize};
/// A change in ambiguity of room members that an `m.room.member` event
/// triggers.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AmbiguityChange {
/// Is the member that is contained in the state key of the `m.room.member`
/// event itself ambiguous because of the event.
pub member_ambiguous: bool,
/// Has another user been disambiguated because of this event.
pub disambiguated_member: Option<UserId>,
/// Has another user become ambiguous because of this event.
pub ambiguated_member: Option<UserId>,
}
/// Collection of ambiguioty changes that room member events trigger.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct AmbiguityChanges {
/// A map from room id to a map of an event id to the `AmbiguityChange` that
/// the event with the given id caused.
pub changes: BTreeMap<RoomId, BTreeMap<EventId, AmbiguityChange>>,
}
/// The verification state of the device that sent an event to us.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum VerificationState {
/// The device is trusted.
Trusted,
/// The device is not trusted.
Untrusted,
/// The device is not known to us.
UnknownDevice,
}
/// The algorithm specific information of a decrypted event.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum AlgorithmInfo {
/// The info if the event was encrypted using m.megolm.v1.aes-sha2
MegolmV1AesSha2 {
/// The curve25519 key of the device that created the megolm decryption
/// key originally.
curve25519_key: String,
/// The signing keys that have created the megolm key that was used to
/// decrypt this session. This map will usually contain a single ed25519
/// key.
sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String>,
/// Chain of curve25519 keys through which this session was forwarded,
/// via m.forwarded_room_key events.
forwarding_curve25519_key_chain: Vec<String>,
},
}
/// Struct containing information on how an event was decrypted.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct EncryptionInfo {
/// The user ID of the event sender, note this is untrusted data unless the
/// `verification_state` is as well trusted.
pub sender: UserId,
/// The device ID of the device that sent us the event, note this is
/// untrusted data unless `verification_state` is as well trusted.
pub sender_device: DeviceIdBox,
/// Information about the algorithm that was used to encrypt the event.
pub algorithm_info: AlgorithmInfo,
/// The verification state of the device that sent us the event, note this
/// is the state of the device at the time of decryption. It may change in
/// the future if a device gets verified or deleted.
pub verification_state: VerificationState,
}
/// A customized version of a room event coming from a sync that holds optional
/// encryption info.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SyncRoomEvent {
/// The actual event.
pub event: Raw<AnySyncRoomEvent>,
/// The encryption info about the event. Will be `None` if the event was not
/// encrypted.
pub encryption_info: Option<EncryptionInfo>,
}
impl From<Raw<AnySyncRoomEvent>> for SyncRoomEvent {
fn from(inner: Raw<AnySyncRoomEvent>) -> Self {
Self { encryption_info: None, event: inner }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct SyncResponse {
/// The batch token to supply in the `since` param of the next `/sync`
/// request.
pub next_batch: String,
/// Updates to rooms.
pub rooms: Rooms,
/// Updates to the presence status of other users.
pub presence: Presence,
/// The global private data created by this user.
pub account_data: GlobalAccountData,
/// Messages sent directly between devices.
pub to_device: ToDevice,
/// Information on E2E device updates.
///
/// Only present on an incremental sync.
pub device_lists: DeviceLists,
/// For each key algorithm, the number of unclaimed one-time keys
/// currently held on the server for a device.
pub device_one_time_keys_count: BTreeMap<DeviceKeyAlgorithm, u64>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
/// New notifications per room.
pub notifications: BTreeMap<RoomId, Vec<Notification>>,
}
impl SyncResponse {
pub fn new(next_batch: String) -> Self {
Self { next_batch, ..Default::default() }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Rooms {
/// The rooms that the user has left or been banned from.
pub leave: BTreeMap<RoomId, LeftRoom>,
/// The rooms that the user has joined.
pub join: BTreeMap<RoomId, JoinedRoom>,
/// The rooms that the user has been invited to.
pub invite: BTreeMap<RoomId, InvitedRoom>,
}
/// Updates to joined rooms.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct JoinedRoom {
/// Counts of unread notifications for this room.
pub unread_notifications: UnreadNotificationsCount,
/// The timeline of messages and state changes in the room.
pub timeline: Timeline,
/// Updates to the state, between the time indicated by the `since`
/// parameter, and the start of the `timeline` (or all state up to the
/// start of the `timeline`, if `since` is not given, or `full_state` is
/// true).
pub state: State,
/// The private data that this user has attached to this room.
pub account_data: RoomAccountData,
/// The ephemeral events in the room that aren't recorded in the timeline or
/// state of the room. e.g. typing.
pub ephemeral: Ephemeral,
}
impl JoinedRoom {
pub fn new(
timeline: Timeline,
state: State,
account_data: RoomAccountData,
ephemeral: Ephemeral,
unread_notifications: UnreadNotificationsCount,
) -> Self {
Self { unread_notifications, timeline, state, account_data, ephemeral }
}
}
/// Counts of unread notifications for a room.
#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize)]
pub struct UnreadNotificationsCount {
/// The number of unread notifications for this room with the highlight flag
/// set.
pub highlight_count: u64,
/// The total number of unread notifications for this room.
pub notification_count: u64,
}
impl From<RumaUnreadNotificationsCount> for UnreadNotificationsCount {
fn from(notifications: RumaUnreadNotificationsCount) -> Self {
Self {
highlight_count: notifications.highlight_count.map(|c| c.into()).unwrap_or(0),
notification_count: notifications.notification_count.map(|c| c.into()).unwrap_or(0),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct LeftRoom {
/// The timeline of messages and state changes in the room up to the point
/// when the user left.
pub timeline: Timeline,
/// Updates to the state, between the time indicated by the `since`
/// parameter, and the start of the `timeline` (or all state up to the
/// start of the `timeline`, if `since` is not given, or `full_state` is
/// true).
pub state: State,
/// The private data that this user has attached to this room.
pub account_data: RoomAccountData,
}
impl LeftRoom {
pub fn new(timeline: Timeline, state: State, account_data: RoomAccountData) -> Self {
Self { timeline, state, account_data }
}
}
/// Events in the room.
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Timeline {
/// True if the number of events returned was limited by the `limit` on the
/// filter.
pub limited: bool,
/// A token that can be supplied to to the `from` parameter of the
/// `/rooms/{roomId}/messages` endpoint.
pub prev_batch: Option<String>,
/// A list of events.
pub events: Vec<SyncRoomEvent>,
}
impl Timeline {
pub fn new(limited: bool, prev_batch: Option<String>) -> Self {
Self { limited, prev_batch, ..Default::default() }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(
try_from = "SyncStateEvent<MemberEventContent>",
into = "SyncStateEvent<MemberEventContent>"
)]
pub struct MemberEvent {
pub content: MemberEventContent,
pub event_id: EventId,
pub origin_server_ts: MilliSecondsSinceUnixEpoch,
pub prev_content: Option<MemberEventContent>,
pub sender: UserId,
pub state_key: UserId,
pub unsigned: Unsigned,
}
impl TryFrom<SyncStateEvent<MemberEventContent>> for MemberEvent {
type Error = ruma::identifiers::Error;
fn try_from(event: SyncStateEvent<MemberEventContent>) -> Result<Self, Self::Error> {
Ok(MemberEvent {
content: event.content,
event_id: event.event_id,
origin_server_ts: event.origin_server_ts,
prev_content: event.prev_content,
sender: event.sender,
state_key: UserId::try_from(event.state_key)?,
unsigned: event.unsigned,
})
}
}
impl TryFrom<StateEvent<MemberEventContent>> for MemberEvent {
type Error = ruma::identifiers::Error;
fn try_from(event: StateEvent<MemberEventContent>) -> Result<Self, Self::Error> {
Ok(MemberEvent {
content: event.content,
event_id: event.event_id,
origin_server_ts: event.origin_server_ts,
prev_content: event.prev_content,
sender: event.sender,
state_key: UserId::try_from(event.state_key)?,
unsigned: event.unsigned,
})
}
}
impl From<MemberEvent> for SyncStateEvent<MemberEventContent> {
fn from(other: MemberEvent) -> SyncStateEvent<MemberEventContent> {
SyncStateEvent {
content: other.content,
event_id: other.event_id,
sender: other.sender,
origin_server_ts: other.origin_server_ts,
state_key: other.state_key.to_string(),
prev_content: other.prev_content,
unsigned: other.unsigned,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(
try_from = "StrippedStateEvent<MemberEventContent>",
into = "StrippedStateEvent<MemberEventContent>"
)]
pub struct StrippedMemberEvent {
pub content: MemberEventContent,
pub sender: UserId,
pub state_key: UserId,
}
impl TryFrom<StrippedStateEvent<MemberEventContent>> for StrippedMemberEvent {
type Error = ruma::identifiers::Error;
fn try_from(event: StrippedStateEvent<MemberEventContent>) -> Result<Self, Self::Error> {
Ok(StrippedMemberEvent {
content: event.content,
sender: event.sender,
state_key: UserId::try_from(event.state_key)?,
})
}
}
impl From<StrippedMemberEvent> for StrippedStateEvent<MemberEventContent> {
fn from(other: StrippedMemberEvent) -> Self {
Self {
content: other.content,
sender: other.sender,
state_key: other.state_key.to_string(),
}
}
}
/// A deserialized response for the rooms members API call.
///
/// [GET /_matrix/client/r0/rooms/{roomId}/members](https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-members)
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct MembersResponse {
/// The list of members events.
pub chunk: Vec<MemberEvent>,
/// Collection of ambiguity changes that room member events trigger.
pub ambiguity_changes: AmbiguityChanges,
}
+40
View File
@@ -0,0 +1,40 @@
//! Abstraction over an executor so we can spawn tasks under WASM the same way
//! we do usually.
#[cfg(target_arch = "wasm32")]
use std::{
pin::Pin,
task::{Context, Poll},
};
#[cfg(target_arch = "wasm32")]
use futures::{future::RemoteHandle, Future, FutureExt};
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::spawn;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_futures::spawn_local;
#[cfg(target_arch = "wasm32")]
pub fn spawn<F, T>(future: F) -> JoinHandle<T>
where
F: Future<Output = T> + 'static,
{
let fut = future.unit_error();
let (fut, handle) = fut.remote_handle();
spawn_local(fut);
JoinHandle { handle }
}
#[cfg(target_arch = "wasm32")]
pub struct JoinHandle<T> {
handle: RemoteHandle<Result<T, ()>>,
}
#[cfg(target_arch = "wasm32")]
impl<T: 'static> Future for JoinHandle<T> {
type Output = Result<T, ()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handle).poll(cx)
}
}
+2 -11
View File
@@ -1,18 +1,9 @@
pub use async_trait::async_trait;
pub use instant;
pub use ruma::{
api::{
client as api,
error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError, ServerError},
AuthScheme, EndpointError, OutgoingRequest,
},
assign, directory, encryption, events, identifiers, int, presence, push,
serde::{CanonicalJsonValue, Raw},
thirdparty, uint, Int, Outgoing, UInt,
};
pub use uuid;
pub mod deserialized_responses;
pub mod executor;
pub mod locks;
/// Super trait that is used for our store traits, this trait will differ if
-1
View File
@@ -4,6 +4,5 @@
#[cfg(target_arch = "wasm32")]
pub use futures_locks::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
+37 -32
View File
@@ -8,7 +8,7 @@ license = "Apache-2.0"
name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.2.0"
version = "0.3.0"
[package.metadata.docs.rs]
features = ["docs"]
@@ -16,44 +16,49 @@ rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = []
sqlite_cryptostore = ["sqlx"]
docs = ["sqlite_cryptostore"]
sled_cryptostore = ["sled"]
docs = ["sled_cryptostore"]
[dependencies]
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
matrix-qrcode = { version = "0.1.0", path = "../matrix_qrcode" }
matrix-sdk-common = { version = "0.3.0", path = "../matrix_sdk_common" }
ruma = { version = "0.2.0", features = ["client-api-c", "unstable-pre-spec"] }
olm-rs = { version = "1.0.0", features = ["serde"] }
getrandom = "0.2.1"
serde = { version = "1.0.118", features = ["derive", "rc"] }
serde_json = "1.0.61"
zeroize = { version = "1.2.0", features = ["zeroize_derive"] }
url = "2.2.0"
olm-rs = { version = "1.0.1", features = ["serde"] }
getrandom = "0.2.3"
serde = { version = "1.0.126", features = ["derive", "rc"] }
serde_json = "1.0.64"
zeroize = { version = "1.3.0", features = ["zeroize_derive"] }
# Misc dependencies
thiserror = "1.0.23"
tracing = "0.1.22"
futures = "0.3.15"
sled = { version = "0.34.6", optional = true }
thiserror = "1.0.25"
tracing = "0.1.26"
atomic = "0.5.0"
dashmap = "4.0.1"
sha2 = "0.9.2"
aes-gcm = "0.8.0"
aes-ctr = "0.6.0"
pbkdf2 = { version = "0.6.0", default-features = false }
hmac = "0.10.1"
dashmap = "4.0.2"
sha2 = "0.9.5"
aes-gcm = "0.9.2"
aes = { version = "0.7.4", features = ["ctr"] }
pbkdf2 = { version = "0.8.0", default-features = false }
hmac = "0.11.0"
base64 = "0.13.0"
byteorder = "1.3.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.sqlx]
version = "0.4.2"
optional = true
default-features = false
features = ["runtime-tokio-native-tls", "sqlite", "macros"]
byteorder = "1.4.3"
[dev-dependencies]
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
futures = "0.3.8"
proptest = "0.10.1"
serde_json = "1.0.61"
tempfile = "3.1.0"
http = "0.2.2"
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
tokio = { version = "1.7.1", default-features = false, features = ["rt-multi-thread", "macros"] }
proptest = "1.0.0"
serde_json = "1.0.64"
tempfile = "3.2.0"
http = "0.2.4"
matrix-sdk-test = { version = "0.3.0", path = "../matrix_sdk_test" }
indoc = "1.0.3"
criterion = { version = "0.3.4", features = ["async", "async_tokio", "html_reports"] }
[target.'cfg(target_os = "linux")'.dev-dependencies]
pprof = { version = "0.4.3", features = ["flamegraph"] }
[[bench]]
name = "crypto_bench"
harness = false
required-features = ["sled_cryptostore"]
+277
View File
@@ -0,0 +1,277 @@
#[cfg(target_os = "linux")]
mod perf;
use std::sync::Arc;
use criterion::*;
use matrix_sdk_common::uuid::Uuid;
use matrix_sdk_crypto::{EncryptionSettings, OlmMachine};
use matrix_sdk_test::response_from_file;
use ruma::{
api::{
client::r0::{
keys::{claim_keys, get_keys},
to_device::send_event_to_device::Response as ToDeviceResponse,
},
IncomingResponse,
},
room_id, user_id, DeviceIdBox, UserId,
};
use serde_json::Value;
use tokio::runtime::Builder;
fn alice_id() -> UserId {
user_id!("@alice:example.org")
}
fn alice_device_id() -> DeviceIdBox {
"JLAFKJWSCS".into()
}
fn keys_query_response() -> get_keys::Response {
let data = include_bytes!("./keys_query.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::Response::try_from_http_response(data).expect("Can't parse the keys upload response")
}
fn keys_claim_response() -> claim_keys::Response {
let data = include_bytes!("./keys_claim.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
claim_keys::Response::try_from_http_response(data)
.expect("Can't parse the keys upload response")
}
fn huge_keys_query_resopnse() -> get_keys::Response {
let data = include_bytes!("./keys_query_2000_members.json");
let data: Value = serde_json::from_slice(data).unwrap();
let data = response_from_file(&data);
get_keys::Response::try_from_http_response(data).expect("Can't parse the keys query response")
}
pub fn keys_query(c: &mut Criterion) {
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
let response = keys_query_response();
let uuid = Uuid::new_v4();
let count = response.device_keys.values().fold(0, |acc, d| acc + d.len())
+ response.master_keys.len()
+ response.self_signing_keys.len()
+ response.user_signing_keys.len();
let mut group = c.benchmark_group("Keys querying");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} device and cross signing keys", count);
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
b.to_async(&runtime)
.iter(|| async { machine.mark_request_as_sent(&uuid, response).await.unwrap() })
});
let dir = tempfile::tempdir().unwrap();
let machine = runtime
.block_on(OlmMachine::new_with_default_store(
&alice_id(),
&alice_device_id(),
dir.path(),
None,
))
.unwrap();
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
b.to_async(&runtime)
.iter(|| async { machine.mark_request_as_sent(&uuid, response).await.unwrap() })
});
group.finish()
}
pub fn keys_claiming(c: &mut Criterion) {
let runtime = Arc::new(Builder::new_multi_thread().build().expect("Can't create runtime"));
let keys_query_response = keys_query_response();
let uuid = Uuid::new_v4();
let response = keys_claim_response();
let count = response.one_time_keys.values().fold(0, |acc, d| acc + d.len());
let mut group = c.benchmark_group("Olm session creation");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} one-time keys", count);
group.bench_with_input(BenchmarkId::new("memory store", &name), &response, |b, response| {
b.iter_batched(
|| {
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
runtime
.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response))
.unwrap();
(machine, runtime.clone())
},
move |(machine, runtime)| {
runtime.block_on(machine.mark_request_as_sent(&uuid, response)).unwrap()
},
BatchSize::SmallInput,
)
});
group.bench_with_input(BenchmarkId::new("sled store", &name), &response, |b, response| {
b.iter_batched(
|| {
let dir = tempfile::tempdir().unwrap();
let machine = runtime
.block_on(OlmMachine::new_with_default_store(
&alice_id(),
&alice_device_id(),
dir.path(),
None,
))
.unwrap();
runtime
.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response))
.unwrap();
(machine, runtime.clone())
},
move |(machine, runtime)| {
runtime.block_on(machine.mark_request_as_sent(&uuid, response)).unwrap()
},
BatchSize::SmallInput,
)
});
group.finish()
}
pub fn room_key_sharing(c: &mut Criterion) {
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
let keys_query_response = keys_query_response();
let uuid = Uuid::new_v4();
let response = keys_claim_response();
let room_id = room_id!("!test:localhost");
let to_device_response = ToDeviceResponse::new();
let users: Vec<UserId> = keys_query_response.device_keys.keys().cloned().collect();
let count = response.one_time_keys.values().fold(0, |acc, d| acc + d.len());
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
runtime.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response)).unwrap();
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
let mut group = c.benchmark_group("Room key sharing");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} devices", count);
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
b.to_async(&runtime).iter(|| async {
let requests = machine
.share_group_session(&room_id, users.iter(), EncryptionSettings::default())
.await
.unwrap();
assert!(!requests.is_empty());
for request in requests {
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
}
machine.invalidate_group_session(&room_id).await.unwrap();
})
});
let dir = tempfile::tempdir().unwrap();
let machine = runtime
.block_on(OlmMachine::new_with_default_store(
&alice_id(),
&alice_device_id(),
dir.path(),
None,
))
.unwrap();
runtime.block_on(machine.mark_request_as_sent(&uuid, &keys_query_response)).unwrap();
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
group.bench_function(BenchmarkId::new("sled store", &name), |b| {
b.to_async(&runtime).iter(|| async {
let requests = machine
.share_group_session(&room_id, users.iter(), EncryptionSettings::default())
.await
.unwrap();
assert!(!requests.is_empty());
for request in requests {
machine.mark_request_as_sent(&request.txn_id, &to_device_response).await.unwrap();
}
machine.invalidate_group_session(&room_id).await.unwrap();
})
});
group.finish()
}
pub fn devices_missing_sessions_collecting(c: &mut Criterion) {
let runtime = Builder::new_multi_thread().build().expect("Can't create runtime");
let machine = OlmMachine::new(&alice_id(), &alice_device_id());
let response = huge_keys_query_resopnse();
let uuid = Uuid::new_v4();
let users: Vec<UserId> = response.device_keys.keys().cloned().collect();
let count = response.device_keys.values().fold(0, |acc, d| acc + d.len());
let mut group = c.benchmark_group("Devices missing sessions collecting");
group.throughput(Throughput::Elements(count as u64));
let name = format!("{} devices", count);
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
group.bench_function(BenchmarkId::new("memory store", &name), |b| {
b.to_async(&runtime).iter_with_large_drop(|| async {
machine.get_missing_sessions(users.iter()).await.unwrap()
})
});
let dir = tempfile::tempdir().unwrap();
let machine = runtime
.block_on(OlmMachine::new_with_default_store(
&alice_id(),
&alice_device_id(),
dir.path(),
None,
))
.unwrap();
runtime.block_on(machine.mark_request_as_sent(&uuid, &response)).unwrap();
group.bench_function(BenchmarkId::new("sled store", &name), |b| {
b.to_async(&runtime)
.iter(|| async { machine.get_missing_sessions(users.iter()).await.unwrap() })
});
group.finish()
}
fn criterion() -> Criterion {
#[cfg(target_os = "linux")]
let criterion = Criterion::default().with_profiler(perf::FlamegraphProfiler::new(100));
#[cfg(not(target_os = "linux"))]
let criterion = Criterion::default();
criterion
}
criterion_group! {
name = benches;
config = criterion();
targets = keys_query, keys_claiming, room_key_sharing, devices_missing_sessions_collecting,
}
criterion_main!(benches);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+76
View File
@@ -0,0 +1,76 @@
//! This is a simple Criterion Profiler implementation using pprof.
//!
//! It's mostly a direct copy from here: https://www.jibbow.com/posts/criterion-flamegraphs/
use std::{fs::File, os::raw::c_int, path::Path};
use criterion::profiler::Profiler;
use pprof::ProfilerGuard;
/// Small custom profiler that can be used with Criterion to create a flamegraph
/// for benchmarks. Also see [the Criterion documentation on
/// this][custom-profiler].
///
/// ## Example on how to enable the custom profiler:
///
/// ```
/// mod perf;
/// use perf::FlamegraphProfiler;
///
/// fn fibonacci_profiled(criterion: &mut Criterion) {
/// // Use the criterion struct as normal here.
/// }
///
/// fn custom() -> Criterion {
/// Criterion::default().with_profiler(FlamegraphProfiler::new())
/// }
///
/// criterion_group! {
/// name = benches;
/// config = custom();
/// targets = fibonacci_profiled
/// }
/// ```
///
/// The neat thing about this is that it will sample _only_ the benchmark, and
/// not other stuff like the setup process.
///
/// Further, it will only kick in if `--profile-time <time>` is passed to the
/// benchmark binary. A flamegraph will be created for each individual benchmark
/// in its report directory under `profile/flamegraph.svg`.
///
/// [custom-profiler]: https://bheisler.github.io/criterion.rs/book/user_guide/profiling.html#implementing-in-process-profiling-hooks
pub struct FlamegraphProfiler<'a> {
frequency: c_int,
active_profiler: Option<ProfilerGuard<'a>>,
}
impl<'a> FlamegraphProfiler<'a> {
pub fn new(frequency: c_int) -> Self {
FlamegraphProfiler { frequency, active_profiler: None }
}
}
impl<'a> Profiler for FlamegraphProfiler<'a> {
fn start_profiling(&mut self, _benchmark_id: &str, _benchmark_dir: &Path) {
self.active_profiler = Some(ProfilerGuard::new(self.frequency).unwrap());
}
fn stop_profiling(&mut self, _benchmark_id: &str, benchmark_dir: &Path) {
std::fs::create_dir_all(benchmark_dir)
.expect("Can't create a directory to store the benchmarking report");
let flamegraph_path = benchmark_dir.join("flamegraph.svg");
let flamegraph_file = File::create(&flamegraph_path)
.expect("File system error while creating flamegraph.svg");
if let Some(profiler) = self.active_profiler.take() {
profiler
.report()
.build()
.expect("Can't build profiling report")
.flamegraph(flamegraph_file)
.expect("Error writing flamegraph");
}
}
}
+10 -1
View File
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_common::identifiers::{DeviceId, Error as IdentifierError, UserId};
use olm_rs::errors::{OlmGroupSessionError, OlmSessionError};
use ruma::{identifiers::Error as IdentifierError, DeviceId, UserId};
use serde_json::Error as SerdeError;
use thiserror::Error;
@@ -86,6 +86,10 @@ pub enum MegolmError {
#[error("can't finish Olm group session operation {0}")]
OlmGroupSession(#[from] OlmGroupSessionError),
/// The room where a group session should be shared is not encrypted.
#[error("The room where a group session should be shared is not encrypted")]
EncryptionNotEnabled,
/// The storage layer returned an error.
#[error(transparent)]
Store(#[from] CryptoStoreError),
@@ -170,6 +174,11 @@ pub(crate) enum SessionCreationError {
one-time key is missing"
)]
OneTimeKeyMissing(UserId, Box<DeviceId>),
#[error(
"Tried to create a new Olm session for {0} {1}, but the one-time \
key algorithm is unsupported"
)]
OneTimeKeyUnknown(UserId, Box<DeviceId>),
#[error("Failed to verify the one-time key signatures for {0} {1}: {2:?}")]
InvalidSignature(UserId, Box<DeviceId>, SignatureError),
#[error(
@@ -17,21 +17,17 @@ use std::{
io::{Error as IoError, ErrorKind, Read},
};
use thiserror::Error;
use zeroize::Zeroizing;
use serde::{Deserialize, Serialize};
use matrix_sdk_common::events::room::JsonWebKey;
use getrandom::getrandom;
use aes_ctr::{
cipher::{NewStreamCipher, SyncStreamCipher},
Aes256Ctr,
use aes::{
cipher::{generic_array::GenericArray, FromBlockCipher, NewBlockCipher, StreamCipher},
Aes256, Aes256Ctr,
};
use base64::DecodeError;
use getrandom::getrandom;
use ruma::events::room::{EncryptedFile, JsonWebKey, JsonWebKeyInit};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use zeroize::Zeroizing;
use crate::utilities::{decode, decode_url_safe, encode, encode_url_safe};
@@ -41,17 +37,25 @@ const VERSION: &str = "v2";
/// A wrapper that transparently encrypts anything that implements `Read` as an
/// Matrix attachment.
#[derive(Debug)]
pub struct AttachmentDecryptor<'a, R: 'a + Read> {
inner_reader: &'a mut R,
inner: &'a mut R,
expected_hash: Vec<u8>,
sha: Sha256,
aes: Aes256Ctr,
}
impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentDecryptor<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AttachmentDecryptor")
.field("inner", &self.inner)
.field("expected_hash", &self.expected_hash)
.finish()
}
}
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read_bytes = self.inner_reader.read(buf)?;
let read_bytes = self.inner.read(buf)?;
if read_bytes == 0 {
let hash = self.sha.finalize_reset();
@@ -59,10 +63,7 @@ impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
if hash.as_slice() == self.expected_hash.as_slice() {
Ok(0)
} else {
Err(IoError::new(
ErrorKind::Other,
"Hash missmatch while decrypting",
))
Err(IoError::new(ErrorKind::Other, "Hash mismatch while decrypting"))
}
} else {
self.sha.update(&buf[0..read_bytes]);
@@ -130,31 +131,23 @@ impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> {
return Err(DecryptorError::UnknownVersion);
}
let hash = decode(
info.hashes
.get("sha256")
.ok_or(DecryptorError::MissingHash)?,
)?;
let hash = decode(info.hashes.get("sha256").ok_or(DecryptorError::MissingHash)?)?;
let key = Zeroizing::from(decode_url_safe(info.web_key.k)?);
let iv = decode(info.iv)?;
let iv = GenericArray::from_exact_iter(iv).ok_or(DecryptorError::KeyNonceLength)?;
let sha = Sha256::default();
let aes = Aes256Ctr::new_var(&key, &iv).map_err(|_| DecryptorError::KeyNonceLength)?;
let aes = Aes256::new_from_slice(&key).map_err(|_| DecryptorError::KeyNonceLength)?;
let aes = Aes256Ctr::from_block_cipher(aes, &iv);
Ok(AttachmentDecryptor {
inner_reader: input,
expected_hash: hash,
sha,
aes,
})
Ok(AttachmentDecryptor { inner: input, expected_hash: hash, sha, aes })
}
}
/// A wrapper that transparently encrypts anything that implements `Read`.
#[derive(Debug)]
pub struct AttachmentEncryptor<'a, R: Read + 'a> {
finished: bool,
inner_reader: &'a mut R,
inner: &'a mut R,
web_key: JsonWebKey,
iv: String,
hashes: BTreeMap<String, String>,
@@ -162,15 +155,22 @@ pub struct AttachmentEncryptor<'a, R: Read + 'a> {
sha: Sha256,
}
impl<'a, R: 'a + Read + std::fmt::Debug> std::fmt::Debug for AttachmentEncryptor<'a, R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AttachmentEncryptor")
.field("inner", &self.inner)
.field("finished", &self.finished)
.finish()
}
}
impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let read_bytes = self.inner_reader.read(buf)?;
let read_bytes = self.inner.read(buf)?;
if read_bytes == 0 {
let hash = self.sha.finalize_reset();
self.hashes
.entry("sha256".to_owned())
.or_insert_with(|| encode(hash));
self.hashes.entry("sha256".to_owned()).or_insert_with(|| encode(hash));
Ok(0)
} else {
self.aes.apply_keystream(&mut buf[0..read_bytes]);
@@ -215,24 +215,27 @@ impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> {
let mut iv = Zeroizing::new([0u8; IV_SIZE]);
getrandom(&mut *key).expect("Can't generate randomness");
// Only populate the the first 8 bits with randomness, the rest is 0
// Only populate the first 8 bits with randomness, the rest is 0
// initialized.
getrandom(&mut iv[0..8]).expect("Can't generate randomness");
let web_key = JsonWebKey {
let web_key = JsonWebKey::from(JsonWebKeyInit {
kty: "oct".to_owned(),
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
alg: "A256CTR".to_owned(),
k: encode_url_safe(&*key),
ext: true,
};
});
let encoded_iv = encode(&*iv);
let iv = GenericArray::from_slice(&*iv);
let key = GenericArray::from_slice(&*key);
let aes = Aes256Ctr::new_var(&*key, &*iv).expect("Cannot create AES encryption object.");
let aes = Aes256::new(key);
let aes = Aes256Ctr::from_block_cipher(aes, iv);
AttachmentEncryptor {
finished: false,
inner_reader: reader,
inner: reader,
iv: encoded_iv,
web_key,
hashes: BTreeMap::new(),
@@ -244,9 +247,7 @@ impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> {
/// Consume the encryptor and get the encryption key.
pub fn finish(mut self) -> EncryptionInfo {
let hash = self.sha.finalize();
self.hashes
.entry("sha256".to_owned())
.or_insert_with(|| encode(hash));
self.hashes.entry("sha256".to_owned()).or_insert_with(|| encode(hash));
EncryptionInfo {
version: VERSION.to_string(),
@@ -257,21 +258,35 @@ impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> {
}
}
/// Struct holding all the information that is needed to decrypt an encrypted
/// file.
#[derive(Debug, Serialize, Deserialize)]
pub struct EncryptionInfo {
#[serde(rename = "v")]
/// The version of the encryption scheme.
pub version: String,
/// The web key that was used to encrypt the file.
pub web_key: JsonWebKey,
/// The initialization vector that was used to encrypt the file.
pub iv: String,
/// The hashes that can be used to check the validity of the file.
pub hashes: BTreeMap<String, String>,
}
impl From<EncryptedFile> for EncryptionInfo {
fn from(file: EncryptedFile) -> Self {
Self { version: file.v, web_key: file.key, iv: file.iv, hashes: file.hashes }
}
}
#[cfg(test)]
mod test {
use super::{AttachmentDecryptor, AttachmentEncryptor, EncryptionInfo};
use serde_json::json;
use std::io::{Cursor, Read};
use serde_json::json;
use super::{AttachmentDecryptor, AttachmentEncryptor, EncryptionInfo};
const EXAMPLE_DATA: &[u8] = &[
179, 154, 118, 127, 186, 127, 110, 33, 203, 33, 33, 134, 67, 100, 173, 46, 235, 27, 215,
172, 36, 26, 75, 47, 33, 160,
@@ -12,20 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use serde_json::Error as SerdeError;
use std::io::{Cursor, Read, Seek, SeekFrom};
use thiserror::Error;
use aes::{
cipher::{generic_array::GenericArray, FromBlockCipher, NewBlockCipher, StreamCipher},
Aes256, Aes256Ctr,
};
use byteorder::{BigEndian, ReadBytesExt};
use getrandom::getrandom;
use aes_ctr::{
cipher::{NewStreamCipher, SyncStreamCipher},
Aes256Ctr,
};
use hmac::{Hmac, Mac, NewMac};
use pbkdf2::pbkdf2;
use serde_json::Error as SerdeError;
use sha2::{Sha256, Sha512};
use thiserror::Error;
use crate::{
olm::ExportedRoomKey,
@@ -52,7 +51,7 @@ pub enum KeyExportError {
UnsupportedVersion,
/// The MAC of the encrypted payload is invalid.
#[error("The MAC of the encrypted payload is invalid.")]
InvalidMAC,
InvalidMac,
/// The decrypted key export isn't valid UTF-8.
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
@@ -64,7 +63,7 @@ pub enum KeyExportError {
Decode(#[from] DecodeError),
/// The key export doesn't all the required fields.
#[error(transparent)]
IO(#[from] std::io::Error),
Io(#[from] std::io::Error),
}
/// Try to decrypt a reader into a list of exported room keys.
@@ -77,14 +76,14 @@ pub enum KeyExportError {
/// ```no_run
/// # use std::io::Cursor;
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_key_export};
/// # use matrix_sdk_common::identifiers::user_id;
/// # use ruma::user_id;
/// # use futures::executor::block_on;
/// # let alice = user_id!("@alice:example.org");
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
/// # block_on(async {
/// # let export = Cursor::new("".to_owned());
/// let exported_keys = decrypt_key_export(export, "1234").unwrap();
/// machine.import_keys(exported_keys).await.unwrap();
/// machine.import_keys(exported_keys, |_, _| {}).await.unwrap();
/// # });
/// ```
pub fn decrypt_key_export(
@@ -99,14 +98,10 @@ pub fn decrypt_key_export(
return Err(KeyExportError::InvalidHeaders);
}
let payload: String = x
.lines()
.filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER)))
.collect();
let payload: String =
x.lines().filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER))).collect();
Ok(serde_json::from_str(&decrypt_helper(
&payload, passphrase,
)?)?)
Ok(serde_json::from_str(&decrypt_helper(&payload, passphrase)?)?)
}
/// Encrypt the list of exported room keys using the given passphrase.
@@ -120,7 +115,7 @@ pub fn decrypt_key_export(
///
/// * `rounds` - The number of rounds that should be used for the key
/// derivation when the passphrase gets turned into an AES key. More rounds are
/// increasingly computationally intensive and as such help against bruteforce
/// increasingly computationally intensive and as such help against brute-force
/// attacks. Should be at least `10000`, while values in the `100000` ranges
/// should be preferred.
///
@@ -132,7 +127,7 @@ pub fn decrypt_key_export(
/// # Examples
/// ```no_run
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export};
/// # use matrix_sdk_common::identifiers::{user_id, room_id};
/// # use ruma::{user_id, room_id};
/// # use futures::executor::block_on;
/// # let alice = user_id!("@alice:example.org");
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
@@ -166,7 +161,12 @@ fn encrypt_helper(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> St
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
let mut aes = Aes256Ctr::new_var(&key, &iv.to_be_bytes()).expect("Can't create AES object");
let key = GenericArray::from_slice(key);
let iv = iv.to_be_bytes();
let iv = GenericArray::from_slice(&iv);
let aes = Aes256::new(key);
let mut aes = Aes256Ctr::from_block_cipher(aes, iv);
aes.apply_keystream(&mut plaintext);
@@ -174,11 +174,11 @@ fn encrypt_helper(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> St
payload.extend(&VERSION.to_be_bytes());
payload.extend(&salt);
payload.extend(&iv.to_be_bytes());
payload.extend(&*iv);
payload.extend(&rounds.to_be_bytes());
payload.extend_from_slice(&plaintext);
payload.extend_from_slice(plaintext);
let mut hmac = Hmac::<Sha256>::new_varkey(hmac_key).expect("Can't create HMAC object");
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("Can't create HMAC object");
hmac.update(&payload);
let mac = hmac.finalize();
@@ -218,12 +218,16 @@ fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExpor
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
let mut hmac = Hmac::<Sha256>::new_varkey(hmac_key).expect("Can't create an HMAC object");
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("Can't create an HMAC object");
hmac.update(&decoded[0..ciphertext_end]);
hmac.verify(&mac).map_err(|_| KeyExportError::InvalidMAC)?;
hmac.verify(&mac).map_err(|_| KeyExportError::InvalidMac)?;
let key = GenericArray::from_slice(key);
let iv = GenericArray::from_slice(&iv);
let mut ciphertext = &mut decoded[ciphertext_start..ciphertext_end];
let mut aes = Aes256Ctr::new_var(&key, &iv).expect("Can't create an AES object");
let aes = Aes256::new(key);
let mut aes = Aes256Ctr::from_block_cipher(aes, iv);
aes.apply_keystream(&mut ciphertext);
Ok(String::from_utf8(ciphertext.to_owned())?)
@@ -231,12 +235,12 @@ fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExpor
#[cfg(test)]
mod test {
use indoc::indoc;
use proptest::prelude::*;
use std::io::Cursor;
use matrix_sdk_common::identifiers::room_id;
use indoc::indoc;
use matrix_sdk_test::async_test;
use proptest::prelude::*;
use ruma::room_id;
use super::{decode, decrypt_helper, decrypt_key_export, encrypt_helper, encrypt_key_export};
use crate::machine::test::get_prepared_machine;
@@ -261,10 +265,7 @@ mod test {
"};
fn export_wihtout_headers() -> String {
TEST_EXPORT
.lines()
.filter(|l| !l.starts_with("-----"))
.collect()
TEST_EXPORT.lines().filter(|l| !l.starts_with("-----")).collect()
}
#[test]
@@ -301,14 +302,8 @@ mod test {
let (machine, _) = get_prepared_machine().await;
let room_id = room_id!("!test:localhost");
machine
.create_outbound_group_session_with_defaults(&room_id)
.await
.unwrap();
let export = machine
.export_keys(|s| s.room_id() == &room_id)
.await
.unwrap();
machine.create_outbound_group_session_with_defaults(&room_id).await.unwrap();
let export = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap();
assert!(!export.is_empty());
@@ -316,7 +311,7 @@ mod test {
let decrypted = decrypt_key_export(Cursor::new(encrypted), "1234").unwrap();
assert_eq!(export, decrypted);
assert_eq!(machine.import_keys(decrypted).await.unwrap(), (0, 1));
assert_eq!(machine.import_keys(decrypted, |_, _| {}).await.unwrap(), (0, 1));
}
#[test]
+2 -2
View File
@@ -1,5 +1,5 @@
mod attachments;
mod key_export;
pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError};
pub use key_export::{decrypt_key_export, encrypt_key_export};
pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError, EncryptionInfo};
pub use key_export::{decrypt_key_export, encrypt_key_export, KeyExportError};
+98 -66
View File
@@ -23,52 +23,84 @@ use std::{
};
use atomic::Atomic;
use matrix_sdk_common::{
api::r0::keys::SignedKey,
encryption::DeviceKeys,
use matrix_sdk_common::locks::Mutex;
use ruma::{
encryption::{DeviceKeys, SignedKey},
events::{
forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
room::encrypted::EncryptedEventContent, EventType,
room::encrypted::EncryptedEventContent, AnyToDeviceEventContent,
},
identifiers::{
DeviceId, DeviceIdBox, DeviceKeyAlgorithm, DeviceKeyId, EventEncryptionAlgorithm, UserId,
},
locks::Mutex,
};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Value};
use tracing::warn;
use super::{atomic_bool_deserializer, atomic_bool_serializer};
use crate::{
olm::{InboundGroupSession, PrivateCrossSigningIdentity, Session},
store::{Changes, DeviceChanges},
error::{EventError, OlmError, OlmResult, SignatureError},
identities::{OwnUserIdentity, UserIdentities},
olm::{InboundGroupSession, PrivateCrossSigningIdentity, Session, Utility},
store::{Changes, CryptoStore, DeviceChanges, Result as StoreResult},
verification::VerificationMachine,
OutgoingVerificationRequest, Sas, ToDeviceRequest,
};
#[cfg(test)]
use crate::{OlmMachine, ReadOnlyAccount};
use crate::{
error::{EventError, OlmError, OlmResult, SignatureError},
identities::{OwnUserIdentity, UserIdentities},
olm::Utility,
store::{CryptoStore, Result as StoreResult},
verification::VerificationMachine,
Sas, ToDeviceRequest,
};
/// A read-only version of a `Device`.
#[derive(Debug, Clone)]
#[derive(Clone, Serialize, Deserialize)]
pub struct ReadOnlyDevice {
user_id: Arc<UserId>,
device_id: Arc<Box<DeviceId>>,
device_id: Arc<DeviceId>,
algorithms: Arc<[EventEncryptionAlgorithm]>,
keys: Arc<BTreeMap<DeviceKeyId, String>>,
pub(crate) signatures: Arc<BTreeMap<UserId, BTreeMap<DeviceKeyId, String>>>,
display_name: Arc<Option<String>>,
#[serde(
serialize_with = "atomic_bool_serializer",
deserialize_with = "atomic_bool_deserializer"
)]
deleted: Arc<AtomicBool>,
#[serde(
serialize_with = "local_trust_serializer",
deserialize_with = "local_trust_deserializer"
)]
trust_state: Arc<Atomic<LocalTrust>>,
}
#[derive(Debug, Clone)]
impl std::fmt::Debug for ReadOnlyDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReadOnlyDevice")
.field("user_id", self.user_id())
.field("device_id", &self.device_id())
.field("display_name", self.display_name())
.field("keys", self.keys())
.field("deleted", &self.deleted.load(Ordering::SeqCst))
.field("trust_state", &self.trust_state)
.finish()
}
}
fn local_trust_serializer<S>(x: &Atomic<LocalTrust>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let value = x.load(Ordering::SeqCst);
s.serialize_some(&value)
}
fn local_trust_deserializer<'de, D>(deserializer: D) -> Result<Arc<Atomic<LocalTrust>>, D::Error>
where
D: Deserializer<'de>,
{
let value = LocalTrust::deserialize(deserializer)?;
Ok(Arc::new(Atomic::new(value)))
}
#[derive(Clone)]
/// A device represents a E2EE capable client of an user.
pub struct Device {
pub(crate) inner: ReadOnlyDevice,
@@ -78,6 +110,12 @@ pub struct Device {
pub(crate) device_owner_identity: Option<UserIdentities>,
}
impl std::fmt::Debug for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Device").field("device", &self.inner).finish()
}
}
impl Deref for Device {
type Target = ReadOnlyDevice;
@@ -91,9 +129,13 @@ impl Device {
///
/// Returns a `Sas` object and to-device request that needs to be sent out.
pub async fn start_verification(&self) -> StoreResult<(Sas, ToDeviceRequest)> {
self.verification_machine
.start_sas(self.inner.clone())
.await
let (sas, request) = self.verification_machine.start_sas(self.inner.clone()).await?;
if let OutgoingVerificationRequest::ToDevice(r) = request {
Ok((sas, r))
} else {
panic!("Invalid verification request type");
}
}
/// Get the Olm sessions that belong to this device.
@@ -107,8 +149,7 @@ impl Device {
/// Get the trust state of the device.
pub fn trust_state(&self) -> bool {
self.inner
.trust_state(&self.own_identity, &self.device_owner_identity)
self.inner.trust_state(&self.own_identity, &self.device_owner_identity)
}
/// Set the local trust state of the device to the given state.
@@ -123,10 +164,7 @@ impl Device {
self.inner.set_trust_state(trust_state);
let changes = Changes {
devices: DeviceChanges {
changed: vec![self.inner.clone()],
..Default::default()
},
devices: DeviceChanges { changed: vec![self.inner.clone()], ..Default::default() },
..Default::default()
};
@@ -137,17 +175,12 @@ impl Device {
///
/// # Arguments
///
/// * `event_type` - The type of the event.
///
/// * `content` - The content of the event that should be encrypted.
pub(crate) async fn encrypt(
&self,
event_type: EventType,
content: Value,
content: AnyToDeviceEventContent,
) -> OlmResult<(Session, EncryptedEventContent)> {
self.inner
.encrypt(&**self.verification_machine.store, event_type, content)
.await
self.inner.encrypt(&*self.verification_machine.store, content).await
}
/// Encrypt the given inbound group session as a forwarded room key for this
@@ -155,8 +188,13 @@ impl Device {
pub async fn encrypt_session(
&self,
session: InboundGroupSession,
message_index: Option<u32>,
) -> OlmResult<(Session, EncryptedEventContent)> {
let export = session.export().await;
let export = if let Some(index) = message_index {
session.export_at_index(index).await
} else {
session.export().await
};
let content: ForwardedRoomKeyToDeviceEventContent = if let Ok(c) = export.try_into() {
c
@@ -171,8 +209,7 @@ impl Device {
);
};
let content = serde_json::to_value(content)?;
self.encrypt(EventType::ForwardedRoomKey, content).await
self.encrypt(AnyToDeviceEventContent::ForwardedRoomKey(content)).await
}
}
@@ -198,6 +235,12 @@ impl UserDevices {
})
}
/// Returns true if there is at least one devices of this user that is
/// considered to be verified, false otherwise.
pub fn is_any_verified(&self) -> bool {
self.inner.values().any(|d| d.trust_state(&self.own_identity, &self.device_owner_identity))
}
/// Iterator over all the device ids of the user devices.
pub fn keys(&self) -> impl Iterator<Item = &DeviceIdBox> {
self.inner.keys()
@@ -253,7 +296,7 @@ impl ReadOnlyDevice {
) -> Self {
Self {
user_id: Arc::new(user_id),
device_id: Arc::new(device_id),
device_id: device_id.into(),
display_name: Arc::new(display_name),
trust_state: Arc::new(Atomic::new(trust_state)),
signatures: Arc::new(signatures),
@@ -280,8 +323,7 @@ impl ReadOnlyDevice {
/// Get the key of the given key algorithm belonging to this device.
pub fn get_key(&self, algorithm: DeviceKeyAlgorithm) -> Option<&String> {
self.keys
.get(&DeviceKeyId::from_parts(algorithm, &self.device_id))
self.keys.get(&DeviceKeyId::from_parts(algorithm, &self.device_id))
}
/// Get a map containing all the device keys.
@@ -338,7 +380,7 @@ impl ReadOnlyDevice {
// only the identity is trusted, if the identity and the device are
// trusted.
if self.is_trusted() {
// If the device is localy marked as verified just return so, no
// If the device is locally marked as verified just return so, no
// need to check signatures.
true
} else {
@@ -351,7 +393,7 @@ impl ReadOnlyDevice {
// If it's one of our own devices, just check that
// we signed the device.
UserIdentities::Own(_) => {
own_identity.is_device_signed(&self).map_or(false, |_| true)
own_identity.is_device_signed(self).map_or(false, |_| true)
}
// If it's a device from someone else, first check
@@ -359,10 +401,10 @@ impl ReadOnlyDevice {
// check if the other user has signed this device.
UserIdentities::Other(device_identity) => {
own_identity
.is_identity_signed(&device_identity)
.is_identity_signed(device_identity)
.map_or(false, |_| true)
&& device_identity
.is_device_signed(&self)
.is_device_signed(self)
.map_or(false, |_| true)
}
})
@@ -374,8 +416,7 @@ impl ReadOnlyDevice {
pub(crate) async fn encrypt(
&self,
store: &dyn CryptoStore,
event_type: EventType,
content: Value,
content: AnyToDeviceEventContent,
) -> OlmResult<(Session, EncryptedEventContent)> {
let sender_key = if let Some(k) = self.get_key(DeviceKeyAlgorithm::Curve25519) {
k
@@ -408,7 +449,7 @@ impl ReadOnlyDevice {
return Err(OlmError::MissingSession);
};
let message = session.encrypt(&self, event_type, content).await?;
let message = session.encrypt(self, content).await?;
Ok((session, message))
}
@@ -428,9 +469,8 @@ impl ReadOnlyDevice {
}
fn is_signed_by_device(&self, json: &mut Value) -> Result<(), SignatureError> {
let signing_key = self
.get_key(DeviceKeyAlgorithm::Ed25519)
.ok_or(SignatureError::MissingSigningKey)?;
let signing_key =
self.get_key(DeviceKeyAlgorithm::Ed25519).ok_or(SignatureError::MissingSigningKey)?;
let utility = Utility::new();
@@ -500,7 +540,7 @@ impl TryFrom<&DeviceKeys> for ReadOnlyDevice {
fn try_from(device_keys: &DeviceKeys) -> Result<Self, Self::Error> {
let device = Self {
user_id: Arc::new(device_keys.user_id.clone()),
device_id: Arc::new(device_keys.device_id.clone()),
device_id: device_keys.device_id.clone().into(),
algorithms: device_keys.algorithms.as_slice().into(),
signatures: Arc::new(device_keys.signatures.clone()),
keys: Arc::new(device_keys.keys.clone()),
@@ -522,14 +562,12 @@ impl PartialEq for ReadOnlyDevice {
#[cfg(test)]
pub(crate) mod test {
use serde_json::json;
use std::convert::TryFrom;
use ruma::{encryption::DeviceKeys, user_id, DeviceKeyAlgorithm};
use serde_json::json;
use crate::identities::{LocalTrust, ReadOnlyDevice};
use matrix_sdk_common::{
encryption::DeviceKeys,
identifiers::{user_id, DeviceKeyAlgorithm},
};
fn device_keys() -> DeviceKeys {
let device_keys = json!({
@@ -572,10 +610,7 @@ pub(crate) mod test {
assert_eq!(device_id, device.device_id());
assert_eq!(device.algorithms.len(), 2);
assert_eq!(LocalTrust::Unset, device.local_trust_state());
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
assert_eq!("Alice's mobile phone", device.display_name().as_ref().unwrap());
assert_eq!(
device.get_key(DeviceKeyAlgorithm::Curve25519).unwrap(),
"xfgbLIC5WAl1OIkpOzoxpCe8FsRDT6nch7NQsOb15nc"
@@ -590,10 +625,7 @@ pub(crate) mod test {
fn update_a_device() {
let mut device = get_device();
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
assert_eq!("Alice's mobile phone", device.display_name().as_ref().unwrap());
let display_name = "Alice's work computer".to_owned();
+167 -256
View File
@@ -17,13 +17,15 @@ use std::{
convert::TryFrom,
sync::Arc,
};
use tracing::{info, trace, warn};
use matrix_sdk_common::{
api::r0::keys::get_keys::Response as KeysQueryResponse,
use futures::future::join_all;
use matrix_sdk_common::executor::spawn;
use ruma::{
api::client::r0::keys::get_keys::Response as KeysQueryResponse,
encryption::DeviceKeys,
identifiers::{DeviceId, DeviceIdBox, UserId},
};
use tracing::{trace, warn};
use crate::{
error::OlmResult,
@@ -32,41 +34,33 @@ use crate::{
UserIdentity, UserSigningPubkey,
},
requests::KeysQueryRequest,
session_manager::GroupSessionManager,
store::{Changes, DeviceChanges, IdentityChanges, Result as StoreResult, Store},
};
enum DeviceChange {
New(ReadOnlyDevice),
Updated(ReadOnlyDevice),
None,
}
#[derive(Debug, Clone)]
pub(crate) struct IdentityManager {
user_id: Arc<UserId>,
device_id: Arc<DeviceIdBox>,
group_manager: GroupSessionManager,
device_id: Arc<DeviceId>,
store: Store,
}
impl IdentityManager {
pub fn new(
user_id: Arc<UserId>,
device_id: Arc<DeviceIdBox>,
store: Store,
group_manager: GroupSessionManager,
) -> Self {
IdentityManager {
user_id,
device_id,
store,
group_manager,
}
const MAX_KEY_QUERY_USERS: usize = 250;
pub fn new(user_id: Arc<UserId>, device_id: Arc<DeviceId>, store: Store) -> Self {
IdentityManager { user_id, device_id, store }
}
fn user_id(&self) -> &UserId {
&self.user_id
}
fn device_id(&self) -> &DeviceId {
&self.device_id
}
/// Receive a successful keys query response.
///
/// Returns a list of devices newly discovered devices and devices that
@@ -80,18 +74,8 @@ impl IdentityManager {
&self,
response: &KeysQueryResponse,
) -> OlmResult<(DeviceChanges, IdentityChanges)> {
// TODO create a enum that tells us how the device/identity changed,
// e.g. new/deleted/display name change.
//
// TODO create a struct that will hold the device/identity and the
// change enum and return the struct.
//
// TODO once outbound group sessions hold on to the set of users that
// received the session, invalidate the session if a user device
// got added/deleted.
let changed_devices = self
.handle_devices_from_key_query(&response.device_keys)
.await?;
let changed_devices =
self.handle_devices_from_key_query(response.device_keys.clone()).await?;
let changed_identities = self.handle_cross_singing_keys(response).await?;
let changes = Changes {
@@ -100,11 +84,107 @@ impl IdentityManager {
..Default::default()
};
// TODO turn this into a single transaction.
self.store.save_changes(changes).await?;
let updated_users: Vec<&UserId> = response.device_keys.keys().collect();
for user_id in updated_users {
self.store.update_tracked_user(user_id, false).await?;
}
Ok((changed_devices, changed_identities))
}
async fn update_or_create_device(
store: Store,
device_keys: DeviceKeys,
) -> StoreResult<DeviceChange> {
let old_device =
store.get_readonly_device(&device_keys.user_id, &device_keys.device_id).await?;
if let Some(mut device) = old_device {
if let Err(e) = device.update_device(&device_keys) {
warn!(
"Failed to update the device keys for {} {}: {:?}",
device.user_id(),
device.device_id(),
e
);
Ok(DeviceChange::None)
} else {
Ok(DeviceChange::Updated(device))
}
} else {
match ReadOnlyDevice::try_from(&device_keys) {
Ok(d) => {
trace!("Adding a new device to the device store {:?}", d);
Ok(DeviceChange::New(d))
}
Err(e) => {
warn!(
"Failed to create a new device for {} {}: {:?}",
device_keys.user_id, device_keys.device_id, e
);
Ok(DeviceChange::None)
}
}
}
}
async fn update_user_devices(
store: Store,
own_user_id: Arc<UserId>,
own_device_id: Arc<DeviceId>,
user_id: UserId,
device_map: BTreeMap<DeviceIdBox, DeviceKeys>,
) -> StoreResult<DeviceChanges> {
let mut changes = DeviceChanges::default();
let current_devices: HashSet<DeviceIdBox> = device_map.keys().cloned().collect();
let tasks = device_map.into_iter().filter_map(|(device_id, device_keys)| {
// We don't need our own device in the device store.
if user_id == *own_user_id && *device_id == *own_device_id {
None
} else if user_id != device_keys.user_id || device_id != device_keys.device_id {
warn!(
"Mismatch in device keys payload of device {}|{} from user {}|{}",
device_id, device_keys.device_id, user_id, device_keys.user_id
);
None
} else {
Some(spawn(Self::update_or_create_device(store.clone(), device_keys)))
}
});
let results = join_all(tasks).await;
for device in results {
let device = device.expect("Creating or updating a device panicked")?;
match device {
DeviceChange::New(d) => changes.new.push(d),
DeviceChange::Updated(d) => changes.changed.push(d),
DeviceChange::None => (),
}
}
let current_devices: HashSet<&DeviceIdBox> = current_devices.iter().collect();
let stored_devices = store.get_readonly_devices(&user_id).await?;
let stored_devices_set: HashSet<&DeviceIdBox> = stored_devices.keys().collect();
let deleted_devices_set = stored_devices_set.difference(&current_devices);
for device_id in deleted_devices_set {
if let Some(device) = stored_devices.get(*device_id) {
device.mark_as_deleted();
changes.deleted.push(device.clone());
}
}
Ok(changes)
}
/// Handle the device keys part of a key query response.
///
/// # Arguments
@@ -116,78 +196,28 @@ impl IdentityManager {
/// they are new, one of their properties has changed or they got deleted.
async fn handle_devices_from_key_query(
&self,
device_keys_map: &BTreeMap<UserId, BTreeMap<DeviceIdBox, DeviceKeys>>,
device_keys_map: BTreeMap<UserId, BTreeMap<DeviceIdBox, DeviceKeys>>,
) -> StoreResult<DeviceChanges> {
let mut users_with_new_or_deleted_devices = HashSet::new();
let mut changes = DeviceChanges::default();
for (user_id, device_map) in device_keys_map {
// TODO move this out into the handle keys query response method
// since we might fail handle the new device at any point here or
// when updating the user identities.
self.store.update_tracked_user(user_id, false).await?;
let tasks = device_keys_map.into_iter().map(|(user_id, device_keys_map)| {
spawn(Self::update_user_devices(
self.store.clone(),
self.user_id.clone(),
self.device_id.clone(),
user_id,
device_keys_map,
))
});
for (device_id, device_keys) in device_map.iter() {
// We don't need our own device in the device store.
if user_id == self.user_id() && &**device_id == self.device_id() {
continue;
}
let results = join_all(tasks).await;
if user_id != &device_keys.user_id || device_id != &device_keys.device_id {
warn!(
"Mismatch in device keys payload of device {}|{} from user {}|{}",
device_id, device_keys.device_id, user_id, device_keys.user_id
);
continue;
}
for result in results {
let change_fragment = result.expect("Panic while updating user devices")?;
let device = self.store.get_readonly_device(&user_id, device_id).await?;
if let Some(mut device) = device {
if let Err(e) = device.update_device(device_keys) {
warn!(
"Failed to update the device keys for {} {}: {:?}",
user_id, device_id, e
);
continue;
}
changes.changed.push(device);
} else {
let device = match ReadOnlyDevice::try_from(device_keys) {
Ok(d) => d,
Err(e) => {
warn!(
"Failed to create a new device for {} {}: {:?}",
user_id, device_id, e
);
continue;
}
};
info!("Adding a new device to the device store {:?}", device);
users_with_new_or_deleted_devices.insert(user_id);
changes.new.push(device);
}
}
let current_devices: HashSet<&DeviceIdBox> = device_map.keys().collect();
let stored_devices = self.store.get_readonly_devices(&user_id).await?;
let stored_devices_set: HashSet<&DeviceIdBox> = stored_devices.keys().collect();
let deleted_devices_set = stored_devices_set.difference(&current_devices);
for device_id in deleted_devices_set {
users_with_new_or_deleted_devices.insert(user_id);
if let Some(device) = stored_devices.get(*device_id) {
device.mark_as_deleted();
changes.deleted.push(device.clone());
}
}
changes.extend(change_fragment);
}
self.group_manager
.invalidate_sessions_new_devices(&users_with_new_or_deleted_devices);
Ok(changes)
}
@@ -211,10 +241,7 @@ impl IdentityManager {
let self_signing = if let Some(s) = response.self_signing_keys.get(user_id) {
SelfSigningPubkey::from(s)
} else {
warn!(
"User identity for user {} didn't contain a self signing pubkey",
user_id
);
warn!("User identity for user {} didn't contain a self signing pubkey", user_id);
continue;
};
@@ -233,13 +260,11 @@ impl IdentityManager {
continue;
};
identity
.update(master_key, self_signing, user_signing)
.map(|_| (i, false))
identity.update(master_key, self_signing, user_signing).map(|_| (i, false))
}
UserIdentities::Other(ref mut identity) => {
identity.update(master_key, self_signing).map(|_| (i, false))
}
UserIdentities::Other(ref mut identity) => identity
.update(master_key, self_signing)
.map(|_| (i, false)),
}
} else if user_id == self.user_id() {
if let Some(s) = response.user_signing_keys.get(user_id) {
@@ -267,10 +292,7 @@ impl IdentityManager {
continue;
}
} else if master_key.user_id() != user_id || self_signing.user_id() != user_id {
warn!(
"User id mismatch in one of the cross signing keys for user {}",
user_id
);
warn!("User id mismatch in one of the cross signing keys for user {}", user_id);
continue;
} else {
UserIdentity::new(master_key, self_signing)
@@ -279,11 +301,7 @@ impl IdentityManager {
match result {
Ok((i, new)) => {
trace!(
"Updated or created new user identity for {}: {:?}",
user_id,
i
);
trace!("Updated or created new user identity for {}: {:?}", user_id, i);
if new {
changes.new.push(i);
} else {
@@ -291,10 +309,7 @@ impl IdentityManager {
}
}
Err(e) => {
warn!(
"Couldn't update or create new user identity for {}: {:?}",
user_id, e
);
warn!("Couldn't update or create new user identity for {}: {:?}", user_id, e);
continue;
}
}
@@ -305,7 +320,7 @@ impl IdentityManager {
/// Get a key query request if one is needed.
///
/// Returns a key query reqeust if the client should query E2E keys,
/// Returns a key query request if the client should query E2E keys,
/// otherwise None.
///
/// The response of a successful key query requests needs to be passed to
@@ -313,19 +328,19 @@ impl IdentityManager {
///
/// [`OlmMachine`]: struct.OlmMachine.html
/// [`receive_keys_query_response`]: #method.receive_keys_query_response
pub async fn users_for_key_query(&self) -> Option<KeysQueryRequest> {
let mut users = self.store.users_for_key_query();
pub async fn users_for_key_query(&self) -> Vec<KeysQueryRequest> {
let users = self.store.users_for_key_query();
if users.is_empty() {
None
Vec::new()
} else {
let mut device_keys: BTreeMap<UserId, Vec<Box<DeviceId>>> = BTreeMap::new();
let users: Vec<UserId> = users.into_iter().collect();
for user in users.drain() {
device_keys.insert(user, Vec::new());
}
Some(KeysQueryRequest::new(device_keys))
users
.chunks(Self::MAX_KEY_QUERY_USERS)
.map(|u| u.iter().map(|u| (u.clone(), Vec::new())).collect())
.map(KeysQueryRequest::new)
.collect()
}
}
@@ -373,23 +388,20 @@ impl IdentityManager {
#[cfg(test)]
pub(crate) mod test {
use std::{convert::TryFrom, sync::Arc};
use matrix_sdk_common::{
api::r0::keys::get_keys::Response as KeyQueryResponse,
identifiers::{room_id, user_id, DeviceIdBox, RoomId, UserId},
locks::Mutex,
};
use std::sync::Arc;
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_test::async_test;
use ruma::{
api::{client::r0::keys::get_keys::Response as KeyQueryResponse, IncomingResponse},
user_id, DeviceIdBox, UserId,
};
use serde_json::json;
use crate::{
identities::IdentityManager,
machine::test::response_from_file,
olm::{Account, PrivateCrossSigningIdentity, ReadOnlyAccount},
session_manager::GroupSessionManager,
olm::{PrivateCrossSigningIdentity, ReadOnlyAccount},
store::{CryptoStore, MemoryStore, Store},
verification::VerificationMachine,
};
@@ -406,28 +418,15 @@ pub(crate) mod test {
"WSKKLTJZCL".into()
}
fn room_id() -> RoomId {
room_id!("!test:localhost")
}
fn manager() -> IdentityManager {
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(user_id())));
let user_id = Arc::new(user_id());
let account = ReadOnlyAccount::new(&user_id, &device_id());
let store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(MemoryStore::new()));
let verification = VerificationMachine::new(account.clone(), identity.clone(), store);
let store = Store::new(
user_id.clone(),
identity,
Arc::new(Box::new(MemoryStore::new())),
verification,
);
let account = Account {
inner: account,
store: store.clone(),
};
let group = GroupSessionManager::new(account, store.clone());
IdentityManager::new(user_id, Arc::new(device_id()), store, group)
let store: Arc<dyn CryptoStore> = Arc::new(MemoryStore::new());
let verification = VerificationMachine::new(account, identity.clone(), store);
let store =
Store::new(user_id.clone(), identity, Arc::new(MemoryStore::new()), verification);
IdentityManager::new(user_id, device_id().into(), store)
}
pub(crate) fn other_key_query() -> KeyQueryResponse {
@@ -485,7 +484,8 @@ pub(crate) mod test {
},
"user_signing_keys": {}
}));
KeyQueryResponse::try_from(data).expect("Can't parse the keys upload response")
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the keys upload response")
}
pub(crate) fn own_key_query() -> KeyQueryResponse {
@@ -585,13 +585,14 @@ pub(crate) mod test {
}
}
}));
KeyQueryResponse::try_from(data).expect("Can't parse the keys upload response")
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the keys upload response")
}
#[async_test]
async fn test_manager_creation() {
let manager = manager();
assert!(manager.users_for_key_query().await.is_none())
assert!(manager.users_for_key_query().await.is_empty())
}
#[async_test]
@@ -601,10 +602,7 @@ pub(crate) mod test {
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 0);
manager
.receive_keys_query_response(&other_key_query())
.await
.unwrap();
manager.receive_keys_query_response(&other_key_query()).await.unwrap();
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 1);
@@ -615,12 +613,7 @@ pub(crate) mod test {
.await
.unwrap()
.unwrap();
let identity = manager
.store
.get_user_identity(&other_user)
.await
.unwrap()
.unwrap();
let identity = manager.store.get_user_identity(&other_user).await.unwrap().unwrap();
let identity = identity.other().unwrap();
assert!(identity.is_device_signed(&device).is_ok())
@@ -633,10 +626,7 @@ pub(crate) mod test {
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 0);
manager
.receive_keys_query_response(&other_key_query())
.await
.unwrap();
manager.receive_keys_query_response(&other_key_query()).await.unwrap();
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
assert_eq!(devices.devices().count(), 1);
@@ -647,88 +637,9 @@ pub(crate) mod test {
.await
.unwrap()
.unwrap();
let identity = manager
.store
.get_user_identity(&other_user)
.await
.unwrap()
.unwrap();
let identity = manager.store.get_user_identity(&other_user).await.unwrap().unwrap();
let identity = identity.other().unwrap();
assert!(identity.is_device_signed(&device).is_ok())
}
#[async_test]
async fn test_session_invalidation() {
let manager = manager();
let room_id = room_id();
let user_id = other_user_id();
let device_id: DeviceIdBox = "SKISMLNIMH".into();
manager
.group_manager
.create_outbound_group_session(&room_id, Default::default())
.await
.unwrap();
let session = manager
.group_manager
.get_outbound_group_session(&room_id)
.unwrap();
session.add_recipient(&user_id);
session.mark_as_shared();
assert!(!session.invalidated());
assert!(!session.expired());
// Receiving a new device invalidates the session.
manager
.receive_keys_query_response(&other_key_query())
.await
.unwrap();
assert!(session.invalidated());
manager
.group_manager
.create_outbound_group_session(&room_id, Default::default())
.await
.unwrap();
let session = manager
.group_manager
.get_outbound_group_session(&room_id)
.unwrap();
session.add_recipient(&user_id);
session.mark_as_shared();
assert!(!session.invalidated());
assert!(!session.expired());
let device = manager
.store
.get_device(&user_id, &device_id)
.await
.unwrap()
.unwrap();
assert!(!device.deleted());
let response = KeyQueryResponse::try_from(response_from_file(&json!({
"device_keys": {
user_id: {}
},
"failures": {},
})))
.unwrap();
// Noticing that a device got deleted invalidates the session as well
manager
.receive_keys_query_response(&response)
.await
.unwrap();
assert!(device.deleted());
assert!(session.invalidated());
}
}
+29 -5
View File
@@ -29,24 +29,48 @@
//!
//! ## User
//!
//! Cross-signing capable devices will upload 3 additional (master, self-signing,
//! user-signing) public keys which represent the user identity owning all the
//! devices. This is represented in two ways, as a `UserIdentity` for other
//! users and as `OwnUserIdentity` for our own user.
//! Cross-signing capable devices will upload 3 additional (master,
//! self-signing, user-signing) public keys which represent the user identity
//! owning all the devices. This is represented in two ways, as a `UserIdentity`
//! for other users and as `OwnUserIdentity` for our own user.
//!
//! This is done because the server will only give us access to 2 of the 3
//! additional public keys for other users, while it will give us access to all
//! 3 for our own user.
//!
//! Both identity sets need to reqularly fetched from the server using the
//! Both identity sets need to regularly fetched from the server using the
//! `/keys/query` API call.
pub(crate) mod device;
mod manager;
pub(crate) mod user;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
pub use device::{Device, LocalTrust, ReadOnlyDevice, UserDevices};
pub(crate) use manager::IdentityManager;
use serde::{Deserialize, Deserializer, Serializer};
pub use user::{
MasterPubkey, OwnUserIdentity, SelfSigningPubkey, UserIdentities, UserIdentity,
UserSigningPubkey,
};
// These methods are only here because Serialize and Deserialize don't seem to
// be implemented for WASM.
fn atomic_bool_serializer<S>(x: &AtomicBool, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let value = x.load(Ordering::SeqCst);
s.serialize_some(&value)
}
fn atomic_bool_deserializer<'de, D>(deserializer: D) -> Result<Arc<AtomicBool>, D::Error>
where
D: Deserializer<'de>,
{
let value = bool::deserialize(deserializer)?;
Ok(Arc::new(AtomicBool::new(value)))
}
+41 -66
View File
@@ -21,14 +21,14 @@ use std::{
},
};
use ruma::{
encryption::{CrossSigningKey, KeyUsage},
DeviceKeyId, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::to_value;
use matrix_sdk_common::{
api::r0::keys::{CrossSigningKey, KeyUsage},
identifiers::{DeviceKeyId, UserId},
};
use super::{atomic_bool_deserializer, atomic_bool_serializer};
#[cfg(test)]
use crate::olm::PrivateCrossSigningIdentity;
use crate::{error::SignatureError, olm::Utility, ReadOnlyDevice};
@@ -88,18 +88,21 @@ impl From<CrossSigningKey> for UserSigningPubkey {
}
}
#[allow(clippy::from_over_into)]
impl Into<CrossSigningKey> for MasterPubkey {
fn into(self) -> CrossSigningKey {
self.0.as_ref().clone()
}
}
#[allow(clippy::from_over_into)]
impl Into<CrossSigningKey> for UserSigningPubkey {
fn into(self) -> CrossSigningKey {
self.0.as_ref().clone()
}
}
#[allow(clippy::from_over_into)]
impl Into<CrossSigningKey> for SelfSigningPubkey {
fn into(self) -> CrossSigningKey {
self.0.as_ref().clone()
@@ -210,6 +213,14 @@ impl MasterPubkey {
self.0.keys.get(key_id.as_str()).map(|k| k.as_str())
}
/// Get the first available master key.
///
/// There's usually only a single master key so this will usually fetch the
/// only key.
pub fn get_first_key(&self) -> Option<&str> {
self.0.keys.values().map(|k| k.as_str()).next()
}
/// Check if the given cross signing sub-key is signed by the master key.
///
/// # Arguments
@@ -222,12 +233,7 @@ impl MasterPubkey {
&self,
subkey: impl Into<CrossSigningSubKeys<'a>>,
) -> Result<(), SignatureError> {
let (key_id, key) = self
.0
.keys
.iter()
.next()
.ok_or(SignatureError::MissingSigningKey)?;
let (key_id, key) = self.0.keys.iter().next().ok_or(SignatureError::MissingSigningKey)?;
let key_id = DeviceKeyId::try_from(key_id.as_str())?;
@@ -284,12 +290,7 @@ impl UserSigningPubkey {
&self,
master_key: &MasterPubkey,
) -> Result<(), SignatureError> {
let (key_id, key) = self
.0
.keys
.iter()
.next()
.ok_or(SignatureError::MissingSigningKey)?;
let (key_id, key) = self.0.keys.iter().next().ok_or(SignatureError::MissingSigningKey)?;
// TODO check that the usage is OK.
@@ -332,12 +333,7 @@ impl SelfSigningPubkey {
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub(crate) fn verify_device(&self, device: &ReadOnlyDevice) -> Result<(), SignatureError> {
let (key_id, key) = self
.0
.keys
.iter()
.next()
.ok_or(SignatureError::MissingSigningKey)?;
let (key_id, key) = self.0.keys.iter().next().ok_or(SignatureError::MissingSigningKey)?;
// TODO check that the usage is OK.
@@ -361,7 +357,7 @@ impl<'a> IntoIterator for &'a SelfSigningPubkey {
}
/// Enum over the different user identity types we can have.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UserIdentities {
/// Our own user identity.
Own(OwnUserIdentity),
@@ -469,37 +465,16 @@ impl UserIdentity {
) -> Result<Self, SignatureError> {
master_key.verify_subkey(&self_signing_key)?;
Ok(Self {
user_id: Arc::new(master_key.0.user_id.clone()),
master_key,
self_signing_key,
})
Ok(Self { user_id: Arc::new(master_key.0.user_id.clone()), master_key, self_signing_key })
}
#[cfg(test)]
pub async fn from_private(identity: &PrivateCrossSigningIdentity) -> Self {
let master_key = identity
.master_key
.lock()
.await
.as_ref()
.unwrap()
.public_key
.clone();
let self_signing_key = identity
.self_signing_key
.lock()
.await
.as_ref()
.unwrap()
.public_key
.clone();
let master_key = identity.master_key.lock().await.as_ref().unwrap().public_key.clone();
let self_signing_key =
identity.self_signing_key.lock().await.as_ref().unwrap().public_key.clone();
Self {
user_id: Arc::new(identity.user_id().clone()),
master_key,
self_signing_key,
}
Self { user_id: Arc::new(identity.user_id().clone()), master_key, self_signing_key }
}
/// Get the user id of this identity.
@@ -567,12 +542,16 @@ impl UserIdentity {
///
/// This identity can verify other identities as well as devices belonging to
/// the identity.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnUserIdentity {
user_id: Arc<UserId>,
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
user_signing_key: UserSigningPubkey,
#[serde(
serialize_with = "atomic_bool_serializer",
deserialize_with = "atomic_bool_deserializer"
)]
verified: Arc<AtomicBool>,
}
@@ -637,8 +616,7 @@ impl OwnUserIdentity {
/// Returns an empty result if the signature check succeeded, otherwise a
/// SignatureError indicating why the check failed.
pub fn is_identity_signed(&self, identity: &UserIdentity) -> Result<(), SignatureError> {
self.user_signing_key
.verify_master_key(&identity.master_key)
self.user_signing_key.verify_master_key(&identity.master_key)
}
/// Check if the given device has been signed by this identity.
@@ -710,6 +688,11 @@ impl OwnUserIdentity {
pub(crate) mod test {
use std::{convert::TryFrom, sync::Arc};
use matrix_sdk_common::locks::Mutex;
use matrix_sdk_test::async_test;
use ruma::{api::client::r0::keys::get_keys::Response as KeyQueryResponse, user_id};
use super::{OwnUserIdentity, UserIdentities, UserIdentity};
use crate::{
identities::{
manager::test::{other_key_query, own_key_query},
@@ -720,13 +703,6 @@ pub(crate) mod test {
verification::VerificationMachine,
};
use matrix_sdk_common::{
api::r0::keys::get_keys::Response as KeyQueryResponse, identifiers::user_id, locks::Mutex,
};
use matrix_sdk_test::async_test;
use super::{OwnUserIdentity, UserIdentities, UserIdentity};
fn device(response: &KeyQueryResponse) -> (ReadOnlyDevice, ReadOnlyDevice) {
let mut devices = response.device_keys.values().next().unwrap().values();
let first = ReadOnlyDevice::try_from(devices.next().unwrap()).unwrap();
@@ -784,13 +760,12 @@ pub(crate) mod test {
assert!(identity.is_device_signed(&first).is_err());
assert!(identity.is_device_signed(&second).is_ok());
let private_identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(
second.user_id().clone(),
)));
let private_identity =
Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(second.user_id().clone())));
let verification_machine = VerificationMachine::new(
ReadOnlyAccount::new(second.user_id(), second.device_id()),
private_identity.clone(),
Arc::new(Box::new(MemoryStore::new())),
Arc::new(MemoryStore::new()),
);
let first = Device {
@@ -833,10 +808,10 @@ pub(crate) mod test {
let verification_machine = VerificationMachine::new(
ReadOnlyAccount::new(device.user_id(), device.device_id()),
id.clone(),
Arc::new(Box::new(MemoryStore::new())),
Arc::new(MemoryStore::new()),
);
let public_identity = identity.as_public_identity().await.unwrap();
let public_identity = identity.to_public_identity().await.unwrap();
let mut device = Device {
inner: device,

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