Compare commits

..

1384 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ć 4f2cad8f62 matrix-sdk: Bump our versions
CI / Check style (push) Failing after 38s
CI / Run clippy (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
CI / windows / stable-x86_64-msvc (push) Has been skipped
2021-01-05 11:23:18 +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ć 8924865c9c crypto: Fix a couple of new clippy warnings. 2021-01-04 17:39:40 +01:00
Damir Jelić c6a80dc921 Merge branch 'master' into new-state-store 2021-01-04 17:34:33 +01:00
Damir Jelić 60950044f2 matrix-sdk: Bump our deps. 2021-01-04 17:22:09 +01:00
Damir Jelić 4c6c1d2107 matrix-sdk: Get rid of the common macros crate
This crate was used to support different trait bounds on WASM vs other
targets, since we only define async traits in a couple of places having
a whole crate to support this feels a bit excessive.

This patch defines a target specific super trait instead, this lowers
the compile time a couple of seconds.
2021-01-04 16:34:14 +01:00
Damir Jelić 2e3b6fba7d common: Use the re-exported versions of js_int and assign 2021-01-04 15:29:49 +01:00
Damir Jelić de51291166 common: Remove the direct dep to js_int now that Ruma re-exports it 2021-01-04 15:13:48 +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ć d84a852ae9 matrix-sdk: Bump ruma to a released version. 2021-01-04 14:06:07 +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ć 9245b2a89a crypto: Properly canonicalize the json when verifying signatures as well. 2020-12-22 15:45:42 +01:00
Damir Jelić d39e3141fc crypto: Use CanonicalJsonValue for all the signature calculations. 2020-12-22 14:12:57 +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
Alex Black d4327d4cfc EventEmitter: add VoIP event support (m.call.* event types)
Signed-off-by: Alex Black <blacka101@gmail.com>
2020-12-17 00:19:37 +11: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ć d9e5a17ab0 crypto: Use a native Rust sha2 implementation to calculate the commitment 2020-12-08 16:21:29 +01:00
Damir Jelić b5c61af472 crypto: Move the base64 helpers into a common module. 2020-12-08 16:21:29 +01:00
Damir Jelić fd705b7d5e crypto: Canonicalize the start event content before calculating the commitment
This fixes: #117.
2020-12-08 16:02:51 +01:00
Damir Jelić 8e53982bcd Merge branch 'master' into crypto-improvements 2020-12-08 15:06:14 +01:00
Damir Jelić ca4e738fff Merge branch 'master' into user-avatar-ci 2020-12-08 14:43:46 +01:00
Damir Jelić 594e9b9e2d README: Swap out the CI badge. 2020-12-08 14:31:14 +01:00
Damir Jelić 15b87e9dc1 CI: Restrict code coverage to the master branch. 2020-12-08 14:30:49 +01:00
Damir Jelić fa3583234c CI: Fix the coverage yml. 2020-12-08 14:09:51 +01:00
Damir Jelić 9679db6ddc CI: Enable code coverage again. 2020-12-08 14:05:25 +01:00
Damir Jelić fdf48d1f30 CI: Rename the workflow file. 2020-12-08 13:58:26 +01:00
Damir Jelić 40d13d9b59 cyrpto: Another timing based test that only works on Linux. 2020-12-08 13:37:55 +01:00
Damir Jelić 4ab6ae7f30 crypto: Fix an os_target definition. 2020-12-08 13:15:19 +01:00
Damir Jelić c8dd6bfd26 crypto: Scope the imports for the unwedging test into the test. 2020-12-08 12:56:16 +01:00
Damir Jelić 0a76f75c22 Revert "CI: Use the cargo-clippy workflow."
This reverts commit 795c1225dd.
2020-12-08 12:45:16 +01:00
Damir Jelić 795c1225dd CI: Use the cargo-clippy workflow. 2020-12-08 12:37:39 +01:00
Damir Jelić b982d36303 crypto: Run the time sensitive tests only on linux. 2020-12-08 12:34:59 +01:00
Damir Jelić a80aa4c2ad base: Fix some lint issues. 2020-12-08 12:11:55 +01:00
Damir Jelić 27d9cf04de base: Remove a flaky state store test.
The state store is undergoing a rewrite and this test fails more often
than i would like making our CI seem flaky.

Remove the test since it's going to become obsolete anyways.
2020-12-08 11:52:21 +01:00
Damir Jelić e4779163b8 CI: Run the tests on the CI. 2020-12-08 11:36:01 +01:00
Damir Jelić 2cc899338a CI: Split out the fmt and clippy runs into two jobs. 2020-12-08 11:21:09 +01:00
Amanda Graven 8dc56ec332 Add methods for setting, getting and uploading avatar 2020-12-08 11:18:00 +01:00
Damir Jelić 1d5ee22dd2 CI: Name our lint stages. 2020-12-08 11:08:41 +01:00
Damir Jelić 59917f45e3 matrix-sdk: Fix a clippy lint. 2020-12-08 11:01:20 +01:00
Damir Jelić 35247fac2a crypto: Fix a lint issue. 2020-12-08 10:50:58 +01:00
Damir Jelić e24fb8b471 CI: Fix the cargo fmt invocation. 2020-12-08 10:44:19 +01:00
Damir Jelić 795900cf39 CI: Add a lint github workflow. 2020-12-08 10:42:05 +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
Amanda Graven bca7f41ca9 Fix error in example 2020-12-07 13:14:23 +01:00
Amanda Graven 7f503eb71c Add examples, remove user from method names 2020-12-07 12:59:10 +01:00
Amanda Graven a26dc3179a Add methods for getting and setting display name 2020-12-07 11:17:26 +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ć aa1a64628f crypto: Remove a bunch of unneeded whitespace in a log line. 2020-12-05 14:59:40 +01:00
Damir Jelić 0e66640b9f crypto: Log both user id versions when the device keys mismatch. 2020-12-05 14:59:40 +01:00
Damir Jelić 3f41e5071b crypto: Preserve the relationship info while decrypting events. 2020-12-05 14:59:40 +01:00
Damir Jelić 9eb17e757c matrix-sdk: Update ruma. 2020-12-05 14:59:40 +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ć 804bd221b2 crypto: Improve key imports.
This patch changes so key imports load all existing sessions at once
instead loading a single session for each session we are importing. It
removes the need to lock the session when we check the first known index
and exposes the total number of sessions the key export contained.
2020-12-02 11:12:46 +01:00
Damir Jelić 45442dfac8 Merge branch 'master' into new-state-store 2020-12-01 17:24:00 +01:00
Damir Jelić e20b1efae9 crypto: Store private identities and accounts with the Changes struct as well. 2020-12-01 17:14:32 +01:00
Damir Jelić e65915e159 Merge branch 'crypto-improvements' 2020-12-01 15:10:58 +01:00
Damir Jelić 4800e80492 matrix-sdk: Remove an unused import. 2020-12-01 15:08:53 +01:00
Damir Jelić 5d0ff961b2 crypto: Check the Olm message hash if we fail to decrypt an Olm message.
Wether by accident (the next_batch token doesn't get stored properly) or
by malicious intent (the server replays a message) an Olm encrypted to-device
message may appear multiple times.

This is usually fine since nothing bad happens, we don't decrypt the message
and the message gets thrown away.

Since the introduction of Olm session unwedging an undecryptable message
leads to the creation of a new fresh Olm session. To avoid this we
remember which Olm messages we already decrypted so they don't trigger
an unwedging dance.
2020-12-01 14:50:04 +01:00
Damir Jelić 270350cd34 crypto: Save the olm message hash. 2020-12-01 14:38:03 +01:00
Damir Jelić ae2391791d crypto: Use a released sqlx version. 2020-12-01 13:25:51 +01:00
Damir Jelić 24592adbba crypto: Return a higher level struct when decrypting olm messages instead of tuples 2020-12-01 12:41:11 +01:00
Damir Jelić efe659910f crypto: Remove some stale TODOs. 2020-12-01 11:20:55 +01:00
Damir Jelić 08babb6d6c crypto: Document the new cross signing methods in the store. 2020-12-01 10:54:41 +01:00
Damir Jelić 50bd408d48 matrix-sdk: Don't use try_from for the u32 -> UInt conversion. 2020-12-01 10:34:10 +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ć ce4d53a88c examples: Feature gate the cross signing bootstrap example. 2020-11-26 14:27:11 +01:00
Damir Jelić 7e9baf2707 crypto: Remove some dead code definitions. 2020-11-26 14:15:52 +01:00
Damir Jelić 43ced3d279 Merge branch 'crypto-improvements' into new-state-store 2020-11-26 14:08:17 +01:00
Damir Jelić 3073883076 crypto: Fix a clippy warning. 2020-11-26 14:02:35 +01:00
Damir Jelić baa5bed1c9 Merge branch 'crypto-improvements' into new-state-store 2020-11-26 14:00:18 +01:00
Damir Jelić 7ec5a5ad1a Merge branch 'master' into crypto-improvements 2020-11-26 13:24:57 +01:00
Jonas Platte 0422bae924 Fix clippy lint rc_buffer 2020-11-25 19:01:28 +01:00
Jonas Platte 27ecab8574 Update ruma 2020-11-25 19:01:28 +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
Jonas Platte 5ca66a6985 Upgrade ruma dependency 2020-11-23 15:34:38 +01:00
Jonas Platte 2e387436cf Remove unstable-synapse-quirks from default feature set for ruma
otherwise there is no point in exposing that feature
2020-11-23 15:27:43 +01:00
Jonas Platte 591f031246 Don't disable default-features on ruma
there are no features that are active by default, this is a no-op.
2020-11-23 14:58:48 +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ć dedb1eb745 Merge branch 'update-ruma' 2020-11-20 21:21:24 +01:00
Damir Jelić c40edcf2fc matrix-sdk: Try to lower our compile times, at least in the crypto part for now. 2020-11-20 20:35:48 +01:00
Damir Jelić 6509e72a74 Revert "base: Don't handle the wildcard case for member events anymore."
Using the exhaustive feature in ruma enables the appservice/federation
apis, adding some 10 more crates to our dependencies. Disable that
feature for now.

This reverts commit 41529a6bff.
2020-11-20 20:35:48 +01:00
Jonas Platte 38fec7f2b3 Upgrade ruma 2020-11-20 20:35:48 +01:00
Damir Jelić 9edf8657d0 base: WIP lazy loading support. 2020-11-20 20:17:59 +01:00
Alejandro Domínguez 95243003c4 Update ruma 2020-11-20 20:14:18 +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ć 11fcf5c42f rust-sdk: Document the cross signing bootstrap method. 2020-11-05 14:33:45 +01:00
Damir Jelić b27f1b0e34 crypto: Fix some clippy warnings. 2020-10-30 14:38:29 +01:00
Damir Jelić b67cd4ddd2 crypto: Create a trusted public cross signing identity when we create a private one. 2020-10-30 13:21:14 +01:00
Damir Jelić 44cc1cef71 crypto: Let devices hold on to the private identity. 2020-10-30 11:41:48 +01:00
Damir Jelić 34bec59389 crypto: Hold on to the private identity in the store. 2020-10-30 11:34:55 +01:00
Damir Jelić cb95f576a5 crypto: Clear out the signatures when signing a device.
This avoids re-uploading all the existing signatures.
2020-10-29 15:37:29 +01:00
Damir Jelić 5c530cf9ee crypto: Upload signatures after verification is done. 2020-10-27 16:39:23 +01:00
Damir Jelić 30a78bb1d6 crypto: Add the private identity to the Sas object. 2020-10-27 14:21:22 +01:00
Damir Jelić 2077ea0ddf crypto: Split out the device_key signing method. 2020-10-27 13:48:51 +01:00
Damir Jelić e757d605f5 crypto: Allow users to be signed as well. 2020-10-27 13:29:19 +01:00
Damir Jelić 61a5293af5 cyrpto: Document the signing module. 2020-10-26 16:03:59 +01:00
Damir Jelić 6e83a4bbca crypto: Split out the signing module into two files. 2020-10-26 16:03:59 +01:00
Damir Jelić 5c14910126 crypto: WIP cross signing bootstrap. 2020-10-26 16:03:59 +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ć 8ed1e37cef crypto: Save the account if we create a new one. 2020-10-23 11:17:37 +02:00
Damir Jelić 5fd004bae5 crypto: Connect the private identity to the verification machine. 2020-10-23 11:17:13 +02:00
Damir Jelić 9ce7feea1a base: Wip. 2020-10-23 09:39:08 +02:00
Damir Jelić 7de002b128 crypto: Fix some lint issues. 2020-10-22 16:40:05 +02:00
Damir Jelić f60dc7ed78 crypto: Allow cross signing identities to be stored/restored. 2020-10-22 16:25:25 +02:00
Damir Jelić bdf32eecc7 base: More work on the new state store. 2020-10-22 09:46:12 +02:00
Damir Jelić 78d7f6c10b crypto: Fix a clippy issue. 2020-10-21 17:05:36 +02:00
Damir Jelić fa25ca4475 crypto: Make the pickle key encryption future proof. 2020-10-21 16:52:40 +02:00
Damir Jelić c9db63509f crypto: Add error handling to the signing module. 2020-10-21 16:24:10 +02:00
Damir Jelić ac0df5dea9 crypto: Properly handle errors in the pickle key decryption. 2020-10-21 15:28:43 +02:00
Damir Jelić d175c47a05 crypto: Use a random pickle key in the sqlite store. 2020-10-21 15:13:21 +02:00
Damir Jelić 959e8450af crypto: Use a transaction to create sqlite tables. 2020-10-21 14:01:27 +02:00
Damir Jelić dd0642cd59 crypto: Add a pickle key struct. 2020-10-21 13:21:22 +02:00
Damir Jelić 6a7da5a8b6 crypto: Correctly generate a random nonce for pickling of the signing objects. 2020-10-21 12:55:45 +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
Damir Jelić 7cab7cadc9 crypto: Rework the cryptostore.
This modifies the cryptostore and storage logic in two ways:
    * The cryptostore trait has only one main save method.
    * The receive_sync method tries to save all the objects in one
    `save_changes()` call.

This means that all the changes a sync makes get commited to the store
in one transaction, leaving us in a consistent state.

This also means that we can pass the Changes struct the receive sync
method collects to our caller if the caller wishes to store the room
state and crypto state changes in a single transaction.
2020-10-20 17:19:37 +02:00
Damir Jelić 728d80ed06 crypto: Connect the cross signing to the main state machine. 2020-10-19 16:03:01 +02:00
Damir Jelić 0c1d33d43f Merge branch 'master' into crypto-improvements 2020-10-18 10:21:52 +02:00
Damir Jelić 8f99180c99 Merge branch 'up-ruma' into master 2020-10-18 10:17:43 +02:00
Jonas Platte 0682292b91 Upgrade ruma 2020-10-18 02:01:39 +02:00
Damir Jelić 404cc410cc crypto: Fix the docs and return value of the import_keys method. 2020-10-17 14:39:19 +02:00
Damir Jelić 17cc4fcb81 matrix-sdk: Fix an import for the non-crypto case. 2020-10-16 19:45:38 +02:00
Damir Jelić 93f49265a6 crypto: Use a git version of sqlx.
The beta release has a nasty bug where one thread would consume 100% of
CPU.
2020-10-16 19:42:41 +02:00
Damir Jelić b1c8c64205 matrix-sdk: Add support to delete devices. 2020-10-16 17:27:00 +02:00
Damir Jelić 425a07d670 crypto: Don't load all the devices in the sqlite store. 2020-10-16 16:57:26 +02:00
Damir Jelić 4262f1d3b0 crypto: Don't cache inbound group sessions in the sqlite store. 2020-10-16 15:54:50 +02:00
Damir Jelić b5560d3cb6 crypto: More transactions in the sqlite store. 2020-10-16 15:23:34 +02:00
Damir Jelić fc54c63a4c crypto: Upgrade sqlx to the beta release.
This change is much needed to enable transactions in our sqlite store,
before this release creating a transaction would take ownership of the
connection, now it just mutably borrows it.
2020-10-16 15:05:53 +02:00
Damir Jelić e7a24d5e68 crypto: Move the session managers under a common module. 2020-10-16 11:09:55 +02:00
Damir Jelić b5c9473424 crypto: Test the session unwedging logic. 2020-10-15 15:03:22 +02:00
Damir Jelić 59d7b53242 crypto: Add an user for a key request if the device was marked as wedged. 2020-10-15 15:02:02 +02:00
Damir Jelić 59a7199202 crypto: Initial test for the session manager. 2020-10-15 13:58:35 +02:00
Damir Jelić d1313b8614 crypto: Fix another clippy warning. 2020-10-14 16:15:26 +02:00
Damir Jelić 4e8ce4cb5d crypto: Fix clippy warnings and don't use the PickleMode for signing pickling. 2020-10-14 16:01:52 +02:00
Damir Jelić c85fe6bc21 crypto: Initial support for private cross signing identities. 2020-10-14 15:35:06 +02:00
Damir Jelić 3338ecf62a Merge branch 'master' into crypto-improvements 2020-10-13 13:02:02 +02:00
Damir Jelić 1c6a67d864 matrix-sdk: Bump our deps. 2020-10-13 13:01:18 +02:00
Damir Jelić e737200fbe matrix-sdk: Remove an useless import. 2020-10-13 11:55:18 +02:00
Damir Jelić 41b3b0651f matrix-sdk: Don't use strings for the content type in the upload methods. 2020-10-13 11:00:52 +02:00
Damir Jelić 1cabc0cac9 crypto: Correctly store the uploaded key count when saving the account.
This fixes: #101.
2020-10-13 09:47:49 +02:00
Denis Kasak e0ee03fa6f Update room_messages docstring to reflect actual state of the API. 2020-10-12 21:18:51 +02:00
Denis Kasak 4ec7aff301 Add documentation on enabling logging. 2020-10-12 20:45:47 +02:00
Denis Kasak f5ff91935f Add missing apostrophe. 2020-10-12 20:45:47 +02:00
Denis Kasak 7519bec9a3 Tweak descriptions given by Describe impls. 2020-10-12 20:45:47 +02:00
Damir Jelić bf7070b8f2 Merge branch 'client-get-session' into master 2020-10-12 15:45:55 +02:00
Damir Jelić 39fc33d37a Merge branch 'ser-deser-session' into master 2020-10-12 15:36:32 +02:00
Denis Kasak d81a6e6872 cargo fmt 2020-10-12 15:17:46 +02:00
Denis Kasak 2afc0c7661 Implement BaseClient::get_session to retrieve the login session.
Closes #100.
2020-10-12 15:12:23 +02:00
Denis Kasak f349811020 Add Serialize/Deserialize impls for matrix_sdk::Session. 2020-10-12 14:56:08 +02:00
Damir Jelić 0ab12fe969 examples: Add some whitespace to the autojoin example. 2020-10-12 12:50:03 +02:00
strct c4ac0d0570 matrix-sdk: example: retry autojoin 2020-10-10 16:17:08 +02:00
Damir Jelić d039a39d84 common: Expose the lock guards publicly. 2020-10-10 14:13:28 +02:00
Damir Jelić 1c008f549c common: Expose the lock guards publicly. 2020-10-10 12:08:58 +02:00
Damir Jelić bd0ac703a0 crypto: Initial logic for session unwedging. 2020-10-09 15:39:35 +02:00
Damir Jelić 6d2e9cfc02 crypto: Share the users_for_key_claim map between modules. 2020-10-09 11:36:31 +02:00
Damir Jelić 661f182382 Merge branch 'master' into crypto-improvements 2020-10-08 18:28:04 +02:00
Dominique Martinet 9ea4835e3e Revert "matrix_sdk examples: set #![type_length_limit = "1075569"]"
This reverts commit 7d023ebdb9.

Rust 1.47.0 got released and the tuning is no longer necessary.
2020-10-08 17:57:23 +02:00
Dominique Martinet 2602c36ad0 matrix_sdk_base: save room states after successfully parsed account events 2020-10-08 16:10:58 +02:00
Dominique Martinet d858940342 matrix_sdk_base: handle response.account_data events
"m.direct" events are not in room account data events but in main one
2020-10-08 16:10:58 +02:00
Dominique Martinet 883183324f matrix_sdk_base: room: add direct_target field
Rooms marked as "direct" are associated a user_id in "m.direct" events.
Clients could want to handle these separately
2020-10-08 16:10:58 +02:00
Dominique Martinet 7d023ebdb9 matrix_sdk examples: set #![type_length_limit = "1075569"]
Some examples no longer build after the following commits, set a
bigger-than-default type_length_limit to let tests pass.

The exceptions are not necessary on nightly and can be removed again
after https://github.com/rust-lang/rust/issues/54540 is fixed.
2020-10-08 16:10:48 +02:00
Damir Jelić 473e49252e crytpo: Get the session from the list of sessions in a safe manner. 2020-10-08 15:56:17 +02:00
Damir Jelić d96c9f85a1 crypto: Add doces for the get_missing_sessions method. 2020-10-08 14:50:35 +02:00
Damir Jelić 279ce0bba0 crypto: Split out the Olm session handling logic into a separate module. 2020-10-08 14:41:34 +02:00
Damir Jelić da5ef42719 crypto: Log when we invalidate a group session. 2020-10-08 14:03:01 +02:00
Dominique Martinet a4eae1053c matrix_sdk: expose RoomMember 2020-10-08 13:16:33 +02:00
Dominique Martinet f7039d9a8d matrix_sdk_base: expose RoomMember 2020-10-08 13:16:33 +02:00
Damir Jelić 723fdeaa06 crypto: Fix a clippy warning. 2020-10-08 12:59:10 +02:00
Damir Jelić 19d513e3c0 crypto: Simplify and test the group session invalidation logic. 2020-10-08 12:40:42 +02:00
Damir Jelić 23ac00c8ec crypto: Initial support for group session invalidation. 2020-10-08 11:16:02 +02:00
Damir Jelić 4019ebf121 crypto: Fix some clippy warnings. 2020-10-07 17:56:29 +02:00
Damir Jelić 220ccfb52b matrix-sdk: Fix the arguments docs for sync_with_callback. 2020-10-07 15:26:44 +02:00
Damir Jelić 9a838abd67 crypto: Log when we're not serving a key request because of a missing session. 2020-10-07 14:22:13 +02:00
Damir Jelić 17d23eb9e5 matrix-sdk: Add automatic key claiming support. 2020-10-07 14:07:47 +02:00
Damir Jelić 8ea0035cd0 crypto: Add the automatic key claim users to the key claim request. 2020-10-07 14:02:50 +02:00
Damir Jelić 06b9c71dbc crypto: Refactor out the key share wait queue. 2020-10-07 12:42:39 +02:00
Damir Jelić 6a8ac62a51 crypto: Remove an unwrap. 2020-10-07 11:57:46 +02:00
Damir Jelić 1e894269c8 crypto: Correctly handle the key share without a session and test it. 2020-10-07 11:57:09 +02:00
Damir Jelić 27c6f30e0f Merge branch 'master' into crypto-improvements 2020-10-06 16:44:11 +02:00
Damir Jelić bc48674f9f Merge branch 'new-sync-methods' into master 2020-10-06 16:43:41 +02:00
Damir Jelić e5f0f64405 crypto: Initial scaffolding for key shares for devices that are missing a session. 2020-10-06 16:38:42 +02:00
Damir Jelić 09f4b07fb7 matrix-sdk: Use a specific version for async-std. 2020-10-06 15:17:45 +02:00
Damir Jelić 2ffac286ed matrix-sdk: Switch to using an enum for the sync loop callback return value. 2020-10-06 15:04:43 +02:00
Damir Jelić 83b48fb53c matrix-sdk: Fix the login example. 2020-10-06 12:43:59 +02:00
Damir Jelić f4137c6bba Merge branch 'master' into crypto-improvements 2020-10-06 12:23:04 +02:00
Damir Jelić e16b7f9c44 matrix-sdk: Add an example for the login method. 2020-10-06 12:01:47 +02:00
Damir Jelić 45953a268c matrix-sdk: Mention that the key import/export methods don't work on WASM. 2020-10-06 11:41:18 +02:00
Damir Jelić 84039ad7aa matrix-sdk: Add links from the login method docs to the restore_login ones. 2020-10-06 11:40:32 +02:00
Damir Jelić 137fa9619f matrix-sdk: Add the ability to stop the sync loop and rename the sync methods.
This renames our sync methods so it's clearer which one the main one is.
Syncing should be done with the sync method, if one wishes to sync only
once the sync_method is provided.

If one wishes to have a callback called with every sync the
sync_with_callback method exists, the callback now returns a boolean
that signals if the loop should be aborted. This does not mean that the
current sync request will abort, a cancelable future is still needed for
this.
2020-10-06 11:37:29 +02:00
Damir Jelić e3d24f5c31 crypto: Fix some clippy warnings. 2020-10-01 16:45:13 +02:00
Damir Jelić 02c765f903 crypto: Don't mark outbound group sessions automatically as shared. 2020-10-01 16:31:24 +02:00
Damir Jelić fc6ff2c78a crytpo: Remove an unneeded map/clone. 2020-10-01 12:46:09 +02:00
Damir Jelić bcdcdeb259 Merge branch 'master' into crypto-improvements 2020-10-01 12:21:45 +02:00
Damir Jelić 1d8f01ef11 crypto: Remove the third Device variant. 2020-10-01 12:15:13 +02:00
Alejandro Domínguez b58d88e0c3 Upgrade ruma 2020-10-01 11:23:26 +02:00
Damir Jelić c8ca93c924 crytpo: Let the verification machine hold on to a raw CryptoStore.
This will later be useful when our higher level store wrapper holds on
to a verification machine to return higher level Device objects.
2020-10-01 11:17:27 +02:00
Damir Jelić d644af7be9 crypto: Remove an unneeded clone. 2020-10-01 09:56:22 +02:00
Damir Jelić ff2079da91 crypto: Move the group session handling logic into separate module. 2020-09-30 15:43:25 +02:00
Damir Jelić 646f18ae18 crypto: Remove an unused import. 2020-09-29 17:53:11 +02:00
Damir Jelić 2b8d4a21a4 crypto: Connect the key request handling to the main state machine. 2020-09-29 17:40:06 +02:00
Damir Jelić 78badd9af8 crypto: Use the correct event type when sending out forwarded room keys. 2020-09-29 17:36:56 +02:00
Damir Jelić 58aef51770 crypto: Remove an unneeded mutable borrow. 2020-09-29 14:44:18 +02:00
Damir Jelić 8fe1eda169 crypto: Test the full key share flow. 2020-09-29 14:18:03 +02:00
Damir Jelić 84066d4a76 crypto: Split out the Account into a read only portion and one with effects. 2020-09-29 12:03:41 +02:00
Damir Jelić e1c220e2f7 crypto: Test a key share cycle. 2020-09-29 10:24:54 +02:00
Damir Jelić 798656dac5 crypto: Allow the key request machine to access the outbound group sessions. 2020-09-29 10:09:47 +02:00
Damir Jelić 721c459577 crypto: Collapse an if tree. 2020-09-28 15:07:57 +02:00
Damir Jelić 23173c4a1e crypto: Test our key sharing decision logic. 2020-09-28 14:51:57 +02:00
Damir Jelić 4a8c5ebab0 crypto: Return an enum that describes why we won't serve a key share request. 2020-09-28 14:12:08 +02:00
Damir Jelić e29508938b crypto: More work on the incoming key request handling. 2020-09-28 13:32:30 +02:00
Damir Jelić a357536ade crypto: Initial scaffolding for incoming key share handling. 2020-09-28 09:27:16 +02:00
Damir Jelić f3be27921c crypto: Move the device trust state logic into the read only device. 2020-09-24 12:45:23 +02:00
Damir Jelić 42c4cf2a30 crypto: Test the outgoing requests method instead of accessing the field. 2020-09-24 12:00:22 +02:00
Damir Jelić c5bece2d58 crypto: Zeroize and remove the session key copies for forwarded room keys. 2020-09-24 11:18:01 +02:00
Damir Jelić 4662ca2e32 crypto: Refactor the one-time key count update logic. 2020-09-24 11:16:15 +02:00
Damir Jelić 5a86b067e4 crypto: Add tests for the identity manager. 2020-09-23 15:45:25 +02:00
Damir Jelić 9a5345ec77 Merge branch 'aledomu-master' into master 2020-09-23 13:45:44 +02:00
Damir Jelić 7c3e751d6e Merge branch 'crypto-improvements' into master 2020-09-23 11:07:49 +02:00
Alejandro Domínguez 3070c98d26 Export "unstable-synapse-quirks" feature from ruma 2020-09-22 21:03:12 +02:00
Damir Jelić 95e906e0dc crypto: Save the account if the one-time key count updates. 2020-09-18 20:50:32 +02:00
Damir Jelić 2e3d30d7b4 crypto: Move the identity/device management logic into a separate struct. 2020-09-18 20:50:32 +02:00
Damir Jelić 5b0457dad0 crypto: Remember the users that received the outbound group session. 2020-09-18 18:55:17 +02:00
Damir Jelić a183584541 crypto: Test that we correctly check the hash when decrypting attachments. 2020-09-18 17:49:44 +02:00
Damir Jelić 562bb5aee3 crypto: Remove some dead key requests code for now. 2020-09-18 17:26:56 +02:00
Damir Jelić dea3e4adf4 crypto: Document when a key export may panic. 2020-09-18 14:04:39 +02:00
Damir Jelić 5d5d5bb141 crypto: Hook up the key requesting to the main state machine. 2020-09-18 13:50:13 +02:00
Damir Jelić c58cf71be1 crypto: Send out key request cancellations once we receive a key. 2020-09-18 13:49:46 +02:00
Damir Jelić af4b00195b crypto: Implement the key/value store for the sqlite store. 2020-09-18 13:42:51 +02:00
Damir Jelić 41529a6bff base: Don't handle the wildcard case for member events anymore. 2020-09-17 17:31:17 +02:00
Damir Jelić 300b03bd9e crypto: Add more test for the outgoing key requests. 2020-09-17 17:13:42 +02:00
Damir Jelić a5b195efc7 crypto: Initial tests for the key requests state machine. 2020-09-17 16:55:33 +02:00
Damir Jelić 692f9baa0e crypto: Add logic to handle outgoing key requests. 2020-09-17 16:09:08 +02:00
Damir Jelić 6b24d91ed9 crypto: Add an initial version of our key request state machine. 2020-09-17 14:16:43 +02:00
Damir Jelić 24ce4881c7 crypto: Add a method to save/load arbitrary objects from a CryptoStore.
This actually adds trait methods that save/load strings from the
CryptoStore. We add a wrapper for the CryptoStore since we can't mix
trait objects and generics, so we add generic methods to save/load
anything that implements Serialize/Deserialize.
2020-09-16 16:03:19 +02:00
Damir Jelić 849934b180 crypto: Use a constant for the attachment encryption version. 2020-09-16 12:39:23 +02:00
Damir Jelić 428b28a985 matrix-sdk: Increase the type length limit for the wasm example. 2020-09-16 12:28:42 +02:00
Damir Jelić 95145fae8f matrix-sdk: Remove the example with encrypted uploads.
The example fail to build on platforms where we don't support encryption. So
remove the example for now.
2020-09-16 12:09:30 +02:00
Damir Jelić ae894e0ff6 crypto: Finish up the attachment encryption.
This adds docs and proper error handling to the attachment encryption.
Zeroing out the key buffers is added as well.
2020-09-16 12:05:44 +02:00
Damir Jelić 86d95518be matrix-sdk: Fix the case where the encryption feature is disabled. 2020-09-15 19:10:26 +02:00
Damir Jelić c8e459bc55 matrix-sdk: Fix the encryption feature. 2020-09-15 18:07:00 +02:00
Damir Jelić 4d431b7c9e matrix-sdk: Test the attachment sending paths. 2020-09-15 18:06:32 +02:00
Damir Jelić 890e6cbc73 crypto: Turn an unwrap into a except. 2020-09-15 17:18:31 +02:00
Damir Jelić e98960f30b matrix-sdk: Add an image uploading bot to the examples. 2020-09-15 17:17:28 +02:00
Damir Jelić c500c06e4b matrix-sdk: Add docs and cleanup the media upload methods. 2020-09-15 17:16:16 +02:00
Damir Jelić 3ac3be501f matrix-sdk: Refactor out the check if a room is encrypted. 2020-09-15 15:02:59 +02:00
Damir Jelić 3573614640 crypto: Add some TODOs for the key query handling. 2020-09-15 12:13:35 +02:00
Damir Jelić a60f60bd7d Merge branch 'master' into encrypted_attachments 2020-09-15 12:04:37 +02:00
Damir Jelić a4980e8a04 matrix-sdk: Remove an unneeded lifetime. 2020-09-14 20:38:53 +02:00
Damir Jelić b628e6286a crypto: Remove an unused import. 2020-09-14 20:27:30 +02:00
Jonas Platte fb47abcc17 Update ruma 2020-09-14 20:26:52 +02:00
Damir Jelić c2756a9a92 matrix-sdk: First draft for our upload method. 2020-09-14 20:07:55 +02:00
Damir Jelić 2d6882c495 crypto: Use a Read implementation for the attachment encryption as well. 2020-09-14 20:06:44 +02:00
Damir Jelić 51f3d90224 crypto: Move the file encryption modules under a submodule. 2020-09-14 17:14:18 +02:00
Damir Jelić 1a140ecc2f crypto: Initial support for attachment encryption. 2020-09-14 16:38:52 +02:00
Damir Jelić f603696ff4 crypto: Expose the olm machine only if the encryption feature is enabled. 2020-09-11 17:06:45 +02:00
Damir Jelić ffd2843b0a matrix-sdk: Expose the import/export keys methods. 2020-09-11 16:34:39 +02:00
Damir Jelić 618a58ba34 crypto: Add error handling to the key exports. 2020-09-10 17:02:36 +02:00
Damir Jelić 126ac3059b Merge branch 'key_export' into master 2020-09-10 16:32:41 +02:00
Damir Jelić 8af18a4df7 crypto: Test the EncryptionSettings conversion. 2020-09-10 16:21:23 +02:00
Damir Jelić 7790c3db8f crypto: Fix a bunch of clippy warnings. 2020-09-10 16:07:28 +02:00
Damir Jelić e3f4c1849c crypto: Finish up the key export feature. 2020-09-10 15:54:41 +02:00
Damir Jelić 848156213b crypto: Add a PartialEq derive for the exported key struct. 2020-09-10 15:51:39 +02:00
Damir Jelić 23e953d9cf crypto: Hide some methods that shouldn't be public. 2020-09-10 15:49:34 +02:00
Damir Jelić 464e181f66 crypto: Add a method to get all group sessions from the store. 2020-09-10 14:59:20 +02:00
Damir Jelić 7bd0e4975b crypto: Store the forwarding chains for group sessions. 2020-09-09 17:27:10 +02:00
Damir Jelić 127d4c225b crypto: Change the crypto store so we can save multiple group sessions at once. 2020-09-09 16:34:18 +02:00
Damir Jelić 9617d9aac9 crypto: Test the import/export of group sessions. 2020-09-09 16:10:16 +02:00
Damir Jelić e828828ace crypto: Document the exported key -> forwarded room key conversion methods. 2020-09-09 15:11:25 +02:00
Damir Jelić 3e9b0a8e7f crypto: Correctly store the ed25519 key map for inbound group sessions. 2020-09-09 15:03:19 +02:00
Damir Jelić aff1e1d0a8 crypto: Add key export methods for inbound group sessions. 2020-09-09 12:47:28 +02:00
Damir Jelić 98f69aed41 crypto: Remove some duplicated types after the group session split. 2020-09-09 11:52:10 +02:00
Damir Jelić acfd0cdb07 crypto: Split out the group session module into multiple files. 2020-09-09 11:07:49 +02:00
Damir Jelić fc60593801 crypto: Remove some unused into implementation. 2020-09-08 17:34:34 +02:00
Damir Jelić 14226c0778 crypto: Refactor some tests. 2020-09-08 16:17:17 +02:00
Damir Jelić 70ffc43ce0 crypto: Store the trust state of our own identities as well. 2020-09-08 16:07:37 +02:00
Damir Jelić 9810a2f630 crypto: Finish up the cross signing storing for the sqlite store. 2020-09-08 15:24:23 +02:00
stoically 35f5117800 matrix-sdk-common: Switch futures-locks to crates.io version 2020-09-08 07:28:32 +02:00
Damir Jelić d35cf56dc8 crypto: Disable the real life key export test since it take a lot of time. 2020-09-07 16:59:30 +02:00
Damir Jelić 083cebe735 crypto: Initial WIP user identity storing logic. 2020-09-07 16:57:58 +02:00
Damir Jelić faaf3f7a29 crypto: Identities add some methods to get the keys/signatures of the keys. 2020-09-07 16:57:17 +02:00
Damir Jelić 34cdf31cc5 matrix-sdk: Don't require the user id to be passed to set a typing notice. 2020-09-05 20:32:16 +02:00
Damir Jelić 6c7dbb814b matrix-sdk: Add a convenience method to get our own devices. 2020-09-05 18:04:15 +02:00
Damir Jelić 217543ef38 matrix-sdk: Bump the versions of our deps. 2020-09-05 18:03:47 +02:00
Damir Jelić f57447527d crypto: Initial logic for encrypting key exports. 2020-09-04 17:59:56 +02:00
Damir Jelić 8dbc7c38e5 crypto: Correctly split the 2 keys in the key export logic. 2020-09-04 16:34:19 +02:00
Damir Jelić 5a069a8721 Merge branch 'master' into key_export 2020-09-04 14:48:56 +02:00
Damir Jelić 89efcee337 crypto: Move the signature verification method under an Utility struct. 2020-09-04 13:18:31 +02:00
Damir Jelić 22daf0d81e Merge branch 'to-device-txn-uuid' into crypto-improvements 2020-09-04 12:54:40 +02:00
Damir Jelić 53fec7a87e crypto: Don't ignore store errors when fetching the identities. 2020-09-04 12:44:03 +02:00
Damir Jelić adf8905d9f crypto: Rename the memory stores into caches and reorder the store module. 2020-09-04 12:42:40 +02:00
Damir Jelić 7b3dfe2f27 crypto: Move the device and user identities under one module. 2020-09-04 10:51:46 +02:00
Jonas Platte 73c104cac1 Replace IncomingToDeviceRequest with customized request type 2020-09-03 20:02:55 +02:00
Damir Jelić d86c05efb3 crypto: Add a fixme to the sqlite store since it's not storing forwarding chains. 2020-09-02 15:08:24 +02:00
Damir Jelić cc236a8765 examples: Fix the wasm bot example. 2020-09-02 14:23:00 +02:00
Damir Jelić 8b5bb7d8c5 crypto: Remove the deserialize implementations for our user identity.
Deriving Serialize/Deserialize for an AtomicBool doesn't seem to be
implemented under WASM. So remove the derives for now.
2020-09-02 13:54:04 +02:00
Damir Jelić 2195da1cd8 crypto: Fix some docs. 2020-09-02 12:28:18 +02:00
Damir Jelić 65843f89dc crypto: Simplify the signature loading in the sqlite cryptostore. 2020-09-02 12:24:46 +02:00
Damir Jelić 8b56546565 crypto: Remove an unwrap from the sqlite cryptostore. 2020-09-02 12:17:38 +02:00
Damir Jelić 8c4acf54e0 crypto: Reorder the errors so unpickling now returns the timestamp error. 2020-09-02 12:11:06 +02:00
Damir Jelić c652762255 crypto: Allow user identities to be seralized/deserialized. 2020-09-02 11:54:04 +02:00
Damir Jelić 4bab678e46 crypto: Allow most of the ReadOnlyDevice to be serialized. 2020-09-02 11:49:49 +02:00
Damir Jelić 81b127b6e7 crypto: Modify all the pickling logic so we return serializeable structs. 2020-09-02 11:45:35 +02:00
Damir Jelić 269cfc3d34 crypto: Add a pickled account struct making account storing easier. 2020-09-02 09:37:10 +02:00
Damir Jelić 987d87cd5d crypto: Use the correct async-trait macro for the CryptoStores. 2020-09-01 17:41:30 +02:00
Damir Jelić 0de4a21320 crypto: Expose some missing structs that are needed to implement a cryptostore. 2020-09-01 17:39:51 +02:00
Devin Ragotzy 6872cc717b matrix_sdk: fix Client docs for methods that used request builders 2020-08-26 16:30:29 -04:00
Devin Ragotzy 5c4e46e908 matrix_sdk_common: Bump ruma 2020-08-26 16:26:40 -04:00
Damir Jelić a2bfa08e09 crypto: Initial decryption method for key exports. 2020-08-26 19:14:24 +02:00
Damir Jelić 977e29c3af matrix-sdk: Fix the wasm bot example. 2020-08-26 16:19:39 +02:00
Damir Jelić a2f7297941 Merge branch 'reexport-reqwest' into master 2020-08-26 16:07:05 +02:00
Alejandro Domínguez 6fa365935f Add "socks" feature from reqwest 2020-08-26 16:01:50 +02:00
Damir Jelić 39628a308b matrix-sdk: Allow any event content to be sent out with room_send(). 2020-08-26 15:41:27 +02:00
Damir Jelić 54391040a4 matrix-sdk: Re-export reqwest. 2020-08-26 14:47:43 +02:00
Damir Jelić 7a418ae09e matrix-sdk: Implement the HttpSend trait directly on the reqwest client. 2020-08-26 14:37:48 +02:00
Damir Jelić deff66ac42 matrix-sdk: Simplify the registration example. 2020-08-26 14:16:31 +02:00
Damir Jelić 2995cebd57 matrix-sdk: Fix some clippy issues. 2020-08-26 13:50:28 +02:00
Damir Jelić ea4befabd9 matrix-sdk: Fix the incorrect return value of the HttpSend trait.
The HttpSend trait incorrectly returns a reqwest::Response, we already
have logic to return the response into a http::Response and we need to
do the conversion since there is no other way to build Ruma responses.
2020-08-26 13:41:15 +02:00
Damir Jelić 6760f81498 matrix-sdk: Update Ruma. 2020-08-26 13:40:38 +02:00
Damir Jelić b3d1e8687e matrix-sdk: Fix to a released version of reqwest. 2020-08-26 10:26:05 +02:00
Damir Jelić 95c8708995 crypto: Document and rename the mark_requests_as_sent() method. 2020-08-24 14:49:57 +02:00
Damir Jelić 8d39821a1f crypto: Remove some unused imports from the top level module. 2020-08-24 14:34:22 +02:00
Damir Jelić 2bcbf1eca4 Merge branch 'power-ev-overflow' into master 2020-08-24 14:27:02 +02:00
Damir Jelić a5f06f772f Merge branch 'rustls' into master 2020-08-24 10:00:21 +02:00
Devin Ragotzy 2b389b920d matrix_sdk_base: Add test for update_member_power overflow 2020-08-23 20:57:59 -04:00
Damir Jelić 298c260c5f crypto: Document the outgoing request types. 2020-08-23 17:03:04 +02:00
Devin Ragotzy 72614e4252 matrix_sdk_crypto: Appease clippy 2020-08-22 08:00:32 -04:00
Devin Ragotzy 8a71cec81a matrix_sdk_base: Member power level math from Int -> i64 2020-08-22 07:52:12 -04:00
Tilo Spannagel a57c6159bd Fix travis ci errors
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2020-08-21 19:11:10 +02:00
Tilo Spannagel 5f10f4301c Add feature flag for rustls
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2020-08-21 18:36:42 +02:00
Damir Jelić 176181bdcf Merge branch 'crypto-improvements' into master 2020-08-21 18:16:48 +02:00
Damir Jelić edea5e1c51 crypto: Fix a clippy warning. 2020-08-21 16:46:28 +02:00
Damir Jelić b3941ca254 crypto: Verify user identities when we're the first one to confirm as well. 2020-08-21 16:39:15 +02:00
Damir Jelić c3c6428717 crypto: Remove some clippy warnings. 2020-08-21 16:31:02 +02:00
Damir Jelić de90da4adc crypto: Make the verification machine compatible with how we queue up requests. 2020-08-21 16:26:34 +02:00
Damir Jelić 002531349e crypto: Decluter the main doc page a bit. 2020-08-21 15:06:54 +02:00
Damir Jelić e38bfc64f4 crypto: Streamline the key claiming so we use the new mark request as sent method. 2020-08-21 14:40:49 +02:00
Damir Jelić 93e1967119 crypto: Initial refactor to switch to the outgoing_requests queue. 2020-08-21 13:35:01 +02:00
Damir Jelić aee40977a3 crypto: Clamp the rotation period ms so users can't wedge E2E.
Users may set a very small rotation period this might mean that a
session might expire by the time it's shared ending up in a loop where
we constantly need to share a group session yet never manage to send a
message.
2020-08-21 12:50:16 +02:00
Damir Jelić 9fe23227af base: Fix the encryption settings Into implementation. 2020-08-21 12:44:14 +02:00
Damir Jelić ce93869915 crypto: Return an Option instead of an empty result for the key uploads. 2020-08-21 09:50:01 +02:00
Damir Jelić 202c20feda crypto: Rename the method to set the local trust of a device. 2020-08-20 18:01:34 +02:00
Damir Jelić c307690c2e crypto: Fix a clippy warning and some spelling. 2020-08-20 16:06:06 +02:00
Damir Jelić 552a12eeed crypto: More docs for the user identities. 2020-08-20 15:52:40 +02:00
Damir Jelić c2ad298963 crypto: Check that the user ids match for the cross signing keys. 2020-08-20 15:40:49 +02:00
Damir Jelić d908d0f817 crypto: Don't allow user identities to verify devices of other users. 2020-08-20 15:17:19 +02:00
Damir Jelić 9edc876160 crypto: Check that the master key and subkeys have the same user id. 2020-08-20 15:14:58 +02:00
Damir Jelić 398edbbe0c crypto: Reset the verification state of our identity if the master keys change. 2020-08-20 15:13:55 +02:00
Damir Jelić 89b56b5af8 crypto: Don't expose the btree map of the master key dirrectly.
This implements PartialEq for the master key so we can check if they
have changed when doing SAS.
2020-08-20 15:06:49 +02:00
Damir Jelić a57f63d614 crypto: Document the user identities. 2020-08-20 14:44:16 +02:00
Damir Jelić 74dd0a00d3 crypto: Simplify the default hashmaps in the memory stores. 2020-08-20 12:23:18 +02:00
Damir Jelić b97e3d7bae crypto: Fix a clippy warning. 2020-08-20 10:49:14 +02:00
Damir Jelić c3eb4d8106 crypto: Simplify some more function definitions. 2020-08-20 10:36:58 +02:00
Damir Jelić ea49a35b43 crypto: Simplify the function signature of share_group_session. 2020-08-20 10:25:05 +02:00
Damir Jelić a99e47c310 crypto: Shorten some log lines. 2020-08-20 10:23:16 +02:00
Damir Jelić 69fbe65ac4 crypto: Add some docs for the cross signing keys handling method. 2020-08-20 10:21:00 +02:00
Damir Jelić aaa15c768c crypto: Simplify the Olm message map construction. 2020-08-20 10:19:55 +02:00
Damir Jelić 58185e08e8 crypto: Move the olm_encrypt() method into the higher level Device. 2020-08-20 10:18:36 +02:00
Nym Seddon 89c9e31140 doc: Add UIAA auth data to registration example
Add direct request authentication data to registration example
2020-08-20 01:42:01 +00:00
Damir Jelić 1bd15b9fdd crypto: Remove some unneeded clones. 2020-08-19 18:04:06 +02:00
Damir Jelić 23126c4e48 crypto: Disable the sqlite store test if the feature is disabled. 2020-08-19 17:55:28 +02:00
Damir Jelić 6f5352b9a9 crypto: Test the signature checking of user identities. 2020-08-19 17:52:38 +02:00
Damir Jelić eb16737d3b crypto: Add some comments about the order of signature checks. 2020-08-19 15:35:34 +02:00
Damir Jelić 56309ae12c matrix-sdk: Bump the versions of our deps. 2020-08-19 14:52:11 +02:00
Damir Jelić 9fe0717cee examples: Update the emoji verification example tho show a list of devices.
This may showcase that cross signing verification works if the other
device uploads valid signatures.
2020-08-19 14:50:35 +02:00
Damir Jelić 7f23cbbeb5 crypto: Add a TODO about cross signing signatures. 2020-08-19 14:49:40 +02:00
Damir Jelić 3153a81cd2 crypto: Add support to check the cross signing verification state of a device. 2020-08-19 14:47:22 +02:00
Damir Jelić c3e593d998 crypto: The device identity can be our own, so store the identity enum instead. 2020-08-19 14:43:49 +02:00
Damir Jelić c2a386b889 crypto: Fix a clippy warning. 2020-08-19 14:40:04 +02:00
Damir Jelić 317a141e07 crypto: If our own identity passed a SAS flow, mark it as verified. 2020-08-19 14:34:18 +02:00
Damir Jelić 3990e50ca6 crypto: Store the verified identities in the SAS states. 2020-08-19 14:28:16 +02:00
Damir Jelić 90ea0229f2 crypto: Rename TrustState to LocalTrust since.
We might still trust the device event if our local trust isn't set, so
rename the enum to better reflect that meaning.
2020-08-19 11:20:08 +02:00
Damir Jelić a42af5da69 crypto: Let the device hold on to identities.
This makes it possible to check the verification state of the device
directly.
2020-08-19 10:58:14 +02:00
Damir Jelić f63a01a85b crypto: Remove a stale TODO. 2020-08-18 15:36:04 +02:00
Damir Jelić 27e1fb9a35 crypto: Pass the user identity to the SAS object when a start event is received. 2020-08-18 15:25:00 +02:00
Damir Jelić c21517c61e crypto: Store the changed user identities. 2020-08-18 15:23:37 +02:00
Damir Jelić f626f2b24e crypto: Add some logging for the user identity update logic. 2020-08-18 15:22:30 +02:00
Damir Jelić 37a7f69e03 crypto: Implement storage for the user identities in the memory store. 2020-08-18 15:13:56 +02:00
Damir Jelić 38cf771f1f crypto: Pass the identity further through the SAS layer and try to verify it. 2020-08-18 14:24:27 +02:00
Damir Jelić 6d0b73cb3d crypto: Pass the user identity to the SAS object when doing verifications. 2020-08-18 13:37:02 +02:00
Damir Jelić f96437a242 crypto: Initial scaffolding for handling user identities in key queries. 2020-08-18 12:50:03 +02:00
Damir Jelić 150862ec0c matrix-sdk: Remove an useless into(). 2020-08-17 17:47:29 +02:00
Damir Jelić 6db7eb0694 crypto: Add a method to directly verify a device. 2020-08-17 17:36:07 +02:00
Damir Jelić 84c0311d80 crypto: Rename the UserDevicesWrap struct. 2020-08-17 17:12:39 +02:00
Damir Jelić de097d3ca0 crypto: Rename UserDevices to ReadOnlyUserDevices. 2020-08-17 17:01:38 +02:00
Damir Jelić 8aedc3077d matrix-sdk: Add an example to the start verification method of the device. 2020-08-17 16:47:24 +02:00
Damir Jelić 0f26e7e3bc crypto: Fix the doc for the read-only device. 2020-08-17 16:40:37 +02:00
Damir Jelić 91db502cfe crypto: Rename DeviceWrap to Device. 2020-08-17 16:36:50 +02:00
Damir Jelić 43aea6e482 crypto: Rename Device to ReadOnlyDevice. 2020-08-17 16:17:28 +02:00
Damir Jelić e778f7d72d matrix-sdk: Remove an unneeded clone. 2020-08-17 15:56:19 +02:00
Damir Jelić 94248523b3 matrix-sdk: Implement deref for our device wrapper. 2020-08-17 15:54:54 +02:00
Damir Jelić fd8377bce2 crypto: Add device wrappers so that the verification can be started with a device. 2020-08-17 15:36:45 +02:00
Damir Jelić 9e609a0fdf matrix-sdk: Move the session into the http client wrapper. 2020-08-17 15:29:07 +02:00
Damir Jelić 16a115d27e Merge branch 'up-ruma' into master 2020-08-17 11:17:02 +02:00
Damir Jelić 8167f5e9de crypto: Simplify the function signature of the share group session method. 2020-08-16 16:25:48 +02:00
Damir Jelić 5876c89858 crypto: The mark_user_as_changed method doesn't need to be public. 2020-08-15 15:51:04 +02:00
Jonas Platte 5040be042f Update ruma 2020-08-15 15:17:27 +02:00
Jonas Platte ad2d3d2037 Simplify tests in matrix_sdk::client 2020-08-15 03:05:22 +02:00
Damir Jelić 09f009ebd7 matrix-sdk: Bump our deps. 2020-08-14 17:11:54 +02:00
Damir Jelić 664d8c239c crypto: Don't share group sessions with blacklisted devices. 2020-08-14 16:20:49 +02:00
Damir Jelić 97ad060d4b crypto: Test that we can create other users identities. 2020-08-14 16:18:18 +02:00
Damir Jelić f4de3580b6 crypto: Expose the device/identity verification methods through the identities. 2020-08-14 15:32:44 +02:00
Damir Jelić 0fc5134563 crypto: Add methods to check if a cross signing key signed a device. 2020-08-14 15:06:24 +02:00
Damir Jelić b0de9d1809 crypto: Allow some test methods to be dead code since macOS can't use them. 2020-08-14 15:04:59 +02:00
Damir Jelić 75fa7e97f9 crypto: Remove some unneeded clones. 2020-08-14 14:29:53 +02:00
Damir Jelić d21e8213b5 crypto: Don't panic if the key id can't be parsed. 2020-08-14 14:25:51 +02:00
Damir Jelić 181c2a92de crypto: Initial scaffolding for the public cross signing keys. 2020-08-14 14:10:29 +02:00
Damir Jelić 08d76f2ff4 crypto: Pass the device key id to the verify signature method. 2020-08-14 14:08:53 +02:00
Damir Jelić 5b758b8344 crypto: Don't allow dead code in the SAS layer anymore. 2020-08-14 11:09:50 +02:00
Damir Jelić 499f2796ba crypto: Add some logging to the MAC calculation for SAS. 2020-08-14 10:57:17 +02:00
Damir Jelić df0444faa5 crypto: Test the full SAS flow from the Olm machine. 2020-08-13 16:46:11 +02:00
Damir Jelić b4c1b26f96 crytpo: Store the SAS object in the machine if we're starting it. 2020-08-13 16:45:12 +02:00
Damir Jelić 0245782cf4 crypto: Better grammar for a panic message. 2020-08-13 15:59:17 +02:00
Damir Jelić 87d0102663 crypto: Test the Olm machine with the default store. 2020-08-13 15:57:31 +02:00
Damir Jelić 6ee8b07cfe crypto: Test that session expiration works correctly. 2020-08-13 15:03:28 +02:00
Damir Jelić 344631b4ee crypto: Respect the encryption settings of a room when creating sessions. 2020-08-13 14:41:59 +02:00
Damir Jelić f3e03c66a5 travis: Don't clippy check all features. 2020-08-13 12:31:41 +02:00
Damir Jelić d4e31f07a1 matrix-sdk: Fix the docs for our feature flags. 2020-08-13 12:18:24 +02:00
Damir Jelić d4de877e09 base: Fix the docs for our feature flags. 2020-08-13 12:17:30 +02:00
Damir Jelić 9b8e11aab9 crypto: Fix the docs for our features. 2020-08-13 11:06:26 +02:00
Damir Jelić a0abffd026 crypto: Fix the link to the share group session method. 2020-08-13 11:04:37 +02:00
Damir Jelić 4e99278eac matrix-sdk: Expose the device methods in the Client. 2020-08-13 10:49:38 +02:00
Damir Jelić cdb8b5c1e9 matrix-sdk: Fix a couple of typoes. 2020-08-13 10:28:40 +02:00
Damir Jelić bf42e1a39f matrix-sdk: Put the send_to_device method behind the encryption feature for now. 2020-08-12 19:18:30 +02:00
Damir Jelić 5883396106 base: Hide the user devices method behind the encryption feature. 2020-08-12 17:49:08 +02:00
Damir Jelić c6b0a19171 base: Fix a stale docstring. 2020-08-12 17:17:56 +02:00
Damir Jelić 7ee0430054 base: Add methods to fetch user devices. 2020-08-12 17:17:22 +02:00
Damir Jelić 36ca784690 crypto: Expose a method to get all devices of an user. 2020-08-12 17:16:27 +02:00
Damir Jelić 2449bd27c1 matrix-sdk: Make sure our doctests don't make HTTP requests. 2020-08-12 17:15:18 +02:00
Damir Jelić 29bd38734f matrix-sdk: Remove an unused import. 2020-08-12 17:10:31 +02:00
Damir Jelić 6c07620a26 matrix-sdk: Fix the to-device imports for the non-crypto case. 2020-08-12 16:52:50 +02:00
Damir Jelić 3e3894b573 matrix-sdk: Fix for the non-encryption enabled use-case. 2020-08-12 16:19:41 +02:00
Damir Jelić 0a26195472 matrix-sdk: Clean up the client tests. 2020-08-12 15:53:42 +02:00
Damir Jelić 0dc232b268 base: Fix a clippy warning. 2020-08-12 15:39:38 +02:00
Damir Jelić c4465e7979 matrix-sdk: Rename cli to client in the doc examples. 2020-08-12 15:23:44 +02:00
Damir Jelić 41f04d4f5d client: Refactor out the group session sharing logic. 2020-08-12 15:22:17 +02:00
Damir Jelić 15d7deddb8 matrix-sdk: Only claim one-time keys if we're also going to share group sessions. 2020-08-12 15:15:50 +02:00
Damir Jelić 18e597aa79 crypto: More doc fixes. 2020-08-12 15:14:16 +02:00
Damir Jelić 407f9a3da8 matrix-sdk: Make sure to not send out multiple group share requests at once. 2020-08-12 15:12:51 +02:00
Damir Jelić 82c3a795ff crypto: More doc improvements. 2020-08-12 13:28:16 +02:00
Damir Jelić ccda5c7260 crypto: Small doc improvements to the OlmMachine. 2020-08-12 13:11:51 +02:00
Damir Jelić d706140a8f crypto: Fix a SAS docstring. 2020-08-12 12:49:29 +02:00
Damir Jelić 8351858be7 crypto: Expose a method to get a users device. 2020-08-12 12:48:22 +02:00
Damir Jelić 7cb25361b2 matrix-sdk: Expose an API to start SAS verifications. 2020-08-12 11:39:47 +02:00
Damir Jelić 42a4ad60e8 Merge branch 'lockless-cryptostore' into master 2020-08-11 17:37:38 +02:00
Damir Jelić 9a325a4505 matrix-sdk: Move the HttpSend trait into the http_client file. 2020-08-11 17:25:33 +02:00
Damir Jelić fe572017b1 Merge branch 'http-send-trait' into master 2020-08-11 17:07:34 +02:00
Damir Jelić c4ed5b6cda matrix-sdk: Upgrade our deps. 2020-08-11 16:54:58 +02:00
Damir Jelić 0d2f8c6d0f crypto: Fix some clippy warnings. 2020-08-11 16:01:48 +02:00
Damir Jelić fa1a40543c crypto: Add a missing license header to the sas helpers file. 2020-08-11 15:55:13 +02:00
Damir Jelić 7637e79f2c matrix-sdk: Fix the tarpaulin skip directives. 2020-08-11 15:49:04 +02:00
Damir Jelić d0a5b86ff3 crypto: Remove our lock around the cryptostore. 2020-08-11 15:39:50 +02:00
Damir Jelić 707b4c1185 crypto: Put a bunch of crypto store stuff behind atomic references. 2020-08-11 15:17:33 +02:00
Devin Ragotzy 9234ac96e1 matrix_sdk: Use our version of the async_trait macro 2020-08-11 09:17:18 -04:00
Damir Jelić 2437a92998 crypto: Don't require the account loading method to borrow self mutably. 2020-08-11 15:12:15 +02:00
Damir Jelić 947fa08dae crypto: Don't require the load_account to mutably borrow self. 2020-08-11 15:08:07 +02:00
Damir Jelić 8f4ac3da7f crypto: Change the way we load the devices/sessions in the SqliteStore. 2020-08-11 14:43:18 +02:00
Damir Jelić 01bcbaf063 crypto: Remove most mutable self borrows from the crypto-store trait. 2020-08-11 14:34:42 +02:00
Devin Ragotzy 4770dc636a matrix_sdk_common_macros: Bump syn fixing conflicting deps 2020-08-11 08:08:17 -04:00
Devin Ragotzy 9294280dc1 matrix_sdk: Add DefaultHttpClient and impl HttpSend 2020-08-11 08:07:45 -04:00
Devin Ragotzy fba3298162 matrix_sdk: Create HttpSend trait to abstract sending requests 2020-08-11 08:06:43 -04:00
Damir Jelić ac2469d270 crypto: Change the way we check if an user is already tracked. 2020-08-11 13:45:32 +02:00
Damir Jelić db553b2040 crypto: Fix some clippy warnings. 2020-08-11 13:38:20 +02:00
Damir Jelić eeb6a811c0 crypto: Make the in-memory stores threadsafe and cloneable. 2020-08-11 13:18:58 +02:00
Damir Jelić 528483ef0e crypto: Remove the last mutable self borrows in the Olm machine methods. 2020-08-11 12:22:14 +02:00
Damir Jelić 72168ce084 crypto: Fix the unknown method tests fot the SAS state transitions. 2020-08-11 11:51:34 +02:00
Damir Jelić 6c85d3e28f crypto: Use TryFrom to check the accepted SAS protocols. 2020-08-11 11:24:29 +02:00
Damir Jelić d5a853f3da crypto: More SAS tests for all the unknown SAS methods. 2020-08-11 11:05:22 +02:00
Damir Jelić 8a2d6a4450 tarpaulin: Disable tarpaulin debugging. 2020-08-11 10:55:10 +02:00
Damir Jelić c15ffb989a crypto: Remove an unused import. 2020-08-11 09:48:01 +02:00
Damir Jelić 2b78f05aad crypto: More SAS tests. 2020-08-11 09:28:28 +02:00
Damir Jelić 1f0a96e31d crypto: Disable the SAS timeout test on macOS. 2020-08-10 17:26:15 +02:00
Damir Jelić 6593cce778 crypto: Simplify the Instant substraction. 2020-08-10 16:53:15 +02:00
Damir Jelić d7bcf42a2b crypto: False alarm with the deadlock we just didn't use the right method. 2020-08-10 16:18:20 +02:00
Damir Jelić 18b655f829 crypto: Test the cancellation of timed out verifications. 2020-08-10 15:55:08 +02:00
Damir Jelić e2e70d6583 crypto: Cancel timed out verifications. 2020-08-10 15:24:22 +02:00
Damir Jelić c305b5052b matrix-sdk: Don't allow dead code anymore. 2020-08-10 15:23:49 +02:00
Damir Jelić 6f4d2022fd Merge branch 'master' into sas-timeout 2020-08-10 15:00:08 +02:00
Damir Jelić ef5201cf35 Merge branch 'up-ruma' into master 2020-08-10 14:58:47 +02:00
Damir Jelić 7bcdc2a3b6 Merge branch 'master' into sas-timeout 2020-08-10 14:57:32 +02:00
Damir Jelić 7eeff64059 crypto: Cancel timed out events on the state transitions. 2020-08-10 14:29:38 +02:00
Matthew Hodgson 9c4229dc57 typoes 2020-08-10 13:15:58 +01:00
Damir Jelić 6c4e2fa508 crypto: Remove mutable borrows in the tests. 2020-08-10 14:15:47 +02:00
Damir Jelić d5cd608045 base: Remove some unnecessary mutable borrows of the olm machine. 2020-08-10 14:11:55 +02:00
Jonas Platte d83fc971ce Update ruma 2020-08-10 13:58:39 +02:00
Damir Jelić d96142b8cb Merge branch 'master' into sas-timeout 2020-08-10 13:48:02 +02:00
Damir Jelić cd5d5da06a matrix-sdk: Use the upstream git repo for olm-rs. 2020-08-10 13:43:18 +02:00
Damir Jelić 87bcba3561 crypto: Add timestamps to the SAS struct so we can check if it timed out. 2020-08-10 13:30:12 +02:00
Damir Jelić 81e9a7cefc crypto: Pass a String when setting the other SAS pubkey. 2020-08-10 10:18:57 +02:00
Damir Jelić 3ddb2199d2 Merge branch 'fix-http-headers' into master 2020-08-09 10:25:15 +02:00
Jonas Platte 4abab73462 Update reqwest to a git dependency 2020-08-09 00:57:58 +02:00
Damir Jelić 17fd85d687 matrix-sdk: Test that we're passing the auth token in the headers. 2020-08-08 15:00:28 +02:00
Jonas Platte 279e88d9f9 Fix handling of headers in HttpClient 2020-08-08 12:23:43 +02:00
Jonas Platte d016ce1848 Use identifier macros in tests 2020-08-06 13:03:32 +02:00
Jonas Platte 591388d13e Upgrade ruma 2020-08-05 18:00:45 +02:00
Devin Ragotzy a3b4cab22e matrix_sdk_crypto: Fix clippy warnings add wasm emscripten to .gitignore 2020-08-04 20:02:09 -04:00
Devin Ragotzy ffdb9c4a79 Fix failing wasm test and clippy warnings for wasm 2020-08-04 17:39:25 -04:00
Devin Ragotzy cb8d5ce8fb Rename CustomOrRawEvent -> CustomEvent and use raw json when failed
When deserialization fails we fallback to providing the user with a
serde_json::RawValue, basically the json string. Ruma should handle all
events that conform to a matrix event shape correctly by either
converting them to their type or returning a custom event.
2020-08-04 17:27:57 -04:00
Devin Ragotzy c10120602a Add test actually testing a correct message edit event 2020-08-04 17:22:54 -04:00
Devin Ragotzy 47690bd268 Bump ruma and fix failing unrecognized_event test
The test was broken because the JSON being fed into it was bad.
2020-08-04 17:22:54 -04:00
Damir Jelić 807432b31f crypto: Calculate the correct extra info when generating emojis. 2020-08-04 13:54:00 +02:00
Damir Jelić 69d2a00759 crypto: Add a TODO about SAS timing out. 2020-08-04 12:56:55 +02:00
Damir Jelić be01ee2de0 crypto: Cancel the verification if we find a MAC mismatch. 2020-08-04 12:31:56 +02:00
Damir Jelić 408fe5da4b crypto: Check that the other device had a valid MAC. 2020-08-04 12:14:19 +02:00
Damir Jelić 28a7831ffd matrix-sdk: Fix the import for the no-encryption case. 2020-08-04 11:41:20 +02:00
Damir Jelić 2bf8c99dfe Merge branch 'master' into sas-verification 2020-08-04 11:23:24 +02:00
Damir Jelić 77f0676a58 matrix-sdk: The emoji example requires the encryption feature. 2020-08-04 10:49:08 +02:00
Damir Jelić e7b2a54e46 matrix-sdk: Add a tarpaulin config. 2020-08-04 10:40:13 +02:00
Devin Ragotzy 33e1601004 matrix_sdk: Fix import error 2020-08-03 20:13:58 -04:00
Damir Jelić 26ec0c6368 crypto: Proptest the emoji/decimal calculation. 2020-08-03 17:22:44 +02:00
Damir Jelić 9f0fbcccf6 crypto: Remove verification objects that are done or canceled. 2020-08-03 16:18:35 +02:00
Damir Jelić 01ba94c670 matrix-sdk: Hide the cryptostore error behind a feature flag. 2020-08-03 15:40:39 +02:00
Damir Jelić e431ba0bf5 crypto: Fix some clippy warnings. 2020-08-03 15:05:19 +02:00
Damir Jelić a3bb8a0d74 examples: Don't use a proxy for the emoji example. 2020-08-03 14:59:03 +02:00
Damir Jelić 3245fbb1c9 examples: Clean up the emoji example a bit. 2020-08-03 14:51:45 +02:00
Damir Jelić f4517c150c crypto: Add more log lines to the SAS code. 2020-08-03 14:51:04 +02:00
Damir Jelić e37229554b crypto: Make sure that we don't hold on to a mutex guard over an await. 2020-08-03 14:49:33 +02:00
Damir Jelić df9da7539a crypto: Expose some more SAS info publicly. 2020-08-03 14:33:15 +02:00
Damir Jelić 1787d2ebe6 crypto: Hook up marking the device as verified. 2020-08-03 12:38:43 +02:00
Damir Jelić faadb4953b Revert "common: Switch to the ruma git repo."
This reverts commit 021193087d.
2020-08-03 10:22:17 +02:00
Damir Jelić 021193087d common: Switch to the ruma git repo. 2020-08-03 10:04:09 +02:00
Devin Ragotzy 0ac2b84c02 Unify import style across workspace 2020-08-02 08:05:43 -04:00
Devin Ragotzy 230b2a229f matrix_sdk: Remove clippy allows 2020-08-02 07:46:02 -04:00
Devin Ragotzy ed1f12ce37 Run cargo fmt with merge-imports true 2020-08-02 07:46:02 -04:00
Damir Jelić 3f83941d57 Merge branch 'master' into sas-verification 2020-07-31 16:27:52 +02:00
Damir Jelić 91d7a8329e matrix-sdk: Add an example that does SAS verification. 2020-07-31 15:34:46 +02:00
Damir Jelić 1a40491c0b matrix-sdk: Fix some clippy warnings. 2020-07-31 15:18:03 +02:00
Devin Ragotzy 79e661d1d9 sdk-base: Remove MessageWrapper and do not order messages in queue 2020-07-31 09:16:41 -04:00
Damir Jelić dce06d31aa Merge branch 'master' into sas-verification 2020-07-31 15:14:05 +02:00
Damir Jelić 3472614649 Merge branch 'remove-possibly-redacted-type-alias' into master 2020-07-31 15:13:23 +02:00
Damir Jelić 7ecd4a035f crypto: Split out the Sas logic into different files. 2020-07-31 14:54:08 +02:00
Denis Kasak 2ce0765206 Remove confusing type alias for AnyPossiblyRedactedSyncMessageEvent.
There's already a SyncMessageEvent in Ruma which is something else.
Let's prefer the full, unambiguous type.
2020-07-31 14:36:58 +02:00
Damir Jelić 108f6d90c9 matrix-sdk-common: Depend on our local Ruma branch. 2020-07-31 12:29:08 +02:00
Damir Jelić 7ceda2f39c crypto: Update to the latest Ruma changes. 2020-07-31 12:05:07 +02:00
Denis Kasak e00e94c6c3 Fix input order. 2020-07-31 11:46:52 +02:00
Damir Jelić a71c7b2964 crypto: Add a method to set the verification state of devices. 2020-07-30 15:54:56 +02:00
Damir Jelić 30c07b4e08 matrix-sdk: Send out to-device events in the sync_forever() loop. 2020-07-30 15:53:55 +02:00
Damir Jelić d9fbc18777 crypto: Update to the latest Ruma changes. 2020-07-30 15:48:13 +02:00
Damir Jelić a58ace70a7 crypto: Fix the SAS MAC calculation KEYIDS -> KEY_IDS. 2020-07-30 15:45:19 +02:00
Denis Kasak 359c5280d7 Expose sender in PossiblyRedactedExt.
Also add a few missing apostrophes.
2020-07-30 14:40:27 +02:00
Damir Jelić a07767d417 base: Hide the SAS getting method behind the encryption feature. 2020-07-30 11:50:42 +02:00
Damir Jelić 5a58fdff98 cyrpto: Fix a clippy warning. 2020-07-29 14:50:39 +02:00
Damir Jelić 5058f09111 matrix-sdk: Remove an incorrect copyright line. 2020-07-29 14:40:05 +02:00
Damir Jelić 21b0afe72c matrix-sdk: Add a Sas wrapper. 2020-07-29 14:19:47 +02:00
Damir Jelić a726ebab39 crypto: Allow Sas objects to be canceled. 2020-07-29 13:53:33 +02:00
Damir Jelić 2b124d98bc matrix-sdk: Pass the rwlock to the http client when doing requests. 2020-07-29 13:50:01 +02:00
Damir Jelić 4634efc092 crypto: More SAS content to to-device request logic. 2020-07-29 13:23:03 +02:00
Damir Jelić 117ebeaf4b crypto: Return requests when you want to accept a verification. 2020-07-29 12:47:36 +02:00
Damir Jelić 27f918e52d matris-sdk: Move the http request sending logic into a separate struct. 2020-07-29 10:56:18 +02:00
Damir Jelić 9facd86d81 base: Expose the verification methods in the base client. 2020-07-28 16:44:06 +02:00
Damir Jelić 7f2df68d62 crypto: Expose some SAS methods publicly. 2020-07-28 16:24:45 +02:00
Damir Jelić a6fa9f99fd crypto: Hook up the verification machine. 2020-07-28 15:37:20 +02:00
Damir Jelić 7e95d85f17 crypto: Move the cryptostore behind a lock. 2020-07-28 15:03:44 +02:00
Damir Jelić 57b65ec8c4 crypto: Add a verification machine. 2020-07-28 14:45:53 +02:00
Damir Jelić 2d6fff7927 crypto: A bit of cleanup and docs. 2020-07-28 11:29:13 +02:00
Damir Jelić 792623f53d crypto: Fix a clippy warning. 2020-07-27 15:57:30 +02:00
Damir Jelić 6e67585bf6 crypto: Handle all the cancel states. 2020-07-27 15:56:28 +02:00
Damir Jelić 5471c07244 crypto: More canceling. 2020-07-27 15:34:18 +02:00
Damir Jelić 0b04f7960b crypto: Add more checks and cancels in the SAS state machine. 2020-07-27 15:28:14 +02:00
Damir Jelić 623f91733e crypto: More verification canceling. 2020-07-27 13:18:00 +02:00
Damir Jelić da3734ffc7 crypto: Add initial SAS canceling. 2020-07-27 13:16:56 +02:00
Damir Jelić 7128505768 Merge branch 'master' into sas-verification 2020-07-26 21:20:53 +02:00
Jonas Platte 6a96368048 Upgrade ruma 2020-07-26 16:58:27 +02:00
Damir Jelić 8c9c843bfc crypto: Fix a comment in the sas file. 2020-07-25 10:59:20 +02:00
Damir Jelić 094b2f90d6 Merge branch 'master' into sas-verification 2020-07-25 10:31:20 +02:00
Damir Jelić 2cbdca1f58 crypto: Make it easier to create canceled SasState. 2020-07-25 10:24:44 +02:00
Jonas Platte d4fe2fe0a2 Remove redundant braces 2020-07-25 02:32:50 +02:00
Jonas Platte 14db34beee Use Option::and_then over manual match 2020-07-25 02:32:18 +02:00
Jonas Platte 7aea6160c3 Flatten nested match for less indentation 2020-07-25 02:31:52 +02:00
Jonas Platte ca88539ec4 Upgrade ruma 2020-07-25 02:23:10 +02:00
Damir Jelić 670755bfce crypto: Start checking and cancelling our SAS flows. 2020-07-24 17:51:20 +02:00
Damir Jelić 46c1657643 crypto: Fix some clippy warnings. 2020-07-24 16:04:47 +02:00
Damir Jelić 9ac1417292 crypto: Add a higher level simple and threadsafe SAS object. 2020-07-24 15:49:00 +02:00
Damir Jelić de94b903d6 crypto: Rename the Sas struct. 2020-07-24 11:32:38 +02:00
Damir Jelić 2f28976694 crypto: Make the Sas struct thread safe. 2020-07-24 11:26:45 +02:00
Damir Jelić 8ff8ea1342 crypto: Add docs for the SAS structs and methods. 2020-07-23 17:25:57 +02:00
Damir Jelić a1edef0ed5 crypto: Fix some clippy warnings. 2020-07-23 14:47:47 +02:00
Damir Jelić ee51ed78be crypto: Allow users to check the SAS even after a mac event was received. 2020-07-23 14:35:29 +02:00
Damir Jelić 2729f01e0f crypto: Move the emoji/decimal sas calculation out of the Sas object. 2020-07-23 14:26:50 +02:00
Damir Jelić e6730a7007 crypto: More SAS refactoring. 2020-07-23 14:14:29 +02:00
Damir Jelić 6fd852d573 crypto: Refactor out some common SAS methods. 2020-07-23 14:02:07 +02:00
Damir Jelić 7f2b268a59 Merge branch 'master' into sas-verification 2020-07-23 13:43:01 +02:00
Damir Jelić bb9adea5de crypto: Implement the whole SAS flow. 2020-07-23 13:41:57 +02:00
Damir Jelić b1ae5534a1 crypto: Hold a copy of the account to get the ed25519 key when doing SAS. 2020-07-23 11:19:19 +02:00
Damir Jelić 9214f01185 cyrpto: Fill out the method to get the MacEventContent. 2020-07-23 11:08:09 +02:00
Damir Jelić c35f73473e crypto: Add a copyright header to the sas file. 2020-07-23 09:21:11 +02:00
Jonas Platte bf54b17a2f Upgrade ruma 2020-07-22 22:31:42 +02:00
Damir Jelić 4ce26f4fa0 crypto: Add support to get the SAS emoji out of a verification. 2020-07-22 16:41:16 +02:00
Damir Jelić cdcbcdfab3 crypto: Add support to display the decimal SAS value. 2020-07-22 15:11:34 +02:00
Damir Jelić 7a2d5c30db crypto: More Sas states and add an initial test. 2020-07-22 13:43:11 +02:00
Damir Jelić a7bc1a95d3 device: Add a method to create a Device from an Account. 2020-07-22 13:41:49 +02:00
Damir Jelić 4fa58bfaac crypto: Add getters for the user and device id in the account. 2020-07-22 13:40:47 +02:00
Damir Jelić 7c92d91c04 crypto: Use a patched olm-rs for now. 2020-07-22 11:39:30 +02:00
Damir Jelić e612326714 Merge branch 'master' into sas-verification 2020-07-22 11:30:58 +02:00
Damir Jelić 9ef784d665 crypto: Simplify the OlmMachine -> Device conversion. 2020-07-22 09:27:43 +02:00
Damir Jelić 2481fbbd27 crypto: Store the device signatures with the devices as well. 2020-07-21 17:33:47 +02:00
Damir Jelić a9d645cbcd crypto: Rewrite the device keys fetching in the SQLiteStore using filter_map. 2020-07-21 16:46:11 +02:00
Damir Jelić 578c927e58 crypto: Simplify the share_group_session method. 2020-07-21 14:13:10 +02:00
Damir Jelić 24baf1fe0f crypto: More doc fixes. 2020-07-21 13:04:51 +02:00
Damir Jelić 861c07d5ce cyrpto: Fix the docs for the Session encrypt method. 2020-07-21 12:59:15 +02:00
Damir Jelić 451d902604 crypto: Allow that many arguments on the from_pickle session method. 2020-07-21 12:57:31 +02:00
Damir Jelić c3f00c96f8 crypto: Don't require the account to be passed when encrypting. 2020-07-21 12:46:06 +02:00
Damir Jelić e50cf39a17 crypto: Store a copy of the user_id/device_id and identity keys in sessions. 2020-07-21 12:40:23 +02:00
Damir Jelić 3f1439fe28 crypto: Move the olm encryption logic into the Session struct. 2020-07-21 12:03:05 +02:00
Damir Jelić 3d6872607e crypto: Move the m.room_key content creation into the outbound group session. 2020-07-21 11:12:20 +02:00
Damir Jelić fe33430e9b crypto: Use DeviceId instead of str everywhere. 2020-07-21 10:48:15 +02:00
Damir Jelić b22324b305 crypto: Split out the olm module into separate files. 2020-07-21 10:38:14 +02:00
Devin R 037d62b165 matrix-sdk-crypto: Remove map clone from user_devices 2020-07-20 08:10:42 -04:00
Devin R 8c39db002b Remove inaccurate comment about DeviceId 2020-07-18 08:52:51 -04:00
Devin R e27b6fb51e matrix-sdk-crypto: Fix map_clone clippy warning 2020-07-18 08:52:51 -04:00
Devin R e4f94cbfec Remove FullOrRedacted use ruma::AnyPossiblyRedacted event enum 2020-07-18 08:52:51 -04:00
Devin R 807435c043 Updates DeviceId to be Box<DeviceId> 2020-07-18 08:51:19 -04:00
Devin R 71f2a042c2 Rename Stub -> Sync for all ruma events 2020-07-18 08:37:43 -04:00
Devin R 2e8fc3e232 matrix-sdk-base: Integrate redacted events into message queue
Redact message events according to spec and ruma types. Remove content
using events redact() method and insert the redacting event into the
event being redacted.
2020-07-17 13:41:55 -04:00
Damir Jelić d273786d83 matrix-sdk: Bump our dependencies. 2020-07-17 10:01:22 +02:00
Damir Jelić 7ddc785a9a Merge branch 'timeout' 2020-07-17 09:59:21 +02:00
Damir Jelić 3e23affc9e Merge branch 'encombhat-master' 2020-07-17 09:41:29 +02:00
Stephen f2163164bf Wasm fix 2020-07-16 20:53:09 -03:00
Stephen 44dfbd2fa6 Fix 2020-07-16 20:21:34 -03:00
Stephen 2f99d0de59 Bugfix 2020-07-16 20:06:26 -03:00
Black Hat 7a72949613 Client::sync_forever(): Add filter in next iteration. 2020-07-16 15:55:55 -07:00
Stephen b0241e51a3 Fixed formatting 2020-07-16 18:40:52 -03:00
Stephen c5ea4fde35 HTTP timeout 2020-07-16 18:16:30 -03:00
Black Hat cc4ae3db1e Client::SyncSettings: Include sync filter 2020-07-16 06:13:35 -07:00
Black Hat a5c5f5a7b1 Revert "Client::sync(): expose sync filter"
This reverts commit 0542e3d83d.
2020-07-16 06:04:26 -07:00
Damir Jelić 7c46953805 travis: Don't allow failures for Windows. 2020-07-16 14:23:36 +02:00
Damir Jelić 2d83c40626 travis: Test the base client on Windows as well. 2020-07-16 14:09:40 +02:00
Damir Jelić d75101d042 travis: Run the tests on Windows. 2020-07-16 14:09:40 +02:00
Damir Jelić 04bb65f43e travis: Don't test encyrption support on Windows for now. 2020-07-16 14:09:40 +02:00
Damir Jelić c1ffed4fc9 base: Sanitize the room id for the path of the state store.
This closes: #71.
2020-07-16 14:08:56 +02:00
Black Hat 0542e3d83d Client::sync(): expose sync filter 2020-07-16 03:49:46 -07:00
Damir Jelić 2d955027e1 travis: Don't set the linux version. 2020-07-15 16:11:08 +02:00
Damir Jelić 38166135dc travis: Add a clippy stage. 2020-07-15 15:59:04 +02:00
Damir Jelić 5bebe1d434 crypto: Clippy fixes for our tests. 2020-07-15 15:58:36 +02:00
Damir Jelić a2a87b9fff matrix-sdk: Fix a bunch of clippy warnings. 2020-07-15 15:53:17 +02:00
Damir Jelić 497b973eb5 travis: Test newer macOS versions. 2020-07-15 15:46:17 +02:00
Damir Jelić de1988265d crypto: Move the outbound session creation logic into the account. 2020-07-15 15:39:56 +02:00
Damir Jelić 6cced25ae1 travis: Don't allow failures on the wasm target. 2020-07-15 14:13:43 +02:00
Damir Jelić 4e40c13b81 travis: Fix the minimal build. 2020-07-15 14:05:52 +02:00
Damir Jelić bf152df322 matrix-sdk: Hide some tracing imports behind the encryption flag. 2020-07-15 14:05:22 +02:00
Damir Jelić 3315cf5bc6 travis: Add a build that doesn't use any of the features. 2020-07-15 13:23:28 +02:00
Damir Jelić 204279c575 matrix-sdk: Don't hide the tracing import behind the encryption feature. 2020-07-15 13:19:56 +02:00
Damir Jelić 83806b42e9 crypto: Remove a stale comment about clearing private keys from events. 2020-07-15 13:07:48 +02:00
Damir Jelić fa0a22b090 Merge branch 'dkasak-master'
High-level summary of changes:

- Rewrite the disambiguation algorithm to simplify it.
- Fixes to state tracking, e.g. use `state_key` instead of `user_id` when
  determining which member an event is acting on.
- Changes to `RoomMember`:
  * Make `RoomMember` "dumber" and don't let it mutate itself. This came about
    primarily because `update_profile` cannot live on `RoomMember` because it
    needs some information from `Room`. The other few mutating methods then
    looked odd so it seemed best to move them to `Room` so that the room takes
    care of updating its members.
  * Each `RoomMember` now contains all information to calculate its set of
    names:
      + `.name()` (short/ergonomic but potentially ambiguous),
      + `.unique_name()` (unique but may be contain MXID when not necessary),
      + `.disambiguated_name()` (shortest possible while being unique).
- Add some logging using the `tracing` crate.
- Improvements to `EventBuilder`:
  * Add a docstring.
  * Make it clear itself when building a sync response so the same builder can
    be reused for later sync responses.
- A few tests.
2020-07-15 12:52:25 +02:00
Denis Kasak bce7fe0217 Equivalence class -> equivalence set. 2020-07-15 11:58:52 +02:00
Denis Kasak 1fd21ee206 Fix docstrings regarding return value related to disambiguation. 2020-07-15 11:54:30 +02:00
Denis Kasak 8a4a4140b3 Remove stale comment. 2020-07-15 11:22:47 +02:00
Denis Kasak 62943f055d Rewrap docstrings and comments to 80 chars. 2020-07-15 11:21:01 +02:00
Denis Kasak ea149ebd8e Update docstring for disambiguation_updates. 2020-07-15 11:16:13 +02:00
Denis Kasak 32737a5517 Use match instead of if-let in sync_forever. 2020-07-15 09:43:58 +02:00
Damir Jelić 1691a26163 crypto: Add initial Sas scaffolding. 2020-07-14 17:04:08 +02:00
Damir Jelić 51012e632e crypto: Rename the StoreError to StoreResult. 2020-07-14 13:11:44 +02:00
Damir Jelić 5d76fd9aac crypto: Refactor the key query handling logic a bit. 2020-07-14 13:08:57 +02:00
Damir Jelić c25f4c0642 crypto: Verify one-time keys using the device. 2020-07-14 12:49:40 +02:00
Denis Kasak 9e48b7172b cargo fmt 2020-07-14 12:38:55 +02:00
Damir Jelić 68125f5de6 crypto: Refactor out the json verification method. 2020-07-14 12:23:42 +02:00
Damir Jelić b602d3007d crypto: Remove some useless mem::replace calls. 2020-07-14 12:03:27 +02:00
Damir Jelić 41cfbaf520 device: Store the device keys with the algorithm and device id.
This will ensure that we can check the signature of the device later on.
2020-07-14 12:00:29 +02:00
Damir Jelić 8206394918 crypto: Use AlgorithmAndDeviceId to get the device signature. 2020-07-14 11:27:50 +02:00
Damir Jelić ca85564a9f crypto: Move the device keys verificatin logic into the device. 2020-07-14 11:17:09 +02:00
Denis Kasak 7d9a699d62 Fix EventBuilder docstring example. 2020-07-14 11:08:16 +02:00
Damir Jelić a38efc0f29 room: Fix a clippy warning, use unwrap_or_else for the member counts. 2020-07-14 10:56:06 +02:00
Denis Kasak 048a2000e7 Merge 2020-07-13 17:10:13 +02:00
Damir Jelić 18b444aac5 crypto: Move the uploaded key count handing into the account. 2020-07-13 16:46:51 +02:00
Damir Jelić a7a9ac24ed crypto: Move the key count field into the account. 2020-07-13 15:49:16 +02:00
Damir Jelić b2ccb61864 crypto: Add the device id and identity keys to the megolm session.
This way we don't need to pass in the account to encrypt events.
2020-07-13 14:32:59 +02:00
Damir Jelić ac264918b8 crypto: Move the megolm decryption logic into the session. 2020-07-13 14:00:42 +02:00
Damir Jelić 8e19c583c6 crypto: Move the megolm encryption logic into the outbound group session. 2020-07-13 13:19:25 +02:00
Damir Jelić 740a5af068 Merge branch 'dan/bugfix/implable-StateStore' 2020-07-13 10:19:37 +02:00
Dan Enman 8c3855221c mark state::AllRooms and state::ClientState as public 2020-07-11 19:14:55 -03:00
Damir Jelić c2f1e4de64 crypto: Disable a clippy warning. 2020-07-11 23:15:10 +02:00
Damir Jelić 8a7c53c00d matrix-sdk: Remove the wrongly committed olm-sys/olm-rs patch defintions. 2020-07-11 22:18:01 +02:00
Damir Jelić c1ae183795 Merge branch 'deps-and-stuff' 2020-07-11 22:13:35 +02:00
Jonas Platte eea00301ff Remove immediately-deref'ed double references 2020-07-11 21:20:02 +02:00
Jonas Platte 85522ac35a Slightly simplify RoomName::calculate_name 2020-07-11 21:14:32 +02:00
Jonas Platte 9b5f95672b Use js_int macros to improve readability 2020-07-11 21:06:21 +02:00
Jonas Platte ffc5204109 Fix two pattern matching related warnings 2020-07-11 20:57:01 +02:00
Jonas Platte a607d70371 Upgrade mockito in matrix-sdk-base 2020-07-11 20:55:19 +02:00
Jonas Platte 1fcb68c59f Remove unused dependencies 2020-07-11 20:55:05 +02:00
Damir Jelić 9bceb2f539 crypto: Add the user id and device id to the account. 2020-07-11 17:23:50 +02:00
Damir Jelić 7003ea2d23 matrix-sdk-common: Depend on a working revision of the ruma mono repo. 2020-07-11 16:24:36 +02:00
Damir Jelić 18ccd30c8c crypto: Add a bunch of TODO lines documenting how to refactor stuff further. 2020-07-11 12:05:52 +02:00
Damir Jelić eb19c19e36 Merge branch 'perf-1' 2020-07-11 10:33:30 +02:00
Damir Jelić df2bcf6f1f crypto: Style fix for a doc comment. 2020-07-11 09:45:52 +02:00
Jonas Platte 3ee06be87b Rewrite MessageQueue deserialization to reduce allocations 2020-07-10 21:41:46 +02:00
Jonas Platte 3a07a17e9d Remove unnecessary calls to clone() 2020-07-10 21:24:01 +02:00
Damir Jelić 27eeeb8db6 crypto: Move the one-time key signing into the accoung. 2020-07-10 17:53:04 +02:00
Damir Jelić 6ded76a5a7 crypto: Move the device_keys() method into the account. 2020-07-10 17:10:34 +02:00
Denis Kasak 05a41d3b4d Move and rename member_display_name to RoomMember::disambiguated_name.
This makes more sense as all the required information is now available
on `RoomMember`. We also don't have to handle the case of the missing
member since now you have to actually get a `RoomMember` before you can
ask for their name.
2020-07-10 15:47:11 +02:00
Damir Jelić 58d79ca9c6 crypto: Put the user id and device id into the account. 2020-07-10 15:43:32 +02:00
Damir Jelić 4ee245dcce examples: Updat the autojoin example to use the ruma mono repo. 2020-07-10 15:30:17 +02:00
Denis Kasak 8daa12ac56 Print error when receiving invalid response in sync_forever. 2020-07-10 15:11:03 +02:00
Denis Kasak 4134ba969a DRY the membership logging a bit. 2020-07-10 15:11:03 +02:00
Denis Kasak a8f24da3ba cargo fmt 2020-07-10 15:11:03 +02:00
Denis Kasak 390a1aa12c Clarify member_display_name docstring. 2020-07-10 15:11:03 +02:00
Denis Kasak b16724841d Correct state tracking of room members.
- use `state_key` instead of `user_id` to determine which member is
  affected by the event
- assign state directly from the event in `add_member` instead of using
  `membership_change`
- expand/fix docstrings
- add some logging
2020-07-10 15:11:03 +02:00
Denis Kasak ec81a5e539 Implement Room::member_is_tracked. 2020-07-10 15:11:03 +02:00
Denis Kasak 949305da72 Clarify comment. 2020-07-10 15:11:03 +02:00
Denis Kasak 559306a33c Rewrite disambiguation algorithm to handle profile changes.
The new algorithm is simpler. Instead of tracking a list of
disambiguated display names in `Room`, we instead track the display name
ambiguity status in `RoomMember`. This allows a client to generate the
correct name for a member using solely the information available in
`RoomMember`.

The disambiguation algorithm itself now only calculates the set of members
whose ambiguity status had changed instead of producing disambiguated
display names for everyone affected. This is called on each room entry
(join or invite), room entry and profile change, and the updates are
propagated to the affected `RoomMember`s.
2020-07-10 15:11:01 +02:00
Denis Kasak 24d2aa8078 Style (cargo fmt, reordering import). 2020-07-10 15:07:21 +02:00
Denis Kasak e70929317a Revert "add_member provably always returns true."
This reverts commit 7943baee49.
2020-07-10 15:07:17 +02:00
Devin R 62eeb3707f Fix wasm test failure gate unknown import 2020-07-10 08:59:02 -04:00
Devin R 3fa06eeb99 matrix-sdk-base: Add test for MessageQueue/JsonStore interaction
Ruma can't currently handle an event with the wrong event content type.
When replacing the MessageEventStub's content it automatically
serializes as "m.room.redaction".
2020-07-10 08:59:02 -04:00
Devin R c0e6279837 matrix-sdk: Update request_builder to use new constructors
The create_room::Request and get_message_events::Request now have
constructors that we use in our builder structs.
2020-07-10 08:59:02 -04:00
Devin R e7c70854ab sdk_base: message events in message queue have content redacted
The MessageQueue holds MessageEventStub<AnyMessageEventContent> so when
a redaction event is encountered the redacted event's content can be
replaced. The Unsigned field redacted_because is also populated with the
redaction event itself with the addition of a room_id Stub -> full
event.
2020-07-10 08:59:02 -04:00
Devin R dcc3d6e755 sdk_base: Remove room_id as argument from all Room methods
Remove room_id paramater from some client methods. Make CreationContent
two methods of RoomBuilder. Add docs for MessageWrapper.
2020-07-10 08:59:02 -04:00
Devin R 2338d3e8fd matrix-sdk-base: clean up recv/iter joined post rebase
The types for account data in a sync response have changed, no longer
Option. Re word comment in hoist prev_content test.
2020-07-10 08:59:02 -04:00
Devin R b83b9dc59d matrix-sdk-base: Use new accessor methods for models/message.rs
ruma now has field access methods for all of the Any*Event enums use
them for MessageWrapper's AnyMessageEventStub contents.
2020-07-10 08:59:02 -04:00
Devin R 68822861d5 Rebase upstream/master into ruma-mono branch 2020-07-10 08:59:02 -04:00
Devin R b1e7bc77a4 Use ruma/ruma master, address review issues 2020-07-10 08:59:02 -04:00
Devin R eb5949dbc2 Move matrix-sdk to ruma monorepo 2020-07-10 08:59:00 -04:00
Denis Kasak 7943baee49 add_member provably always returns true. 2020-07-10 11:08:40 +02:00
Denis Kasak 7abdeed449 fix: Don't issue a disambiguation in case of a unique display name. 2020-07-10 11:08:40 +02:00
Denis Kasak eeebb43e32 Move mutating methods from RoomMember to Room.
The `update_profile` method cannot live in `RoomMember` since that
operation needs information which only exists in `Room` (for instance,
it needs other members in order to perform display name disambiguation).

Leaving other mutating methods on `RoomMember` (like `update_power` and
`update_presence`) then seemed illogical so they were all moved into
`Room`.

In addition, a small refactoring was done to remove
`did_update_presence` and `update_presence` since their existence
doesn't make much sense anymore and it saves us from repeating work.
Their function is now done in `receive_presence_event`.

Also, several docstrings were corrected and reworded.
2020-07-10 11:08:38 +02:00
Denis Kasak 5f49dab1fa Correct docstring. 2020-07-10 11:05:51 +02:00
Denis Kasak 6cacf83661 Add (failing) test for displayname disambiguation on profile updates. 2020-07-10 11:05:51 +02:00
Denis Kasak 599c1ba98f Add test to ensure member is only treated as joined or invited, not both. 2020-07-10 11:05:51 +02:00
Denis Kasak c2ec69cf44 Style fixes (comment grammar and correctness, whitespace). 2020-07-10 11:05:51 +02:00
Denis Kasak 32bdcede0c Small refactoring to simplify member_disambiguations. 2020-07-10 11:05:51 +02:00
Denis Kasak 9af48920f6 Add some TODOs and FIXMEs. 2020-07-10 11:05:51 +02:00
Damir Jelić a3441429da matrix-sdk: Add an autojoin example. 2020-07-08 20:22:50 +02:00
Damir Jelić 9f957655d0 travis: Allow the wasm target to fail. 2020-07-07 17:40:00 +02:00
Damir Jelić 2663a17065 travis: Allow windows to fail for now. 2020-07-07 17:19:26 +02:00
Damir Jelić 7124235d1b travis: Add a windows test target. 2020-07-07 16:57:20 +02:00
Damir Jelić babbdb4b90 travis: Remove redundant targets. 2020-07-07 16:52:17 +02:00
Damir Jelić 18f6cbc23a travis: Add a lint stage. 2020-07-07 16:48:45 +02:00
Damir Jelić 583dbb07a5 Merge branch 'deps-bump' 2020-07-07 16:13:25 +02:00
Damir Jelić 25207a1586 matrix-sdk: Make the wasm feature for future-timers target specific. 2020-07-07 16:11:33 +02:00
Damir Jelić 283cf0d782 matrix-sdk: Bump all our deps. 2020-07-07 15:52:08 +02:00
Damir Jelić 98d36d0ef0 base: Only update the tracked users when we're done with the state and timeline. 2020-07-07 15:48:28 +02:00
Damir Jelić f33298b1a6 matrix-sdk: Explain what needs to be done to restore a client. 2020-07-07 15:47:34 +02:00
Damir Jelić 11aa306de2 base: Swap around the store creation.
The state store creates directory structure but the crypto store does
not.

This can lead to confusing errors where we require a directory to be
created by the library user but it gets created by the library.
2020-07-06 10:54:01 +02:00
Damir Jelić 669a3f22d2 matrix-sdk: Allow getting the user id from the client. 2020-07-05 16:47:38 +02:00
Denis Kasak ff5f638b60 Remove member from invited_members when he joins. 2020-07-03 15:35:54 +02:00
Denis Kasak 2a0c6c6474 Add test and example event to ensure display name changes work correctly. 2020-07-03 15:35:54 +02:00
Denis Kasak f447c55fcb Move prev_content in test data to top level for now.
Until Ruma fixes it upstream, see hoist_room_event_prev_content for more
information.
2020-07-03 15:35:54 +02:00
Denis Kasak 84fc662614 Document and improve EventBuilder.
EventBuilder now clears itself between `build_sync_response` calls so
that each subsequent call will return an empty response if nothing was
added.

This allows reuse of a single EventBuilder instance which is important
for correct sync token rotation.
2020-07-03 15:35:54 +02:00
Denis Kasak c57f076375 Remove unused import. 2020-07-03 15:35:54 +02:00
Denis Kasak 4561b94f33 Remove outdated TODO. 2020-07-03 15:35:54 +02:00
Denis Kasak 9bd8699e18 Get rid of match on membership change in RoomMember::update_profile.
The calling method already did this when it determined that
update_profile should be called so we don't need to repeat it.
2020-07-03 15:35:54 +02:00
Denis Kasak 5ef9a7b924 tests: Rename get_room_id to test_room_id.
To make it more obvious it's a special room ID value used in tests.
2020-07-03 15:35:54 +02:00
Damir Jelić bd56c52b37 base: Don't double borrow the response in one iter rooms method. 2020-07-03 12:30:57 +02:00
Denis Kasak 1f25c4cf4b Fix and test hoisting of prev_content for timeline events.
The previous test only tested using the `EventEmitter`, which missed the
fact that the client was receiving unhoisted events. The test now also
tests the client state to detect this.
2020-07-03 11:54:08 +02:00
Denis Kasak 3f1a40a7d1 Add a bunch of FIXMEs to have receive_* methods do the emitting. 2020-07-03 10:31:47 +02:00
Damir Jelić b092ed0a82 base: Put the decrypted event replacing in the correct place. 2020-07-02 23:16:56 +02:00
Damir Jelić cd9252cc3d matrix-sdk: Remove an unused import. 2020-06-26 18:21:44 +02:00
Damir Jelić 8b13602b3b Merge branch 'room-search' 2020-06-26 10:15:31 +02:00
Devin R 92a43e7685 Move test data to test crate, fix docs 2020-06-25 08:31:51 -04:00
Damir Jelić 262a61afc9 crypto: Simplify the group session pair creation. 2020-06-25 13:31:30 +02:00
Devin R 1016519bb6 matrix_sdk: Rename public room builder and client methods
Remove 'get' from get_public_rooms* methods.
Rename RoomSearchBuilder -> RoomListFilterBuilder.
Use u32 over UInt in builders and Into<String> for String.
Fix docs of public room methods and builders.
2020-06-24 07:46:40 -04:00
Devin R 4dbe785bd7 matrix_sdk: Add get_public_rooms* methods to Client
This also adds a RoomSearchBuilder for making get_public_rooms_filtered
requests and a test for each method.
2020-06-24 06:54:45 -04:00
Damir Jelić 676d547161 matrix-sdk: Disable the tarpaulin skip lines since it fails to run with them. 2020-06-24 11:25:31 +02:00
Damir Jelić 6a670163d3 Merge branch 'feature/display-name' 2020-06-24 10:42:58 +02:00
Damir Jelić b8c4d1d5fa matrix-sdk: Remove the last test_data folder and fix the remaining tests. 2020-06-24 10:07:03 +02:00
Devin R 9e738f45ef crypto/base: Finish moving to using static json values for test data 2020-06-22 16:18:12 -04:00
Devin R 8e8ac8c5ac matrix_sdk_base: Use test_json values for tests in base 2020-06-21 14:22:28 -04:00
Devin R 4a7b3a103c matrix_sdk_test: Use static JSON values instead of reading files 2020-06-21 14:13:26 -04:00
Devin R fc077bcd6b matrix-sdk-test: Remove duplicate test_data folder, leave top-level 2020-06-21 14:13:03 -04:00
Denis Kasak c0c02baffc Run cargo fmt and apply clippy lints. 2020-06-20 13:05:16 +02:00
Denis Kasak 1174ccfc89 Merge branch 'master' into feature/display-name 2020-06-20 12:54:46 +02:00
Denis Kasak 733689870e Fix compilation error and remaining test.
Ref. for compilation error:
https://github.com/rust-lang/rust/issues/64552
2020-06-20 12:51:02 +02:00
Marcel 255451b8c7 Add missing dependency matrix-sdk-common-macros to matrix-sdk 2020-06-17 19:42:07 +02:00
Marcel d4087a1aae Fix cargo fmt issues that the local version didn't auto fix 2020-06-17 19:16:04 +02:00
Marcel f07ac5d679 Commit missing matrix_sdk_common_macros folder 2020-06-17 19:08:26 +02:00
Marcel 8b77b4171a Do wasm sepcific changes:
- Only use send+sync when not using wasm
- Use wasm capabale async_trait wrapper macro
- Make room and room_member specific structs always clonable
2020-06-17 18:57:39 +02:00
Damir Jelić ea427cf366 Merge branch 'upload-keys' 2020-06-17 09:33:09 +02:00
Devin R 15191d0230 crypto: Fix overflow in should_upload_keys, bail out if uploaded keys > max uploaded 2020-06-16 18:07:13 -04:00
Denis Kasak 5bd3c49afc Correctly handle disambiguation for exiting members, refactor and test. 2020-06-15 17:29:38 +02:00
Denis Kasak 765487dd9f Fix comment style. 2020-06-15 17:29:38 +02:00
Denis Kasak 03e53e991b Hoist prev_content to top-level in both timeline and state events.
Also refactor and document why this hoisting is needed.

This change makes the user_presence test fail because the hoisting
exposes an error encoded into the test's expected result.

Previously, the test expected 2 members in the room at the end. This is
incorrect since one of the members in the test data leaves the room.
However, since the prev_content of state events was previously not
hoisted to the top level, the `membership_change` method would not
notice it and thus not realize the member had left the room. The test
was corrected to expect only a single member in the room.

Another test change was made due to a limitation of EventBuilder: due to
the fact that it makes the test data go through a de/ser cycle, it
cannot easily hoist prev_content to the top level. Because of this, the
tests were change to put prev_content into the top level from the
outset.
2020-06-15 17:21:26 +02:00
Damir Jelić c3373f796b Merge branch 'export-base-error' 2020-06-15 09:47:51 +02:00
Damir Jelić 311e41ee0d matrix-sdk: Fix the author field in the cargo files. 2020-06-15 09:47:13 +02:00
Devin R f8b5fceeb1 matrix-sdk: Export matrix-sdk-base Error type as BaseError 2020-06-14 20:00:41 -04:00
Denis Kasak 97b1bb6004 Must not take our user into account when calculating room name. 2020-06-10 22:53:31 +02:00
Denis Kasak 331cb02266 Split joined/invited users and handle removing users. 2020-06-10 18:12:27 +02:00
Denis Kasak 7751605e37 Nix RoomMember::update_member and tracking membership.
After discussing with poljar, we concluded we don't actually need to
tracking membership state, since we won't be tracking users that
left (banned, kicked, disinvited).

The only thing we need to keep track of is the difference between joined
and invited users which will be dealt with in a separate commit.
2020-06-10 16:36:51 +02:00
Denis Kasak a0eaa9c364 Implement RoomMember::unique_name.
This gives us a name that is as ergonomic as possible while guaranteeing
it is unique.
2020-06-10 14:44:41 +02:00
Denis Kasak 241d456a81 Add RoomMember::name.
Returns the most ergonomic name for the member (either the display name
(if set) or the MXID).
2020-06-10 14:39:12 +02:00
Denis Kasak 3e5b6bb460 Style fixes. 2020-06-10 12:04:58 +02:00
Denis Kasak 5868c72662 Small refactor so we don't duplicate user_id creation. 2020-06-10 12:01:01 +02:00
Denis Kasak 4c184a30a2 Add doc comment to RoomName::calculate_name. 2020-06-10 00:28:56 +02:00
Denis Kasak e4977d1d2a Refactor member_display_name.
Make it more readable, add comments.
2020-06-09 23:02:01 +02:00
Denis Kasak ac069152b9 Retrieve user id from RoomMember instead of reconstructing. 2020-06-09 22:19:51 +02:00
Denis Kasak 82827542b7 fixup: explicit type annotations 2020-06-09 19:31:01 +02:00
Denis Kasak 20a8e8e49b Fix comment styling. 2020-06-09 19:24:00 +02:00
Denis Kasak 098cc1f9f8 Add explicit type annotation. 2020-06-09 19:08:14 +02:00
Denis Kasak a3c46c6144 Run cargo fmt. 2020-06-09 16:41:26 +02:00
Damir Jelić f35fbdf8b0 Merge branch 'register' 2020-06-09 16:30:01 +02:00
Damir Jelić 442464add6 matrix-sdk: Implement sending of Http DELETE requests. 2020-06-09 16:29:17 +02:00
Damir Jelić abe40dff11 matrix_sdk: Remove code duplication in our send methods. 2020-06-09 16:28:54 +02:00
Denis Kasak b93eb0e318 Make Room::member_display_name return MXID as fallback.
If there is no display name set. This means the method can now always
return something so there is no need to wrap in an `Option`.
2020-06-09 16:16:21 +02:00
Denis Kasak e6b67e5fa7 Add short explanation to Room::member_display_name. 2020-06-09 15:35:43 +02:00
Denis Kasak 22ba253103 Use "disambiguated" instead of "resolved" display name in the doc comment.
To match how the C2S spec calls it.
2020-06-09 15:29:37 +02:00
Denis Kasak a9fd63fd4b Fix display name disambiguation so it passes the test. 2020-06-09 15:20:21 +02:00
Denis Kasak 60a43439e5 Properly test for display name disambiguation. 2020-06-09 15:20:21 +02:00
Denis Kasak b6d7939685 matrix-sdk: Vary sync token with each EventBuilder::build_sync_response call.
This allows us to hold onto an EventBuilder object and use it to build
multiple sync responses. Previously this would have resulted in each
of the responses having the same next_batch sync token. This would make
clients ignore the latter responses if they have already received any of
the previous ones.
2020-06-09 15:20:21 +02:00
Denis Kasak 4df0a839aa Fix Markdown in doc comment. 2020-06-09 15:20:21 +02:00
Denis Kasak e3cb3566bf Rename display_names -> disambiguated_display_names. 2020-06-09 15:20:21 +02:00
Valentin Brandl 9f34615869 Add first test for display names 2020-06-09 12:33:24 +02:00
Valentin Brandl 05503b28b7 Only add name duplicates to the display name map 2020-06-09 12:33:24 +02:00
Valentin Brandl 49e913865d Fix failing test 2020-06-09 12:33:06 +02:00
Valentin Brandl 4675a72e6b Rename accessor for display name 2020-06-09 12:30:12 +02:00
Valentin Brandl d5f66631c1 Implement display name resolving 2020-06-09 12:30:12 +02:00
Devin R 81baca2f92 base_client: emit typing events and test using EventEmitter 2020-06-06 17:00:29 -04:00
Damir Jelić 6e5870bd2b crypto: Simplify the max keys calculation for one-time key uploads. 2020-06-04 17:36:33 +02:00
Devin R 6df1f12b45 async_client: add docs/test for register_user, send_uiaa and RegistrationBuilder 2020-06-02 17:13:29 -04:00
Devin R 5abac19b72 request_builder/async_client: add register endpoint and RegistrationBuilder for making the request 2020-06-02 17:13:01 -04:00
Damir Jelić 62e959a94d Merge branch 'expose-send' 2020-06-02 11:20:47 +02:00
Damir Jelić 54871f2af9 matrix-sdk: Make the example for the send method comiple. 2020-06-02 11:15:04 +02:00
Marcel 6a323525b5 Add example to the Client::send() doccomment 2020-06-02 10:40:50 +02:00
Marcel 1d00f79675 Run cargo fmt for the get_profiles example 2020-06-02 10:40:32 +02:00
Marcel 7201749280 Add small example on how to use Client::send 2020-06-02 10:39:50 +02:00
Damir Jelić 5175cd8ddb crypto: Remove some unnecessary mem::replace calls. 2020-06-02 10:36:51 +02:00
Damir Jelić 21b33f4e61 Merge branch 'doc-fix' 2020-06-02 10:31:09 +02:00
Damir Jelić 9f34b371be Merge branch 'unify-ee-methods' 2020-06-02 10:30:20 +02:00
Damir Jelić 587614cdd7 Merge branch 'unrecognized' 2020-06-02 10:28:57 +02:00
Devin R db38bf1276 event_emitter: use enum to represent custom events and raw json 2020-06-01 17:02:12 -04:00
Devin R 761071dac5 base_client: fix doc grammer and consistency, group request methods together 2020-06-01 07:50:45 -04:00
Devin R 8f017e7b27 event-emitter: rename on_account_data_* -> on_non_room_* 2020-06-01 07:13:57 -04:00
Devin R b1864887aa matrix-sdk: enable messages feature by default 2020-06-01 06:45:38 -04:00
Devin R 9cb86596d8 add support for custom events and unrecognized by ruma events, test new code 2020-05-29 17:36:58 -04:00
Damir Jelić 8ee6c3bdc8 matrix-sdk: Don't require Send for the sync callback. 2020-05-29 09:39:17 +02:00
Damir Jelić 16f4021800 common: Depend on the git version of futures-locks again. 2020-05-26 22:21:03 +02:00
Emi Simpson 53876ea6e8 Make Client::send a public method, add a short doccomment 2020-05-20 14:24:35 -04:00
240 changed files with 79402 additions and 13955 deletions
+258
View File
@@ -0,0 +1,258 @@
name: CI
on:
push:
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
style:
name: Check style
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
components: rustfmt
profile: minimal
override: true
- name: Cargo fmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Run clippy
needs: [style]
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy
profile: minimal
override: true
- name: Clippy
uses: actions-rs/cargo@v1
with:
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]
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
strategy:
matrix:
name:
- linux / stable
- linux / beta
- macOS / stable
include:
- name: linux / stable
- name: linux / beta
rust: beta
- name: macOS / stable
os: macOS-latest
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: Build
uses: actions-rs/cargo@v1
with:
command: build
- name: Test
uses: actions-rs/cargo@v1
with:
command: test
+39
View File
@@ -0,0 +1,39 @@
name: Code coverage
on:
push:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
code_coverage:
name: Code Coverage
runs-on: "ubuntu-latest"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Install tarpaulin
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-tarpaulin -f
- name: Run tarpaulin
uses: actions-rs/cargo@v1
with:
command: tarpaulin
args: --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml
- name: Upload to codecov.io
uses: codecov/codecov-action@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/
+2
View File
@@ -1,2 +1,4 @@
Cargo.lock
target
master.zip
emsdk-*
+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"
-53
View File
@@ -1,53 +0,0 @@
language: rust
rust: stable
addons:
apt:
packages:
- libssl-dev
jobs:
include:
- os: linux
dist: bionic
- os: osx
- os: linux
name: Coverage
before_script:
- cargo install cargo-tarpaulin
script:
- cargo tarpaulin --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
before_script:
- rustup component add rustfmt
script:
- cargo fmt --all -- --check
- 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"
]
+3 -1
View File
@@ -1,7 +1,9 @@
[![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)
![Build Status](https://img.shields.io/github/workflow/status/matrix-org/matrix-rust-sdk/CI?style=flat-square)
[![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))
}
+78 -26
View File
@@ -1,5 +1,5 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "A high level Matrix client-server library."
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -8,45 +8,97 @@ license = "Apache-2.0"
name = "matrix-sdk"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.3.0"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = ["encryption", "sqlite-cryptostore"]
messages = ["matrix-sdk-base/messages"]
default = ["encryption", "sled_cryptostore", "sled_state_store", "require_auth_for_profile_requests", "native-tls"]
encryption = ["matrix-sdk-base/encryption"]
sqlite-cryptostore = ["matrix-sdk-base/sqlite-cryptostore"]
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", "sled_cryptostore", "sled_state_store", "sso_login"]
[dependencies]
http = "0.2.1"
reqwest = "0.10.4"
serde_json = "1.0.53"
thiserror = "1.0.19"
tracing = "0.1.14"
url = "2.1.1"
futures-timer = { version = "3.0.2", features = ["wasm-bindgen"] }
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.1.0", path = "../matrix_sdk_common" }
matrix-sdk-common = { version = "0.3.0", path = "../matrix_sdk_common" }
[dependencies.matrix-sdk-base]
version = "0.1.0"
version = "0.3.0"
path = "../matrix_sdk_base"
default_features = false
[dependencies.reqwest]
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"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
futures-timer = "3.0.2"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "1.7.1"
default-features = false
features = ["fs", "rt"]
[target.'cfg(target_arch = "wasm32")'.dependencies.futures-timer]
version = "3.0.2"
features = ["wasm-bindgen"]
[dev-dependencies]
async-trait = "0.1.31"
dirs = "2.0.2"
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
serde_json = "1.0.53"
tracing-subscriber = "0.2.5"
tempfile = "3.1.0"
mockito = "0.25.1"
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.5"
[[example]]
name = "emoji_verification"
required-features = ["encryption"]
+104
View File
@@ -0,0 +1,104 @@
use std::{env, process::exit};
use matrix_sdk::{
self, async_trait,
events::{room::member::MemberEventContent, StrippedStateEvent},
room::Room,
Client, ClientConfig, EventHandler, SyncSettings,
};
use tokio::time::{sleep, Duration};
use url::Url;
struct AutoJoinBot {
client: Client,
}
impl AutoJoinBot {
pub fn new(client: Client) -> Self {
Self { client }
}
}
#[async_trait]
impl EventHandler for AutoJoinBot {
async fn on_stripped_state_member(
&self,
room: Room,
room_member: &StrippedStateEvent<MemberEventContent>,
_: Option<MemberEventContent>,
) {
if room_member.state_key != self.client.user_id().await.unwrap() {
return;
}
if let Room::Invited(room) = room {
println!("Autojoining room {}", room.room_id());
let mut delay = 2;
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
);
sleep(Duration::from_secs(delay)).await;
delay *= 2;
if delay > 3600 {
eprintln!("Can't join room {} ({:?})", room.room_id(), err);
break;
}
}
println!("Successfully joined room {}", room.room_id());
}
}
}
async fn login_and_sync(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let mut home = dirs::home_dir().expect("no home directory found");
home.push("autojoin_bot");
let client_config = ClientConfig::new().store_path(home);
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();
client.login(username, password, None, Some("autojoin bot")).await?;
println!("logged in as {}", username);
client.set_event_handler(Box::new(AutoJoinBot::new(client.clone()))).await;
client.sync(SyncSettings::default()).await;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
login_and_sync(homeserver_url, &username, &password).await?;
Ok(())
}
+43 -59
View File
@@ -1,56 +1,52 @@
use std::{env, process::exit};
use matrix_sdk::{
self,
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings,
self, async_trait,
events::{
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
AnyMessageEventContent, SyncMessageEvent,
},
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_forever`.
client: Client,
}
struct CommandBot;
impl CommandBot {
pub fn new(client: Client) -> Self {
Self { client }
pub fn new() -> Self {
Self {}
}
}
#[async_trait::async_trait]
impl EventEmitter for CommandBot {
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
if let SyncRoom::Joined(room) = room {
let msg_body = if let MessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
#[async_trait]
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 {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
} = event
{
msg_body.clone()
msg_body
} else {
String::new()
return;
};
if msg_body.contains("!party") {
let content = MessageEventContent::Text(TextMessageEventContent {
body: "🎉🎊🥳 let's PARTY!! 🥳🎊🎉".to_string(),
format: None,
formatted_body: None,
relates_to: None,
});
// we clone here to hold the lock for as little time as possible.
let room_id = room.read().await.room_id.clone();
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::text_plain(
"🎉🎊🥳 let's PARTY!! 🥳🎊🎉",
));
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,42 +63,30 @@ 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.clone(),
password,
None,
Some("command bot".to_string()),
)
.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
client.sync(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;
// 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.set_event_handler(Box::new(CommandBot::new())).await;
// since we called sync before we `sync_forever` we must pass that sync token to
// `sync_forever`
// 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
client.sync_forever(settings, |_| async {}).await;
// this keeps state from the server streaming in to CommandBot via the
// EventHandler trait
client.sync(settings).await;
Ok(())
}
@@ -0,0 +1,107 @@
use std::{
collections::BTreeMap,
env, io,
process::exit,
sync::atomic::{AtomicBool, Ordering},
};
use matrix_sdk::{
self, api::r0::uiaa::AuthData, identifiers::UserId, Client, LoopCtrl, SyncSettings,
};
use serde_json::json;
use url::Url;
fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> {
let mut auth_parameters = BTreeMap::new();
let identifier = json!({
"type": "m.id.user",
"user": user,
});
auth_parameters.insert("identifier".to_owned(), identifier);
auth_parameters.insert("password".to_owned(), password.to_owned().into());
AuthData::DirectRequest { kind: "m.login.password", auth_parameters, session }
}
async fn bootstrap(client: Client, user_id: UserId, password: String) {
println!("Bootstrapping a new cross signing identity, press enter to continue.");
let mut input = String::new();
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 {
if let Some(response) = e.uiaa_response() {
let auth_data = auth_data(&user_id, &password, response.session.as_deref());
client
.bootstrap_cross_signing(Some(auth_data))
.await
.expect("Couldn't bootstrap cross signing")
} else {
panic!("Error during cross-signing bootstrap {:#?}", e);
}
}
#[cfg(not(feature = "encryption"))]
panic!("Cross signing requires the encryption feature to be enabled");
}
async fn login(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new(homeserver_url).unwrap();
let response = client.login(username, password, None, Some("rust-sdk")).await?;
let user_id = &response.user_id;
let client_ref = &client;
let asked = AtomicBool::new(false);
let asked_ref = &asked;
client
.sync_with_callback(SyncSettings::new(), |_| async move {
let asked = asked_ref;
let client = &client_ref;
let user_id = &user_id;
let password = &password;
// Wait for sync to be done then ask the user to bootstrap.
if !asked.load(Ordering::SeqCst) {
tokio::spawn(bootstrap(
(*client).clone(),
(*user_id).clone(),
password.to_string(),
));
}
asked.store(true, Ordering::SeqCst);
LoopCtrl::Continue
})
.await;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
login(homeserver_url, &username, &password).await
}
+199
View File
@@ -0,0 +1,199 @@
use std::{
env, io,
process::exit,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
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");
match input.trim().to_lowercase().as_ref() {
"yes" | "true" | "ok" => {
sas.confirm().await.unwrap();
if sas.is_done() {
print_result(&sas);
print_devices(sas.other_device().user_id(), &client).await;
}
}
_ => sas.cancel().await.unwrap(),
}
}
fn print_result(sas: &SasVerification) {
let device = sas.other_device();
println!(
"Successfully verified device {} {} {:?}",
device.user_id(),
device.device_id(),
device.local_trust_state()
);
}
async fn print_devices(user_id: &UserId, client: &Client) {
println!("Devices of user {}", user_id);
for device in client.get_user_devices(user_id).await.unwrap().devices() {
println!(
" {:<10} {:<30} {:<}",
device.device_id(),
device.display_name().as_deref().unwrap_or_default(),
device.is_trusted()
);
}
}
async fn login(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<(), matrix_sdk::Error> {
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new(homeserver_url).unwrap();
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.iter().filter_map(|e| e.deserialize().ok()) {
match event {
AnyToDeviceEvent::KeyVerificationStart(e) => {
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) => {
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) => {
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;
}
}
}
_ => (),
}
}
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;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <username> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
login(homeserver_url, &username, &password).await
}
+70
View File
@@ -0,0 +1,70 @@
use std::{convert::TryFrom, env, process::exit};
use matrix_sdk::{
self,
api::r0::profile,
identifiers::{MxcUri, UserId},
Client, Result as MatrixResult,
};
use url::Url;
#[derive(Debug)]
struct UserProfile {
avatar_url: Option<MxcUri>,
displayname: Option<String>,
}
/// This function calls the GET profile endpoint
/// Spec: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-profile-userid
/// Ruma: https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/index.html
async fn get_profile(client: Client, mxid: &UserId) -> MatrixResult<UserProfile> {
// First construct the request you want to make
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/index.html for all available Endpoints
let request = profile::get_profile::Request::new(mxid);
// Start the request using matrix_sdk::Client::send
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 };
Ok(user_profile)
}
async fn login(
homeserver_url: String,
username: &str,
password: &str,
) -> Result<Client, matrix_sdk::Error> {
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new(homeserver_url).unwrap();
client.login(username, password, None, Some("rust-sdk")).await?;
Ok(client)
}
#[tokio::main]
async fn main() -> Result<(), matrix_sdk::Error> {
tracing_subscriber::fmt::init();
let (homeserver_url, username, password) =
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
(Some(a), Some(b), Some(c)) => (a, b, c),
_ => {
eprintln!(
"Usage: {} <homeserver_url> <mxid> <password>",
env::args().next().unwrap()
);
exit(1)
}
};
let client = login(homeserver_url, &username, &password).await?;
let user_id = UserId::try_from(username).expect("Couldn't parse the MXID");
let profile = get_profile(client, &user_id).await?;
println!("{:#?}", profile);
Ok(())
}
+106
View File
@@ -0,0 +1,106 @@
use std::{
env,
fs::File,
io::{Seek, SeekFrom},
path::PathBuf,
process::exit,
sync::Arc,
};
use matrix_sdk::{
self, async_trait,
events::{
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
SyncMessageEvent,
},
room::Room,
Client, EventHandler, SyncSettings,
};
use tokio::sync::Mutex;
use url::Url;
struct ImageBot {
image: Arc<Mutex<File>>,
}
impl ImageBot {
pub fn new(image: File) -> Self {
let image = Arc::new(Mutex::new(image));
Self { image }
}
}
#[async_trait]
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 {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
} = event
{
msg_body
} else {
return;
};
if msg_body.contains("!image") {
println!("sending image");
let mut image = self.image.lock().await;
room.send_attachment("cat", &mime::IMAGE_JPEG, &mut *image, None).await.unwrap();
image.seek(SeekFrom::Start(0)).unwrap();
println!("message sent");
}
}
}
}
async fn login_and_sync(
homeserver_url: String,
username: String,
password: String,
image: File,
) -> Result<(), matrix_sdk::Error> {
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
let client = Client::new(homeserver_url).unwrap();
client.login(&username, &password, None, Some("command bot")).await?;
client.sync_once(SyncSettings::default()).await.unwrap();
client.set_event_handler(Box::new(ImageBot::new(image))).await;
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client.sync(settings).await;
Ok(())
}
#[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)
}
};
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.");
login_and_sync(homeserver_url, username, password, image).await?;
Ok(())
}
+27 -33
View File
@@ -1,35 +1,34 @@
use std::{env, process::exit};
use url::Url;
use matrix_sdk::{
self,
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
self, async_trait,
events::{
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
SyncMessageEvent,
},
room::Room,
Client, EventHandler, SyncSettings,
};
use url::Url;
struct EventCallback;
#[async_trait::async_trait]
impl EventEmitter for EventCallback {
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
if let SyncRoom::Joined(room) = room {
if let MessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
#[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 name = {
// any reads should be held for the shortest time possible to
// avoid dead locks
let room = room.read().await;
let member = room.members.get(&sender).unwrap();
member
.display_name
.as_ref()
.map(ToString::to_string)
.unwrap_or(sender.to_string())
};
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);
}
}
@@ -38,21 +37,16 @@ impl EventEmitter for EventCallback {
async fn login(
homeserver_url: String,
username: String,
password: String,
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".to_string()))
.await?;
client.sync_forever(SyncSettings::new(), |_| async {}).await;
client.login(username, password, None, Some("rust-sdk")).await?;
client.sync(SyncSettings::new()).await;
Ok(())
}
@@ -73,5 +67,5 @@ async fn main() -> Result<(), matrix_sdk::Error> {
}
};
login(homeserver_url, username, password).await
login(homeserver_url, &username, &password).await
}
@@ -10,10 +10,15 @@ edition = "2018"
crate-type = ["cdylib"]
[dependencies]
matrix-sdk = { path = "../..", default-features = false }
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"
+47 -24
View File
@@ -1,9 +1,11 @@
use matrix_sdk::{
api::r0::sync::sync_events::Response as SyncResponse,
events::collections::all::RoomEvent,
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
deserialized_responses::SyncResponse,
events::{
room::message::{MessageEventContent, MessageType, TextMessageEventContent},
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent,
},
identifiers::RoomId,
Client, ClientConfig, SyncSettings,
Client, LoopCtrl, SyncSettings,
};
use url::Url;
use wasm_bindgen::prelude::*;
@@ -12,49 +14,70 @@ use web_sys::console;
struct WasmBot(Client);
impl WasmBot {
async fn on_room_message(&self, room_id: &RoomId, event: RoomEvent) {
let msg_body = if let RoomEvent::RoomMessage(MessageEvent {
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
async fn on_room_message(
&self,
room_id: &RoomId,
event: &SyncMessageEvent<MessageEventContent>,
) {
let msg_body = if let SyncMessageEvent {
content:
MessageEventContent {
msgtype: MessageType::Text(TextMessageEventContent { body: msg_body, .. }),
..
},
..
}) = event
} = 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 = MessageEventContent::Text(TextMessageEventContent::new_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) {
console::log_1(&format!("Synced").into());
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() {
self.on_room_message(&room_id, event).await
if let Ok(AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev))) = event.event.deserialize() {
self.on_room_message(&room_id, &ev).await
}
}
}
LoopCtrl::Continue
}
}
#[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"))
@@ -63,11 +86,11 @@ pub async fn run() -> Result<JsValue, JsValue> {
let bot = WasmBot(client.clone());
client.sync(SyncSettings::default()).await.unwrap();
client.sync_once(SyncSettings::default()).await.unwrap();
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
client
.sync_forever(settings, |response| bot.on_sync_response(response))
.sync_with_callback(settings, |response| bot.on_sync_response(response))
.await;
Ok(JsValue::NULL)
+3157 -1383
View File
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
// 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, result::Result as StdResult};
use matrix_sdk_base::crypto::{
store::CryptoStoreError, Device as BaseDevice, LocalTrust, ReadOnlyDevice,
UserDevices as BaseUserDevices,
};
use ruma::{DeviceId, DeviceIdBox};
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) client: Client,
}
impl Deref for Device {
type Target = ReadOnlyDevice;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Device {
/// Start a interactive verification with this `Device`
///
/// Returns a `Sas` object that represents the interactive verification
/// flow.
///
/// # Example
///
/// ```no_run
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, identifiers::UserId};
/// # use url::Url;
/// # use futures::executor::block_on;
/// # let alice = UserId::try_from("@alice:example.org").unwrap();
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # let client = Client::new(homeserver).unwrap();
/// # block_on(async {
/// let device = client.get_device(&alice, "DEVICEID".into())
/// .await
/// .unwrap()
/// .unwrap();
///
/// let verification = device.start_verification().await.unwrap();
/// # });
/// ```
pub async fn start_verification(&self) -> Result<SasVerification> {
let (sas, request) = self.inner.start_verification().await?;
self.client.send_to_device(&request).await?;
Ok(SasVerification { inner: sas, client: self.client.clone() })
}
/// Is the device trusted.
pub fn is_trusted(&self) -> bool {
self.inner.trust_state()
}
/// Set the local trust state of the device to the given state.
///
/// This won't affect any cross signing trust state, this only sets a flag
/// marking to have the given trust state.
///
/// # Arguments
///
/// * `trust_state` - The new trust state that should be set for the device.
pub async fn set_local_trust(
&self,
trust_state: LocalTrust,
) -> StdResult<(), CryptoStoreError> {
self.inner.set_local_trust(trust_state).await
}
}
/// A read only view over all devices belonging to a user.
#[derive(Debug)]
pub struct UserDevices {
pub(crate) inner: BaseUserDevices,
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, client: self.client.clone() })
}
/// Iterator over all the device ids of the user devices.
pub fn keys(&self) -> impl Iterator<Item = &DeviceIdBox> {
self.inner.keys()
}
/// Iterator over all the devices of the user devices.
pub fn devices(&self) -> impl Iterator<Item = Device> + '_ {
let client = self.client.clone();
self.inner.devices().map(move |d| Device { inner: d, client: client.clone() })
}
}
+130 -26
View File
@@ -14,55 +14,159 @@
//! Error conditions.
use std::io::Error as IoError;
use http::StatusCode;
#[cfg(feature = "encryption")]
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 matrix_sdk_base::Error as MatrixError;
use crate::api::Error as RumaClientError;
use crate::FromHttpResponseError as RumaResponseError;
use crate::IntoHttpError as RumaIntoHttpError;
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.
#[error("the queried endpoint requires authentication but was called before logging in")]
AuthenticationRequired,
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 converting between ruma_*_api types and Hyper types.
#[error(transparent)]
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)]
SerdeJson(#[from] JsonError),
/// 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 IO error happened.
#[error(transparent)]
Io(#[from] IoError),
/// An error converting between ruma_client_api types and Hyper types.
#[error("can't convert between ruma_client_api and hyper types.")]
IntoHttp(RumaIntoHttpError),
/// An error occured in the Matrix client library.
/// An error occurred in the Matrix client library.
#[error(transparent)]
MatrixError(#[from] MatrixError),
/// An error occurred in the crypto store.
#[cfg(feature = "encryption")]
#[error(transparent)]
CryptoStoreError(#[from] CryptoStoreError),
/// 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 From<RumaResponseError<RumaClientError>> for Error {
fn from(error: RumaResponseError<RumaClientError>) -> Self {
Self::RumaResponse(error)
impl Error {
/// Try to destructure the error into an universal interactive auth info.
///
/// Some requests require universal interactive auth, doing such a request
/// will always fail the first time with a 401 status code, the response
/// body will contain info how the client can authenticate.
///
/// The request will need to be retried, this time containing additional
/// authentication data.
///
/// 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::Http(HttpError::UiaaError(FromHttpResponseError::Http(ServerError::Known(
UiaaError::AuthResponse(i),
)))) = self
{
Some(i)
} else {
None
}
}
}
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",
],
)
}
}
+364
View File
@@ -0,0 +1,364 @@
// 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.
#[cfg(all(not(target_arch = "wasm32")))]
use std::sync::atomic::{AtomicU64, Ordering};
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
#[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 crate::{error::HttpError, Bytes, BytesMut, ClientConfig, RequestConfig, Session};
/// Abstraction around the http layer. The allows implementors to use different
/// http libraries.
#[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.
///
/// # Arguments
///
/// * `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, async_trait, HttpError, RequestConfig, Bytes};
///
/// #[derive(Debug)]
/// struct Client(reqwest::Client);
///
/// impl Client {
/// async fn response_to_http_response(
/// &self,
/// mut response: reqwest::Response,
/// ) -> Result<http::Response<Bytes>, HttpError> {
/// // Convert the reqwest response to a http one.
/// todo!()
/// }
/// }
///
/// #[async_trait]
/// impl HttpSend for Client {
/// async fn send_request(
/// &self,
/// request: http::Request<Bytes>,
/// config: RequestConfig,
/// ) -> Result<http::Response<Bytes>, HttpError> {
/// Ok(self
/// .response_to_http_response(
/// self.0
/// .execute(reqwest::Request::try_from(request)?)
/// .await?,
/// )
/// .await?)
/// }
/// }
/// ```
async fn send_request(
&self,
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<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>>>,
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() {
SendAccessToken::IfRequired(session.access_token.as_str())
} else {
return Err(HttpError::AuthenticationRequired);
}
}
AuthScheme::None => SendAccessToken::None,
_ => return Err(HttpError::NotClientRequest),
}
};
let http_request = request
.try_into_http_request::<BytesMut>(
&self.homeserver.read().await.to_string(),
access_token,
)?
.map(|body| body.freeze());
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<'_>,
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,
config: Option<RequestConfig>,
) -> Result<Request::IncomingResponse, HttpError>
where
Request: OutgoingRequest + Debug,
HttpError: From<FromHttpResponseError<Request::EndpointError>>,
{
let response = self.send_request(request, self.session.clone(), config).await?;
trace!("Got response: {:?}", 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, HttpError> {
let http_client = reqwest::Client::builder();
#[cfg(not(target_arch = "wasm32"))]
let http_client = {
let http_client = if config.disable_ssl_verification {
http_client.danger_accept_invalid_certs(true)
} else {
http_client
};
let http_client = match &config.proxy {
Some(p) => http_client.proxy(p.clone()),
None => http_client,
};
let mut headers = reqwest::header::HeaderMap::new();
let user_agent = match &config.user_agent {
Some(a) => a.clone(),
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).timeout(config.request_config.timeout)
};
#[cfg(target_arch = "wasm32")]
#[allow(unused)]
let _ = config;
Ok(http_client.build()?)
}
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().expect("Can't get the response builder headers");
for (k, v) in response.headers_mut().drain() {
if let Some(key) = k {
headers.insert(key, v);
}
}
let body = response.bytes().await?;
Ok(http_builder.body(body).expect("Can't construct a response using the given body"))
}
#[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<Bytes>,
config: RequestConfig,
) -> Result<http::Response<Bytes>, HttpError> {
send_request(self, request, config).await
}
}
+85 -13
View File
@@ -15,15 +15,44 @@
//! This crate implements a [Matrix](https://matrix.org/) client library.
//!
//! ## Crate Feature Flags
//! # 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`):
//!
//! ```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 `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
//!
//! 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.
//! * `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,
@@ -35,23 +64,66 @@
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(feature = "docs", feature(doc_cfg))]
#[cfg(not(target_arch = "wasm32"))]
pub use matrix_sdk_base::JsonStore;
pub use matrix_sdk_base::{EventEmitter, Room, Session, SyncRoom};
pub use matrix_sdk_base::{RoomState, StateStore};
pub use matrix_sdk_common::*;
pub use reqwest::header::InvalidHeaderValue;
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls",)))]
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")]
pub use matrix_sdk_base::{Device, TrustState};
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use matrix_sdk_base::crypto::{EncryptionInfo, LocalTrust};
pub use matrix_sdk_base::{
media, Error as BaseError, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType,
Session, StateChanges, StoreError,
};
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 request_builder;
pub use client::{Client, ClientConfig, SyncSettings};
pub use error::{Error, Result};
pub use request_builder::{MessagesRequestBuilder, RoomBuilder};
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")]
pub mod verification;
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, HttpError, Result};
pub use event_handler::{CustomEvent, EventHandler};
pub use http_client::HttpSend;
pub use room_member::RoomMember;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
-386
View File
@@ -1,386 +0,0 @@
use crate::api;
use crate::events::room::power_levels::PowerLevelsEventContent;
use crate::events::EventJson;
use crate::identifiers::{RoomId, UserId};
use api::r0::filter::RoomEventFilter;
use api::r0::membership::Invite3pid;
use api::r0::message::get_message_events::{self, Direction};
use api::r0::room::{
create_room::{self, CreationContent, InitialStateEvent, RoomPreset},
Visibility,
};
use crate::js_int::UInt;
/// A builder used to create rooms.
///
/// # Examples
/// ```
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, RoomBuilder};
/// # use matrix_sdk::api::r0::room::Visibility;
/// # use matrix_sdk::identifiers::UserId;
/// # use url::Url;
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async {
/// let mut builder = RoomBuilder::default();
/// builder.creation_content(false)
/// .initial_state(vec![])
/// .visibility(Visibility::Public)
/// .name("name")
/// .room_version("v1.0");
/// let mut client = Client::new(homeserver).unwrap();
/// client.create_room(builder).await;
/// # })
/// ```
#[derive(Clone, Debug, Default)]
pub struct RoomBuilder {
/// Extra keys to be added to the content of the `m.room.create`.
creation_content: Option<CreationContent>,
/// List of state events to send to the new room.
///
/// Takes precedence over events set by preset, but gets overriden by
/// name and topic keys.
initial_state: Vec<InitialStateEvent>,
/// A list of user IDs to invite to the room.
///
/// This will tell the server to invite everyone in the list to the newly created room.
invite: Vec<UserId>,
/// List of third party IDs of users to invite.
invite_3pid: Vec<Invite3pid>,
/// If set, this sets the `is_direct` flag on room invites.
is_direct: Option<bool>,
/// If this is included, an `m.room.name` event will be sent into the room to indicate
/// the name of the room.
name: Option<String>,
/// Power level content to override in the default power level event.
power_level_content_override: Option<PowerLevelsEventContent>,
/// Convenience parameter for setting various default state events based on a preset.
preset: Option<RoomPreset>,
/// The desired room alias local part.
room_alias_name: Option<String>,
/// Room version to set for the room. Defaults to homeserver's default if not specified.
room_version: Option<String>,
/// If this is included, an `m.room.topic` event will be sent into the room to indicate
/// the topic for the room.
topic: Option<String>,
/// A public visibility indicates that the room will be shown in the published room
/// list. A private visibility will hide the room from the published room list. Rooms
/// default to private visibility if this key is not included.
visibility: Option<Visibility>,
}
impl RoomBuilder {
/// Returns an empty `RoomBuilder` for creating rooms.
pub fn new() -> Self {
Self::default()
}
/// Set the `CreationContent`.
///
/// Weather users on other servers can join this room.
pub fn creation_content(&mut self, federate: bool) -> &mut Self {
let federate = Some(federate);
self.creation_content = Some(CreationContent { federate });
self
}
/// Set the `InitialStateEvent` vector.
pub fn initial_state(&mut self, state: Vec<InitialStateEvent>) -> &mut Self {
self.initial_state = state;
self
}
/// Set the vec of `UserId`s.
pub fn invite(&mut self, invite: Vec<UserId>) -> &mut Self {
self.invite = invite;
self
}
/// Set the vec of `Invite3pid`s.
pub fn invite_3pid(&mut self, invite: Vec<Invite3pid>) -> &mut Self {
self.invite_3pid = invite;
self
}
/// Set the vec of `Invite3pid`s.
pub fn is_direct(&mut self, direct: bool) -> &mut Self {
self.is_direct = Some(direct);
self
}
/// Set the room name. A `m.room.name` event will be sent to the room.
pub fn name<S: Into<String>>(&mut self, name: S) -> &mut Self {
self.name = Some(name.into());
self
}
/// Set the room's power levels.
pub fn power_level_override(&mut self, power: PowerLevelsEventContent) -> &mut Self {
self.power_level_content_override = Some(power);
self
}
/// Convenience for setting various default state events based on a preset.
pub fn preset(&mut self, preset: RoomPreset) -> &mut Self {
self.preset = Some(preset);
self
}
/// The local part of a room alias.
pub fn room_alias_name<S: Into<String>>(&mut self, alias: S) -> &mut Self {
self.room_alias_name = Some(alias.into());
self
}
/// Room version, defaults to homeserver's version if left unspecified.
pub fn room_version<S: Into<String>>(&mut self, version: S) -> &mut Self {
self.room_version = Some(version.into());
self
}
/// If included, a `m.room.topic` event will be sent to the room.
pub fn topic<S: Into<String>>(&mut self, topic: S) -> &mut Self {
self.topic = Some(topic.into());
self
}
/// A public visibility indicates that the room will be shown in the published
/// room list. A private visibility will hide the room from the published room list.
/// Rooms default to private visibility if this key is not included.
pub fn visibility(&mut self, vis: Visibility) -> &mut Self {
self.visibility = Some(vis);
self
}
}
impl Into<create_room::Request> for RoomBuilder {
fn into(self) -> create_room::Request {
create_room::Request {
creation_content: self.creation_content,
initial_state: self.initial_state,
invite: self.invite,
invite_3pid: self.invite_3pid,
is_direct: self.is_direct,
name: self.name,
power_level_content_override: self.power_level_content_override.map(EventJson::from),
preset: self.preset,
room_alias_name: self.room_alias_name,
room_version: self.room_version,
topic: self.topic,
visibility: self.visibility,
}
}
}
/// Create a builder for making get_message_event requests.
///
/// # Examples
/// ```
/// # use std::convert::TryFrom;
/// # use matrix_sdk::{Client, MessagesRequestBuilder};
/// # use matrix_sdk::api::r0::message::get_message_events::{self, Direction};
/// # use matrix_sdk::identifiers::RoomId;
/// # use url::Url;
/// # let homeserver = Url::parse("http://example.com").unwrap();
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async {
/// # let room_id = RoomId::try_from("!test:localhost").unwrap();
/// # let last_sync_token = "".to_string();
/// let mut client = Client::new(homeserver).unwrap();
///
/// let mut builder = MessagesRequestBuilder::new();
/// builder.room_id(room_id)
/// .from(last_sync_token)
/// .direction(Direction::Forward);
///
/// client.room_messages(builder).await.is_err();
/// # })
/// ```
#[derive(Clone, Debug, Default)]
pub struct MessagesRequestBuilder {
/// The room to get events from.
room_id: Option<RoomId>,
/// The token to start returning events from.
///
/// This token can be obtained from a
/// prev_batch token returned for each room by the sync API, or from a start or end token
/// returned by a previous request to this endpoint.
from: Option<String>,
/// The token to stop returning events at.
///
/// This token can be obtained from a prev_batch
/// token returned for each room by the sync endpoint, or from a start or end token returned
/// by a previous request to this endpoint.
to: Option<String>,
/// The direction to return events from.
direction: Option<Direction>,
/// The maximum number of events to return.
///
/// Default: 10.
limit: Option<UInt>,
/// A filter of the returned events with.
filter: Option<RoomEventFilter>,
}
impl MessagesRequestBuilder {
/// Create a `MessagesRequestBuilder` builder to make a `get_message_events::Request`.
///
/// The `room_id` and `from`` fields **need to be set** to create the request.
pub fn new() -> Self {
Self::default()
}
/// RoomId is required to create a `get_message_events::Request`.
pub fn room_id(&mut self, room_id: RoomId) -> &mut Self {
self.room_id = Some(room_id);
self
}
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
///
/// This is required to create a `get_message_events::Request`.
pub fn from(&mut self, from: String) -> &mut Self {
self.from = Some(from);
self
}
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
///
/// This token signals when to stop receiving events.
pub fn to(&mut self, to: String) -> &mut Self {
self.to = Some(to);
self
}
/// The direction to return events from.
///
/// If not specified `Direction::Backward` is used.
pub fn direction(&mut self, direction: Direction) -> &mut Self {
self.direction = Some(direction);
self
}
/// The maximum number of events to return.
pub fn limit(&mut self, limit: UInt) -> &mut Self {
self.limit = Some(limit);
self
}
/// Filter events by the given `RoomEventFilter`.
pub fn filter(&mut self, filter: RoomEventFilter) -> &mut Self {
self.filter = Some(filter);
self
}
}
impl Into<get_message_events::Request> for MessagesRequestBuilder {
fn into(self) -> get_message_events::Request {
get_message_events::Request {
room_id: self.room_id.expect("`room_id` and `from` need to be set"),
from: self.from.expect("`room_id` and `from` need to be set"),
to: self.to,
dir: self.direction.unwrap_or(Direction::Backward),
limit: self.limit,
filter: self.filter,
}
}
}
#[cfg(test)]
mod test {
use std::collections::BTreeMap;
use super::*;
use crate::api::r0::filter::{LazyLoadOptions, RoomEventFilter};
use crate::events::room::power_levels::NotificationPowerLevels;
use crate::js_int::Int;
use crate::{identifiers::RoomId, Client, Session};
use mockito::{mock, Matcher};
use std::convert::TryFrom;
use url::Url;
#[tokio::test]
async fn create_room_builder() {
let homeserver = Url::parse(&mockito::server_url()).unwrap();
let _m = mock("POST", "/_matrix/client/r0/createRoom")
.with_status(200)
.with_body_from_file("../test_data/room_id.json")
.create();
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
};
let mut builder = RoomBuilder::new();
builder
.creation_content(false)
.initial_state(vec![])
.visibility(Visibility::Public)
.name("room_name")
.room_version("v1.0")
.invite_3pid(vec![])
.is_direct(true)
.power_level_override(PowerLevelsEventContent {
ban: Int::MAX,
events: BTreeMap::default(),
events_default: Int::MIN,
invite: Int::MIN,
kick: Int::MIN,
redact: Int::MAX,
state_default: Int::MIN,
users_default: Int::MIN,
notifications: NotificationPowerLevels { room: Int::MIN },
users: BTreeMap::default(),
})
.preset(RoomPreset::PrivateChat)
.room_alias_name("room_alias")
.topic("room topic")
.visibility(Visibility::Private);
let cli = Client::new(homeserver).unwrap();
cli.restore_login(session).await.unwrap();
assert!(cli.create_room(builder).await.is_ok());
}
#[tokio::test]
async fn get_message_events() {
let homeserver = Url::parse(&mockito::server_url()).unwrap();
let _m = mock(
"GET",
Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/messages".to_string()),
)
.with_status(200)
.with_body_from_file("../test_data/room_messages.json")
.create();
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
};
let mut builder = MessagesRequestBuilder::new();
builder
.room_id(RoomId::try_from("!roomid:example.com").unwrap())
.from("t47429-4392820_219380_26003_2265".to_string())
.to("t4357353_219380_26003_2265".to_string())
.direction(Direction::Backward)
.limit(UInt::new(10).unwrap())
.filter(RoomEventFilter {
lazy_load_options: LazyLoadOptions::Enabled {
include_redundant_members: false,
},
..Default::default()
});
let cli = Client::new(homeserver).unwrap();
cli.restore_login(session).await.unwrap();
assert!(cli.room_messages(builder).await.is_ok());
}
}
+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)
}
}
}
+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(())
}
}
+42 -19
View File
@@ -1,5 +1,5 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "The base component to build a Matrix client library."
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -8,39 +8,62 @@ license = "Apache-2.0"
name = "matrix-sdk-base"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.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"]
sled_state_store = ["sled", "pbkdf2", "hmac", "sha2", "rand", "chacha20poly1305"]
sled_cryptostore = ["matrix-sdk-crypto/sled_cryptostore"]
markdown = ["ruma/markdown"]
docs = ["encryption", "sled_cryptostore"]
[dependencies]
async-trait = "0.1.31"
serde = "1.0.110"
serde_json = "1.0.53"
zeroize = "1.1.0"
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.1.0", path = "../matrix_sdk_common" }
matrix-sdk-crypto = { version = "0.1.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.19"
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.21"
version = "1.7.1"
default-features = false
features = ["sync", "fs"]
[dev-dependencies]
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
http = "0.2.1"
tracing-subscriber = "0.2.5"
tempfile = "3.1.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.21", 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.12"
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.
+1069 -1622
View File
File diff suppressed because it is too large Load Diff
+16 -9
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,15 +46,21 @@ 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(docsrs, doc(cfg(feature = "encryption")))]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
#[error(transparent)]
OlmError(#[from] OlmError),
/// An error occurred during a E2EE group operation.
#[cfg(feature = "encryption")]
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
#[error(transparent)]
MegolmError(#[from] MegolmError),
}
-397
View File
@@ -1,397 +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 crate::events::{
fully_read::FullyReadEvent,
ignored_user_list::IgnoredUserListEvent,
presence::PresenceEvent,
push_rules::PushRulesEvent,
receipt::ReceiptEvent,
room::{
aliases::AliasesEvent,
avatar::AvatarEvent,
canonical_alias::CanonicalAliasEvent,
join_rules::JoinRulesEvent,
member::{MemberEvent, MemberEventContent},
message::{feedback::FeedbackEvent, MessageEvent},
name::NameEvent,
power_levels::PowerLevelsEvent,
redaction::RedactionEvent,
tombstone::TombstoneEvent,
},
stripped::{
StrippedRoomAliases, StrippedRoomAvatar, StrippedRoomCanonicalAlias, StrippedRoomJoinRules,
StrippedRoomMember, StrippedRoomName, StrippedRoomPowerLevels,
},
typing::TypingEvent,
};
use crate::{Room, RoomState};
/// Type alias for `RoomState` enum when passed to `EventEmitter` methods.
pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
/// 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::{MessageEvent, MessageEventContent, TextMessageEventContent},
/// # },
/// # EventEmitter, SyncRoom
/// # };
/// # use matrix_sdk_common::locks::RwLock;
///
/// struct EventCallback;
///
/// #[async_trait::async_trait]
/// impl EventEmitter for EventCallback {
/// async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
/// if let SyncRoom::Joined(room) = room {
/// if let MessageEvent {
/// content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
/// sender,
/// ..
/// } = event
/// {
/// let name = {
/// let room = room.read().await;
/// let member = room.members.get(&sender).unwrap();
/// member
/// .display_name
/// .as_ref()
/// .map(ToString::to_string)
/// .unwrap_or(sender.to_string())
/// };
/// println!("{}: {}", name, msg_body);
/// }
/// }
/// }
/// }
/// ```
#[async_trait::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, _: &MemberEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {}
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {}
// `RoomEvent`s from `IncomingState`
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {}
/// Fires when `Client` receives a `StateEvent::RoomName` event.
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {}
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {}
// `AnyStrippedStateEvent`s
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedRoomMember,
_: Option<MemberEventContent>,
) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
async fn on_stripped_state_canonical_alias(&self, _: SyncRoom, _: &StrippedRoomCanonicalAlias) {
}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {}
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {}
// `NonRoomEvent` (this is a type alias from ruma_events)
/// Fires when `Client` receives a `NonRoomEvent::RoomMember` event.
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomName` event.
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomCanonicalAlias` event.
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::Typing` event.
async fn on_account_data_typing(&self, _: SyncRoom, _: &TypingEvent) {}
/// Fires when `Client` receives a `NonRoomEvent::Receipt` event.
///
/// This is always a read receipt.
async fn on_account_data_receipt(&self, _: SyncRoom, _: &ReceiptEvent) {}
// `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) {}
}
#[cfg(test)]
mod test {
use super::*;
use matrix_sdk_common::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>>>);
#[async_trait::async_trait]
impl EventEmitter for EvEmitterTest {
async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) {
self.0.lock().await.push("member".to_string())
}
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {
self.0.lock().await.push("name".to_string())
}
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
self.0.lock().await.push("canonical".to_string())
}
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
self.0.lock().await.push("aliases".to_string())
}
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
self.0.lock().await.push("avatar".to_string())
}
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {
self.0.lock().await.push("message".to_string())
}
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {
self.0.lock().await.push("feedback".to_string())
}
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {
self.0.lock().await.push("redaction".to_string())
}
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
self.0.lock().await.push("power".to_string())
}
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {
self.0.lock().await.push("tombstone".to_string())
}
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {
self.0.lock().await.push("state member".to_string())
}
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {
self.0.lock().await.push("state name".to_string())
}
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
self.0.lock().await.push("state canonical".to_string())
}
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
self.0.lock().await.push("state aliases".to_string())
}
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
self.0.lock().await.push("state avatar".to_string())
}
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
self.0.lock().await.push("state power".to_string())
}
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {
self.0.lock().await.push("state rules".to_string())
}
async fn on_stripped_state_member(
&self,
_: SyncRoom,
_: &StrippedRoomMember,
_: Option<MemberEventContent>,
) {
self.0
.lock()
.await
.push("stripped state member".to_string())
}
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {
self.0.lock().await.push("stripped state name".to_string())
}
async fn on_stripped_state_canonical_alias(
&self,
_: SyncRoom,
_: &StrippedRoomCanonicalAlias,
) {
self.0
.lock()
.await
.push("stripped state canonical".to_string())
}
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {
self.0
.lock()
.await
.push("stripped state aliases".to_string())
}
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {
self.0
.lock()
.await
.push("stripped state avatar".to_string())
}
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {
self.0.lock().await.push("stripped state power".to_string())
}
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {
self.0.lock().await.push("stripped state rules".to_string())
}
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {
self.0.lock().await.push("account presence".to_string())
}
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {
self.0.lock().await.push("account ignore".to_string())
}
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {
self.0.lock().await.push("account push rules".to_string())
}
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {
self.0.lock().await.push("account read".to_string())
}
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {
self.0.lock().await.push("presence event".to_string())
}
}
use crate::identifiers::UserId;
use crate::{BaseClient, Session};
use std::convert::TryFrom;
async fn get_client() -> BaseClient {
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:example.com").unwrap(),
device_id: "DEVICEID".to_owned(),
};
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 read",
"account ignore",
"presence event"
],
)
}
#[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"
],
)
}
}
+16 -13
View File
@@ -20,13 +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.
//! * `markdown`: Support for sending markdown formatted messages.
#![deny(
missing_debug_implementations,
dead_code,
missing_docs,
trivial_casts,
trivial_numeric_casts,
@@ -34,22 +34,25 @@
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(feature = "docs", feature(doc_cfg))]
pub use crate::{error::Error, error::Result, session::Session};
pub use matrix_sdk_common::*;
pub use crate::{
error::{Error, Result},
session::Session,
};
mod client;
mod error;
mod event_emitter;
mod models;
pub mod media;
mod rooms;
mod session;
mod state;
mod store;
pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType};
pub use event_emitter::{EventEmitter, SyncRoom};
pub use client::{BaseClient, BaseClientConfig};
#[cfg(feature = "encryption")]
pub use matrix_sdk_crypto::{Device, TrustState};
pub use models::Room;
#[cfg(not(target_arch = "wasm32"))]
pub use state::JsonStore;
pub use state::StateStore;
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
pub use matrix_sdk_crypto as crypto;
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()))
}
})
}
}
-56
View File
@@ -1,56 +0,0 @@
//! De-/serialization functions to and from json strings, allows the type to be used as a query string.
use serde::de::{Deserialize, Deserializer, Error as _};
use crate::events::collections::all::Event;
use crate::events::presence::PresenceEvent;
use crate::events::EventJson;
pub fn deserialize_events<'de, D>(deserializer: D) -> Result<Vec<Event>, D::Error>
where
D: Deserializer<'de>,
{
let mut events = vec![];
let ev = Vec::<EventJson<Event>>::deserialize(deserializer)?;
for event in ev {
events.push(event.deserialize().map_err(D::Error::custom)?);
}
Ok(events)
}
pub fn deserialize_presence<'de, D>(deserializer: D) -> Result<Vec<PresenceEvent>, D::Error>
where
D: Deserializer<'de>,
{
let mut events = vec![];
let ev = Vec::<EventJson<PresenceEvent>>::deserialize(deserializer)?;
for event in ev {
events.push(event.deserialize().map_err(D::Error::custom)?);
}
Ok(events)
}
#[cfg(test)]
mod test {
use std::fs;
use crate::events::room::member::MemberEvent;
use crate::events::EventJson;
use crate::models::RoomMember;
#[test]
fn events_and_presence_deserialization() {
let ev_json = fs::read_to_string("../test_data/events/member.json").unwrap();
let ev = serde_json::from_str::<EventJson<MemberEvent>>(&ev_json)
.unwrap()
.deserialize()
.unwrap();
let member = RoomMember::new(&ev);
let member_json = serde_json::to_string(&member).unwrap();
let mem = serde_json::from_str::<RoomMember>(&member_json).unwrap();
assert_eq!(member, mem);
}
}
-253
View File
@@ -1,253 +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::cmp::Ordering;
use std::ops::Deref;
use std::vec::IntoIter;
use crate::events::room::message::MessageEvent;
use crate::events::EventJson;
use serde::{de, ser, Serialize};
/// A queue that holds the 10 most recent messages received from the server.
#[derive(Clone, Debug, Default)]
pub struct MessageQueue {
msgs: Vec<MessageWrapper>,
}
#[derive(Clone, Debug, Serialize)]
pub struct MessageWrapper(MessageEvent);
impl Deref for MessageWrapper {
type Target = MessageEvent;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PartialEq for MessageWrapper {
fn eq(&self, other: &MessageWrapper) -> bool {
self.0.event_id == other.0.event_id
}
}
impl Eq for MessageWrapper {}
impl PartialOrd for MessageWrapper {
fn partial_cmp(&self, other: &MessageWrapper) -> Option<Ordering> {
Some(self.0.origin_server_ts.cmp(&other.0.origin_server_ts))
}
}
impl Ord for MessageWrapper {
fn cmp(&self, other: &MessageWrapper) -> Ordering {
self.partial_cmp(other).unwrap_or(Ordering::Equal)
}
}
impl PartialEq for MessageQueue {
fn eq(&self, other: &MessageQueue) -> bool {
self.msgs.len() == other.msgs.len()
&& self
.msgs
.iter()
.zip(other.msgs.iter())
.all(|(msg_a, msg_b)| msg_a.event_id == msg_b.event_id)
}
}
impl MessageQueue {
/// Create a new empty `MessageQueue`.
pub fn new() -> Self {
Self {
msgs: Vec::with_capacity(20),
}
}
/// 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: MessageEvent) -> 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;
}
}
let message = MessageWrapper(msg);
match self.msgs.binary_search_by(|m| m.cmp(&message)) {
Ok(pos) => {
if self.msgs[pos] != message {
self.msgs.insert(pos, message)
}
}
Err(pos) => self.msgs.insert(pos, message),
}
if self.msgs.len() > 10 {
self.msgs.remove(0);
}
true
}
pub fn iter(&self) -> impl Iterator<Item = &MessageWrapper> {
self.msgs.iter()
}
}
impl IntoIterator for MessageQueue {
type Item = MessageWrapper;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.msgs.into_iter()
}
}
pub(crate) mod ser_deser {
use super::*;
pub fn deserialize<'de, D>(deserializer: D) -> Result<MessageQueue, D::Error>
where
D: de::Deserializer<'de>,
{
use serde::de::Error;
let messages: Vec<EventJson<MessageEvent>> = de::Deserialize::deserialize(deserializer)?;
let mut msgs = vec![];
for json in messages {
let msg = json.deserialize().map_err(D::Error::custom)?;
msgs.push(MessageWrapper(msg));
}
Ok(MessageQueue { msgs })
}
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 super::*;
use std::collections::HashMap;
use std::convert::TryFrom;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use crate::events::{collections::all::RoomEvent, EventJson};
use crate::identifiers::{RoomId, UserId};
use crate::Room;
#[test]
fn serialize() {
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let mut room = Room::new(&id, &user);
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
let mut msgs = MessageQueue::new();
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
msgs.push(msg.clone());
msg
} else {
panic!("this should always be a RoomMessage")
};
room.messages = msgs.clone();
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,
"members": {},
"messages": [ message ],
"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 = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
let mut room = Room::new(&id, &user);
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
let mut msgs = MessageQueue::new();
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
msgs.push(msg.clone());
msg
} else {
panic!("this should always be a RoomMessage")
};
room.messages = msgs;
let mut joined_rooms = HashMap::new();
joined_rooms.insert(id, room.clone());
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,
"members": {},
"messages": [ message ],
"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()
);
}
}
-9
View File
@@ -1,9 +0,0 @@
mod event_deser;
#[cfg(feature = "messages")]
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
mod message;
mod room;
mod room_member;
pub use room::{Room, RoomName};
pub use room_member::RoomMember;
-781
View File
@@ -1,781 +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::collections::{BTreeMap, HashMap};
use std::convert::TryFrom;
#[cfg(feature = "messages")]
use super::message::MessageQueue;
use super::RoomMember;
use crate::api::r0::sync::sync_events::{RoomSummary, UnreadNotificationsCount};
use crate::events::collections::all::{RoomEvent, StateEvent};
use crate::events::presence::PresenceEvent;
use crate::events::room::{
aliases::AliasesEvent,
canonical_alias::CanonicalAliasEvent,
encryption::EncryptionEvent,
member::{MemberEvent, MembershipChange},
name::NameEvent,
power_levels::{NotificationPowerLevels, PowerLevelsEvent, PowerLevelsEventContent},
tombstone::TombstoneEvent,
};
use crate::events::stripped::{AnyStrippedStateEvent, StrippedRoomName};
use crate::events::{Algorithm, EventType};
#[cfg(feature = "messages")]
use crate::events::room::message::MessageEvent;
use crate::identifiers::{RoomAliasId, RoomId, UserId};
use crate::js_int::{Int, UInt};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(test, derive(Clone))]
/// `RoomName` allows the calculation of a text room name.
pub struct RoomName {
/// The displayed name of the room.
name: Option<String>,
/// The canonical alias of the room ex. `#room-name:example.com` and port number.
canonical_alias: Option<RoomAliasId>,
/// List of `RoomAliasId`s the room has been given.
aliases: Vec<RoomAliasId>,
/// Users which can be used to generate a room name if the room does not have
/// one. Required if room name or canonical aliases are not set or empty.
pub heroes: Vec<String>,
/// Number of users whose membership status is `join`.
/// Required if field has changed since last sync; otherwise, it may be
/// omitted.
pub joined_member_count: Option<UInt>,
/// Number of users whose membership status is `invite`.
/// Required if field has changed since last sync; otherwise, it may be
/// omitted.
pub invited_member_count: Option<UInt>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(test, derive(Clone))]
pub struct PowerLevels {
/// The level required to ban a user.
pub ban: Int,
/// The level required to send specific event types.
///
/// This is a mapping from event type to power level required.
pub events: BTreeMap<EventType, Int>,
/// The default level required to send message events.
pub events_default: Int,
/// The level required to invite a user.
pub invite: Int,
/// The level required to kick a user.
pub kick: Int,
/// The level required to redact an event.
pub redact: Int,
/// The default level required to send state events.
pub state_default: Int,
/// The default power level for every user in the room.
pub users_default: Int,
/// The power level requirements for specific notification types.
///
/// This is a mapping from `key` to power level for that notifications key.
pub notifications: Int,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
/// Encryption info of the room.
pub struct EncryptionInfo {
/// The encryption algorithm that should be used to encrypt messages in the
/// room.
algorithm: Algorithm,
/// How long should a session be used before it is rotated.
rotation_period_ms: u64,
/// The maximum amount of messages that should be encrypted using the same
/// session.
rotation_period_messages: u64,
}
impl EncryptionInfo {
/// The encryption algorithm that should be used to encrypt messages in the
/// room.
pub fn algorithm(&self) -> &Algorithm {
&self.algorithm
}
/// How long should a session be used before it is rotated.
pub fn rotation_period(&self) -> u64 {
self.rotation_period_ms
}
/// The maximum amount of messages that should be encrypted using the same
/// session.
pub fn rotation_period_messages(&self) -> u64 {
self.rotation_period_messages
}
}
impl From<&EncryptionEvent> for EncryptionInfo {
fn from(event: &EncryptionEvent) -> Self {
EncryptionInfo {
algorithm: event.content.algorithm.clone(),
rotation_period_ms: event
.content
.rotation_period_ms
.map_or(604_800_000, Into::into),
rotation_period_messages: event.content.rotation_period_msgs.map_or(100, Into::into),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(test, derive(Clone))]
pub struct Tombstone {
/// A server-defined message.
body: String,
/// The room that is now active.
replacement: RoomId,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(test, derive(Clone))]
/// 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 queue of messages, holds no more than 10 of the most recent messages.
///
/// This is helpful when using a `StateStore` to avoid multiple requests
/// to the server for messages.
#[cfg(feature = "messages")]
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
#[serde(with = "super::message::ser_deser")]
pub messages: MessageQueue,
/// 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>,
/// Optional encryption info, will be `Some` if the room is encrypted.
pub encrypted: Option<EncryptionInfo>,
/// Number of unread notifications with highlight flag set.
pub unread_highlight: Option<UInt>,
/// Number of unread notifications.
pub unread_notifications: Option<UInt>,
/// The tombstone state of this room.
pub tombstone: Option<Tombstone>,
}
impl RoomName {
pub fn push_alias(&mut self, alias: RoomAliasId) -> bool {
self.aliases.push(alias);
true
}
pub fn set_canonical(&mut self, alias: RoomAliasId) -> bool {
self.canonical_alias = Some(alias);
true
}
pub fn set_name(&mut self, name: &str) -> bool {
self.name = Some(name.to_string());
true
}
pub fn calculate_name(&self, members: &HashMap<UserId, RoomMember>) -> String {
// https://matrix.org/docs/spec/client_server/latest#calculating-the-display-name-for-a-room.
// the order in which we check for a name ^^
if let Some(name) = &self.name {
let name = name.trim();
name.to_string()
} else if let Some(alias) = &self.canonical_alias {
let alias = alias.alias().trim();
alias.to_string()
} else if !self.aliases.is_empty() && !self.aliases[0].alias().is_empty() {
self.aliases[0].alias().trim().to_string()
} else {
let joined = self.joined_member_count.unwrap_or(UInt::MIN);
let invited = self.invited_member_count.unwrap_or(UInt::MIN);
let heroes = UInt::new(self.heroes.len() as u64).unwrap();
let one = UInt::new(1).unwrap();
let invited_joined = if invited + joined == UInt::MIN {
UInt::MIN
} else {
invited + joined - one
};
// TODO this should use `self.heroes but it is always empty??
if heroes >= invited_joined {
let mut names = members
.values()
.take(3)
.map(|mem| {
mem.display_name
.clone()
.unwrap_or_else(|| mem.user_id.localpart().to_string())
})
.collect::<Vec<String>>();
// stabilize ordering
names.sort();
names.join(", ")
} else if heroes < invited_joined && invited + joined > one {
let mut names = members
.values()
.take(3)
.map(|mem| {
mem.display_name
.clone()
.unwrap_or_else(|| mem.user_id.localpart().to_string())
})
.collect::<Vec<String>>();
names.sort();
// TODO what length does the spec want us to use here and in the `else`
format!("{}, and {} others", names.join(", "), (joined + invited))
} else {
format!("Empty Room (was {} others)", members.len())
}
}
}
}
impl Room {
/// Create a new room.
///
/// # Arguments
///
/// * `room_id` - The unique id of the room.
///
/// * `own_user_id` - The mxid of our own user.
pub fn new(room_id: &RoomId, own_user_id: &UserId) -> Self {
Room {
room_id: room_id.clone(),
room_name: RoomName::default(),
own_user_id: own_user_id.clone(),
creator: None,
members: HashMap::new(),
#[cfg(feature = "messages")]
messages: MessageQueue::new(),
typing_users: Vec::new(),
power_levels: None,
encrypted: None,
unread_highlight: None,
unread_notifications: None,
tombstone: None,
}
}
/// Return the display name of the room.
pub fn display_name(&self) -> String {
self.room_name.calculate_name(&self.members)
}
/// Is the room a encrypted room.
pub fn is_encrypted(&self) -> bool {
self.encrypted.is_some()
}
/// Get the encryption info if any of the room.
///
/// Returns None if the room is not encrypted.
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
self.encrypted.as_ref()
}
fn add_member(&mut self, event: &MemberEvent) -> bool {
if self
.members
.contains_key(&UserId::try_from(event.state_key.as_str()).unwrap())
{
return false;
}
let member = RoomMember::new(event);
self.members
.insert(UserId::try_from(event.state_key.as_str()).unwrap(), member);
true
}
/// Add to the list of `RoomAliasId`s.
fn push_room_alias(&mut self, alias: &RoomAliasId) -> bool {
self.room_name.push_alias(alias.clone());
true
}
/// RoomAliasId is `#alias:hostname` and `port`
fn canonical_alias(&mut self, alias: &RoomAliasId) -> bool {
self.room_name.set_canonical(alias.clone());
true
}
fn set_room_name(&mut self, name: &str) -> bool {
self.room_name.set_name(name);
true
}
fn set_room_power_level(&mut self, event: &PowerLevelsEvent) -> bool {
let PowerLevelsEventContent {
ban,
events,
events_default,
invite,
kick,
redact,
state_default,
users_default,
notifications: NotificationPowerLevels { room },
..
} = &event.content;
let power = PowerLevels {
ban: *ban,
events: events.clone(),
events_default: *events_default,
invite: *invite,
kick: *kick,
redact: *redact,
state_default: *state_default,
users_default: *users_default,
notifications: *room,
};
self.power_levels = Some(power);
true
}
pub(crate) fn set_room_summary(&mut self, summary: &RoomSummary) {
let RoomSummary {
heroes,
joined_member_count,
invited_member_count,
} = summary;
self.room_name.heroes = heroes.clone();
self.room_name.invited_member_count = *invited_member_count;
self.room_name.joined_member_count = *joined_member_count;
}
pub(crate) fn set_unread_notice_count(&mut self, notifications: &UnreadNotificationsCount) {
self.unread_highlight = notifications.highlight_count;
self.unread_notifications = notifications.notification_count;
}
/// Handle a room.member updating the room state if necessary.
///
/// Returns true if the joined member list changed, false otherwise.
pub fn handle_membership(&mut self, event: &MemberEvent) -> bool {
// TODO this would not be handled correctly as all the MemberEvents have the `prev_content`
// inside of `unsigned` field
match event.membership_change() {
MembershipChange::Invited | MembershipChange::Joined => self.add_member(event),
_ => {
let user = if let Ok(id) = UserId::try_from(event.state_key.as_str()) {
id
} else {
return false;
};
if let Some(member) = self.members.get_mut(&user) {
member.update_member(event)
} else {
false
}
}
}
}
/// Handle a room.message event and update the `MessageQueue` if necessary.
///
/// Returns true if `MessageQueue` was added to.
#[cfg(feature = "messages")]
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
pub fn handle_message(&mut self, event: &MessageEvent) -> bool {
self.messages.push(event.clone())
}
/// Handle a room.aliases event, updating the room state if necessary.
///
/// Returns true if the room name changed, false otherwise.
pub fn handle_room_aliases(&mut self, event: &AliasesEvent) -> bool {
match event.content.aliases.as_slice() {
[alias] => self.push_room_alias(alias),
[alias, ..] => self.push_room_alias(alias),
_ => false,
}
}
/// Handle a room.canonical_alias event, updating the room state if necessary.
///
/// Returns true if the room name changed, false otherwise.
pub fn handle_canonical(&mut self, event: &CanonicalAliasEvent) -> bool {
match &event.content.alias {
Some(name) => self.canonical_alias(&name),
_ => false,
}
}
/// Handle a room.name event, updating the room state if necessary.
///
/// Returns true if the room name changed, false otherwise.
pub fn handle_room_name(&mut self, event: &NameEvent) -> bool {
match event.content.name() {
Some(name) => self.set_room_name(name),
_ => false,
}
}
/// Handle a room.name event, updating the room state if necessary.
///
/// Returns true if the room name changed, false otherwise.
pub fn handle_stripped_room_name(&mut self, event: &StrippedRoomName) -> bool {
match event.content.name() {
Some(name) => self.set_room_name(name),
_ => false,
}
}
/// Handle a room.power_levels event, updating the room state if necessary.
///
/// Returns true if the room name changed, false otherwise.
pub fn handle_power_level(&mut self, event: &PowerLevelsEvent) -> bool {
// NOTE: this is always true, we assume that if we get an event their is an update.
let mut updated = self.set_room_power_level(event);
let mut max_power = event.content.users_default;
for power in event.content.users.values() {
max_power = *power.max(&max_power);
}
for user in event.content.users.keys() {
if let Some(member) = self.members.get_mut(user) {
if member.update_power(event, max_power) {
updated = true;
}
}
}
updated
}
fn handle_tombstone(&mut self, event: &TombstoneEvent) -> bool {
self.tombstone = Some(Tombstone {
body: event.content.body.clone(),
replacement: event.content.replacement_room.clone(),
});
true
}
fn handle_encryption_event(&mut self, event: &EncryptionEvent) -> bool {
self.encrypted = Some(event.into());
true
}
/// Receive a timeline event for this room and update the room state.
///
/// Returns true if the joined member list changed, false otherwise.
///
/// # Arguments
///
/// * `event` - The event of the room.
pub fn receive_timeline_event(&mut self, event: &RoomEvent) -> bool {
match event {
// update to the current members of the room
RoomEvent::RoomMember(member) => self.handle_membership(member),
// finds all events related to the name of the room for later use
RoomEvent::RoomName(name) => self.handle_room_name(name),
RoomEvent::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias),
RoomEvent::RoomAliases(alias) => self.handle_room_aliases(alias),
// power levels of the room members
RoomEvent::RoomPowerLevels(power) => self.handle_power_level(power),
RoomEvent::RoomTombstone(tomb) => self.handle_tombstone(tomb),
RoomEvent::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt),
#[cfg(feature = "messages")]
RoomEvent::RoomMessage(msg) => self.handle_message(msg),
_ => false,
}
}
/// Receive a state event for this room and update the room state.
///
/// Returns true if the state of the `Room` has changed, false otherwise.
///
/// # Arguments
///
/// * `event` - The event of the room.
pub fn receive_state_event(&mut self, event: &StateEvent) -> bool {
match event {
// update to the current members of the room
StateEvent::RoomMember(member) => self.handle_membership(member),
// finds all events related to the name of the room for later use
StateEvent::RoomName(name) => self.handle_room_name(name),
StateEvent::RoomCanonicalAlias(c_alias) => self.handle_canonical(c_alias),
StateEvent::RoomAliases(alias) => self.handle_room_aliases(alias),
// power levels of the room members
StateEvent::RoomPowerLevels(power) => self.handle_power_level(power),
StateEvent::RoomTombstone(tomb) => self.handle_tombstone(tomb),
StateEvent::RoomEncryption(encrypt) => self.handle_encryption_event(encrypt),
_ => false,
}
}
/// Receive a stripped state event for this room and update the room state.
///
/// Returns true if the state of the `Room` has changed, false otherwise.
///
/// # Arguments
///
/// * `event` - The `AnyStrippedStateEvent` sent by the server for invited but not
/// joined rooms.
pub fn receive_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
match event {
AnyStrippedStateEvent::RoomName(n) => self.handle_stripped_room_name(n),
_ => false,
}
}
/// Receive a presence event from an `IncomingResponse` and updates the client state.
///
/// This will only update the user if found in the current room looped through
/// by `Client::sync`.
/// Returns true if the specific users presence has changed, false otherwise.
///
/// # Arguments
///
/// * `event` - The presence event for a specified room member.
pub fn receive_presence_event(&mut self, event: &PresenceEvent) -> bool {
if let Some(member) = self.members.get_mut(&event.sender) {
if member.did_update_presence(event) {
false
} else {
member.update_presence(event);
true
}
} else {
// this is probably an error as we have a `PresenceEvent` for a user
// we don't know about
false
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::events::{
room::{encryption::EncryptionEventContent, member::MembershipState},
UnsignedData,
};
use crate::identifiers::{EventId, UserId};
use crate::{BaseClient, Session};
use matrix_sdk_test::{async_test, sync_response, EventBuilder, EventsFile, SyncResponseFile};
use std::time::SystemTime;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use std::convert::TryFrom;
use std::ops::Deref;
async fn get_client() -> BaseClient {
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
client
}
fn get_room_id() -> RoomId {
RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()
}
#[async_test]
async fn user_presence() {
let client = get_client().await;
let mut response = sync_response(SyncResponseFile::Default);
client.receive_sync_response(&mut response).await.unwrap();
let rooms_lock = &client.joined_rooms();
let rooms = rooms_lock.read().await;
let room = &rooms
.get(&RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap())
.unwrap()
.read()
.await;
assert_eq!(2, room.members.len());
for member in room.members.values() {
assert_eq!(MembershipState::Join, member.membership);
}
assert!(room.deref().power_levels.is_some())
}
#[async_test]
async fn room_events() {
let client = get_client().await;
let room_id = get_room_id();
let user_id = UserId::try_from("@example:localhost").unwrap();
let mut response = EventBuilder::default()
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
.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;
assert_eq!(room.members.len(), 1);
assert!(room.power_levels.is_some());
assert_eq!(
room.power_levels.as_ref().unwrap().kick,
crate::js_int::Int::new(50).unwrap()
);
let admin = room.members.get(&user_id).unwrap();
assert_eq!(
admin.power_level.unwrap(),
crate::js_int::Int::new(100).unwrap()
);
}
#[async_test]
async fn calculate_aliases() {
let client = get_client().await;
let room_id = get_room_id();
let mut response = EventBuilder::default()
.add_state_event(EventsFile::Aliases, StateEvent::RoomAliases)
.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;
assert_eq!("tutorial", room.display_name());
}
#[async_test]
async fn calculate_alias() {
let client = get_client().await;
let room_id = get_room_id();
let mut response = EventBuilder::default()
.add_state_event(EventsFile::Alias, StateEvent::RoomCanonicalAlias)
.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;
assert_eq!("tutorial", room.display_name());
}
#[async_test]
async fn calculate_name() {
let client = get_client().await;
let room_id = get_room_id();
let mut response = EventBuilder::default()
.add_state_event(EventsFile::Name, StateEvent::RoomName)
.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;
assert_eq!("room name", room.display_name());
}
#[async_test]
async fn calculate_room_names_from_summary() {
let mut response = sync_response(SyncResponseFile::DefaultWithSummary);
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
client.receive_sync_response(&mut response).await.unwrap();
let mut room_names = vec![];
for room in client.joined_rooms().read().await.values() {
room_names.push(room.read().await.display_name())
}
assert_eq!(vec!["example, example2"], room_names);
}
#[async_test]
#[cfg(not(target_arch = "wasm32"))]
async fn encryption_info_test() {
let mut response = sync_response(SyncResponseFile::DefaultWithSummary);
let user_id = UserId::try_from("@example:localhost").unwrap();
let session = Session {
access_token: "1234".to_owned(),
user_id: user_id.clone(),
device_id: "DEVICEID".to_owned(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
client.receive_sync_response(&mut response).await.unwrap();
let event = EncryptionEvent {
event_id: EventId::try_from("$h29iv0s8:example.com").unwrap(),
origin_server_ts: SystemTime::now(),
sender: user_id,
state_key: "".into(),
unsigned: UnsignedData::default(),
content: EncryptionEventContent {
algorithm: Algorithm::MegolmV1AesSha2,
rotation_period_ms: Some(100_000u32.into()),
rotation_period_msgs: Some(100u32.into()),
},
prev_content: None,
room_id: None,
};
let room_id = get_room_id();
let room = client.get_joined_room(&room_id).await.unwrap();
assert!(!room.read().await.is_encrypted());
room.write().await.handle_encryption_event(&event);
assert!(room.read().await.is_encrypted());
let room_lock = room.read().await;
let encryption_info = room_lock.encryption_info().unwrap();
assert_eq!(encryption_info.algorithm(), &Algorithm::MegolmV1AesSha2);
assert_eq!(encryption_info.rotation_period(), 100_000);
assert_eq!(encryption_info.rotation_period_messages(), 100);
}
}
-284
View File
@@ -1,284 +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 crate::events::collections::all::Event;
use crate::events::presence::{PresenceEvent, PresenceEventContent, PresenceState};
use crate::events::room::{
member::{MemberEvent, MembershipChange, MembershipState},
power_levels::PowerLevelsEvent,
};
use crate::identifiers::UserId;
use crate::js_int::{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)]
#[cfg_attr(test, derive(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>,
/// 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.
#[serde(deserialize_with = "super::event_deser::deserialize_events")]
pub events: Vec<Event>,
/// The `PresenceEvent`s connected to this user.
#[serde(deserialize_with = "super::event_deser::deserialize_presence")]
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.avatar_url == other.avatar_url
&& self.last_active_ago == other.last_active_ago
&& self.membership == other.membership
}
}
impl RoomMember {
pub fn new(event: &MemberEvent) -> Self {
Self {
name: event.state_key.clone(),
room_id: event.room_id.as_ref().map(|id| id.to_string()),
user_id: UserId::try_from(event.state_key.as_str()).unwrap(),
display_name: event.content.displayname.clone(),
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,
membership: event.content.membership,
presence_events: Vec::default(),
events: vec![Event::RoomMember(event.clone())],
}
}
pub fn update_member(&mut self, event: &MemberEvent) -> bool {
use MembershipChange::*;
match event.membership_change() {
ProfileChanged => {
self.display_name = event.content.displayname.clone();
self.avatar_url = event.content.avatar_url.clone();
true
}
Banned | Kicked | KickedAndBanned | InvitationRejected | InvitationRevoked | Left
| Unbanned | Joined | Invited => {
self.membership = event.content.membership;
true
}
NotImplemented => false,
None => false,
// we ignore the error here as only a buggy or malicious server would send this
Error => false,
}
}
pub fn update_power(&mut self, event: &PowerLevelsEvent, max_power: Int) -> bool {
let changed;
if let Some(user_power) = event.content.users.get(&self.user_id) {
changed = self.power_level != Some(*user_power);
self.power_level = Some(*user_power);
} else {
changed = self.power_level != Some(event.content.users_default);
self.power_level = Some(event.content.users_default);
}
if max_power > Int::from(0) {
self.power_level_norm = Some((self.power_level.unwrap() * Int::from(100)) / max_power);
}
changed
}
/// If the current `PresenceEvent` updated the state of this `User`.
///
/// Returns true if the specific users presence has changed, false otherwise.
///
/// # Arguments
///
/// * `presence` - The presence event for a this room member.
pub fn did_update_presence(&self, presence: &PresenceEvent) -> bool {
let PresenceEvent {
content:
PresenceEventContent {
avatar_url,
currently_active,
displayname,
last_active_ago,
presence,
status_msg,
},
..
} = presence;
self.display_name == *displayname
&& self.avatar_url == *avatar_url
&& self.presence.as_ref() == Some(presence)
&& self.status_msg == *status_msg
&& self.last_active_ago == *last_active_ago
&& self.currently_active == *currently_active
}
/// Updates the `User`s presence.
///
/// This should only be used if `did_update_presence` was true.
///
/// # Arguments
///
/// * `presence` - The presence event for a this room member.
pub fn update_presence(&mut self, presence_ev: &PresenceEvent) {
let PresenceEvent {
content:
PresenceEventContent {
avatar_url,
currently_active,
displayname,
last_active_ago,
presence,
status_msg,
},
..
} = presence_ev;
self.presence_events.push(presence_ev.clone());
self.avatar_url = avatar_url.clone();
self.currently_active = *currently_active;
self.display_name = displayname.clone();
self.last_active_ago = *last_active_ago;
self.presence = Some(*presence);
self.status_msg = status_msg.clone();
}
}
#[cfg(test)]
mod test {
use matrix_sdk_test::{async_test, EventBuilder, EventsFile};
use crate::events::collections::all::RoomEvent;
use crate::events::room::member::MembershipState;
use crate::identifiers::{RoomId, UserId};
use crate::{BaseClient, Session};
use crate::js_int::Int;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::*;
use std::convert::TryFrom;
async fn get_client() -> BaseClient {
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@example:localhost").unwrap(),
device_id: "DEVICEID".to_owned(),
};
let client = BaseClient::new().unwrap();
client.restore_login(session).await.unwrap();
client
}
fn get_room_id() -> RoomId {
RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()
}
#[async_test]
async fn room_member_events() {
let client = get_client().await;
let room_id = get_room_id();
let mut response = EventBuilder::default()
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
.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
.members
.get(&UserId::try_from("@example:localhost").unwrap())
.unwrap();
assert_eq!(member.membership, MembershipState::Join);
assert_eq!(member.power_level, Int::new(100));
}
#[async_test]
async fn member_presence_events() {
let client = get_client().await;
let room_id = get_room_id();
let mut response = EventBuilder::default()
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
.add_presence_event(EventsFile::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
.members
.get(&UserId::try_from("@example:localhost").unwrap())
.unwrap();
assert_eq!(member.membership, MembershipState::Join);
assert_eq!(member.power_level, Int::new(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)
}
}
+5 -3
View File
@@ -15,15 +15,17 @@
//! User sessions.
use crate::identifiers::UserId;
use ruma::{DeviceId, UserId};
use serde::{Deserialize, Serialize};
/// A user session, containing an access token and information about the
/// associated user account.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Session {
/// The access token used for this session.
pub access_token: String,
/// The user the access token was issued for.
pub user_id: UserId,
/// The ID of the client device
pub device_id: String,
pub device_id: Box<DeviceId>,
}
-389
View File
@@ -1,389 +0,0 @@
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use matrix_sdk_common::identifiers::RoomId;
use matrix_sdk_common::locks::RwLock;
use tokio::fs as async_fs;
use tokio::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 it's 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),
})
}
}
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::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 mut path = self.path.read().await.clone();
path.push("client.json");
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 mut path = self.path.read().await.clone();
path.push("client.json");
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 mut path = self.path.read().await.clone();
path.push("rooms");
path.push(&format!("{}/{}.json", room_state, room.room_id));
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 mut to_del = self.path.read().await.clone();
to_del.push("rooms");
to_del.push(&format!("{}/{}.json", room_state, room_id));
if !to_del.exists() {
return Err(Error::StateStore(format!("file {:?} not found", to_del)));
}
tokio::fs::remove_file(to_del).await.map_err(Error::from)
}
}
#[cfg(test)]
mod test {
use super::*;
use http::Response;
use std::convert::TryFrom;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
use tempfile::tempdir;
use crate::api::r0::sync::sync_events::Response as SyncResponse;
use crate::identifiers::{RoomId, UserId};
use crate::{BaseClient, BaseClientConfig, Session};
fn sync_response(file: &str) -> SyncResponse {
let mut file = File::open(file).unwrap();
let mut data = vec![];
file.read_to_end(&mut data).unwrap();
let response = Response::builder().body(data).unwrap();
SyncResponse::try_from(response).unwrap()
}
#[tokio::test]
async fn test_store_client_state() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let user = UserId::try_from("@example:example.com").unwrap();
let sess = Session {
access_token: "32nj9zu034btz90".to_string(),
user_id: user.clone(),
device_id: "Tester".to_string(),
};
let state = ClientState {
sync_token: Some("hello".into()),
ignored_users: vec![user],
push_ruleset: None,
};
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 = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
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 = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
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 = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
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 = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
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 = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
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());
}
#[tokio::test]
async fn test_client_sync_store() {
let dir = tempdir().unwrap();
let path: &Path = dir.path();
let session = Session {
access_token: "1234".to_owned(),
user_id: UserId::try_from("@cheeky_monkey:matrix.org").unwrap(),
device_id: "DEVICEID".to_owned(),
};
// a sync response to populate our JSON store
let store = Box::new(JsonStore::open(path).unwrap());
let client =
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
client.restore_login(session.clone()).await.unwrap();
let mut response = sync_response("../test_data/sync.json");
// gather state to save to the db, the first time through loading will be skipped
client.receive_sync_response(&mut response).await.unwrap();
// now syncing the client will update from the state store
let store = Box::new(JsonStore::open(path).unwrap());
let client =
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
client.restore_login(session.clone()).await.unwrap();
// assert the synced client and the logged in client are equal
assert_eq!(*client.session().read().await, Some(session));
assert_eq!(
client.sync_token().await,
Some("s526_47314_0_7_1_1_1_11444_1".to_string())
);
assert_eq!(
*client.ignored_users.read().await,
vec![UserId::try_from("@someone:example.org").unwrap()]
);
}
}
-219
View File
@@ -1,219 +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 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};
use crate::events::push_rules::Ruleset;
use crate::identifiers::{RoomId, UserId};
use crate::{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.
#[async_trait::async_trait]
pub trait StateStore: Send + Sync {
/// 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 std::convert::TryFrom;
use crate::identifiers::RoomId;
#[test]
fn serialize() {
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
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!(
r#"{
"!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,
"members": {},
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}"#,
serde_json::to_string_pretty(&joined_rooms).unwrap()
);
#[cfg(feature = "messages")]
assert_eq!(
r#"{
"!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,
"members": {},
"messages": [],
"typing_users": [],
"power_levels": null,
"encrypted": null,
"unread_highlight": null,
"unread_notifications": null,
"tombstone": null
}
}"#,
serde_json::to_string_pretty(&joined_rooms).unwrap()
);
}
#[test]
fn deserialize() {
let id = RoomId::try_from("!roomid:example.com").unwrap();
let user = UserId::try_from("@example:example.com").unwrap();
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);
}
}
+14 -14
View File
@@ -1,6 +1,6 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
description = "Collection of common types used in the matrix-sdk"
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "Collection of common types and imports used in the matrix-sdk"
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
@@ -8,24 +8,24 @@ license = "Apache-2.0"
name = "matrix-sdk-common"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.3.0"
[dependencies]
js_int = "0.1.5"
ruma-api = "0.16.1"
ruma-client-api = "0.9.0"
ruma-events = "0.21.2"
ruma-identifiers = "0.16.1"
instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] }
async-trait = "0.1.50"
instant = { version = "0.1.9", features = ["wasm-bindgen", "now"] }
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", features = ["v4"] }
uuid = { version = "0.8.2", default-features = false, features = ["v4", "serde"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
version = "0.2.21"
version = "1.7.1"
default-features = false
features = ["sync", "time", "fs"]
features = ["rt", "sync"]
[target.'cfg(target_arch = "wasm32")'.dependencies]
futures-locks = { version = "0.5.0", default-features = false }
uuid = { version = "0.8.1", features = ["v4", "wasm-bindgen"] }
futures = "0.3.15"
futures-locks = { version = "0.6.0", default-features = false }
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)
}
}
+19 -9
View File
@@ -1,13 +1,23 @@
pub use async_trait::async_trait;
pub use instant;
pub use js_int;
pub use ruma_api::{
error::{FromHttpResponseError, IntoHttpError, ServerError},
Endpoint,
};
pub use ruma_client_api as api;
pub use ruma_events as events;
pub use ruma_identifiers as identifiers;
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
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
/// implemented, while other targets will.
#[cfg(not(target_arch = "wasm32"))]
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
#[cfg(not(target_arch = "wasm32"))]
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
/// Super trait that is used for our store traits, this trait will differ if
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
/// implemented, while other targets will.
#[cfg(target_arch = "wasm32")]
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
#[cfg(target_arch = "wasm32")]
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
+2 -7
View File
@@ -3,11 +3,6 @@
// https://www.reddit.com/r/rust/comments/f4zldz/i_audited_3_different_implementation_of_async/
#[cfg(target_arch = "wasm32")]
pub use futures_locks::Mutex;
#[cfg(target_arch = "wasm32")]
pub use futures_locks::RwLock;
pub use futures_locks::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::sync::Mutex;
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::sync::RwLock;
pub use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
+45 -32
View File
@@ -1,5 +1,5 @@
[package]
authors = ["Damir Jelić <poljar@termina.org.uk"]
authors = ["Damir Jelić <poljar@termina.org.uk>"]
description = "Matrix encryption library"
edition = "2018"
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
@@ -8,44 +8,57 @@ license = "Apache-2.0"
name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
version = "0.1.0"
version = "0.3.0"
[package.metadata.docs.rs]
features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]
[features]
default = []
sqlite-cryptostore = ["sqlx"]
sled_cryptostore = ["sled"]
docs = ["sled_cryptostore"]
[dependencies]
async-trait = "0.1.31"
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"] }
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
olm-rs = { version = "0.5.0", features = ["serde"] }
serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.53"
cjson = "0.1.0"
zeroize = { version = "1.1.0", features = ["zeroize_derive"] }
url = "2.1.1"
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.19"
tracing = "0.1.14"
atomic = "0.4.5"
dashmap = "3.11.2"
[dependencies.tracing-futures]
version = "0.2.4"
default-features = false
features = ["std", "std-future"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.sqlx]
version = "0.3.5"
optional = true
default-features = false
features = ["runtime-tokio", "sqlite"]
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.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.4.3"
[dev-dependencies]
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
serde_json = "1.0.53"
tempfile = "3.1.0"
http = "0.2.1"
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");
}
}
}
-319
View File
@@ -1,319 +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 std::collections::BTreeMap;
#[cfg(test)]
use std::convert::TryFrom;
use std::mem;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use atomic::Atomic;
#[cfg(test)]
use super::OlmMachine;
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
use matrix_sdk_common::events::Algorithm;
use matrix_sdk_common::identifiers::{DeviceId, UserId};
/// A device represents a E2EE capable client of an user.
#[derive(Debug, Clone)]
pub struct Device {
user_id: Arc<UserId>,
device_id: Arc<DeviceId>,
algorithms: Arc<Vec<Algorithm>>,
keys: Arc<BTreeMap<KeyAlgorithm, String>>,
display_name: Arc<Option<String>>,
deleted: Arc<AtomicBool>,
trust_state: Arc<Atomic<TrustState>>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
/// The trust state of a device.
pub enum TrustState {
/// The device has been verified and is trusted.
Verified = 0,
/// The device been blacklisted from communicating.
BlackListed = 1,
/// The trust state of the device is being ignored.
Ignored = 2,
/// The trust state is unset.
Unset = 3,
}
impl From<i64> for TrustState {
fn from(state: i64) -> Self {
match state {
0 => TrustState::Verified,
1 => TrustState::BlackListed,
2 => TrustState::Ignored,
3 => TrustState::Unset,
_ => TrustState::Unset,
}
}
}
impl Device {
/// Create a new Device.
pub fn new(
user_id: UserId,
device_id: DeviceId,
display_name: Option<String>,
trust_state: TrustState,
algorithms: Vec<Algorithm>,
keys: BTreeMap<KeyAlgorithm, String>,
) -> Self {
Device {
user_id: Arc::new(user_id),
device_id: Arc::new(device_id),
display_name: Arc::new(display_name),
trust_state: Arc::new(Atomic::new(trust_state)),
algorithms: Arc::new(algorithms),
keys: Arc::new(keys),
deleted: Arc::new(AtomicBool::new(false)),
}
}
/// The user id of the device owner.
pub fn user_id(&self) -> &UserId {
&self.user_id
}
/// The unique ID of the device.
pub fn device_id(&self) -> &DeviceId {
&self.device_id
}
/// Get the human readable name of the device.
pub fn display_name(&self) -> &Option<String> {
&self.display_name
}
/// Get the key of the given key algorithm belonging to this device.
pub fn get_key(&self, algorithm: KeyAlgorithm) -> Option<&String> {
self.keys.get(&algorithm)
}
/// Get a map containing all the device keys.
pub fn keys(&self) -> &BTreeMap<KeyAlgorithm, String> {
&self.keys
}
/// Get the trust state of the device.
pub fn trust_state(&self) -> TrustState {
self.trust_state.load(Ordering::Relaxed)
}
/// Get the list of algorithms this device supports.
pub fn algorithms(&self) -> &[Algorithm] {
&self.algorithms
}
/// Is the device deleted.
pub fn deleted(&self) -> bool {
self.deleted.load(Ordering::Relaxed)
}
/// Update a device with a new device keys struct.
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) {
let mut keys = BTreeMap::new();
for (key_id, key) in device_keys.keys.iter() {
let key_id = key_id.0;
let _ = keys.insert(key_id, key.clone());
}
let display_name = Arc::new(
device_keys
.unsigned
.as_ref()
.map(|d| d.device_display_name.clone())
.flatten(),
);
let _ = mem::replace(
&mut self.algorithms,
Arc::new(device_keys.algorithms.clone()),
);
let _ = mem::replace(&mut self.keys, Arc::new(keys));
let _ = mem::replace(&mut self.display_name, display_name);
}
/// Mark the device as deleted.
pub(crate) fn mark_as_deleted(&self) {
self.deleted.store(true, Ordering::Relaxed);
}
}
#[cfg(test)]
impl From<&OlmMachine> for Device {
fn from(machine: &OlmMachine) -> Self {
Device {
user_id: Arc::new(machine.user_id().clone()),
device_id: Arc::new(machine.device_id().clone()),
algorithms: Arc::new(vec![
Algorithm::MegolmV1AesSha2,
Algorithm::OlmV1Curve25519AesSha2,
]),
keys: Arc::new(
machine
.identity_keys()
.iter()
.map(|(key, value)| {
(
KeyAlgorithm::try_from(key.as_ref()).unwrap(),
value.to_owned(),
)
})
.collect(),
),
display_name: Arc::new(None),
deleted: Arc::new(AtomicBool::new(false)),
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
}
}
}
impl From<&DeviceKeys> for Device {
fn from(device_keys: &DeviceKeys) -> Self {
let mut keys = BTreeMap::new();
for (key_id, key) in device_keys.keys.iter() {
let key_id = key_id.0;
let _ = keys.insert(key_id, key.clone());
}
Device {
user_id: Arc::new(device_keys.user_id.clone()),
device_id: Arc::new(device_keys.device_id.clone()),
algorithms: Arc::new(device_keys.algorithms.clone()),
keys: Arc::new(keys),
display_name: Arc::new(
device_keys
.unsigned
.as_ref()
.map(|d| d.device_display_name.clone())
.flatten(),
),
deleted: Arc::new(AtomicBool::new(false)),
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
}
}
}
impl PartialEq for Device {
fn eq(&self, other: &Self) -> bool {
self.user_id() == other.user_id() && self.device_id() == other.device_id()
}
}
#[cfg(test)]
pub(crate) mod test {
use serde_json::json;
use std::convert::{From, TryFrom};
use crate::device::{Device, TrustState};
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
use matrix_sdk_common::identifiers::UserId;
fn device_keys() -> DeviceKeys {
let user_id = UserId::try_from("@alice:example.org").unwrap();
let device_id = "DEVICEID";
let device_keys = json!({
"algorithms": vec![
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": device_id,
"user_id": user_id.to_string(),
"keys": {
"curve25519:DEVICEID": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
"ed25519:DEVICEID": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
},
"signatures": {
user_id.to_string(): {
"ed25519:DEVICEID": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA"
}
},
"unsigned": {
"device_display_name": "Alice's mobile phone"
}
});
serde_json::from_value(device_keys).unwrap()
}
pub(crate) fn get_device() -> Device {
let device_keys = device_keys();
Device::from(&device_keys)
}
#[test]
fn create_a_device() {
let user_id = UserId::try_from("@alice:example.org").unwrap();
let device_id = "DEVICEID";
let device = get_device();
assert_eq!(&user_id, device.user_id());
assert_eq!(device_id, device.device_id());
assert_eq!(device.algorithms.len(), 2);
assert_eq!(TrustState::Unset, device.trust_state());
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
assert_eq!(
device.get_key(KeyAlgorithm::Curve25519).unwrap(),
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
);
assert_eq!(
device.get_key(KeyAlgorithm::Ed25519).unwrap(),
"nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
);
}
#[test]
fn update_a_device() {
let mut device = get_device();
assert_eq!(
"Alice's mobile phone",
device.display_name().as_ref().unwrap()
);
let mut device_keys = device_keys();
device_keys.unsigned.as_mut().unwrap().device_display_name =
Some("Alice's work computer".to_owned());
device.update_device(&device_keys);
assert_eq!(
"Alice's work computer",
device.display_name().as_ref().unwrap()
);
}
#[test]
fn delete_a_device() {
let device = get_device();
assert!(!device.deleted());
let device_clone = device.clone();
device.mark_as_deleted();
assert!(device.deleted());
assert!(device_clone.deleted());
}
}
+77 -11
View File
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use cjson::Error as CjsonError;
use olm_rs::errors::{OlmGroupSessionError, OlmSessionError};
use ruma::{identifiers::Error as IdentifierError, DeviceId, UserId};
use serde_json::Error as SerdeError;
use thiserror::Error;
@@ -47,8 +47,23 @@ pub enum OlmError {
Store(#[from] CryptoStoreError),
/// The session with a device has become corrupted.
#[error("decryption failed likely because a Olm session was wedged")]
SessionWedged,
#[error(
"decryption failed likely because an Olm session from {0} with sender key {1} was wedged"
)]
SessionWedged(UserId, String),
/// An Olm message got replayed while the Olm ratchet has already moved
/// forward.
#[error("decryption failed because an Olm message from {0} with sender key {1} was replayed")]
ReplayedMessage(UserId, String),
/// Encryption failed because the device does not have a valid Olm session
/// with us.
#[error(
"encryption failed because the device does not \
have a valid Olm session with us"
)]
MissingSession,
}
/// Error representing a failure during a group encryption operation.
@@ -71,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),
@@ -93,6 +112,9 @@ pub enum EventError {
#[error("the Encrypted message is missing the signing key of the sender")]
MissingSigningKey,
#[error("the Encrypted message is missing the sender key")]
MissingSenderKey,
#[error("the Encrypted message is missing the field {0}")]
MissingField(String),
@@ -104,22 +126,66 @@ pub enum EventError {
}
#[derive(Error, Debug)]
pub(crate) enum SignatureError {
pub enum SessionUnpicklingError {
/// The underlying Olm session operation returned an error.
#[error("can't finish Olm Session operation {0}")]
OlmSession(#[from] OlmSessionError),
/// The Session timestamp was invalid.
#[error("can't load session timestamps")]
SessionTimestampError,
}
#[derive(Error, Debug)]
pub enum SignatureError {
#[error("the signature used a unsupported algorithm")]
UnsupportedAlgorithm,
#[error("the key id of the signing key is invalid")]
InvalidKeyId(#[from] IdentifierError),
#[error("the signing key is missing from the object that signed the message")]
MissingSigningKey,
#[error("the user id of the signing differs from the subkey user id")]
UserIdMissmatch,
#[error("the provided JSON value isn't an object")]
NotAnObject,
#[error("the provided JSON object doesn't contain a signatures field")]
NoSignatureFound,
#[error("the provided JSON object can't be converted to a canonical representation")]
CanonicalJsonError(CjsonError),
#[error("the signature didn't match the provided key")]
VerificationError,
#[error(transparent)]
JsonError(#[from] SerdeError),
}
impl From<CjsonError> for SignatureError {
fn from(error: CjsonError) -> Self {
Self::CanonicalJsonError(error)
}
#[derive(Error, Debug)]
pub(crate) enum SessionCreationError {
#[error(
"Failed to create a new Olm session for {0} {1}, the requested \
one-time key isn't a signed curve key"
)]
OneTimeKeyNotSigned(UserId, Box<DeviceId>),
#[error(
"Tried to create a new Olm session for {0} {1}, but the signed \
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(
"Tried to create an Olm session for {0} {1}, but the device is missing \
a curve25519 key"
)]
DeviceMissingCurveKey(UserId, Box<DeviceId>),
#[error("Error creating new Olm session for {0} {1}: {2:?}")]
OlmError(UserId, Box<DeviceId>, OlmSessionError),
}
@@ -0,0 +1,362 @@
// 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::BTreeMap,
io::{Error as IoError, ErrorKind, Read},
};
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};
const IV_SIZE: usize = 16;
const KEY_SIZE: usize = 32;
const VERSION: &str = "v2";
/// A wrapper that transparently encrypts anything that implements `Read` as an
/// Matrix attachment.
pub struct AttachmentDecryptor<'a, R: 'a + Read> {
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.read(buf)?;
if read_bytes == 0 {
let hash = self.sha.finalize_reset();
if hash.as_slice() == self.expected_hash.as_slice() {
Ok(0)
} else {
Err(IoError::new(ErrorKind::Other, "Hash mismatch while decrypting"))
}
} else {
self.sha.update(&buf[0..read_bytes]);
self.aes.apply_keystream(&mut buf[0..read_bytes]);
Ok(read_bytes)
}
}
}
/// Error type for attachment decryption.
#[derive(Error, Debug)]
pub enum DecryptorError {
/// Some data in the encrypted attachment coldn't be decoded, this may be a
/// hash, the secret key, or the initialization vector.
#[error(transparent)]
Decode(#[from] DecodeError),
/// A hash is missing from the encryption info.
#[error("The encryption info is missing a hash")]
MissingHash,
/// The supplied key or IV has an invalid length.
#[error("The supplied key or IV has an invalid length.")]
KeyNonceLength,
/// The supplied data was encrypted with an unknown version of the
/// attachment encryption spec.
#[error("Unknown version for the encrypted attachment.")]
UnknownVersion,
}
impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> {
/// Wrap the given reader decrypting all the data we read from it.
///
/// # Arguments
///
/// * `reader` - The `Reader` that should be wrapped and decrypted.
///
/// * `info` - The encryption info that is necessary to decrypt data from
/// the reader.
///
/// # Examples
/// ```
/// # use std::io::{Cursor, Read};
/// # use matrix_sdk_crypto::{AttachmentEncryptor, AttachmentDecryptor};
/// let data = "Hello world".to_owned();
/// let mut cursor = Cursor::new(data.clone());
///
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
///
/// let mut encrypted = Vec::new();
/// encryptor.read_to_end(&mut encrypted).unwrap();
/// let info = encryptor.finish();
///
/// let mut cursor = Cursor::new(encrypted);
/// let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).unwrap();
/// let mut decrypted_data = Vec::new();
/// decryptor.read_to_end(&mut decrypted_data).unwrap();
///
/// let decrypted = String::from_utf8(decrypted_data).unwrap();
/// ```
pub fn new(
input: &'a mut R,
info: EncryptionInfo,
) -> Result<AttachmentDecryptor<'a, R>, DecryptorError> {
if info.version != VERSION {
return Err(DecryptorError::UnknownVersion);
}
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 = Aes256::new_from_slice(&key).map_err(|_| DecryptorError::KeyNonceLength)?;
let aes = Aes256Ctr::from_block_cipher(aes, &iv);
Ok(AttachmentDecryptor { inner: input, expected_hash: hash, sha, aes })
}
}
/// A wrapper that transparently encrypts anything that implements `Read`.
pub struct AttachmentEncryptor<'a, R: Read + 'a> {
finished: bool,
inner: &'a mut R,
web_key: JsonWebKey,
iv: String,
hashes: BTreeMap<String, String>,
aes: Aes256Ctr,
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.read(buf)?;
if read_bytes == 0 {
let hash = self.sha.finalize_reset();
self.hashes.entry("sha256".to_owned()).or_insert_with(|| encode(hash));
Ok(0)
} else {
self.aes.apply_keystream(&mut buf[0..read_bytes]);
self.sha.update(&buf[0..read_bytes]);
Ok(read_bytes)
}
}
}
impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> {
/// Wrap the given reader encrypting all the data we read from it.
///
/// After all the reads are done, and all the data is encrypted that we wish
/// to encrypt a call to [`finish()`](#method.finish) is necessary to get
/// the decryption key for the data.
///
/// # Arguments
///
/// * `reader` - The `Reader` that should be wrapped and enrypted.
///
/// # Panics
///
/// Panics if we can't generate enough random data to create a fresh
/// encryption key.
///
/// # Examples
/// ```
/// # use std::io::{Cursor, Read};
/// # use matrix_sdk_crypto::AttachmentEncryptor;
/// let data = "Hello world".to_owned();
/// let mut cursor = Cursor::new(data.clone());
///
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
///
/// let mut encrypted = Vec::new();
/// encryptor.read_to_end(&mut encrypted).unwrap();
/// let key = encryptor.finish();
/// ```
pub fn new(reader: &'a mut R) -> Self {
let mut key = Zeroizing::new([0u8; KEY_SIZE]);
let mut iv = Zeroizing::new([0u8; IV_SIZE]);
getrandom(&mut *key).expect("Can't generate randomness");
// 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::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 = Aes256::new(key);
let aes = Aes256Ctr::from_block_cipher(aes, iv);
AttachmentEncryptor {
finished: false,
inner: reader,
iv: encoded_iv,
web_key,
hashes: BTreeMap::new(),
aes,
sha: Sha256::default(),
}
}
/// 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));
EncryptionInfo {
version: VERSION.to_string(),
hashes: self.hashes,
iv: self.iv,
web_key: self.web_key,
}
}
}
/// 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 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,
];
fn example_key() -> EncryptionInfo {
let info = json!({
"v": "v2",
"web_key": {
"kty": "oct",
"alg": "A256CTR",
"ext": true,
"k": "Voq2nkPme_x8no5-Tjq_laDAdxE6iDbxnlQXxwFPgE4",
"key_ops": ["encrypt", "decrypt"]
},
"iv": "i0DovxYdJEcAAAAAAAAAAA",
"hashes": {
"sha256": "ANdt819a8bZl4jKy3Z+jcqtiNICa2y0AW4BBJ/iQRAU"
}
});
serde_json::from_value(info).unwrap()
}
#[test]
fn encrypt_decrypt_cycle() {
let data = "Hello world".to_owned();
let mut cursor = Cursor::new(data.clone());
let mut encryptor = AttachmentEncryptor::new(&mut cursor);
let mut encrypted = Vec::new();
encryptor.read_to_end(&mut encrypted).unwrap();
let key = encryptor.finish();
assert_ne!(encrypted.as_slice(), data.as_bytes());
let mut cursor = Cursor::new(encrypted);
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
let mut decrypted_data = Vec::new();
decryptor.read_to_end(&mut decrypted_data).unwrap();
let decrypted = String::from_utf8(decrypted_data).unwrap();
assert_eq!(data, decrypted);
}
#[test]
fn real_decrypt() {
let mut cursor = Cursor::new(EXAMPLE_DATA.to_vec());
let key = example_key();
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
let mut decrypted_data = Vec::new();
decryptor.read_to_end(&mut decrypted_data).unwrap();
let decrypted = String::from_utf8(decrypted_data).unwrap();
assert_eq!("It's a secret to everybody", decrypted);
}
#[test]
fn decrypt_invalid_hash() {
let mut cursor = Cursor::new("fake message");
let key = example_key();
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
let mut decrypted_data = Vec::new();
assert!(decryptor.read_to_end(&mut decrypted_data).is_err())
}
}
@@ -0,0 +1,323 @@
// 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::io::{Cursor, Read, Seek, SeekFrom};
use aes::{
cipher::{generic_array::GenericArray, FromBlockCipher, NewBlockCipher, StreamCipher},
Aes256, Aes256Ctr,
};
use byteorder::{BigEndian, ReadBytesExt};
use getrandom::getrandom;
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,
utilities::{decode, encode, DecodeError},
};
const SALT_SIZE: usize = 16;
const IV_SIZE: usize = 16;
const MAC_SIZE: usize = 32;
const KEY_SIZE: usize = 32;
const VERSION: u8 = 1;
const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----";
const FOOTER: &str = "-----END MEGOLM SESSION DATA-----";
/// Error representing a failure during key export or import.
#[derive(Error, Debug)]
pub enum KeyExportError {
/// The key export doesn't contain valid headers.
#[error("Invalid or missing key export headers.")]
InvalidHeaders,
/// The key export has been encrypted with an unsupported version.
#[error("The key export has been encrypted with an unsupported version.")]
UnsupportedVersion,
/// The MAC of the encrypted payload is invalid.
#[error("The MAC of the encrypted payload is invalid.")]
InvalidMac,
/// The decrypted key export isn't valid UTF-8.
#[error(transparent)]
InvalidUtf8(#[from] std::string::FromUtf8Error),
/// The decrypted key export doesn't contain valid JSON.
#[error(transparent)]
Json(#[from] SerdeError),
/// The key export string isn't valid base64.
#[error(transparent)]
Decode(#[from] DecodeError),
/// The key export doesn't all the required fields.
#[error(transparent)]
Io(#[from] std::io::Error),
}
/// Try to decrypt a reader into a list of exported room keys.
///
/// # Arguments
///
/// * `passphrase` - The passphrase that was used to encrypt the exported keys.
///
/// # Examples
/// ```no_run
/// # use std::io::Cursor;
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_key_export};
/// # 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();
/// # });
/// ```
pub fn decrypt_key_export(
mut input: impl Read,
passphrase: &str,
) -> Result<Vec<ExportedRoomKey>, KeyExportError> {
let mut x: String = String::new();
input.read_to_string(&mut x)?;
if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) {
return Err(KeyExportError::InvalidHeaders);
}
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)?)?)
}
/// Encrypt the list of exported room keys using the given passphrase.
///
/// # Arguments
///
/// * `keys` - A list of sessions that should be encrypted.
///
/// * `passphrase` - The passphrase that will be used to encrypt the exported
/// room keys.
///
/// * `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 brute-force
/// attacks. Should be at least `10000`, while values in the `100000` ranges
/// should be preferred.
///
/// # Panics
///
/// This method will panic if it can't get enough randomness from the OS to
/// encrypt the exported keys securely.
///
/// # Examples
/// ```no_run
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export};
/// # 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());
/// # block_on(async {
/// let room_id = room_id!("!test:localhost");
/// let exported_keys = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap();
/// let encrypted_export = encrypt_key_export(&exported_keys, "1234", 1);
/// # });
/// ```
pub fn encrypt_key_export(
keys: &[ExportedRoomKey],
passphrase: &str,
rounds: u32,
) -> Result<String, SerdeError> {
let mut plaintext = serde_json::to_string(keys)?.into_bytes();
let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds);
Ok([HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n"))
}
fn encrypt_helper(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String {
let mut salt = [0u8; SALT_SIZE];
let mut iv = [0u8; IV_SIZE];
let mut derived_keys = [0u8; KEY_SIZE * 2];
getrandom(&mut salt).expect("Can't generate randomness");
getrandom(&mut iv).expect("Can't generate randomness");
let mut iv = u128::from_be_bytes(iv);
iv &= !(1 << 63);
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
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);
let mut payload: Vec<u8> = vec![];
payload.extend(&VERSION.to_be_bytes());
payload.extend(&salt);
payload.extend(&*iv);
payload.extend(&rounds.to_be_bytes());
payload.extend_from_slice(plaintext);
let mut hmac = Hmac::<Sha256>::new_from_slice(hmac_key).expect("Can't create HMAC object");
hmac.update(&payload);
let mac = hmac.finalize();
payload.extend(mac.into_bytes());
encode(payload)
}
fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExportError> {
let decoded = decode(ciphertext)?;
let mut decoded = Cursor::new(decoded);
let mut salt = [0u8; SALT_SIZE];
let mut iv = [0u8; IV_SIZE];
let mut mac = [0u8; MAC_SIZE];
let mut derived_keys = [0u8; KEY_SIZE * 2];
let version = decoded.read_u8()?;
decoded.read_exact(&mut salt)?;
decoded.read_exact(&mut iv)?;
let rounds = decoded.read_u32::<BigEndian>()?;
let ciphertext_start = decoded.position() as usize;
decoded.seek(SeekFrom::End(-32))?;
let ciphertext_end = decoded.position() as usize;
decoded.read_exact(&mut mac)?;
let mut decoded = decoded.into_inner();
if version != VERSION {
return Err(KeyExportError::UnsupportedVersion);
}
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_from_slice(hmac_key).expect("Can't create an HMAC object");
hmac.update(&decoded[0..ciphertext_end]);
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 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())?)
}
#[cfg(test)]
mod test {
use std::io::Cursor;
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;
const PASSPHRASE: &str = "1234";
const TEST_EXPORT: &str = indoc! {"
-----BEGIN MEGOLM SESSION DATA-----
Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo
Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI
CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU
bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD
ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9
41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U
XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53
uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR
RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo
SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9
MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ
GYW19pdjg0qdXNk/eqZsQTsNWVo6A
-----END MEGOLM SESSION DATA-----
"};
fn export_wihtout_headers() -> String {
TEST_EXPORT.lines().filter(|l| !l.starts_with("-----")).collect()
}
#[test]
fn test_decode() {
let export = export_wihtout_headers();
assert!(decode(export).is_ok());
}
proptest! {
#[test]
fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) {
let mut plaintext_bytes = plaintext.clone().into_bytes();
let ciphertext = encrypt_helper(&mut plaintext_bytes, "test", 1);
let decrypted = decrypt_helper(&ciphertext, "test").unwrap();
prop_assert!(plaintext == decrypted);
}
}
#[test]
fn test_encrypt_decrypt() {
let data = "It's a secret to everybody";
let mut bytes = data.to_owned().into_bytes();
let encrypted = encrypt_helper(&mut bytes, PASSPHRASE, 10);
let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap();
assert_eq!(data, decrypted);
}
#[async_test]
async fn test_session_encrypt() {
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();
assert!(!export.is_empty());
let encrypted = encrypt_key_export(&export, "1234", 1).unwrap();
let decrypted = decrypt_key_export(Cursor::new(encrypted), "1234").unwrap();
assert_eq!(export, decrypted);
assert_eq!(machine.import_keys(decrypted, |_, _| {}).await.unwrap(), (0, 1));
}
#[test]
fn test_real_decrypt() {
let reader = Cursor::new(TEST_EXPORT);
let imported = decrypt_key_export(reader, PASSPHRASE).expect("Can't decrypt key export");
assert!(!imported.is_empty())
}
}
@@ -0,0 +1,5 @@
mod attachments;
mod key_export;
pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError, EncryptionInfo};
pub use key_export::{decrypt_key_export, encrypt_key_export, KeyExportError};

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