mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-16 12:43:01 -04:00
Merge branch 'main' into mauroromito/directory_search
This commit is contained in:
@@ -22,6 +22,7 @@ sing = "sign"
|
||||
singed = "signed"
|
||||
singing = "signing"
|
||||
Nd = "Nd"
|
||||
ratatui = "ratatui"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
|
||||
263
Cargo.lock
generated
263
Cargo.lock
generated
@@ -635,12 +635,27 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -803,6 +818,33 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"color-spantrace",
|
||||
"eyre",
|
||||
"indenter",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-spantrace"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-core",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
@@ -815,6 +857,19 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.4.0"
|
||||
@@ -986,6 +1041,31 @@ version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
@@ -2693,6 +2773,15 @@ dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
@@ -3121,6 +3210,7 @@ dependencies = [
|
||||
"stream_assert",
|
||||
"subtle",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
@@ -3447,10 +3537,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multiverse"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"color-eyre",
|
||||
"crossterm",
|
||||
"futures-util",
|
||||
"imbl",
|
||||
"matrix-sdk",
|
||||
"matrix-sdk-ui",
|
||||
"ratatui",
|
||||
"rpassword",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.11"
|
||||
@@ -3833,6 +3945,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
@@ -4488,6 +4606,26 @@ dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"itertools 0.12.1",
|
||||
"lru",
|
||||
"paste",
|
||||
"stability",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.1"
|
||||
@@ -4692,19 +4830,14 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rrrepl"
|
||||
version = "0.1.0"
|
||||
name = "rpassword"
|
||||
version = "7.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
"matrix-sdk",
|
||||
"matrix-sdk-ui",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4727,10 +4860,20 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rtoolbox"
|
||||
version = "0.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruma"
|
||||
version = "0.9.4"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"assign",
|
||||
"js_int",
|
||||
@@ -4746,7 +4889,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-client-api"
|
||||
version = "0.17.4"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"assign",
|
||||
@@ -4765,7 +4908,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-common"
|
||||
version = "0.12.1"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"base64 0.21.7",
|
||||
@@ -4795,7 +4938,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-events"
|
||||
version = "0.27.11"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"indexmap 2.2.2",
|
||||
@@ -4819,7 +4962,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-federation-api"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -4831,7 +4974,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-html"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"as_variant",
|
||||
"html5ever",
|
||||
@@ -4843,7 +4986,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-identifiers-validation"
|
||||
version = "0.9.3"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"thiserror",
|
||||
@@ -4852,7 +4995,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-macros"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate 2.0.2",
|
||||
@@ -4867,7 +5010,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "ruma-push-gateway-api"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
|
||||
source = "git+https://github.com/ruma/ruma?rev=b2542df2bbbdf09af0612c9f28bcfa5620e1911c#b2542df2bbbdf09af0612c9f28bcfa5620e1911c"
|
||||
dependencies = [
|
||||
"js_int",
|
||||
"ruma-common",
|
||||
@@ -5299,6 +5442,36 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
@@ -5377,6 +5550,16 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stability"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -5459,6 +5642,28 @@ dependencies = [
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.48",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
@@ -5926,6 +6131,16 @@ dependencies = [
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
@@ -6053,6 +6268,12 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.11"
|
||||
|
||||
@@ -35,9 +35,10 @@ futures-core = "0.3.28"
|
||||
futures-executor = "0.3.21"
|
||||
futures-util = { version = "0.3.26", default-features = false, features = ["alloc"] }
|
||||
http = "0.2.6"
|
||||
imbl = "2.0.0"
|
||||
itertools = "0.12.0"
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "68c9bb0930f2195fa8672fbef9633ef62737df5d", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "68c9bb0930f2195fa8672fbef9633ef62737df5d" }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "b2542df2bbbdf09af0612c9f28bcfa5620e1911c", features = ["client-api-c", "compat-upload-signatures", "compat-user-id", "compat-arbitrary-length-ids", "unstable-msc3401"] }
|
||||
ruma-common = { git = "https://github.com/ruma/ruma", rev = "b2542df2bbbdf09af0612c9f28bcfa5620e1911c" }
|
||||
once_cell = "1.16.0"
|
||||
rand = "0.8.5"
|
||||
serde = "1.0.151"
|
||||
|
||||
@@ -29,7 +29,7 @@ eyeball-im = { workspace = true }
|
||||
extension-trait = "1.0.1"
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi"] }
|
||||
matrix-sdk-ui = { workspace = true, features = ["e2e-encryption", "uniffi", "experimental-room-list-with-unified-invites"] }
|
||||
mime = "0.3.16"
|
||||
once_cell = { workspace = true }
|
||||
opentelemetry = "0.21.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use matrix_sdk::{
|
||||
self, encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, HttpError,
|
||||
encryption::CryptoStoreError, event_cache::EventCacheError, oidc::OidcError, HttpError,
|
||||
IdParseError, NotificationSettingsError as SdkNotificationSettingsError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, sync_service, timeline};
|
||||
|
||||
@@ -319,6 +319,13 @@ impl NotificationSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether [MSC 4028 push rule][rule] is enabled on the homeserver.
|
||||
///
|
||||
/// [rule]: https://github.com/matrix-org/matrix-spec-proposals/blob/giomfo/push_encrypted_events/proposals/4028-push-all-encrypted-events-except-for-muted-rooms.md
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> bool {
|
||||
self.sdk_client.can_homeserver_push_encrypted_event_to_device().await.unwrap()
|
||||
}
|
||||
|
||||
/// Set whether user mentions are enabled.
|
||||
pub async fn set_user_mention_enabled(
|
||||
&self,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use matrix_sdk::{
|
||||
@@ -10,7 +10,13 @@ use mime::Mime;
|
||||
use ruma::{
|
||||
api::client::room::report_content,
|
||||
assign,
|
||||
events::room::{avatar::ImageInfo as RumaAvatarImageInfo, MediaSource},
|
||||
events::{
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
power_levels::RoomPowerLevels as RumaPowerLevels, MediaSource,
|
||||
},
|
||||
TimelineEventType,
|
||||
},
|
||||
EventId, Int, UserId,
|
||||
};
|
||||
use tokio::sync::RwLock;
|
||||
@@ -561,11 +567,9 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn build_power_level_changes_from_current(
|
||||
&self,
|
||||
) -> Result<RoomPowerLevelChanges, ClientError> {
|
||||
pub async fn get_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
let power_levels = self.inner.room_power_levels().await?;
|
||||
Ok(power_levels.into())
|
||||
Ok(RoomPowerLevels::from(power_levels))
|
||||
}
|
||||
|
||||
pub async fn apply_power_level_changes(
|
||||
@@ -603,6 +607,58 @@ impl Room {
|
||||
let user_id = UserId::parse(&user_id)?;
|
||||
Ok(self.inner.get_suggested_user_role(&user_id).await?)
|
||||
}
|
||||
|
||||
pub async fn reset_power_levels(&self) -> Result<RoomPowerLevels, ClientError> {
|
||||
Ok(RoomPowerLevels::from(self.inner.reset_power_levels().await?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct RoomPowerLevels {
|
||||
/// The level required to ban a user.
|
||||
pub ban: i64,
|
||||
/// The level required to invite a user.
|
||||
pub invite: i64,
|
||||
/// The level required to kick a user.
|
||||
pub kick: i64,
|
||||
/// The level required to redact an event.
|
||||
pub redact: i64,
|
||||
/// The default level required to send message events.
|
||||
pub events_default: i64,
|
||||
/// The default level required to send state events.
|
||||
pub state_default: i64,
|
||||
/// The default power level for every user in the room.
|
||||
pub users_default: i64,
|
||||
/// The level required to change the room's name.
|
||||
pub room_name: i64,
|
||||
/// The level required to change the room's avatar.
|
||||
pub room_avatar: i64,
|
||||
/// The level required to change the room's topic.
|
||||
pub room_topic: i64,
|
||||
}
|
||||
|
||||
impl From<RumaPowerLevels> for RoomPowerLevels {
|
||||
fn from(value: RumaPowerLevels) -> Self {
|
||||
fn state_event_level_for(
|
||||
power_levels: &RumaPowerLevels,
|
||||
event_type: &TimelineEventType,
|
||||
) -> i64 {
|
||||
let default_state: i64 = power_levels.state_default.into();
|
||||
power_levels.events.get(event_type).map_or(default_state, |&level| level.into())
|
||||
}
|
||||
Self {
|
||||
ban: value.ban.into(),
|
||||
invite: value.invite.into(),
|
||||
kick: value.kick.into(),
|
||||
redact: value.redact.into(),
|
||||
events_default: value.events_default.into(),
|
||||
state_default: value.state_default.into(),
|
||||
users_default: value.users_default.into(),
|
||||
room_name: state_event_level_for(&value, &TimelineEventType::RoomName),
|
||||
room_avatar: state_event_level_for(&value, &TimelineEventType::RoomAvatar),
|
||||
room_topic: state_event_level_for(&value, &TimelineEventType::RoomTopic),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export(callback_interface)]
|
||||
|
||||
@@ -95,6 +95,15 @@ impl SyncServiceBuilder {
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl SyncServiceBuilder {
|
||||
pub fn with_unified_invites_in_room_list(
|
||||
self: Arc<Self>,
|
||||
with_unified_invites: bool,
|
||||
) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_unified_invites_in_room_list(with_unified_invites);
|
||||
Arc::new(Self { builder })
|
||||
}
|
||||
|
||||
pub fn with_cross_process_lock(self: Arc<Self>, app_identifier: Option<String>) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_cross_process_lock(app_identifier);
|
||||
|
||||
@@ -310,7 +310,7 @@ pub enum OtherState {
|
||||
RoomJoinRules,
|
||||
RoomName { name: Option<String> },
|
||||
RoomPinnedEvents,
|
||||
RoomPowerLevels { users: HashMap<String, i64> },
|
||||
RoomPowerLevels { users: HashMap<String, i64>, previous: Option<HashMap<String, i64>> },
|
||||
RoomServerAcl,
|
||||
RoomThirdPartyInvite { display_name: Option<String> },
|
||||
RoomTombstone,
|
||||
@@ -353,18 +353,20 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat
|
||||
Self::RoomName { name }
|
||||
}
|
||||
Content::RoomPinnedEvents(_) => Self::RoomPinnedEvents,
|
||||
Content::RoomPowerLevels(c) => {
|
||||
let changes = match c {
|
||||
FullContent::Original { content, prev_content } => {
|
||||
power_level_user_changes(content, prev_content)
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect()
|
||||
}
|
||||
FullContent::Redacted(_) => Default::default(),
|
||||
};
|
||||
Self::RoomPowerLevels { users: changes }
|
||||
}
|
||||
Content::RoomPowerLevels(c) => match c {
|
||||
FullContent::Original { content, prev_content } => Self::RoomPowerLevels {
|
||||
users: power_level_user_changes(content, prev_content)
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect(),
|
||||
previous: prev_content.as_ref().map(|prev_content| {
|
||||
prev_content.users.iter().map(|(k, &v)| (k.to_string(), v.into())).collect()
|
||||
}),
|
||||
},
|
||||
FullContent::Redacted(_) => {
|
||||
Self::RoomPowerLevels { users: Default::default(), previous: None }
|
||||
}
|
||||
},
|
||||
Content::RoomServerAcl(_) => Self::RoomServerAcl,
|
||||
Content::RoomThirdPartyInvite(c) => {
|
||||
let display_name = match c {
|
||||
|
||||
@@ -302,7 +302,8 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
/// * `key` - The key to fetch data for
|
||||
async fn get_custom_value(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Put arbitrary data into the custom store
|
||||
/// Put arbitrary data into the custom store, return the data previously
|
||||
/// stored
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -315,6 +316,27 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
value: Vec<u8>,
|
||||
) -> Result<Option<Vec<u8>>, Self::Error>;
|
||||
|
||||
/// Put arbitrary data into the custom store, do not attempt to read any
|
||||
/// previous data
|
||||
///
|
||||
/// Optimization option for set_custom_values for stores that would perform
|
||||
/// better withouts the extra read and the caller not needing that data
|
||||
/// returned. Otherwise this just wraps around `set_custom_data` and
|
||||
/// discards the result.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key` - The key to insert data into
|
||||
///
|
||||
/// * `value` - The value to insert
|
||||
async fn set_custom_value_no_read(
|
||||
&self,
|
||||
key: &[u8],
|
||||
value: Vec<u8>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.set_custom_value(key, value).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Remove arbitrary data from the custom store and return it if existed
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -29,6 +29,9 @@ Additions:
|
||||
- Add new API `store::Store::export_room_keys_stream` that provides room
|
||||
keys on demand.
|
||||
|
||||
- Include event timestamps on logs from event decryption.
|
||||
([#3194](https://github.com/matrix-org/matrix-rust-sdk/pull/3194))
|
||||
|
||||
# 0.7.0
|
||||
|
||||
- Add method to mark a list of inbound group sessions as backed up:
|
||||
|
||||
@@ -52,6 +52,7 @@ serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
subtle = "2.5.0"
|
||||
time = { version = "0.3.34", features = ["formatting"] }
|
||||
tokio-stream = { workspace = true, features = ["sync"] }
|
||||
tokio = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -88,6 +88,7 @@ use crate::{
|
||||
},
|
||||
EventEncryptionAlgorithm, Signatures,
|
||||
},
|
||||
utilities::timestamp_to_iso8601,
|
||||
verification::{Verification, VerificationMachine, VerificationRequest},
|
||||
CrossSigningKeyExport, CryptoStoreError, KeysQueryRequest, LocalTrust, ReadOnlyDevice,
|
||||
RoomKeyImportResult, SignatureError, ToDeviceRequest,
|
||||
@@ -1534,7 +1535,7 @@ impl OlmMachine {
|
||||
/// * `event` - The event that should be decrypted.
|
||||
///
|
||||
/// * `room_id` - The ID of the room where the event was sent to.
|
||||
#[instrument(skip_all, fields(?room_id, event_id, sender, algorithm, session_id, sender_key))]
|
||||
#[instrument(skip_all, fields(?room_id, event_id, origin_server_ts, sender, algorithm, session_id, sender_key))]
|
||||
pub async fn decrypt_room_event(
|
||||
&self,
|
||||
event: &Raw<EncryptedEvent>,
|
||||
@@ -1545,6 +1546,11 @@ impl OlmMachine {
|
||||
tracing::Span::current()
|
||||
.record("sender", debug(&event.sender))
|
||||
.record("event_id", debug(&event.event_id))
|
||||
.record(
|
||||
"origin_server_ts",
|
||||
timestamp_to_iso8601(event.origin_server_ts)
|
||||
.unwrap_or_else(|| "<out of range>".to_owned()),
|
||||
)
|
||||
.record("algorithm", debug(event.content.algorithm()));
|
||||
|
||||
let content: SupportedEventEncryptionSchemes<'_> = match &event.content.scheme {
|
||||
|
||||
@@ -735,20 +735,16 @@ pub struct PickledOutboundGroupSession {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{sync::atomic::Ordering, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
device_id,
|
||||
events::room::{
|
||||
encryption::RoomEncryptionEventContent, history_visibility::HistoryVisibility,
|
||||
message::RoomMessageEventContent,
|
||||
},
|
||||
room_id, uint, user_id, EventEncryptionAlgorithm,
|
||||
uint, EventEncryptionAlgorithm,
|
||||
};
|
||||
|
||||
use super::{EncryptionSettings, ROTATION_MESSAGES, ROTATION_PERIOD};
|
||||
use crate::{Account, MegolmError};
|
||||
|
||||
#[test]
|
||||
fn test_encryption_settings_conversion() {
|
||||
@@ -768,78 +764,163 @@ mod tests {
|
||||
assert_eq!(settings.rotation_period_msgs, 500);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", target_arch = "wasm32"))]
|
||||
async fn test_expiration() -> Result<(), MegolmError> {
|
||||
use ruma::{serde::Raw, SecondsSinceUnixEpoch};
|
||||
mod expiration {
|
||||
use std::{sync::atomic::Ordering, time::Duration};
|
||||
|
||||
let settings = EncryptionSettings { rotation_period_msgs: 1, ..Default::default() };
|
||||
|
||||
let account =
|
||||
Account::with_device_id(user_id!("@alice:example.org"), device_id!("DEVICEID"))
|
||||
.static_data;
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!session.expired());
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
assert!(session.expired());
|
||||
|
||||
let settings = EncryptionSettings {
|
||||
rotation_period: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
use matrix_sdk_test::async_test;
|
||||
use ruma::{
|
||||
device_id, events::room::message::RoomMessageEventContent, room_id, serde::Raw, uint,
|
||||
user_id, SecondsSinceUnixEpoch,
|
||||
};
|
||||
|
||||
let (mut session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
use crate::{olm::OutboundGroupSession, Account, EncryptionSettings, MegolmError};
|
||||
|
||||
assert!(!session.expired());
|
||||
|
||||
let now = SecondsSinceUnixEpoch::now();
|
||||
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(3600));
|
||||
assert!(session.expired());
|
||||
|
||||
let settings = EncryptionSettings { rotation_period_msgs: 0, ..Default::default() };
|
||||
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!session.expired());
|
||||
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
#[async_test]
|
||||
async fn session_is_not_expired_if_no_messages_sent_and_no_time_passed() {
|
||||
// Given a session that expires after one message
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 1,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
assert!(session.expired());
|
||||
|
||||
let settings = EncryptionSettings { rotation_period_msgs: 100_000, ..Default::default() };
|
||||
// When we send no messages at all
|
||||
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
// Then it is not expired
|
||||
assert!(!session.expired());
|
||||
}
|
||||
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(1000, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(9999, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(10_000, Ordering::SeqCst);
|
||||
assert!(session.expired());
|
||||
#[async_test]
|
||||
async fn session_is_expired_if_we_rotate_every_message_and_one_was_sent(
|
||||
) -> Result<(), MegolmError> {
|
||||
// Given a session that expires after one message
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 1,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
// When we send a message
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Then the session is expired
|
||||
assert!(session.expired());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_with_short_rotation_period_is_not_expired_after_no_time() {
|
||||
// Given a session with a 100ms expiration
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When we don't allow any time to pass
|
||||
|
||||
// Then it is not expired
|
||||
assert!(!session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_is_expired_after_rotation_period() {
|
||||
// Given a session with a 100ms expiration
|
||||
let mut session = create_session(EncryptionSettings {
|
||||
rotation_period: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When one hour has passed
|
||||
let now = SecondsSinceUnixEpoch::now();
|
||||
session.creation_time = SecondsSinceUnixEpoch(now.get() - uint!(3600));
|
||||
|
||||
// Then the session is expired
|
||||
assert!(session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_with_zero_msgs_rotation_is_not_expired_initially() {
|
||||
// Given a session that is supposed to expire after zero messages
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 0,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When we send no messages
|
||||
|
||||
// Then the session is not expired: we are protected against this nonsensical
|
||||
// setup
|
||||
assert!(!session.expired());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_with_zero_msgs_rotation_expires_after_one_message(
|
||||
) -> Result<(), MegolmError> {
|
||||
// Given a session that is supposed to expire after zero messages
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 0,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// When we send a message
|
||||
let _ = session
|
||||
.encrypt(
|
||||
"m.room.message",
|
||||
&Raw::new(&RoomMessageEventContent::text_plain("Test message"))?.cast(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Then the session is expired: we treated rotation_period_msgs=0 as if it were
|
||||
// =1
|
||||
assert!(session.expired());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_expires_after_10k_messages_even_if_we_ask_for_more() {
|
||||
// Given we asked to expire after 100K messages
|
||||
let session = create_session(EncryptionSettings {
|
||||
rotation_period_msgs: 100_000,
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
|
||||
// Sanity: it does not expire after <10K messages
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(1000, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
session.message_count.store(9999, Ordering::SeqCst);
|
||||
assert!(!session.expired());
|
||||
|
||||
// When we have sent >= 10K messages
|
||||
session.message_count.store(10_000, Ordering::SeqCst);
|
||||
|
||||
// Then it is considered expired: we enforce a maximum of 10K messages before
|
||||
// rotation.
|
||||
assert!(session.expired());
|
||||
}
|
||||
|
||||
async fn create_session(settings: EncryptionSettings) -> OutboundGroupSession {
|
||||
let account =
|
||||
Account::with_device_id(user_id!("@alice:example.org"), device_id!("DEVICEID"))
|
||||
.static_data;
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
session
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::num::NonZeroU8;
|
||||
|
||||
use ruma::MilliSecondsSinceUnixEpoch;
|
||||
use time::{
|
||||
format_description::well_known::{iso8601, Iso8601},
|
||||
OffsetDateTime,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn json_convert<T, U>(value: &T) -> serde_json::Result<U>
|
||||
where
|
||||
@@ -21,3 +29,57 @@ where
|
||||
let json = serde_json::to_string(value)?;
|
||||
serde_json::from_str(&json)
|
||||
}
|
||||
|
||||
const ISO8601_WITH_MILLIS: iso8601::EncodedConfig = iso8601::Config::DEFAULT
|
||||
.set_time_precision(iso8601::TimePrecision::Second { decimal_digits: NonZeroU8::new(3) })
|
||||
.encode();
|
||||
|
||||
/// Format the given timestamp into a human-readable timestamp.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Provided the timestamp fits within an `OffsetDateTime` (ie, it is on or
|
||||
/// before year 9999), a string that looks like `1970-01-01T00:00:00.000Z`.
|
||||
/// Otherwise, `None`.
|
||||
pub fn timestamp_to_iso8601(ts: MilliSecondsSinceUnixEpoch) -> Option<String> {
|
||||
let nanos_since_epoch = i128::from(ts.get()) * 1_000_000;
|
||||
|
||||
// OffsetDateTime has a max year of 9999, whereas MilliSecondsSinceUnixEpoch has
|
||||
// a max year of 285427, so `from_unix_timestamp_nanos` can overflow for very
|
||||
// large timestamps. (The Y10K problem!)
|
||||
let dt = OffsetDateTime::from_unix_timestamp_nanos(nanos_since_epoch).ok()?;
|
||||
|
||||
// SAFETY: `format` can fail if:
|
||||
// * The input lacks information on a component we have asked it to format
|
||||
// (eg, it is given a `Time` and we ask it for a date), or
|
||||
// * The input contains an invalid component (eg 30th February), or
|
||||
// * An `io::Error` is raised internally.
|
||||
//
|
||||
// The first two cannot occur because we know we are giving it a valid
|
||||
// OffsetDateTime that has all the components we are asking it to print.
|
||||
//
|
||||
// The third should not occur because we are formatting a short string to an
|
||||
// in-memory buffer.
|
||||
|
||||
Some(dt.format(&Iso8601::<ISO8601_WITH_MILLIS>).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use ruma::{MilliSecondsSinceUnixEpoch, UInt};
|
||||
|
||||
use super::timestamp_to_iso8601;
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_to_iso8601() {
|
||||
assert_eq!(
|
||||
timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::new_saturating(0))),
|
||||
Some("1970-01-01T00:00:00.000Z".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::new_saturating(1709657033012))),
|
||||
Some("2024-03-05T16:43:53.012Z".to_owned())
|
||||
);
|
||||
assert_eq!(timestamp_to_iso8601(MilliSecondsSinceUnixEpoch(UInt::MAX)), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1517,6 +1517,13 @@ impl StateStore for SqliteStateStore {
|
||||
self.acquire().await?.get_kv_blob(self.encode_custom_key(key)).await
|
||||
}
|
||||
|
||||
async fn set_custom_value_no_read(&self, key: &[u8], value: Vec<u8>) -> Result<()> {
|
||||
let conn = self.acquire().await?;
|
||||
let key = self.encode_custom_key(key);
|
||||
conn.set_kv_blob(key, value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_custom_value(&self, key: &[u8], value: Vec<u8>) -> Result<Option<Vec<u8>>> {
|
||||
let conn = self.acquire().await?;
|
||||
let key = self.encode_custom_key(key);
|
||||
|
||||
@@ -12,6 +12,9 @@ default = ["e2e-encryption", "native-tls"]
|
||||
|
||||
e2e-encryption = ["matrix-sdk/e2e-encryption"]
|
||||
|
||||
# This feature will unify the `invites` list with the `all_rooms` list.
|
||||
experimental-room-list-with-unified-invites = []
|
||||
|
||||
native-tls = ["matrix-sdk/native-tls"]
|
||||
rustls-tls = ["matrix-sdk/rustls-tls"]
|
||||
|
||||
@@ -31,7 +34,7 @@ eyeball-im-util = { workspace = true }
|
||||
futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
fuzzy-matcher = "0.3.7"
|
||||
imbl = { version = "2.0.0", features = ["serde"] }
|
||||
imbl = { workspace = true, features = ["serde"] }
|
||||
indexmap = "2.0.0"
|
||||
itertools = { workspace = true }
|
||||
matrix-sdk = { workspace = true, features = ["experimental-oidc", "experimental-sliding-sync"] }
|
||||
|
||||
@@ -136,7 +136,24 @@ impl RoomListService {
|
||||
/// This won't start an encryption sync, and it's the user's responsibility
|
||||
/// to create one in this case using `EncryptionSync`.
|
||||
pub async fn new(client: Client) -> Result<Self, Error> {
|
||||
Self::new_internal(client, false).await
|
||||
Self::new_internal(
|
||||
client,
|
||||
false,
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Create a new `RoomList` that disables encryption, and enables the
|
||||
/// unified invites (i.e. invites are part of the `all_rooms` list; side
|
||||
/// note: the `invites` list is still present).
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
pub async fn new_with_unified_invites(
|
||||
client: Client,
|
||||
with_unified_invites: bool,
|
||||
) -> Result<Self, Error> {
|
||||
Self::new_internal(client, false, with_unified_invites).await
|
||||
}
|
||||
|
||||
/// Create a new `RoomList` that enables encryption.
|
||||
@@ -144,10 +161,20 @@ impl RoomListService {
|
||||
/// This will include syncing the encryption information, so there must not
|
||||
/// be any instance of `EncryptionSync` running in the background.
|
||||
pub async fn new_with_encryption(client: Client) -> Result<Self, Error> {
|
||||
Self::new_internal(client, true).await
|
||||
Self::new_internal(
|
||||
client,
|
||||
true,
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_internal(client: Client, with_encryption: bool) -> Result<Self, Error> {
|
||||
async fn new_internal(
|
||||
client: Client,
|
||||
with_encryption: bool,
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")] with_unified_invites: bool,
|
||||
) -> Result<Self, Error> {
|
||||
let mut builder = client
|
||||
.sliding_sync("room-list")
|
||||
.map_err(Error::SlidingSync)?
|
||||
@@ -185,6 +212,8 @@ impl RoomListService {
|
||||
(StateEventType::RoomMember, "$ME".to_owned()),
|
||||
(StateEventType::RoomPowerLevels, "".to_owned()),
|
||||
]),
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
with_unified_invites,
|
||||
))
|
||||
.await
|
||||
.map_err(Error::SlidingSync)?
|
||||
@@ -479,11 +508,15 @@ impl RoomListService {
|
||||
/// properties, so that they are exactly the same.
|
||||
fn configure_all_or_visible_rooms_list(
|
||||
list_builder: SlidingSyncListBuilder,
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")] with_invites: bool,
|
||||
) -> SlidingSyncListBuilder {
|
||||
#[cfg(not(feature = "experimental-room-list-with-unified-invites"))]
|
||||
let with_invites = false;
|
||||
|
||||
list_builder
|
||||
.sort(vec!["by_recency".to_owned(), "by_name".to_owned()])
|
||||
.filters(Some(assign!(SyncRequestListFilters::default(), {
|
||||
is_invite: Some(false),
|
||||
is_invite: Some(with_invites),
|
||||
is_tombstoned: Some(false),
|
||||
not_room_types: vec!["m.space".to_owned()],
|
||||
})))
|
||||
|
||||
@@ -120,6 +120,8 @@ impl Action for AddVisibleRooms {
|
||||
(StateEventType::RoomEncryption, "".to_owned()),
|
||||
(StateEventType::RoomMember, "$LAZY".to_owned()),
|
||||
]),
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
false,
|
||||
))
|
||||
.await
|
||||
.map_err(Error::SlidingSync)?;
|
||||
|
||||
@@ -435,6 +435,10 @@ pub struct SyncServiceBuilder {
|
||||
/// SDK client.
|
||||
client: Client,
|
||||
|
||||
/// Whether we want to unify `all_rooms` and `invites`.
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
with_unified_invites_in_room_list: bool,
|
||||
|
||||
/// Is the cross-process lock for the crypto store enabled?
|
||||
with_cross_process_lock: bool,
|
||||
|
||||
@@ -445,7 +449,20 @@ pub struct SyncServiceBuilder {
|
||||
|
||||
impl SyncServiceBuilder {
|
||||
fn new(client: Client) -> Self {
|
||||
Self { client, with_cross_process_lock: false, identifier: "app".to_owned() }
|
||||
Self {
|
||||
client,
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
with_unified_invites_in_room_list: false,
|
||||
with_cross_process_lock: false,
|
||||
identifier: "app".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
pub fn with_unified_invites_in_room_list(mut self, with_unified_invites: bool) -> Self {
|
||||
self.with_unified_invites_in_room_list = with_unified_invites;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables the cross-process lock, if the sync service is being built in a
|
||||
@@ -475,8 +492,16 @@ impl SyncServiceBuilder {
|
||||
pub async fn build(self) -> Result<SyncService, Error> {
|
||||
let encryption_sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new()));
|
||||
|
||||
#[cfg(not(feature = "experimental-room-list-with-unified-invites"))]
|
||||
let room_list = RoomListService::new(self.client.clone()).await?;
|
||||
|
||||
#[cfg(feature = "experimental-room-list-with-unified-invites")]
|
||||
let room_list = RoomListService::new_with_unified_invites(
|
||||
self.client.clone(),
|
||||
self.with_unified_invites_in_room_list,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let encryption_sync = Arc::new(
|
||||
EncryptionSyncService::new(
|
||||
self.identifier,
|
||||
|
||||
@@ -82,7 +82,7 @@ futures-core = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
http = { workspace = true }
|
||||
hyper = { version = "0.14.20", features = ["http1", "http2", "server"], optional = true }
|
||||
imbl = { version = "2.0.0", features = ["serde"] }
|
||||
imbl = { workspace = true, features = ["serde"] }
|
||||
indexmap = "2.0.2"
|
||||
js_int = "0.2.2"
|
||||
language-tags = { version = "0.3.2", optional = true }
|
||||
|
||||
@@ -508,6 +508,7 @@ impl ClientBuilder {
|
||||
http_client,
|
||||
base_client,
|
||||
self.server_versions,
|
||||
None,
|
||||
self.respect_login_well_known,
|
||||
event_cache,
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
|
||||
@@ -235,6 +235,9 @@ pub(crate) struct ClientInner {
|
||||
/// The Matrix versions the server supports (well-known ones only)
|
||||
server_versions: OnceCell<Box<[MatrixVersion]>>,
|
||||
|
||||
/// The unstable features and their on/off state on the server
|
||||
unstable_features: OnceCell<BTreeMap<String, bool>>,
|
||||
|
||||
/// Collection of locks individual client methods might want to use, either
|
||||
/// to ensure that only a single call to a method happens at once or to
|
||||
/// deduplicate multiple calls to a method.
|
||||
@@ -292,6 +295,7 @@ impl ClientInner {
|
||||
http_client: HttpClient,
|
||||
base_client: BaseClient,
|
||||
server_versions: Option<Box<[MatrixVersion]>>,
|
||||
unstable_features: Option<BTreeMap<String, bool>>,
|
||||
respect_login_well_known: bool,
|
||||
event_cache: OnceCell<EventCache>,
|
||||
#[cfg(feature = "e2e-encryption")] encryption_settings: EncryptionSettings,
|
||||
@@ -305,6 +309,7 @@ impl ClientInner {
|
||||
base_client,
|
||||
locks: Default::default(),
|
||||
server_versions: OnceCell::new_with(server_versions),
|
||||
unstable_features: OnceCell::new_with(unstable_features),
|
||||
typing_notice_times: Default::default(),
|
||||
event_handlers: Default::default(),
|
||||
notification_handlers: Default::default(),
|
||||
@@ -1170,12 +1175,10 @@ impl Client {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use matrix_sdk::Client;
|
||||
///
|
||||
/// # use matrix_sdk::ruma::api::client::room::{
|
||||
/// # create_room::v3::Request as CreateRoomRequest,
|
||||
/// # Visibility,
|
||||
/// # };
|
||||
/// use matrix_sdk::{
|
||||
/// ruma::api::client::room::create_room::v3::Request as CreateRoomRequest,
|
||||
/// Client,
|
||||
/// };
|
||||
/// # use url::Url;
|
||||
/// #
|
||||
/// # async {
|
||||
@@ -1403,6 +1406,67 @@ impl Client {
|
||||
Ok(server_versions)
|
||||
}
|
||||
|
||||
/// Fetch unstable_features from homeserver
|
||||
async fn request_unstable_features(&self) -> HttpResult<BTreeMap<String, bool>> {
|
||||
let unstable_features: BTreeMap<String, bool> = self
|
||||
.inner
|
||||
.http_client
|
||||
.send(
|
||||
get_supported_versions::Request::new(),
|
||||
None,
|
||||
self.homeserver().to_string(),
|
||||
None,
|
||||
&[MatrixVersion::V1_0],
|
||||
Default::default(),
|
||||
)
|
||||
.await?
|
||||
.unstable_features;
|
||||
|
||||
Ok(unstable_features)
|
||||
}
|
||||
|
||||
/// Get unstable features from `request_unstable_features` or cache
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::{Client, config::SyncSettings};
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://localhost:8080")?;
|
||||
/// # let mut client = Client::new(homeserver).await?;
|
||||
/// let unstable_features = client.unstable_features().await?;
|
||||
/// let msc_x = unstable_features.get("msc_x").unwrap_or(&false);
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub async fn unstable_features(&self) -> HttpResult<&BTreeMap<String, bool>> {
|
||||
let unstable_features = self
|
||||
.inner
|
||||
.unstable_features
|
||||
.get_or_try_init(|| self.request_unstable_features())
|
||||
.await?;
|
||||
|
||||
Ok(unstable_features)
|
||||
}
|
||||
|
||||
/// Check whether MSC 4028 is enabled on the homeserver.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk::{Client, config::SyncSettings};
|
||||
/// # use url::Url;
|
||||
/// # async {
|
||||
/// # let homeserver = Url::parse("http://localhost:8080")?;
|
||||
/// # let mut client = Client::new(homeserver).await?;
|
||||
/// let msc4028_enabled =
|
||||
/// client.can_homeserver_push_encrypted_event_to_device().await?;
|
||||
/// # anyhow::Ok(()) };
|
||||
/// ```
|
||||
pub async fn can_homeserver_push_encrypted_event_to_device(&self) -> HttpResult<bool> {
|
||||
Ok(self.unstable_features().await?.get("org.matrix.msc4028").copied().unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Get information of all our own devices.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -2008,6 +2072,7 @@ impl Client {
|
||||
self.inner.http_client.clone(),
|
||||
self.inner.base_client.clone_with_in_memory_state_store(),
|
||||
self.inner.server_versions.get().cloned(),
|
||||
self.inner.unstable_features.get().cloned(),
|
||||
self.inner.respect_login_well_known,
|
||||
self.inner.event_cache.clone(),
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
@@ -2271,4 +2336,39 @@ pub(crate) mod tests {
|
||||
assert_eq!(result.avatar_url.clone().unwrap().to_string(), "mxc://example.me/someid");
|
||||
assert!(!response.limited);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_request_unstable_features() {
|
||||
let server = MockServer::start().await;
|
||||
let client = logged_in_client(Some(server.uri())).await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("_matrix/client/versions"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(&*test_json::api_responses::VERSIONS),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let unstable_features = client.request_unstable_features().await.unwrap();
|
||||
|
||||
assert_eq!(unstable_features.get("org.matrix.e2e_cross_signing"), Some(&true));
|
||||
assert_eq!(unstable_features, client.unstable_features().await.unwrap().clone());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_can_homeserver_push_encrypted_event_to_device() {
|
||||
let server = MockServer::start().await;
|
||||
let client = logged_in_client(Some(server.uri())).await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("_matrix/client/versions"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(&*test_json::api_responses::VERSIONS),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let msc4028_enabled = client.can_homeserver_push_encrypted_event_to_device().await.unwrap();
|
||||
assert!(msc4028_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +128,7 @@ impl EventCache {
|
||||
let inner = Arc::new(EventCacheInner {
|
||||
client: Arc::downgrade(client),
|
||||
by_room: Default::default(),
|
||||
store,
|
||||
process_lock: Default::default(),
|
||||
store: Arc::new(Mutex::new(store)),
|
||||
drop_handles: Default::default(),
|
||||
});
|
||||
|
||||
@@ -181,9 +180,10 @@ impl EventCache {
|
||||
// Forget everything we know; we could have missed events, and we have
|
||||
// no way to reconcile at the moment!
|
||||
// TODO: implement Smart Matching™,
|
||||
let store = inner.store.lock().await;
|
||||
let mut by_room = inner.by_room.write().await;
|
||||
for room_id in by_room.keys() {
|
||||
if let Err(err) = inner.store.clear_room_events(room_id).await {
|
||||
if let Err(err) = store.clear_room_events(room_id).await {
|
||||
error!("unable to clear room after room updates lag: {err}");
|
||||
}
|
||||
}
|
||||
@@ -230,10 +230,12 @@ impl EventCache {
|
||||
// We could have received events during a previous sync; remove them all, since
|
||||
// we can't know where to insert the "initial events" with respect to
|
||||
// them.
|
||||
self.inner.store.clear_room_events(room_id).await?;
|
||||
let store = self.inner.store.lock().await;
|
||||
|
||||
store.clear_room_events(room_id).await?;
|
||||
let _ = room_cache.inner.sender.send(RoomEventCacheUpdate::Clear);
|
||||
|
||||
room_cache.inner.append_events(events).await?;
|
||||
room_cache.inner.append_events(&**store, events).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -248,14 +250,13 @@ struct EventCacheInner {
|
||||
by_room: RwLock<BTreeMap<OwnedRoomId, RoomEventCache>>,
|
||||
|
||||
/// Backend used for storage.
|
||||
store: Arc<dyn EventCacheStore>,
|
||||
|
||||
/// A lock to make sure that despite multiple updates coming to the
|
||||
/// `EventCache`, it will only handle one at a time.
|
||||
///
|
||||
/// [`Mutex`] is “fair”, as it is implemented as a FIFO. It is important to
|
||||
/// ensure that multiple updates will be applied in the correct order.
|
||||
process_lock: Mutex<()>,
|
||||
/// ensure that multiple updates will be applied in the correct order, which
|
||||
/// is enforced by taking the store lock when handling an update.
|
||||
///
|
||||
/// TODO: replace with a cross-process lock
|
||||
store: Arc<Mutex<Arc<dyn EventCacheStore>>>,
|
||||
|
||||
/// Handles to keep alive the task listening to updates.
|
||||
drop_handles: OnceLock<Arc<EventCacheDropHandles>>,
|
||||
@@ -271,7 +272,7 @@ impl EventCacheInner {
|
||||
async fn handle_room_updates(&self, updates: RoomUpdates) -> Result<()> {
|
||||
// First, take the lock that indicates we're processing updates, to avoid
|
||||
// handling multiple updates concurrently.
|
||||
let _process_lock = self.process_lock.lock().await;
|
||||
let store = self.store.lock().await;
|
||||
|
||||
// Left rooms.
|
||||
for (room_id, left_room_update) in updates.leave {
|
||||
@@ -280,7 +281,7 @@ impl EventCacheInner {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Err(err) = room.inner.handle_left_room_update(left_room_update).await {
|
||||
if let Err(err) = room.inner.handle_left_room_update(&**store, left_room_update).await {
|
||||
// Non-fatal error, try to continue to the next room.
|
||||
error!("handling left room update: {err}");
|
||||
}
|
||||
@@ -293,7 +294,9 @@ impl EventCacheInner {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Err(err) = room.inner.handle_joined_room_update(joined_room_update).await {
|
||||
if let Err(err) =
|
||||
room.inner.handle_joined_room_update(&**store, joined_room_update).await
|
||||
{
|
||||
// Non-fatal error, try to continue to the next room.
|
||||
error!("handling joined room update: {err}");
|
||||
}
|
||||
@@ -358,7 +361,7 @@ impl Debug for RoomEventCache {
|
||||
|
||||
impl RoomEventCache {
|
||||
/// Create a new [`RoomEventCache`] using the given room and store.
|
||||
fn new(room: Room, store: Arc<dyn EventCacheStore>) -> Self {
|
||||
fn new(room: Room, store: Arc<Mutex<Arc<dyn EventCacheStore>>>) -> Self {
|
||||
Self { inner: Arc::new(RoomEventCacheInner::new(room, store)) }
|
||||
}
|
||||
|
||||
@@ -369,10 +372,9 @@ impl RoomEventCache {
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
) -> Result<(Vec<SyncTimelineEvent>, Receiver<RoomEventCacheUpdate>)> {
|
||||
Ok((
|
||||
self.inner.store.room_events(self.inner.room.room_id()).await?,
|
||||
self.inner.sender.subscribe(),
|
||||
))
|
||||
let store = self.inner.store.lock().await;
|
||||
|
||||
Ok((store.room_events(self.inner.room.room_id()).await?, self.inner.sender.subscribe()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,8 +383,10 @@ struct RoomEventCacheInner {
|
||||
/// Sender part for subscribers to this room.
|
||||
sender: Sender<RoomEventCacheUpdate>,
|
||||
|
||||
/// A pointer to the store implementation used for this event cache.
|
||||
store: Arc<dyn EventCacheStore>,
|
||||
/// Backend used for storage, shared with the parent [`EventCacheInner`].
|
||||
///
|
||||
/// See comment there.
|
||||
store: Arc<Mutex<Arc<dyn EventCacheStore>>>,
|
||||
|
||||
/// The Client [`Room`] this event cache pertains to.
|
||||
room: Room,
|
||||
@@ -391,13 +395,18 @@ struct RoomEventCacheInner {
|
||||
impl RoomEventCacheInner {
|
||||
/// Creates a new cache for a room, and subscribes to room updates, so as
|
||||
/// to handle new timeline events.
|
||||
fn new(room: Room, store: Arc<dyn EventCacheStore>) -> Self {
|
||||
fn new(room: Room, store: Arc<Mutex<Arc<dyn EventCacheStore>>>) -> Self {
|
||||
let sender = Sender::new(32);
|
||||
Self { room, store, sender }
|
||||
}
|
||||
|
||||
async fn handle_joined_room_update(&self, updates: JoinedRoomUpdate) -> Result<()> {
|
||||
async fn handle_joined_room_update(
|
||||
&self,
|
||||
store: &dyn EventCacheStore,
|
||||
updates: JoinedRoomUpdate,
|
||||
) -> Result<()> {
|
||||
self.handle_timeline(
|
||||
store,
|
||||
updates.timeline,
|
||||
updates.ephemeral.clone(),
|
||||
updates.account_data,
|
||||
@@ -409,6 +418,7 @@ impl RoomEventCacheInner {
|
||||
|
||||
async fn handle_timeline(
|
||||
&self,
|
||||
store: &dyn EventCacheStore,
|
||||
timeline: Timeline,
|
||||
ephemeral: Vec<Raw<AnySyncEphemeralRoomEvent>>,
|
||||
account_data: Vec<Raw<AnyRoomAccountDataEvent>>,
|
||||
@@ -419,7 +429,7 @@ impl RoomEventCacheInner {
|
||||
// timeline, but we're not there yet. In the meanwhile, clear the
|
||||
// items from the room. TODO: implement Smart Matching™.
|
||||
trace!("limited timeline, clearing all previous events");
|
||||
self.store.clear_room_events(self.room.room_id()).await?;
|
||||
store.clear_room_events(self.room.room_id()).await?;
|
||||
let _ = self.sender.send(RoomEventCacheUpdate::Clear);
|
||||
}
|
||||
|
||||
@@ -431,7 +441,7 @@ impl RoomEventCacheInner {
|
||||
|| !ambiguity_changes.is_empty()
|
||||
{
|
||||
trace!("adding new events");
|
||||
self.store.add_room_events(self.room.room_id(), timeline.events.clone()).await?;
|
||||
store.add_room_events(self.room.room_id(), timeline.events.clone()).await?;
|
||||
|
||||
// Propagate events to observers.
|
||||
let _ = self.sender.send(RoomEventCacheUpdate::Append {
|
||||
@@ -446,20 +456,34 @@ impl RoomEventCacheInner {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_left_room_update(&self, updates: LeftRoomUpdate) -> Result<()> {
|
||||
self.handle_timeline(updates.timeline, Vec::new(), Vec::new(), updates.ambiguity_changes)
|
||||
.await?;
|
||||
async fn handle_left_room_update(
|
||||
&self,
|
||||
store: &dyn EventCacheStore,
|
||||
updates: LeftRoomUpdate,
|
||||
) -> Result<()> {
|
||||
self.handle_timeline(
|
||||
store,
|
||||
updates.timeline,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
updates.ambiguity_changes,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Append a set of events to the room cache and storage, notifying
|
||||
/// observers.
|
||||
async fn append_events(&self, events: Vec<SyncTimelineEvent>) -> Result<()> {
|
||||
async fn append_events(
|
||||
&self,
|
||||
store: &dyn EventCacheStore,
|
||||
events: Vec<SyncTimelineEvent>,
|
||||
) -> Result<()> {
|
||||
if events.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.store.add_room_events(self.room.room_id(), events.clone()).await?;
|
||||
store.add_room_events(self.room.room_id(), events.clone()).await?;
|
||||
|
||||
let _ = self.sender.send(RoomEventCacheUpdate::Append {
|
||||
events,
|
||||
|
||||
@@ -136,8 +136,11 @@ impl HttpClient {
|
||||
span.record("config", debug(config)).record("request_id", request_id);
|
||||
|
||||
let auth_scheme = R::METADATA.authentication;
|
||||
if !matches!(auth_scheme, AuthScheme::AccessToken | AuthScheme::None) {
|
||||
return Err(HttpError::NotClientRequest);
|
||||
match auth_scheme {
|
||||
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None => {}
|
||||
AuthScheme::ServerSignatures => {
|
||||
return Err(HttpError::NotClientRequest);
|
||||
}
|
||||
}
|
||||
|
||||
let request =
|
||||
|
||||
@@ -871,9 +871,11 @@ impl MatrixAuth {
|
||||
use ruma::api::client::uiaa::{AuthData, Password};
|
||||
|
||||
let auth_data = match login_info {
|
||||
Some(login::v3::LoginInfo::Password(p)) => {
|
||||
Some(AuthData::Password(Password::new(p.identifier, p.password)))
|
||||
}
|
||||
Some(login::v3::LoginInfo::Password(login::v3::Password {
|
||||
identifier: Some(identifier),
|
||||
password,
|
||||
..
|
||||
})) => Some(AuthData::Password(Password::new(identifier, password))),
|
||||
// Other methods can't be immediately translated to an auth.
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -1860,6 +1860,16 @@ impl Room {
|
||||
.power_levels())
|
||||
}
|
||||
|
||||
/// Resets the room's power levels to the default values
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/v1.9/client-server-api/#mroompower_levels
|
||||
pub async fn reset_power_levels(&self) -> Result<RoomPowerLevels> {
|
||||
let default_power_levels = RoomPowerLevels::from(RoomPowerLevelsEventContent::new());
|
||||
let changes = RoomPowerLevelChanges::from(default_power_levels);
|
||||
self.apply_power_level_changes(changes).await?;
|
||||
self.room_power_levels().await
|
||||
}
|
||||
|
||||
/// Gets the suggested role for the user with the provided `user_id`.
|
||||
///
|
||||
/// This method checks the `RoomPowerLevels` events instead of loading the
|
||||
|
||||
@@ -14,13 +14,13 @@ use matrix_sdk::{
|
||||
};
|
||||
use matrix_sdk_base::RoomState;
|
||||
use matrix_sdk_test::{
|
||||
async_test, test_json, EphemeralTestEvent, JoinedRoomBuilder, SyncResponseBuilder,
|
||||
DEFAULT_TEST_ROOM_ID,
|
||||
async_test, test_json, test_json::sync::CUSTOM_ROOM_POWER_LEVELS, EphemeralTestEvent,
|
||||
JoinedRoomBuilder, SyncResponseBuilder, DEFAULT_TEST_ROOM_ID,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{membership::Invite3pidInit, receipt::create_receipt::v3::ReceiptType},
|
||||
assign, event_id,
|
||||
events::{receipt::ReceiptThread, room::message::RoomMessageEventContent},
|
||||
events::{receipt::ReceiptThread, room::message::RoomMessageEventContent, TimelineEventType},
|
||||
int, mxc_uri, owned_event_id, room_id, thirdparty, uint, user_id, OwnedUserId, TransactionId,
|
||||
};
|
||||
use serde_json::json;
|
||||
@@ -817,3 +817,38 @@ async fn get_users_with_power_levels_is_empty_if_power_level_info_is_not_availab
|
||||
|
||||
assert!(room.users_with_power_levels().await.is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn reset_power_levels() {
|
||||
let (client, server) = logged_in_client_with_server().await;
|
||||
|
||||
mock_sync(&server, &*CUSTOM_ROOM_POWER_LEVELS, None).await;
|
||||
|
||||
let sync_settings = SyncSettings::new().timeout(Duration::from_millis(3000));
|
||||
let _response = client.sync_once(sync_settings).await.unwrap();
|
||||
let room = client.get_room(&DEFAULT_TEST_ROOM_ID).unwrap();
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path_regex(r"^/_matrix/client/r0/rooms/.*/state/m.room.power_levels/$"))
|
||||
.and(header("authorization", "Bearer 1234"))
|
||||
.and(body_partial_json(json!({
|
||||
"events": {
|
||||
// 'm.room.avatar' is 100 here, if we receive a value '50', the reset worked
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.topic": 50
|
||||
},
|
||||
})))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::EVENT_ID))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let initial_power_levels = room.room_power_levels().await.unwrap();
|
||||
assert_eq!(initial_power_levels.events[&TimelineEventType::RoomAvatar], int!(100));
|
||||
|
||||
room.reset_power_levels().await.unwrap();
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ Rust SDK can evolve, feel free to propose an experiment.
|
||||
|
||||
## Current experiments
|
||||
|
||||
- rrrepl: a *R*ead *R*eceipts REPL, to help with client-side computation of read-receipts. Useful
|
||||
for debugging.
|
||||
- multiverse: a TUI client mostly for quick development iteration of SDK features and debugging.
|
||||
Run with `cargo run --bin multiverse matrix.org ~/.cache/multiverse-cache`.
|
||||
|
||||
|
||||
## Archived experiments
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
[package]
|
||||
name = "rrrepl"
|
||||
name = "multiverse"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "rrrepl"
|
||||
name = "multiverse"
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
url = "2.2.2"
|
||||
# when copy-pasting this, please use a git dependency or make sure that you
|
||||
# have copied the example as it was at the time of the release you use.
|
||||
color-eyre = "0.6.2"
|
||||
crossterm = "0.27.0"
|
||||
futures-util = { workspace = true }
|
||||
imbl = { workspace = true }
|
||||
matrix-sdk = { path = "../../crates/matrix-sdk", features = ["sso-login"] }
|
||||
matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui" }
|
||||
ratatui = "0.26.1"
|
||||
rpassword = "7.3.1"
|
||||
serde_json = { workspace = true }
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { version = "0.2.2" }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
futures-util = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
url = "2.2.2"
|
||||
861
labs/multiverse/src/main.rs
Normal file
861
labs/multiverse/src/main.rs
Normal file
@@ -0,0 +1,861 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
io::{self, stdout, Write},
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::config::HookBuilder;
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use imbl::Vector;
|
||||
use matrix_sdk::{
|
||||
config::StoreConfig,
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
matrix_auth::MatrixSession,
|
||||
ruma::{
|
||||
api::client::receipt::create_receipt::v3::ReceiptType, events::room::message::MessageType,
|
||||
OwnedRoomId, RoomId,
|
||||
},
|
||||
AuthSession, Client, RoomListEntry, ServerName, SqliteCryptoStore, SqliteStateStore,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service,
|
||||
sync_service::{self, SyncService},
|
||||
timeline::{
|
||||
PaginationOptions, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem,
|
||||
},
|
||||
Timeline as SdkTimeline,
|
||||
};
|
||||
use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
|
||||
use tokio::{spawn, task::JoinHandle};
|
||||
use tracing::error;
|
||||
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter};
|
||||
|
||||
const HEADER_BG: Color = tailwind::BLUE.c950;
|
||||
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
|
||||
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
|
||||
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
|
||||
const TEXT_COLOR: Color = tailwind::SLATE.c200;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(tracing_appender::rolling::hourly("/tmp/", "logs-"));
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(std::env::var("RUST_LOG").unwrap_or("".into())))
|
||||
.with(file_layer)
|
||||
.init();
|
||||
|
||||
// Read the server name from the command line.
|
||||
let Some(server_name) = env::args().nth(1) else {
|
||||
eprintln!("Usage: {} <server_name> <session_path?>", env::args().next().unwrap());
|
||||
exit(1)
|
||||
};
|
||||
|
||||
let config_path = env::args().nth(2).unwrap_or("/tmp/".to_owned());
|
||||
let client = configure_client(server_name, config_path).await?;
|
||||
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
|
||||
let mut app = App::new(client).await?;
|
||||
|
||||
app.run(terminal).await
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> anyhow::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info)
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> anyhow::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> anyhow::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Arc<Mutex<Vector<T>>>,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
enum DetailsMode {
|
||||
#[default]
|
||||
ReadReceipts,
|
||||
TimelineItems,
|
||||
// Events // TODO: Soon™
|
||||
}
|
||||
|
||||
struct Timeline {
|
||||
timeline: Arc<SdkTimeline>,
|
||||
items: Arc<Mutex<Vector<Arc<TimelineItem>>>>,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
struct App {
|
||||
/// Reference to the main SDK client.
|
||||
client: Client,
|
||||
|
||||
/// The sync service used for synchronizing events.
|
||||
sync_service: Arc<SyncService>,
|
||||
|
||||
/// Room list service rooms known to the app.
|
||||
ui_rooms: Arc<Mutex<HashMap<OwnedRoomId, room_list_service::Room>>>,
|
||||
|
||||
/// Timelines data structures for each room.
|
||||
timelines: Arc<Mutex<HashMap<OwnedRoomId, Timeline>>>,
|
||||
|
||||
/// Ratatui's list of room list entries.
|
||||
room_list_entries: StatefulList<RoomListEntry>,
|
||||
|
||||
/// Task listening to room list service changes, and spawning timelines.
|
||||
listen_task: JoinHandle<()>,
|
||||
|
||||
/// Content of the latest status message, if set.
|
||||
last_status_message: Arc<Mutex<Option<String>>>,
|
||||
|
||||
/// A task to automatically clear the status message in N seconds, if set.
|
||||
clear_status_message: Option<JoinHandle<()>>,
|
||||
|
||||
/// What's shown in the details view, aka the right panel.
|
||||
details_mode: DetailsMode,
|
||||
|
||||
/// The current room that's subscribed to in the room list's sliding sync.
|
||||
current_room_subscription: Option<room_list_service::Room>,
|
||||
|
||||
current_pagination: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
async fn new(client: Client) -> anyhow::Result<Self> {
|
||||
let sync_service = Arc::new(SyncService::builder(client.clone()).build().await?);
|
||||
|
||||
let room_list_service = sync_service.room_list_service();
|
||||
|
||||
let all_rooms = room_list_service.all_rooms().await?;
|
||||
let (rooms, stream) = all_rooms.entries();
|
||||
|
||||
let rooms = Arc::new(Mutex::new(rooms));
|
||||
let ui_rooms: Arc<Mutex<HashMap<OwnedRoomId, room_list_service::Room>>> =
|
||||
Default::default();
|
||||
let timelines = Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let r = rooms.clone();
|
||||
let ur = ui_rooms.clone();
|
||||
let s = sync_service.clone();
|
||||
let t = timelines.clone();
|
||||
|
||||
let listen_task = spawn(async move {
|
||||
pin_mut!(stream);
|
||||
let rooms = r;
|
||||
let ui_rooms = ur;
|
||||
let sync_service = s;
|
||||
let timelines = t;
|
||||
|
||||
while let Some(diffs) = stream.next().await {
|
||||
let all_rooms = {
|
||||
// Apply the diffs to the list of room entries.
|
||||
let mut rooms = rooms.lock().unwrap();
|
||||
for diff in diffs {
|
||||
diff.apply(&mut rooms);
|
||||
}
|
||||
|
||||
// Collect rooms early to release the room entries list lock.
|
||||
rooms
|
||||
.iter()
|
||||
.filter_map(|entry| entry.as_room_id().map(ToOwned::to_owned))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
// Clone the previous set of ui rooms to avoid keeping the ui_rooms lock (which
|
||||
// we couldn't do below, because it's a sync lock, and has to be
|
||||
// sync b/o rendering; and we'd have to cross await points
|
||||
// below).
|
||||
let previous_ui_rooms = ui_rooms.lock().unwrap().clone();
|
||||
|
||||
let mut new_ui_rooms = HashMap::new();
|
||||
let mut new_timelines = Vec::new();
|
||||
|
||||
// Initialize all the new rooms.
|
||||
for room_id in
|
||||
all_rooms.into_iter().filter(|room_id| !previous_ui_rooms.contains_key(room_id))
|
||||
{
|
||||
// Retrieve the room list service's Room.
|
||||
let Ok(ui_room) = sync_service.room_list_service().room(&room_id).await else {
|
||||
error!("error when retrieving room after an update");
|
||||
continue;
|
||||
};
|
||||
|
||||
// Initialize the timeline.
|
||||
let builder = match ui_room.default_room_timeline_builder().await {
|
||||
Ok(builder) => builder,
|
||||
Err(err) => {
|
||||
error!("error when getting default timeline builder: {err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = ui_room.init_timeline_with_builder(builder).await {
|
||||
error!("error when creating default timeline: {err}");
|
||||
}
|
||||
|
||||
// Save the timeline in the cache.
|
||||
let sdk_timeline = ui_room.timeline().unwrap();
|
||||
let (items, stream) = sdk_timeline.subscribe().await;
|
||||
let items = Arc::new(Mutex::new(items));
|
||||
|
||||
// Spawn a timeline task that will listen to all the timeline item changes.
|
||||
let i = items.clone();
|
||||
let timeline_task = spawn(async move {
|
||||
pin_mut!(stream);
|
||||
let items = i;
|
||||
while let Some(diff) = stream.next().await {
|
||||
let mut items = items.lock().unwrap();
|
||||
diff.apply(&mut items);
|
||||
}
|
||||
});
|
||||
|
||||
new_timelines.push((
|
||||
room_id.clone(),
|
||||
Timeline { timeline: sdk_timeline, items, task: timeline_task },
|
||||
));
|
||||
|
||||
// Save the room list service room in the cache.
|
||||
new_ui_rooms.insert(room_id, ui_room);
|
||||
}
|
||||
|
||||
ui_rooms.lock().unwrap().extend(new_ui_rooms);
|
||||
timelines.lock().unwrap().extend(new_timelines);
|
||||
}
|
||||
});
|
||||
|
||||
// This will sync (with encryption) until an error happens or the program is
|
||||
// stopped.
|
||||
sync_service.start().await;
|
||||
|
||||
Ok(Self {
|
||||
sync_service,
|
||||
room_list_entries: StatefulList { state: Default::default(), items: rooms },
|
||||
client,
|
||||
listen_task,
|
||||
last_status_message: Default::default(),
|
||||
clear_status_message: None,
|
||||
ui_rooms,
|
||||
details_mode: Default::default(),
|
||||
timelines,
|
||||
current_room_subscription: None,
|
||||
current_pagination: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Set the current status message (displayed at the bottom), for a few
|
||||
/// seconds.
|
||||
fn set_status_message(&mut self, status: String) {
|
||||
if let Some(handle) = self.clear_status_message.take() {
|
||||
// Cancel the previous task to clear the status message.
|
||||
handle.abort();
|
||||
}
|
||||
|
||||
*self.last_status_message.lock().unwrap() = Some(status);
|
||||
|
||||
let message = self.last_status_message.clone();
|
||||
self.clear_status_message = Some(spawn(async move {
|
||||
// Clear the status message in 4 seconds.
|
||||
tokio::time::sleep(Duration::from_secs(4)).await;
|
||||
|
||||
*message.lock().unwrap() = None;
|
||||
}));
|
||||
}
|
||||
|
||||
/// Mark the currently selected room as read.
|
||||
async fn mark_as_read(&mut self) {
|
||||
let Some(room) = self
|
||||
.get_selected_room_id(None)
|
||||
.and_then(|room_id| self.ui_rooms.lock().unwrap().get(&room_id).cloned())
|
||||
else {
|
||||
self.set_status_message("missing room or nothing to show".to_owned());
|
||||
return;
|
||||
};
|
||||
|
||||
// Mark as read!
|
||||
match room.timeline().unwrap().mark_as_read(ReceiptType::Read).await {
|
||||
Ok(did) => {
|
||||
self.set_status_message(format!(
|
||||
"did {}send a read receipt!",
|
||||
if did { "" } else { "not " }
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
self.set_status_message(format!("error when marking a room as read: {err}",));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a small back-pagination (expect a batch of 20 events, continue until
|
||||
/// we get 10 timeline items or hit the timeline start).
|
||||
async fn back_paginate(&mut self) {
|
||||
let Some(sdk_timeline) = self.get_selected_room_id(None).and_then(|room_id| {
|
||||
self.timelines.lock().unwrap().get(&room_id).map(|timeline| timeline.timeline.clone())
|
||||
}) else {
|
||||
self.set_status_message("missing timeline for room".to_owned());
|
||||
return;
|
||||
};
|
||||
|
||||
let mut pagination = self.current_pagination.lock().unwrap();
|
||||
|
||||
// Cancel the previous back-pagination, if any.
|
||||
if let Some(prev) = pagination.take() {
|
||||
prev.abort();
|
||||
}
|
||||
|
||||
// Start a new one, request batches of 20 events, stop after 10 timeline items
|
||||
// have been added.
|
||||
*pagination = Some(spawn(async move {
|
||||
if let Err(err) =
|
||||
sdk_timeline.paginate_backwards(PaginationOptions::until_num_items(20, 10)).await
|
||||
{
|
||||
// TODO: would be nice to be able to set the status
|
||||
// message remotely?
|
||||
//self.set_status_message(format!(
|
||||
//"Error during backpagination: {err}"
|
||||
//));
|
||||
error!("Error during backpagination: {err}")
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Returns the currently selected room id, if any.
|
||||
fn get_selected_room_id(&self, selected: Option<usize>) -> Option<OwnedRoomId> {
|
||||
let selected = selected.or_else(|| self.room_list_entries.state.selected())?;
|
||||
|
||||
self.room_list_entries
|
||||
.items
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(selected)
|
||||
.cloned()
|
||||
.and_then(|entry| entry.as_room_id().map(ToOwned::to_owned))
|
||||
}
|
||||
|
||||
fn subscribe_to_selected_room(&mut self, selected: usize) {
|
||||
// Delete the subscription to the previous room, if any.
|
||||
if let Some(room) = self.current_room_subscription.take() {
|
||||
room.unsubscribe();
|
||||
}
|
||||
|
||||
// Subscribe to the new room.
|
||||
if let Some(room) = self
|
||||
.get_selected_room_id(Some(selected))
|
||||
.and_then(|room_id| self.ui_rooms.lock().unwrap().get(&room_id).cloned())
|
||||
{
|
||||
room.subscribe(None);
|
||||
self.current_room_subscription = Some(room);
|
||||
}
|
||||
}
|
||||
|
||||
async fn render_loop(&mut self, mut terminal: Terminal<impl Backend>) -> anyhow::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| f.render_widget(&mut *self, f.size()))?;
|
||||
|
||||
if crossterm::event::poll(Duration::from_millis(16))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
use KeyCode::*;
|
||||
match key.code {
|
||||
Char('q') | Esc => return Ok(()),
|
||||
|
||||
Char('j') | Down => {
|
||||
if let Some(i) = self.room_list_entries.next() {
|
||||
self.subscribe_to_selected_room(i);
|
||||
}
|
||||
}
|
||||
|
||||
Char('k') | Up => {
|
||||
if let Some(i) = self.room_list_entries.previous() {
|
||||
self.subscribe_to_selected_room(i);
|
||||
}
|
||||
}
|
||||
|
||||
Char('s') => self.sync_service.start().await,
|
||||
Char('S') => self.sync_service.stop().await?,
|
||||
Char('r') => self.details_mode = DetailsMode::ReadReceipts,
|
||||
Char('t') => self.details_mode = DetailsMode::TimelineItems,
|
||||
|
||||
Char('b') if self.details_mode == DetailsMode::TimelineItems => {
|
||||
self.back_paginate().await;
|
||||
}
|
||||
|
||||
Char('m') if self.details_mode == DetailsMode::ReadReceipts => {
|
||||
self.mark_as_read().await
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self, terminal: Terminal<impl Backend>) -> anyhow::Result<()> {
|
||||
self.render_loop(terminal).await?;
|
||||
|
||||
// At this point the user has exited the loop, so shut down the application.
|
||||
restore_terminal()?;
|
||||
|
||||
println!("Closing sync service...");
|
||||
|
||||
let s = self.sync_service.clone();
|
||||
let wait_for_termination = spawn(async move {
|
||||
while let Some(state) = s.state().next().await {
|
||||
if !matches!(state, sync_service::State::Running) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.sync_service.stop().await?;
|
||||
self.listen_task.abort();
|
||||
for timeline in self.timelines.lock().unwrap().values() {
|
||||
timeline.task.abort();
|
||||
}
|
||||
wait_for_termination.await.unwrap();
|
||||
|
||||
println!("okthxbye!");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
/// Render the whole app.
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// Create a space for header, todo list and the footer.
|
||||
let vertical =
|
||||
Layout::vertical([Constraint::Length(2), Constraint::Min(0), Constraint::Length(2)]);
|
||||
let [header_area, rest_area, footer_area] = vertical.areas(area);
|
||||
|
||||
// Create two chunks with equal horizontal screen space. One for the list and
|
||||
// the other for the info block.
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [lhs, rhs] = horizontal.areas(rest_area);
|
||||
|
||||
self.render_title(header_area, buf);
|
||||
self.render_left(lhs, buf);
|
||||
self.render_right(rhs, buf);
|
||||
self.render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// Render the top square (title of the program).
|
||||
fn render_title(&self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Multiverse").bold().centered().render(area, buf);
|
||||
}
|
||||
|
||||
/// Renders the left part of the screen, that is, the list of rooms.
|
||||
fn render_left(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// We create two blocks, one is for the header (outer) and the other is for list
|
||||
// (inner).
|
||||
let outer_block = Block::default()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(HEADER_BG)
|
||||
.title("Room list")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_block =
|
||||
Block::default().borders(Borders::NONE).fg(TEXT_COLOR).bg(NORMAL_ROW_COLOR);
|
||||
|
||||
// We get the inner area from outer_block. We'll use this area later to render
|
||||
// the table.
|
||||
let outer_area = area;
|
||||
let inner_area = outer_block.inner(outer_area);
|
||||
|
||||
// We can render the header in outer_area.
|
||||
outer_block.render(outer_area, buf);
|
||||
|
||||
// Iterate through all elements in the `items` and stylize them.
|
||||
let items: Vec<ListItem<'_>> = self
|
||||
.room_list_entries
|
||||
.items
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, item)| {
|
||||
let bg_color = match i % 2 {
|
||||
0 => NORMAL_ROW_COLOR,
|
||||
_ => ALT_ROW_COLOR,
|
||||
};
|
||||
|
||||
let line = if let Some(room) =
|
||||
item.as_room_id().and_then(|room_id| self.client.get_room(room_id))
|
||||
{
|
||||
format!("#{i} {}", room.room_id())
|
||||
} else {
|
||||
"non-filled room".to_owned()
|
||||
};
|
||||
|
||||
let line = Line::styled(line, TEXT_COLOR);
|
||||
ListItem::new(line).bg(bg_color)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one.
|
||||
let items = List::new(items)
|
||||
.block(inner_block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(SELECTED_STYLE_FG),
|
||||
)
|
||||
.highlight_symbol(">")
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
|
||||
StatefulWidget::render(items, inner_area, buf, &mut self.room_list_entries.state);
|
||||
}
|
||||
|
||||
/// Render the right part of the screen, showing the details of the current
|
||||
/// view.
|
||||
fn render_right(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
// Split the block into two parts:
|
||||
// - outer_block with the title of the block.
|
||||
// - inner_block that will contain the actual details.
|
||||
let outer_block = Block::default()
|
||||
.borders(Borders::NONE)
|
||||
.fg(TEXT_COLOR)
|
||||
.bg(HEADER_BG)
|
||||
.title("Room view")
|
||||
.title_alignment(Alignment::Center);
|
||||
let inner_block = Block::default()
|
||||
.borders(Borders::NONE)
|
||||
.bg(NORMAL_ROW_COLOR)
|
||||
.padding(Padding::horizontal(1));
|
||||
|
||||
// This is a similar process to what we did for list. outer_info_area will be
|
||||
// used for header inner_info_area will be used for the list info.
|
||||
let outer_area = area;
|
||||
let inner_area = outer_block.inner(outer_area);
|
||||
|
||||
// We can render the header. Inner area will be rendered later.
|
||||
outer_block.render(outer_area, buf);
|
||||
|
||||
// Helper to render some string as a paragraph.
|
||||
let render_paragraph = |buf: &mut Buffer, content: String| {
|
||||
Paragraph::new(content)
|
||||
.block(inner_block.clone())
|
||||
.fg(TEXT_COLOR)
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(inner_area, buf);
|
||||
};
|
||||
|
||||
if let Some(room_id) = self.get_selected_room_id(None) {
|
||||
match self.details_mode {
|
||||
DetailsMode::ReadReceipts => {
|
||||
// In read receipts mode, show the read receipts object as computed
|
||||
// by the client.
|
||||
match self.ui_rooms.lock().unwrap().get(&room_id).cloned() {
|
||||
Some(room) => {
|
||||
let receipts = room.read_receipts();
|
||||
render_paragraph(
|
||||
buf,
|
||||
format!(
|
||||
r#"Read receipts:
|
||||
- unread: {}
|
||||
- notifications: {}
|
||||
- mentions: {}
|
||||
|
||||
---
|
||||
|
||||
{:?}
|
||||
"#,
|
||||
receipts.num_unread,
|
||||
receipts.num_notifications,
|
||||
receipts.num_mentions,
|
||||
receipts
|
||||
),
|
||||
)
|
||||
}
|
||||
None => render_paragraph(
|
||||
buf,
|
||||
"(room disappeared in the room list service)".to_owned(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
DetailsMode::TimelineItems => {
|
||||
if !self.render_timeline(&room_id, inner_block.clone(), inner_area, buf) {
|
||||
render_paragraph(buf, "(room's timeline disappeared)".to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
render_paragraph(buf, "Nothing to see here...".to_owned())
|
||||
};
|
||||
}
|
||||
|
||||
/// Renders the list of timeline items for the given room.
|
||||
fn render_timeline(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
inner_block: Block<'_>,
|
||||
inner_area: Rect,
|
||||
buf: &mut Buffer,
|
||||
) -> bool {
|
||||
let Some(items) =
|
||||
self.timelines.lock().unwrap().get(room_id).map(|timeline| timeline.items.clone())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let items = items.lock().unwrap();
|
||||
let mut content = Vec::new();
|
||||
|
||||
for item in items.iter() {
|
||||
match item.kind() {
|
||||
TimelineItemKind::Event(ev) => {
|
||||
let sender = ev.sender();
|
||||
|
||||
match ev.content() {
|
||||
TimelineItemContent::Message(message) => {
|
||||
if let MessageType::Text(text) = message.msgtype() {
|
||||
content.push(format!("{}: {}", sender, text.body))
|
||||
}
|
||||
}
|
||||
|
||||
TimelineItemContent::RedactedMessage => {
|
||||
content.push(format!("{}: -- redacted --", sender))
|
||||
}
|
||||
TimelineItemContent::UnableToDecrypt(_) => {
|
||||
content.push(format!("{}: (UTD)", sender))
|
||||
}
|
||||
TimelineItemContent::Sticker(_)
|
||||
| TimelineItemContent::MembershipChange(_)
|
||||
| TimelineItemContent::ProfileChange(_)
|
||||
| TimelineItemContent::OtherState(_)
|
||||
| TimelineItemContent::FailedToParseMessageLike { .. }
|
||||
| TimelineItemContent::FailedToParseState { .. }
|
||||
| TimelineItemContent::Poll(_)
|
||||
| TimelineItemContent::CallInvite => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TimelineItemKind::Virtual(virt) => match virt {
|
||||
VirtualTimelineItem::DayDivider(unix_ts) => {
|
||||
content.push(format!("Date: {unix_ts:?}"));
|
||||
}
|
||||
VirtualTimelineItem::ReadMarker => {
|
||||
content.push("Read marker".to_owned());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let list_items = content
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
let bg_color = match i % 2 {
|
||||
0 => NORMAL_ROW_COLOR,
|
||||
_ => ALT_ROW_COLOR,
|
||||
};
|
||||
let line = Line::styled(line, TEXT_COLOR);
|
||||
ListItem::new(line).bg(bg_color)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let list = List::new(list_items)
|
||||
.block(inner_block)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::REVERSED)
|
||||
.fg(SELECTED_STYLE_FG),
|
||||
)
|
||||
.highlight_symbol(">")
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
|
||||
let mut dummy_list_state = ListState::default();
|
||||
StatefulWidget::render(list, inner_area, buf, &mut dummy_list_state);
|
||||
true
|
||||
}
|
||||
|
||||
/// Render the bottom part of the screen, with a status message if one is
|
||||
/// set, or a default help message otherwise.
|
||||
fn render_footer(&self, area: Rect, buf: &mut Buffer) {
|
||||
let content = if let Some(status_message) = self.last_status_message.lock().unwrap().clone()
|
||||
{
|
||||
status_message
|
||||
} else {
|
||||
match self.details_mode {
|
||||
DetailsMode::ReadReceipts => {
|
||||
"\nUse ↓↑ to move, s/S to start/stop the sync service, m to mark as read, t to show the timeline.".to_owned()
|
||||
}
|
||||
DetailsMode::TimelineItems => {
|
||||
"\nUse ↓↑ to move, s/S to start/stop the sync service, r to show read receipts.".to_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
Paragraph::new(content).centered().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
/// Focus the list on the next item, wraps around if needs be.
|
||||
///
|
||||
/// Returns the index only if there was a meaningful change.
|
||||
fn next(&mut self) -> Option<usize> {
|
||||
let num_items = self.items.lock().unwrap().len();
|
||||
|
||||
// If there's no item to select, leave early.
|
||||
if num_items == 0 {
|
||||
self.state.select(None);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Otherwise, select the next one or wrap around.
|
||||
let prev = self.state.selected();
|
||||
let new = prev.map_or(0, |i| if i >= num_items - 1 { 0 } else { i + 1 });
|
||||
|
||||
if prev != Some(new) {
|
||||
self.state.select(Some(new));
|
||||
Some(new)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Focus the list on the previous item, wraps around if needs be.
|
||||
///
|
||||
/// Returns the index only if there was a meaningful change.
|
||||
fn previous(&mut self) -> Option<usize> {
|
||||
let num_items = self.items.lock().unwrap().len();
|
||||
|
||||
// If there's no item to select, leave early.
|
||||
if num_items == 0 {
|
||||
self.state.select(None);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Otherwise, select the previous one or wrap around.
|
||||
let prev = self.state.selected();
|
||||
let new = prev.map_or(0, |i| if i == 0 { num_items - 1 } else { i - 1 });
|
||||
|
||||
if prev != Some(new) {
|
||||
self.state.select(Some(new));
|
||||
Some(new)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the client so it's ready for sync'ing.
|
||||
///
|
||||
/// Will log in or reuse a previous session.
|
||||
async fn configure_client(server_name: String, config_path: String) -> anyhow::Result<Client> {
|
||||
let server_name = ServerName::parse(&server_name)?;
|
||||
|
||||
let config_path = PathBuf::from(config_path);
|
||||
let client = Client::builder()
|
||||
.store_config(
|
||||
StoreConfig::default()
|
||||
.crypto_store(
|
||||
SqliteCryptoStore::open(config_path.join("crypto.sqlite"), None).await?,
|
||||
)
|
||||
.state_store(SqliteStateStore::open(config_path.join("state.sqlite"), None).await?),
|
||||
)
|
||||
.server_name(&server_name)
|
||||
.with_encryption_settings(EncryptionSettings {
|
||||
auto_enable_cross_signing: true,
|
||||
backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
|
||||
auto_enable_backups: true,
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Try reading a session, otherwise create a new one.
|
||||
let session_path = config_path.join("session.json");
|
||||
if let Ok(serialized) = std::fs::read_to_string(&session_path) {
|
||||
let session: MatrixSession = serde_json::from_str(&serialized)?;
|
||||
client.restore_session(session).await?;
|
||||
println!("restored session");
|
||||
} else {
|
||||
login_with_password(&client).await?;
|
||||
println!("new login");
|
||||
|
||||
// Immediately save the session to disk.
|
||||
if let Some(session) = client.session() {
|
||||
let AuthSession::Matrix(session) = session else { panic!("unexpected oidc session") };
|
||||
let serialized = serde_json::to_string(&session)?;
|
||||
std::fs::write(session_path, serialized)?;
|
||||
println!("saved session");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Asks the user of a username and password, and try to login using the matrix
|
||||
/// auth with those.
|
||||
async fn login_with_password(client: &Client) -> anyhow::Result<()> {
|
||||
println!("Logging in with username and password…");
|
||||
|
||||
loop {
|
||||
print!("\nUsername: ");
|
||||
stdout().flush().expect("Unable to write to stdout");
|
||||
let mut username = String::new();
|
||||
io::stdin().read_line(&mut username).expect("Unable to read user input");
|
||||
username = username.trim().to_owned();
|
||||
|
||||
let password = rpassword::prompt_password("Password.")?;
|
||||
|
||||
match client.matrix_auth().login_username(&username, password.trim()).await {
|
||||
Ok(_) => {
|
||||
println!("Logged in as {username}");
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error logging in: {error}");
|
||||
println!("Please try again\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
use std::{
|
||||
env,
|
||||
io::{self, Write},
|
||||
process::exit,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use futures_util::{pin_mut, StreamExt as _};
|
||||
use matrix_sdk::{
|
||||
config::StoreConfig, matrix_auth::MatrixSession,
|
||||
ruma::api::client::receipt::create_receipt::v3::ReceiptType, AuthSession, Client, ServerName,
|
||||
SqliteCryptoStore, SqliteStateStore,
|
||||
};
|
||||
use matrix_sdk_ui::sync_service::{self, SyncService};
|
||||
use tokio::spawn;
|
||||
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.with_writer(tracing_appender::rolling::hourly("/tmp/", "logs-"));
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::new(std::env::var("RUST_LOG").unwrap_or("".into())))
|
||||
.with(file_layer)
|
||||
.init();
|
||||
|
||||
let Some(server_name) = env::args().nth(1) else {
|
||||
eprintln!("Usage: {} <server_name>", env::args().next().unwrap());
|
||||
exit(1)
|
||||
};
|
||||
|
||||
login_and_sync(server_name).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Log in to the given homeserver and sync.
|
||||
async fn login_and_sync(server_name: String) -> anyhow::Result<()> {
|
||||
let server_name = ServerName::parse(&server_name)?;
|
||||
|
||||
let client = Client::builder()
|
||||
.store_config(
|
||||
StoreConfig::default()
|
||||
.crypto_store(SqliteCryptoStore::open("/tmp/crypto.sqlite", None).await?)
|
||||
.state_store(SqliteStateStore::open("/tmp/state.sqlite", None).await?),
|
||||
)
|
||||
.server_name(&server_name)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Try reading from /tmp/session.json
|
||||
if let Ok(serialized) = std::fs::read_to_string("/tmp/session.json") {
|
||||
let session: MatrixSession = serde_json::from_str(&serialized)?;
|
||||
client.restore_session(session).await?;
|
||||
println!("restored session");
|
||||
} else {
|
||||
login_with_password(&client).await?;
|
||||
println!("new login");
|
||||
}
|
||||
|
||||
let sync_service = SyncService::builder(client.clone()).build().await?;
|
||||
|
||||
let room_list_service = sync_service.room_list_service();
|
||||
|
||||
let all_rooms = room_list_service.all_rooms().await?;
|
||||
let (rooms, stream) = all_rooms.entries();
|
||||
|
||||
let rooms = Arc::new(Mutex::new(rooms.clone()));
|
||||
|
||||
// This will sync (with encryption) until an error happens or the program is
|
||||
// killed.
|
||||
sync_service.start().await;
|
||||
|
||||
let c = client.clone();
|
||||
let r = rooms.clone();
|
||||
let handle = spawn(async move {
|
||||
pin_mut!(stream);
|
||||
let rooms = r;
|
||||
let client = c;
|
||||
|
||||
while let Some(diffs) = stream.next().await {
|
||||
let mut rooms = rooms.lock().unwrap();
|
||||
for diff in diffs {
|
||||
diff.apply(&mut rooms);
|
||||
}
|
||||
println!("New update!");
|
||||
for (id, room) in rooms.iter().enumerate() {
|
||||
if let Some(room) = room.as_room_id().and_then(|room_id| client.get_room(room_id)) {
|
||||
println!("> #{id} {}: {:?}", room.room_id(), room.read_receipts());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
let mut command = String::new();
|
||||
|
||||
print!("$ ");
|
||||
let _ = io::stdout().flush();
|
||||
io::stdin().read_line(&mut command).expect("Unable to read user input");
|
||||
|
||||
match command.trim() {
|
||||
"rooms" => {
|
||||
let rooms = rooms.lock().unwrap();
|
||||
for (id, room) in rooms.iter().enumerate() {
|
||||
if let Some(room) =
|
||||
room.as_room_id().and_then(|room_id| client.get_room(room_id))
|
||||
{
|
||||
println!("> #{id} {}: {:?}", room.room_id(), room.read_receipts());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"start" => {
|
||||
sync_service.start().await;
|
||||
println!("> sync service started!");
|
||||
}
|
||||
|
||||
"stop" => {
|
||||
sync_service.stop().await?;
|
||||
println!("> sync service stopped!");
|
||||
}
|
||||
|
||||
"" | "exit" => {
|
||||
break;
|
||||
}
|
||||
|
||||
_ => {
|
||||
if let Some((_, id)) = command.split_once("send ") {
|
||||
let id = id.trim().parse::<usize>()?;
|
||||
let room_id = { rooms.lock().unwrap()[id].as_room_id().map(ToOwned::to_owned) };
|
||||
if let Some(room_id) = &room_id {
|
||||
let room = room_list_service.room(room_id).await?;
|
||||
|
||||
if !room.is_timeline_initialized() {
|
||||
room.init_timeline_with_builder(
|
||||
room.default_room_timeline_builder().await?,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let timeline = room.timeline().unwrap();
|
||||
|
||||
let did = timeline.mark_as_read(ReceiptType::Read).await?;
|
||||
println!("> did {}send a read receipt!", if did { "" } else { "not " });
|
||||
}
|
||||
} else {
|
||||
println!("unknown command");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Closing sync service...");
|
||||
|
||||
let sync_service = Arc::new(sync_service);
|
||||
let s = sync_service.clone();
|
||||
let wait_for_termination = spawn(async move {
|
||||
while let Some(state) = s.state().next().await {
|
||||
if !matches!(state, sync_service::State::Running) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sync_service.stop().await?;
|
||||
handle.abort();
|
||||
wait_for_termination.await.unwrap();
|
||||
|
||||
if let Some(session) = client.session() {
|
||||
let AuthSession::Matrix(session) = session else { panic!("unexpected oidc session") };
|
||||
let serialized = serde_json::to_string(&session)?;
|
||||
std::fs::write("/tmp/session.json", serialized)?;
|
||||
println!("saved session");
|
||||
}
|
||||
|
||||
println!("okthxbye!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn login_with_password(client: &Client) -> anyhow::Result<()> {
|
||||
println!("Logging in with username and password…");
|
||||
|
||||
loop {
|
||||
print!("\nUsername: ");
|
||||
io::stdout().flush().expect("Unable to write to stdout");
|
||||
let mut username = String::new();
|
||||
io::stdin().read_line(&mut username).expect("Unable to read user input");
|
||||
username = username.trim().to_owned();
|
||||
|
||||
print!("Password: ");
|
||||
io::stdout().flush().expect("Unable to write to stdout");
|
||||
let mut password = String::new();
|
||||
io::stdin().read_line(&mut password).expect("Unable to read user input");
|
||||
password = password.trim().to_owned();
|
||||
|
||||
match client.matrix_auth().login_username(&username, &password).await {
|
||||
Ok(_) => {
|
||||
println!("Logged in as {username}");
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error logging in: {error}");
|
||||
println!("Please try again\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -347,7 +347,8 @@ pub static VERSIONS: Lazy<JsonValue> = Lazy::new(|| {
|
||||
],
|
||||
"unstable_features": {
|
||||
"org.matrix.label_based_filtering":true,
|
||||
"org.matrix.e2e_cross_signing":true
|
||||
"org.matrix.e2e_cross_signing":true,
|
||||
"org.matrix.msc4028":true
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1714,3 +1714,144 @@ pub static SYNC_ADMIN_AND_MOD: Lazy<JsonValue> = Lazy::new(|| {
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
pub static CUSTOM_ROOM_POWER_LEVELS: Lazy<JsonValue> = Lazy::new(|| {
|
||||
json!({
|
||||
"device_one_time_keys_count": {},
|
||||
"next_batch": "s526_47314_0_7_1_1_1_11444_1",
|
||||
"device_lists": {
|
||||
"changed": [
|
||||
"@admin:example.org"
|
||||
],
|
||||
"left": []
|
||||
},
|
||||
"rooms": {
|
||||
"invite": {},
|
||||
"join": {
|
||||
*DEFAULT_TEST_ROOM_ID: {
|
||||
"summary": {
|
||||
"m.heroes": [
|
||||
"@example2:localhost"
|
||||
],
|
||||
"m.joined_member_count": 1,
|
||||
"m.invited_member_count": 0
|
||||
},
|
||||
"account_data": {
|
||||
"events": []
|
||||
},
|
||||
"ephemeral": {
|
||||
"events": []
|
||||
},
|
||||
"state": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"join_rule": "public"
|
||||
},
|
||||
"event_id": "$15139375514WsgmR:localhost",
|
||||
"origin_server_ts": 151393755000000_u64,
|
||||
"sender": "@admin:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": {
|
||||
"age": 7034220
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "admin",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140517rfvjc:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140000000_u64,
|
||||
"sender": "@admin:localhost",
|
||||
"state_key": "@admin:localhost",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 297036,
|
||||
"replaces_state": "$151800111315tsynI:localhost"
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"creator": "@example:localhost"
|
||||
},
|
||||
"event_id": "$15139375510KUZHi:localhost",
|
||||
"origin_server_ts": 151393755000000_u64,
|
||||
"sender": "@admin:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"ban": 100,
|
||||
"events": {
|
||||
"m.room.avatar": 100,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100
|
||||
},
|
||||
"events_default": 0,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@admin:localhost": 100
|
||||
},
|
||||
"users_default": 0
|
||||
},
|
||||
"event_id": "$15139375512JaHAW:localhost",
|
||||
"origin_server_ts": 151393755000000_u64,
|
||||
"sender": "@admin:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"timeline": {
|
||||
"events": [
|
||||
{
|
||||
"content": {
|
||||
"body": "baba",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<strong>baba</strong>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$152037280074GZeOm:localhost",
|
||||
"origin_server_ts": 152037280000000_u64,
|
||||
"sender": "@admin:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 598971425
|
||||
}
|
||||
}
|
||||
],
|
||||
"limited": true,
|
||||
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
|
||||
},
|
||||
"unread_notifications": {
|
||||
"highlight_count": 0,
|
||||
"notification_count": 11
|
||||
}
|
||||
}
|
||||
},
|
||||
"leave": {}
|
||||
},
|
||||
"to_device": {
|
||||
"events": []
|
||||
},
|
||||
"presence": {
|
||||
"events": []
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user