atagen

malice

public
Star0
Watch0

Commit 29f8ccacfdc7

tarball

atagen <boss@atagen.co>2026-10-23 06:26 UTC

42 files changed+50-13722
devendor macltok
Modified file: .cargo/config.toml+3 additions
@@ -1,2 +1,5 @@
11 [target.wasm32-unknown-unknown]
22 rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]
3+
4+[net]
5+git-fetch-with-cli = true
Modified file: Cargo.lock+11 additions−10 deletions
@@ -1871,7 +1871,6 @@ version = "0.35.0"
18711871 source = "registry+https://github.com/rust-lang/crates.io-index"
18721872 checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
18731873 dependencies = [
1874- "cc",
18751874 "pkg-config",
18761875 "vcpkg",
18771876 ]
@@ -1936,6 +1935,7 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
19361935 [[package]]
19371936 name = "macltok"
19381937 version = "0.1.0"
1938+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=7515819c2f3ed19dc742baec9701ccaa6bba83c5#7515819c2f3ed19dc742baec9701ccaa6bba83c5"
19391939 dependencies = [
19401940 "futures-util",
19411941 "getrandom 0.2.17",
@@ -1943,29 +1943,21 @@ dependencies = [
19431943 "getrandom 0.4.3",
19441944 "http",
19451945 "imbl",
1946- "js-sys",
19471946 "matrix-sdk",
19481947 "matrix-sdk-base",
19491948 "matrix-sdk-crypto",
19501949 "matrix-sdk-store-encryption",
1951- "matrix-sdk-test",
19521950 "matrix-sdk-ui",
1953- "matrix_indexed_db_futures",
19541951 "mime",
19551952 "reqwest",
19561953 "ruma",
19571954 "serde",
1958- "serde_json",
19591955 "sha2",
19601956 "thiserror 2.0.21",
19611957 "tokio",
19621958 "tracing",
19631959 "url",
19641960 "uuid",
1965- "wasm-bindgen",
1966- "wasm-bindgen-futures",
1967- "web-sys",
1968- "wiremock",
19691961 ]
19701962
19711963 [[package]]
@@ -2172,6 +2164,7 @@ dependencies = [
21722164 [[package]]
21732165 name = "matrix-sdk"
21742166 version = "0.18.0"
2167+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
21752168 dependencies = [
21762169 "anymap2",
21772170 "aquamarine",
@@ -2234,6 +2227,7 @@ dependencies = [
22342227 [[package]]
22352228 name = "matrix-sdk-base"
22362229 version = "0.18.0"
2230+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
22372231 dependencies = [
22382232 "as_variant",
22392233 "assert_matches",
@@ -2263,6 +2257,7 @@ dependencies = [
22632257 [[package]]
22642258 name = "matrix-sdk-common"
22652259 version = "0.18.0"
2260+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
22662261 dependencies = [
22672262 "eyeball-im",
22682263 "futures-core",
@@ -2285,6 +2280,7 @@ dependencies = [
22852280 [[package]]
22862281 name = "matrix-sdk-crypto"
22872282 version = "0.18.0"
2283+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
22882284 dependencies = [
22892285 "aes",
22902286 "aquamarine",
@@ -2325,13 +2321,13 @@ dependencies = [
23252321 [[package]]
23262322 name = "matrix-sdk-indexeddb"
23272323 version = "0.18.0"
2324+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
23282325 dependencies = [
23292326 "async-trait",
23302327 "base64 0.22.1",
23312328 "futures-util",
23322329 "getrandom 0.4.3",
23332330 "gloo-utils",
2334- "growable-bloom-filter",
23352331 "hkdf",
23362332 "js-sys",
23372333 "matrix-sdk-base",
@@ -2356,6 +2352,7 @@ dependencies = [
23562352 [[package]]
23572353 name = "matrix-sdk-sqlite"
23582354 version = "0.18.0"
2355+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
23592356 dependencies = [
23602357 "as_variant",
23612358 "async-trait",
@@ -2382,6 +2379,7 @@ dependencies = [
23822379 [[package]]
23832380 name = "matrix-sdk-store-encryption"
23842381 version = "0.18.0"
2382+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
23852383 dependencies = [
23862384 "base64 0.22.1",
23872385 "blake3",
@@ -2402,6 +2400,7 @@ dependencies = [
24022400 [[package]]
24032401 name = "matrix-sdk-test"
24042402 version = "0.18.0"
2403+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
24052404 dependencies = [
24062405 "as_variant",
24072406 "ctor",
@@ -2424,6 +2423,7 @@ dependencies = [
24242423 [[package]]
24252424 name = "matrix-sdk-test-macros"
24262425 version = "0.18.0"
2426+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
24272427 dependencies = [
24282428 "quote",
24292429 "syn 2.0.119",
@@ -2432,6 +2432,7 @@ dependencies = [
24322432 [[package]]
24332433 name = "matrix-sdk-ui"
24342434 version = "0.18.0"
2435+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
24352436 dependencies = [
24362437 "as_variant",
24372438 "async-rx",
Modified file: Cargo.toml+1 additions−13 deletions
@@ -3,7 +3,7 @@ resolver = "3"
33 members = ["crates/malice-core", "crates/malice-server", "crates/malice-static"]
44
55 [workspace.dependencies]
6-macltok = { path = "vendor/macltok" }
6+macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "7515819c2f3ed19dc742baec9701ccaa6bba83c5" }
77 ruma = { version = "0.16", default-features = false }
88 serde = { version = "1", features = ["derive"] }
99 serde_json = "1"
@@ -13,15 +13,3 @@ http = "1"
1313
1414 [workspace.lints.rust]
1515 unsafe_code = "forbid"
16-
17-[patch.crates-io]
18-matrix-sdk = { path = "../matrix-rust-sdk/crates/matrix-sdk" }
19-matrix-sdk-base = { path = "../matrix-rust-sdk/crates/matrix-sdk-base" }
20-matrix-sdk-common = { path = "../matrix-rust-sdk/crates/matrix-sdk-common" }
21-matrix-sdk-crypto = { path = "../matrix-rust-sdk/crates/matrix-sdk-crypto" }
22-matrix-sdk-indexeddb = { path = "../matrix-rust-sdk/crates/matrix-sdk-indexeddb" }
23-matrix-sdk-sqlite = { path = "../matrix-rust-sdk/crates/matrix-sdk-sqlite" }
24-matrix-sdk-store-encryption = { path = "../matrix-rust-sdk/crates/matrix-sdk-store-encryption" }
25-matrix-sdk-test = { path = "../matrix-rust-sdk/testing/matrix-sdk-test" }
26-matrix-sdk-test-macros = { path = "../matrix-rust-sdk/testing/matrix-sdk-test-macros" }
27-matrix-sdk-ui = { path = "../matrix-rust-sdk/crates/matrix-sdk-ui" }
Modified file: crates/malice-server/Cargo.toml+1 additions−1 deletions
@@ -31,7 +31,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
3131 [dev-dependencies]
3232 http-body-util = "0.1"
3333 tower = { version = "0.5", features = ["util"] }
34-matrix-sdk = { version = "0.18", default-features = false, features = ["testing"] }
34+matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "cabc67b5cc0529bcf548c004f2e7c889aed6c4e5", default-features = false, features = ["testing"] }
3535 reqwest = { version = "0.13", default-features = false, features = ["json", "query", "rustls"] }
3636 tempfile = "3"
3737 wiremock = "0.6"
Modified file: crates/malice-worker/.cargo/config.toml+3 additions
@@ -1,3 +1,6 @@
11 [target.wasm32-unknown-unknown]
22 runner = "wasm-bindgen-test-runner"
33 rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""]
4+
5+[net]
6+git-fetch-with-cli = true
Modified file: crates/malice-worker/Cargo.lock+9 additions−8 deletions
@@ -1722,6 +1722,7 @@ checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f"
17221722 [[package]]
17231723 name = "macltok"
17241724 version = "0.1.0"
1725+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/macltok?rev=7515819c2f3ed19dc742baec9701ccaa6bba83c5#7515819c2f3ed19dc742baec9701ccaa6bba83c5"
17251726 dependencies = [
17261727 "futures-util",
17271728 "getrandom 0.2.17",
@@ -1926,6 +1927,7 @@ dependencies = [
19261927 [[package]]
19271928 name = "matrix-sdk"
19281929 version = "0.18.0"
1930+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
19291931 dependencies = [
19301932 "anymap2",
19311933 "aquamarine",
@@ -1985,6 +1987,7 @@ dependencies = [
19851987 [[package]]
19861988 name = "matrix-sdk-base"
19871989 version = "0.18.0"
1990+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
19881991 dependencies = [
19891992 "as_variant",
19901993 "async-trait",
@@ -2010,6 +2013,7 @@ dependencies = [
20102013 [[package]]
20112014 name = "matrix-sdk-common"
20122015 version = "0.18.0"
2016+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
20132017 dependencies = [
20142018 "eyeball-im",
20152019 "futures-core",
@@ -2032,6 +2036,7 @@ dependencies = [
20322036 [[package]]
20332037 name = "matrix-sdk-crypto"
20342038 version = "0.18.0"
2039+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
20352040 dependencies = [
20362041 "aes",
20372042 "aquamarine",
@@ -2071,6 +2076,7 @@ dependencies = [
20712076 [[package]]
20722077 name = "matrix-sdk-indexeddb"
20732078 version = "0.18.0"
2079+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
20742080 dependencies = [
20752081 "async-trait",
20762082 "base64 0.22.1",
@@ -2102,6 +2108,7 @@ dependencies = [
21022108 [[package]]
21032109 name = "matrix-sdk-sqlite"
21042110 version = "0.18.0"
2111+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
21052112 dependencies = [
21062113 "as_variant",
21072114 "async-trait",
@@ -2128,6 +2135,7 @@ dependencies = [
21282135 [[package]]
21292136 name = "matrix-sdk-store-encryption"
21302137 version = "0.18.0"
2138+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
21312139 dependencies = [
21322140 "base64 0.22.1",
21332141 "blake3",
@@ -2148,6 +2156,7 @@ dependencies = [
21482156 [[package]]
21492157 name = "matrix-sdk-ui"
21502158 version = "0.18.0"
2159+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=cabc67b5cc0529bcf548c004f2e7c889aed6c4e5#cabc67b5cc0529bcf548c004f2e7c889aed6c4e5"
21512160 dependencies = [
21522161 "as_variant",
21532162 "async-rx",
@@ -4513,11 +4522,3 @@ name = "zmij"
45134522 version = "1.0.23"
45144523 source = "registry+https://github.com/rust-lang/crates.io-index"
45154524 checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
4516-
4517-[[patch.unused]]
4518-name = "matrix-sdk-test"
4519-version = "0.18.0"
4520-
4521-[[patch.unused]]
4522-name = "matrix-sdk-test-macros"
4523-version = "0.18.0"
Modified file: crates/malice-worker/Cargo.toml+1 additions−13 deletions
@@ -14,7 +14,7 @@ publish = false
1414 crate-type = ["cdylib", "rlib"]
1515
1616 [target.'cfg(target_family = "wasm")'.dependencies]
17-macltok = { path = "../../vendor/macltok", features = ["indexeddb", "serde", "mirror"] }
17+macltok = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/macltok", rev = "7515819c2f3ed19dc742baec9701ccaa6bba83c5", features = ["indexeddb", "serde", "mirror"] }
1818 malice-core = { path = "../malice-core" }
1919 ruma = { version = "0.16", default-features = false }
2020 futures-util = { version = "0.3", default-features = false, features = ["std"] }
@@ -45,15 +45,3 @@ lto = true
4545 codegen-units = 1
4646 panic = "abort"
4747 strip = true
48-
49-[patch.crates-io]
50-matrix-sdk = { path = "../../../matrix-rust-sdk/crates/matrix-sdk" }
51-matrix-sdk-base = { path = "../../../matrix-rust-sdk/crates/matrix-sdk-base" }
52-matrix-sdk-common = { path = "../../../matrix-rust-sdk/crates/matrix-sdk-common" }
53-matrix-sdk-crypto = { path = "../../../matrix-rust-sdk/crates/matrix-sdk-crypto" }
54-matrix-sdk-indexeddb = { path = "../../../matrix-rust-sdk/crates/matrix-sdk-indexeddb" }
55-matrix-sdk-sqlite = { path = "../../../matrix-rust-sdk/crates/matrix-sdk-sqlite" }
56-matrix-sdk-store-encryption = { path = "../../../matrix-rust-sdk/crates/matrix-sdk-store-encryption" }
57-matrix-sdk-test = { path = "../../../matrix-rust-sdk/testing/matrix-sdk-test" }
58-matrix-sdk-test-macros = { path = "../../../matrix-rust-sdk/testing/matrix-sdk-test-macros" }
59-matrix-sdk-ui = { path = "../../../matrix-rust-sdk/crates/matrix-sdk-ui" }
Modified file: flake.nix+20 additions−4 deletions
@@ -87,6 +87,10 @@
8787 version = "0.1.0";
8888 src = cleanSrc ./web;
8989 npmDeps = webNodeModules pkgs;
90+ disallowedReferences = [
91+ (webNodeModules pkgs)
92+ pkgs.nodejs_22
93+ ];
9094 nativeBuildInputs = [
9195 pkgs.nodejs_22
9296 pkgs.importNpmLock.hooks.linkNodeModulesHook
@@ -123,7 +127,10 @@
123127 pname = "malice-server";
124128 version = "0.1.0";
125129 src = cleanSrc ./.;
126- cargoLock.lockFile = ./Cargo.lock;
130+ cargoLock = {
131+ lockFile = ./Cargo.lock;
132+ allowBuiltinFetchGit = true;
133+ };
127134 nativeBuildInputs = [ pkgs.pkg-config ];
128135 buildInputs = [ pkgs.sqlite ];
129136 cargoBuildFlags = [
@@ -144,7 +151,10 @@
144151 pname = "malice-static";
145152 version = "0.1.0";
146153 src = cleanSrc ./.;
147- cargoLock.lockFile = ./Cargo.lock;
154+ cargoLock = {
155+ lockFile = ./Cargo.lock;
156+ allowBuiltinFetchGit = true;
157+ };
148158 cargoBuildFlags = [
149159 "-p"
150160 "malice-static"
@@ -173,7 +183,10 @@
173183 version = "0.1.0";
174184 src = cleanSrc ./.;
175185 cargoRoot = "crates/malice-worker";
176- cargoLock.lockFile = ./crates/malice-worker/Cargo.lock;
186+ cargoLock = {
187+ lockFile = ./crates/malice-worker/Cargo.lock;
188+ allowBuiltinFetchGit = true;
189+ };
177190 nativeBuildInputs = [
178191 (wasmBindgenCli pkgs)
179192 pkgs.removeReferencesTo
@@ -290,7 +303,10 @@
290303 version = "0.1.0";
291304 src = cleanSrc ./.;
292305 cargoRoot = "crates/malice-worker";
293- cargoLock.lockFile = ./crates/malice-worker/Cargo.lock;
306+ cargoLock = {
307+ lockFile = ./crates/malice-worker/Cargo.lock;
308+ allowBuiltinFetchGit = true;
309+ };
294310 nativeBuildInputs = [
295311 (wasmBindgenCli pkgs)
296312 pkgs.firefox
Deleted file: vendor/macltok/Cargo.toml−79 deletions
@@ -1,79 +0,0 @@
1-[package]
2-name = "macltok"
3-version = "0.1.0"
4-edition = "2024"
5-description = "A compact application backend on top of the Matrix Rust SDK"
6-
7-[features]
8-default = []
9-mirror = []
10-serde = ["dep:serde", "imbl/serde"]
11-sqlite = ["matrix-sdk/sqlite", "serde", "dep:url", "dep:matrix-sdk-store-encryption"]
12-bundled-sqlite = ["sqlite", "matrix-sdk/bundled-sqlite"]
13-indexeddb = [
14- "matrix-sdk/indexeddb",
15- "serde",
16- "dep:url",
17- "dep:matrix-sdk-store-encryption",
18- "dep:indexed_db_futures",
19- "dep:js-sys",
20- "dep:wasm-bindgen",
21- "dep:wasm-bindgen-futures",
22- "dep:web-sys",
23-]
24-
25-[dependencies]
26-futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
27-http = "1"
28-imbl = { version = "6.1.0", default-features = false }
29-matrix-sdk = { version = "0.18.0", default-features = false, features = ["e2e-encryption"] }
30-matrix-sdk-base = { version = "0.18.0", default-features = false }
31-matrix-sdk-crypto = { version = "0.18.0", default-features = false }
32-matrix-sdk-store-encryption = { version = "0.18.0", optional = true }
33-matrix-sdk-ui = { version = "0.18.0", default-features = false }
34-ruma = { version = "0.16.0", default-features = false, features = ["unstable-msc2545", "unstable-msc4095"] }
35-serde = { version = "1.0.219", features = ["derive", "rc"], optional = true }
36-thiserror = "2.0.16"
37-mime = "0.3.17"
38-sha2 = "0.10.9"
39-tokio = { version = "1.47.1", features = ["macros", "sync"] }
40-tracing = "0.1.41"
41-url = { version = "2.5.7", optional = true }
42-
43-[target.'cfg(target_family = "wasm")'.dependencies]
44-getrandom = { version = "0.2", features = ["js"] }
45-getrandom3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
46-getrandom4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
47-indexed_db_futures = { package = "matrix_indexed_db_futures", version = "0.7.0", optional = true, features = ["typed-arrays"] }
48-js-sys = { version = "0.3.105", optional = true }
49-matrix-sdk = { version = "0.18.0", default-features = false, features = ["js"] }
50-matrix-sdk-ui = { version = "0.18.0", default-features = false, features = ["js"] }
51-# the sdk's own wasm reqwest leaves out stream, which the bounded media download needs
52-reqwest = { version = "0.13.1", default-features = false, features = ["stream"] }
53-uuid = { version = "1", features = ["js"] }
54-wasm-bindgen = { version = "0.2.128", optional = true }
55-wasm-bindgen-futures = { version = "0.4.78", optional = true }
56-web-sys = { version = "0.3.105", optional = true, features = [
57- "IdbFactory",
58- "IdbOpenDbRequest",
59- "IdbRequest",
60- "Window",
61- "WorkerGlobalScope",
62-] }
63-
64-[dev-dependencies]
65-matrix-sdk = { version = "0.18.0", default-features = false, features = ["testing"] }
66-matrix-sdk-test = "0.18.0"
67-serde_json = "1.0.143"
68-tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "signal", "time"] }
69-wiremock = "0.6.5"
70-
71-[[example]]
72-name = "headless"
73-required-features = ["sqlite"]
74-
75-[lints.rust]
76-unsafe_code = "forbid"
77-
78-[lints.clippy]
79-all = "warn"
Deleted file: vendor/macltok/examples/headless.rs−119 deletions
@@ -1,119 +0,0 @@
1-use std::sync::Arc;
2-
3-use macltok::{
4- EncryptedSessionStore, EngineConfig, Event, PasswordLogin, Restore, Retain, RoomMembership,
5- Session, SessionStore, StoreConfig, TimelineContent, TimelineDiff, TimelineId, TimelineItem,
6- imbl::Vector,
7-};
8-
9-#[tokio::main]
10-async fn main() -> Result<(), Box<dyn std::error::Error>> {
11- let store_path = std::path::PathBuf::from(required("MATRIX_STORE_PATH")?);
12- let passphrase = required("MATRIX_STORE_PASSPHRASE")?;
13- let sessions = Arc::new(EncryptedSessionStore::file(
14- store_path.with_extension("session"),
15- StoreConfig::sqlite(&store_path, &passphrase),
16- ));
17- let session = match Session::restore(sessions.clone()).await? {
18- Some(Restore::Resumed(session)) => session,
19- Some(Restore::SoftLoggedOut(soft)) => {
20- Session::login_password(
21- soft.password_login(required("MATRIX_PASSWORD")?),
22- sessions.clone(),
23- )
24- .await?
25- }
26- None | Some(Restore::OAuthPending) => {
27- Session::login_password(
28- PasswordLogin::new(
29- required("MATRIX_HOMESERVER")?,
30- required("MATRIX_USERNAME")?,
31- required("MATRIX_PASSWORD")?,
32- )
33- .with_device_display_name("macltok headless example"),
34- sessions.clone(),
35- )
36- .await?
37- }
38- };
39- let (engine, handle, mut events) = session.build(EngineConfig::default()).await?;
40- let running = tokio::spawn(engine.run());
41- let mut opened: Option<TimelineId> = None;
42- let mut items = Vector::new();
43- let mut stale = false;
44-
45- loop {
46- tokio::select! {
47- event = events.recv() => {
48- let Some(event) = event else { break };
49- match event {
50- Event::RoomsChanged if opened.is_none() => {
51- let Some(room) = handle
52- .rooms()
53- .await
54- .into_iter()
55- .find(|room| room.membership == RoomMembership::Joined)
56- else {
57- continue;
58- };
59- opened = Some(room.room_id.clone().into());
60- // answers arrive on their own, the event loop keeps draining meanwhile
61- let handle = handle.clone();
62- tokio::spawn(async move {
63- if let Err(error) = handle.open_room(room.room_id).await {
64- eprintln!("could not open {error}");
65- }
66- });
67- }
68- Event::TimelineReset { timeline_id, items: snapshot }
69- if opened.as_ref() == Some(&timeline_id) =>
70- {
71- items = snapshot;
72- stale = false;
73- print_messages(&items);
74- }
75- Event::TimelineChanged { timeline_id, diffs }
76- if !stale && opened.as_ref() == Some(&timeline_id) =>
77- {
78- if TimelineDiff::try_apply_all(diffs, &mut items).is_err() {
79- stale = true;
80- let handle = handle.clone();
81- tokio::spawn(async move { handle.resync(timeline_id).await });
82- continue;
83- }
84- print_messages(&items);
85- }
86- Event::Stopped => break,
87- _ => {}
88- }
89- }
90- result = tokio::signal::ctrl_c() => {
91- result?;
92- let handle = handle.clone();
93- tokio::spawn(async move { handle.shutdown().await });
94- }
95- }
96- }
97- let termination = running.await??;
98- drop((handle, events));
99- match termination.retain() {
100- Retain::RecordAndStore => {}
101- Retain::Store => sessions.soft_log_out().await?,
102- Retain::Nothing => sessions.discard().await?,
103- }
104- Ok(())
105-}
106-
107-fn print_messages(items: &Vector<TimelineItem>) {
108- for item in items {
109- if let TimelineItem::Event(event) = item
110- && let TimelineContent::Message(message) = &event.content
111- {
112- println!("{} {}", event.sender.name(), message.body);
113- }
114- }
115-}
116-
117-fn required(name: &str) -> Result<String, Box<dyn std::error::Error>> {
118- std::env::var(name).map_err(|_| format!("{name} is required").into())
119-}
Deleted file: vendor/macltok/src/account.rs−1021 deletions
@@ -1,1021 +0,0 @@
1-#[cfg(feature = "sqlite")]
2-use std::path::PathBuf;
3-#[cfg(not(target_family = "wasm"))]
4-use std::sync::atomic::{AtomicBool, Ordering};
5-use std::{collections::HashMap, sync::Arc};
6-
7-use matrix_sdk::{
8- Client, ClientBuilder, SendOutsideWasm, SessionChange, SessionMeta, SyncOutsideWasm,
9- ThreadingSupport,
10- authentication::{
11- AuthSession,
12- matrix::MatrixSession,
13- oauth::{
14- ClientId, ClientRegistrationData, CsrfToken, OAuthSession, PendingAuthorization,
15- UserSession,
16- registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
17- },
18- },
19- cross_process_lock::CrossProcessLockConfig,
20- encryption::{BackupDownloadStrategy, EncryptionSettings},
21- ruma::{MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, serde::Raw},
22-};
23-use serde::{Deserialize, Serialize};
24-use thiserror::Error;
25-use tokio::sync::{broadcast, mpsc};
26-use url::Url;
27-
28-use crate::{
29- ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, EventStream, ServerError,
30- SessionStore,
31- engine::{Persistence, StoreLock},
32- error::SourceError,
33-};
34-
35-/// a logged in client and the `SessionStore` its record lives in. the engine it builds
36-/// saves every token rotation there, unless told `without_persistence`
37-pub struct Session {
38- client: Client,
39- tracking: Tracking,
40- store_lock: StoreLock,
41-}
42-
43-/// taken as soon as the client exists, since the sdk can refresh before there's an engine
44-struct Tracking {
45- session_changes: broadcast::Receiver<SessionChange>,
46- persistence: Option<Persistence>,
47- #[cfg(not(target_family = "wasm"))]
48- inline: Arc<AtomicBool>,
49-}
50-
51-#[non_exhaustive]
52-pub struct PasswordLogin {
53- pub homeserver: String,
54- pub username: String,
55- pub password: String,
56- pub device_display_name: Option<String>,
57- /// set after a soft logout so the store's keys still belong to us
58- pub device_id: Option<OwnedDeviceId>,
59- /// let plain http through, for loopback hosts only
60- pub allow_insecure_loopback: bool,
61-}
62-
63-impl PasswordLogin {
64- #[must_use]
65- pub fn new(
66- homeserver: impl Into<String>,
67- username: impl Into<String>,
68- password: impl Into<String>,
69- ) -> Self {
70- Self {
71- homeserver: homeserver.into(),
72- username: username.into(),
73- password: password.into(),
74- device_display_name: None,
75- device_id: None,
76- allow_insecure_loopback: false,
77- }
78- }
79-
80- #[must_use]
81- pub fn with_device_display_name(mut self, name: impl Into<String>) -> Self {
82- self.device_display_name = Some(name.into());
83- self
84- }
85-
86- #[must_use]
87- pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
88- self.device_id = Some(device_id);
89- self
90- }
91-
92- #[must_use]
93- pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
94- self.allow_insecure_loopback = allow;
95- self
96- }
97-}
98-
99-#[non_exhaustive]
100-pub struct OAuthLogin {
101- pub homeserver: String,
102- pub redirect_uri: String,
103- pub client_uri: String,
104- pub client_name: Option<String>,
105- pub client_id: Option<String>,
106- pub device_id: Option<OwnedDeviceId>,
107- pub application_type: OAuthApplicationType,
108- /// let plain http through, for loopback hosts only
109- pub allow_insecure_loopback: bool,
110-}
111-
112-impl OAuthLogin {
113- #[must_use]
114- pub fn new(
115- homeserver: impl Into<String>,
116- redirect_uri: impl Into<String>,
117- client_uri: impl Into<String>,
118- application_type: OAuthApplicationType,
119- ) -> Self {
120- Self {
121- homeserver: homeserver.into(),
122- redirect_uri: redirect_uri.into(),
123- client_uri: client_uri.into(),
124- client_name: None,
125- client_id: None,
126- device_id: None,
127- application_type,
128- allow_insecure_loopback: false,
129- }
130- }
131-
132- #[must_use]
133- pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
134- self.client_name = Some(name.into());
135- self
136- }
137-
138- /// a statically registered client id, skipping dynamic registration
139- #[must_use]
140- pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
141- self.client_id = Some(client_id.into());
142- self
143- }
144-
145- #[must_use]
146- pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
147- self.device_id = Some(device_id);
148- self
149- }
150-
151- #[must_use]
152- pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
153- self.allow_insecure_loopback = allow;
154- self
155- }
156-}
157-
158-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159-#[non_exhaustive]
160-pub enum OAuthApplicationType {
161- Native,
162- Web,
163-}
164-
165-pub struct PendingOAuthLogin {
166- client: Client,
167- sessions: Arc<dyn SessionStore>,
168- tracking: Option<(Tracking, StoreLock)>,
169- authorization_url: String,
170- state: Option<CsrfToken>,
171- previous: Option<SessionRecord>,
172-}
173-
174-impl PendingOAuthLogin {
175- #[must_use]
176- pub fn authorization_url(&self) -> &str {
177- &self.authorization_url
178- }
179-
180- #[must_use]
181- pub fn registered_client_id(&self) -> String {
182- self.client
183- .oauth()
184- .client_id()
185- .expect("OAuth client was registered before authorization")
186- .as_str()
187- .to_owned()
188- }
189-
190- #[must_use]
191- pub const fn is_pending(&self) -> bool {
192- self.state.is_some()
193- }
194-
195- pub async fn finish(&mut self, callback_url: &str) -> Result<Session, AccountError> {
196- let callback_url = Url::parse(callback_url)
197- .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
198- let state = self
199- .state
200- .take()
201- .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
202- let oauth = self.client.oauth();
203- if let Err(error) = oauth.finish_login(callback_url.into()).await {
204- oauth.abort_login(&state).await;
205- if let Some(previous) = self.previous.take() {
206- put_back(self.sessions.as_ref(), previous).await;
207- }
208- return Err(account_error("OAuth login", error));
209- }
210- let (tracking, store_lock) = self
211- .tracking
212- .take()
213- .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
214- Session::saved(
215- self.client.clone(),
216- self.sessions.clone(),
217- tracking,
218- store_lock,
219- )
220- .await
221- }
222-
223- /// puts back what the store held before, or deletes the store if it held nothing
224- pub async fn abort(self) {
225- let Self {
226- client,
227- sessions,
228- tracking,
229- state,
230- previous,
231- ..
232- } = self;
233- let Some(state) = state else {
234- return;
235- };
236- client.oauth().abort_login(&state).await;
237- drop(client);
238- match previous {
239- Some(previous) => put_back(sessions.as_ref(), previous).await,
240- None => {
241- if let Err(error) = sessions.discard().await {
242- tracing::warn!(%error, "deleting an abandoned OAuth login's store failed");
243- }
244- }
245- }
246- drop(tracking);
247- }
248-}
249-
250-async fn put_back(sessions: &dyn SessionStore, previous: SessionRecord) {
251- if let Err(error) = sessions.save(&previous).await {
252- tracing::warn!(%error, "putting back the record an OAuth login replaced failed");
253- }
254-}
255-
256-pub struct StoreConfig {
257- location: StoreLocation,
258- passphrase: String,
259-}
260-
261-enum StoreLocation {
262- #[cfg(feature = "sqlite")]
263- Sqlite(PathBuf),
264- #[cfg(feature = "indexeddb")]
265- IndexedDb(String),
266-}
267-
268-impl StoreConfig {
269- #[cfg(feature = "sqlite")]
270- #[must_use]
271- pub fn sqlite(path: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
272- Self {
273- location: StoreLocation::Sqlite(path.into()),
274- passphrase: passphrase.into(),
275- }
276- }
277-
278- #[cfg(feature = "indexeddb")]
279- #[must_use]
280- pub fn indexeddb(name: impl Into<String>, passphrase: impl Into<String>) -> Self {
281- Self {
282- location: StoreLocation::IndexedDb(name.into()),
283- passphrase: passphrase.into(),
284- }
285- }
286-
287- /// delete the store's databases, once nothing has them open
288- pub async fn delete(&self) -> Result<(), AccountError> {
289- match &self.location {
290- #[cfg(feature = "sqlite")]
291- StoreLocation::Sqlite(path) => crate::session_store::delete_sqlite_store(path)
292- .map_err(|error| account_error("deleting the Matrix store", error)),
293- #[cfg(feature = "indexeddb")]
294- StoreLocation::IndexedDb(name) => crate::session_store::delete_indexeddb_store(name)
295- .await
296- .map_err(|error| error.into_account_error("deleting the Matrix store")),
297- }
298- }
299-
300- /// taken before the client opens the store, and held until the engine and every
301- /// `ClientHandle` are gone
302- pub(crate) async fn lock(&self) -> Result<StoreLock, AccountError> {
303- match &self.location {
304- #[cfg(feature = "sqlite")]
305- StoreLocation::Sqlite(path) => StoreLock::file(path),
306- #[cfg(feature = "indexeddb")]
307- StoreLocation::IndexedDb(name) => StoreLock::web(name).await,
308- }
309- }
310-
311- pub(crate) fn passphrase(&self) -> &str {
312- &self.passphrase
313- }
314-
315- #[cfg(feature = "indexeddb")]
316- pub(crate) fn indexeddb_name(&self) -> &str {
317- let StoreLocation::IndexedDb(name) = &self.location;
318- name
319- }
320-}
321-
322-/// Serializable authentication secrets. A `SessionStore` keeps it, and the engine
323-/// replaces it there on every token rotation.
324-#[derive(Clone, Deserialize, Serialize)]
325-pub struct SessionRecord {
326- homeserver: String,
327- authentication: AuthenticationRecord,
328-}
329-
330-#[derive(Clone, Deserialize, Serialize)]
331-#[serde(tag = "type", content = "session", rename_all = "snake_case")]
332-enum AuthenticationRecord {
333- Matrix(MatrixSession),
334- OAuth {
335- client_id: ClientId,
336- user: UserSession,
337- },
338- SoftLoggedOut {
339- meta: SessionMeta,
340- client_id: Option<ClientId>,
341- },
342- /// an oauth login waiting on its redirect. `authorization` holds the pkce verifier
343- PendingOAuth {
344- client_id: ClientId,
345- state: CsrfToken,
346- authorization: Box<PendingAuthorization>,
347- started_ms: u64,
348- /// what the store held before, put back if the login never finishes
349- previous: Option<Box<SessionRecord>>,
350- },
351-}
352-
353-/// authorization codes live for minutes, so a login this old was abandoned
354-const OAUTH_PENDING_FOR_MS: u64 = 60 * 60 * 1000;
355-
356-/// the authorization server answers in the query, or the fragment when asked to
357-pub(crate) fn callback_state(callback: &Url) -> Option<String> {
358- let query = callback.query_pairs();
359- let fragment = url::form_urlencoded::parse(callback.fragment().unwrap_or_default().as_bytes());
360- query
361- .chain(fragment)
362- .find(|(key, _)| key == "state")
363- .map(|(_, state)| state.into_owned())
364-}
365-
366-fn now_ms() -> u64 {
367- MilliSecondsSinceUnixEpoch::now().get().into()
368-}
369-
370-impl SessionRecord {
371- #[must_use]
372- pub fn homeserver(&self) -> &str {
373- &self.homeserver
374- }
375-
376- /// a pending oauth login is nobody yet, unless it resumes someone
377- pub(crate) fn user_id(&self) -> Option<&OwnedUserId> {
378- match &self.authentication {
379- AuthenticationRecord::Matrix(session) => Some(&session.meta.user_id),
380- AuthenticationRecord::OAuth { user, .. } => Some(&user.meta.user_id),
381- AuthenticationRecord::SoftLoggedOut { meta, .. } => Some(&meta.user_id),
382- AuthenticationRecord::PendingOAuth { previous, .. } => previous.as_ref()?.user_id(),
383- }
384- }
385-
386- /// what a login should resume. a pending oauth login stands in for the record before it
387- pub(crate) fn resumable(self) -> Option<Self> {
388- match self.authentication {
389- AuthenticationRecord::PendingOAuth { previous, .. } => {
390- previous.and_then(|previous| previous.resumable())
391- }
392- _ => Some(self),
393- }
394- }
395-
396- pub(crate) fn pending_oauth_state(&self) -> Option<&str> {
397- match &self.authentication {
398- AuthenticationRecord::PendingOAuth { state, .. } => Some(state.secret()),
399- _ => None,
400- }
401- }
402-
403- pub(crate) fn oauth_expired(&self) -> bool {
404- let AuthenticationRecord::PendingOAuth { started_ms, .. } = &self.authentication else {
405- return false;
406- };
407- now_ms().saturating_sub(*started_ms) > OAUTH_PENDING_FOR_MS
408- }
409-
410- pub(crate) fn soft_logout(&self) -> Option<SoftLogout> {
411- let (meta, client_id) = match &self.authentication {
412- AuthenticationRecord::SoftLoggedOut { meta, client_id } => (meta, client_id),
413- AuthenticationRecord::PendingOAuth { previous, .. } => {
414- return previous.as_ref()?.soft_logout();
415- }
416- _ => return None,
417- };
418- Some(SoftLogout {
419- homeserver: self.homeserver.clone(),
420- user_id: meta.user_id.clone(),
421- device_id: meta.device_id.clone(),
422- client_id: client_id
423- .as_ref()
424- .map(|client_id| client_id.as_str().to_owned()),
425- })
426- }
427-
428- pub(crate) fn soft_logged_out(self) -> Self {
429- let authentication = match self.authentication {
430- AuthenticationRecord::Matrix(session) => AuthenticationRecord::SoftLoggedOut {
431- meta: session.meta,
432- client_id: None,
433- },
434- AuthenticationRecord::OAuth { client_id, user } => {
435- AuthenticationRecord::SoftLoggedOut {
436- meta: user.meta,
437- client_id: Some(client_id),
438- }
439- }
440- soft @ AuthenticationRecord::SoftLoggedOut { .. } => soft,
441- pending @ AuthenticationRecord::PendingOAuth { .. } => pending,
442- };
443- Self {
444- homeserver: self.homeserver,
445- authentication,
446- }
447- }
448-}
449-
450-pub enum Restore {
451- Resumed(Session),
452- SoftLoggedOut(SoftLogout),
453- /// an oauth login is waiting on its redirect. `Session::finish_oauth` takes the callback
454- OAuthPending,
455-}
456-
457-/// who to log in again as. the store keeps this device's e2ee keys, so a login
458-/// as any other device would orphan them
459-#[derive(Clone, Debug)]
460-#[non_exhaustive]
461-pub struct SoftLogout {
462- pub homeserver: String,
463- pub user_id: OwnedUserId,
464- pub device_id: OwnedDeviceId,
465- /// set when the login was OAuth, for `OAuthLogin::with_client_id`
466- pub client_id: Option<String>,
467-}
468-
469-impl SoftLogout {
470- #[must_use]
471- pub fn password_login(&self, password: impl Into<String>) -> PasswordLogin {
472- PasswordLogin::new(self.homeserver.clone(), self.user_id.as_str(), password)
473- .with_device_id(self.device_id.clone())
474- .allow_insecure_loopback(true)
475- }
476-
477- #[must_use]
478- pub fn oauth_login(
479- &self,
480- redirect_uri: impl Into<String>,
481- client_uri: impl Into<String>,
482- application_type: OAuthApplicationType,
483- ) -> OAuthLogin {
484- let login = OAuthLogin::new(
485- self.homeserver.clone(),
486- redirect_uri,
487- client_uri,
488- application_type,
489- )
490- .with_device_id(self.device_id.clone())
491- .allow_insecure_loopback(true);
492- match &self.client_id {
493- Some(client_id) => login.with_client_id(client_id.clone()),
494- None => login,
495- }
496- }
497-}
498-
499-#[derive(Debug, Error)]
500-pub enum AccountError {
501- #[error("{context} failed")]
502- Operation {
503- context: &'static str,
504- kind: ErrorKind,
505- server: Option<ServerError>,
506- #[source]
507- source: Box<SourceError>,
508- },
509- #[error("the homeserver returned no authenticated session")]
510- MissingSession,
511- #[error("the SDK returned an unsupported authentication session")]
512- UnsupportedAuthentication,
513- #[error("the OAuth callback was already submitted")]
514- OAuthCallbackAlreadySubmitted,
515- /// nothing is waiting for this callback. it was finished, abandoned, expired, or begun
516- /// somewhere else
517- #[error("no OAuth login is waiting for this callback")]
518- NoPendingOAuth,
519- /// what was typed can't name a homeserver, so nothing was sent anywhere
520- #[error("that isn't a usable homeserver, {0}")]
521- InvalidHomeserver(&'static str),
522- /// `Accounts` already has a live login for them. a second would be another device
523- #[error("{0} is already logged in")]
524- AlreadyLoggedIn(OwnedUserId),
525- /// an IndexedDB delete is waiting on the store's other connections. it goes through
526- /// once they close, and opening the store again waits behind it
527- #[error("the store is still open elsewhere, it will be deleted once that closes")]
528- StoreDeletePending,
529- /// another engine, in this process or another, has the store. only one runs per store
530- #[error("another engine is using this store")]
531- StoreInUse,
532- /// the login worked but its first record didn't save. the server already has the
533- /// device, so retry the save rather than log in again
534- #[error("saving the new login's record failed")]
535- Unsaved {
536- session: Box<UnsavedSession>,
537- #[source]
538- source: Box<AccountError>,
539- },
540- #[error(transparent)]
541- Engine(#[from] EngineError),
542-}
543-
544-impl AccountError {
545- /// what the homeserver said, when it refused
546- #[must_use]
547- pub fn server(&self) -> Option<&ServerError> {
548- match self {
549- Self::Operation { server, .. } => server.as_ref(),
550- Self::Unsaved { source, .. } => source.server(),
551- _ => None,
552- }
553- }
554-}
555-
556-/// a live login whose record never reached its `SessionStore`
557-pub struct UnsavedSession {
558- session: Session,
559- sessions: Arc<dyn SessionStore>,
560-}
561-
562-impl UnsavedSession {
563- pub async fn save(self) -> Result<Session, AccountError> {
564- let saved = match session_record(&self.session.client) {
565- Ok(record) => self.sessions.save(&record).await,
566- Err(error) => Err(error),
567- };
568- match saved {
569- Ok(()) => Ok(self.session),
570- Err(source) => Err(AccountError::Unsaved {
571- session: Box::new(self),
572- source: Box::new(source),
573- }),
574- }
575- }
576-
577- /// the record to keep somewhere else, if the store won't take it
578- pub fn record(&self) -> Result<SessionRecord, AccountError> {
579- session_record(&self.session.client)
580- }
581-}
582-
583-impl std::fmt::Debug for UnsavedSession {
584- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585- formatter
586- .debug_struct("UnsavedSession")
587- .finish_non_exhaustive()
588- }
589-}
590-
591-impl Session {
592- pub async fn login_password(
593- login: PasswordLogin,
594- sessions: Arc<dyn SessionStore>,
595- ) -> Result<Self, AccountError> {
596- homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
597- let store_lock = sessions.store().lock().await?;
598- Self::login_password_locked(login, sessions, store_lock).await
599- }
600-
601- pub(crate) async fn login_password_locked(
602- login: PasswordLogin,
603- sessions: Arc<dyn SessionStore>,
604- store_lock: StoreLock,
605- ) -> Result<Self, AccountError> {
606- let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
607- let client = matrix_client_builder(sessions.store())
608- .server_name_or_homeserver_url(homeserver)
609- .build()
610- .await
611- .map_err(|error| account_error("building the Matrix client", error))?;
612- let tracking = Tracking::start(&client, sessions.clone())?;
613- let mut request = client
614- .matrix_auth()
615- .login_username(login.username.trim(), &login.password)
616- .request_refresh_token();
617- if let Some(name) = login.device_display_name.as_deref() {
618- request = request.initial_device_display_name(name);
619- }
620- if let Some(device_id) = login.device_id.as_deref() {
621- request = request.device_id(device_id.as_str());
622- }
623- request
624- .send()
625- .await
626- .map_err(|error| account_error("password login", error))?;
627- Self::saved(client, sessions, tracking, store_lock).await
628- }
629-
630- /// the pending login lives only in this process, so the one that begins it has to finish it
631- pub async fn begin_oauth(
632- login: OAuthLogin,
633- sessions: Arc<dyn SessionStore>,
634- ) -> Result<PendingOAuthLogin, AccountError> {
635- homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
636- let store_lock = sessions.store().lock().await?;
637- Self::begin_oauth_locked(login, sessions, store_lock).await
638- }
639-
640- pub(crate) async fn begin_oauth_locked(
641- login: OAuthLogin,
642- sessions: Arc<dyn SessionStore>,
643- store_lock: StoreLock,
644- ) -> Result<PendingOAuthLogin, AccountError> {
645- let redirect_uri = Url::parse(login.redirect_uri.trim())
646- .map_err(|error| account_error("parsing the OAuth redirect URI", error))?;
647- let client_uri = Url::parse(login.client_uri.trim())
648- .map_err(|error| account_error("parsing the OAuth client URI", error))?;
649- let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
650- let client = matrix_client_builder(sessions.store())
651- .server_name_or_homeserver_url(homeserver)
652- .build()
653- .await
654- .map_err(|error| account_error("building the Matrix client", error))?;
655- let tracking = Tracking::start(&client, sessions.clone())?;
656- let application_type = match login.application_type {
657- OAuthApplicationType::Native => ApplicationType::Native,
658- OAuthApplicationType::Web => ApplicationType::Web,
659- };
660- let mut metadata = ClientMetadata::new(
661- application_type,
662- vec![OAuthGrantType::AuthorizationCode {
663- redirect_uris: vec![redirect_uri.clone()],
664- }],
665- Localized::new(client_uri, []),
666- );
667- metadata.client_name = login.client_name.map(|name| Localized::new(name, []));
668- let metadata = Raw::new(&metadata)
669- .map_err(|error| account_error("serializing OAuth client metadata", error))?;
670- let mut registration = ClientRegistrationData::new(metadata);
671- if let Some(client_id) = login.client_id {
672- registration.static_registrations = Some(HashMap::from([(
673- client.homeserver(),
674- ClientId::new(client_id),
675- )]));
676- }
677- let authorization = client
678- .oauth()
679- .login(redirect_uri, login.device_id, Some(registration), None)
680- .build()
681- .await
682- .map_err(|error| account_error("starting OAuth login", error))?;
683- let previous = sessions.load().await?.and_then(SessionRecord::resumable);
684- let pending = SessionRecord {
685- homeserver: client.homeserver().to_string(),
686- authentication: AuthenticationRecord::PendingOAuth {
687- client_id: client
688- .oauth()
689- .client_id()
690- .ok_or(AccountError::MissingSession)?
691- .clone(),
692- state: authorization.state.clone(),
693- authorization: Box::new(
694- client
695- .oauth()
696- .pending_authorization(&authorization.state)
697- .await
698- .ok_or(AccountError::MissingSession)?,
699- ),
700- started_ms: now_ms(),
701- previous: previous.clone().map(Box::new),
702- },
703- };
704- sessions.save(&pending).await?;
705- Ok(PendingOAuthLogin {
706- client,
707- sessions,
708- tracking: Some((tracking, store_lock)),
709- authorization_url: authorization.url.to_string(),
710- state: Some(authorization.state),
711- previous,
712- })
713- }
714-
715- /// finishes a login `begin_oauth` started in another process, from the callback the
716- /// redirect brought back. the pending login is in `sessions`, so nothing else is needed
717- pub async fn finish_oauth(
718- sessions: Arc<dyn SessionStore>,
719- callback_url: &str,
720- ) -> Result<Self, AccountError> {
721- let store_lock = sessions.store().lock().await?;
722- Self::finish_oauth_locked(sessions, store_lock, callback_url).await
723- }
724-
725- pub(crate) async fn finish_oauth_locked(
726- sessions: Arc<dyn SessionStore>,
727- store_lock: StoreLock,
728- callback_url: &str,
729- ) -> Result<Self, AccountError> {
730- let callback = Url::parse(callback_url)
731- .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
732- let record = sessions.load().await?.ok_or(AccountError::NoPendingOAuth)?;
733- if record.pending_oauth_state() != callback_state(&callback).as_deref() {
734- return Err(AccountError::NoPendingOAuth);
735- }
736- let AuthenticationRecord::PendingOAuth {
737- client_id,
738- state,
739- authorization,
740- previous,
741- ..
742- } = record.authentication
743- else {
744- return Err(AccountError::NoPendingOAuth);
745- };
746- let client = matrix_client_builder(sessions.store())
747- .homeserver_url(&record.homeserver)
748- .build()
749- .await
750- .map_err(|error| account_error("reopening the Matrix store", error))?;
751- let tracking = Tracking::start(&client, sessions.clone())?;
752- let oauth = client.oauth();
753- oauth.restore_registered_client(client_id);
754- let finished = match oauth
755- .restore_pending_authorization(state, *authorization)
756- .await
757- {
758- Ok(()) => oauth.finish_login(callback.into()).await,
759- Err(error) => Err(error.into()),
760- };
761- if let Err(error) = finished {
762- match previous {
763- Some(previous) => put_back(sessions.as_ref(), *previous).await,
764- None => {
765- drop((tracking, client));
766- if let Err(error) = sessions.discard().await {
767- tracing::warn!(%error, "deleting a failed OAuth login's store failed");
768- }
769- }
770- }
771- return Err(account_error("OAuth login", error));
772- }
773- Self::saved(client, sessions, tracking, store_lock).await
774- }
775-
776- /// Restore the record `sessions` holds, or `None` when it holds none.
777- /// The caller must also handle rejection and explicit logout.
778- pub async fn restore(sessions: Arc<dyn SessionStore>) -> Result<Option<Restore>, AccountError> {
779- let Some(record) = sessions.load().await? else {
780- return Ok(None);
781- };
782- let authentication = match record.authentication {
783- AuthenticationRecord::Matrix(session) => AuthSession::Matrix(session),
784- AuthenticationRecord::OAuth { client_id, user } => {
785- OAuthSession { client_id, user }.into()
786- }
787- AuthenticationRecord::PendingOAuth { .. } if record.oauth_expired() => {
788- let previous = record.resumable();
789- match previous {
790- Some(previous) => put_back(sessions.as_ref(), previous).await,
791- None => sessions.discard().await?,
792- }
793- return Box::pin(Self::restore(sessions)).await;
794- }
795- AuthenticationRecord::PendingOAuth { .. } => return Ok(Some(Restore::OAuthPending)),
796- AuthenticationRecord::SoftLoggedOut { meta, client_id } => {
797- return Ok(Some(Restore::SoftLoggedOut(SoftLogout {
798- homeserver: record.homeserver,
799- user_id: meta.user_id,
800- device_id: meta.device_id,
801- client_id: client_id.map(|client_id| client_id.as_str().to_owned()),
802- })));
803- }
804- };
805- let store_lock = sessions.store().lock().await?;
806- let client = matrix_client_builder(sessions.store())
807- .homeserver_url(&record.homeserver)
808- .build()
809- .await
810- .map_err(|error| account_error("reopening the Matrix store", error))?;
811- let tracking = Tracking::start(&client, sessions)?;
812- client
813- .restore_session(authentication)
814- .await
815- .map_err(|error| account_error("restoring the authenticated session", error))?;
816- Ok(Some(Restore::Resumed(Self {
817- client,
818- tracking,
819- store_lock,
820- })))
821- }
822-
823- /// leave the record to the host, which then saves `ClientHandle::session_record()`
824- /// on every `SessionTokensChanged`
825- #[must_use]
826- pub fn without_persistence(mut self) -> Self {
827- self.tracking.persistence = None;
828- #[cfg(not(target_family = "wasm"))]
829- self.tracking.inline.store(false, Ordering::Release);
830- self
831- }
832-
833- pub async fn build(
834- self,
835- config: EngineConfig,
836- ) -> Result<(Engine, ClientHandle, EventStream), AccountError> {
837- Engine::with_persistence(
838- self.client,
839- config,
840- self.tracking.session_changes,
841- self.tracking.persistence,
842- Some(self.store_lock),
843- )
844- .await
845- .map_err(AccountError::from)
846- }
847-
848- async fn saved(
849- client: Client,
850- sessions: Arc<dyn SessionStore>,
851- tracking: Tracking,
852- store_lock: StoreLock,
853- ) -> Result<Self, AccountError> {
854- UnsavedSession {
855- session: Self {
856- client,
857- tracking,
858- store_lock,
859- },
860- sessions,
861- }
862- .save()
863- .await
864- }
865-}
866-
867-impl Tracking {
868- fn start(client: &Client, sessions: Arc<dyn SessionStore>) -> Result<Self, AccountError> {
869- let session_changes = client.subscribe_to_session_changes();
870- #[cfg_attr(target_family = "wasm", allow(unused_variables))]
871- let (results, inline_results) = mpsc::unbounded_channel();
872- #[cfg(not(target_family = "wasm"))]
873- let inline = save_inline(client, sessions.clone(), results)?;
874- Ok(Self {
875- session_changes,
876- persistence: Some(Persistence {
877- save: Box::new(move |client| {
878- let sessions = sessions.clone();
879- Box::pin(async move {
880- let record = session_record(&client).map_err(save_failed)?;
881- sessions.save(&record).await.map_err(save_failed)
882- })
883- }),
884- inline: inline_results,
885- }),
886- #[cfg(not(target_family = "wasm"))]
887- inline,
888- })
889- }
890-}
891-
892-/// the sdk calls this inside its refresh, after it has the new tokens and before it uses
893-/// them, so a store that writes there and then can't lose a refresh token to a crash
894-#[cfg(not(target_family = "wasm"))]
895-fn save_inline(
896- client: &Client,
897- sessions: Arc<dyn SessionStore>,
898- results: mpsc::UnboundedSender<Result<(), crate::Error>>,
899-) -> Result<Arc<AtomicBool>, AccountError> {
900- let enabled = Arc::new(AtomicBool::new(true));
901- let saving = enabled.clone();
902- client
903- .set_session_callbacks(
904- // only asked for with the cross process refresh lock, which we never enable
905- Box::new(|client| {
906- client
907- .session_tokens()
908- .ok_or_else(|| AccountError::MissingSession.into())
909- }),
910- Box::new(move |client| {
911- if !saving.load(Ordering::Acquire) {
912- return Ok(());
913- }
914- let record = session_record(&client)?;
915- if let Some(result) = sessions.save_blocking(&record) {
916- let _ = results.send(result.map_err(save_failed));
917- }
918- Ok(())
919- }),
920- )
921- .map_err(|error| account_error("registering the session callbacks", error))?;
922- Ok(enabled)
923-}
924-
925-fn save_failed(error: AccountError) -> crate::Error {
926- let message = format!("saving the session record failed, {error}");
927- crate::Error::with_source(ErrorKind::Store, message, error)
928-}
929-
930-pub(crate) fn session_record(client: &Client) -> Result<SessionRecord, AccountError> {
931- let authentication = match client.session().ok_or(AccountError::MissingSession)? {
932- AuthSession::Matrix(session) => AuthenticationRecord::Matrix(session),
933- AuthSession::OAuth(session) => {
934- let OAuthSession { client_id, user } = *session;
935- AuthenticationRecord::OAuth { client_id, user }
936- }
937- _ => return Err(AccountError::UnsupportedAuthentication),
938- };
939- Ok(SessionRecord {
940- homeserver: client.homeserver().to_string(),
941- authentication,
942- })
943-}
944-
945-fn encryption_settings() -> EncryptionSettings {
946- EncryptionSettings {
947- auto_enable_cross_signing: true,
948- backup_download_strategy: BackupDownloadStrategy::OneShot,
949- auto_enable_backups: true,
950- }
951-}
952-
953-/// `StoreLock` already keeps every other client off the store, and the sdk's lease would cost
954-/// a store write per locked section and a task renewing it every 50ms
955-fn matrix_client_builder(store: &StoreConfig) -> ClientBuilder {
956- let builder = Client::builder()
957- .with_encryption_settings(encryption_settings())
958- .with_threading_support(ThreadingSupport::Enabled {
959- with_subscriptions: false,
960- })
961- .handle_refresh_tokens()
962- .cross_process_store_config(CrossProcessLockConfig::SingleProcess);
963- match &store.location {
964- #[cfg(feature = "sqlite")]
965- StoreLocation::Sqlite(path) => builder.sqlite_store(path, Some(&store.passphrase)),
966- #[cfg(feature = "indexeddb")]
967- StoreLocation::IndexedDb(name) => builder.indexeddb_store(name, Some(&store.passphrase)),
968- }
969-}
970-
971-/// a bare server name goes through untouched, since that's what makes the sdk try `.well-known`
972-pub(crate) fn homeserver_input(
973- input: &str,
974- allow_insecure_loopback: bool,
975-) -> Result<&str, AccountError> {
976- let input = input.trim();
977- let invalid = |reason| Err(AccountError::InvalidHomeserver(reason));
978- if input.is_empty() {
979- return invalid("it's empty");
980- }
981- if input.contains(char::is_whitespace) {
982- return invalid("it has spaces in it");
983- }
984- if !input.contains("://") {
985- return match <&ruma::ServerName>::try_from(input) {
986- Ok(_) => Ok(input),
987- Err(_) => invalid("it's neither a server name nor a url"),
988- };
989- }
990- let Ok(url) = Url::parse(input) else {
991- return invalid("it isn't a valid url");
992- };
993- if !url.username().is_empty() || url.password().is_some() {
994- return invalid("it has a username in it");
995- }
996- let loopback = match url.host() {
997- None => return invalid("it has no host"),
998- Some(url::Host::Domain(domain)) => domain.eq_ignore_ascii_case("localhost"),
999- Some(url::Host::Ipv4(address)) => address.is_loopback(),
1000- Some(url::Host::Ipv6(address)) => address.is_loopback(),
1001- };
1002- match url.scheme() {
1003- "https" => Ok(input),
1004- "http" if allow_insecure_loopback && loopback => Ok(input),
1005- "http" if allow_insecure_loopback => invalid("plain http only reaches loopback hosts"),
1006- "http" => invalid("it isn't https"),
1007- _ => invalid("only http and https are spoken"),
1008- }
1009-}
1010-
1011-pub(crate) fn account_error(
1012- context: &'static str,
1013- source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
1014-) -> AccountError {
1015- AccountError::Operation {
1016- context,
1017- kind: crate::error::classify(&source),
1018- server: ServerError::find(&source),
1019- source: Box::new(source),
1020- }
1021-}
Deleted file: vendor/macltok/src/accounts.rs−470 deletions
@@ -1,470 +0,0 @@
1-use std::sync::Arc;
2-#[cfg(feature = "sqlite")]
3-use std::{collections::BTreeSet, path::PathBuf};
4-
5-use ruma::TransactionId;
6-
7-use crate::{
8- AccountError, EncryptedSessionStore, OAuthLogin, PasswordLogin, PendingOAuthLogin, Restore,
9- Session, SessionRecord, SessionStore, SoftLogout, StoreConfig,
10- account::{callback_state, homeserver_input},
11- engine::StoreLock,
12-};
13-
14-/// every account one owner has, a store each under one root and one passphrase. whoever holds
15-/// the set can resume any soft-logged-out device in it with just its password, and the device's
16-/// store has its e2ee keys, so a set belongs to one person and never to a whole machine
17-pub struct Accounts {
18- root: Root,
19- passphrase: String,
20-}
21-
22-enum Root {
23- #[cfg(feature = "sqlite")]
24- Directory(PathBuf),
25- #[cfg(feature = "indexeddb")]
26- Prefix(String),
27-}
28-
29-/// one account's record and store
30-#[derive(Clone)]
31-pub struct Account {
32- id: String,
33- sessions: Arc<EncryptedSessionStore>,
34-}
35-
36-impl Account {
37- /// stable for the account's life, and safe in a cookie or a url
38- #[must_use]
39- pub fn id(&self) -> &str {
40- &self.id
41- }
42-
43- /// for `Termination::retain`. a discarded account drops out of `Accounts::list`
44- #[must_use]
45- pub fn sessions(&self) -> Arc<dyn SessionStore> {
46- self.sessions.clone()
47- }
48-
49- pub async fn restore(&self) -> Result<Option<Restore>, AccountError> {
50- Session::restore(self.sessions()).await
51- }
52-}
53-
54-impl std::fmt::Debug for Account {
55- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56- formatter
57- .debug_struct("Account")
58- .field("id", &self.id)
59- .finish_non_exhaustive()
60- }
61-}
62-
63-enum Found {
64- Nobody,
65- SoftLoggedOut(Account, SoftLogout),
66-}
67-
68-impl Accounts {
69- /// each account is a store directory under `root` with its record beside it
70- #[cfg(feature = "sqlite")]
71- #[must_use]
72- pub fn sqlite(root: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
73- Self {
74- root: Root::Directory(root.into()),
75- passphrase: passphrase.into(),
76- }
77- }
78-
79- /// each account is a store named `<prefix>::<id>`, listed in `<prefix>::macltok-accounts`
80- #[cfg(feature = "indexeddb")]
81- #[must_use]
82- pub fn indexeddb(prefix: impl Into<String>, passphrase: impl Into<String>) -> Self {
83- Self {
84- root: Root::Prefix(prefix.into()),
85- passphrase: passphrase.into(),
86- }
87- }
88-
89- /// `None` for anything `Account::id` couldn't have been
90- #[must_use]
91- pub fn account(&self, id: &str) -> Option<Account> {
92- let valid =
93- !id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric());
94- valid.then(|| self.open(id.to_owned()))
95- }
96-
97- /// every account with a record. a store left without one, by a login that died halfway or a
98- /// discard that couldn't finish, is deleted on the way past, and so is an oauth login whose
99- /// redirect never came back. one that resumed an account puts that account back
100- pub async fn list(&self) -> Result<Vec<Account>, AccountError> {
101- let mut accounts = Vec::new();
102- for account in self.slots().await? {
103- match account.sessions.load().await {
104- Ok(None) => self.sweep(&account).await,
105- Ok(Some(record)) if record.oauth_expired() => {
106- if self.expire(&account).await {
107- accounts.push(account);
108- }
109- }
110- Ok(Some(_)) | Err(_) => accounts.push(account),
111- }
112- }
113- Ok(accounts)
114- }
115-
116- /// finishes an oauth login begun before a redirect, finding its account by the callback's
117- /// state, so the host needs to have kept nothing across the redirect
118- pub async fn finish_oauth(
119- &self,
120- callback_url: &str,
121- ) -> Result<(Account, Session), AccountError> {
122- let state = url::Url::parse(callback_url)
123- .ok()
124- .as_ref()
125- .and_then(callback_state)
126- .ok_or(AccountError::NoPendingOAuth)?;
127- for account in self.slots().await? {
128- let Ok(Some(record)) = account.sessions.load().await else {
129- continue;
130- };
131- if record.pending_oauth_state() != Some(state.as_str()) {
132- continue;
133- }
134- let store_lock = account.sessions.store().lock().await?;
135- let session =
136- Session::finish_oauth_locked(account.sessions(), store_lock, callback_url).await?;
137- return Ok((account, session));
138- }
139- Err(AccountError::NoPendingOAuth)
140- }
141-
142- /// a soft-logged-out account the login names is resumed as its own device, keeping its keys.
143- /// anyone else gets a new account
144- pub async fn login_password(
145- &self,
146- login: PasswordLogin,
147- ) -> Result<(Account, Session), AccountError> {
148- homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
149- let found = self
150- .find(|record| record_names(record, &login.homeserver, &login.username))
151- .await?;
152- match found {
153- Found::SoftLoggedOut(account, soft) => {
154- let store_lock = account.sessions.store().lock().await?;
155- let login = PasswordLogin {
156- device_display_name: login.device_display_name,
157- ..soft.password_login(login.password)
158- };
159- let session =
160- Session::login_password_locked(login, account.sessions(), store_lock).await?;
161- Ok((account, session))
162- }
163- Found::Nobody => {
164- let (account, store_lock) = self.fresh().await?;
165- let login = PasswordLogin {
166- device_id: None,
167- ..login
168- };
169- match Session::login_password_locked(login, account.sessions(), store_lock).await {
170- Ok(session) => Ok((account, session)),
171- Err(error) => Err(self.abandon(&account, error).await),
172- }
173- }
174- }
175- }
176-
177- /// resumes the soft-logged-out account whose device `login` names, as
178- /// `SoftLogout::oauth_login` sets it. without one it's a new account, since which account an
179- /// OAuth login is for isn't known until it finishes
180- pub async fn begin_oauth(
181- &self,
182- login: OAuthLogin,
183- ) -> Result<(Account, PendingOAuthLogin), AccountError> {
184- homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
185- let device_id = login.device_id.clone();
186- let found = self
187- .find(|record| {
188- record
189- .soft_logout()
190- .is_some_and(|soft| device_id.as_ref() == Some(&soft.device_id))
191- })
192- .await?;
193- match found {
194- Found::SoftLoggedOut(account, soft) => {
195- let store_lock = account.sessions.store().lock().await?;
196- let login = OAuthLogin {
197- homeserver: soft.homeserver,
198- client_id: login.client_id.or(soft.client_id),
199- allow_insecure_loopback: true,
200- ..login
201- };
202- let pending =
203- Session::begin_oauth_locked(login, account.sessions(), store_lock).await?;
204- Ok((account, pending))
205- }
206- Found::Nobody => {
207- let (account, store_lock) = self.fresh().await?;
208- let login = OAuthLogin {
209- device_id: None,
210- ..login
211- };
212- match Session::begin_oauth_locked(login, account.sessions(), store_lock).await {
213- Ok(pending) => Ok((account, pending)),
214- Err(error) => Err(self.abandon(&account, error).await),
215- }
216- }
217- }
218- }
219-
220- /// a live login for the same person is refused rather than given a second device
221- async fn find(&self, resumes: impl Fn(&SessionRecord) -> bool) -> Result<Found, AccountError> {
222- for account in self.slots().await? {
223- let Ok(Some(record)) = account.sessions.load().await else {
224- continue;
225- };
226- if !resumes(&record) {
227- continue;
228- }
229- return match (record.soft_logout(), record.user_id()) {
230- (Some(soft), _) => Ok(Found::SoftLoggedOut(account, soft)),
231- (None, Some(user_id)) => Err(AccountError::AlreadyLoggedIn(user_id.clone())),
232- (None, None) => continue,
233- };
234- }
235- Ok(Found::Nobody)
236- }
237-
238- /// locked before anything else can see it, so `list` never sweeps a login still under way
239- async fn fresh(&self) -> Result<(Account, StoreLock), AccountError> {
240- let account = self.open(TransactionId::new().to_string());
241- let store_lock = account.sessions.store().lock().await?;
242- #[cfg(feature = "indexeddb")]
243- {
244- let Root::Prefix(prefix) = &self.root;
245- index::add(prefix, &account.id).await?;
246- }
247- Ok((account, store_lock))
248- }
249-
250- /// a failed first login leaves nothing behind. one whose record didn't save has a live device
251- /// on the server, so that stays for `UnsavedSession::save`
252- async fn abandon(&self, account: &Account, error: AccountError) -> AccountError {
253- if !matches!(error, AccountError::Unsaved { .. }) {
254- self.sweep(account).await;
255- }
256- error
257- }
258-
259- /// best effort, since whatever it leaves is found and swept again by the next `list`
260- async fn sweep(&self, account: &Account) {
261- let Ok(_store_lock) = account.sessions.store().lock().await else {
262- return;
263- };
264- if matches!(account.sessions.load().await, Ok(None)) {
265- self.remove(account).await;
266- }
267- }
268-
269- /// whether the account is still there. a login running in another tab holds the lock and
270- /// is left alone
271- async fn expire(&self, account: &Account) -> bool {
272- let Ok(_store_lock) = account.sessions.store().lock().await else {
273- return true;
274- };
275- let Ok(Some(record)) = account.sessions.load().await else {
276- return true;
277- };
278- if !record.oauth_expired() {
279- return true;
280- }
281- match record.resumable() {
282- Some(previous) => {
283- let _ = account.sessions.save(&previous).await;
284- true
285- }
286- None => {
287- self.remove(account).await;
288- false
289- }
290- }
291- }
292-
293- async fn remove(&self, account: &Account) {
294- if account.sessions.discard().await.is_err() {
295- return;
296- }
297- #[cfg(feature = "indexeddb")]
298- {
299- let Root::Prefix(prefix) = &self.root;
300- let _ = index::remove(prefix, &account.id).await;
301- }
302- }
303-
304- fn open(&self, id: String) -> Account {
305- let sessions = match &self.root {
306- #[cfg(feature = "sqlite")]
307- Root::Directory(root) => EncryptedSessionStore::file(
308- root.join(format!("{id}.session")),
309- StoreConfig::sqlite(root.join(&id), self.passphrase.clone()),
310- ),
311- #[cfg(feature = "indexeddb")]
312- Root::Prefix(prefix) => EncryptedSessionStore::indexeddb(StoreConfig::indexeddb(
313- format!("{prefix}::{id}"),
314- self.passphrase.clone(),
315- )),
316- };
317- Account {
318- id,
319- sessions: Arc::new(sessions),
320- }
321- }
322-
323- async fn slots(&self) -> Result<Vec<Account>, AccountError> {
324- let ids = match &self.root {
325- #[cfg(feature = "sqlite")]
326- Root::Directory(root) => directory_slots(root)?,
327- #[cfg(feature = "indexeddb")]
328- Root::Prefix(prefix) => index::list(prefix).await?,
329- };
330- Ok(ids.iter().filter_map(|id| self.account(id)).collect())
331- }
332-}
333-
334-/// a localpart or a full user id names the same account, but only on the same server. the server
335-/// is compared by name as well as by url, so a delegated `example.org` still finds the account
336-/// discovery took to `matrix.example.org`
337-fn record_names(record: &SessionRecord, homeserver: &str, username: &str) -> bool {
338- let Some(user_id) = record.user_id() else {
339- return false;
340- };
341- let username = username.trim();
342- let user = if username.starts_with('@') {
343- username.eq_ignore_ascii_case(user_id.as_str())
344- } else {
345- username.eq_ignore_ascii_case(user_id.localpart())
346- };
347- let typed = authority(homeserver);
348- user && (typed.eq_ignore_ascii_case(user_id.server_name().as_str())
349- || typed.eq_ignore_ascii_case(authority(record.homeserver())))
350-}
351-
352-fn authority(homeserver: &str) -> &str {
353- let homeserver = homeserver.trim();
354- let rest = homeserver
355- .split_once("://")
356- .map_or(homeserver, |(_, rest)| rest);
357- rest.split('/').next().unwrap_or(rest)
358-}
359-
360-#[cfg(feature = "sqlite")]
361-fn directory_slots(root: &std::path::Path) -> Result<BTreeSet<String>, AccountError> {
362- use std::io::ErrorKind;
363-
364- let entries = match std::fs::read_dir(root) {
365- Ok(entries) => entries,
366- Err(error) if error.kind() == ErrorKind::NotFound => return Ok(BTreeSet::new()),
367- Err(error) => return Err(crate::account::account_error("listing the accounts", error)),
368- };
369- let mut ids = BTreeSet::new();
370- for entry in entries {
371- let entry =
372- entry.map_err(|error| crate::account::account_error("listing the accounts", error))?;
373- let name = entry.file_name();
374- let Some(name) = name.to_str() else {
375- continue;
376- };
377- if entry.path().is_dir() {
378- ids.insert(name.to_owned());
379- } else if let Some(id) = name.strip_suffix(".session") {
380- ids.insert(id.to_owned());
381- }
382- }
383- Ok(ids)
384-}
385-
386-/// a key per account, so two tabs adding at once can't lose one to the other
387-#[cfg(feature = "indexeddb")]
388-mod index {
389- use std::collections::BTreeSet;
390-
391- use indexed_db_futures::{
392- Build,
393- database::Database,
394- error::{Error, OpenDbError},
395- prelude::*,
396- transaction::{Transaction, TransactionMode},
397- };
398-
399- use crate::{AccountError, account::account_error};
400-
401- const ACCOUNTS: &str = "accounts";
402-
403- fn name(prefix: &str) -> String {
404- format!("{prefix}::macltok-accounts")
405- }
406-
407- async fn open(prefix: &str) -> Result<Database, OpenDbError> {
408- Database::open(&name(prefix))
409- .with_version(1u32)
410- .with_on_upgrade_needed(|_, transaction: &Transaction<'_>| -> Result<(), Error> {
411- transaction.db().create_object_store(ACCOUNTS).build()?;
412- Ok(())
413- })
414- .build()?
415- .await
416- }
417-
418- pub(super) async fn list(prefix: &str) -> Result<BTreeSet<String>, AccountError> {
419- let failed = |error| account_error("listing the accounts", error);
420- let database = open(prefix).await.map_err(failed)?;
421- let ids = keys(&database).await;
422- database.close();
423- ids.map_err(|error| account_error("listing the accounts", error))
424- }
425-
426- pub(super) async fn add(prefix: &str, id: &str) -> Result<(), AccountError> {
427- let failed = |error| account_error("adding the account", error);
428- let database = open(prefix).await.map_err(failed)?;
429- let added = write(&database, |store| {
430- store.put(true).with_key(id).build()?;
431- Ok(())
432- })
433- .await;
434- database.close();
435- added.map_err(|error| account_error("adding the account", error))
436- }
437-
438- pub(super) async fn remove(prefix: &str, id: &str) -> Result<(), AccountError> {
439- let failed = |error| account_error("removing the account", error);
440- let database = open(prefix).await.map_err(failed)?;
441- let removed = write(&database, |store| {
442- store.delete(id).build()?;
443- Ok(())
444- })
445- .await;
446- database.close();
447- removed.map_err(|error| account_error("removing the account", error))
448- }
449-
450- async fn keys(database: &Database) -> Result<BTreeSet<String>, Error> {
451- let transaction = database
452- .transaction(ACCOUNTS)
453- .with_mode(TransactionMode::Readonly)
454- .build()?;
455- let store = transaction.object_store(ACCOUNTS)?;
456- store.get_all_keys::<String>().await?.collect()
457- }
458-
459- async fn write(
460- database: &Database,
461- change: impl FnOnce(&indexed_db_futures::object_store::ObjectStore<'_>) -> Result<(), Error>,
462- ) -> Result<(), Error> {
463- let transaction = database
464- .transaction(ACCOUNTS)
465- .with_mode(TransactionMode::Readwrite)
466- .build()?;
467- change(&transaction.object_store(ACCOUNTS)?)?;
468- transaction.commit().await
469- }
470-}
Deleted file: vendor/macltok/src/encryption.rs−903 deletions
@@ -1,903 +0,0 @@
1-use std::sync::Arc;
2-
3-use futures_util::StreamExt;
4-use matrix_sdk::executor::{JoinHandle, spawn};
5-use matrix_sdk::{
6- Client,
7- encryption::{
8- VerificationState,
9- backups::BackupState,
10- recovery::RecoveryState as SdkRecoveryState,
11- verification::{
12- SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState,
13- },
14- },
15- ruma::events::key::verification::VerificationMethod,
16-};
17-use ruma::{OwnedDeviceId, UserId};
18-use tokio::sync::Mutex;
19-
20-use crate::{
21- EncryptionState, Error, ErrorKind, Event, KeyBackupState, RecoveryState, VerificationAction,
22- VerificationState as UiState, VerificationTarget, model::VerificationFailure,
23- transport::EventSender,
24-};
25-
26-pub(crate) fn encryption_state(
27- device_id: OwnedDeviceId,
28- recovery: SdkRecoveryState,
29- verification: VerificationState,
30- backup: BackupState,
31-) -> EncryptionState {
32- EncryptionState {
33- device_id,
34- device_verified: matches!(verification, VerificationState::Verified),
35- recovery: match recovery {
36- SdkRecoveryState::Unknown => RecoveryState::Unknown,
37- SdkRecoveryState::Disabled => RecoveryState::Disabled,
38- SdkRecoveryState::Incomplete => RecoveryState::Incomplete,
39- SdkRecoveryState::Enabled => RecoveryState::Enabled,
40- },
41- key_backup: match backup {
42- BackupState::Unknown => KeyBackupState::Unknown,
43- BackupState::Creating => KeyBackupState::Creating,
44- BackupState::Enabling => KeyBackupState::Enabling,
45- BackupState::Resuming => KeyBackupState::Resuming,
46- BackupState::Enabled => KeyBackupState::Enabled,
47- BackupState::Downloading => KeyBackupState::Downloading,
48- BackupState::Disabling => KeyBackupState::Disabling,
49- },
50- }
51-}
52-
53-#[derive(Default)]
54-pub(crate) struct VerificationController {
55- active: Arc<Mutex<VerificationSession>>,
56- task: Option<JoinHandle<()>>,
57-}
58-
59-#[derive(Default)]
60-struct VerificationSession {
61- request: Option<VerificationRequest>,
62- sas: Option<SasVerification>,
63-}
64-
65-impl VerificationController {
66- pub(crate) async fn apply(
67- &mut self,
68- sender: EventSender,
69- action: VerificationAction,
70- ) -> Result<(), Error> {
71- match action {
72- VerificationAction::Accept => self.accept().await,
73- VerificationAction::Confirm => {
74- // Retain a handle across the operation: the watcher clears
75- // `active` when it publishes a terminal event, but this clone
76- // still observes the shared SDK state afterwards.
77- let sas = self.active.lock().await.sas.clone();
78- match self.confirm().await {
79- Ok(()) => Ok(()),
80- Err(error) if error.kind == ErrorKind::VerificationInactive => Err(error),
81- // The confirm advanced local SDK state before its send failed,
82- // so the retained session could report success on retry without
83- // network traffic. Best-effort cancel (no lock held across the
84- // network), clear for restart, then report the terminal state.
85- Err(error) => {
86- // Stop the watcher first so it cannot emit a stale
87- // Confirmed from the local transition we are unwinding.
88- self.stop_task().await;
89- // The peer can complete or cancel the verification
90- // while our own Done send fails. The stopped watcher may
91- // already have published that terminal outcome (losing the
92- // claim below); otherwise this path owns it and must emit
93- // it instead of regressing to Cancelled.
94- if sas.as_ref().is_some_and(|sas| sas.is_done()) {
95- if take_verification(&self.active).await {
96- let _ =
97- sender.send(Event::VerificationChanged(UiState::Done)).await;
98- }
99- return Err(error);
100- }
101- if let Some(reason) = sas
102- .as_ref()
103- .and_then(|sas| sas.cancel_info())
104- .map(|info| info.reason().to_owned())
105- {
106- if take_verification(&self.active).await {
107- let _ = sender
108- .send(Event::VerificationChanged(UiState::Cancelled(reason)))
109- .await;
110- }
111- return Err(error);
112- }
113- if let Some(sas) = sas {
114- if sas.cancel().await.is_err() {
115- // The best-effort unwind already advanced
116- // local state; clearing here keeps the failed
117- // cancellation from looking retryable. The
118- // send is owned only if the watcher has not
119- // already claimed the session for its own
120- // terminal outcome.
121- if take_verification(&self.active).await {
122- let _ = sender
123- .send(Event::VerificationChanged(UiState::Cancelled(
124- error.to_string(),
125- )))
126- .await;
127- }
128- return Err(error);
129- }
130- // The peer can complete while the unwind cancel is in
131- // flight and the SDK call no-ops; the Done then owns the
132- // terminal outcome, so report it instead of attributing
133- // it to the send failure.
134- if sas.is_done() {
135- if take_verification(&self.active).await {
136- let _ = sender
137- .send(Event::VerificationChanged(UiState::Done))
138- .await;
139- }
140- return Err(error);
141- }
142- if let Some(info) = sas.cancel_info() {
143- // Our own unwind cancel just marked the flow; it does
144- // not own the outcome yet, so fall through and report
145- // the send failure as Cancelled below. Only a peer
146- // cancellation racing the unwind owns it instead.
147- if !info.cancelled_by_us() {
148- if take_verification(&self.active).await {
149- let _ = sender
150- .send(Event::VerificationChanged(UiState::Cancelled(
151- info.reason().to_owned(),
152- )))
153- .await;
154- }
155- return Err(Error::new(
156- ErrorKind::VerificationInactive,
157- info.reason().to_owned(),
158- ));
159- }
160- }
161- }
162- if take_verification(&self.active).await {
163- let _ = sender
164- .send(Event::VerificationChanged(UiState::Cancelled(
165- error.to_string(),
166- )))
167- .await;
168- }
169- Err(error)
170- }
171- }
172- }
173- VerificationAction::Reject => self.reject().await,
174- VerificationAction::Cancel => {
175- // The SDK no-ops a cancel of an already-settled flow, so a
176- // failed claim inside `cancel` already reported the true
177- // outcome as an error and owns no event here. Only the claim
178- // winner emits the local Cancelled.
179- let claimed = self.cancel_claimed(Some(&sender)).await?;
180- if claimed {
181- sender
182- .send(Event::VerificationChanged(UiState::Cancelled(
183- "cancelled by this client".to_owned(),
184- )))
185- .await
186- .map_err(|source| {
187- Error::with_source(
188- ErrorKind::Interrupted,
189- "event receiver has closed",
190- source,
191- )
192- })?;
193- }
194- Ok(())
195- }
196- }
197- }
198-
199- pub(crate) async fn start(
200- &mut self,
201- client: &Client,
202- sender: EventSender,
203- target: VerificationTarget,
204- ) -> Result<(), Error> {
205- client
206- .encryption()
207- .wait_for_e2ee_initialization_tasks()
208- .await;
209- let own_user_id = client.user_id().ok_or_else(|| {
210- Error::new(
211- ErrorKind::Unauthenticated,
212- "the Matrix client is not authenticated",
213- )
214- })?;
215- if self.has_active_verification().await {
216- self.cancel().await?;
217- } else {
218- self.stop_task().await;
219- }
220- let request = match &target {
221- VerificationTarget::OwnIdentity => {
222- request_identity_verification(client, own_user_id).await?
223- }
224- VerificationTarget::User(user_id) => {
225- request_identity_verification(client, user_id).await?
226- }
227- VerificationTarget::Device { user_id, device_id } => {
228- let device = client
229- .encryption()
230- .get_device(user_id, device_id)
231- .await
232- .map_err(|source| {
233- Error::with_source(
234- ErrorKind::Encryption,
235- "reading the verification device failed",
236- source,
237- )
238- })?
239- .ok_or_else(|| {
240- Error::new(
241- ErrorKind::VerificationUnsupported,
242- "the verification device is unavailable",
243- )
244- })?;
245- device
246- .request_verification_with_methods(vec![VerificationMethod::SasV1])
247- .await
248- .map_err(|source| {
249- Error::from_sdk("starting device verification failed", source)
250- })?
251- }
252- };
253- // subscribed before anything can wait on the event channel, or a quick peer's
254- // answer lands while we're blocked and nobody ever hears it
255- let changes = request.changes();
256- self.active.lock().await.request = Some(request.clone());
257- sender
258- .send(Event::VerificationTargetChanged(target))
259- .await
260- .map_err(|source| {
261- Error::with_source(ErrorKind::Interrupted, "event receiver has closed", source)
262- })?;
263- sender
264- .send(Event::VerificationChanged(UiState::Waiting))
265- .await
266- .map_err(|source| {
267- Error::with_source(ErrorKind::Interrupted, "event receiver has closed", source)
268- })?;
269- let active = self.active.clone();
270- self.task = Some(spawn(async move {
271- follow_request(request, changes, active, sender).await;
272- }));
273- Ok(())
274- }
275-
276- /// another device asked us. only taken when nothing else is in flight, and not accepted
277- /// until the user says so with `VerificationAction::Accept`
278- pub(crate) async fn adopt_incoming(
279- &mut self,
280- sender: EventSender,
281- request: VerificationRequest,
282- ) {
283- if request.we_started() || request.is_done() || request.cancel_info().is_some() {
284- return;
285- }
286- if self.has_active_verification().await {
287- tracing::debug!("ignoring a verification request while another is in flight");
288- return;
289- }
290- self.stop_task().await;
291- let changes = request.changes();
292- self.active.lock().await.request = Some(request.clone());
293- let incoming = UiState::Incoming {
294- user_id: request.other_user_id().to_owned(),
295- device_id: match request.state() {
296- VerificationRequestState::Requested {
297- other_device_data, ..
298- }
299- | VerificationRequestState::Ready {
300- other_device_data, ..
301- } => Some(other_device_data.device_id().to_owned()),
302- _ => None,
303- },
304- };
305- let target = VerificationTarget::User(request.other_user_id().to_owned());
306- for event in [
307- Event::VerificationTargetChanged(target),
308- Event::VerificationChanged(incoming),
309- ] {
310- if sender.send(event).await.is_err() {
311- return;
312- }
313- }
314- let active = self.active.clone();
315- self.task = Some(spawn(async move {
316- follow_request(request, changes, active, sender).await;
317- }));
318- }
319-
320- async fn accept(&self) -> Result<(), Error> {
321- let inactive = |message: &str| Error::new(ErrorKind::VerificationInactive, message);
322- let (request, sas) = {
323- let active = self.active.lock().await;
324- (active.request.clone(), active.sas.is_some())
325- };
326- let request = request.ok_or_else(|| inactive("no verification request is pending"))?;
327- if request.we_started() {
328- return Err(inactive("nobody asked us, we asked them"));
329- }
330- if request.is_done() {
331- return Err(inactive("verification is already complete"));
332- }
333- if let Some(info) = request.cancel_info() {
334- return Err(inactive(info.reason()));
335- }
336- if sas {
337- return Err(inactive("verification has already started"));
338- }
339- request
340- .accept_with_methods(vec![VerificationMethod::SasV1])
341- .await
342- .map_err(|source| Error::from_sdk("accepting verification request failed", source))
343- }
344-
345- async fn confirm(&self) -> Result<(), Error> {
346- let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
347- Error::new(
348- ErrorKind::VerificationInactive,
349- "verification has not reached comparison",
350- )
351- })?;
352- // Report a settled flow truthfully instead of as "not reached
353- // comparison": the SDK would no-op the confirm with success.
354- if sas.is_done() {
355- return Err(Error::new(
356- ErrorKind::VerificationInactive,
357- "verification is already complete",
358- ));
359- }
360- if let Some(info) = sas.cancel_info() {
361- return Err(Error::new(
362- ErrorKind::VerificationInactive,
363- info.reason().to_owned(),
364- ));
365- }
366- // Only the short-auth-string stage is confirmable. The SDK reports the
367- // remote-MAC-arrived state through the same presentable flag, while an
368- // early confirm would locally no-op and falsely report success.
369- if !sas.can_be_presented() {
370- return Err(Error::new(
371- ErrorKind::VerificationInactive,
372- "verification has not reached comparison",
373- ));
374- }
375- // Our own confirm can move the flow to Done, so only Done that predates
376- // the send means the SDK no-op'd on a stale session; a cancellation
377- // racing the send must not report success.
378- let was_done = sas.is_done();
379- sas.confirm()
380- .await
381- .map_err(|source| Error::from_sdk("confirming verification failed", source))?;
382- if sas.is_done() {
383- if was_done {
384- return Err(Error::new(
385- ErrorKind::VerificationInactive,
386- "verification is already complete",
387- ));
388- }
389- } else if sas.is_cancelled() {
390- let reason = sas
391- .cancel_info()
392- .map(|info| info.reason().to_owned())
393- .unwrap_or_else(|| "verification was cancelled".to_owned());
394- return Err(Error::new(ErrorKind::VerificationInactive, reason));
395- }
396- Ok(())
397- }
398-
399- async fn reject(&self) -> Result<(), Error> {
400- let sas = self.active.lock().await.sas.clone().ok_or_else(|| {
401- Error::new(
402- ErrorKind::VerificationInactive,
403- "verification has not reached comparison",
404- )
405- })?;
406- // Report a settled flow truthfully instead of as "not reached
407- // comparison": the SDK would no-op the mismatch with success.
408- if sas.is_done() {
409- return Err(Error::new(
410- ErrorKind::VerificationInactive,
411- "verification is already complete",
412- ));
413- }
414- if let Some(info) = sas.cancel_info() {
415- return Err(Error::new(
416- ErrorKind::VerificationInactive,
417- info.reason().to_owned(),
418- ));
419- }
420- // Rejecting before the comparison stage would cancel a flow the user
421- // never compared, so it requires the same presentable state.
422- if !sas.can_be_presented() {
423- return Err(Error::new(
424- ErrorKind::VerificationInactive,
425- "verification has not reached comparison",
426- ));
427- }
428- sas.mismatch()
429- .await
430- .map_err(|source| Error::from_sdk("rejecting verification failed", source))?;
431- // A peer completing the flow concurrently must not be reported as
432- // rejected. A remote cancellation racing the send turns the SDK call
433- // into a no-op success, so report it instead of a false success.
434- if sas.is_done() {
435- return Err(Error::new(
436- ErrorKind::VerificationInactive,
437- "verification is already complete",
438- ));
439- }
440- if let Some(info) = sas.cancel_info() {
441- // Our own mismatch just marked the flow cancelled-by-us; that is the
442- // expected reject path and stays Ok. Only a peer cancel reports an
443- // error.
444- if !info.cancelled_by_us() {
445- return Err(Error::new(
446- ErrorKind::VerificationInactive,
447- info.reason().to_owned(),
448- ));
449- }
450- }
451- Ok(())
452- }
453- pub(crate) async fn cancel(&mut self) -> Result<(), Error> {
454- self.cancel_claimed(None).await.map(|_| ())
455- }
456-
457- /// Return whether this action won the claim for a local cancellation.
458- /// The optional sender reports a peer terminal outcome when it races after
459- /// the claim; starting a replacement verification does not publish it.
460- async fn cancel_claimed(&mut self, sender: Option<&EventSender>) -> Result<bool, Error> {
461- let (sas, request) = {
462- let active = self.active.lock().await;
463- (active.sas.clone(), active.request.clone())
464- };
465- if sas.is_none() && request.is_none() {
466- return Err(Error::new(
467- ErrorKind::VerificationInactive,
468- "no verification is active",
469- ));
470- }
471- // The watcher owns terminal emission and claims the session when it
472- // publishes. If the SDK is already terminal, the watcher either already
473- // reported it or is about to: leave session and watcher alone so the
474- // outcome is emitted exactly once, and report it instead of letting the
475- // SDK no-op success regress Done to "cancelled by this client".
476- if let Some(sas) = sas.as_ref() {
477- if sas.is_done() {
478- return Err(Error::new(
479- ErrorKind::VerificationInactive,
480- "verification is already complete",
481- ));
482- }
483- if let Some(info) = sas.cancel_info() {
484- return Err(Error::new(
485- ErrorKind::VerificationInactive,
486- info.reason().to_owned(),
487- ));
488- }
489- } else if let Some(request) = request.as_ref() {
490- if request.is_done() {
491- return Err(Error::new(
492- ErrorKind::VerificationInactive,
493- "verification is already complete",
494- ));
495- }
496- if let Some(info) = request.cancel_info() {
497- return Err(Error::new(
498- ErrorKind::VerificationInactive,
499- info.reason().to_owned(),
500- ));
501- }
502- }
503- // Own the terminal outcome from here: claim the session before
504- // stopping the watcher so a watcher that already claimed (and will
505- // emit its own terminal event) turns this into a truthful error
506- // instead of a duplicate local cancel; no lock is held across sends.
507- if !take_verification(&self.active).await {
508- // Re-check the SDK: the claim loss may be a stale session after a
509- // terminal outcome, or a concurrent start of a new flow. Only the
510- // terminal case reports here; a live session belongs to the new
511- // flow and must not be disturbed.
512- if let Some(sas) = sas.as_ref() {
513- if sas.is_done() {
514- return Err(Error::new(
515- ErrorKind::VerificationInactive,
516- "verification is already complete",
517- ));
518- }
519- if let Some(info) = sas.cancel_info() {
520- return Err(Error::new(
521- ErrorKind::VerificationInactive,
522- info.reason().to_owned(),
523- ));
524- }
525- } else if let Some(request) = request.as_ref() {
526- if request.is_done() {
527- return Err(Error::new(
528- ErrorKind::VerificationInactive,
529- "verification is already complete",
530- ));
531- }
532- if let Some(info) = request.cancel_info() {
533- return Err(Error::new(
534- ErrorKind::VerificationInactive,
535- info.reason().to_owned(),
536- ));
537- }
538- }
539- return Err(Error::new(
540- ErrorKind::VerificationInactive,
541- "no verification is active",
542- ));
543- }
544- self.stop_task().await;
545- if let Some(sas) = sas {
546- let result = sas.cancel().await;
547- if sas.is_done() {
548- report_claimed_terminal(sender, UiState::Done).await;
549- return Err(Error::new(
550- ErrorKind::VerificationInactive,
551- "verification is already complete",
552- ));
553- }
554- if let Some(info) = sas.cancel_info()
555- && !info.cancelled_by_us()
556- {
557- let reason = info.reason().to_owned();
558- report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
559- return Err(Error::new(ErrorKind::VerificationInactive, reason));
560- }
561- if let Err(source) = result {
562- let error = Error::from_sdk("cancelling verification failed", source);
563- report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
564- return Err(error);
565- }
566- Ok(true)
567- } else if let Some(request) = request {
568- let result = request.cancel().await;
569- if request.is_done() {
570- report_claimed_terminal(sender, UiState::Done).await;
571- return Err(Error::new(
572- ErrorKind::VerificationInactive,
573- "verification is already complete",
574- ));
575- }
576- if let Some(info) = request.cancel_info()
577- && !info.cancelled_by_us()
578- {
579- let reason = info.reason().to_owned();
580- report_claimed_terminal(sender, UiState::Cancelled(reason.clone())).await;
581- return Err(Error::new(ErrorKind::VerificationInactive, reason));
582- }
583- if let Err(source) = result {
584- let error = Error::from_sdk("cancelling verification failed", source);
585- report_claimed_terminal(sender, UiState::Cancelled(error.to_string())).await;
586- return Err(error);
587- }
588- Ok(true)
589- } else {
590- Ok(false)
591- }
592- }
593-
594- async fn has_active_verification(&self) -> bool {
595- let active = self.active.lock().await;
596- active.request.is_some() || active.sas.is_some()
597- }
598-
599- async fn stop_task(&mut self) {
600- if let Some(task) = self.task.take() {
601- task.abort();
602- let _ = task.await;
603- }
604- }
605-}
606-
607-/// shutdown forgets the flow without telling the sdk, a network send could hang it
608-impl Drop for VerificationController {
609- fn drop(&mut self) {
610- if let Some(task) = self.task.take() {
611- task.abort();
612- }
613- }
614-}
615-
616-async fn request_identity_verification(
617- client: &Client,
618- user_id: &UserId,
619-) -> Result<VerificationRequest, Error> {
620- let identity = client
621- .encryption()
622- .request_user_identity(user_id)
623- .await
624- .map_err(|source| Error::from_sdk("requesting the user identity failed", source))?
625- .ok_or_else(|| {
626- Error::new(
627- ErrorKind::NoCrossSigningIdentity,
628- "the user has no cross-signing identity",
629- )
630- })?;
631- identity
632- .request_verification_with_methods(vec![VerificationMethod::SasV1])
633- .await
634- .map_err(|source| Error::from_verification_request("starting verification failed", source))
635-}
636-
637-/// `changes` is subscribed by the caller before the request could move, and the current state
638-/// is handled first, since the stream only reports what happens after it was taken
639-async fn follow_request(
640- request: VerificationRequest,
641- changes: impl futures_util::Stream<Item = VerificationRequestState> + Unpin,
642- active: Arc<Mutex<VerificationSession>>,
643- sender: EventSender,
644-) {
645- let mut states = futures_util::stream::iter([request.state()]).chain(changes);
646- while let Some(state) = states.next().await {
647- match state {
648- // created is ours waiting on them, requested is theirs waiting on `Accept`
649- VerificationRequestState::Created { .. }
650- | VerificationRequestState::Requested { .. } => {}
651- VerificationRequestState::Ready { .. } => match request.start_sas().await {
652- Ok(Some(sas)) => {
653- follow_sas(sas, active.clone(), sender.clone()).await;
654- return;
655- }
656- Ok(None) => {}
657- Err(error) => {
658- tracing::warn!(error = %error, "starting SAS verification failed");
659- // Claim the session so this terminal outcome is emitted exactly
660- // once; a lost claim means an action path already owns it.
661- report_watcher_terminal(
662- &active,
663- &sender,
664- UiState::Failed(VerificationFailure::SasStartup(error.to_string())),
665- "SAS startup failure",
666- )
667- .await;
668- return;
669- }
670- },
671- VerificationRequestState::Transitioned { verification } => {
672- if let Verification::SasV1(sas) = verification {
673- follow_sas(sas, active.clone(), sender.clone()).await;
674- } else {
675- report_watcher_terminal(
676- &active,
677- &sender,
678- UiState::Cancelled(
679- "the other device selected an unsupported method".into(),
680- ),
681- "unsupported verification method",
682- )
683- .await;
684- }
685- return;
686- }
687- VerificationRequestState::Done => {
688- report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
689- return;
690- }
691- VerificationRequestState::Cancelled(info) => {
692- report_watcher_terminal(
693- &active,
694- &sender,
695- UiState::Cancelled(info.reason().to_owned()),
696- "cancellation",
697- )
698- .await;
699- return;
700- }
701- }
702- }
703- tracing::debug!("verification request watcher ended unexpectedly");
704- // The stream may end after the SDK already settled without delivering the
705- // terminal update; report the settled outcome instead of StreamEnded, and
706- // stay silent if an action path already claimed the session.
707- if request.is_done() {
708- report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
709- } else if let Some(info) = request.cancel_info() {
710- let reason = info.reason().to_owned();
711- report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
712- } else {
713- report_watcher_terminal(
714- &active,
715- &sender,
716- UiState::Failed(VerificationFailure::RequestStreamEnded),
717- "request stream termination",
718- )
719- .await;
720- }
721-}
722-
723-async fn follow_sas(
724- sas: SasVerification,
725- active: Arc<Mutex<VerificationSession>>,
726- sender: EventSender,
727-) {
728- // the sdk may already have moved the flow along before we got here, a peer starting
729- // sas is the usual case, so take the stream first and the current state after
730- let changes = sas.changes();
731- active.lock().await.sas = Some(sas.clone());
732- let mut states = futures_util::stream::iter([sas.state()]).chain(changes);
733- while let Some(state) = states.next().await {
734- let update = match state {
735- SasState::Created { .. } | SasState::Accepted { .. } => continue,
736- SasState::Started { .. } => {
737- if let Err(error) = sas.accept().await {
738- tracing::warn!(error = %error, "accepting SAS verification failed");
739- // Claim the session so this terminal outcome is emitted exactly
740- // once; a lost claim means an action path already owns it.
741- report_watcher_terminal(
742- &active,
743- &sender,
744- UiState::Failed(VerificationFailure::SasAcceptance(error.to_string())),
745- "SAS acceptance failure",
746- )
747- .await;
748- return;
749- }
750- continue;
751- }
752- SasState::KeysExchanged { emojis, decimals } => UiState::Comparison {
753- emojis: emojis
754- .map(|short_auth| {
755- short_auth
756- .emojis
757- .into_iter()
758- .map(|emoji| (emoji.symbol.to_owned(), emoji.description.to_owned()))
759- .collect()
760- })
761- .unwrap_or_default(),
762- decimals,
763- },
764- SasState::Confirmed => UiState::Confirmed,
765- SasState::Done { .. } => UiState::Done,
766- SasState::Cancelled(info) => UiState::Cancelled(info.reason().to_owned()),
767- };
768- let terminal = matches!(update, UiState::Done | UiState::Cancelled(_));
769- // Claim the session before the terminal send so a racing Cancel observes
770- // no active session and the SDK no-op cannot regress Done back to
771- // "cancelled by this client". A lost claim means an action path already
772- // owns the terminal outcome, so stay silent instead of duplicating it.
773- if terminal && !take_verification(&active).await {
774- return;
775- }
776- if sender
777- .send(Event::VerificationChanged(update))
778- .await
779- .is_err()
780- {
781- tracing::debug!("SAS watcher stopped because the event receiver closed");
782- return;
783- }
784- if terminal {
785- return;
786- }
787- }
788- tracing::debug!("SAS watcher ended unexpectedly");
789- // The stream may end after the SDK already settled without delivering the
790- // terminal update; report the settled outcome instead of StreamEnded, and
791- // stay silent if an action path already claimed the session.
792- if sas.is_done() {
793- report_watcher_terminal(&active, &sender, UiState::Done, "completion").await;
794- } else if let Some(info) = sas.cancel_info() {
795- let reason = info.reason().to_owned();
796- report_watcher_terminal(&active, &sender, UiState::Cancelled(reason), "cancellation").await;
797- } else {
798- report_watcher_terminal(
799- &active,
800- &sender,
801- UiState::Failed(VerificationFailure::SasStreamEnded),
802- "SAS stream termination",
803- )
804- .await;
805- }
806-}
807-
808-async fn report_watcher_terminal(
809- active: &Arc<Mutex<VerificationSession>>,
810- sender: &EventSender,
811- state: UiState,
812- context: &'static str,
813-) {
814- if take_verification(active).await
815- && sender
816- .send(Event::VerificationChanged(state))
817- .await
818- .is_err()
819- {
820- tracing::debug!(
821- context,
822- "verification watcher could not report terminal outcome"
823- );
824- }
825-}
826-
827-async fn report_claimed_terminal(sender: Option<&EventSender>, state: UiState) {
828- if let Some(sender) = sender {
829- let _ = sender.send(Event::VerificationChanged(state)).await;
830- }
831-}
832-
833-/// Claim the active session for terminal emission, if one is still present.
834-///
835-/// Exactly one party (watcher or action path) wins the claim and may emit the
836-/// terminal event; the loser stays silent so Done is never overwritten by a
837-/// later local Cancelled/Failed.
838-async fn take_verification(active: &Arc<Mutex<VerificationSession>>) -> bool {
839- let mut active = active.lock().await;
840- let had = active.sas.is_some() || active.request.is_some();
841- active.sas.take();
842- active.request.take();
843- had
844-}
845-
846-#[cfg(test)]
847-mod tests {
848- use matrix_sdk::encryption::VerificationState;
849- use matrix_sdk::encryption::backups::BackupState;
850- use ruma::owned_device_id;
851-
852- use super::*;
853-
854- #[test]
855- fn sdk_security_state_is_normalized() {
856- assert_eq!(
857- encryption_state(
858- owned_device_id!("DEVICE"),
859- SdkRecoveryState::Incomplete,
860- VerificationState::Unverified,
861- BackupState::Unknown,
862- ),
863- EncryptionState {
864- device_id: owned_device_id!("DEVICE"),
865- device_verified: false,
866- recovery: RecoveryState::Incomplete,
867- key_backup: KeyBackupState::Unknown,
868- }
869- );
870- assert_eq!(
871- encryption_state(
872- owned_device_id!("DEVICE"),
873- SdkRecoveryState::Enabled,
874- VerificationState::Verified,
875- BackupState::Enabled,
876- ),
877- EncryptionState {
878- device_id: owned_device_id!("DEVICE"),
879- device_verified: true,
880- recovery: RecoveryState::Enabled,
881- key_backup: KeyBackupState::Enabled,
882- }
883- );
884- }
885-
886- #[tokio::test]
887- async fn comparison_actions_require_an_active_sas() {
888- let mut controller = VerificationController::default();
889-
890- assert_eq!(
891- controller.confirm().await.unwrap_err().to_string(),
892- "verification has not reached comparison"
893- );
894- assert_eq!(
895- controller.reject().await.unwrap_err().to_string(),
896- "verification has not reached comparison"
897- );
898- assert_eq!(
899- controller.cancel().await.unwrap_err().to_string(),
900- "no verification is active"
901- );
902- }
903-}
Deleted file: vendor/macltok/src/engine/commands.rs−329 deletions
@@ -1,329 +0,0 @@
1-use std::{future::Future, panic::AssertUnwindSafe};
2-
3-use futures_util::FutureExt;
4-use matrix_sdk::encryption::verification::VerificationRequest;
5-use matrix_sdk::{
6- Client,
7- notification_settings::{IsEncrypted, IsOneToOne, RoomNotificationMode as SdkMode},
8- ruma::{
9- OwnedDeviceId, OwnedEventId, RoomId, TransactionId,
10- api::client::receipt::create_receipt::v3::ReceiptType, events::receipt::ReceiptThread,
11- },
12- send_queue::{LocalEchoContent, SendHandle},
13-};
14-use tokio::sync::mpsc;
15-
16-use crate::{
17- EncryptionCommand, Error, ErrorKind, InteractiveAuth, InteractiveAuthChallenge,
18- NotificationCommand, RoomNotificationMode, TimelineId,
19- encryption::VerificationController,
20- transport::{EventSender, Responder},
21-};
22-
23-pub(super) struct Job<T> {
24- pub(super) command: T,
25- pub(super) reply: Responder,
26-}
27-
28-/// a panicking command answers with an error instead of taking its worker down with it
29-pub(super) async fn guarded<T>(
30- label: &'static str,
31- work: impl Future<Output = Result<T, Error>>,
32-) -> Result<T, Error> {
33- AssertUnwindSafe(work)
34- .catch_unwind()
35- .await
36- .unwrap_or_else(|_| Err(Error::new(ErrorKind::Other, format!("{label} panicked"))))
37-}
38-
39-pub(super) async fn run_notifications(
40- client: Client,
41- mut jobs: mpsc::Receiver<Job<NotificationCommand>>,
42-) {
43- while let Some(Job { command, reply }) = jobs.recv().await {
44- reply.done(guarded("notification command", notification(&client, command)).await);
45- }
46-}
47-
48-pub(super) async fn run_encryption(
49- client: Client,
50- events: EventSender,
51- mut jobs: mpsc::Receiver<Job<EncryptionCommand>>,
52- mut incoming: mpsc::UnboundedReceiver<VerificationRequest>,
53-) {
54- let mut verification = VerificationController::default();
55- loop {
56- tokio::select! {
57- job = jobs.recv() => {
58- let Some(Job { command, reply }) = job else { return };
59- let work = encryption(&client, &events, &mut verification, command);
60- reply.done(guarded("encryption command", work).await);
61- }
62- Some(request) = incoming.recv() => {
63- verification.adopt_incoming(events.clone(), request).await;
64- }
65- }
66- }
67-}
68-
69-async fn encryption(
70- client: &Client,
71- events: &EventSender,
72- verification: &mut VerificationController,
73- command: EncryptionCommand,
74-) -> Result<(), Error> {
75- let encryption = client.encryption();
76- match command {
77- EncryptionCommand::Recover(key) => encryption
78- .recovery()
79- .recover(&key)
80- .await
81- .map_err(|source| Error::from_recovery("account recovery failed", source)),
82- EncryptionCommand::RecoverKeyBackup(key) => encryption
83- .recovery()
84- .recover_and_fix_backup(&key)
85- .await
86- .map_err(|source| Error::from_recovery("recovering key backup failed", source)),
87- EncryptionCommand::CreateKeyBackup => encryption
88- .backups()
89- .create()
90- .await
91- .map_err(|source| Error::from_sdk("creating key backup failed", source)),
92- EncryptionCommand::EnableKeyBackup => encryption
93- .recovery()
94- .enable_backup()
95- .await
96- .map_err(|source| Error::from_recovery("enabling key backup failed", source)),
97- EncryptionCommand::DisableKeyBackup { delete_from_server } => {
98- let backups = encryption.backups();
99- if delete_from_server {
100- backups.disable_and_delete().await
101- } else {
102- backups.disable().await
103- }
104- .map_err(|source| Error::from_sdk("disabling key backup failed", source))
105- }
106- EncryptionCommand::StartVerification(target) => {
107- verification.start(client, events.clone(), target).await
108- }
109- EncryptionCommand::Verify(action) => verification.apply(events.clone(), action).await,
110- }
111-}
112-
113-async fn notification(client: &Client, command: NotificationCommand) -> Result<(), Error> {
114- let settings = client.notification_settings().await;
115- match command {
116- NotificationCommand::SetRoomMode { room_id, mode } => {
117- settings
118- .set_room_notification_mode(&room_id, sdk_mode(mode))
119- .await
120- }
121- NotificationCommand::ResetRoomMode(room_id) => {
122- settings.delete_user_defined_room_rules(&room_id).await
123- }
124- NotificationCommand::SetDefaultMode {
125- encrypted,
126- one_to_one,
127- mode,
128- } => {
129- settings
130- .set_default_room_notification_mode(
131- IsEncrypted::from(encrypted),
132- IsOneToOne::from(one_to_one),
133- sdk_mode(mode),
134- )
135- .await
136- }
137- NotificationCommand::AddKeyword(keyword) => settings.add_keyword(keyword).await,
138- NotificationCommand::RemoveKeyword(keyword) => settings.remove_keyword(&keyword).await,
139- }
140- .map_err(|source| {
141- Error::with_source(
142- ErrorKind::Server,
143- "updating notification settings failed",
144- source,
145- )
146- })
147-}
148-
149-const fn sdk_mode(mode: RoomNotificationMode) -> SdkMode {
150- match mode {
151- RoomNotificationMode::AllMessages => SdkMode::AllMessages,
152- RoomNotificationMode::MentionsAndKeywordsOnly => SdkMode::MentionsAndKeywordsOnly,
153- RoomNotificationMode::Mute => SdkMode::Mute,
154- }
155-}
156-
157-fn room(client: &Client, room_id: &RoomId) -> Result<matrix_sdk::Room, Error> {
158- client.get_room(room_id).ok_or_else(Error::room_unknown)
159-}
160-
161-/// the closed-room path. without a timeline nothing stops the marker going backwards
162-pub(super) async fn mark_read(
163- client: &Client,
164- timeline_id: &TimelineId,
165- event_id: OwnedEventId,
166-) -> Result<(), Error> {
167- let room = room(client, timeline_id.room_id())?;
168- let receipt_failed = |source| Error::from_sdk("sending read receipt failed", source);
169- let thread = match timeline_id.thread_root() {
170- None => ReceiptThread::Main,
171- Some(root) => ReceiptThread::Thread(root.clone()),
172- };
173- room.send_single_receipt(ReceiptType::Read, thread, event_id.clone())
174- .await
175- .map_err(receipt_failed)?;
176- if timeline_id.thread_root().is_none() {
177- room.send_single_receipt(ReceiptType::FullyRead, ReceiptThread::Unthreaded, event_id)
178- .await
179- .map_err(receipt_failed)?;
180- }
181- Ok(())
182-}
183-
184-pub(super) async fn set_typing(
185- client: &Client,
186- room_id: &RoomId,
187- typing: bool,
188-) -> Result<(), Error> {
189- room(client, room_id)?
190- .typing_notice(typing)
191- .await
192- .map_err(|source| Error::from_sdk("typing update failed", source))
193-}
194-
195-async fn send_handle(
196- client: &Client,
197- room_id: &RoomId,
198- transaction_id: &TransactionId,
199-) -> Result<SendHandle, Error> {
200- let (echoes, _) = room(client, room_id)?
201- .send_queue()
202- .subscribe()
203- .await
204- .map_err(|source| {
205- Error::with_source(ErrorKind::Store, "reading send queue failed", source)
206- })?;
207- echoes
208- .into_iter()
209- .find_map(|echo| match echo.content {
210- LocalEchoContent::Event { send_handle, .. }
211- if echo.transaction_id.as_str() == transaction_id.as_str() =>
212- {
213- Some(send_handle)
214- }
215- _ => None,
216- })
217- .ok_or_else(|| {
218- Error::new(
219- ErrorKind::Invalid,
220- "send is no longer pending or is not a message",
221- )
222- })
223-}
224-
225-pub(super) async fn cancel_send(
226- client: &Client,
227- room_id: &RoomId,
228- transaction_id: &TransactionId,
229-) -> Result<(), Error> {
230- match send_handle(client, room_id, transaction_id)
231- .await?
232- .abort()
233- .await
234- {
235- Ok(true) => Ok(()),
236- Ok(false) => Err(Error::new(
237- ErrorKind::Invalid,
238- "send could not be cancelled because it has already been sent",
239- )),
240- Err(source) => Err(Error::with_source(
241- ErrorKind::Store,
242- "cancelling send failed",
243- source,
244- )),
245- }
246-}
247-
248-pub(super) async fn retry_send(
249- client: &Client,
250- room_id: &RoomId,
251- transaction_id: &TransactionId,
252-) -> Result<(), Error> {
253- send_handle(client, room_id, transaction_id)
254- .await?
255- .unwedge()
256- .await
257- .map_err(|source| Error::with_source(ErrorKind::Store, "retrying send failed", source))?;
258- room(client, room_id)?.send_queue().set_enabled(true);
259- Ok(())
260-}
261-
262-pub(super) async fn upload_avatar(
263- client: &Client,
264- mimetype: &str,
265- bytes: Vec<u8>,
266- max_bytes: usize,
267-) -> Result<(), Error> {
268- if bytes.len() > max_bytes {
269- return Err(Error::new(
270- ErrorKind::MediaTooLarge,
271- "avatar exceeds the configured byte limit",
272- ));
273- }
274- let mimetype: mime::Mime = mimetype
275- .parse()
276- .map_err(|_| Error::new(ErrorKind::Invalid, "avatar MIME type is invalid"))?;
277- if mimetype.type_() != mime::IMAGE {
278- return Err(Error::new(
279- ErrorKind::Invalid,
280- "an avatar has to be an image",
281- ));
282- }
283- client
284- .account()
285- .upload_avatar(&mimetype, bytes)
286- .await
287- .map(drop)
288- .map_err(|source| Error::from_sdk("uploading avatar failed", source))
289-}
290-
291-pub(super) async fn rename_device(
292- client: &Client,
293- device_id: &OwnedDeviceId,
294- display_name: &str,
295-) -> Result<(), Error> {
296- client
297- .rename_device(device_id, display_name)
298- .await
299- .map(drop)
300- .map_err(|source| Error::from_http("renaming device failed", source))
301-}
302-
303-pub(super) async fn delete_devices(
304- client: &Client,
305- device_ids: &[OwnedDeviceId],
306- auth: Option<InteractiveAuth>,
307-) -> Result<(), Error> {
308- let user_id = client
309- .user_id()
310- .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?;
311- let auth = auth
312- .map(|auth| auth.into_sdk(user_id))
313- .transpose()
314- .map_err(|message| {
315- Error::new(
316- ErrorKind::Invalid,
317- format!("interactive authentication data is invalid, {message}"),
318- )
319- })?;
320- match client.delete_devices(device_ids, auth).await {
321- Ok(_) => Ok(()),
322- Err(source) => match source.as_uiaa_response() {
323- Some(info) => Err(Error::interactive_auth(InteractiveAuthChallenge::from(
324- info,
325- ))),
326- None => Err(Error::from_http("deleting devices failed", source)),
327- },
328- }
329-}
Deleted file: vendor/macltok/src/engine/executor.rs−82 deletions
@@ -1,82 +0,0 @@
1-use std::future::Future;
2-
3-use futures_util::{StreamExt, stream::FuturesUnordered};
4-use matrix_sdk::SendOutsideWasm;
5-pub(super) use matrix_sdk::executor::{AbortHandle, JoinError, JoinHandle, spawn};
6-
7-#[cfg(not(target_family = "wasm"))]
8-pub(crate) type EngineBoxFuture<'a, T> = futures_util::future::BoxFuture<'a, T>;
9-#[cfg(target_family = "wasm")]
10-pub(crate) type EngineBoxFuture<'a, T> = futures_util::future::LocalBoxFuture<'a, T>;
11-
12-#[cfg(not(target_family = "wasm"))]
13-pub(super) fn is_panic(error: &JoinError) -> bool {
14- error.is_panic()
15-}
16-
17-/// the sdk's wasm join error has no payload, and a real panic traps before it gets here
18-#[cfg(target_family = "wasm")]
19-pub(super) fn is_panic(error: &JoinError) -> bool {
20- !error.is_cancelled()
21-}
22-
23-#[cfg(not(target_family = "wasm"))]
24-pub(super) fn resume_unwind(error: JoinError) -> ! {
25- std::panic::resume_unwind(error.into_panic())
26-}
27-
28-#[cfg(target_family = "wasm")]
29-pub(super) fn resume_unwind(error: JoinError) -> ! {
30- panic!("{error}")
31-}
32-
33-/// a `JoinSet` that runs on wasm too, and like it aborts whatever's left when dropped
34-pub(super) struct TaskSet<T: 'static> {
35- handles: FuturesUnordered<JoinHandle<T>>,
36-}
37-
38-impl<T: 'static> TaskSet<T> {
39- pub(super) fn new() -> Self {
40- Self {
41- handles: FuturesUnordered::new(),
42- }
43- }
44-
45- pub(super) fn spawn(
46- &mut self,
47- task: impl Future<Output = T> + SendOutsideWasm + 'static,
48- ) -> AbortHandle
49- where
50- T: SendOutsideWasm,
51- {
52- let handle = spawn(task);
53- let abort = handle.abort_handle();
54- self.handles.push(handle);
55- abort
56- }
57-
58- pub(super) fn is_empty(&self) -> bool {
59- self.handles.is_empty()
60- }
61-
62- pub(super) async fn join_next(&mut self) -> Option<Result<T, JoinError>> {
63- self.handles.next().await
64- }
65-
66- fn abort_all(&self) {
67- for handle in &self.handles {
68- handle.abort();
69- }
70- }
71-
72- pub(super) async fn shutdown(&mut self) {
73- self.abort_all();
74- while self.handles.next().await.is_some() {}
75- }
76-}
77-
78-impl<T: 'static> Drop for TaskSet<T> {
79- fn drop(&mut self) {
80- self.abort_all();
81- }
82-}
Deleted file: vendor/macltok/src/engine/media.rs−743 deletions
@@ -1,743 +0,0 @@
1-use std::{
2- borrow::Cow,
3- collections::{HashMap, VecDeque},
4- io::Read as _,
5- panic::AssertUnwindSafe,
6- sync::Arc,
7- time::Duration,
8-};
9-
10-use futures_util::{
11- FutureExt, StreamExt,
12- future::{Shared, WeakShared},
13-};
14-use matrix_sdk::{
15- Client, Room, SendOutsideWasm, SyncOutsideWasm,
16- media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
17- room::reply::{EnforceThread, Reply as SdkReply},
18- ruma::{
19- api::{
20- Metadata, OutgoingRequest,
21- auth_scheme::SendAccessToken,
22- client::{authenticated_media, media},
23- },
24- events::room::{
25- EncryptedFileHash, EncryptedFileHashAlgorithm, EncryptedFileInfo, MediaSource,
26- message::{AddMentions, ReplyWithinThread},
27- },
28- },
29-};
30-use matrix_sdk_crypto::{AttachmentDecryptor, MediaEncryptionInfo};
31-use ruma::MxcUri;
32-use tokio::sync::Mutex;
33-
34-use super::{executor::EngineBoxFuture, url_preview::UrlPreviews};
35-use crate::{
36- AttachmentEncryption, AttachmentUpload, Error, ErrorKind, LinkPreview, Media, MediaFile,
37- MediaId, MediaKind, MediaLocation, MediaSize, TimelineId,
38- normalise::{MediaRegistration, MediaScope, MediaSourceInfo},
39- upload::prepare_upload,
40-};
41-
42-/// a stalled download gives up after this long without a byte
43-const DOWNLOAD_IDLE: Duration = Duration::from_secs(60);
44-/// the sdk client gives up on any request after thirty seconds, far too soon for a video
45-const DOWNLOAD_TOTAL: Duration = Duration::from_secs(60 * 60);
46-
47-type FlightFuture = EngineBoxFuture<'static, Result<Media, Error>>;
48-type Flight = Shared<FlightFuture>;
49-
50-/// owns everything needed to turn a media id or an mxc uri into bytes, so the
51-/// handle can answer media queries without a trip through the engine
52-pub(crate) struct MediaService {
53- client: Client,
54- registry: Mutex<MediaRegistry>,
55- flights: Mutex<HashMap<String, WeakShared<FlightFuture>>>,
56- max_bytes: usize,
57- url_previews: UrlPreviews,
58-}
59-
60-impl MediaService {
61- pub(crate) fn new(client: Client, max_bytes: usize, max_sources: usize) -> Self {
62- Self {
63- url_previews: UrlPreviews::new(client.clone()),
64- client,
65- registry: Mutex::new(MediaRegistry::new(max_sources)),
66- flights: Mutex::new(HashMap::new()),
67- max_bytes,
68- }
69- }
70-
71- pub(crate) async fn url_preview(&self, url: &str) -> Result<Option<LinkPreview>, Error> {
72- self.url_previews.preview(url).await
73- }
74-
75- pub(crate) async fn register(&self, media: Vec<MediaRegistration>) {
76- if !media.is_empty() {
77- self.registry.lock().await.insert_all(media);
78- }
79- }
80-
81- pub(crate) async fn release(&self, scope: &MediaScope) {
82- self.registry.lock().await.release(scope);
83- }
84-
85- pub(crate) async fn clear(&self) {
86- self.registry.lock().await.clear();
87- }
88-
89- pub(crate) async fn timeline_media(
90- &self,
91- media_id: &MediaId,
92- size: MediaSize,
93- ) -> Result<Media, Error> {
94- let chosen = choose_source(self.registered(media_id).await?, size)?;
95- if chosen
96- .declared_size
97- .is_some_and(|size| size > self.max_bytes as u64)
98- {
99- return Err(too_large("media exceeds the configured byte limit"));
100- }
101- self.fetch_or_file(chosen.request, chosen.mimetype).await
102- }
103-
104- pub(crate) async fn media_location(
105- &self,
106- media_id: &MediaId,
107- size: MediaSize,
108- ) -> Result<MediaLocation, Error> {
109- let media = self.registered(media_id).await?;
110- let chosen = choose_source(media.clone(), size)?;
111- if is_local(&chosen.request.source) {
112- return Ok(MediaLocation::Local);
113- }
114- if !matches!(chosen.request.format, MediaFormat::Thumbnail(_)) {
115- return Ok(MediaLocation::File(
116- media_file(
117- &self.client,
118- chosen.request,
119- chosen.mimetype,
120- chosen.declared_size,
121- )
122- .await?,
123- ));
124- }
125- let url = media_http_request(&self.client, &chosen.request)
126- .await?
127- .uri()
128- .to_string();
129- let fallback = MediaRequestParameters {
130- source: media.full.source,
131- format: MediaFormat::File,
132- };
133- Ok(MediaLocation::ServerThumbnail {
134- url,
135- fallback: media_file(
136- &self.client,
137- fallback,
138- media.full.mimetype,
139- media.full.declared_size,
140- )
141- .await?,
142- })
143- }
144-
145- async fn registered(&self, media_id: &MediaId) -> Result<MediaRegistration, Error> {
146- if let Some(uri) = media_id.as_plain() {
147- return plain_registration(media_id, uri);
148- }
149- self.registry
150- .lock()
151- .await
152- .get(media_id)
153- .cloned()
154- .ok_or_else(|| {
155- Error::new(
156- ErrorKind::MediaUnknown,
157- "media is not registered in a retained timeline",
158- )
159- })
160- }
161-
162- /// plenty of servers won't thumbnail everything, and the file itself is the next best
163- /// thing. the byte cap still holds either way
164- async fn fetch_or_file(
165- &self,
166- request: MediaRequestParameters,
167- mimetype: Option<String>,
168- ) -> Result<Media, Error> {
169- if !matches!(request.format, MediaFormat::Thumbnail(_)) {
170- return self.fetch(request, mimetype).await;
171- }
172- match self.fetch(request.clone(), mimetype.clone()).await {
173- Err(error) if error.kind == ErrorKind::MediaUnavailable => {
174- let file = MediaRequestParameters {
175- source: request.source,
176- format: MediaFormat::File,
177- };
178- self.fetch(file, mimetype).await
179- }
180- result => result,
181- }
182- }
183-
184- async fn fetch(
185- &self,
186- request: MediaRequestParameters,
187- mimetype: Option<String>,
188- ) -> Result<Media, Error> {
189- let key = flight_key(&request);
190- let flight = {
191- let mut flights = self.flights.lock().await;
192- flights.retain(|_, flight| flight.upgrade().is_some());
193- match flights.get(&key).and_then(WeakShared::upgrade) {
194- Some(flight) => flight,
195- None => {
196- let prepared = AssertUnwindSafe(fetch(
197- self.client.clone(),
198- request,
199- mimetype,
200- self.max_bytes,
201- ))
202- .catch_unwind()
203- .map(|result| {
204- result.unwrap_or_else(|_| {
205- Err(Error::new(ErrorKind::Other, "media fetch panicked"))
206- })
207- });
208- let boxed: FlightFuture = Box::pin(prepared);
209- let flight: Flight = boxed.shared();
210- if let Some(weak) = flight.downgrade() {
211- flights.insert(key, weak);
212- }
213- flight
214- }
215- }
216- };
217- flight.await
218- }
219-}
220-
221-struct ChosenSource {
222- request: MediaRequestParameters,
223- mimetype: Option<String>,
224- declared_size: Option<u64>,
225-}
226-
227-fn plain_registration(media_id: &MediaId, uri: &MxcUri) -> Result<MediaRegistration, Error> {
228- if !uri.is_valid() {
229- return Err(Error::new(ErrorKind::Invalid, "mxc uri is malformed"));
230- }
231- Ok(MediaRegistration {
232- id: media_id.clone(),
233- kind: MediaKind::Image,
234- full: MediaSourceInfo {
235- source: MediaSource::Plain(uri.to_owned()),
236- mimetype: None,
237- declared_size: None,
238- },
239- thumbnail: None,
240- })
241-}
242-
243-fn choose_source(media: MediaRegistration, size: MediaSize) -> Result<ChosenSource, Error> {
244- let (source, format, mimetype, declared_size) = match size {
245- MediaSize::Full => (
246- media.full.source,
247- MediaFormat::File,
248- media.full.mimetype,
249- media.full.declared_size,
250- ),
251- MediaSize::Thumbnail { width, height } => match media.thumbnail {
252- Some(thumbnail) => (
253- thumbnail.source,
254- MediaFormat::File,
255- thumbnail.mimetype,
256- thumbnail.declared_size,
257- ),
258- // the server can only resize what it can read
259- None if matches!(media.kind, MediaKind::Image | MediaKind::Video)
260- && matches!(media.full.source, MediaSource::Plain(_)) =>
261- {
262- // the server picks the thumbnail's type, so the original's would be a lie
263- let format = if is_local(&media.full.source) {
264- MediaFormat::File
265- } else {
266- thumbnail_format(width, height)
267- };
268- (media.full.source, format, None, None)
269- }
270- None => {
271- return Err(Error::new(
272- ErrorKind::MediaUnavailable,
273- "the requested media size is unavailable",
274- ));
275- }
276- },
277- };
278- Ok(ChosenSource {
279- request: MediaRequestParameters { source, format },
280- mimetype,
281- declared_size,
282- })
283-}
284-
285-async fn media_file(
286- client: &Client,
287- request: MediaRequestParameters,
288- mimetype: Option<String>,
289- declared_size: Option<u64>,
290-) -> Result<MediaFile, Error> {
291- let url = media_http_request(client, &request)
292- .await?
293- .uri()
294- .to_string();
295- let encryption = match &request.source {
296- MediaSource::Plain(_) => None,
297- MediaSource::Encrypted(file) => {
298- let (EncryptedFileInfo::V2(info), Some(EncryptedFileHash::Sha256(sha256))) = (
299- &file.info,
300- file.hashes.get(&EncryptedFileHashAlgorithm::Sha256),
301- ) else {
302- return Err(Error::new(
303- ErrorKind::MediaUnavailable,
304- "encrypted media is not v2 with a sha256 hash",
305- ));
306- };
307- Some(AttachmentEncryption {
308- key: info.k.encode(),
309- iv: info.iv.encode(),
310- sha256: sha256.encode(),
311- })
312- }
313- };
314- Ok(MediaFile {
315- url,
316- mimetype,
317- declared_size,
318- encryption,
319- })
320-}
321-
322-fn thumbnail_format(width: u32, height: u32) -> MediaFormat {
323- MediaFormat::Thumbnail(MediaThumbnailSettings::new(width.into(), height.into()))
324-}
325-
326-fn is_local(source: &MediaSource) -> bool {
327- let uri = match source {
328- MediaSource::Plain(uri) => uri,
329- MediaSource::Encrypted(file) => &file.url,
330- };
331- uri.server_name()
332- .is_ok_and(|server| server.as_str() == "send-queue.localhost")
333-}
334-
335-/// plain and encrypted sources share a cache key in the sdk, but must never share a flight
336-fn flight_key(request: &MediaRequestParameters) -> String {
337- let uri = match &request.source {
338- MediaSource::Plain(uri) => format!("plain|{uri}"),
339- MediaSource::Encrypted(file) => format!("encrypted|{}", file.url),
340- };
341- match &request.format {
342- MediaFormat::File => uri.to_owned(),
343- MediaFormat::Thumbnail(settings) => format!(
344- "{uri}|{}|{}x{}|{}",
345- settings.method, settings.width, settings.height, settings.animated
346- ),
347- }
348-}
349-
350-fn too_large(message: &'static str) -> Error {
351- Error::new(ErrorKind::MediaTooLarge, message)
352-}
353-
354-async fn fetch(
355- client: Client,
356- request: MediaRequestParameters,
357- mimetype: Option<String>,
358- max_bytes: usize,
359-) -> Result<Media, Error> {
360- if let Some(cached) = cached_media(&client, &request).await? {
361- if cached.len() > max_bytes {
362- if let Ok(store) = client.media_store().lock().await {
363- let _ = store.remove_media_content(&request).await;
364- }
365- return Err(too_large("cached media exceeds the configured byte limit"));
366- }
367- // plain and encrypted share a cache key, so a plain fetch of an encrypted file's uri
368- // leaves ciphertext here. its hash gives it away
369- if is_ciphertext(&cached, &request.source) {
370- let bytes = maybe_decrypt(cached, &request, max_bytes)?;
371- cache(&client, &request, &bytes).await;
372- return Ok(media(bytes, mimetype));
373- }
374- return Ok(media(cached, mimetype));
375- }
376- let (bytes, served_mimetype) = download_bounded(&client, &request, max_bytes).await?;
377- // the sdk caches plaintext under the encrypted key, so we do too or nobody can read anything
378- let bytes = maybe_decrypt(bytes, &request, max_bytes)?;
379- cache(&client, &request, &bytes).await;
380- Ok(media(bytes, mimetype.or(served_mimetype)))
381-}
382-
383-async fn cache(client: &Client, request: &MediaRequestParameters, bytes: &[u8]) {
384- if let Ok(store) = client.media_store().lock().await {
385- let _ = store
386- .add_media_content(
387- request,
388- bytes.to_vec(),
389- matrix_sdk::media::store::IgnoreMediaRetentionPolicy::No,
390- )
391- .await;
392- }
393-}
394-
395-fn is_ciphertext(bytes: &[u8], source: &MediaSource) -> bool {
396- use sha2::{Digest, Sha256};
397- let MediaSource::Encrypted(file) = source else {
398- return false;
399- };
400- file.hashes
401- .get(&ruma::events::room::EncryptedFileHashAlgorithm::Sha256)
402- .is_some_and(|hash| hash.as_bytes() == Sha256::digest(bytes).as_slice())
403-}
404-
405-fn media(bytes: Vec<u8>, mimetype: Option<String>) -> Media {
406- let mimetype = mimetype
407- .or_else(|| sniff_image(&bytes).map(str::to_owned))
408- .unwrap_or_else(|| "application/octet-stream".to_owned());
409- Media {
410- bytes: Arc::from(bytes),
411- mimetype,
412- }
413-}
414-
415-/// the cache forgets content types, and avatars never declare one. images are the common case
416-fn sniff_image(bytes: &[u8]) -> Option<&'static str> {
417- match bytes {
418- [0x89, b'P', b'N', b'G', ..] => Some("image/png"),
419- [0xff, 0xd8, 0xff, ..] => Some("image/jpeg"),
420- [b'G', b'I', b'F', b'8', ..] => Some("image/gif"),
421- [
422- b'R',
423- b'I',
424- b'F',
425- b'F',
426- _,
427- _,
428- _,
429- _,
430- b'W',
431- b'E',
432- b'B',
433- b'P',
434- ..,
435- ] => Some("image/webp"),
436- _ => None,
437- }
438-}
439-
440-async fn cached_media(
441- client: &Client,
442- request: &MediaRequestParameters,
443-) -> Result<Option<Vec<u8>>, Error> {
444- fn cache_failed(
445- source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
446- ) -> Error {
447- Error::with_source(ErrorKind::Store, "reading media cache failed", source)
448- }
449- client
450- .media_store()
451- .lock()
452- .await
453- .map_err(cache_failed)?
454- .get_media_content(request)
455- .await
456- .map_err(cache_failed)
457-}
458-
459-fn maybe_decrypt(
460- bytes: Vec<u8>,
461- request: &MediaRequestParameters,
462- max_bytes: usize,
463-) -> Result<Vec<u8>, Error> {
464- let MediaSource::Encrypted(file) = &request.source else {
465- return Ok(bytes);
466- };
467- fn decrypt_failed(
468- source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
469- ) -> Error {
470- Error::with_source(
471- ErrorKind::MediaUnavailable,
472- "media decryption failed",
473- source,
474- )
475- }
476- let info: MediaEncryptionInfo = (**file).clone().into();
477- let mut cursor = std::io::Cursor::new(bytes);
478- let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).map_err(decrypt_failed)?;
479- let mut decrypted = Vec::new();
480- let mut chunk = [0_u8; 8192];
481- loop {
482- let read = decryptor.read(&mut chunk).map_err(decrypt_failed)?;
483- if read == 0 {
484- return Ok(decrypted);
485- }
486- if read > max_bytes.saturating_sub(decrypted.len()) {
487- return Err(too_large(
488- "decrypted media exceeds the configured byte limit",
489- ));
490- }
491- decrypted.extend_from_slice(&chunk[..read]);
492- }
493-}
494-
495-/// the sdk's own download has no size cap, so we do it by hand
496-async fn download_bounded(
497- client: &Client,
498- request: &MediaRequestParameters,
499- max_bytes: usize,
500-) -> Result<(Vec<u8>, Option<String>), Error> {
501- if is_local(&request.source) {
502- return Err(Error::new(
503- ErrorKind::MediaUnavailable,
504- "local media is not in the media cache",
505- ));
506- }
507- let network_failed =
508- |source| Error::with_source(ErrorKind::Network, "media download failed", source);
509- let mut refreshed = false;
510- let response = loop {
511- let http_request = media_http_request(client, request).await?;
512- let (parts, _) = http_request.into_parts();
513- let mut builder = client
514- .http_client()
515- .get(parts.uri.to_string())
516- .timeout(DOWNLOAD_TOTAL);
517- for (name, value) in &parts.headers {
518- builder = builder.header(name.as_str(), value.as_bytes());
519- }
520- let response = builder.send().await.map_err(network_failed)?;
521- if response.status() != matrix_sdk::reqwest::StatusCode::UNAUTHORIZED {
522- break response;
523- }
524- if refreshed
525- || client
526- .session_tokens()
527- .is_none_or(|tokens| tokens.refresh_token.is_none())
528- {
529- return Err(Error::new(
530- ErrorKind::Unauthenticated,
531- "media download was not authorized",
532- ));
533- }
534- client.refresh_access_token().await.map_err(|source| {
535- Error::from_refresh_token("refreshing media access token failed", source)
536- })?;
537- refreshed = true;
538- };
539- let status = response.status();
540- if !status.is_success() {
541- return Err(Error::new(
542- ErrorKind::MediaUnavailable,
543- format!("media download failed with status {status}"),
544- ));
545- }
546- if response
547- .content_length()
548- .is_some_and(|length| length > max_bytes as u64)
549- {
550- return Err(too_large("media exceeds the configured byte limit"));
551- }
552- let mimetype = response
553- .headers()
554- .get(http::header::CONTENT_TYPE)
555- .and_then(|value| value.to_str().ok())
556- .map(str::to_owned);
557- let mut stream = response.bytes_stream();
558- let mut bytes = Vec::new();
559- loop {
560- let chunk = matrix_sdk::timeout::timeout(stream.next(), DOWNLOAD_IDLE)
561- .await
562- .map_err(|_| Error::new(ErrorKind::Network, "media download stalled"))?;
563- let Some(chunk) = chunk else { break };
564- let chunk = chunk.map_err(network_failed)?;
565- if chunk.len() > max_bytes.saturating_sub(bytes.len()) {
566- return Err(too_large(
567- "downloaded media exceeds the configured byte limit",
568- ));
569- }
570- bytes.extend_from_slice(&chunk);
571- }
572- Ok((bytes, mimetype))
573-}
574-
575-async fn media_http_request(
576- client: &Client,
577- request: &MediaRequestParameters,
578-) -> Result<http::Request<Vec<u8>>, Error> {
579- let versions = client.supported_versions().await.map_err(|source| {
580- Error::with_source(
581- ErrorKind::Network,
582- "reading supported server versions failed",
583- source,
584- )
585- })?;
586- let authenticated =
587- authenticated_media::get_content::v1::Request::PATH_BUILDER.is_supported(&versions);
588- let base = client.homeserver().to_string();
589- let token = client.access_token();
590- let invalid_uri =
591- |source| Error::with_source(ErrorKind::Invalid, "media URI is invalid", source);
592- let (uri, thumbnail) = match (&request.source, &request.format) {
593- (MediaSource::Encrypted(file), _) => (&file.url, None),
594- (MediaSource::Plain(uri), MediaFormat::File) => (uri, None),
595- (MediaSource::Plain(uri), MediaFormat::Thumbnail(settings)) => (uri, Some(settings)),
596- };
597- // each endpoint has its own auth input type, so no generic helper can take them all
598- macro_rules! into_http {
599- ($endpoint:expr, $token:expr) => {
600- $endpoint
601- .try_into_http_request::<Vec<u8>>(&base, $token, Cow::Borrowed(&versions))
602- .map_err(|source| {
603- Error::with_source(ErrorKind::Other, "media request is invalid", source)
604- })?
605- };
606- }
607- let authed = SendAccessToken::IfRequired(token.as_deref().unwrap_or(""));
608- #[allow(deprecated)]
609- let mut http_request = match (thumbnail, authenticated) {
610- (None, true) => into_http!(
611- authenticated_media::get_content::v1::Request::from_uri(uri).map_err(invalid_uri)?,
612- authed
613- ),
614- (None, false) => into_http!(
615- media::get_content::v3::Request::from_url(uri).map_err(invalid_uri)?,
616- SendAccessToken::None
617- ),
618- (Some(settings), true) => {
619- let mut endpoint = authenticated_media::get_content_thumbnail::v1::Request::from_uri(
620- uri,
621- settings.width,
622- settings.height,
623- )
624- .map_err(invalid_uri)?;
625- endpoint.method = Some(settings.method.clone());
626- endpoint.animated = Some(settings.animated);
627- into_http!(endpoint, authed)
628- }
629- (Some(settings), false) => {
630- let mut endpoint = media::get_content_thumbnail::v3::Request::from_url(
631- uri,
632- settings.width,
633- settings.height,
634- )
635- .map_err(invalid_uri)?;
636- endpoint.method = Some(settings.method.clone());
637- endpoint.animated = Some(settings.animated);
638- into_http!(endpoint, SendAccessToken::None)
639- }
640- };
641- // legacy endpoints don't ask for a token but plenty of servers want one anyway
642- if let Some(token) = token.as_deref() {
643- let value = format!("Bearer {token}").parse().map_err(|_| {
644- Error::new(ErrorKind::Unauthenticated, "stored access token is invalid")
645- })?;
646- http_request
647- .headers_mut()
648- .insert(http::header::AUTHORIZATION, value);
649- }
650- Ok(http_request)
651-}
652-
653-pub(crate) async fn queue_upload(
654- room: Room,
655- upload: AttachmentUpload,
656- max_bytes: usize,
657-) -> Result<(), Error> {
658- let (upload, mime, mut config) = prepare_upload(upload, max_bytes)?;
659- if let TimelineId::Thread { root_event_id, .. } = &upload.timeline_id {
660- config.reply = Some(SdkReply {
661- event_id: root_event_id.clone(),
662- enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No),
663- add_mentions: AddMentions::No,
664- });
665- }
666- room.send_queue()
667- .send_attachment(upload.filename, mime, upload.bytes, config)
668- .await
669- .map(drop)
670- .map_err(|source| {
671- use matrix_sdk::send_queue::RoomSendQueueError;
672- let kind = match &source {
673- RoomSendQueueError::RoomNotJoined | RoomSendQueueError::RoomDisappeared => {
674- ErrorKind::RoomUnknown
675- }
676- RoomSendQueueError::StorageError(_) => ErrorKind::Store,
677- _ => ErrorKind::MediaUnavailable,
678- };
679- Error::with_source(kind, "media upload could not be queued", source)
680- })
681-}
682-
683-/// least recently seen goes first. stale order slots are skipped by stamp rather than hunted down
684-struct MediaRegistry {
685- entries: HashMap<MediaId, (MediaRegistration, u64)>,
686- order: VecDeque<(MediaId, u64)>,
687- next_stamp: u64,
688- limit: usize,
689-}
690-
691-impl MediaRegistry {
692- fn new(limit: usize) -> Self {
693- Self {
694- entries: HashMap::new(),
695- order: VecDeque::new(),
696- next_stamp: 0,
697- limit,
698- }
699- }
700-
701- fn get(&self, id: &MediaId) -> Option<&MediaRegistration> {
702- self.entries.get(id).map(|(media, _)| media)
703- }
704-
705- fn insert_all(&mut self, media: Vec<MediaRegistration>) {
706- for media in media {
707- let stamp = self.next_stamp;
708- self.next_stamp += 1;
709- self.order.push_back((media.id.clone(), stamp));
710- self.entries.insert(media.id.clone(), (media, stamp));
711- }
712- while self.entries.len() > self.limit {
713- let Some((id, stamp)) = self.order.pop_front() else {
714- break;
715- };
716- if self.is_current(&id, stamp) {
717- self.entries.remove(&id);
718- }
719- }
720- if self.order.len() > self.limit.saturating_mul(4) {
721- let entries = &self.entries;
722- self.order
723- .retain(|(id, stamp)| entries.get(id).is_some_and(|(_, current)| current == stamp));
724- }
725- }
726-
727- fn is_current(&self, id: &MediaId, stamp: u64) -> bool {
728- self.entries
729- .get(id)
730- .is_some_and(|(_, current)| *current == stamp)
731- }
732-
733- fn release(&mut self, scope: &MediaScope) {
734- self.entries.retain(|id, _| !scope.owns(id));
735- let entries = &self.entries;
736- self.order.retain(|(id, _)| entries.contains_key(id));
737- }
738-
739- fn clear(&mut self) {
740- self.entries.clear();
741- self.order.clear();
742- }
743-}
Deleted file: vendor/macltok/src/engine/mod.rs−968 deletions
@@ -1,968 +0,0 @@
1-mod commands;
2-pub(crate) mod executor;
3-mod media;
4-mod mutations;
5-mod prepare;
6-mod room;
7-mod session;
8-mod timeline;
9-mod url_preview;
10-mod watchers;
11-
12-use std::{
13- collections::{HashMap, VecDeque},
14- future::Future,
15- panic::AssertUnwindSafe,
16- sync::Arc,
17-};
18-
19-use futures_util::FutureExt;
20-use matrix_sdk::{Client, SendOutsideWasm, SessionChange};
21-use matrix_sdk_ui::sync_service::SyncService;
22-use ruma::{OwnedDeviceId, OwnedRoomId};
23-use thiserror::Error as ThisError;
24-use tokio::sync::{broadcast, mpsc, watch};
25-
26-pub(crate) use media::MediaService;
27-pub(crate) use session::Persistence;
28-
29-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
30-pub(crate) use crate::store_lock::StoreLock;
31-#[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
32-pub(crate) enum StoreLock {}
33-
34-use self::{
35- commands::Job,
36- executor::{EngineBoxFuture, TaskSet},
37- mutations::{Mutation, MutationJob},
38- room::{ActorNote, ActorRequest, RoomActor},
39- timeline::{SessionContext, Supervisor},
40-};
41-use crate::{
42- AccountCommand, ClientHandle, Command, ConnectionState, EncryptionCommand, EngineErrorKind,
43- EngineFailure, Error, ErrorKind, Event, EventStream, LifecycleCommand, MediaCommand,
44- NotificationCommand, RoomCommand, Termination, TimelineCommand,
45- normalise::{ROOM_PACKS, ROOM_PREVIEW_URLS},
46- transport::{DriverPort, Request, Responder, transport_pair},
47-};
48-
49-#[derive(Clone, Debug, Eq, PartialEq)]
50-#[non_exhaustive]
51-pub struct EngineConfig {
52- pub command_capacity: usize,
53- pub event_capacity: usize,
54- pub initial_history_limit: u16,
55- pub max_media_bytes: usize,
56- pub max_media_sources: usize,
57- /// warm a room up for sending when it's opened or someone starts typing in it
58- pub prepare_to_send: bool,
59-}
60-
61-impl EngineConfig {
62- #[must_use]
63- pub const fn with_command_capacity(mut self, capacity: usize) -> Self {
64- self.command_capacity = capacity;
65- self
66- }
67-
68- #[must_use]
69- pub const fn with_event_capacity(mut self, capacity: usize) -> Self {
70- self.event_capacity = capacity;
71- self
72- }
73-
74- #[must_use]
75- pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {
76- self.initial_history_limit = limit;
77- self
78- }
79-
80- #[must_use]
81- pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {
82- self.max_media_bytes = bytes;
83- self
84- }
85-
86- #[must_use]
87- pub const fn with_max_media_sources(mut self, sources: usize) -> Self {
88- self.max_media_sources = sources;
89- self
90- }
91-
92- #[must_use]
93- pub const fn with_prepare_to_send(mut self, prepare: bool) -> Self {
94- self.prepare_to_send = prepare;
95- self
96- }
97-}
98-
99-impl Default for EngineConfig {
100- fn default() -> Self {
101- Self {
102- command_capacity: 64,
103- event_capacity: 128,
104- initial_history_limit: 30,
105- max_media_bytes: 20 * 1024 * 1024,
106- max_media_sources: 2_048,
107- prepare_to_send: true,
108- }
109- }
110-}
111-
112-#[derive(Debug, ThisError)]
113-pub enum EngineError {
114- #[error("the Matrix client is not authenticated")]
115- Unauthenticated,
116- #[error("command, event, and media source capacities must be nonzero")]
117- ZeroCapacity,
118- #[error("could not build the sync service")]
119- SyncService(#[source] matrix_sdk_ui::sync_service::Error),
120- #[error("the event receiver has closed")]
121- EventReceiverClosed,
122- #[error("engine task {task} panicked")]
123- TaskPanicked { task: String },
124-}
125-
126-impl EngineError {
127- fn failure(&self) -> EngineFailure {
128- EngineFailure {
129- kind: match self {
130- Self::EventReceiverClosed => EngineErrorKind::EventReceiverClosed,
131- Self::TaskPanicked { .. } => EngineErrorKind::Watcher,
132- Self::Unauthenticated | Self::ZeroCapacity | Self::SyncService(_) => {
133- EngineErrorKind::Other
134- }
135- },
136- message: self.to_string(),
137- }
138- }
139-}
140-
141-#[derive(Clone)]
142-enum TaskContext {
143- /// a panic here costs the caller an `Interrupted` answer and nothing else
144- Command(&'static str),
145- /// a panic here takes the engine with it
146- Watcher(&'static str),
147- RoomActor {
148- room_id: OwnedRoomId,
149- generation: u64,
150- },
151-}
152-
153-impl TaskContext {
154- fn label(&self) -> String {
155- match self {
156- Self::Command(name) | Self::Watcher(name) => (*name).to_owned(),
157- Self::RoomActor {
158- room_id,
159- generation,
160- } => format!("room actor {room_id} generation {generation}"),
161- }
162- }
163-}
164-
165-/// tasks catch their own panics so each comes back with its context, since joining
166-/// through the sdk's executor gives no task id to look one up by. wasm has nothing to catch
167-fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
168- payload
169- .downcast_ref::<&str>()
170- .map(ToString::to_string)
171- .or_else(|| payload.downcast_ref::<String>().cloned())
172- .unwrap_or_else(|| "task panicked with a non-string payload".to_owned())
173-}
174-
175-struct TaskRegistry {
176- tasks: TaskSet<(TaskContext, Option<String>)>,
177-}
178-
179-impl TaskRegistry {
180- fn spawn(
181- &mut self,
182- context: TaskContext,
183- task: impl Future<Output = ()> + SendOutsideWasm + 'static,
184- ) {
185- self.tasks.spawn(async move {
186- let panic = AssertUnwindSafe(task)
187- .catch_unwind()
188- .await
189- .err()
190- .map(panic_message);
191- (context, panic)
192- });
193- }
194-
195- fn is_empty(&self) -> bool {
196- self.tasks.is_empty()
197- }
198-
199- async fn join_next(&mut self) -> Option<(TaskContext, Option<String>)> {
200- loop {
201- if let Ok(done) = self.tasks.join_next().await? {
202- return Some(done);
203- }
204- }
205- }
206-
207- async fn shutdown(&mut self) {
208- self.tasks.shutdown().await;
209- }
210-}
211-
212-enum RoomSlot {
213- Active {
214- generation: u64,
215- sender: mpsc::Sender<ActorRequest>,
216- },
217- /// opens and closes that arrive while the actor winds down, replayed in order once it's gone
218- Closing {
219- generation: u64,
220- pending: VecDeque<Deferred>,
221- },
222-}
223-
224-impl RoomSlot {
225- const fn generation(&self) -> u64 {
226- match self {
227- Self::Active { generation, .. } | Self::Closing { generation, .. } => *generation,
228- }
229- }
230-}
231-
232-enum Deferred {
233- Open(Responder),
234- Close(Responder),
235-}
236-
237-/// logouts in flight, and a token rejection held back because one of them caused it
238-#[derive(Default)]
239-pub(super) struct Logouts {
240- pub(super) in_flight: usize,
241- pub(super) rejected: Option<Option<OwnedDeviceId>>,
242-}
243-
244-struct Queues {
245- mutations: mpsc::Receiver<MutationJob>,
246- notifications: mpsc::Receiver<Job<NotificationCommand>>,
247- encryption: mpsc::Receiver<Job<EncryptionCommand>>,
248- session_changes: broadcast::Receiver<SessionChange>,
249- persistence: Option<Persistence>,
250-}
251-
252-/// requests other devices send us, handed from the sdk's event handler to the encryption worker
253-type IncomingVerifications =
254- mpsc::UnboundedSender<matrix_sdk::encryption::verification::VerificationRequest>;
255-
256-pub struct Engine {
257- client: Client,
258- sync: Arc<SyncService>,
259- port: DriverPort,
260- config: EngineConfig,
261- media: Arc<MediaService>,
262- tasks: TaskRegistry,
263- rooms: HashMap<OwnedRoomId, RoomSlot>,
264- next_generation: u64,
265- notes: mpsc::UnboundedReceiver<ActorNote>,
266- note_sender: mpsc::UnboundedSender<ActorNote>,
267- /// the rooms sliding sync should follow, applied off the run loop since it touches the store
268- subscriptions: watch::Sender<Vec<OwnedRoomId>>,
269- mutations: mpsc::Sender<MutationJob>,
270- notifications: mpsc::Sender<Job<NotificationCommand>>,
271- encryption: mpsc::Sender<Job<EncryptionCommand>>,
272- queues: Option<Queues>,
273- /// the client can outlive us, so the handler has to come off it when we stop
274- verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
275- stop: mpsc::UnboundedSender<Termination>,
276- stops: mpsc::UnboundedReceiver<Termination>,
277- logouts: Arc<watch::Sender<Logouts>>,
278- preparations: prepare::Preparations,
279- /// shared with every `ClientHandle`, so the store stays locked while the client is reachable
280- _store_lock: Option<Arc<StoreLock>>,
281-}
282-
283-impl Engine {
284- /// the escape hatch for a client you built yourself. `Session` is the usual way in
285- pub async fn from_client(
286- client: Client,
287- config: EngineConfig,
288- ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
289- let session_changes = client.subscribe_to_session_changes();
290- Self::with_persistence(client, config, session_changes, None, None).await
291- }
292-
293- /// `session_changes` is taken when the client is built, since the sdk can refresh
294- /// before an engine exists
295- pub(crate) async fn with_persistence(
296- client: Client,
297- config: EngineConfig,
298- session_changes: broadcast::Receiver<SessionChange>,
299- persistence: Option<Persistence>,
300- store_lock: Option<StoreLock>,
301- ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
302- if client.user_id().is_none() {
303- return Err(EngineError::Unauthenticated);
304- }
305- if config.command_capacity == 0
306- || config.event_capacity == 0
307- || config.max_media_sources == 0
308- {
309- return Err(EngineError::ZeroCapacity);
310- }
311- let sync = Arc::new(
312- SyncService::builder(client.clone())
313- .with_offline_mode()
314- .with_room_list_extra_required_state(vec![
315- (ROOM_PACKS.into(), "*".to_owned()),
316- (ROOM_PREVIEW_URLS.into(), String::new()),
317- ])
318- .build()
319- .await
320- .map_err(EngineError::SyncService)?,
321- );
322- let store_lock = store_lock.map(Arc::new);
323- let media = Arc::new(MediaService::new(
324- client.clone(),
325- config.max_media_bytes,
326- config.max_media_sources,
327- ));
328- let (handle, port, events) = transport_pair(
329- client.clone(),
330- sync.clone(),
331- media.clone(),
332- store_lock.clone(),
333- config.command_capacity,
334- config.event_capacity,
335- );
336- client.send_queue().enable_upload_progress(true);
337- client.send_queue().enable_global_upload_progress(true);
338- let (note_sender, notes) = mpsc::unbounded_channel();
339- let (stop, stops) = mpsc::unbounded_channel();
340- let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
341- let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
342- let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
343- let engine = Self {
344- client,
345- sync,
346- port,
347- config,
348- media,
349- tasks: TaskRegistry {
350- tasks: TaskSet::new(),
351- },
352- rooms: HashMap::new(),
353- next_generation: 0,
354- notes,
355- note_sender,
356- subscriptions: watch::Sender::new(Vec::new()),
357- mutations,
358- notifications,
359- encryption,
360- queues: Some(Queues {
361- mutations: mutation_queue,
362- notifications: notification_queue,
363- encryption: encryption_queue,
364- session_changes,
365- persistence,
366- }),
367- verification_handler: None,
368- preparations: prepare::Preparations::default(),
369- stop,
370- stops,
371- logouts: Arc::new(watch::Sender::new(Logouts::default())),
372- _store_lock: store_lock,
373- };
374- Ok((engine, handle, events))
375- }
376-
377- /// boxed because the engine's future is deep enough to blow the compiler's query depth
378- /// in every crate that spawns it, which isn't something a client should have to fix
379- pub fn run(self) -> EngineBoxFuture<'static, Result<Termination, EngineError>> {
380- Box::pin(self.run_inner())
381- }
382-
383- async fn run_inner(mut self) -> Result<Termination, EngineError> {
384- let events = self.port.event_sender();
385- let user_id = self
386- .client
387- .user_id()
388- .expect("authentication was checked during construction")
389- .to_owned();
390- self.port
391- .connection_sender()
392- .send_replace(ConnectionState::Connecting);
393- for event in [
394- Event::Authenticated { user_id },
395- Event::ConnectionChanged(ConnectionState::Connecting),
396- ] {
397- if events.send(event).await.is_err() {
398- self.shutdown().await;
399- return Err(EngineError::EventReceiverClosed);
400- }
401- }
402- self.start_tasks();
403- self.sync.start().await;
404-
405- let mut shutdown_reply = None;
406- let mut handles_dropped = false;
407- let mut logouts = self.logouts.subscribe();
408- let ending = loop {
409- tokio::select! {
410- biased;
411- Some(termination) = self.stops.recv() => break Ok(termination),
412- () = events.closed() => break Err(EngineError::EventReceiverClosed),
413- () = settled(&mut logouts), if handles_dropped => {
414- break Ok(Termination::HandlesDropped);
415- }
416- Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {
417- let Some(message) = panic else { continue };
418- if let TaskContext::Command(label) = context {
419- tracing::error!(label, panic = %message, "command task panicked");
420- continue;
421- }
422- break Err(EngineError::TaskPanicked { task: context.label() });
423- }
424- Some(note) = self.notes.recv() => self.actor_note(note).await,
425- request = self.port.recv(), if !handles_dropped => {
426- // nobody's left to ask, but a logout they asked for still deserves to land
427- let Some(request) = request else {
428- handles_dropped = true;
429- continue;
430- };
431- if let Some(reply) = self.dispatch(request).await {
432- shutdown_reply = Some(reply);
433- break Ok(Termination::ShutDown);
434- }
435- }
436- }
437- };
438-
439- if let Err(error) = &ending {
440- let _ = events.send(Event::Failed(error.failure())).await;
441- }
442- self.shutdown().await;
443- let _ = events.send(Event::Stopped).await;
444- if let Some(reply) = shutdown_reply {
445- reply.done(Ok(()));
446- }
447- // everything else still queued answers `Interrupted` as it drops, but a shutdown did happen
448- while let Some(Request { command, reply }) = self.port.try_recv() {
449- if matches!(command, Command::Lifecycle(LifecycleCommand::Shutdown)) {
450- reply.done(Ok(()));
451- }
452- }
453- ending
454- }
455-
456- fn start_tasks(&mut self) {
457- let queues = self.queues.take().expect("engine tasks start once");
458- let (incoming, incoming_verifications) = mpsc::unbounded_channel();
459- self.verification_handler = Some(self.verification_handler(incoming));
460- let client = &self.client;
461- let events = || self.port.event_sender();
462- let (notices, notice_queue) = mpsc::unbounded_channel();
463- let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 13] = [
464- (
465- "upload tracker",
466- Box::pin(watchers::uploads(
467- client.clone(),
468- client.send_queue().subscribe(),
469- events(),
470- )),
471- ),
472- (
473- "room subscription watcher",
474- Box::pin(watchers::room_subscriptions(
475- self.sync.clone(),
476- self.subscriptions.subscribe(),
477- )),
478- ),
479- (
480- "session change watcher",
481- Box::pin(session::session(
482- client.clone(),
483- queues.session_changes,
484- queues.persistence,
485- notices,
486- client
487- .device_id()
488- .expect("authentication was checked during construction")
489- .to_owned(),
490- self.logouts.clone(),
491- )),
492- ),
493- (
494- "session notice watcher",
495- Box::pin(session::session_notices(
496- notice_queue,
497- events(),
498- self.stop.clone(),
499- )),
500- ),
501- (
502- "connection state watcher",
503- Box::pin(watchers::connection(
504- self.sync.state(),
505- self.sync.clone(),
506- client.clone(),
507- events(),
508- self.port.connection_sender(),
509- )),
510- ),
511- (
512- "send queue watcher",
513- Box::pin(watchers::send_queue_errors(
514- client.clone(),
515- client.send_queue().subscribe_errors(),
516- self.port.connection_sender().subscribe(),
517- )),
518- ),
519- (
520- "encryption state watcher",
521- Box::pin(watchers::encryption(client.clone(), events())),
522- ),
523- (
524- "room list watcher",
525- Box::pin(watchers::rooms(client.clone(), events())),
526- ),
527- (
528- "image pack watcher",
529- Box::pin(watchers::image_packs(client.clone(), events())),
530- ),
531- (
532- "notification settings watcher",
533- Box::pin(watchers::notification_settings(client.clone(), events())),
534- ),
535- (
536- "room command worker",
537- Box::pin(mutations::run_scheduler(
538- client.clone(),
539- queues.mutations,
540- self.config.command_capacity,
541- )),
542- ),
543- (
544- "notification command worker",
545- Box::pin(commands::run_notifications(
546- client.clone(),
547- queues.notifications,
548- )),
549- ),
550- (
551- "encryption command worker",
552- Box::pin(commands::run_encryption(
553- client.clone(),
554- events(),
555- queues.encryption,
556- incoming_verifications,
557- )),
558- ),
559- ];
560- for (label, task) in watchers {
561- self.tasks.spawn(TaskContext::Watcher(label), task);
562- }
563- }
564-
565- fn verification_handler(
566- &self,
567- incoming: IncomingVerifications,
568- ) -> matrix_sdk::event_handler::EventHandlerHandle {
569- use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
570- self.client.add_event_handler(
571- move |event: ToDeviceKeyVerificationRequestEvent, client: Client| {
572- let incoming = incoming.clone();
573- async move {
574- let request = client
575- .encryption()
576- .get_verification_request(
577- &event.sender,
578- event.content.transaction_id.as_str(),
579- )
580- .await;
581- if let Some(request) = request {
582- let _ = incoming.send(request);
583- }
584- }
585- },
586- )
587- }
588-
589- /// hands back the shutdown request's responder, which is answered once we've stopped
590- async fn dispatch(&mut self, Request { command, reply }: Request) -> Option<Responder> {
591- let client = self.client.clone();
592- match command {
593- Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
594- Command::Account(AccountCommand::Logout) => {
595- let events = self.port.event_sender();
596- let stop = self.stop.clone();
597- let logouts = self.logouts.clone();
598- logouts.send_modify(|logouts| logouts.in_flight += 1);
599- self.spawn_command("logout", reply, async move {
600- let result = client.logout().await;
601- if result.is_ok() {
602- let _ = events.send(Event::LoggedOut).await;
603- let _ = stop.send(Termination::LoggedOut);
604- }
605- let mut rejected = None;
606- logouts.send_if_modified(|logouts| {
607- if result.is_err() && logouts.in_flight == 1 {
608- rejected = logouts.rejected.take();
609- }
610- false
611- });
612- if let Some(soft_logout) = rejected {
613- session::reject_session(&events, &stop, soft_logout).await;
614- }
615- logouts.send_modify(|logouts| logouts.in_flight -= 1);
616- result.map_err(|source| Error::from_sdk("logout failed", source))
617- });
618- }
619- Command::Account(AccountCommand::SetAccountData {
620- event_type,
621- content,
622- }) => enqueue(
623- &self.mutations,
624- Job {
625- command: Mutation::GlobalAccountData {
626- event_type,
627- content,
628- },
629- reply,
630- },
631- ),
632- Command::Account(AccountCommand::SetDisplayName(name)) => {
633- self.spawn_command("set display name", reply, async move {
634- client
635- .account()
636- .set_display_name(name.as_deref())
637- .await
638- .map_err(|source| Error::from_sdk("setting display name failed", source))
639- });
640- }
641- Command::Account(AccountCommand::SetAvatar(avatar)) => {
642- self.spawn_command("set avatar", reply, async move {
643- client
644- .account()
645- .set_avatar_url(avatar.as_deref())
646- .await
647- .map_err(|source| Error::from_sdk("setting avatar failed", source))
648- });
649- }
650- Command::Account(AccountCommand::UploadAvatar { mimetype, bytes }) => {
651- let max_bytes = self.config.max_media_bytes;
652- self.spawn_command("upload avatar", reply, async move {
653- commands::upload_avatar(&client, &mimetype, bytes, max_bytes).await
654- });
655- }
656- Command::Account(AccountCommand::RenameDevice {
657- device_id,
658- display_name,
659- }) => self.spawn_command("rename device", reply, async move {
660- commands::rename_device(&client, &device_id, &display_name).await
661- }),
662- Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
663- self.spawn_command("delete devices", reply, async move {
664- commands::delete_devices(&client, &device_ids, auth).await
665- });
666- }
667- Command::Room(RoomCommand::Open(room_id)) => {
668- if self.config.prepare_to_send {
669- self.prepare_quietly(room_id.clone());
670- }
671- self.open_room(room_id, reply).await;
672- }
673- Command::Room(RoomCommand::Close(room_id)) => self.close_room(room_id, reply).await,
674- Command::Room(RoomCommand::PrepareToSend(room_id)) => {
675- let flight = self.preparations.join(&client, room_id);
676- self.spawn_command("prepare to send", reply, flight);
677- }
678- Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
679- if typing && self.config.prepare_to_send {
680- self.prepare_quietly(room_id.clone());
681- }
682- self.spawn_command("typing", reply, async move {
683- commands::set_typing(&client, &room_id, typing).await
684- });
685- }
686- Command::Room(RoomCommand::CancelSend {
687- room_id,
688- transaction_id,
689- }) => self.spawn_command("cancel send", reply, async move {
690- commands::cancel_send(&client, &room_id, &transaction_id).await
691- }),
692- Command::Room(RoomCommand::RetrySend {
693- room_id,
694- transaction_id,
695- }) => self.spawn_command("retry send", reply, async move {
696- commands::retry_send(&client, &room_id, &transaction_id).await
697- }),
698- Command::Room(command) => enqueue(
699- &self.mutations,
700- Job {
701- command: Mutation::Room(command),
702- reply,
703- },
704- ),
705- Command::Timeline(command) => self.route_timeline(command, reply),
706- Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),
707- Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),
708- Command::Notification(command) => {
709- enqueue(&self.notifications, Job { command, reply });
710- }
711- }
712- None
713- }
714-
715- fn spawn_command(
716- &mut self,
717- label: &'static str,
718- reply: Responder,
719- work: impl Future<Output = Result<(), Error>> + SendOutsideWasm + 'static,
720- ) {
721- self.tasks.spawn(TaskContext::Command(label), async move {
722- reply.done(work.await);
723- });
724- }
725-
726- fn prepare_quietly(&mut self, room_id: OwnedRoomId) {
727- let flight = self.preparations.join(&self.client, room_id);
728- self.tasks
729- .spawn(TaskContext::Command("prepare to send"), async move {
730- let _ = flight.await;
731- });
732- }
733-
734- fn upload(&mut self, upload: crate::AttachmentUpload, reply: Responder) {
735- let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
736- return reply.done(Err(Error::room_unknown()));
737- };
738- let max_bytes = self.config.max_media_bytes;
739- self.spawn_command("upload", reply, async move {
740- media::queue_upload(room, upload, max_bytes).await
741- });
742- }
743-
744- fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
745- let sender = match self.rooms.get(command.room_id()) {
746- Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
747- _ => None,
748- };
749- match (sender, command) {
750- (Some(sender), command) => send_actor(&sender, ActorRequest::Execute(command, reply)),
751- (
752- None,
753- TimelineCommand::MarkRead {
754- timeline_id,
755- event_id,
756- },
757- ) => {
758- let client = self.client.clone();
759- self.spawn_command("mark read", reply, async move {
760- commands::mark_read(&client, &timeline_id, event_id).await
761- });
762- }
763- (None, _) => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
764- }
765- }
766-
767- async fn open_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
768- match self.rooms.get_mut(&room_id) {
769- Some(RoomSlot::Active { sender, .. }) => send_actor(sender, ActorRequest::Open(reply)),
770- Some(RoomSlot::Closing { pending, .. }) => {
771- defer(pending, Deferred::Open(reply), self.config.command_capacity);
772- }
773- None => self.start_actor(room_id, reply).await,
774- }
775- }
776-
777- async fn close_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
778- let (generation, sender) = match self.rooms.get_mut(&room_id) {
779- None => return reply.done(Ok(())),
780- Some(RoomSlot::Closing { pending, .. }) => {
781- return defer(
782- pending,
783- Deferred::Close(reply),
784- self.config.command_capacity,
785- );
786- }
787- Some(RoomSlot::Active { generation, sender }) => (*generation, sender.clone()),
788- };
789- match sender.try_send(ActorRequest::Close(reply)) {
790- Ok(()) => {}
791- // the actor already left on its own, which is closed enough
792- Err(mpsc::error::TrySendError::Closed(request)) => request.into_reply().done(Ok(())),
793- Err(mpsc::error::TrySendError::Full(request)) => {
794- return request.into_reply().done(Err(Error::new(
795- ErrorKind::RoomBusy,
796- "room command queue is full",
797- )));
798- }
799- }
800- self.rooms.insert(
801- room_id,
802- RoomSlot::Closing {
803- generation,
804- pending: VecDeque::new(),
805- },
806- );
807- self.update_room_subscriptions();
808- }
809-
810- async fn start_actor(&mut self, room_id: OwnedRoomId, reply: Responder) {
811- let Some(room) = self.client.get_room(&room_id) else {
812- return reply.done(Err(Error::room_unknown()));
813- };
814- self.next_generation = self.next_generation.wrapping_add(1);
815- let generation = self.next_generation;
816- let (sender, requests) = mpsc::channel(self.config.command_capacity);
817- let (supervisor, panics) = Supervisor::new();
818- let actor = RoomActor {
819- room_id: room_id.clone(),
820- generation,
821- room,
822- requests,
823- notes: self.note_sender.clone(),
824- context: SessionContext {
825- events: self.port.event_sender(),
826- media: self.media.clone(),
827- current_user_id: self.client.user_id().map(ToOwned::to_owned),
828- initial_history_limit: self.config.initial_history_limit,
829- supervisor,
830- },
831- panics,
832- opening: vec![reply],
833- };
834- self.rooms
835- .insert(room_id.clone(), RoomSlot::Active { generation, sender });
836- self.tasks.spawn(
837- TaskContext::RoomActor {
838- room_id,
839- generation,
840- },
841- actor.run(),
842- );
843- self.update_room_subscriptions();
844- }
845-
846- async fn actor_note(&mut self, note: ActorNote) {
847- let (room_id, generation) = match &note {
848- ActorNote::Leaving {
849- room_id,
850- generation,
851- }
852- | ActorNote::Exited {
853- room_id,
854- generation,
855- ..
856- } => (room_id.clone(), *generation),
857- };
858- if self
859- .rooms
860- .get(&room_id)
861- .is_none_or(|slot| slot.generation() != generation)
862- {
863- return;
864- }
865- let ActorNote::Exited { leftovers, .. } = note else {
866- if let Some(slot @ RoomSlot::Active { .. }) = self.rooms.get_mut(&room_id) {
867- *slot = RoomSlot::Closing {
868- generation,
869- pending: VecDeque::new(),
870- };
871- self.update_room_subscriptions();
872- }
873- return;
874- };
875- let pending = match self.rooms.remove(&room_id) {
876- Some(RoomSlot::Closing { pending, .. }) => pending,
877- _ => VecDeque::new(),
878- };
879- // leftovers were sent before anything deferred, so they go first
880- for request in leftovers {
881- match request {
882- ActorRequest::Open(reply) => self.open_room(room_id.clone(), reply).await,
883- ActorRequest::Close(reply) => reply.done(Ok(())),
884- ActorRequest::Execute(command, reply) => self.route_timeline(command, reply),
885- }
886- }
887- for deferred in pending {
888- match deferred {
889- Deferred::Open(reply) => self.open_room(room_id.clone(), reply).await,
890- Deferred::Close(reply) => self.close_room(room_id.clone(), reply).await,
891- }
892- }
893- self.update_room_subscriptions();
894- }
895-
896- fn update_room_subscriptions(&self) {
897- let room_ids = self
898- .rooms
899- .iter()
900- .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))
901- .map(|(room_id, _)| room_id.clone())
902- .collect();
903- self.subscriptions.send_replace(room_ids);
904- }
905-
906- async fn shutdown(&mut self) {
907- if let Some(handler) = self.verification_handler.take() {
908- self.client.remove_event_handler(handler);
909- }
910- self.port.close();
911- self.tasks.shutdown().await;
912- self.rooms.clear();
913- self.media.clear().await;
914- self.sync.stop().await;
915- self.port
916- .connection_sender()
917- .send_replace(ConnectionState::Stopped);
918- let _ = self
919- .port
920- .event_sender()
921- .send(Event::ConnectionChanged(ConnectionState::Stopped))
922- .await;
923- }
924-}
925-
926-fn send_actor(sender: &mpsc::Sender<ActorRequest>, request: ActorRequest) {
927- if let Err(error) = sender.try_send(request) {
928- let (request, error) = match error {
929- mpsc::error::TrySendError::Full(request) => (
930- request,
931- Error::new(ErrorKind::RoomBusy, "room command queue is full"),
932- ),
933- mpsc::error::TrySendError::Closed(request) => (
934- request,
935- Error::new(ErrorKind::RoomNotOpen, "room is not open"),
936- ),
937- };
938- request.into_reply().done(Err(error));
939- }
940-}
941-
942-fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize) {
943- if pending.len() < capacity {
944- return pending.push_back(deferred);
945- }
946- let (Deferred::Open(reply) | Deferred::Close(reply)) = deferred;
947- reply.done(Err(Error::new(
948- ErrorKind::RoomBusy,
949- "room lifecycle queue is full",
950- )));
951-}
952-
953-async fn settled(logouts: &mut watch::Receiver<Logouts>) {
954- let _ = logouts.wait_for(|logouts| logouts.in_flight == 0).await;
955-}
956-
957-fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
958- if let Err(error) = sender.try_send(job) {
959- let (job, error) = match error {
960- mpsc::error::TrySendError::Full(job) => (
961- job,
962- Error::new(ErrorKind::QueueFull, "command queue is full"),
963- ),
964- mpsc::error::TrySendError::Closed(job) => (job, Error::interrupted()),
965- };
966- job.reply.done(Err(error));
967- }
968-}
Deleted file: vendor/macltok/src/engine/mutations.rs−379 deletions
@@ -1,379 +0,0 @@
1-use std::{
2- collections::{HashMap, HashSet, VecDeque},
3- sync::Arc,
4-};
5-
6-use matrix_sdk::{
7- Client, Room,
8- ruma::{
9- Int,
10- api::client::room::{
11- Visibility as SdkVisibility,
12- create_room::v3::{Request as CreateRoomRequest, RoomPreset as SdkPreset},
13- },
14- events::{
15- AnyGlobalAccountDataEventContent, GlobalAccountDataEventType, MessageLikeEventType,
16- StateEventType, room::power_levels::RoomPowerLevelsEventContent,
17- },
18- serde::Raw,
19- },
20-};
21-use ruma::{OwnedRoomId, OwnedUserId};
22-use tokio::sync::mpsc;
23-
24-use super::commands::{Job, guarded};
25-use super::executor::TaskSet;
26-use crate::{
27- CommandOutput, CreateRoomOptions, Error, ErrorKind, PowerLevelChanges, RoomCommand, RoomPreset,
28- RoomVisibility,
29-};
30-
31-/// state changes that must not race each other. same room, same alias, same dm
32-/// partner or same account data type run in order, everything else runs at once
33-pub(super) enum Mutation {
34- Room(RoomCommand),
35- GlobalAccountData {
36- event_type: GlobalAccountDataEventType,
37- content: Raw<AnyGlobalAccountDataEventContent>,
38- },
39-}
40-
41-pub(super) type MutationJob = Job<Mutation>;
42-
43-#[derive(Clone, Debug, Eq, Hash, PartialEq)]
44-enum Key {
45- Room(OwnedRoomId),
46- Alias(String),
47- Direct(OwnedUserId),
48- AccountData(String),
49- Independent(u64),
50-}
51-
52-pub(super) async fn run_scheduler(
53- client: Client,
54- mut jobs: mpsc::Receiver<MutationJob>,
55- queue_limit: usize,
56-) {
57- let fresh_dms = FreshDms::default();
58- let mut waiting = HashMap::<Key, VecDeque<MutationJob>>::new();
59- let mut running = HashSet::<Key>::new();
60- let mut tasks = TaskSet::<Key>::new();
61- let mut queued = 0_usize;
62- let mut independent = 0_u64;
63- let mut receiving = true;
64- while receiving || !tasks.is_empty() {
65- tokio::select! {
66- biased;
67- Some(finished) = tasks.join_next(), if !tasks.is_empty() => {
68- // a job's own panic is caught inside it, so this is only ever a key
69- let Ok(key) = finished else { continue };
70- let next = waiting.get_mut(&key).and_then(VecDeque::pop_front);
71- if waiting.get(&key).is_some_and(VecDeque::is_empty) {
72- waiting.remove(&key);
73- }
74- match next {
75- Some(job) => {
76- queued -= 1;
77- spawn(&mut tasks, &client, &fresh_dms, key, job);
78- }
79- None => {
80- running.remove(&key);
81- }
82- }
83- }
84- job = jobs.recv(), if receiving => {
85- let Some(job) = job else {
86- receiving = false;
87- continue;
88- };
89- let key = key(&job.command, client.user_id().map(ruma::UserId::server_name)).unwrap_or_else(|| {
90- independent += 1;
91- Key::Independent(independent)
92- });
93- if running.insert(key.clone()) {
94- spawn(&mut tasks, &client, &fresh_dms, key, job);
95- } else if queued < queue_limit {
96- waiting.entry(key).or_default().push_back(job);
97- queued += 1;
98- } else {
99- job.reply.done(Err(Error::new(
100- ErrorKind::QueueFull,
101- "room command queue is full",
102- )));
103- }
104- }
105- }
106- }
107-}
108-
109-/// `server` is ours, since a new alias lives there
110-fn key(mutation: &Mutation, server: Option<&ruma::ServerName>) -> Option<Key> {
111- let room_id = match mutation {
112- Mutation::GlobalAccountData { event_type, .. } => {
113- return Some(Key::AccountData(event_type.to_string()));
114- }
115- Mutation::Room(RoomCommand::Join { room, .. }) => {
116- return Some(match OwnedRoomId::try_from(room.clone()) {
117- Ok(room_id) => Key::Room(room_id),
118- Err(alias) => Key::Alias(alias.to_string()),
119- });
120- }
121- Mutation::Room(RoomCommand::CreateDirect(user_id)) => {
122- return Some(Key::Direct(user_id.clone()));
123- }
124- Mutation::Room(RoomCommand::Create(options)) => {
125- return match (&options.alias_localpart, options.invite.as_slice()) {
126- (Some(localpart), _) => Some(Key::Alias(format!("#{localpart}:{}", server?))),
127- (None, [user_id]) if options.direct => Some(Key::Direct(user_id.clone())),
128- _ => None,
129- };
130- }
131- Mutation::Room(
132- RoomCommand::Leave(room_id)
133- | RoomCommand::Forget(room_id)
134- | RoomCommand::AcceptInvite(room_id)
135- | RoomCommand::RejectInvite(room_id)
136- | RoomCommand::InviteUser { room_id, .. }
137- | RoomCommand::KickUser { room_id, .. }
138- | RoomCommand::BanUser { room_id, .. }
139- | RoomCommand::UnbanUser { room_id, .. }
140- | RoomCommand::SetPowerLevels { room_id, .. }
141- | RoomCommand::SetUserPowerLevel { room_id, .. }
142- | RoomCommand::SetAccountData { room_id, .. },
143- ) => room_id,
144- Mutation::Room(_) => return None,
145- };
146- Some(Key::Room(room_id.clone()))
147-}
148-
149-/// dms made here that the next sync hasn't told the sdk about yet, since `get_dm_room`
150-/// only knows `m.direct` once it comes back round
151-type FreshDms = Arc<std::sync::Mutex<HashMap<OwnedUserId, OwnedRoomId>>>;
152-
153-fn spawn(
154- tasks: &mut TaskSet<Key>,
155- client: &Client,
156- fresh_dms: &FreshDms,
157- key: Key,
158- job: MutationJob,
159-) {
160- let client = client.clone();
161- let fresh_dms = fresh_dms.clone();
162- tasks.spawn(async move {
163- let Job { command, reply } = job;
164- reply.send(guarded("room command", execute(&client, &fresh_dms, command)).await);
165- key
166- });
167-}
168-
169-async fn execute(
170- client: &Client,
171- fresh_dms: &FreshDms,
172- mutation: Mutation,
173-) -> Result<CommandOutput, Error> {
174- let command = match mutation {
175- Mutation::GlobalAccountData {
176- event_type,
177- content,
178- } => {
179- client
180- .account()
181- .set_account_data_raw(event_type, content)
182- .await
183- .map_err(|source| Error::from_sdk("writing account data failed", source))?;
184- return Ok(CommandOutput::Done);
185- }
186- Mutation::Room(command) => command,
187- };
188- let room = |room_id: &OwnedRoomId| client.get_room(room_id).ok_or_else(Error::room_unknown);
189- let joined = |room: Room| CommandOutput::Room(room.room_id().to_owned());
190- let sdk = |message: &'static str| move |source| Error::from_sdk(message, source);
191- match command {
192- RoomCommand::Join { room, via } => client
193- .join_room_by_id_or_alias(&room, &via)
194- .await
195- .map(joined)
196- .map_err(sdk("joining room failed")),
197- RoomCommand::Leave(room_id) => room(&room_id)?
198- .leave()
199- .await
200- .map(done)
201- .map_err(sdk("leaving room failed")),
202- RoomCommand::Forget(room_id) => room(&room_id)?
203- .forget()
204- .await
205- .map(done)
206- .map_err(sdk("forgetting room failed")),
207- RoomCommand::AcceptInvite(room_id) => room(&room_id)?
208- .join()
209- .await
210- .map(done)
211- .map_err(sdk("accepting invite failed")),
212- RoomCommand::RejectInvite(room_id) => room(&room_id)?
213- .leave()
214- .await
215- .map(done)
216- .map_err(sdk("rejecting invite failed")),
217- RoomCommand::InviteUser { room_id, user_id } => room(&room_id)?
218- .invite_user_by_id(&user_id)
219- .await
220- .map(done)
221- .map_err(sdk("inviting user failed")),
222- RoomCommand::KickUser {
223- room_id,
224- user_id,
225- reason,
226- } => room(&room_id)?
227- .kick_user(&user_id, reason.as_deref())
228- .await
229- .map(done)
230- .map_err(sdk("kicking user failed")),
231- RoomCommand::BanUser {
232- room_id,
233- user_id,
234- reason,
235- } => room(&room_id)?
236- .ban_user(&user_id, reason.as_deref())
237- .await
238- .map(done)
239- .map_err(sdk("banning user failed")),
240- RoomCommand::UnbanUser {
241- room_id,
242- user_id,
243- reason,
244- } => room(&room_id)?
245- .unban_user(&user_id, reason.as_deref())
246- .await
247- .map(done)
248- .map_err(sdk("unbanning user failed")),
249- RoomCommand::SetPowerLevels { room_id, changes } => {
250- apply_power_level_changes(&room(&room_id)?, changes)
251- .await
252- .map(done)
253- }
254- RoomCommand::SetUserPowerLevel {
255- room_id,
256- user_id,
257- level,
258- } => room(&room_id)?
259- .update_power_levels(vec![(&user_id, power_level(level)?)])
260- .await
261- .map(done)
262- .map_err(sdk("updating user power level failed")),
263- RoomCommand::SetAccountData {
264- room_id,
265- event_type,
266- content,
267- } => room(&room_id)?
268- .set_account_data_raw(event_type, content)
269- .await
270- .map(done)
271- .map_err(sdk("writing room account data failed")),
272- RoomCommand::Create(options) => client
273- .create_room(create_room_request(options))
274- .await
275- .map(joined)
276- .map_err(sdk("creating room failed")),
277- RoomCommand::CreateDirect(user_id) => {
278- if let Some(existing) = client.get_dm_room(&user_id) {
279- return Ok(joined(existing));
280- }
281- let fresh = fresh_dms
282- .lock()
283- .expect("nothing panics holding this")
284- .get(&user_id)
285- .cloned();
286- if let Some(room_id) = fresh.filter(|room_id| client.get_room(room_id).is_some()) {
287- return Ok(CommandOutput::Room(room_id));
288- }
289- let room = client
290- .create_dm(&user_id)
291- .await
292- .map_err(sdk("creating direct room failed"))?;
293- fresh_dms
294- .lock()
295- .expect("nothing panics holding this")
296- .insert(user_id, room.room_id().to_owned());
297- Ok(joined(room))
298- }
299- _ => Err(Error::new(ErrorKind::Other, "room command was misrouted")),
300- }
301-}
302-
303-/// one state event carrying every requested change, since the sdk's own patcher is private
304-async fn apply_power_level_changes(room: &Room, changes: PowerLevelChanges) -> Result<(), Error> {
305- let mut levels = room
306- .power_levels()
307- .await
308- .map_err(|source| Error::from_sdk("reading power levels failed", source.into()))?;
309- for (target, value) in [
310- (&mut levels.ban, changes.ban),
311- (&mut levels.invite, changes.invite),
312- (&mut levels.kick, changes.kick),
313- (&mut levels.redact, changes.redact),
314- (&mut levels.events_default, changes.events_default),
315- (&mut levels.state_default, changes.state_default),
316- (&mut levels.users_default, changes.users_default),
317- (&mut levels.notifications.room, changes.room_notification),
318- ] {
319- if let Some(value) = value {
320- *target = power_level(value)?;
321- }
322- }
323- for (event_type, value) in [
324- (StateEventType::RoomName.into(), changes.room_name),
325- (StateEventType::RoomAvatar.into(), changes.room_avatar),
326- (StateEventType::RoomTopic.into(), changes.room_topic),
327- (StateEventType::SpaceChild.into(), changes.space_child),
328- (MessageLikeEventType::Beacon.into(), changes.beacon),
329- (StateEventType::BeaconInfo.into(), changes.beacon_info),
330- ] {
331- if let Some(value) = value {
332- levels.events.insert(event_type, power_level(value)?);
333- }
334- }
335- let content = RoomPowerLevelsEventContent::try_from(levels).map_err(|source| {
336- Error::with_source(
337- ErrorKind::Invalid,
338- "normalising power levels failed",
339- source,
340- )
341- })?;
342- room.send_state_event(content)
343- .await
344- .map(drop)
345- .map_err(|source| Error::from_sdk("updating power levels failed", source))
346-}
347-
348-fn done<T>(_: T) -> CommandOutput {
349- CommandOutput::Done
350-}
351-
352-fn power_level(value: i64) -> Result<Int, Error> {
353- value.try_into().map_err(|source| {
354- Error::with_source(
355- ErrorKind::Invalid,
356- "power level is outside the Matrix integer range",
357- source,
358- )
359- })
360-}
361-
362-fn create_room_request(options: CreateRoomOptions) -> CreateRoomRequest {
363- let mut request = CreateRoomRequest::new();
364- request.name = options.name;
365- request.topic = options.topic;
366- request.room_alias_name = options.alias_localpart;
367- request.invite = options.invite;
368- request.is_direct = options.direct;
369- request.visibility = match options.visibility {
370- RoomVisibility::Private => SdkVisibility::Private,
371- RoomVisibility::Public => SdkVisibility::Public,
372- };
373- request.preset = options.preset.map(|preset| match preset {
374- RoomPreset::PrivateChat => SdkPreset::PrivateChat,
375- RoomPreset::TrustedPrivateChat => SdkPreset::TrustedPrivateChat,
376- RoomPreset::PublicChat => SdkPreset::PublicChat,
377- });
378- request
379-}
Deleted file: vendor/macltok/src/engine/prepare.rs−43 deletions
@@ -1,43 +0,0 @@
1-use std::collections::HashMap;
2-
3-use futures_util::{
4- FutureExt,
5- future::{Shared, WeakShared},
6-};
7-use matrix_sdk::Client;
8-use ruma::OwnedRoomId;
9-
10-use super::executor::EngineBoxFuture;
11-use crate::Error;
12-
13-type Flight = Shared<EngineBoxFuture<'static, Result<(), Error>>>;
14-
15-/// one warm-up per room at a time, later asks join the one in flight
16-#[derive(Default)]
17-pub(super) struct Preparations(
18- HashMap<OwnedRoomId, WeakShared<EngineBoxFuture<'static, Result<(), Error>>>>,
19-);
20-
21-impl Preparations {
22- pub(super) fn join(&mut self, client: &Client, room_id: OwnedRoomId) -> Flight {
23- self.0.retain(|_, flight| flight.upgrade().is_some());
24- if let Some(flight) = self.0.get(&room_id).and_then(WeakShared::upgrade) {
25- return flight;
26- }
27- let work: EngineBoxFuture<'static, _> = Box::pin(prepare(client.clone(), room_id.clone()));
28- let flight = work.shared();
29- if let Some(weak) = flight.downgrade() {
30- self.0.insert(room_id, weak);
31- }
32- flight
33- }
34-}
35-
36-/// the sdk's own pre-send work, run early. a send landing mid-share waits on the
37-/// sdk's per-room share lock instead of starting another (forked sdk, see design-decisions)
38-async fn prepare(client: Client, room_id: OwnedRoomId) -> Result<(), Error> {
39- let room = client.get_room(&room_id).ok_or_else(Error::room_unknown)?;
40- room.prepare_encryption()
41- .await
42- .map_err(|source| Error::from_sdk("preparing room encryption failed", source))
43-}
Deleted file: vendor/macltok/src/engine/room.rs−459 deletions
@@ -1,459 +0,0 @@
1-use std::collections::{HashMap, VecDeque};
2-
3-use matrix_sdk::{Room, RoomState};
4-use matrix_sdk_ui::timeline::{RoomExt, Timeline, TimelineFocus, TimelineReadReceiptTracking};
5-use ruma::{OwnedEventId, OwnedRoomId};
6-use tokio::sync::mpsc;
7-
8-use super::executor::{AbortHandle, JoinError, TaskSet, is_panic, resume_unwind};
9-use super::timeline::{SessionContext, TaskGuard, TimelineSession};
10-use crate::{
11- Error, ErrorKind, Event, TimelineCommand, TimelineId, TypingState, transport::Responder,
12-};
13-
14-pub(super) enum ActorRequest {
15- /// opening an open room answers with a fresh reset
16- Open(Responder),
17- Execute(TimelineCommand, Responder),
18- Close(Responder),
19-}
20-
21-impl ActorRequest {
22- pub(super) fn into_reply(self) -> Responder {
23- match self {
24- Self::Open(reply) | Self::Execute(_, reply) | Self::Close(reply) => reply,
25- }
26- }
27-}
28-
29-pub(super) enum ActorNote {
30- /// stopping on its own, after a leave or a kick. anything sent its way now comes back in `Exited`
31- Leaving {
32- room_id: OwnedRoomId,
33- generation: u64,
34- },
35- Exited {
36- room_id: OwnedRoomId,
37- generation: u64,
38- /// requests that arrived too late to be served, for the engine to answer or replay
39- leftovers: Vec<ActorRequest>,
40- },
41-}
42-
43-pub(super) struct RoomActor {
44- pub(super) room_id: OwnedRoomId,
45- pub(super) generation: u64,
46- pub(super) room: Room,
47- pub(super) requests: mpsc::Receiver<ActorRequest>,
48- pub(super) notes: mpsc::UnboundedSender<ActorNote>,
49- pub(super) context: SessionContext,
50- pub(super) panics: mpsc::UnboundedReceiver<&'static str>,
51- pub(super) opening: Vec<Responder>,
52-}
53-
54-impl RoomActor {
55- pub(super) async fn run(mut self) {
56- self.serve().await;
57- self.requests.close();
58- let mut leftovers = Vec::new();
59- while let Ok(request) = self.requests.try_recv() {
60- leftovers.push(request);
61- }
62- let _ = self.notes.send(ActorNote::Exited {
63- room_id: self.room_id.clone(),
64- generation: self.generation,
65- leftovers,
66- });
67- }
68-
69- async fn serve(&mut self) {
70- let timeline = match build_timeline(
71- &self.room,
72- TimelineFocus::Live {
73- hide_threaded_events: true,
74- },
75- )
76- .await
77- {
78- Ok(timeline) => timeline,
79- Err(error) => return self.refuse_everything(error).await,
80- };
81- let Ok(room_timeline) = TimelineSession::start(
82- TimelineId::Room(self.room_id.clone()),
83- timeline,
84- self.context.clone(),
85- )
86- .await
87- else {
88- return;
89- };
90- for reply in self.opening.drain(..) {
91- reply.done(Ok(()));
92- }
93- let (typing_guard, typing) = self.room.subscribe_to_typing_notifications();
94- let mut typing_watcher = TaskGuard(self.context.supervisor.spawn(
95- "typing watcher",
96- watch_typing(self.room_id.clone(), typing, self.context.events.clone()),
97- ));
98- let mut timelines = Timelines {
99- room: room_timeline,
100- threads: Threads::new(self.requests.max_capacity()),
101- work: TaskSet::new(),
102- };
103- let close = self.serve_open(&mut timelines).await;
104- if close.is_none() {
105- self.requests.close();
106- let _ = self.notes.send(ActorNote::Leaving {
107- room_id: self.room_id.clone(),
108- generation: self.generation,
109- });
110- }
111-
112- // let redactions and receipts that already left finish rather than lie about them
113- while timelines.work.join_next().await.is_some() {}
114- drop(typing_guard);
115- typing_watcher.0.abort();
116- let _ = (&mut typing_watcher.0).await;
117- let _ = self
118- .context
119- .events
120- .send(Event::TypingChanged(TypingState {
121- room_id: self.room_id.clone(),
122- users: Vec::new(),
123- }))
124- .await;
125- timelines.threads.close().await;
126- timelines.room.close().await;
127- if let Some(reply) = close {
128- reply.done(Ok(()));
129- }
130- }
131-
132- /// returns the close request's responder, if a close is why we stopped
133- async fn serve_open(&mut self, timelines: &mut Timelines) -> Option<Responder> {
134- let mut room_info = self.room.subscribe_info();
135- // a leave that landed while the timeline was building won't come round again
136- if matches!(self.room.state(), RoomState::Left | RoomState::Banned) {
137- return None;
138- }
139- loop {
140- tokio::select! {
141- Some(info) = room_info.next() => {
142- if matches!(info.state(), RoomState::Left | RoomState::Banned) {
143- tracing::debug!(room_id = %self.room_id, "room actor stopped because we left the room");
144- return None;
145- }
146- }
147- request = self.requests.recv() => match request {
148- None => return None,
149- Some(ActorRequest::Close(reply)) => return Some(reply),
150- Some(ActorRequest::Open(reply)) => reply.done(timelines.room.resync().await),
151- Some(ActorRequest::Execute(command, reply)) => {
152- timelines.execute(command, reply, &self.room).await;
153- }
154- },
155- Some(built) = timelines.threads.builds.join_next(), if !timelines.threads.builds.is_empty() => {
156- timelines.thread_built(built, &self.room_id, &self.context).await;
157- }
158- Some(label) = self.panics.recv() => panic!("{label} panicked in room {}", self.room_id),
159- Some(done) = timelines.work.join_next(), if !timelines.work.is_empty() => {
160- if let Err(error) = done && is_panic(&error) {
161- tracing::warn!(room_id = %self.room_id, %error, "timeline operation panicked");
162- }
163- }
164- }
165- }
166- }
167-
168- async fn refuse_everything(&mut self, error: matrix_sdk_ui::timeline::Error) {
169- tracing::warn!(%error, room_id = %self.room_id, "room actor could not build its timeline");
170- let error = Error::with_source(ErrorKind::Timeline, "room timeline build failed", error);
171- for reply in self.opening.drain(..) {
172- reply.done(Err(error.clone()));
173- }
174- self.requests.close();
175- while let Some(request) = self.requests.recv().await {
176- request.into_reply().done(Err(error.clone()));
177- }
178- }
179-}
180-
181-async fn build_timeline(
182- room: &Room,
183- focus: TimelineFocus,
184-) -> Result<Timeline, matrix_sdk_ui::timeline::Error> {
185- room.timeline_builder()
186- .with_focus(focus)
187- .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents)
188- .build()
189- .await
190-}
191-
192-async fn watch_typing(
193- room_id: OwnedRoomId,
194- mut typing: tokio::sync::broadcast::Receiver<Vec<ruma::OwnedUserId>>,
195- events: crate::transport::EventSender,
196-) {
197- loop {
198- match typing.recv().await {
199- Ok(users) => {
200- let state = TypingState {
201- room_id: room_id.clone(),
202- users,
203- };
204- if events.send(Event::TypingChanged(state)).await.is_err() {
205- return;
206- }
207- }
208- Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
209- tracing::warn!(%room_id, skipped, "typing watcher lagged");
210- }
211- Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
212- }
213- }
214-}
215-
216-struct Timelines {
217- room: TimelineSession,
218- threads: Threads,
219- work: TaskSet<()>,
220-}
221-
222-impl Timelines {
223- async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {
224- match command {
225- TimelineCommand::OpenThread { root_event_id, .. } => {
226- self.threads.open(root_event_id, reply, room);
227- }
228- TimelineCommand::CloseThread { root_event_id, .. } => {
229- reply.done(self.threads.close_one(&root_event_id).await);
230- }
231- TimelineCommand::Resync {
232- timeline_id: TimelineId::Room(_),
233- } => reply.done(self.room.resync().await),
234- command => {
235- let root = command
236- .timeline_id()
237- .and_then(TimelineId::thread_root)
238- .cloned();
239- let Some(root) = root else {
240- return self.room.execute(command, reply, &mut self.work).await;
241- };
242- if let TimelineCommand::MarkRead {
243- timeline_id,
244- event_id,
245- } = &command
246- && !self.threads.open.contains_key(&root)
247- {
248- let client = room.client();
249- let (timeline_id, event_id) = (timeline_id.clone(), event_id.clone());
250- self.work.spawn(async move {
251- reply.done(
252- super::commands::mark_read(&client, &timeline_id, event_id).await,
253- );
254- });
255- return;
256- }
257- match self.threads.route(root, command, reply) {
258- Routed::Open(session, TimelineCommand::Resync { .. }, reply) => {
259- reply.done(session.resync().await);
260- }
261- Routed::Open(session, command, reply) => {
262- session.execute(command, reply, &mut self.work).await;
263- }
264- Routed::Queued => {}
265- }
266- }
267- }
268- }
269-
270- async fn thread_built(
271- &mut self,
272- built: Result<Built, JoinError>,
273- room_id: &OwnedRoomId,
274- context: &SessionContext,
275- ) {
276- let Some((root, queued, timeline)) = self.threads.built(built) else {
277- return;
278- };
279- let timeline = match timeline {
280- Ok(timeline) => timeline,
281- Err(error) => {
282- tracing::warn!(%error, %room_id, %root, "thread timeline build failed");
283- let error =
284- Error::with_source(ErrorKind::Timeline, "thread timeline build failed", error);
285- for (_, reply) in queued {
286- reply.done(Err(error.clone()));
287- }
288- return;
289- }
290- };
291- let id = TimelineId::thread(room_id.clone(), root.clone());
292- let Ok(session) = TimelineSession::start(id, timeline, context.clone()).await else {
293- return;
294- };
295- let session = self
296- .threads
297- .open
298- .entry(root)
299- .insert_entry(session)
300- .into_mut();
301- for (command, reply) in queued {
302- match command {
303- None => reply.done(Ok(())),
304- Some(TimelineCommand::Resync { .. }) => reply.done(session.resync().await),
305- Some(command) => session.execute(command, reply, &mut self.work).await,
306- }
307- }
308- }
309-}
310-
311-type Built = (
312- OwnedEventId,
313- u64,
314- Result<Timeline, matrix_sdk_ui::timeline::Error>,
315-);
316-
317-/// `None` is the open itself, everything else waits behind it
318-type Queued = VecDeque<(Option<TimelineCommand>, Responder)>;
319-
320-enum Routed<'a> {
321- Open(&'a mut TimelineSession, TimelineCommand, Responder),
322- Queued,
323-}
324-
325-struct OpeningThread {
326- generation: u64,
327- build: AbortHandle,
328- queued: Queued,
329-}
330-
331-struct Threads {
332- open: HashMap<OwnedEventId, TimelineSession>,
333- opening: HashMap<OwnedEventId, OpeningThread>,
334- builds: TaskSet<Built>,
335- next_generation: u64,
336- queued: usize,
337- capacity: usize,
338-}
339-
340-impl Threads {
341- fn new(capacity: usize) -> Self {
342- Self {
343- open: HashMap::new(),
344- opening: HashMap::new(),
345- builds: TaskSet::new(),
346- next_generation: 0,
347- queued: 0,
348- capacity,
349- }
350- }
351-
352- fn enqueue(&mut self, root: &OwnedEventId, command: Option<TimelineCommand>, reply: Responder) {
353- if self.queued >= self.capacity {
354- return reply.done(Err(Error::new(
355- ErrorKind::QueueFull,
356- "thread command queue is full",
357- )));
358- }
359- let Some(opening) = self.opening.get_mut(root) else {
360- return reply.done(Err(Error::new(
361- ErrorKind::TimelineNotOpen,
362- "thread timeline is not open",
363- )));
364- };
365- opening.queued.push_back((command, reply));
366- self.queued += 1;
367- }
368-
369- fn open(&mut self, root: OwnedEventId, reply: Responder, room: &Room) {
370- if self.open.contains_key(&root) {
371- return reply.done(Ok(()));
372- }
373- if !self.opening.contains_key(&root) {
374- let generation = self.next_generation;
375- self.next_generation = self.next_generation.wrapping_add(1);
376- let room = room.clone();
377- let build_root = root.clone();
378- let build = self.builds.spawn(async move {
379- let focus = TimelineFocus::Thread {
380- root_event_id: build_root.clone(),
381- };
382- (build_root, generation, build_timeline(&room, focus).await)
383- });
384- self.opening.insert(
385- root.clone(),
386- OpeningThread {
387- generation,
388- build,
389- queued: VecDeque::new(),
390- },
391- );
392- }
393- self.enqueue(&root, None, reply);
394- }
395-
396- fn route(
397- &mut self,
398- root: OwnedEventId,
399- command: TimelineCommand,
400- reply: Responder,
401- ) -> Routed<'_> {
402- if !self.open.contains_key(&root) {
403- self.enqueue(&root, Some(command), reply);
404- return Routed::Queued;
405- }
406- let session = self.open.get_mut(&root).expect("thread was just checked");
407- Routed::Open(session, command, reply)
408- }
409-
410- /// `None` when the build belongs to a thread that was closed while it ran
411- fn built(
412- &mut self,
413- built: Result<Built, JoinError>,
414- ) -> Option<(
415- OwnedEventId,
416- Queued,
417- Result<Timeline, matrix_sdk_ui::timeline::Error>,
418- )> {
419- let (root, generation, timeline) = match built {
420- Ok(built) => built,
421- Err(error) if error.is_cancelled() => return None,
422- Err(error) => resume_unwind(error),
423- };
424- if self
425- .opening
426- .get(&root)
427- .is_none_or(|opening| opening.generation != generation)
428- {
429- return None;
430- }
431- let opening = self.opening.remove(&root)?;
432- self.queued -= opening.queued.len();
433- Some((root, opening.queued, timeline))
434- }
435-
436- async fn close_one(&mut self, root: &OwnedEventId) -> Result<(), Error> {
437- if let Some(session) = self.open.remove(root) {
438- session.close().await;
439- return Ok(());
440- }
441- let Some(opening) = self.opening.remove(root) else {
442- return Err(Error::new(
443- ErrorKind::TimelineNotOpen,
444- "thread timeline is not open",
445- ));
446- };
447- opening.build.abort();
448- self.queued -= opening.queued.len();
449- Ok(())
450- }
451-
452- async fn close(&mut self) {
453- self.builds.shutdown().await;
454- self.opening.clear();
455- for (_, session) in self.open.drain() {
456- session.close().await;
457- }
458- }
459-}
Deleted file: vendor/macltok/src/engine/session.rs−132 deletions
@@ -1,132 +0,0 @@
1-use std::sync::Arc;
2-
3-use matrix_sdk::{BoxFuture, Client, SessionChange};
4-use ruma::OwnedDeviceId;
5-use tokio::sync::{
6- broadcast::{self, error::RecvError},
7- mpsc, watch,
8-};
9-
10-use super::Logouts;
11-use crate::{Error, Event, Termination, transport::EventSender};
12-
13-#[cfg(not(target_family = "wasm"))]
14-pub(crate) type SaveRecord = dyn Fn(Client) -> BoxFuture<'static, Result<(), Error>> + Send + Sync;
15-#[cfg(target_family = "wasm")]
16-pub(crate) type SaveRecord = dyn Fn(Client) -> BoxFuture<'static, Result<(), Error>>;
17-
18-/// how the engine keeps the saved record current, handed over by `Session::build`
19-pub(crate) struct Persistence {
20- pub(crate) save: Box<SaveRecord>,
21- /// what the sdk's own save callback made of each refresh, when the store could write
22- /// there and then
23- pub(crate) inline: mpsc::UnboundedReceiver<Result<(), Error>>,
24-}
25-
26-impl Persistence {
27- async fn persist(&mut self, client: &Client) -> Result<(), Error> {
28- let mut inline = None;
29- while let Ok(result) = self.inline.try_recv() {
30- inline = Some(result);
31- }
32- if let Some(Ok(())) = inline {
33- return Ok(());
34- }
35- // an inline save for a newer refresh can land while this one writes, so go again
36- // until what's on disk is what the client holds
37- loop {
38- let tokens = client.session_tokens();
39- (self.save)(client.clone()).await?;
40- if client.session_tokens() == tokens {
41- return Ok(());
42- }
43- }
44- }
45-}
46-
47-pub(super) enum Notice {
48- Event(Event),
49- Rejected(Option<OwnedDeviceId>),
50-}
51-
52-/// saves and notices are split, so a full event stream never holds a save back
53-pub(super) async fn session(
54- client: Client,
55- mut changes: broadcast::Receiver<SessionChange>,
56- mut persistence: Option<Persistence>,
57- notices: mpsc::UnboundedSender<Notice>,
58- device_id: OwnedDeviceId,
59- logouts: Arc<watch::Sender<Logouts>>,
60-) {
61- loop {
62- let event = match changes.recv().await {
63- // a missed refresh costs one needless save, a missed rejection comes round again
64- Ok(SessionChange::TokensRefreshed) | Err(RecvError::Lagged(_)) => {
65- match persistence.as_mut() {
66- None => Event::SessionTokensChanged,
67- Some(persistence) => match persistence.persist(&client).await {
68- Ok(()) => Event::SessionTokensChanged,
69- Err(error) => Event::SessionSaveFailed {
70- message: error.message,
71- },
72- },
73- }
74- }
75- Ok(SessionChange::UnknownToken(data)) => {
76- let soft_logout = data.soft_logout.then_some(device_id);
77- let mut held = false;
78- // our own logout revokes the token, so a rejection meanwhile is its doing
79- logouts.send_if_modified(|logouts| {
80- held = logouts.in_flight > 0;
81- if held {
82- logouts.rejected = Some(soft_logout.clone());
83- }
84- held
85- });
86- if !held {
87- let _ = notices.send(Notice::Rejected(soft_logout));
88- }
89- return;
90- }
91- Err(RecvError::Closed) => return,
92- };
93- if notices.send(Notice::Event(event)).is_err() {
94- return;
95- }
96- }
97-}
98-
99-pub(super) async fn session_notices(
100- mut notices: mpsc::UnboundedReceiver<Notice>,
101- events: EventSender,
102- stop: mpsc::UnboundedSender<Termination>,
103-) {
104- while let Some(notice) = notices.recv().await {
105- match notice {
106- Notice::Event(event) => {
107- if events.send(event).await.is_err() {
108- return;
109- }
110- }
111- Notice::Rejected(soft_logout) => {
112- return reject_session(&events, &stop, soft_logout).await;
113- }
114- }
115- }
116-}
117-
118-pub(super) async fn reject_session(
119- events: &EventSender,
120- stop: &mpsc::UnboundedSender<Termination>,
121- soft_logout: Option<OwnedDeviceId>,
122-) {
123- let _ = events
124- .send(Event::SessionAccessRejected {
125- soft_logout: soft_logout.clone(),
126- })
127- .await;
128- let _ = stop.send(match soft_logout {
129- Some(device_id) => Termination::SoftLoggedOut { device_id },
130- None => Termination::AccessRevoked,
131- });
132-}
Deleted file: vendor/macltok/src/engine/timeline.rs−468 deletions
@@ -1,468 +0,0 @@
1-use std::{collections::HashSet, future::Future, panic::AssertUnwindSafe, sync::Arc};
2-
3-use futures_util::{FutureExt, StreamExt};
4-use matrix_sdk::SendOutsideWasm;
5-use matrix_sdk::{
6- room::edit::EditedContent,
7- room::reply::{EnforceThread, Reply as SdkReply},
8- ruma::{
9- api::client::receipt::create_receipt::v3::ReceiptType,
10- events::{
11- Mentions,
12- room::message::{
13- AddMentions, Relation, ReplyWithinThread, RoomMessageEventContentWithoutRelation,
14- },
15- },
16- },
17-};
18-use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};
19-use ruma::{OwnedEventId, OwnedUserId};
20-use tokio::sync::mpsc;
21-
22-use super::executor::{EngineBoxFuture, JoinHandle, TaskSet, spawn};
23-use crate::{
24- Error, ErrorKind, Event, EventRef, HistoryState, MessageContent, MessageEditContent,
25- MessageMentions, RichText, TimelineCommand, TimelineId,
26- engine::MediaService,
27- normalise::{MediaScope, normalise_timeline, normalise_timeline_diffs},
28- transport::{EventSender, Responder, SendEventError},
29-};
30-
31-const PAGINATION_QUEUE: usize = 32;
32-
33-/// long-lived tasks inside a room actor report panics here so the actor can die loudly
34-#[derive(Clone)]
35-pub(super) struct Supervisor(mpsc::UnboundedSender<&'static str>);
36-
37-impl Supervisor {
38- pub(super) fn new() -> (Self, mpsc::UnboundedReceiver<&'static str>) {
39- let (sender, receiver) = mpsc::unbounded_channel();
40- (Self(sender), receiver)
41- }
42-
43- pub(super) fn spawn(
44- &self,
45- label: &'static str,
46- task: impl Future<Output = ()> + SendOutsideWasm + 'static,
47- ) -> JoinHandle<()> {
48- let panics = self.0.clone();
49- spawn(async move {
50- if AssertUnwindSafe(task).catch_unwind().await.is_err() {
51- let _ = panics.send(label);
52- }
53- })
54- }
55-}
56-
57-/// aborts its task when dropped, so an actor that dies mid-flight takes its helpers with it
58-pub(super) struct TaskGuard(pub(super) JoinHandle<()>);
59-
60-impl Drop for TaskGuard {
61- fn drop(&mut self) {
62- self.0.abort();
63- }
64-}
65-
66-/// what a timeline session needs from the room that owns it
67-#[derive(Clone)]
68-pub(super) struct SessionContext {
69- pub(super) events: EventSender,
70- pub(super) media: Arc<MediaService>,
71- pub(super) current_user_id: Option<OwnedUserId>,
72- pub(super) initial_history_limit: u16,
73- pub(super) supervisor: Supervisor,
74-}
75-
76-pub(super) struct TimelineSession {
77- pub(super) id: TimelineId,
78- timeline: Arc<Timeline>,
79- context: SessionContext,
80- scope: MediaScope,
81- watcher: JoinHandle<()>,
82- pagination: mpsc::Sender<PaginationRequest>,
83- pagination_worker: JoinHandle<()>,
84-}
85-
86-struct PaginationRequest {
87- limit: u16,
88- reply: Option<Responder>,
89-}
90-
91-impl TimelineSession {
92- pub(super) async fn start(
93- id: TimelineId,
94- timeline: Timeline,
95- context: SessionContext,
96- ) -> Result<Self, SendEventError> {
97- let timeline = Arc::new(timeline);
98- let scope = MediaScope::new();
99- let watcher = start_watcher(&id, &timeline, &context, &scope).await?;
100- let (pagination, requests) = mpsc::channel(PAGINATION_QUEUE);
101- let pagination_worker = context.supervisor.spawn(
102- "pagination worker",
103- run_pagination(
104- requests,
105- timeline.clone(),
106- context.events.clone(),
107- id.clone(),
108- ),
109- );
110- let session = Self {
111- id,
112- timeline,
113- scope,
114- watcher,
115- pagination,
116- pagination_worker,
117- context,
118- };
119- if session.context.initial_history_limit > 0 {
120- session.paginate(session.context.initial_history_limit, None);
121- }
122- Ok(session)
123- }
124-
125- /// swaps in a fresh watcher. the old one is dead before the reset goes out,
126- /// so no stale diff can land after it
127- pub(super) async fn resync(&mut self) -> Result<(), Error> {
128- self.watcher.abort();
129- let _ = (&mut self.watcher).await;
130- self.watcher = start_watcher(&self.id, &self.timeline, &self.context, &self.scope)
131- .await
132- .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;
133- Ok(())
134- }
135-
136- pub(super) fn paginate(&self, limit: u16, reply: Option<Responder>) {
137- if let Err(error) = self.pagination.try_send(PaginationRequest { limit, reply }) {
138- let (request, error) = match error {
139- mpsc::error::TrySendError::Full(request) => (
140- request,
141- Error::new(ErrorKind::QueueFull, "pagination queue is full"),
142- ),
143- mpsc::error::TrySendError::Closed(request) => (
144- request,
145- Error::new(ErrorKind::TimelineNotOpen, "timeline is closing"),
146- ),
147- };
148- if let Some(reply) = request.reply {
149- reply.done(Err(error));
150- }
151- }
152- }
153-
154- /// pending paginations answer `Interrupted` as their responders drop
155- pub(super) async fn close(mut self) {
156- self.pagination_worker.abort();
157- let _ = (&mut self.pagination_worker).await;
158- self.watcher.abort();
159- let _ = (&mut self.watcher).await;
160- // after the watcher, or a last diff could register media for a timeline that's gone
161- self.context.media.release(&self.scope).await;
162- let _ = self
163- .context
164- .events
165- .send(Event::TimelineClosed {
166- timeline_id: self.id.clone(),
167- })
168- .await;
169- }
170-
171- pub(super) async fn execute(
172- &self,
173- command: TimelineCommand,
174- reply: Responder,
175- work: &mut TaskSet<()>,
176- ) {
177- let timeline = self.timeline.clone();
178- let thread_root = self.id.thread_root().cloned();
179- let background: EngineBoxFuture<'static, Result<(), Error>> = match command {
180- TimelineCommand::Paginate { limit, .. } => return self.paginate(limit, Some(reply)),
181- // network-bound, so it runs beside the actor instead of queueing the room behind it
182- TimelineCommand::Redact { target, reason, .. } => Box::pin(async move {
183- timeline
184- .redact(&item_id(target), reason.as_deref())
185- .await
186- .map_err(timeline_error)
187- }),
188- TimelineCommand::ToggleReaction {
189- target,
190- key,
191- shortcode,
192- ..
193- } => Box::pin(async move {
194- timeline
195- .toggle_reaction_with_shortcode(&item_id(target), &key, shortcode.as_deref())
196- .await
197- .map(drop)
198- .map_err(timeline_error)
199- }),
200- TimelineCommand::MarkRead { event_id, .. } => Box::pin(async move {
201- let receipt_failed =
202- |source| Error::from_sdk("sending read receipt failed", source);
203- timeline
204- .send_single_receipt(ReceiptType::Read, event_id.clone())
205- .await
206- .map_err(receipt_failed)?;
207- if thread_root.is_none() {
208- timeline
209- .send_single_receipt(ReceiptType::FullyRead, event_id)
210- .await
211- .map_err(receipt_failed)?;
212- }
213- Ok(())
214- }),
215- command => return reply.done(self.execute_serial(command).await),
216- };
217- work.spawn(async move { reply.done(background.await) });
218- }
219-
220- /// sends stay on the actor so they hit the send queue in the order they were asked for
221- async fn execute_serial(&self, command: TimelineCommand) -> Result<(), Error> {
222- let timeline = &self.timeline;
223- match command {
224- TimelineCommand::Send { content, .. } => {
225- let body = match &content {
226- MessageContent::Text { content, .. }
227- | MessageContent::Emote { content, .. }
228- | MessageContent::Reply { content, .. } => &content.body,
229- };
230- if body.trim().is_empty() {
231- return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
232- }
233- let content = match content {
234- MessageContent::Text { content, mentions } => {
235- message_content(content, mentions, false).with_relation(None)
236- }
237- MessageContent::Emote { content, mentions } => {
238- message_content(content, mentions, true).with_relation(None)
239- }
240- MessageContent::Reply {
241- event_id,
242- content,
243- mentions,
244- notify_replied_to,
245- } => {
246- self.reply_content(event_id, content, mentions, notify_replied_to)
247- .await?
248- }
249- };
250- timeline
251- .send(content.into())
252- .await
253- .map(drop)
254- .map_err(timeline_error)
255- }
256- TimelineCommand::Edit {
257- target, content, ..
258- } => {
259- let (content, mentions, emote) = match content {
260- MessageEditContent::Text { content, mentions } => (content, mentions, false),
261- MessageEditContent::Emote { content, mentions } => (content, mentions, true),
262- };
263- if content.body.trim().is_empty() {
264- return Err(Error::new(ErrorKind::EmptyBody, "message body is empty"));
265- }
266- timeline
267- .edit(
268- &item_id(target),
269- EditedContent::RoomMessage(message_content(content, mentions, emote)),
270- )
271- .await
272- .map_err(timeline_error)
273- }
274- _ => Err(Error::new(
275- ErrorKind::Other,
276- "timeline command was misrouted",
277- )),
278- }
279- }
280-
281- async fn reply_content(
282- &self,
283- event_id: OwnedEventId,
284- content: RichText,
285- mentions: MessageMentions,
286- notify_replied_to: bool,
287- ) -> Result<matrix_sdk::ruma::events::room::message::RoomMessageEventContent, Error> {
288- let thread_root = self.id.thread_root();
289- let content = self
290- .timeline
291- .room()
292- .make_reply_event(
293- message_content(content, mentions, false),
294- SdkReply {
295- event_id,
296- enforce_thread: if thread_root.is_some() {
297- EnforceThread::Threaded(ReplyWithinThread::Yes)
298- } else {
299- EnforceThread::MaybeThreaded
300- },
301- add_mentions: if notify_replied_to {
302- AddMentions::Yes
303- } else {
304- AddMentions::No
305- },
306- },
307- )
308- .await
309- .map_err(|source| {
310- Error::with_source(ErrorKind::Timeline, "reply operation failed", source)
311- })?;
312- if let Some(thread_root) = thread_root
313- && !matches!(
314- content.relates_to.as_ref(),
315- Some(Relation::Thread(thread)) if &thread.event_id == thread_root
316- )
317- {
318- return Err(Error::new(
319- ErrorKind::Invalid,
320- "reply target does not belong to the selected thread",
321- ));
322- }
323- Ok(content)
324- }
325-}
326-
327-impl Drop for TimelineSession {
328- fn drop(&mut self) {
329- self.watcher.abort();
330- self.pagination_worker.abort();
331- }
332-}
333-
334-fn item_id(target: EventRef) -> TimelineEventItemId {
335- match target {
336- EventRef::Remote(event_id) => TimelineEventItemId::EventId(event_id),
337- EventRef::Local(transaction_id) => TimelineEventItemId::TransactionId(transaction_id),
338- }
339-}
340-
341-/// the sdk leaves a reply's target unloaded until someone asks, so the watcher asks, once per event
342-struct ReplyFetcher {
343- timeline: Arc<Timeline>,
344- asked: HashSet<OwnedEventId>,
345- fetches: TaskSet<()>,
346-}
347-
348-impl ReplyFetcher {
349- fn fetch(&mut self, missing: Vec<OwnedEventId>) {
350- for event_id in missing {
351- if self.asked.insert(event_id.clone()) {
352- let timeline = self.timeline.clone();
353- self.fetches.spawn(async move {
354- if let Err(error) = timeline.fetch_details_for_event(&event_id).await {
355- tracing::debug!(%event_id, %error, "fetching reply details failed");
356- }
357- });
358- }
359- }
360- }
361-}
362-
363-async fn start_watcher(
364- id: &TimelineId,
365- timeline: &Arc<Timeline>,
366- context: &SessionContext,
367- scope: &MediaScope,
368-) -> Result<JoinHandle<()>, SendEventError> {
369- let (items, mut stream) = timeline.subscribe().await;
370- let current_user_id = context.current_user_id.clone();
371- let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), scope);
372- context.media.register(normalised.media).await;
373- context
374- .events
375- .send(Event::TimelineReset {
376- timeline_id: id.clone(),
377- items: normalised.items,
378- })
379- .await?;
380- let mut replies = ReplyFetcher {
381- timeline: timeline.clone(),
382- asked: HashSet::new(),
383- fetches: TaskSet::new(),
384- };
385- replies.fetch(normalised.missing_replies);
386- let id = id.clone();
387- let events = context.events.clone();
388- let media = context.media.clone();
389- let scope = scope.clone();
390- Ok(context.supervisor.spawn("timeline watcher", async move {
391- loop {
392- let diffs = tokio::select! {
393- diffs = stream.next() => match diffs {
394- Some(diffs) => diffs,
395- None => return,
396- },
397- Some(_) = replies.fetches.join_next(), if !replies.fetches.is_empty() => continue,
398- };
399- let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);
400- media.register(normalised.media).await;
401- replies.fetch(normalised.missing_replies);
402- let event = Event::TimelineChanged {
403- timeline_id: id.clone(),
404- diffs: normalised.diffs,
405- };
406- if events.send(event).await.is_err() {
407- return;
408- }
409- }
410- }))
411-}
412-
413-async fn run_pagination(
414- mut requests: mpsc::Receiver<PaginationRequest>,
415- timeline: Arc<Timeline>,
416- events: EventSender,
417- timeline_id: TimelineId,
418-) {
419- while let Some(PaginationRequest { limit, reply }) = requests.recv().await {
420- let history = |state| Event::HistoryChanged {
421- timeline_id: timeline_id.clone(),
422- state,
423- };
424- if events.send(history(HistoryState::Loading)).await.is_err() {
425- return;
426- }
427- let result = timeline
428- .paginate_backwards(limit.max(1))
429- .await
430- .map_err(timeline_error);
431- let state = match &result {
432- Ok(true) => HistoryState::StartReached,
433- Ok(false) => HistoryState::Idle,
434- Err(error) => HistoryState::Failed(error.to_string()),
435- };
436- let _ = events.send(history(state)).await;
437- if let Some(reply) = reply {
438- reply.done(result.map(drop));
439- }
440- }
441-}
442-
443-fn message_content(
444- content: RichText,
445- mentions: MessageMentions,
446- emote: bool,
447-) -> RoomMessageEventContentWithoutRelation {
448- let content = match content.formatted_body {
449- Some(formatted_body) if emote => {
450- RoomMessageEventContentWithoutRelation::emote_html(content.body, formatted_body)
451- }
452- Some(formatted_body) => {
453- RoomMessageEventContentWithoutRelation::text_html(content.body, formatted_body)
454- }
455- None if emote => RoomMessageEventContentWithoutRelation::emote_plain(content.body),
456- None => RoomMessageEventContentWithoutRelation::text_plain(content.body),
457- };
458- let content = content.add_mentions(Mentions::with_user_ids(mentions.user_ids));
459- if mentions.room {
460- content.add_mentions(Mentions::with_room_mention())
461- } else {
462- content
463- }
464-}
465-
466-pub(super) fn timeline_error(source: matrix_sdk_ui::timeline::Error) -> Error {
467- Error::with_source(ErrorKind::Timeline, "timeline operation failed", source)
468-}
Deleted file: vendor/macltok/src/engine/url_preview.rs−166 deletions
@@ -1,166 +0,0 @@
1-use std::{
2- collections::{HashMap, VecDeque},
3- sync::{
4- Arc,
5- atomic::{AtomicBool, Ordering},
6- },
7- time::Duration,
8-};
9-
10-use futures_util::{FutureExt, future::WeakShared};
11-use matrix_sdk::{
12- Client, HttpError,
13- ruma::api::{
14- Metadata,
15- client::{authenticated_media, media},
16- },
17-};
18-use ruma::{
19- api::error::ErrorKind as ApiErrorKind,
20- serde::{JsonObject, Raw},
21- time::Instant,
22-};
23-use tokio::sync::Mutex;
24-
25-use super::executor::EngineBoxFuture;
26-use crate::{Error, LinkPreview, normalise::server_preview};
27-
28-const HIT_FOR: Duration = Duration::from_secs(60 * 60);
29-const MISS_FOR: Duration = Duration::from_secs(10 * 60);
30-const REMEMBERED: usize = 512;
31-
32-type Lookup = EngineBoxFuture<'static, Result<Option<LinkPreview>, Error>>;
33-
34-/// the homeserver rate limits `preview_url` and a busy timeline repeats links, so answers are
35-/// kept a while, misses too, and asks for one url at once share one request
36-pub(crate) struct UrlPreviews(Arc<Inner>);
37-
38-struct Inner {
39- client: Client,
40- /// a server that doesn't preview at all says so once and isn't asked again
41- unsupported: AtomicBool,
42- cache: Mutex<Cache>,
43- lookups: Mutex<HashMap<String, WeakShared<Lookup>>>,
44-}
45-
46-#[derive(Default)]
47-struct Cache {
48- entries: HashMap<String, (Instant, Option<LinkPreview>)>,
49- order: VecDeque<String>,
50-}
51-
52-impl UrlPreviews {
53- pub(crate) fn new(client: Client) -> Self {
54- Self(Arc::new(Inner {
55- client,
56- unsupported: AtomicBool::new(false),
57- cache: Mutex::new(Cache::default()),
58- lookups: Mutex::new(HashMap::new()),
59- }))
60- }
61-
62- pub(crate) async fn preview(&self, url: &str) -> Result<Option<LinkPreview>, Error> {
63- if self.0.unsupported.load(Ordering::Relaxed) {
64- return Ok(None);
65- }
66- if let Some(preview) = self.0.cache.lock().await.get(url) {
67- return Ok(preview);
68- }
69- let lookup = {
70- let mut lookups = self.0.lookups.lock().await;
71- lookups.retain(|_, lookup| lookup.upgrade().is_some());
72- match lookups.get(url).and_then(WeakShared::upgrade) {
73- Some(lookup) => lookup,
74- None => {
75- let inner = self.0.clone();
76- let owned = url.to_owned();
77- let lookup: Lookup = Box::pin(async move { inner.look_up(owned).await });
78- let lookup = lookup.shared();
79- if let Some(weak) = lookup.downgrade() {
80- lookups.insert(url.to_owned(), weak);
81- }
82- lookup
83- }
84- }
85- };
86- lookup.await
87- }
88-}
89-
90-impl Inner {
91- async fn look_up(&self, url: String) -> Result<Option<LinkPreview>, Error> {
92- let data = match self.fetch(&url).await {
93- Ok(data) => data,
94- Err(error) => {
95- let Some(api) = error.as_client_api_error() else {
96- return Err(Error::from_sdk(
97- "asking for a url preview failed",
98- error.into(),
99- ));
100- };
101- let unrecognised = matches!(api.error_kind(), Some(ApiErrorKind::Unrecognized))
102- || api.status_code.as_u16() == 405;
103- if unrecognised {
104- self.unsupported.store(true, Ordering::Relaxed);
105- return Ok(None);
106- }
107- // synapse answers 500 for a page it can't download, 502 when dns fails and 403 for
108- // its blocklist (synapse/media/url_previewer.py). a real fault hides in the 500s too,
109- // but a missing card is cheap and only cached ten minutes
110- if !matches!(api.status_code.as_u16(), 400 | 403 | 404 | 500 | 502) {
111- return Err(Error::from_sdk(
112- "asking for a url preview failed",
113- error.into(),
114- ));
115- }
116- None
117- }
118- };
119- let preview = data.and_then(|data| server_preview(&url, &data));
120- self.cache.lock().await.insert(url, preview.clone());
121- Ok(preview)
122- }
123-
124- async fn fetch(&self, url: &str) -> Result<Option<Raw<JsonObject>>, HttpError> {
125- let versions = self.client.supported_versions().await?;
126- let data = if authenticated_media::get_media_preview::v1::Request::PATH_BUILDER
127- .is_supported(&versions)
128- {
129- self.client
130- .send(authenticated_media::get_media_preview::v1::Request::new(
131- url.to_owned(),
132- ))
133- .await?
134- .data
135- } else {
136- #[allow(deprecated)]
137- let request = media::get_media_preview::v3::Request::new(url.to_owned());
138- self.client.send(request).await?.data
139- };
140- Ok(data.map(Raw::from_json))
141- }
142-}
143-
144-impl Cache {
145- fn get(&mut self, url: &str) -> Option<Option<LinkPreview>> {
146- let (at, preview) = self.entries.get(url)?;
147- let lasts = if preview.is_some() { HIT_FOR } else { MISS_FOR };
148- (at.elapsed() < lasts).then(|| preview.clone())
149- }
150-
151- fn insert(&mut self, url: String, preview: Option<LinkPreview>) {
152- if self
153- .entries
154- .insert(url.clone(), (Instant::now(), preview))
155- .is_none()
156- {
157- self.order.push_back(url);
158- }
159- while self.entries.len() > REMEMBERED {
160- let Some(oldest) = self.order.pop_front() else {
161- break;
162- };
163- self.entries.remove(&oldest);
164- }
165- }
166-}
Deleted file: vendor/macltok/src/engine/watchers.rs−565 deletions
@@ -1,565 +0,0 @@
1-use std::{
2- collections::{HashSet, VecDeque},
3- sync::Arc,
4- time::Duration,
5-};
6-
7-use futures_util::StreamExt;
8-use matrix_sdk::{
9- Client,
10- ruma::events::{AnyMessageLikeEventContent, room::message::MessageType},
11- send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendQueueUpdate},
12-};
13-use matrix_sdk_base::RoomInfoNotableUpdateReasons;
14-use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};
15-use ruma::{
16- OwnedRoomId, OwnedTransactionId,
17- events::{AnyGlobalAccountDataEvent, AnySyncStateEvent},
18- serde::Raw,
19- time::Instant,
20-};
21-use tokio::sync::{Notify, broadcast::error::RecvError, watch};
22-
23-use super::executor::TaskSet;
24-use crate::{
25- ConnectionState, Event, UploadOutcome,
26- encryption::encryption_state,
27- error::classify_sdk_error,
28- normalise::{ENABLED_ROOMS, ROOM_PACKS, USER_PACK},
29- transport::EventSender,
30-};
31-
32-const SYNC_RESTART_MIN: Duration = Duration::from_secs(1);
33-const SYNC_RESTART_MAX: Duration = Duration::from_secs(60);
34-const SEND_QUEUE_RETRY: Duration = Duration::from_secs(5);
35-const SYNC_STABLE: Duration = Duration::from_secs(30);
36-
37-pub(super) async fn room_subscriptions(
38- sync: Arc<SyncService>,
39- mut rooms: watch::Receiver<Vec<ruma::OwnedRoomId>>,
40-) {
41- while rooms.changed().await.is_ok() {
42- let room_ids = rooms.borrow_and_update().clone();
43- let room_ids = room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>();
44- sync.room_list_service().subscribe_to_rooms(&room_ids).await;
45- }
46-}
47-
48-/// `states` must be taken before sync starts, a late subscriber never hears the transition it missed
49-pub(super) async fn connection(
50- mut states: impl futures_util::Stream<Item = SyncState> + Unpin,
51- sync: Arc<SyncService>,
52- client: Client,
53- events: EventSender,
54- connection: watch::Sender<ConnectionState>,
55-) {
56- let mut backoff = SYNC_RESTART_MIN;
57- let mut online_since: Option<Instant> = None;
58- while let Some(state) = states.next().await {
59- // only a run that lasted counts as recovered, or a server that answers /versions
60- // but refuses sync would have us bounce between offline and online forever
61- let stable = online_since
62- .take()
63- .map(|since| since.elapsed() >= SYNC_STABLE);
64- if stable == Some(true) {
65- backoff = SYNC_RESTART_MIN;
66- }
67- let (state, restart) = match state {
68- // only ever us stopping it for a restart, nothing a client needs to hear
69- SyncState::Idle => continue,
70- SyncState::Running => {
71- online_since = Some(Instant::now());
72- // the sdk parks a room queue on any send error and never unparks it
73- client.send_queue().set_enabled(true).await;
74- (ConnectionState::Online, false)
75- }
76- SyncState::Offline => (ConnectionState::Offline, stable == Some(false)),
77- SyncState::Terminated => (ConnectionState::Offline, true),
78- SyncState::Error(error) => {
79- tracing::warn!(%error, "sync service reported an error");
80- (ConnectionState::Offline, true)
81- }
82- };
83- if *connection.borrow() != state {
84- connection.send_replace(state);
85- if events.send(Event::ConnectionChanged(state)).await.is_err() {
86- return;
87- }
88- }
89- if restart {
90- sync.stop().await;
91- matrix_sdk::sleep::sleep(backoff).await;
92- backoff = (backoff * 2).min(SYNC_RESTART_MAX);
93- sync.start().await;
94- }
95- }
96-}
97-
98-/// a recoverable failure while we're online is a hiccup, so give the room another go
99-pub(super) async fn send_queue_errors(
100- client: Client,
101- mut errors: tokio::sync::broadcast::Receiver<matrix_sdk::send_queue::SendQueueRoomError>,
102- online: watch::Receiver<ConnectionState>,
103-) {
104- let mut retries = TaskSet::new();
105- loop {
106- let error = tokio::select! {
107- error = errors.recv() => error,
108- Some(_) = retries.join_next(), if !retries.is_empty() => continue,
109- };
110- match error {
111- Ok(error) if error.is_recoverable => {
112- let client = client.clone();
113- let online = online.clone();
114- retries.spawn(async move {
115- matrix_sdk::sleep::sleep(SEND_QUEUE_RETRY).await;
116- if *online.borrow() == ConnectionState::Online
117- && let Some(room) = client.get_room(&error.room_id)
118- {
119- room.send_queue().set_enabled(true);
120- }
121- });
122- }
123- Ok(_) | Err(RecvError::Lagged(_)) => {}
124- Err(RecvError::Closed) => return,
125- }
126- }
127-}
128-
129-/// the one place upload events leave from, so an upload is never reported after it
130-/// ended. the client-wide channel exists before any upload does. it holds 32 updates and
131-/// progress ticks share it, so this keeps draining while the event stream is full
132-pub(super) async fn uploads(
133- client: Client,
134- mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,
135- events: EventSender,
136-) {
137- let (_, mut rooms) = client.rooms_stream();
138- let mut tracker = UploadTracker::new(attachment_echoes(&client).await);
139- let mut pending = VecDeque::new();
140- loop {
141- let reports: Vec<Event> = tokio::select! {
142- biased;
143- update = updates.recv() => match update {
144- Ok(update) => tracker.update(update).into_iter().collect(),
145- Err(RecvError::Lagged(skipped)) => {
146- tracing::warn!(skipped, "upload tracker lagged");
147- tracker.resnapshot(attachment_echoes(&client).await)
148- }
149- Err(RecvError::Closed) => return,
150- },
151- update = rooms.next() => {
152- if update.is_none() {
153- return;
154- }
155- tracker.finish_where(
156- |(room_id, _)| client.get_room(room_id).is_none(),
157- &UploadOutcome::RoomForgotten,
158- )
159- }
160- permit = events.reserve(), if !pending.is_empty() => {
161- let Ok(permit) = permit else { return };
162- permit.send(pending.pop_front().expect("pending was checked"));
163- continue;
164- }
165- };
166- for event in reports {
167- queue_upload_event(&mut pending, event);
168- }
169- }
170-}
171-
172-/// a progress tick replaces a waiting one for the same upload, unless something else about
173-/// that upload is queued after it
174-fn queue_upload_event(pending: &mut VecDeque<Event>, event: Event) {
175- if matches!(event, Event::MediaUploadProgress { .. })
176- && let Some(waiting) = pending
177- .iter_mut()
178- .rev()
179- .find(|waiting| upload_of(waiting) == upload_of(&event))
180- && matches!(waiting, Event::MediaUploadProgress { .. })
181- {
182- *waiting = event;
183- return;
184- }
185- pending.push_back(event);
186-}
187-
188-fn upload_of(event: &Event) -> Option<(&OwnedRoomId, &OwnedTransactionId)> {
189- match event {
190- Event::MediaUploadProgress {
191- room_id,
192- transaction_id,
193- ..
194- }
195- | Event::MediaUploadFailed {
196- room_id,
197- transaction_id,
198- ..
199- }
200- | Event::MediaUploadFinished {
201- room_id,
202- transaction_id,
203- ..
204- } => Some((room_id, transaction_id)),
205- _ => None,
206- }
207-}
208-
209-type Upload = (OwnedRoomId, OwnedTransactionId);
210-
211-/// the sdk sends progress ticks from a detached task, so a tick can land after its
212-/// upload's end. it trails by one task hop, so a short memory of ends covers it
213-const REMEMBERED_ENDS: usize = 256;
214-
215-/// everything reported gets exactly one `MediaUploadFinished` and nothing after it
216-struct UploadTracker {
217- live: HashSet<Upload>,
218- ended: HashSet<Upload>,
219- ended_order: VecDeque<Upload>,
220-}
221-
222-impl UploadTracker {
223- fn new(live: HashSet<Upload>) -> Self {
224- Self {
225- live,
226- ended: HashSet::new(),
227- ended_order: VecDeque::new(),
228- }
229- }
230-
231- fn update(&mut self, update: SendQueueUpdate) -> Option<Event> {
232- let room_id = update.room_id;
233- let (transaction_id, outcome) = match update.update {
234- RoomSendQueueUpdate::NewLocalEvent(echo) => {
235- let upload = (room_id, echo.transaction_id);
236- if is_attachment_echo(&echo.content) && !self.ended.contains(&upload) {
237- self.live.insert(upload);
238- }
239- return None;
240- }
241- RoomSendQueueUpdate::MediaUpload {
242- related_to,
243- file,
244- progress,
245- ..
246- } => {
247- let upload = (room_id, related_to);
248- if self.ended.contains(&upload) {
249- return None;
250- }
251- self.live.insert(upload.clone());
252- let (room_id, transaction_id) = upload;
253- return Some(Event::MediaUploadProgress {
254- room_id,
255- transaction_id,
256- uploaded_bytes: progress.current as u64,
257- total_bytes: progress.total as u64,
258- complete: file.is_some() && progress.current == progress.total,
259- });
260- }
261- RoomSendQueueUpdate::SendError {
262- transaction_id,
263- error,
264- is_recoverable,
265- } => {
266- let upload = (room_id, transaction_id);
267- if !self.live.contains(&upload) {
268- return None;
269- }
270- let (room_id, transaction_id) = upload;
271- return Some(Event::MediaUploadFailed {
272- room_id,
273- transaction_id,
274- kind: classify_sdk_error(&error),
275- will_retry: is_recoverable,
276- });
277- }
278- RoomSendQueueUpdate::SentEvent {
279- transaction_id,
280- event_id,
281- } => (transaction_id, UploadOutcome::Sent { event_id }),
282- RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
283- (transaction_id, UploadOutcome::Cancelled)
284- }
285- _ => return None,
286- };
287- self.finish((room_id, transaction_id), outcome)
288- }
289-
290- fn resnapshot(&mut self, current: HashSet<Upload>) -> Vec<Event> {
291- let reports = self.finish_where(|upload| !current.contains(upload), &UploadOutcome::Missed);
292- let ended = &self.ended;
293- self.live
294- .extend(current.into_iter().filter(|upload| !ended.contains(upload)));
295- reports
296- }
297-
298- fn finish_where(
299- &mut self,
300- ends: impl Fn(&Upload) -> bool,
301- outcome: &UploadOutcome,
302- ) -> Vec<Event> {
303- let ending = self
304- .live
305- .iter()
306- .filter(|upload| ends(upload))
307- .cloned()
308- .collect::<Vec<_>>();
309- ending
310- .into_iter()
311- .filter_map(|upload| self.finish(upload, outcome.clone()))
312- .collect()
313- }
314-
315- /// an end for an upload we never saw is remembered too, its ticks may still be coming
316- fn finish(&mut self, upload: Upload, outcome: UploadOutcome) -> Option<Event> {
317- if self.ended.insert(upload.clone()) {
318- self.ended_order.push_back(upload.clone());
319- if self.ended_order.len() > REMEMBERED_ENDS
320- && let Some(oldest) = self.ended_order.pop_front()
321- {
322- self.ended.remove(&oldest);
323- }
324- }
325- if !self.live.remove(&upload) {
326- return None;
327- }
328- let (room_id, transaction_id) = upload;
329- Some(Event::MediaUploadFinished {
330- room_id,
331- transaction_id,
332- outcome,
333- })
334- }
335-}
336-
337-async fn attachment_echoes(client: &Client) -> HashSet<Upload> {
338- let Ok(echoes) = client.send_queue().local_echoes().await else {
339- return HashSet::new();
340- };
341- echoes
342- .into_iter()
343- .flat_map(|(room_id, echoes)| {
344- echoes
345- .into_iter()
346- .filter(|echo| is_attachment_echo(&echo.content))
347- .map(move |echo| (room_id.clone(), echo.transaction_id))
348- })
349- .collect()
350-}
351-
352-fn is_attachment_echo(content: &LocalEchoContent) -> bool {
353- let LocalEchoContent::Event {
354- serialized_event, ..
355- } = content
356- else {
357- return false;
358- };
359- matches!(
360- serialized_event.deserialize(),
361- Ok(AnyMessageLikeEventContent::RoomMessage(content))
362- if matches!(
363- content.msgtype,
364- MessageType::Audio(_) | MessageType::File(_) | MessageType::Image(_) | MessageType::Video(_)
365- )
366- )
367-}
368-
369-pub(super) async fn encryption(client: Client, events: EventSender) {
370- let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {
371- tracing::warn!("encryption watcher stopped because the client has no device id");
372- return;
373- };
374- let recovery = client.encryption().recovery();
375- let backups = client.encryption().backups();
376- let mut recovery_stream = recovery.state_stream();
377- let mut backup_stream = backups.state_stream();
378- let mut verification_stream = client.encryption().verification_state();
379- let mut recovery_state = recovery.state();
380- let mut backup_state = backups.state();
381- let mut verification_state = verification_stream.get();
382- loop {
383- let state = encryption_state(
384- device_id.clone(),
385- recovery_state,
386- verification_state,
387- backup_state,
388- );
389- if events.send(Event::EncryptionChanged(state)).await.is_err() {
390- return;
391- }
392- tokio::select! {
393- Some(update) = recovery_stream.next() => recovery_state = update,
394- Some(update) = verification_stream.next() => verification_state = update,
395- Some(update) = backup_stream.next() => {
396- backup_state = update.unwrap_or_else(|_| backups.state());
397- }
398- else => return,
399- }
400- }
401-}
402-
403-pub(super) async fn rooms(client: Client, events: EventSender) {
404- let (_, mut rooms) = client.rooms_stream();
405- let mut info = client.room_info_notable_update_receiver();
406- loop {
407- if events.send(Event::RoomsChanged).await.is_err() {
408- return;
409- }
410- tokio::select! {
411- update = rooms.next() => if update.is_none() { return },
412- update = info.recv() => if matches!(update, Err(RecvError::Closed)) { return },
413- }
414- // a sync touching fifty rooms is one invalidation, not fifty
415- while let Ok(_) | Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) =
416- info.try_recv()
417- {}
418- }
419-}
420-
421-/// packs live in room state and account data, and leaving a room takes its packs with it
422-pub(super) async fn image_packs(client: Client, events: EventSender) {
423- let changed = Arc::new(Notify::new());
424- let handlers = [
425- client.add_event_handler({
426- let changed = changed.clone();
427- move |event: Raw<AnySyncStateEvent>| {
428- let changed = changed.clone();
429- async move {
430- if event_type(&event).as_deref() == Some(ROOM_PACKS) {
431- changed.notify_one();
432- }
433- }
434- }
435- }),
436- client.add_event_handler({
437- let changed = changed.clone();
438- move |event: Raw<AnyGlobalAccountDataEvent>| {
439- let changed = changed.clone();
440- async move {
441- if matches!(
442- event_type(&event).as_deref(),
443- Some(USER_PACK | ENABLED_ROOMS)
444- ) {
445- changed.notify_one();
446- }
447- }
448- }
449- }),
450- ];
451- let mut info = client.room_info_notable_update_receiver();
452- loop {
453- tokio::select! {
454- () = changed.notified() => {}
455- update = info.recv() => match update {
456- Ok(update) if !update.reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP) => {
457- continue;
458- }
459- Ok(_) | Err(RecvError::Lagged(_)) => {}
460- Err(RecvError::Closed) => break,
461- },
462- }
463- if events.send(Event::ImagePacksChanged).await.is_err() {
464- break;
465- }
466- }
467- for handler in handlers {
468- client.remove_event_handler(handler);
469- }
470-}
471-
472-fn event_type<T>(event: &Raw<T>) -> Option<String> {
473- event.get_field("type").ok().flatten()
474-}
475-
476-pub(super) async fn notification_settings(client: Client, events: EventSender) {
477- let settings = client.notification_settings().await;
478- let mut changes = settings.subscribe_to_changes();
479- while let Ok(()) | Err(RecvError::Lagged(_)) = changes.recv().await {
480- // room summaries carry the effective mode, and push rule changes never touch room info
481- for event in [Event::NotificationSettingsChanged, Event::RoomsChanged] {
482- if events.send(event).await.is_err() {
483- return;
484- }
485- }
486- }
487-}
488-
489-#[cfg(test)]
490-mod tests {
491- use matrix_sdk::send_queue::{AbstractProgress, RoomSendQueueUpdate, SendQueueUpdate};
492- use ruma::{OwnedTransactionId, event_id, owned_room_id};
493-
494- use super::{HashSet, Upload, UploadTracker};
495- use crate::{Event, UploadOutcome};
496-
497- fn upload() -> Upload {
498- (
499- owned_room_id!("!room:localhost"),
500- OwnedTransactionId::from("upload"),
501- )
502- }
503-
504- fn update(update: RoomSendQueueUpdate) -> SendQueueUpdate {
505- SendQueueUpdate {
506- room_id: upload().0,
507- update,
508- }
509- }
510-
511- fn tick() -> SendQueueUpdate {
512- update(RoomSendQueueUpdate::MediaUpload {
513- related_to: upload().1,
514- file: None,
515- index: 0,
516- progress: AbstractProgress {
517- current: 1,
518- total: 2,
519- },
520- })
521- }
522-
523- fn sent() -> SendQueueUpdate {
524- update(RoomSendQueueUpdate::SentEvent {
525- transaction_id: upload().1,
526- event_id: event_id!("$sent:localhost").to_owned(),
527- })
528- }
529-
530- fn finished(event: Option<Event>) -> Option<UploadOutcome> {
531- match event? {
532- Event::MediaUploadFinished { outcome, .. } => Some(outcome),
533- _ => None,
534- }
535- }
536-
537- #[test]
538- fn a_tick_trailing_its_end_says_nothing() {
539- let mut tracker = UploadTracker::new(HashSet::new());
540- assert!(tracker.update(tick()).is_some());
541- assert!(matches!(
542- finished(tracker.update(sent())),
543- Some(UploadOutcome::Sent { .. })
544- ));
545- assert!(tracker.update(tick()).is_none());
546- assert!(tracker.live.is_empty());
547- }
548-
549- #[test]
550- fn a_stale_tick_after_a_missed_end_changes_nothing() {
551- let mut tracker = UploadTracker::new(HashSet::new());
552- assert!(tracker.update(tick()).is_some());
553- let missed = tracker.resnapshot(HashSet::new());
554- assert!(matches!(
555- missed.as_slice(),
556- [Event::MediaUploadFinished {
557- outcome: UploadOutcome::Missed,
558- ..
559- }]
560- ));
561- assert!(tracker.update(tick()).is_none());
562- assert!(tracker.update(sent()).is_none());
563- assert!(tracker.live.is_empty());
564- }
565-}
Deleted file: vendor/macltok/src/error.rs−322 deletions
@@ -1,322 +0,0 @@
1-use std::sync::Arc;
2-
3-use matrix_sdk::{
4- HttpError, RefreshTokenError, SendOutsideWasm, SyncOutsideWasm,
5- authentication::oauth::error::{
6- OAuthAuthorizationCodeError, OAuthDiscoveryError, OAuthError, OAuthTokenRevocationError,
7- RequestTokenError,
8- },
9- encryption::{
10- identities::{ManualVerifyError, RequestVerificationError},
11- recovery::RecoveryError,
12- secret_storage::{ImportError, SecretStorageError},
13- },
14-};
15-#[cfg(feature = "serde")]
16-use serde::{Deserialize, Serialize};
17-
18-use crate::InteractiveAuthChallenge;
19-
20-/// the sdk's `SendOutsideWasm` isn't an auto trait, so it can't go in a trait object
21-#[cfg(not(target_family = "wasm"))]
22-pub(crate) type SourceError = dyn std::error::Error + Send + Sync;
23-#[cfg(target_family = "wasm")]
24-pub(crate) type SourceError = dyn std::error::Error;
25-
26-/// the one thing that goes wrong, for commands and queries alike. match on `kind`, show `message`
27-#[derive(Clone, Debug, thiserror::Error)]
28-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29-#[error("{message}")]
30-#[non_exhaustive]
31-pub struct Error {
32- pub kind: ErrorKind,
33- pub message: String,
34- /// set when the server wants interactive auth before it'll do the thing
35- pub challenge: Option<Box<InteractiveAuthChallenge>>,
36- /// set when the homeserver refused with a matrix error
37- pub server: Option<ServerError>,
38- #[source]
39- #[cfg_attr(feature = "serde", serde(skip))]
40- source: Option<Arc<SourceError>>,
41-}
42-
43-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45-#[non_exhaustive]
46-pub enum ErrorKind {
47- /// the engine stopped, or the task doing the work died, before answering
48- Interrupted,
49- Unauthenticated,
50- InteractiveAuthRequired,
51- Invalid,
52- RoomUnknown,
53- RoomNotOpen,
54- TimelineNotOpen,
55- RoomBusy,
56- QueueFull,
57- EmptyBody,
58- MediaUnknown,
59- MediaTooLarge,
60- MediaUnavailable,
61- VerificationInactive,
62- VerificationUnsupported,
63- NoCrossSigningIdentity,
64- RecoveryRejected,
65- Network,
66- /// the homeserver wants us to slow down. try again later
67- RateLimited,
68- Server,
69- Store,
70- Timeline,
71- Encryption,
72- Other,
73-}
74-
75-/// the homeserver's own answer, worth showing a person as it stands
76-#[derive(Clone, Debug, Eq, PartialEq)]
77-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
78-#[non_exhaustive]
79-pub struct ServerError {
80- pub status: u16,
81- pub errcode: String,
82- pub message: String,
83-}
84-
85-impl ServerError {
86- pub(crate) fn find(source: &(dyn std::error::Error + 'static)) -> Option<Self> {
87- let mut current = Some(source);
88- while let Some(error) = current {
89- let api = if let Some(error) = error.downcast_ref::<matrix_sdk::Error>() {
90- error.as_client_api_error()
91- } else if let Some(error) = error.downcast_ref::<HttpError>() {
92- error.as_client_api_error()
93- } else {
94- error.downcast_ref::<ruma::api::error::Error>()
95- };
96- if let Some(api) = api
97- && let ruma::api::error::ErrorBody::Standard(body) = &api.body
98- {
99- return Some(Self {
100- status: api.status_code.as_u16(),
101- errcode: body.kind.errcode().to_string(),
102- message: body.message.clone(),
103- });
104- }
105- current = error.source();
106- }
107- None
108- }
109-}
110-
111-impl std::fmt::Display for ServerError {
112- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113- write!(formatter, "{} ({})", self.message, self.errcode)
114- }
115-}
116-
117-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
118-pub(crate) fn classify(source: &(dyn std::error::Error + 'static)) -> ErrorKind {
119- let mut current = Some(source);
120- while let Some(error) = current {
121- if let Some(error) = error.downcast_ref::<matrix_sdk::Error>() {
122- return classify_sdk_error(error);
123- }
124- if let Some(error) = error.downcast_ref::<HttpError>() {
125- return classify_http_error(error);
126- }
127- if let Some(error) = error.downcast_ref::<OAuthError>() {
128- return classify_oauth_error(error);
129- }
130- if let Some(error) = error.downcast_ref::<RefreshTokenError>() {
131- return classify_refresh_token_error(error);
132- }
133- current = error.source();
134- }
135- ErrorKind::Other
136-}
137-
138-impl Error {
139- pub(crate) fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
140- Self {
141- kind,
142- message: message.into(),
143- challenge: None,
144- server: None,
145- source: None,
146- }
147- }
148-
149- pub(crate) fn with_source(
150- kind: ErrorKind,
151- message: impl Into<String>,
152- source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
153- ) -> Self {
154- Self {
155- server: ServerError::find(&source),
156- source: Some(Arc::new(source)),
157- ..Self::new(kind, message)
158- }
159- }
160-
161- pub(crate) fn interrupted() -> Self {
162- Self::new(
163- ErrorKind::Interrupted,
164- "the engine stopped before the operation finished",
165- )
166- }
167-
168- pub(crate) fn room_unknown() -> Self {
169- Self::new(
170- ErrorKind::RoomUnknown,
171- "room is not available in the local store",
172- )
173- }
174-
175- pub(crate) fn interactive_auth(challenge: InteractiveAuthChallenge) -> Self {
176- Self {
177- challenge: Some(Box::new(challenge)),
178- ..Self::new(
179- ErrorKind::InteractiveAuthRequired,
180- "interactive authentication is required",
181- )
182- }
183- }
184-
185- pub(crate) fn from_sdk(message: impl Into<String>, source: matrix_sdk::Error) -> Self {
186- Self::with_source(classify_sdk_error(&source), message, source)
187- }
188-
189- pub(crate) fn from_http(message: impl Into<String>, source: HttpError) -> Self {
190- Self::with_source(classify_http_error(&source), message, source)
191- }
192-
193- pub(crate) fn from_refresh_token(
194- message: impl Into<String>,
195- source: RefreshTokenError,
196- ) -> Self {
197- Self::with_source(classify_refresh_token_error(&source), message, source)
198- }
199-
200- pub(crate) fn from_recovery(message: impl Into<String>, source: RecoveryError) -> Self {
201- let kind = match &source {
202- RecoveryError::Sdk(source) => classify_sdk_error(source),
203- RecoveryError::SecretStorage(SecretStorageError::Sdk(source)) => {
204- classify_sdk_error(source)
205- }
206- RecoveryError::SecretStorage(SecretStorageError::ImportError { error, .. }) => {
207- match error {
208- ImportError::Sdk(source) => classify_sdk_error(source),
209- _ => ErrorKind::RecoveryRejected,
210- }
211- }
212- RecoveryError::SecretStorage(SecretStorageError::Verification(source)) => {
213- match source {
214- ManualVerifyError::Http(source) => classify_http_error(source),
215- _ => ErrorKind::RecoveryRejected,
216- }
217- }
218- RecoveryError::SecretStorage(SecretStorageError::SecretStorageKey(_)) => {
219- ErrorKind::RecoveryRejected
220- }
221- RecoveryError::SecretStorage(SecretStorageError::Storage(_)) => ErrorKind::Store,
222- RecoveryError::BackupExistsOnServer | RecoveryError::SecretStorage(_) => {
223- ErrorKind::RecoveryRejected
224- }
225- };
226- Self::with_source(kind, message, source)
227- }
228-
229- pub(crate) fn from_verification_request(
230- message: impl Into<String>,
231- source: RequestVerificationError,
232- ) -> Self {
233- let kind = match &source {
234- RequestVerificationError::Sdk(source) => classify_sdk_error(source),
235- RequestVerificationError::RoomCreation(_) => ErrorKind::Encryption,
236- };
237- Self::with_source(kind, message, source)
238- }
239-}
240-
241-impl PartialEq for Error {
242- fn eq(&self, other: &Self) -> bool {
243- self.kind == other.kind
244- && self.message == other.message
245- && self.challenge == other.challenge
246- && self.server == other.server
247- }
248-}
249-
250-pub(crate) fn classify_sdk_error(source: &matrix_sdk::Error) -> ErrorKind {
251- match source {
252- matrix_sdk::Error::Http(source) => classify_http_error(source),
253- matrix_sdk::Error::OAuth(source) => classify_oauth_error(source),
254- matrix_sdk::Error::AuthenticationRequired => ErrorKind::Unauthenticated,
255- matrix_sdk::Error::BadCryptoStoreState
256- | matrix_sdk::Error::CryptoStoreError(_)
257- | matrix_sdk::Error::CrossProcessLockError(_)
258- | matrix_sdk::Error::StateStore(_)
259- | matrix_sdk::Error::EventCacheStore(_)
260- | matrix_sdk::Error::MediaStore(_) => ErrorKind::Store,
261- matrix_sdk::Error::NoOlmMachine
262- | matrix_sdk::Error::OlmError(_)
263- | matrix_sdk::Error::MegolmError(_)
264- | matrix_sdk::Error::DecryptorError(_) => ErrorKind::Encryption,
265- matrix_sdk::Error::Media(_) => ErrorKind::MediaUnavailable,
266- matrix_sdk::Error::Timeout => ErrorKind::Network,
267- _ => ErrorKind::Other,
268- }
269-}
270-
271-fn classify_http_error(source: &HttpError) -> ErrorKind {
272- match source {
273- HttpError::Reqwest(_) => ErrorKind::Network,
274- HttpError::Api(_)
275- if source
276- .as_client_api_error()
277- .is_some_and(|api| api.status_code.as_u16() == 429) =>
278- {
279- ErrorKind::RateLimited
280- }
281- HttpError::Api(_) => ErrorKind::Server,
282- HttpError::RefreshToken(source) => classify_refresh_token_error(source),
283- HttpError::Cached(source) => classify_http_error(source),
284- _ => ErrorKind::Other,
285- }
286-}
287-
288-fn classify_refresh_token_error(source: &RefreshTokenError) -> ErrorKind {
289- match source {
290- RefreshTokenError::RefreshTokenRequired => ErrorKind::Unauthenticated,
291- RefreshTokenError::MatrixAuth(source) => classify_http_error(source),
292- RefreshTokenError::OAuth(source) => classify_oauth_error(source),
293- }
294-}
295-
296-fn classify_oauth_error(source: &OAuthError) -> ErrorKind {
297- let classify_token_request_error = |source: &_| match source {
298- RequestTokenError::Request(_) => ErrorKind::Network,
299- RequestTokenError::ServerResponse(_) => ErrorKind::Unauthenticated,
300- _ => ErrorKind::Other,
301- };
302- match source {
303- OAuthError::Discovery(source) => match source {
304- OAuthDiscoveryError::Http(source) => classify_http_error(source),
305- OAuthDiscoveryError::Oidc(source) => classify_token_request_error(source),
306- _ => ErrorKind::Unauthenticated,
307- },
308- OAuthError::RefreshToken(source) => classify_token_request_error(source),
309- OAuthError::AuthorizationCode(OAuthAuthorizationCodeError::RequestToken(source)) => {
310- classify_token_request_error(source)
311- }
312- OAuthError::AuthorizationCode(_) => ErrorKind::Unauthenticated,
313- OAuthError::Logout(source) => match source {
314- OAuthTokenRevocationError::Revoke(RequestTokenError::Request(_)) => ErrorKind::Network,
315- OAuthTokenRevocationError::Revoke(RequestTokenError::ServerResponse(_)) => {
316- ErrorKind::Unauthenticated
317- }
318- _ => ErrorKind::Other,
319- },
320- _ => ErrorKind::Unauthenticated,
321- }
322-}
Deleted file: vendor/macltok/src/lib.rs−73 deletions
@@ -1,73 +0,0 @@
1-//! A compact application backend contract on top of the Matrix Rust SDK.
2-
3-mod encryption;
4-mod engine;
5-mod error;
6-#[cfg(feature = "mirror")]
7-mod mirror;
8-mod model;
9-mod normalise;
10-mod transport;
11-mod uiaa;
12-mod upload;
13-#[cfg(target_family = "wasm")]
14-mod wasm;
15-
16-#[cfg(all(feature = "sqlite", target_family = "wasm"))]
17-compile_error!("sqlite has no filesystem to live on in wasm, use indexeddb");
18-#[cfg(all(feature = "indexeddb", not(target_family = "wasm")))]
19-compile_error!("indexeddb only exists in a browser, use sqlite");
20-
21-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
22-mod account;
23-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
24-mod accounts;
25-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
26-mod session_store;
27-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
28-mod store_lock;
29-
30-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
31-pub use account::{
32- AccountError, OAuthApplicationType, OAuthLogin, PasswordLogin, PendingOAuthLogin, Restore,
33- Session, SessionRecord, SoftLogout, StoreConfig, UnsavedSession,
34-};
35-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
36-pub use accounts::{Account, Accounts};
37-#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
38-pub use session_store::{EncryptedSessionStore, SessionStore};
39-
40-pub use engine::{Engine, EngineConfig, EngineError};
41-pub use error::{Error, ErrorKind, ServerError};
42-pub use imbl;
43-#[cfg(feature = "mirror")]
44-pub use mirror::{
45- Mirror, MirrorHandle, SessionStatus, State, StateChange, Subscription, TimelineChange,
46- TimelineState, Update, UploadFailure, UploadState,
47-};
48-pub use model::{
49- AccountCommand, AttachmentEncryption, AttachmentUpload, Change, Command, CommandOutput,
50- ConnectionState, CreateRoomOptions, EncryptionCommand, EncryptionState, EngineErrorKind,
51- EngineFailure, Event, EventRef, HistoryState, ImagePack, ImagePackSource, ImagePacks,
52- KeyBackupState, LatestEvent, LifecycleCommand, LinkPreview, LinkPreviewImage, Media,
53- MediaCommand, MediaDescriptor, MediaFile, MediaId, MediaKind, MediaLocation, MediaSize,
54- MemberFilter, MemberMembership, MemberSummary, MembershipChange, Message, MessageContent,
55- MessageEditContent, MessageKind, MessageMentions, NotificationCommand, OwnDevice, PackImage,
56- PackUsage, PowerLevelChanges, Profile, PushNotification, PushNotificationEvent,
57- PushNotificationRoom, PushNotificationSender, PushNotificationStatus, Reaction, RecoveryState,
58- ReplyDetails, ReplyReference, Retain, RichText, RoomCategory, RoomCommand, RoomHero,
59- RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode, RoomNotificationSettings,
60- RoomPowerLevels, RoomPreset, RoomSummary, RoomVisibility, SendState, Sender, SpaceChild,
61- Termination, ThreadSummary, TimelineCommand, TimelineContent, TimelineDiff, TimelineDiffError,
62- TimelineEvent, TimelineId, TimelineItem, TimelineItemId, TypingState, UploadDetails,
63- UploadOutcome, UploadThumbnail, UtdCause, VerificationAction, VerificationFailure,
64- VerificationState, VerificationTarget,
65-};
66-pub use ruma;
67-pub use transport::{ClientHandle, EventStream, Pending, SpaceHierarchy};
68-pub use uiaa::{
69- InteractiveAuth, InteractiveAuthChallenge, InteractiveAuthFailure, InteractiveAuthFlow,
70- InteractiveAuthOutcome,
71-};
72-#[cfg(target_family = "wasm")]
73-pub use wasm::on_panic;
Deleted file: vendor/macltok/src/mirror.rs−707 deletions
@@ -1,707 +0,0 @@
1-use std::{
2- collections::HashMap,
3- fmt,
4- pin::Pin,
5- sync::{Arc, Mutex, MutexGuard, PoisonError},
6- task::{Context, Poll},
7- time::Duration,
8-};
9-
10-use futures_util::{Stream, StreamExt};
11-use imbl::Vector;
12-use matrix_sdk::{
13- SendOutsideWasm,
14- executor::{AbortHandle, spawn},
15-};
16-use ruma::{OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId};
17-#[cfg(feature = "serde")]
18-use serde::{Deserialize, Serialize};
19-use tokio::sync::broadcast;
20-
21-use crate::{
22- ClientHandle, Command, ConnectionState, EncryptionState, EngineFailure, ErrorKind, Event,
23- HistoryState, ImagePacks, RoomSummary, TimelineCommand, TimelineDiff, TimelineId, TimelineItem,
24- TypingState, UploadOutcome, VerificationState, VerificationTarget,
25- engine::executor::EngineBoxFuture,
26-};
27-
28-#[cfg(not(target_family = "wasm"))]
29-type EngineBoxStream<T> = futures_util::stream::BoxStream<'static, T>;
30-#[cfg(target_family = "wasm")]
31-type EngineBoxStream<T> = futures_util::stream::LocalBoxStream<'static, T>;
32-
33-const DEFAULT_CAPACITY: usize = 256;
34-const RESYNC_BACKOFF: Duration = Duration::from_millis(100);
35-const RESYNC_BACKOFF_CAP: Duration = Duration::from_secs(10);
36-
37-/// everything the engine has said so far, folded together
38-#[derive(Clone, Debug, PartialEq)]
39-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40-#[non_exhaustive]
41-pub struct State {
42- pub user_id: Option<OwnedUserId>,
43- pub session: SessionStatus,
44- pub connection: ConnectionState,
45- pub encryption: Option<EncryptionState>,
46- pub verification: Option<VerificationState>,
47- pub verification_target: Option<VerificationTarget>,
48- pub rooms: Vector<RoomSummary>,
49- pub image_packs: ImagePacks,
50- pub timelines: imbl::HashMap<TimelineId, TimelineState>,
51- pub typing: imbl::HashMap<OwnedRoomId, Vec<OwnedUserId>>,
52- pub uploads: imbl::HashMap<OwnedTransactionId, UploadState>,
53- /// what stopped the engine, if something went wrong
54- pub engine_failure: Option<EngineFailure>,
55- /// the last save of the session record that failed, until one succeeds
56- pub session_save_failure: Option<String>,
57-}
58-
59-#[derive(Clone, Debug, PartialEq)]
60-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
61-#[non_exhaustive]
62-pub struct TimelineState {
63- pub items: Vector<TimelineItem>,
64- pub history: HistoryState,
65- /// a batch didn't fit, so `items` is the last good copy until the next reset
66- pub diverged: bool,
67-}
68-
69-#[derive(Clone, Debug, Eq, PartialEq)]
70-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
71-#[non_exhaustive]
72-pub struct UploadState {
73- pub room_id: OwnedRoomId,
74- pub uploaded_bytes: u64,
75- pub total_bytes: u64,
76- pub complete: bool,
77- pub failure: Option<UploadFailure>,
78-}
79-
80-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
82-#[non_exhaustive]
83-pub struct UploadFailure {
84- pub kind: ErrorKind,
85- pub will_retry: bool,
86-}
87-
88-/// the `Termination` it'll end in, as far as the events tell
89-#[derive(Clone, Debug, Eq, PartialEq)]
90-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
91-#[non_exhaustive]
92-pub enum SessionStatus {
93- Active,
94- SoftLoggedOut {
95- device_id: OwnedDeviceId,
96- },
97- AccessRevoked,
98- LoggedOut,
99- /// stopped with the session still good
100- Stopped,
101-}
102-
103-#[derive(Clone, Debug)]
104-#[non_exhaustive]
105-pub struct Update {
106- /// one more than the update before it, and a `State` carries the one it's current as of
107- pub sequence: u64,
108- pub change: StateChange,
109-}
110-
111-#[derive(Clone, Debug)]
112-#[non_exhaustive]
113-pub enum StateChange {
114- /// start over from this. a subscription opens with one and gets another after lagging
115- State(Arc<State>),
116- Authenticated(OwnedUserId),
117- Session(SessionStatus),
118- /// the record on disk may now hold a spent refresh token
119- SessionSaveFailed(String),
120- /// saved again after `SessionSaveFailed`
121- SessionSaved,
122- Connection(ConnectionState),
123- Encryption(EncryptionState),
124- Verification(VerificationState),
125- VerificationTarget(VerificationTarget),
126- /// room modes arrive with the `Rooms` that follows
127- NotificationSettings,
128- Rooms(Vector<RoomSummary>),
129- ImagePacks(ImagePacks),
130- Timeline {
131- timeline_id: TimelineId,
132- change: TimelineChange,
133- },
134- History {
135- timeline_id: TimelineId,
136- state: HistoryState,
137- },
138- Typing(TypingState),
139- Upload {
140- transaction_id: OwnedTransactionId,
141- upload: UploadState,
142- },
143- UploadFinished {
144- room_id: OwnedRoomId,
145- transaction_id: OwnedTransactionId,
146- outcome: UploadOutcome,
147- },
148- Failed(EngineFailure),
149-}
150-
151-#[derive(Clone, Debug)]
152-#[non_exhaustive]
153-pub enum TimelineChange {
154- Reset {
155- items: Vector<TimelineItem>,
156- },
157- /// `items` is what applying `diffs` left
158- Changed {
159- diffs: Arc<[TimelineDiff]>,
160- items: Vector<TimelineItem>,
161- },
162- /// a batch didn't fit. a resync is on its way and nothing changes until its reset
163- Diverged,
164- Closed,
165-}
166-
167-/// drains the event stream into a `State` that any number of `MirrorHandle`s watch
168-pub struct Mirror {
169- handle: ClientHandle,
170- events: EngineBoxStream<Event>,
171- shared: Arc<Shared>,
172- resyncs: HashMap<TimelineId, AbortHandle>,
173- rooms_fetch: Option<AbortHandle>,
174- packs_fetch: Option<AbortHandle>,
175-}
176-
177-impl fmt::Debug for Mirror {
178- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179- formatter.debug_struct("Mirror").finish_non_exhaustive()
180- }
181-}
182-
183-impl Mirror {
184- /// takes any stream of the engine's events, so a host can still look at them on the way past
185- pub fn new(
186- handle: ClientHandle,
187- events: impl Stream<Item = Event> + SendOutsideWasm + 'static,
188- ) -> (Self, MirrorHandle) {
189- Self::with_capacity(handle, events, DEFAULT_CAPACITY)
190- }
191-
192- /// `capacity` updates can queue for a subscriber before it lags and starts over. a zero
193- /// capacity counts as one
194- pub fn with_capacity(
195- handle: ClientHandle,
196- events: impl Stream<Item = Event> + SendOutsideWasm + 'static,
197- capacity: usize,
198- ) -> (Self, MirrorHandle) {
199- let state = State {
200- user_id: None,
201- session: SessionStatus::Active,
202- connection: *handle.connection_state().borrow(),
203- encryption: handle.encryption_state(),
204- verification: None,
205- verification_target: None,
206- rooms: Vector::new(),
207- image_packs: ImagePacks::default(),
208- timelines: imbl::HashMap::new(),
209- typing: imbl::HashMap::new(),
210- uploads: imbl::HashMap::new(),
211- engine_failure: None,
212- session_save_failure: None,
213- };
214- let shared = Arc::new(Shared {
215- published: Mutex::new(Published {
216- state: Arc::new(state),
217- sequence: 0,
218- updates: Some(broadcast::channel(capacity.max(1)).0),
219- rooms_fetch: Fetch::Idle,
220- packs_fetch: Fetch::Idle,
221- }),
222- });
223- #[cfg(not(target_family = "wasm"))]
224- let events = events.boxed();
225- #[cfg(target_family = "wasm")]
226- let events = events.boxed_local();
227- let mirror = Self {
228- handle,
229- events,
230- shared: shared.clone(),
231- resyncs: HashMap::new(),
232- rooms_fetch: None,
233- packs_fetch: None,
234- };
235- (mirror, MirrorHandle { shared })
236- }
237-
238- /// resolves once the event stream ends, which is just after `Stopped`
239- pub fn run(mut self) -> EngineBoxFuture<'static, ()> {
240- Box::pin(async move {
241- self.refetch_rooms();
242- self.refetch_image_packs();
243- while let Some(event) = self.events.next().await {
244- self.apply(event);
245- }
246- })
247- }
248-
249- fn apply(&mut self, event: Event) {
250- match event {
251- Event::ConnectionChanged(connection) => self.publish(|state| {
252- state.connection = connection;
253- Some(StateChange::Connection(connection))
254- }),
255- Event::Authenticated { user_id } => self.publish(|state| {
256- state.user_id = Some(user_id.clone());
257- Some(StateChange::Authenticated(user_id))
258- }),
259- Event::SessionSaveFailed { message } => self.publish(|state| {
260- state.session_save_failure = Some(message.clone());
261- Some(StateChange::SessionSaveFailed(message))
262- }),
263- Event::SessionTokensChanged => self.publish(|state| {
264- state.session_save_failure.take()?;
265- Some(StateChange::SessionSaved)
266- }),
267- Event::SessionAccessRejected { soft_logout } => self.publish(|state| {
268- state.session = match soft_logout {
269- Some(device_id) => SessionStatus::SoftLoggedOut { device_id },
270- None => SessionStatus::AccessRevoked,
271- };
272- Some(StateChange::Session(state.session.clone()))
273- }),
274- Event::LoggedOut => self.publish(|state| {
275- state.session = SessionStatus::LoggedOut;
276- Some(StateChange::Session(SessionStatus::LoggedOut))
277- }),
278- Event::Stopped => {
279- self.cancel_resyncs();
280- self.publish(|state| {
281- if state.session != SessionStatus::Active {
282- return None;
283- }
284- state.session = SessionStatus::Stopped;
285- Some(StateChange::Session(SessionStatus::Stopped))
286- });
287- }
288- Event::Failed(failure) => self.publish(|state| {
289- state.engine_failure = Some(failure.clone());
290- Some(StateChange::Failed(failure))
291- }),
292- Event::NotificationSettingsChanged => {
293- self.publish(|_| Some(StateChange::NotificationSettings));
294- }
295- Event::RoomsChanged => self.refetch_rooms(),
296- Event::ImagePacksChanged => self.refetch_image_packs(),
297- Event::EncryptionChanged(encryption) => self.publish(|state| {
298- state.encryption = Some(encryption.clone());
299- Some(StateChange::Encryption(encryption))
300- }),
301- Event::VerificationChanged(verification) => self.publish(|state| {
302- state.verification = Some(verification.clone());
303- Some(StateChange::Verification(verification))
304- }),
305- Event::VerificationTargetChanged(target) => self.publish(|state| {
306- state.verification_target = Some(target.clone());
307- Some(StateChange::VerificationTarget(target))
308- }),
309- Event::TimelineReset { timeline_id, items } => {
310- self.cancel_resync(&timeline_id);
311- self.publish(|state| {
312- let history = state
313- .timelines
314- .get(&timeline_id)
315- .map_or(HistoryState::Idle, |timeline| timeline.history.clone());
316- let timeline = TimelineState {
317- items: items.clone(),
318- history,
319- diverged: false,
320- };
321- state.timelines.insert(timeline_id.clone(), timeline);
322- Some(StateChange::Timeline {
323- timeline_id,
324- change: TimelineChange::Reset { items },
325- })
326- });
327- }
328- Event::TimelineChanged { timeline_id, diffs } => self.apply_diffs(timeline_id, diffs),
329- Event::HistoryChanged {
330- timeline_id,
331- state: history,
332- } => self.publish(|state| {
333- state.timelines.get_mut(&timeline_id)?.history = history.clone();
334- Some(StateChange::History {
335- timeline_id,
336- state: history,
337- })
338- }),
339- Event::TimelineClosed { timeline_id } => {
340- self.cancel_resync(&timeline_id);
341- self.publish(|state| {
342- state.timelines.remove(&timeline_id)?;
343- Some(StateChange::Timeline {
344- timeline_id,
345- change: TimelineChange::Closed,
346- })
347- });
348- }
349- Event::TypingChanged(typing) => self.publish(|state| {
350- if typing.users.is_empty() {
351- state.typing.remove(&typing.room_id);
352- } else {
353- state
354- .typing
355- .insert(typing.room_id.clone(), typing.users.clone());
356- }
357- Some(StateChange::Typing(typing))
358- }),
359- Event::MediaUploadProgress {
360- room_id,
361- transaction_id,
362- uploaded_bytes,
363- total_bytes,
364- complete,
365- } => self.publish(|state| {
366- let upload = UploadState {
367- room_id,
368- uploaded_bytes,
369- total_bytes,
370- complete,
371- failure: None,
372- };
373- state.uploads.insert(transaction_id.clone(), upload.clone());
374- Some(StateChange::Upload {
375- transaction_id,
376- upload,
377- })
378- }),
379- Event::MediaUploadFailed {
380- room_id,
381- transaction_id,
382- kind,
383- will_retry,
384- } => self.publish(|state| {
385- let upload = state
386- .uploads
387- .entry(transaction_id.clone())
388- .or_insert_with(|| UploadState {
389- room_id,
390- uploaded_bytes: 0,
391- total_bytes: 0,
392- complete: false,
393- failure: None,
394- });
395- upload.failure = Some(UploadFailure { kind, will_retry });
396- Some(StateChange::Upload {
397- transaction_id,
398- upload: upload.clone(),
399- })
400- }),
401- Event::MediaUploadFinished {
402- room_id,
403- transaction_id,
404- outcome,
405- } => self.publish(|state| {
406- state.uploads.remove(&transaction_id);
407- Some(StateChange::UploadFinished {
408- room_id,
409- transaction_id,
410- outcome,
411- })
412- }),
413- }
414- }
415-
416- fn apply_diffs(&mut self, timeline_id: TimelineId, diffs: Vec<TimelineDiff>) {
417- let mut diverged = false;
418- self.publish(|state| {
419- let Some(timeline) = state.timelines.get_mut(&timeline_id) else {
420- diverged = true;
421- state.timelines.insert(
422- timeline_id.clone(),
423- TimelineState {
424- items: Vector::new(),
425- history: HistoryState::Idle,
426- diverged: true,
427- },
428- );
429- return Some(StateChange::Timeline {
430- timeline_id: timeline_id.clone(),
431- change: TimelineChange::Diverged,
432- });
433- };
434- if timeline.diverged {
435- return None;
436- }
437- let diffs: Arc<[TimelineDiff]> = diffs.into();
438- let change = match TimelineDiff::try_apply_all(diffs.to_vec(), &mut timeline.items) {
439- Ok(()) => TimelineChange::Changed {
440- diffs,
441- items: timeline.items.clone(),
442- },
443- Err(error) => {
444- tracing::debug!(?timeline_id, length = error.length, "timeline diverged");
445- timeline.diverged = true;
446- diverged = true;
447- TimelineChange::Diverged
448- }
449- };
450- Some(StateChange::Timeline {
451- timeline_id: timeline_id.clone(),
452- change,
453- })
454- });
455- if diverged {
456- let task = spawn(resync(self.handle.clone(), timeline_id.clone()));
457- self.resyncs.insert(timeline_id, task.abort_handle());
458- }
459- }
460-
461- fn refetch_rooms(&mut self) {
462- let fetch = fetch_rooms(self.handle.clone(), self.shared.clone());
463- if let Some(task) = self
464- .shared
465- .start(|published| &mut published.rooms_fetch, fetch)
466- {
467- self.rooms_fetch = Some(task);
468- }
469- }
470-
471- fn refetch_image_packs(&mut self) {
472- let fetch = fetch_image_packs(self.handle.clone(), self.shared.clone());
473- if let Some(task) = self
474- .shared
475- .start(|published| &mut published.packs_fetch, fetch)
476- {
477- self.packs_fetch = Some(task);
478- }
479- }
480-
481- fn cancel_resync(&mut self, timeline_id: &TimelineId) {
482- if let Some(task) = self.resyncs.remove(timeline_id) {
483- task.abort();
484- }
485- }
486-
487- fn cancel_resyncs(&mut self) {
488- for (_, task) in self.resyncs.drain() {
489- task.abort();
490- }
491- }
492-
493- fn publish(&self, change: impl FnOnce(&mut State) -> Option<StateChange>) {
494- self.shared.publish(change);
495- }
496-}
497-
498-impl Drop for Mirror {
499- fn drop(&mut self) {
500- self.cancel_resyncs();
501- for task in [self.rooms_fetch.take(), self.packs_fetch.take()]
502- .into_iter()
503- .flatten()
504- {
505- task.abort();
506- }
507- self.shared.lock().updates = None;
508- }
509-}
510-
511-/// asks until the engine answers, since a busy room or a full queue is only ever for now.
512-/// a reset or a close for the timeline aborts it, and the engine sends a reset before it answers
513-async fn resync(handle: ClientHandle, timeline_id: TimelineId) {
514- let mut backoff = RESYNC_BACKOFF;
515- loop {
516- let command = Command::Timeline(TimelineCommand::Resync {
517- timeline_id: timeline_id.clone(),
518- });
519- match handle.execute(command).await {
520- Ok(_) => return,
521- Err(error) if error.kind == ErrorKind::Interrupted => return,
522- Err(error) => {
523- tracing::debug!(%error, ?timeline_id, "resync refused, trying again");
524- matrix_sdk::sleep::sleep(backoff).await;
525- backoff = (backoff * 2).min(RESYNC_BACKOFF_CAP);
526- }
527- }
528- }
529-}
530-
531-async fn fetch_rooms(handle: ClientHandle, shared: Arc<Shared>) {
532- loop {
533- let rooms: Vector<RoomSummary> = handle.rooms().await.into();
534- shared.publish(|state| {
535- state.rooms = rooms.clone();
536- Some(StateChange::Rooms(rooms))
537- });
538- if !shared.fetched(|published| &mut published.rooms_fetch) {
539- return;
540- }
541- }
542-}
543-
544-/// a failed read keeps the packs from before, the next change asks again
545-async fn fetch_image_packs(handle: ClientHandle, shared: Arc<Shared>) {
546- loop {
547- match handle.image_packs().await {
548- Ok(packs) => shared.publish(|state| {
549- if state.image_packs == packs {
550- return None;
551- }
552- state.image_packs = packs.clone();
553- Some(StateChange::ImagePacks(packs))
554- }),
555- Err(error) => tracing::warn!(%error, "reading image packs failed"),
556- }
557- if !shared.fetched(|published| &mut published.packs_fetch) {
558- return;
559- }
560- }
561-}
562-
563-/// cheap to clone, one per frontend
564-#[derive(Clone, Debug)]
565-pub struct MirrorHandle {
566- shared: Arc<Shared>,
567-}
568-
569-impl MirrorHandle {
570- #[must_use]
571- pub fn snapshot(&self) -> Arc<State> {
572- self.shared.lock().state.clone()
573- }
574-
575- /// opens with the current state, then every update after it. ends once the mirror stops
576- #[must_use]
577- pub fn subscribe(&self) -> Subscription {
578- let (first, receiving) = self.shared.clone().subscribe();
579- let rest = futures_util::stream::unfold(receiving, |(shared, receiver)| async move {
580- let mut receiver = receiver?;
581- match receiver.recv().await {
582- Ok(update) => Some((update, (shared, Some(receiver)))),
583- Err(broadcast::error::RecvError::Lagged(missed)) => {
584- tracing::debug!(missed, "mirror subscriber lagged, starting it over");
585- Some(shared.subscribe())
586- }
587- Err(broadcast::error::RecvError::Closed) => None,
588- }
589- });
590- let updates = futures_util::stream::once(std::future::ready(first)).chain(rest);
591- #[cfg(not(target_family = "wasm"))]
592- let updates = updates.boxed();
593- #[cfg(target_family = "wasm")]
594- let updates = updates.boxed_local();
595- Subscription(updates)
596- }
597-}
598-
599-pub struct Subscription(EngineBoxStream<Update>);
600-
601-impl fmt::Debug for Subscription {
602- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
603- formatter
604- .debug_struct("Subscription")
605- .finish_non_exhaustive()
606- }
607-}
608-
609-impl Stream for Subscription {
610- type Item = Update;
611-
612- fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Update>> {
613- self.0.poll_next_unpin(context)
614- }
615-}
616-
617-#[derive(Debug)]
618-struct Shared {
619- published: Mutex<Published>,
620-}
621-
622-#[derive(Debug)]
623-struct Published {
624- state: Arc<State>,
625- sequence: u64,
626- /// gone once the mirror stops, which ends every subscription
627- updates: Option<broadcast::Sender<Update>>,
628- rooms_fetch: Fetch,
629- packs_fetch: Fetch,
630-}
631-
632-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
633-enum Fetch {
634- Idle,
635- Running,
636- Again,
637-}
638-
639-impl Shared {
640- fn lock(&self) -> MutexGuard<'_, Published> {
641- self.published
642- .lock()
643- .unwrap_or_else(PoisonError::into_inner)
644- }
645-
646- /// one fetch at a time, with at most one more owed
647- fn start(
648- &self,
649- fetch: impl FnOnce(&mut Published) -> &mut Fetch,
650- task: impl Future<Output = ()> + SendOutsideWasm + 'static,
651- ) -> Option<AbortHandle> {
652- let mut published = self.lock();
653- let fetch = fetch(&mut published);
654- match *fetch {
655- Fetch::Idle => {
656- *fetch = Fetch::Running;
657- drop(published);
658- Some(spawn(task).abort_handle())
659- }
660- Fetch::Running => {
661- *fetch = Fetch::Again;
662- None
663- }
664- Fetch::Again => None,
665- }
666- }
667-
668- /// whether another fetch was asked for while this one ran
669- fn fetched(&self, fetch: impl FnOnce(&mut Published) -> &mut Fetch) -> bool {
670- let mut published = self.lock();
671- let fetch = fetch(&mut published);
672- if *fetch == Fetch::Again {
673- *fetch = Fetch::Running;
674- return true;
675- }
676- *fetch = Fetch::Idle;
677- false
678- }
679-
680- /// state, sequence and send move together under the one lock, which is what lets
681- /// `subscribe` take a snapshot and a receiver with nothing between them
682- fn publish(&self, change: impl FnOnce(&mut State) -> Option<StateChange>) {
683- let mut published = self.lock();
684- let Some(change) = change(Arc::make_mut(&mut published.state)) else {
685- return;
686- };
687- published.sequence += 1;
688- let update = Update {
689- sequence: published.sequence,
690- change,
691- };
692- if let Some(updates) = &published.updates {
693- let _ = updates.send(update);
694- }
695- }
696-
697- fn subscribe(self: Arc<Self>) -> (Update, (Arc<Self>, Option<broadcast::Receiver<Update>>)) {
698- let published = self.lock();
699- let update = Update {
700- sequence: published.sequence,
701- change: StateChange::State(published.state.clone()),
702- };
703- let receiver = published.updates.as_ref().map(broadcast::Sender::subscribe);
704- drop(published);
705- (update, (self, receiver))
706- }
707-}
Deleted file: vendor/macltok/src/model.rs−1979 deletions
@@ -1,1979 +0,0 @@
1-use std::{fmt, sync::Arc};
2-
3-use imbl::Vector;
4-use ruma::{
5- MxcUri, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId,
6- OwnedRoomOrAliasId, OwnedServerName, OwnedTransactionId, OwnedUserId, RoomId,
7- events::{
8- AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent, AnySyncTimelineEvent,
9- GlobalAccountDataEventType, RoomAccountDataEventType,
10- room::member::StrippedRoomMemberEvent,
11- },
12- serde::Raw,
13-};
14-#[cfg(feature = "serde")]
15-use serde::{Deserialize, Serialize};
16-
17-/// every command answers exactly once through `ClientHandle::send` or `execute`.
18-/// the event stream only ever carries state
19-#[derive(Debug)]
20-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21-#[non_exhaustive]
22-pub enum Command {
23- Account(AccountCommand),
24- Room(RoomCommand),
25- Timeline(TimelineCommand),
26- Media(MediaCommand),
27- Encryption(EncryptionCommand),
28- Notification(NotificationCommand),
29- Lifecycle(LifecycleCommand),
30-}
31-
32-#[derive(Debug)]
33-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34-#[non_exhaustive]
35-pub enum AccountCommand {
36- Logout,
37- SetAccountData {
38- event_type: GlobalAccountDataEventType,
39- content: Raw<AnyGlobalAccountDataEventContent>,
40- },
41- SetDisplayName(Option<String>),
42- SetAvatar(Option<OwnedMxcUri>),
43- /// uploads the image and points the profile at it, same byte cap as any upload
44- UploadAvatar {
45- mimetype: String,
46- bytes: Vec<u8>,
47- },
48- RenameDevice {
49- device_id: OwnedDeviceId,
50- display_name: String,
51- },
52- DeleteDevices {
53- device_ids: Vec<OwnedDeviceId>,
54- auth: Option<crate::InteractiveAuth>,
55- },
56-}
57-
58-#[derive(Clone, Debug)]
59-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60-#[non_exhaustive]
61-pub enum RoomCommand {
62- /// the snapshot turns up as `Event::TimelineReset`. opening an open room sends a fresh one
63- Open(OwnedRoomId),
64- Close(OwnedRoomId),
65- Join {
66- room: OwnedRoomOrAliasId,
67- via: Vec<OwnedServerName>,
68- },
69- Leave(OwnedRoomId),
70- Forget(OwnedRoomId),
71- AcceptInvite(OwnedRoomId),
72- RejectInvite(OwnedRoomId),
73- InviteUser {
74- room_id: OwnedRoomId,
75- user_id: OwnedUserId,
76- },
77- KickUser {
78- room_id: OwnedRoomId,
79- user_id: OwnedUserId,
80- reason: Option<String>,
81- },
82- BanUser {
83- room_id: OwnedRoomId,
84- user_id: OwnedUserId,
85- reason: Option<String>,
86- },
87- UnbanUser {
88- room_id: OwnedRoomId,
89- user_id: OwnedUserId,
90- reason: Option<String>,
91- },
92- SetPowerLevels {
93- room_id: OwnedRoomId,
94- changes: PowerLevelChanges,
95- },
96- SetUserPowerLevel {
97- room_id: OwnedRoomId,
98- user_id: OwnedUserId,
99- level: i64,
100- },
101- Create(CreateRoomOptions),
102- /// hands back the existing dm if there is one
103- CreateDirect(OwnedUserId),
104- CancelSend {
105- room_id: OwnedRoomId,
106- transaction_id: OwnedTransactionId,
107- },
108- RetrySend {
109- room_id: OwnedRoomId,
110- transaction_id: OwnedTransactionId,
111- },
112- SetTyping {
113- room_id: OwnedRoomId,
114- typing: bool,
115- },
116- /// the key work a send into an encrypted room would otherwise start with, done now.
117- /// nothing in a plain room, and a second ask joins the first
118- PrepareToSend(OwnedRoomId),
119- SetAccountData {
120- room_id: OwnedRoomId,
121- event_type: RoomAccountDataEventType,
122- content: Raw<AnyRoomAccountDataEventContent>,
123- },
124-}
125-
126-#[derive(Clone, Debug, Default, Eq, PartialEq)]
127-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
128-#[non_exhaustive]
129-pub struct CreateRoomOptions {
130- pub name: Option<String>,
131- pub topic: Option<String>,
132- pub alias_localpart: Option<String>,
133- pub invite: Vec<OwnedUserId>,
134- pub direct: bool,
135- pub visibility: RoomVisibility,
136- pub preset: Option<RoomPreset>,
137-}
138-
139-impl CreateRoomOptions {
140- #[must_use]
141- pub fn with_name(mut self, name: impl Into<String>) -> Self {
142- self.name = Some(name.into());
143- self
144- }
145-
146- #[must_use]
147- pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
148- self.topic = Some(topic.into());
149- self
150- }
151-
152- #[must_use]
153- pub fn with_alias_localpart(mut self, localpart: impl Into<String>) -> Self {
154- self.alias_localpart = Some(localpart.into());
155- self
156- }
157-
158- #[must_use]
159- pub fn with_invite(mut self, invite: impl IntoIterator<Item = OwnedUserId>) -> Self {
160- self.invite = invite.into_iter().collect();
161- self
162- }
163-
164- #[must_use]
165- pub const fn with_direct(mut self, direct: bool) -> Self {
166- self.direct = direct;
167- self
168- }
169-
170- #[must_use]
171- pub const fn with_visibility(mut self, visibility: RoomVisibility) -> Self {
172- self.visibility = visibility;
173- self
174- }
175-
176- #[must_use]
177- pub const fn with_preset(mut self, preset: RoomPreset) -> Self {
178- self.preset = Some(preset);
179- self
180- }
181-}
182-
183-#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
184-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
185-pub enum RoomVisibility {
186- #[default]
187- Private,
188- Public,
189-}
190-
191-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
192-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
193-pub enum RoomPreset {
194- PrivateChat,
195- TrustedPrivateChat,
196- PublicChat,
197-}
198-
199-#[derive(Clone, Debug, Eq, PartialEq)]
200-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
201-#[non_exhaustive]
202-pub enum TimelineCommand {
203- OpenThread {
204- room_id: OwnedRoomId,
205- root_event_id: OwnedEventId,
206- },
207- CloseThread {
208- room_id: OwnedRoomId,
209- root_event_id: OwnedEventId,
210- },
211- /// Re-emit an authoritative reset for an open room or thread without
212- /// closing it. The reset arrives before this command's terminal result.
213- Resync {
214- timeline_id: TimelineId,
215- },
216- Paginate {
217- timeline_id: TimelineId,
218- limit: u16,
219- },
220- Send {
221- timeline_id: TimelineId,
222- content: MessageContent,
223- },
224- Edit {
225- timeline_id: TimelineId,
226- target: EventRef,
227- content: MessageEditContent,
228- },
229- /// redacting your own unsent message just cancels it
230- Redact {
231- timeline_id: TimelineId,
232- target: EventRef,
233- reason: Option<String>,
234- },
235- ToggleReaction {
236- timeline_id: TimelineId,
237- target: EventRef,
238- key: String,
239- /// a custom emote's shortcode, sent along so clients without the pack can label it
240- shortcode: Option<String>,
241- },
242- /// works on a closed room too, it just can't refuse to move the marker backwards
243- MarkRead {
244- timeline_id: TimelineId,
245- event_id: OwnedEventId,
246- },
247-}
248-
249-impl TimelineCommand {
250- #[must_use]
251- pub const fn room_id(&self) -> &OwnedRoomId {
252- match self {
253- Self::OpenThread { room_id, .. } | Self::CloseThread { room_id, .. } => room_id,
254- Self::Resync { timeline_id }
255- | Self::Paginate { timeline_id, .. }
256- | Self::Send { timeline_id, .. }
257- | Self::Edit { timeline_id, .. }
258- | Self::Redact { timeline_id, .. }
259- | Self::ToggleReaction { timeline_id, .. }
260- | Self::MarkRead { timeline_id, .. } => timeline_id.room_id(),
261- }
262- }
263-
264- #[must_use]
265- pub const fn timeline_id(&self) -> Option<&TimelineId> {
266- match self {
267- Self::OpenThread { .. } | Self::CloseThread { .. } => None,
268- Self::Resync { timeline_id }
269- | Self::Paginate { timeline_id, .. }
270- | Self::Send { timeline_id, .. }
271- | Self::Edit { timeline_id, .. }
272- | Self::Redact { timeline_id, .. }
273- | Self::ToggleReaction { timeline_id, .. }
274- | Self::MarkRead { timeline_id, .. } => Some(timeline_id),
275- }
276- }
277-}
278-
279-#[derive(Clone, Debug, Eq, Hash, PartialEq)]
280-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
281-#[non_exhaustive]
282-pub enum TimelineId {
283- Room(OwnedRoomId),
284- Thread {
285- room_id: OwnedRoomId,
286- root_event_id: OwnedEventId,
287- },
288-}
289-
290-impl TimelineId {
291- #[must_use]
292- pub const fn room_id(&self) -> &OwnedRoomId {
293- match self {
294- Self::Room(room_id) | Self::Thread { room_id, .. } => room_id,
295- }
296- }
297-
298- #[must_use]
299- pub fn thread(room_id: OwnedRoomId, root_event_id: OwnedEventId) -> Self {
300- Self::Thread {
301- room_id,
302- root_event_id,
303- }
304- }
305-
306- #[must_use]
307- pub fn thread_root(&self) -> Option<&OwnedEventId> {
308- match self {
309- Self::Room(_) => None,
310- Self::Thread { root_event_id, .. } => Some(root_event_id),
311- }
312- }
313-}
314-
315-impl From<OwnedRoomId> for TimelineId {
316- fn from(room_id: OwnedRoomId) -> Self {
317- Self::Room(room_id)
318- }
319-}
320-
321-#[derive(Clone, Debug, Eq, PartialEq)]
322-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
323-#[non_exhaustive]
324-pub enum MediaCommand {
325- /// succeeds once the send queue has it. progress and failure arrive as events
326- Upload(AttachmentUpload),
327-}
328-
329-#[derive(Clone, Debug, Eq, PartialEq)]
330-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
331-#[non_exhaustive]
332-pub struct AttachmentUpload {
333- pub timeline_id: TimelineId,
334- pub filename: String,
335- pub mimetype: String,
336- pub bytes: Vec<u8>,
337- pub caption: Option<String>,
338- pub details: Option<UploadDetails>,
339- pub thumbnail: Option<UploadThumbnail>,
340-}
341-
342-impl AttachmentUpload {
343- #[must_use]
344- pub fn new(
345- timeline_id: impl Into<TimelineId>,
346- filename: impl Into<String>,
347- mimetype: impl Into<String>,
348- bytes: Vec<u8>,
349- ) -> Self {
350- Self {
351- timeline_id: timeline_id.into(),
352- filename: filename.into(),
353- mimetype: mimetype.into(),
354- bytes,
355- caption: None,
356- details: None,
357- thumbnail: None,
358- }
359- }
360-}
361-
362-#[derive(Clone, Debug, Eq, PartialEq)]
363-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
364-#[non_exhaustive]
365-pub enum UploadDetails {
366- Image {
367- width: Option<u32>,
368- height: Option<u32>,
369- blurhash: Option<String>,
370- },
371- Video {
372- width: Option<u32>,
373- height: Option<u32>,
374- duration_ms: Option<u64>,
375- blurhash: Option<String>,
376- },
377- Audio {
378- duration_ms: Option<u64>,
379- },
380- Voice {
381- duration_ms: u64,
382- waveform: Vec<u16>,
383- },
384- File,
385-}
386-
387-#[derive(Clone, Debug, Eq, PartialEq)]
388-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
389-#[non_exhaustive]
390-pub struct UploadThumbnail {
391- pub bytes: Vec<u8>,
392- pub mimetype: String,
393- pub width: u32,
394- pub height: u32,
395-}
396-
397-impl UploadThumbnail {
398- #[must_use]
399- pub fn new(bytes: Vec<u8>, mimetype: impl Into<String>, width: u32, height: u32) -> Self {
400- Self {
401- bytes,
402- mimetype: mimetype.into(),
403- width,
404- height,
405- }
406- }
407-}
408-
409-#[derive(Eq, PartialEq)]
410-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
411-#[non_exhaustive]
412-pub enum EncryptionCommand {
413- Recover(String),
414- CreateKeyBackup,
415- EnableKeyBackup,
416- DisableKeyBackup { delete_from_server: bool },
417- RecoverKeyBackup(String),
418- StartVerification(VerificationTarget),
419- Verify(VerificationAction),
420-}
421-
422-impl fmt::Debug for EncryptionCommand {
423- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
424- match self {
425- Self::Recover(_) => formatter.debug_tuple("Recover").field(&"...").finish(),
426- Self::CreateKeyBackup => formatter.write_str("CreateKeyBackup"),
427- Self::EnableKeyBackup => formatter.write_str("EnableKeyBackup"),
428- Self::DisableKeyBackup { delete_from_server } => formatter
429- .debug_struct("DisableKeyBackup")
430- .field("delete_from_server", delete_from_server)
431- .finish(),
432- Self::RecoverKeyBackup(_) => formatter
433- .debug_tuple("RecoverKeyBackup")
434- .field(&"...")
435- .finish(),
436- Self::StartVerification(target) => formatter
437- .debug_tuple("StartVerification")
438- .field(target)
439- .finish(),
440- Self::Verify(action) => formatter.debug_tuple("Verify").field(action).finish(),
441- }
442- }
443-}
444-
445-#[derive(Clone, Debug, Eq, PartialEq)]
446-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
447-#[non_exhaustive]
448-pub enum NotificationCommand {
449- SetRoomMode {
450- room_id: OwnedRoomId,
451- mode: RoomNotificationMode,
452- },
453- ResetRoomMode(OwnedRoomId),
454- SetDefaultMode {
455- encrypted: bool,
456- one_to_one: bool,
457- mode: RoomNotificationMode,
458- },
459- AddKeyword(String),
460- RemoveKeyword(String),
461-}
462-
463-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
464-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
465-#[non_exhaustive]
466-pub enum LifecycleCommand {
467- Shutdown,
468-}
469-
470-#[derive(Clone, Debug, Eq, PartialEq)]
471-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
472-#[non_exhaustive]
473-pub enum CommandOutput {
474- Done,
475- Room(OwnedRoomId),
476-}
477-
478-#[derive(Clone, Debug, Eq, PartialEq)]
479-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
480-#[non_exhaustive]
481-pub enum MessageContent {
482- Text {
483- content: RichText,
484- mentions: MessageMentions,
485- },
486- Emote {
487- content: RichText,
488- mentions: MessageMentions,
489- },
490- Reply {
491- event_id: OwnedEventId,
492- content: RichText,
493- mentions: MessageMentions,
494- notify_replied_to: bool,
495- },
496-}
497-
498-impl MessageContent {
499- #[must_use]
500- pub fn text(body: impl Into<String>) -> Self {
501- Self::Text {
502- content: RichText::plain(body),
503- mentions: MessageMentions::default(),
504- }
505- }
506-
507- #[must_use]
508- pub fn html(body: impl Into<String>, formatted_body: impl Into<String>) -> Self {
509- Self::Text {
510- content: RichText::html(body, formatted_body),
511- mentions: MessageMentions::default(),
512- }
513- }
514-
515- #[must_use]
516- pub fn emote(content: RichText, mentions: MessageMentions) -> Self {
517- Self::Emote { content, mentions }
518- }
519-
520- #[must_use]
521- pub fn reply(event_id: OwnedEventId, content: RichText, mentions: MessageMentions) -> Self {
522- Self::Reply {
523- event_id,
524- content,
525- mentions,
526- notify_replied_to: true,
527- }
528- }
529-
530- #[must_use]
531- pub fn reply_without_notification(
532- event_id: OwnedEventId,
533- content: RichText,
534- mentions: MessageMentions,
535- ) -> Self {
536- Self::Reply {
537- event_id,
538- content,
539- mentions,
540- notify_replied_to: false,
541- }
542- }
543-}
544-
545-#[derive(Clone, Debug, Eq, PartialEq)]
546-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
547-#[non_exhaustive]
548-pub enum MessageEditContent {
549- Text {
550- content: RichText,
551- mentions: MessageMentions,
552- },
553- Emote {
554- content: RichText,
555- mentions: MessageMentions,
556- },
557-}
558-
559-impl MessageEditContent {
560- #[must_use]
561- pub fn text(body: impl Into<String>) -> Self {
562- Self::Text {
563- content: RichText::plain(body),
564- mentions: MessageMentions::default(),
565- }
566- }
567-
568- #[must_use]
569- pub fn html(body: impl Into<String>, formatted_body: impl Into<String>) -> Self {
570- Self::Text {
571- content: RichText::html(body, formatted_body),
572- mentions: MessageMentions::default(),
573- }
574- }
575-
576- #[must_use]
577- pub fn emote(content: RichText, mentions: MessageMentions) -> Self {
578- Self::Emote { content, mentions }
579- }
580-}
581-
582-#[derive(Clone, Debug, Default, Eq, PartialEq)]
583-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
584-#[non_exhaustive]
585-pub struct RichText {
586- pub body: String,
587- pub formatted_body: Option<String>,
588-}
589-
590-impl RichText {
591- #[must_use]
592- pub fn plain(body: impl Into<String>) -> Self {
593- Self {
594- body: body.into(),
595- formatted_body: None,
596- }
597- }
598-
599- #[must_use]
600- pub fn html(body: impl Into<String>, formatted_body: impl Into<String>) -> Self {
601- Self {
602- body: body.into(),
603- formatted_body: Some(formatted_body.into()),
604- }
605- }
606-}
607-
608-#[derive(Clone, Debug, Default, Eq, PartialEq)]
609-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
610-#[non_exhaustive]
611-pub struct MessageMentions {
612- pub user_ids: Vec<OwnedUserId>,
613- pub room: bool,
614-}
615-
616-impl MessageMentions {
617- #[must_use]
618- pub fn with_user_ids(user_ids: impl IntoIterator<Item = OwnedUserId>) -> Self {
619- Self {
620- user_ids: user_ids.into_iter().collect(),
621- room: false,
622- }
623- }
624-
625- #[must_use]
626- pub fn room() -> Self {
627- Self {
628- user_ids: Vec::new(),
629- room: true,
630- }
631- }
632-}
633-
634-#[derive(Clone, Debug, Eq, PartialEq)]
635-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
636-#[non_exhaustive]
637-pub enum Event {
638- ConnectionChanged(ConnectionState),
639- Authenticated {
640- user_id: OwnedUserId,
641- },
642- /// the tokens rotated. with a `SessionStore` the record is already saved by now,
643- /// without one persist a fresh `ClientHandle::session_record()` atomically
644- SessionTokensChanged,
645- /// the rotated tokens didn't reach the `SessionStore`, so the saved record may not
646- /// restore. the engine tries again on the next rotation
647- SessionSaveFailed {
648- message: String,
649- },
650- /// the server stopped honouring our token and the engine stops. a soft logout
651- /// names the device to log in again as, keeping the store. otherwise the record
652- /// and the store are both dead
653- SessionAccessRejected {
654- soft_logout: Option<OwnedDeviceId>,
655- },
656- NotificationSettingsChanged,
657- VerificationTargetChanged(VerificationTarget),
658- RoomsChanged,
659- /// an image pack came, went or changed somewhere. ask `image_packs` again
660- ImagePacksChanged,
661- /// replaces whatever you had for this timeline. every diff after it applies to it
662- TimelineReset {
663- timeline_id: TimelineId,
664- items: Vector<TimelineItem>,
665- },
666- TimelineChanged {
667- timeline_id: TimelineId,
668- diffs: Vec<TimelineDiff>,
669- },
670- HistoryChanged {
671- timeline_id: TimelineId,
672- state: HistoryState,
673- },
674- /// the timeline was closed, or its room was left, and nothing more comes for it
675- /// until another `TimelineReset`
676- TimelineClosed {
677- timeline_id: TimelineId,
678- },
679- /// a room that stops being watched says so with an empty `users`
680- TypingChanged(TypingState),
681- MediaUploadProgress {
682- room_id: OwnedRoomId,
683- transaction_id: OwnedTransactionId,
684- uploaded_bytes: u64,
685- total_bytes: u64,
686- complete: bool,
687- },
688- /// not the end of it. `will_retry` means the send queue has another go by itself,
689- /// otherwise it waits for `retry_send` or `cancel_send`
690- MediaUploadFailed {
691- room_id: OwnedRoomId,
692- transaction_id: OwnedTransactionId,
693- kind: crate::ErrorKind,
694- will_retry: bool,
695- },
696- /// an upload that turned up in a progress or failure event gets exactly one of
697- /// these while the engine runs, and nothing about it after. stopping ends
698- /// nothing, `Stopped` covers whatever is still going and the sdk resumes it next run
699- MediaUploadFinished {
700- room_id: OwnedRoomId,
701- transaction_id: OwnedTransactionId,
702- outcome: UploadOutcome,
703- },
704- EncryptionChanged(EncryptionState),
705- VerificationChanged(VerificationState),
706- /// logout went through and the engine stops. delete the record and the store
707- LoggedOut,
708- Failed(EngineFailure),
709- /// always the last event. keep the saved record unless `LoggedOut` or a
710- /// hard `SessionAccessRejected` came first. commands still in flight answer `Interrupted`
711- Stopped,
712-}
713-
714-#[derive(Clone, Debug, Eq, PartialEq)]
715-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
716-#[non_exhaustive]
717-pub enum UploadOutcome {
718- Sent {
719- event_id: OwnedEventId,
720- },
721- Cancelled,
722- /// its room was forgotten, and the queue with it
723- RoomForgotten,
724- /// the sdk's update channel overflowed while it ended, so whether it was sent is unknown
725- Missed,
726-}
727-
728-/// why `Engine::run` came back, when nothing went wrong
729-#[derive(Clone, Debug, Eq, PartialEq)]
730-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
731-#[non_exhaustive]
732-pub enum Termination {
733- ShutDown,
734- /// every `ClientHandle` was dropped, so nobody could have asked for anything else
735- HandlesDropped,
736- LoggedOut,
737- /// the server rejected our token for good
738- AccessRevoked,
739- /// the server wants this device to log in again, and its e2ee keys live in the store
740- SoftLoggedOut {
741- device_id: OwnedDeviceId,
742- },
743-}
744-
745-impl Termination {
746- #[must_use]
747- pub const fn retain(&self) -> Retain {
748- match self {
749- Self::ShutDown | Self::HandlesDropped => Retain::RecordAndStore,
750- Self::SoftLoggedOut { .. } => Retain::Store,
751- Self::LoggedOut | Self::AccessRevoked => Retain::Nothing,
752- }
753- }
754-}
755-
756-/// what's still worth keeping of the saved record and the store
757-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
758-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
759-pub enum Retain {
760- RecordAndStore,
761- /// keep the store and a record naming the device to log in again as
762- Store,
763- Nothing,
764-}
765-
766-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
767-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
768-#[non_exhaustive]
769-pub enum ConnectionState {
770- Idle,
771- Connecting,
772- Online,
773- Offline,
774- Stopped,
775-}
776-
777-#[derive(Clone, Debug, Eq, PartialEq)]
778-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
779-#[non_exhaustive]
780-pub struct RoomSummary {
781- pub room_id: OwnedRoomId,
782- pub name: String,
783- pub membership: RoomMembership,
784- pub unread_messages: u64,
785- pub unread_mentions: u64,
786- /// unread events the push rules said to notify about, which is neither of the above
787- pub unread_notifications: u64,
788- pub encrypted: bool,
789- pub space: bool,
790- pub direct: bool,
791- pub favourite: bool,
792- pub low_priority: bool,
793- pub avatar: Option<MediaId>,
794- pub latest_event_timestamp_ms: Option<u64>,
795- pub latest_event: Option<LatestEvent>,
796- pub notification_mode: Option<RoomNotificationMode>,
797- pub notification_mode_custom: bool,
798- pub topic: Option<String>,
799- pub canonical_alias: Option<OwnedRoomAliasId>,
800- pub joined_members: u64,
801- pub invited_members: u64,
802- /// who a nameless room gets named after
803- pub heroes: Vec<RoomHero>,
804- /// set once the room has been upgraded, this is where everyone went
805- pub successor: Option<OwnedRoomId>,
806- pub inviter: Option<OwnedUserId>,
807- /// joined spaces that list this room as a child
808- pub parent_spaces: Vec<OwnedRoomId>,
809- /// for a space, the children it lists, joined or not
810- pub children: Vec<OwnedRoomId>,
811-}
812-
813-#[derive(Clone, Debug, Eq, PartialEq)]
814-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
815-#[non_exhaustive]
816-pub struct RoomHero {
817- pub user_id: OwnedUserId,
818- pub display_name: Option<String>,
819- pub avatar: Option<MediaId>,
820-}
821-
822-/// enough for a room list preview line. `kind` and `body` are only set for messages,
823-/// and an edit shows its new text
824-#[derive(Clone, Debug, Eq, PartialEq)]
825-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
826-#[non_exhaustive]
827-pub struct LatestEvent {
828- pub sender: Option<OwnedUserId>,
829- pub kind: Option<MessageKind>,
830- pub body: Option<String>,
831- pub timestamp_ms: Option<u64>,
832- pub send_state: SendState,
833-}
834-
835-#[derive(Clone, Debug, Eq, PartialEq)]
836-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
837-#[non_exhaustive]
838-pub struct Profile {
839- pub user_id: OwnedUserId,
840- pub display_name: Option<String>,
841- pub avatar: Option<MediaId>,
842-}
843-
844-/// one room in a space's hierarchy, as the server describes it, which may be a room you're not in
845-#[derive(Clone, Debug, Eq, PartialEq)]
846-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
847-#[non_exhaustive]
848-pub struct SpaceChild {
849- pub room_id: OwnedRoomId,
850- pub name: String,
851- pub topic: Option<String>,
852- pub avatar: Option<MediaId>,
853- pub canonical_alias: Option<OwnedRoomAliasId>,
854- pub joined_members: u64,
855- pub space: bool,
856- pub children_count: u64,
857- pub membership: Option<RoomMembership>,
858- pub via: Vec<OwnedServerName>,
859- pub suggested: bool,
860- pub world_readable: Option<bool>,
861- pub guest_can_join: bool,
862-}
863-
864-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
865-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
866-#[non_exhaustive]
867-pub enum RoomNotificationMode {
868- AllMessages,
869- MentionsAndKeywordsOnly,
870- Mute,
871-}
872-
873-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
874-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
875-#[non_exhaustive]
876-pub struct RoomNotificationSettings {
877- pub mode: RoomNotificationMode,
878- pub custom: bool,
879-}
880-
881-#[derive(Clone, Debug)]
882-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
883-#[non_exhaustive]
884-pub enum PushNotificationStatus {
885- Event(Box<PushNotification>),
886- NotFound,
887- FilteredOut,
888- Redacted,
889-}
890-
891-#[derive(Clone, Debug)]
892-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
893-#[non_exhaustive]
894-pub struct PushNotification {
895- pub event_id: OwnedEventId,
896- pub event: PushNotificationEvent,
897- pub sender: PushNotificationSender,
898- pub room: PushNotificationRoom,
899- pub noisy: Option<bool>,
900- pub has_mention: Option<bool>,
901- pub thread_id: Option<OwnedEventId>,
902-}
903-
904-#[derive(Clone, Debug, Eq, PartialEq)]
905-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
906-#[non_exhaustive]
907-pub struct PushNotificationSender {
908- pub user_id: OwnedUserId,
909- pub display_name: Option<String>,
910- pub avatar_url: Option<MediaId>,
911- pub name_ambiguous: bool,
912-}
913-
914-#[derive(Clone, Debug, Eq, PartialEq)]
915-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
916-#[non_exhaustive]
917-pub struct PushNotificationRoom {
918- pub room_id: OwnedRoomId,
919- pub name: String,
920- pub avatar_url: Option<MediaId>,
921- pub canonical_alias: Option<OwnedRoomAliasId>,
922- pub topic: Option<String>,
923- pub encrypted: Option<bool>,
924- pub direct: bool,
925- pub joined_members_count: u64,
926- pub service_members: Vec<OwnedUserId>,
927- pub active_service_members_count: u64,
928- pub space: bool,
929-}
930-
931-#[derive(Clone, Debug)]
932-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
933-#[non_exhaustive]
934-pub enum PushNotificationEvent {
935- Timeline(Raw<AnySyncTimelineEvent>),
936- Invite(Raw<StrippedRoomMemberEvent>),
937-}
938-
939-#[derive(Clone, Debug, Eq, PartialEq)]
940-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
941-#[non_exhaustive]
942-pub struct RoomPowerLevels {
943- pub ban: i64,
944- pub invite: i64,
945- pub kick: i64,
946- pub redact: i64,
947- pub events_default: i64,
948- pub state_default: i64,
949- pub users_default: i64,
950- pub room_notification: i64,
951- pub users: Vec<(OwnedUserId, i64)>,
952- pub events: Vec<(String, i64)>,
953- /// in rooms from version 12 the creators outrank everyone and aren't listed in `users`
954- pub creators: Vec<OwnedUserId>,
955-}
956-
957-#[derive(Clone, Debug, Default, Eq, PartialEq)]
958-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
959-#[non_exhaustive]
960-pub struct PowerLevelChanges {
961- pub ban: Option<i64>,
962- pub invite: Option<i64>,
963- pub kick: Option<i64>,
964- pub redact: Option<i64>,
965- pub events_default: Option<i64>,
966- pub state_default: Option<i64>,
967- pub users_default: Option<i64>,
968- pub room_notification: Option<i64>,
969- pub room_name: Option<i64>,
970- pub room_avatar: Option<i64>,
971- pub room_topic: Option<i64>,
972- pub space_child: Option<i64>,
973- pub beacon: Option<i64>,
974- pub beacon_info: Option<i64>,
975-}
976-
977-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
978-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
979-#[non_exhaustive]
980-pub enum RoomMembership {
981- Joined,
982- Invited,
983- Left,
984- Knocked,
985- Banned,
986-}
987-
988-#[derive(Clone, Debug, Default, Eq, PartialEq)]
989-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
990-#[non_exhaustive]
991-pub struct RoomListFilter {
992- pub membership: Option<RoomMembership>,
993- pub category: Option<RoomCategory>,
994- pub favourite: Option<bool>,
995- pub unread_only: bool,
996- pub search: Option<String>,
997-}
998-
999-impl RoomListFilter {
1000- #[must_use]
1001- pub fn with_membership(mut self, membership: RoomMembership) -> Self {
1002- self.membership = Some(membership);
1003- self
1004- }
1005-
1006- #[must_use]
1007- pub fn with_category(mut self, category: RoomCategory) -> Self {
1008- self.category = Some(category);
1009- self
1010- }
1011-
1012- #[must_use]
1013- pub fn with_favourite(mut self, favourite: bool) -> Self {
1014- self.favourite = Some(favourite);
1015- self
1016- }
1017-
1018- #[must_use]
1019- pub const fn with_unread_only(mut self, unread_only: bool) -> Self {
1020- self.unread_only = unread_only;
1021- self
1022- }
1023-
1024- #[must_use]
1025- pub fn with_search(mut self, search: impl Into<String>) -> Self {
1026- self.search = Some(search.into());
1027- self
1028- }
1029-
1030- #[must_use]
1031- pub fn matches(&self, room: &RoomSummary) -> bool {
1032- self.matcher().matches(room)
1033- }
1034-
1035- /// lowercases the search once, for filtering a whole list
1036- #[must_use]
1037- pub fn matcher(&self) -> RoomMatcher<'_> {
1038- RoomMatcher {
1039- filter: self,
1040- search: self.search.as_deref().map(str::to_lowercase),
1041- }
1042- }
1043-}
1044-
1045-#[derive(Debug)]
1046-pub struct RoomMatcher<'a> {
1047- filter: &'a RoomListFilter,
1048- search: Option<String>,
1049-}
1050-
1051-impl RoomMatcher<'_> {
1052- #[must_use]
1053- pub fn matches(&self, room: &RoomSummary) -> bool {
1054- let filter = self.filter;
1055- filter
1056- .membership
1057- .is_none_or(|membership| room.membership == membership)
1058- && filter.category.is_none_or(|category| match category {
1059- RoomCategory::People => room.direct && !room.space,
1060- RoomCategory::Rooms => !room.direct && !room.space,
1061- RoomCategory::Spaces => room.space,
1062- })
1063- && filter
1064- .favourite
1065- .is_none_or(|favourite| room.favourite == favourite)
1066- && (!filter.unread_only || room.unread_messages > 0 || room.unread_mentions > 0)
1067- && self.search.as_ref().is_none_or(|search| {
1068- room.name.to_lowercase().contains(search)
1069- || room.room_id.as_str().to_lowercase().contains(search)
1070- })
1071- }
1072-}
1073-
1074-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1075-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1076-#[non_exhaustive]
1077-pub enum RoomCategory {
1078- People,
1079- Rooms,
1080- Spaces,
1081-}
1082-
1083-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1084-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1085-#[non_exhaustive]
1086-pub enum MemberFilter {
1087- Active,
1088- Joined,
1089- Invited,
1090- Banned,
1091- Left,
1092- All,
1093-}
1094-
1095-#[derive(Clone, Debug, Eq, PartialEq)]
1096-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1097-#[non_exhaustive]
1098-pub enum MemberMembership {
1099- Joined,
1100- Invited,
1101- Left,
1102- Knocked,
1103- Banned,
1104- Custom(String),
1105-}
1106-
1107-#[derive(Clone, Debug, Eq, PartialEq)]
1108-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1109-#[non_exhaustive]
1110-pub struct MemberSummary {
1111- pub user_id: OwnedUserId,
1112- pub name: String,
1113- pub display_name: Option<String>,
1114- pub avatar: Option<MediaId>,
1115- pub membership: MemberMembership,
1116- pub own: bool,
1117- pub name_ambiguous: bool,
1118- pub ignored: bool,
1119- pub service: bool,
1120-}
1121-
1122-#[derive(Clone, Debug, Eq, PartialEq)]
1123-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1124-#[non_exhaustive]
1125-pub struct OwnDevice {
1126- pub device_id: OwnedDeviceId,
1127- pub display_name: Option<String>,
1128- pub last_seen_ip: Option<String>,
1129- pub last_seen_timestamp_ms: Option<u64>,
1130- pub current: bool,
1131-}
1132-
1133-#[derive(Clone, Debug, Eq, PartialEq)]
1134-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1135-#[non_exhaustive]
1136-pub enum TimelineItem {
1137- Event(Box<TimelineEvent>),
1138- DayDivider { timestamp_ms: u64 },
1139- ReadMarker,
1140- TimelineStart,
1141-}
1142-
1143-#[derive(Clone, Debug, Eq, PartialEq)]
1144-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1145-#[non_exhaustive]
1146-pub enum TimelineDiff {
1147- Append { values: Vec<TimelineItem> },
1148- Clear,
1149- PushFront { value: TimelineItem },
1150- PushBack { value: TimelineItem },
1151- PopFront,
1152- PopBack,
1153- Insert { index: usize, value: TimelineItem },
1154- Set { index: usize, value: TimelineItem },
1155- Remove { index: usize },
1156- Truncate { length: usize },
1157- Reset { values: Vec<TimelineItem> },
1158-}
1159-
1160-#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
1161-#[error("timeline diff is incompatible with the current {length}-item snapshot")]
1162-pub struct TimelineDiffError {
1163- pub length: usize,
1164-}
1165-
1166-impl TimelineDiff {
1167- /// Apply a diff only when its indices match the current snapshot.
1168- /// On error the snapshot is unchanged; request `TimelineCommand::Resync`
1169- /// and ignore further diffs until the corresponding `TimelineReset`.
1170- pub fn try_apply(self, items: &mut Vector<TimelineItem>) -> Result<(), TimelineDiffError> {
1171- let length = items.len();
1172- if self.length_after(length).is_none() {
1173- return Err(TimelineDiffError { length });
1174- }
1175- self.apply(items);
1176- Ok(())
1177- }
1178-
1179- /// a whole `TimelineChanged` or none of it. whether a diff fits depends only on the
1180- /// length before it, so the batch is checked against lengths alone before anything
1181- /// moves. the error carries the length the snapshot would have had at the misfit
1182- pub fn try_apply_all(
1183- diffs: Vec<Self>,
1184- items: &mut Vector<TimelineItem>,
1185- ) -> Result<(), TimelineDiffError> {
1186- diffs.iter().try_fold(items.len(), |length, diff| {
1187- diff.length_after(length)
1188- .ok_or(TimelineDiffError { length })
1189- })?;
1190- for diff in diffs {
1191- diff.apply(items);
1192- }
1193- Ok(())
1194- }
1195-
1196- fn length_after(&self, length: usize) -> Option<usize> {
1197- match self {
1198- Self::Append { values } => Some(length + values.len()),
1199- Self::Clear => Some(0),
1200- Self::PushFront { .. } | Self::PushBack { .. } => Some(length + 1),
1201- Self::PopFront | Self::PopBack => length.checked_sub(1),
1202- Self::Insert { index, .. } => (*index <= length).then_some(length + 1),
1203- Self::Set { index, .. } => (*index < length).then_some(length),
1204- Self::Remove { index } => (*index < length).then(|| length - 1),
1205- Self::Truncate { length: target } => (*target <= length).then_some(*target),
1206- Self::Reset { values } => Some(values.len()),
1207- }
1208- }
1209-
1210- fn apply(self, items: &mut Vector<TimelineItem>) {
1211- match self {
1212- Self::Append { values } => items.extend(values),
1213- Self::Clear => items.clear(),
1214- Self::PushFront { value } => items.push_front(value),
1215- Self::PushBack { value } => items.push_back(value),
1216- Self::PopFront => {
1217- items.pop_front();
1218- }
1219- Self::PopBack => {
1220- items.pop_back();
1221- }
1222- Self::Insert { index, value } => items.insert(index, value),
1223- Self::Set { index, value } => {
1224- items.set(index, value);
1225- }
1226- Self::Remove { index } => {
1227- items.remove(index);
1228- }
1229- Self::Truncate { length } => items.truncate(length),
1230- Self::Reset { values } => *items = values.into_iter().collect(),
1231- }
1232- }
1233-}
1234-
1235-#[cfg(test)]
1236-mod tests {
1237- use imbl::{Vector, vector};
1238-
1239- use super::{TimelineDiff, TimelineItem};
1240-
1241- fn item(timestamp_ms: u64) -> TimelineItem {
1242- TimelineItem::DayDivider { timestamp_ms }
1243- }
1244-
1245- #[test]
1246- fn timeline_diff_try_apply_handles_every_variant() {
1247- let mut items = vector![item(1), item(2)];
1248-
1249- TimelineDiff::Append {
1250- values: vec![item(3)],
1251- }
1252- .try_apply(&mut items)
1253- .unwrap();
1254- assert_eq!(items, vector![item(1), item(2), item(3)]);
1255-
1256- TimelineDiff::Clear.try_apply(&mut items).unwrap();
1257- assert!(items.is_empty());
1258-
1259- TimelineDiff::PushFront { value: item(2) }
1260- .try_apply(&mut items)
1261- .unwrap();
1262- TimelineDiff::PushBack { value: item(3) }
1263- .try_apply(&mut items)
1264- .unwrap();
1265- assert_eq!(items, vector![item(2), item(3)]);
1266-
1267- TimelineDiff::PopFront.try_apply(&mut items).unwrap();
1268- TimelineDiff::PopBack.try_apply(&mut items).unwrap();
1269- assert!(items.is_empty());
1270-
1271- TimelineDiff::Reset {
1272- values: vec![item(1), item(3)],
1273- }
1274- .try_apply(&mut items)
1275- .unwrap();
1276- TimelineDiff::Insert {
1277- index: 1,
1278- value: item(2),
1279- }
1280- .try_apply(&mut items)
1281- .unwrap();
1282- assert_eq!(items, vector![item(1), item(2), item(3)]);
1283-
1284- TimelineDiff::Set {
1285- index: 1,
1286- value: item(4),
1287- }
1288- .try_apply(&mut items)
1289- .unwrap();
1290- assert_eq!(items, vector![item(1), item(4), item(3)]);
1291-
1292- TimelineDiff::Remove { index: 1 }
1293- .try_apply(&mut items)
1294- .unwrap();
1295- assert_eq!(items, vector![item(1), item(3)]);
1296-
1297- TimelineDiff::Truncate { length: 1 }
1298- .try_apply(&mut items)
1299- .unwrap();
1300- assert_eq!(items, vector![item(1)]);
1301- }
1302-
1303- #[test]
1304- fn invalid_timeline_diff_leaves_snapshot_unchanged() {
1305- let invalid = [
1306- TimelineDiff::Insert {
1307- index: 3,
1308- value: item(3),
1309- },
1310- TimelineDiff::Set {
1311- index: 2,
1312- value: item(3),
1313- },
1314- TimelineDiff::Remove { index: 2 },
1315- TimelineDiff::Truncate { length: 3 },
1316- ];
1317- for diff in invalid {
1318- let mut items = vector![item(1), item(2)];
1319- assert_eq!(diff.try_apply(&mut items).unwrap_err().length, 2);
1320- assert_eq!(items, vector![item(1), item(2)]);
1321- }
1322- for diff in [TimelineDiff::PopFront, TimelineDiff::PopBack] {
1323- let mut items = Vector::new();
1324- assert_eq!(diff.try_apply(&mut items).unwrap_err().length, 0);
1325- assert!(items.is_empty());
1326- }
1327- }
1328-
1329- #[test]
1330- fn a_batch_that_goes_wrong_halfway_changes_nothing() {
1331- let mut items = vector![item(1), item(2)];
1332- let batch = vec![
1333- TimelineDiff::PushBack { value: item(3) },
1334- TimelineDiff::PushBack { value: item(4) },
1335- TimelineDiff::Remove { index: 0 },
1336- TimelineDiff::Set {
1337- index: 3,
1338- value: item(5),
1339- },
1340- ];
1341- assert_eq!(
1342- TimelineDiff::try_apply_all(batch, &mut items)
1343- .unwrap_err()
1344- .length,
1345- 3
1346- );
1347- assert_eq!(items, vector![item(1), item(2)]);
1348-
1349- let batch = vec![
1350- TimelineDiff::PushBack { value: item(3) },
1351- TimelineDiff::Remove { index: 0 },
1352- TimelineDiff::Set {
1353- index: 1,
1354- value: item(4),
1355- },
1356- ];
1357- TimelineDiff::try_apply_all(batch, &mut items).unwrap();
1358- assert_eq!(items, vector![item(2), item(4)]);
1359- }
1360-}
1361-
1362-#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1363-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1364-pub struct TimelineItemId(String);
1365-
1366-impl TimelineItemId {
1367- pub(crate) fn new(value: String) -> Self {
1368- Self(value)
1369- }
1370-
1371- #[must_use]
1372- pub fn as_str(&self) -> &str {
1373- &self.0
1374- }
1375-}
1376-
1377-#[derive(Clone, Debug, Eq, PartialEq)]
1378-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1379-#[non_exhaustive]
1380-pub struct TimelineEvent {
1381- pub item_id: TimelineItemId,
1382- pub event_id: Option<OwnedEventId>,
1383- pub transaction_id: Option<OwnedTransactionId>,
1384- pub sender: Sender,
1385- pub timestamp_ms: u64,
1386- pub own: bool,
1387- pub send_state: SendState,
1388- pub content: TimelineContent,
1389- pub reply_to: Option<ReplyReference>,
1390- pub thread_root: Option<OwnedEventId>,
1391- pub thread_summary: Option<ThreadSummary>,
1392- pub reactions: Vec<Reaction>,
1393- pub read_by: Vec<OwnedUserId>,
1394- pub editable: bool,
1395-}
1396-
1397-impl TimelineEvent {
1398- /// what to hand edit, redact and react. works before the server has echoed it back
1399- #[must_use]
1400- pub fn target(&self) -> Option<EventRef> {
1401- self.event_id
1402- .clone()
1403- .map(EventRef::Remote)
1404- .or_else(|| self.transaction_id.clone().map(EventRef::Local))
1405- }
1406-}
1407-
1408-#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1409-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1410-#[non_exhaustive]
1411-pub enum EventRef {
1412- Remote(OwnedEventId),
1413- /// a local echo, by the transaction id it was queued under
1414- Local(OwnedTransactionId),
1415-}
1416-
1417-impl From<OwnedEventId> for EventRef {
1418- fn from(event_id: OwnedEventId) -> Self {
1419- Self::Remote(event_id)
1420- }
1421-}
1422-
1423-#[derive(Clone, Debug, Eq, PartialEq)]
1424-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1425-#[non_exhaustive]
1426-pub struct Sender {
1427- pub user_id: OwnedUserId,
1428- pub display_name: Option<String>,
1429- pub avatar: Option<MediaId>,
1430- /// someone else in the room goes by the same name, so show the user id too
1431- pub name_ambiguous: bool,
1432-}
1433-
1434-impl Sender {
1435- #[must_use]
1436- pub fn name(&self) -> &str {
1437- self.display_name
1438- .as_deref()
1439- .unwrap_or_else(|| self.user_id.as_str())
1440- }
1441-}
1442-
1443-#[derive(Clone, Debug, Eq, PartialEq)]
1444-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1445-#[non_exhaustive]
1446-pub enum TimelineContent {
1447- Message(Message),
1448- Sticker {
1449- body: String,
1450- media: MediaDescriptor,
1451- },
1452- Poll {
1453- question: String,
1454- },
1455- Redacted,
1456- UnableToDecrypt(UtdCause),
1457- Membership {
1458- user_id: OwnedUserId,
1459- display_name: Option<String>,
1460- change: MembershipChange,
1461- },
1462- Profile {
1463- user_id: OwnedUserId,
1464- display_name: Option<Change<Option<String>>>,
1465- avatar: Option<Change<Option<MediaId>>>,
1466- },
1467- /// `None` means it was cleared, or the event was redacted
1468- RoomName(Option<String>),
1469- RoomTopic(Option<String>),
1470- RoomAvatar(Option<MediaId>),
1471- EncryptionEnabled,
1472- RoomCreated,
1473- RoomUpgraded {
1474- successor: OwnedRoomId,
1475- },
1476- OtherState {
1477- event_type: String,
1478- state_key: String,
1479- },
1480- Other {
1481- event_type: String,
1482- },
1483-}
1484-
1485-#[derive(Clone, Debug, Eq, PartialEq)]
1486-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1487-#[non_exhaustive]
1488-pub struct Message {
1489- pub kind: MessageKind,
1490- pub body: String,
1491- /// only for `MessageKind::Location`
1492- pub geo_uri: Option<String>,
1493- pub formatted_body: Option<String>,
1494- pub mentions: Option<MessageMentions>,
1495- pub edited: bool,
1496- pub media: Option<MediaDescriptor>,
1497- /// previews the sender attached (msc4095), so showing them asks no one about the url.
1498- /// one for a url the body doesn't contain is dropped
1499- pub link_previews: Vec<LinkPreview>,
1500-}
1501-
1502-/// a link card, from the sender's own message or from `ClientHandle::url_preview`. page
1503-/// text passes through whoever made it unfiltered, so it arrives trimmed to a sane length
1504-#[derive(Clone, Debug, Eq, PartialEq)]
1505-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1506-#[non_exhaustive]
1507-pub struct LinkPreview {
1508- /// the url as it appears in the message, or as it was asked about
1509- pub url: String,
1510- /// where the page says it lives, when that's somewhere else
1511- pub canonical_url: Option<String>,
1512- pub title: Option<String>,
1513- pub description: Option<String>,
1514- pub site_name: Option<String>,
1515- pub image: Option<LinkPreviewImage>,
1516-}
1517-
1518-#[derive(Clone, Debug, Eq, PartialEq)]
1519-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1520-#[non_exhaustive]
1521-pub struct LinkPreviewImage {
1522- pub media_id: MediaId,
1523- pub mimetype: Option<String>,
1524- pub size_bytes: Option<u64>,
1525- pub width: Option<u64>,
1526- pub height: Option<u64>,
1527-}
1528-
1529-#[derive(Clone, Debug, Eq, PartialEq)]
1530-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1531-#[non_exhaustive]
1532-pub enum MessageKind {
1533- Text,
1534- Emote,
1535- Notice,
1536- ServerNotice,
1537- Image,
1538- Video,
1539- Audio,
1540- File,
1541- Location,
1542- VerificationRequest,
1543- Other(String),
1544-}
1545-
1546-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1547-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1548-#[non_exhaustive]
1549-pub enum MembershipChange {
1550- Joined,
1551- Left,
1552- Banned,
1553- Unbanned,
1554- Kicked,
1555- KickedAndBanned,
1556- Invited,
1557- InvitationAccepted,
1558- InvitationRejected,
1559- InvitationRevoked,
1560- Knocked,
1561- KnockAccepted,
1562- KnockRetracted,
1563- KnockDenied,
1564- /// nothing a person would call a change, or something the sdk couldn't make sense of
1565- Other,
1566-}
1567-
1568-#[derive(Clone, Debug, Eq, PartialEq)]
1569-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1570-pub struct Change<T> {
1571- pub old: T,
1572- pub new: T,
1573-}
1574-
1575-/// why a message couldn't be read, worth telling the user since some of these they can fix
1576-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1577-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1578-#[non_exhaustive]
1579-pub enum UtdCause {
1580- Unknown,
1581- SentBeforeWeJoined,
1582- VerificationViolation,
1583- UnsignedDevice,
1584- UnknownDevice,
1585- HistoricalMessageAndBackupIsDisabled,
1586- WithheldForUnverifiedOrInsecureDevice,
1587- WithheldBySender,
1588- HistoricalMessageAndDeviceIsUnverified,
1589-}
1590-
1591-#[derive(Clone, Debug, Eq, PartialEq)]
1592-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1593-#[non_exhaustive]
1594-pub struct ThreadSummary {
1595- pub reply_count: u32,
1596- pub latest_event_id: Option<OwnedEventId>,
1597- pub latest_event_timestamp_ms: Option<u64>,
1598-}
1599-
1600-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1601-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1602-#[non_exhaustive]
1603-pub enum SendState {
1604- Sent,
1605- Sending,
1606- /// `will_retry` means macltok has it in hand. otherwise it's wedged until you
1607- /// `retry_send` or `cancel_send`, so that's when to show a retry button
1608- Failed {
1609- will_retry: bool,
1610- },
1611-}
1612-
1613-#[derive(Clone, Debug, Eq, PartialEq)]
1614-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1615-#[non_exhaustive]
1616-pub struct ReplyReference {
1617- pub event_id: OwnedEventId,
1618- pub details: ReplyDetails,
1619-}
1620-
1621-/// the replied-to event. macltok asks for it when it isn't loaded, `Pending` then turns into one of the others
1622-#[derive(Clone, Debug, Eq, PartialEq)]
1623-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1624-#[non_exhaustive]
1625-pub enum ReplyDetails {
1626- Pending,
1627- Ready {
1628- sender: Sender,
1629- content: Box<TimelineContent>,
1630- timestamp_ms: u64,
1631- },
1632- Unavailable,
1633-}
1634-
1635-#[derive(Clone, Debug, Eq, PartialEq)]
1636-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1637-#[non_exhaustive]
1638-pub struct Reaction {
1639- pub key: String,
1640- /// a custom emote's shortcode, from whichever sender gave one
1641- pub shortcode: Option<String>,
1642- /// the custom emote itself, when the key is an image
1643- pub image: Option<MediaId>,
1644- pub count: usize,
1645- pub own: bool,
1646- pub senders: Vec<OwnedUserId>,
1647-}
1648-
1649-/// every custom emote and sticker pack the account can reach, from the user's own
1650-/// pack, each joined room's packs, and the rooms the user enabled everywhere
1651-#[derive(Clone, Debug, Default, Eq, PartialEq)]
1652-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1653-#[non_exhaustive]
1654-pub struct ImagePacks {
1655- /// the user's own pack, then the packs of rooms enabled everywhere
1656- pub global: Vec<ImagePack>,
1657- /// each joined room's own packs, rooms without any left out
1658- pub rooms: imbl::HashMap<OwnedRoomId, Vec<ImagePack>>,
1659-}
1660-
1661-impl ImagePacks {
1662- /// what a picker in this room offers. the user's pack, the room's own, then the
1663- /// rest of the enabled ones, each pack once
1664- #[must_use]
1665- pub fn for_room(&self, room_id: Option<&RoomId>) -> Vec<&ImagePack> {
1666- let is_user = |pack: &&ImagePack| pack.source == ImagePackSource::User;
1667- let own = room_id.and_then(|room_id| self.rooms.get(room_id));
1668- let mut packs: Vec<&ImagePack> = self.global.iter().filter(is_user).collect();
1669- packs.extend(own.into_iter().flatten());
1670- for pack in self.global.iter().filter(|pack| !is_user(pack)) {
1671- if !packs.iter().any(|seen| seen.id == pack.id) {
1672- packs.push(pack);
1673- }
1674- }
1675- packs
1676- }
1677-}
1678-
1679-#[derive(Clone, Debug, Eq, PartialEq)]
1680-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1681-#[non_exhaustive]
1682-pub struct ImagePack {
1683- /// `user`, or `room:<room id>:<state key>`, the same across calls
1684- pub id: String,
1685- pub display_name: Option<String>,
1686- pub avatar: Option<MediaId>,
1687- pub attribution: Option<String>,
1688- pub source: ImagePackSource,
1689- pub images: Vec<PackImage>,
1690-}
1691-
1692-#[derive(Clone, Debug, Eq, PartialEq)]
1693-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1694-#[non_exhaustive]
1695-pub enum ImagePackSource {
1696- User,
1697- Room {
1698- room_id: OwnedRoomId,
1699- state_key: String,
1700- },
1701-}
1702-
1703-#[derive(Clone, Debug, Eq, PartialEq)]
1704-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1705-#[non_exhaustive]
1706-pub struct PackImage {
1707- pub shortcode: String,
1708- pub media_id: MediaId,
1709- /// what `toggle_reaction` takes to react with this image
1710- pub reaction_key: String,
1711- pub body: Option<String>,
1712- pub mimetype: Option<String>,
1713- pub width: Option<u64>,
1714- pub height: Option<u64>,
1715- /// the image's own, else its pack's, else both
1716- pub usage: Vec<PackUsage>,
1717-}
1718-
1719-#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1720-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1721-#[non_exhaustive]
1722-pub enum PackUsage {
1723- Emoticon,
1724- Sticker,
1725-}
1726-
1727-#[derive(Clone, Debug, Eq, PartialEq)]
1728-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1729-#[non_exhaustive]
1730-pub struct MediaDescriptor {
1731- pub id: MediaId,
1732- pub kind: MediaKind,
1733- pub filename: String,
1734- pub caption: Option<String>,
1735- pub mimetype: Option<String>,
1736- pub size_bytes: Option<u64>,
1737- pub width: Option<u64>,
1738- pub height: Option<u64>,
1739- pub duration_ms: Option<u64>,
1740- pub blurhash: Option<String>,
1741- /// the uploader sent a thumbnail, so `MediaSize::Thumbnail` won't need the server to resize
1742- pub has_thumbnail: bool,
1743-}
1744-
1745-#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1746-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1747-pub struct MediaId(String);
1748-
1749-impl MediaId {
1750- pub(crate) fn new(value: String) -> Self {
1751- Self(value)
1752- }
1753-
1754- /// plain media carries no keys, so it needs no registering and never expires
1755- pub(crate) fn plain<U: AsRef<str> + ?Sized>(uri: &U) -> Self {
1756- Self(uri.as_ref().to_owned())
1757- }
1758-
1759- pub(crate) fn as_plain(&self) -> Option<&MxcUri> {
1760- self.0.starts_with("mxc://").then(|| self.0.as_str().into())
1761- }
1762-
1763- #[must_use]
1764- pub fn as_str(&self) -> &str {
1765- &self.0
1766- }
1767-}
1768-
1769-/// media ids are opaque but they do have to survive a trip through a browser. a made up one just fails `MediaUnknown`
1770-impl std::str::FromStr for MediaId {
1771- type Err = std::convert::Infallible;
1772-
1773- fn from_str(value: &str) -> Result<Self, Self::Err> {
1774- Ok(Self(value.to_owned()))
1775- }
1776-}
1777-
1778-impl From<String> for MediaId {
1779- fn from(value: String) -> Self {
1780- Self(value)
1781- }
1782-}
1783-
1784-impl std::fmt::Display for MediaId {
1785- fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1786- self.0.fmt(formatter)
1787- }
1788-}
1789-
1790-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1791-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1792-#[non_exhaustive]
1793-pub enum MediaSize {
1794- Full,
1795- /// a hint. an uploader's own thumbnail wins over the server resizing anything
1796- Thumbnail {
1797- width: u32,
1798- height: u32,
1799- },
1800-}
1801-
1802-#[derive(Clone, Debug, Eq, PartialEq)]
1803-#[non_exhaustive]
1804-pub struct Media {
1805- pub bytes: Arc<[u8]>,
1806- pub mimetype: String,
1807-}
1808-
1809-/// where media lives, for a host that fetches and decrypts it itself
1810-#[derive(Clone, Debug, Eq, PartialEq)]
1811-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1812-#[non_exhaustive]
1813-pub enum MediaLocation {
1814- File(MediaFile),
1815- /// a resize of plaintext, so never encrypted. plenty of servers refuse some, and `media`
1816- /// then fetches `fallback` instead, so should you
1817- ServerThumbnail {
1818- url: String,
1819- fallback: MediaFile,
1820- },
1821- /// still in the send queue with no url yet. `media` has the bytes
1822- Local,
1823-}
1824-
1825-#[derive(Clone, Debug, Eq, PartialEq)]
1826-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1827-#[non_exhaustive]
1828-pub struct MediaFile {
1829- pub url: String,
1830- pub mimetype: Option<String>,
1831- pub declared_size: Option<u64>,
1832- pub encryption: Option<AttachmentEncryption>,
1833-}
1834-
1835-/// AES-256-CTR, the only attachment cipher matrix has
1836-#[derive(Clone, Debug, Eq, PartialEq)]
1837-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1838-#[non_exhaustive]
1839-pub struct AttachmentEncryption {
1840- /// the JWK `k`, unpadded url-safe base64
1841- pub key: String,
1842- /// unpadded base64, the initial counter block
1843- pub iv: String,
1844- /// unpadded base64 SHA-256 of the whole ciphertext, so a partial read goes unchecked
1845- pub sha256: String,
1846-}
1847-
1848-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1849-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1850-#[non_exhaustive]
1851-pub enum MediaKind {
1852- Image,
1853- Video,
1854- Audio,
1855- File,
1856- Sticker,
1857-}
1858-
1859-#[derive(Clone, Debug, Eq, PartialEq)]
1860-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1861-#[non_exhaustive]
1862-pub struct TypingState {
1863- pub room_id: OwnedRoomId,
1864- pub users: Vec<OwnedUserId>,
1865-}
1866-
1867-#[derive(Clone, Debug, Eq, PartialEq)]
1868-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1869-#[non_exhaustive]
1870-pub enum HistoryState {
1871- Idle,
1872- Loading,
1873- StartReached,
1874- Failed(String),
1875-}
1876-
1877-#[derive(Clone, Debug, Eq, PartialEq)]
1878-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1879-#[non_exhaustive]
1880-pub struct EncryptionState {
1881- pub device_id: OwnedDeviceId,
1882- pub device_verified: bool,
1883- pub recovery: RecoveryState,
1884- pub key_backup: KeyBackupState,
1885-}
1886-
1887-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1888-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1889-#[non_exhaustive]
1890-pub enum KeyBackupState {
1891- Unknown,
1892- Creating,
1893- Enabling,
1894- Resuming,
1895- Downloading,
1896- Enabled,
1897- Disabling,
1898-}
1899-
1900-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1901-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1902-#[non_exhaustive]
1903-pub enum RecoveryState {
1904- Unknown,
1905- Disabled,
1906- Incomplete,
1907- Enabled,
1908-}
1909-
1910-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1911-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1912-#[non_exhaustive]
1913-pub enum VerificationAction {
1914- /// take up a request another device sent, which arrives as `VerificationState::Incoming`
1915- Accept,
1916- Confirm,
1917- Reject,
1918- Cancel,
1919-}
1920-
1921-#[derive(Clone, Debug, Eq, PartialEq)]
1922-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1923-#[non_exhaustive]
1924-pub enum VerificationTarget {
1925- OwnIdentity,
1926- User(OwnedUserId),
1927- Device {
1928- user_id: OwnedUserId,
1929- device_id: OwnedDeviceId,
1930- },
1931-}
1932-
1933-#[derive(Clone, Debug, Eq, PartialEq)]
1934-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1935-#[non_exhaustive]
1936-pub enum VerificationState {
1937- Waiting,
1938- /// someone else asked. `Accept` to go ahead, `Cancel` to turn them down
1939- Incoming {
1940- user_id: OwnedUserId,
1941- device_id: Option<OwnedDeviceId>,
1942- },
1943- Comparison {
1944- emojis: Vec<(String, String)>,
1945- decimals: (u16, u16, u16),
1946- },
1947- Confirmed,
1948- Done,
1949- Cancelled(String),
1950- Failed(VerificationFailure),
1951-}
1952-
1953-#[derive(Clone, Debug, Eq, PartialEq)]
1954-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1955-#[non_exhaustive]
1956-pub enum VerificationFailure {
1957- RequestAcceptance(String),
1958- SasStartup(String),
1959- SasAcceptance(String),
1960- RequestStreamEnded,
1961- SasStreamEnded,
1962-}
1963-
1964-#[derive(Clone, Debug, Eq, PartialEq)]
1965-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1966-#[non_exhaustive]
1967-pub struct EngineFailure {
1968- pub kind: EngineErrorKind,
1969- pub message: String,
1970-}
1971-
1972-#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1973-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1974-#[non_exhaustive]
1975-pub enum EngineErrorKind {
1976- EventReceiverClosed,
1977- Watcher,
1978- Other,
1979-}
Deleted file: vendor/macltok/src/normalise/image_pack.rs−152 deletions
@@ -1,152 +0,0 @@
1-use std::collections::{BTreeMap, BTreeSet};
2-
3-use matrix_sdk::{Client, Room, deserialized_responses::RawAnySyncOrStrippedState};
4-use ruma::{
5- OwnedRoomId,
6- events::{
7- GlobalAccountDataEventType, StateEventType,
8- image_pack::{PackImage as SourceImage, PackInfo, PackUsage as SourceUsage},
9- },
10- serde::{JsonObject, Raw},
11-};
12-
13-use crate::{Error, ImagePack, ImagePackSource, ImagePacks, MediaId, PackImage, PackUsage};
14-
15-pub(crate) const ROOM_PACKS: &str = "im.ponies.room_emotes";
16-pub(crate) const USER_PACK: &str = "im.ponies.user_emotes";
17-pub(crate) const ENABLED_ROOMS: &str = "im.ponies.emote_rooms";
18-
19-/// rooms enabled everywhere that the user hasn't joined are skipped, we hold no state for them
20-pub(crate) async fn image_packs(client: &Client) -> Result<ImagePacks, Error> {
21- let mut rooms = imbl::HashMap::new();
22- for room in client.joined_rooms() {
23- let packs = room_packs(&room).await?;
24- if !packs.is_empty() {
25- rooms.insert(room.room_id().to_owned(), packs);
26- }
27- }
28- let mut global = Vec::new();
29- if let Some(content) = account_data(client, USER_PACK).await? {
30- global.extend(pack("user".to_owned(), ImagePackSource::User, &content));
31- }
32- if let Some(content) = account_data(client, ENABLED_ROOMS).await? {
33- let enabled = content
34- .get_field::<BTreeMap<OwnedRoomId, BTreeMap<String, Raw<JsonObject>>>>("rooms")
35- .ok()
36- .flatten()
37- .unwrap_or_default();
38- for (room_id, state_keys) in enabled {
39- let packs = rooms.get(&room_id).into_iter().flatten();
40- global.extend(
41- packs
42- .filter(|pack| match &pack.source {
43- ImagePackSource::Room { state_key, .. } => {
44- state_keys.contains_key(state_key)
45- }
46- ImagePackSource::User => false,
47- })
48- .cloned(),
49- );
50- }
51- }
52- Ok(ImagePacks { global, rooms })
53-}
54-
55-async fn room_packs(room: &Room) -> Result<Vec<ImagePack>, Error> {
56- let events = room
57- .get_state_events(StateEventType::from(ROOM_PACKS))
58- .await
59- .map_err(|source| Error::from_sdk("reading image packs failed", source))?;
60- let by_state_key: BTreeMap<String, Raw<JsonObject>> = events
61- .into_iter()
62- .filter_map(|event| match event {
63- RawAnySyncOrStrippedState::Sync(event) => Some((
64- event.get_field::<String>("state_key").ok()??,
65- event.get_field::<Raw<JsonObject>>("content").ok()??,
66- )),
67- RawAnySyncOrStrippedState::Stripped(_) => None,
68- })
69- .collect();
70- Ok(by_state_key
71- .into_iter()
72- .filter_map(|(state_key, content)| {
73- let source = ImagePackSource::Room {
74- room_id: room.room_id().to_owned(),
75- state_key: state_key.clone(),
76- };
77- pack(
78- format!("room:{}:{state_key}", room.room_id()),
79- source,
80- &content,
81- )
82- })
83- .collect())
84-}
85-
86-async fn account_data(client: &Client, event_type: &str) -> Result<Option<Raw<JsonObject>>, Error> {
87- Ok(client
88- .account()
89- .account_data_raw(GlobalAccountDataEventType::from(event_type))
90- .await
91- .map_err(|source| Error::from_sdk("reading image packs failed", source))?
92- .map(|content| content.cast_unchecked()))
93-}
94-
95-/// one bad image costs that image, not its pack. a pack with nothing usable left is dropped
96-fn pack(id: String, source: ImagePackSource, content: &Raw<JsonObject>) -> Option<ImagePack> {
97- let info = content
98- .get_field::<PackInfo>("pack")
99- .ok()
100- .flatten()
101- .unwrap_or_default();
102- let pack_usage = usage(&info.usage);
103- let images: Vec<PackImage> = content
104- .get_field::<BTreeMap<String, Raw<SourceImage>>>("images")
105- .ok()
106- .flatten()
107- .unwrap_or_default()
108- .into_iter()
109- .filter_map(|(shortcode, image)| {
110- let image = image.deserialize().ok()?;
111- if !image.url.is_valid() {
112- return None;
113- }
114- let info = image.info.as_ref();
115- Some(PackImage {
116- shortcode,
117- media_id: MediaId::plain(&image.url),
118- reaction_key: image.url.to_string(),
119- body: image.body,
120- mimetype: info.and_then(|info| info.mimetype.clone()),
121- width: info.and_then(|info| info.width).map(u64::from),
122- height: info.and_then(|info| info.height).map(u64::from),
123- usage: usage(&image.usage)
124- .or_else(|| pack_usage.clone())
125- .unwrap_or_else(|| vec![PackUsage::Emoticon, PackUsage::Sticker]),
126- })
127- })
128- .collect();
129- (!images.is_empty()).then(|| ImagePack {
130- id,
131- display_name: info.display_name,
132- avatar: info
133- .avatar_url
134- .filter(|uri| uri.is_valid())
135- .map(|uri| MediaId::plain(&uri)),
136- attribution: info.attribution,
137- source,
138- images,
139- })
140-}
141-
142-fn usage(usage: &BTreeSet<SourceUsage>) -> Option<Vec<PackUsage>> {
143- let usage: Vec<PackUsage> = usage
144- .iter()
145- .filter_map(|usage| match usage {
146- SourceUsage::Emoticon => Some(PackUsage::Emoticon),
147- SourceUsage::Sticker => Some(PackUsage::Sticker),
148- _ => None,
149- })
150- .collect();
151- (!usage.is_empty()).then_some(usage)
152-}
Deleted file: vendor/macltok/src/normalise/link_preview.rs−217 deletions
@@ -1,217 +0,0 @@
1-use matrix_sdk::{Room, deserialized_responses::RawAnySyncOrStrippedState};
2-use ruma::{
3- MxcUri,
4- events::{
5- GlobalAccountDataEventType, RoomAccountDataEventType, StateEventType,
6- room::{
7- MediaSource,
8- message::{MessageType, PreviewImageSource, UrlPreview},
9- },
10- },
11- serde::{JsonObject, Raw},
12-};
13-
14-use super::media::{MediaRegistration, MediaSourceInfo, media_id};
15-use crate::{Error, LinkPreview, LinkPreviewImage, MediaId, MediaKind};
16-
17-/// room state an admin sets, and room account data a user sets, under the same name
18-pub(crate) const ROOM_PREVIEW_URLS: &str = "org.matrix.room.preview_urls";
19-const USER_PREVIEW_URLS: &str = "org.matrix.preview_urls";
20-
21-const SHORT_TEXT: usize = 256;
22-const LONG_TEXT: usize = 1024;
23-
24-/// the previews a text message's sender attached, each with the registration its image needs
25-/// when it's encrypted. embedded reply content has no media identity, so its encrypted images
26-/// go without
27-pub(crate) fn sender_previews(
28- identity: Option<&str>,
29- message: &MessageType,
30-) -> Vec<(LinkPreview, Option<MediaRegistration>)> {
31- let MessageType::Text(text) = message else {
32- return Vec::new();
33- };
34- text.url_previews
35- .iter()
36- .flatten()
37- .enumerate()
38- .filter_map(|(index, preview)| sender_preview(identity, &text.body, index, preview))
39- .collect()
40-}
41-
42-/// the matched url has to be in the body, or a sender could caption any link with any card
43-fn sender_preview(
44- identity: Option<&str>,
45- body: &str,
46- index: usize,
47- preview: &UrlPreview,
48-) -> Option<(LinkPreview, Option<MediaRegistration>)> {
49- let url = preview.matched_url.as_deref()?;
50- if url.is_empty() || !body.contains(url) || !preview.contains_preview() {
51- return None;
52- }
53- let (image, registration) = match &preview.image {
54- None => (None, None),
55- Some(image) => {
56- let (media_id, registration) = match &image.source {
57- PreviewImageSource::Url(uri) => (plain(uri)?, None),
58- PreviewImageSource::EncryptedImage(file) => {
59- let id = media_id(identity?, &format!("preview{index}"));
60- let registration = MediaRegistration {
61- id: id.clone(),
62- kind: MediaKind::Image,
63- full: MediaSourceInfo {
64- source: MediaSource::Encrypted(Box::new(file.clone())),
65- mimetype: image.mimetype.clone(),
66- declared_size: image.size.map(u64::from),
67- },
68- thumbnail: None,
69- };
70- (id, Some(registration))
71- }
72- };
73- let image = LinkPreviewImage {
74- media_id,
75- mimetype: image.mimetype.clone(),
76- size_bytes: image.size.map(u64::from),
77- width: image.width.map(u64::from),
78- height: image.height.map(u64::from),
79- };
80- (Some(image), registration)
81- }
82- };
83- let preview = LinkPreview {
84- url: url.to_owned(),
85- canonical_url: preview.url.clone().filter(|canonical| canonical != url),
86- title: clamp(preview.title.as_deref(), SHORT_TEXT),
87- description: clamp(preview.description.as_deref(), LONG_TEXT),
88- site_name: None,
89- image,
90- };
91- Some((preview, registration))
92-}
93-
94-/// what the homeserver's `preview_url` answered, a loose map of `og:` keys. `None` when
95-/// nothing in it is worth a card
96-pub(crate) fn server_preview(url: &str, data: &Raw<JsonObject>) -> Option<LinkPreview> {
97- let text = |key: &str| data.get_field::<String>(key).ok().flatten();
98- let image = text("og:image")
99- .as_deref()
100- .and_then(|uri| plain(<&MxcUri>::from(uri)))
101- .map(|media_id| LinkPreviewImage {
102- media_id,
103- mimetype: text("og:image:type"),
104- size_bytes: number(data, "matrix:image:size"),
105- width: number(data, "og:image:width"),
106- height: number(data, "og:image:height"),
107- });
108- let preview = LinkPreview {
109- url: url.to_owned(),
110- canonical_url: text("og:url").filter(|canonical| canonical != url),
111- title: clamp(text("og:title").as_deref(), SHORT_TEXT),
112- description: clamp(text("og:description").as_deref(), LONG_TEXT),
113- site_name: clamp(text("og:site_name").as_deref(), SHORT_TEXT),
114- image,
115- };
116- (preview.title.is_some() || preview.description.is_some() || preview.image.is_some())
117- .then_some(preview)
118-}
119-
120-/// element's order, so both clients agree. the user's choice for this room, then their own
121-/// default, then the room admins'. encrypted rooms are the host's call
122-pub(crate) async fn url_previews_allowed(room: &Room) -> Result<bool, Error> {
123- let room_choice = room
124- .account_data(RoomAccountDataEventType::from(ROOM_PREVIEW_URLS))
125- .await
126- .map_err(|source| Error::from_sdk("reading url preview settings failed", source))?
127- .and_then(|event| disabled(&event.get_field::<Raw<JsonObject>>("content").ok()??));
128- let own_default = || async {
129- room.client()
130- .account()
131- .account_data_raw(GlobalAccountDataEventType::from(USER_PREVIEW_URLS))
132- .await
133- .map(|content| content.and_then(|content| disabled(&content.cast_unchecked())))
134- .map_err(|source| Error::from_sdk("reading url preview settings failed", source))
135- };
136- let admins = || async {
137- room.get_state_event(StateEventType::from(ROOM_PREVIEW_URLS), "")
138- .await
139- .map(|event| match event {
140- Some(RawAnySyncOrStrippedState::Sync(event)) => {
141- disabled(&event.get_field::<Raw<JsonObject>>("content").ok()??)
142- }
143- _ => None,
144- })
145- .map_err(|source| Error::from_sdk("reading url preview settings failed", source))
146- };
147- let disabled = match room_choice {
148- Some(disabled) => disabled,
149- None => match own_default().await? {
150- Some(disabled) => disabled,
151- None => admins().await?.unwrap_or(false),
152- },
153- };
154- Ok(!disabled)
155-}
156-
157-fn disabled(content: &Raw<JsonObject>) -> Option<bool> {
158- content.get_field::<bool>("disable").ok().flatten()
159-}
160-
161-fn plain(uri: &MxcUri) -> Option<MediaId> {
162- uri.is_valid().then(|| MediaId::plain(uri))
163-}
164-
165-/// servers send dimensions as numbers or as the page's own strings
166-fn number(data: &Raw<JsonObject>, key: &str) -> Option<u64> {
167- data.get_field::<u64>(key).ok().flatten().or_else(|| {
168- data.get_field::<String>(key)
169- .ok()
170- .flatten()?
171- .trim()
172- .parse()
173- .ok()
174- })
175-}
176-
177-fn clamp(text: Option<&str>, chars: usize) -> Option<String> {
178- let text = text?.trim();
179- (!text.is_empty()).then(|| match text.char_indices().nth(chars) {
180- Some((end, _)) => text[..end].trim_end().to_owned(),
181- None => text.to_owned(),
182- })
183-}
184-
185-#[cfg(test)]
186-mod tests {
187- use ruma::{events::room::message::MessageType, serde::Raw};
188- use serde_json::json;
189-
190- use super::sender_previews;
191-
192- #[test]
193- fn a_sender_cannot_caption_a_link_the_message_does_not_contain() {
194- let message: MessageType = Raw::new(&json!({
195- "msgtype": "m.text",
196- "body": "look at https://example.org/real",
197- "com.beeper.linkpreviews": [
198- {
199- "matched_url": "https://example.org/real",
200- "og:title": "real",
201- "og:image": "mxc://example.org/image",
202- },
203- { "matched_url": "https://bank.example/login", "og:title": "your bank" },
204- ],
205- }))
206- .unwrap()
207- .cast_unchecked()
208- .deserialize()
209- .unwrap();
210- let previews = sender_previews(Some("1#item"), &message);
211- assert_eq!(previews.len(), 1);
212- let (preview, registration) = &previews[0];
213- assert_eq!(preview.url, "https://example.org/real");
214- assert_eq!(preview.title.as_deref(), Some("real"));
215- assert!(preview.image.is_some() && registration.is_none());
216- }
217-}
Deleted file: vendor/macltok/src/normalise/media.rs−339 deletions
@@ -1,339 +0,0 @@
1-use std::sync::atomic::{AtomicU64, Ordering};
2-
3-use matrix_sdk::{media::MediaEventContent, ruma::events::room::MediaSource};
4-use matrix_sdk_ui::timeline::{MsgLikeKind, TimelineItem as SdkTimelineItem, TimelineItemContent};
5-use ruma::events::{
6- room::{
7- ImageInfo, ThumbnailInfo,
8- message::{FileInfo, MessageType, VideoInfo},
9- },
10- sticker::StickerEventContent,
11-};
12-
13-use super::link_preview::sender_previews;
14-use crate::{MediaDescriptor, MediaId, MediaKind};
15-
16-static NEXT_MEDIA_SCOPE: AtomicU64 = AtomicU64::new(1);
17-
18-/// Per-timeline-session nonce namespacing recycled SDK unique IDs.
19-///
20-/// SDK 0.18 recycles [`SdkTimelineItem::unique_id`] across the local-to-remote
21-/// echo replacement, while transaction IDs disappear. Scoping the recycled ID
22-/// by a process-wide nonce keeps media IDs stable across replacement without
23-/// colliding across rooms, threads, or reopened sessions.
24-#[derive(Clone, Debug)]
25-pub(crate) struct MediaScope {
26- nonce: u64,
27-}
28-
29-impl MediaScope {
30- pub(crate) fn new() -> Self {
31- Self {
32- nonce: NEXT_MEDIA_SCOPE.fetch_add(1, Ordering::Relaxed),
33- }
34- }
35-
36- pub(crate) fn owns(&self, id: &MediaId) -> bool {
37- id.as_str()
38- .split_once('#')
39- .is_some_and(|(nonce, _)| nonce.parse() == Ok(self.nonce))
40- }
41-}
42-
43-#[derive(Clone)]
44-pub(crate) struct MediaRegistration {
45- pub(crate) id: MediaId,
46- pub(crate) kind: MediaKind,
47- pub(crate) full: MediaSourceInfo,
48- pub(crate) thumbnail: Option<MediaSourceInfo>,
49-}
50-
51-#[derive(Clone)]
52-pub(crate) struct MediaSourceInfo {
53- pub(crate) source: MediaSource,
54- pub(crate) mimetype: Option<String>,
55- pub(crate) declared_size: Option<u64>,
56-}
57-
58-pub(super) fn media_registrations_for_item(
59- item: &SdkTimelineItem,
60- scope: &MediaScope,
61-) -> Vec<MediaRegistration> {
62- let Some(event) = item.as_event() else {
63- return Vec::new();
64- };
65- let identity = media_identity(item, scope);
66- let TimelineItemContent::MsgLike(content) = event.content() else {
67- return Vec::new();
68- };
69- match &content.kind {
70- MsgLikeKind::Message(message) => media_registration(&identity, message.msgtype())
71- .into_iter()
72- .chain(
73- sender_previews(Some(&identity), message.msgtype())
74- .into_iter()
75- .filter_map(|(_, registration)| registration),
76- )
77- .collect(),
78- MsgLikeKind::Sticker(sticker) => media_sticker_registration(&identity, sticker.content())
79- .into_iter()
80- .collect(),
81- _ => Vec::new(),
82- }
83-}
84-
85-pub(super) fn media_identity(item: &SdkTimelineItem, scope: &MediaScope) -> String {
86- format!("{}#{}", scope.nonce, item.unique_id().0)
87-}
88-
89-pub(super) fn media_from_message(identity: &str, message: &MessageType) -> Option<MediaDescriptor> {
90- let descriptor = |kind, filename: &str, caption: Option<&str>| MediaDescriptor {
91- id: media_id(identity, "media"),
92- kind,
93- filename: filename.to_owned(),
94- caption: caption.map(str::to_owned),
95- mimetype: None,
96- size_bytes: None,
97- width: None,
98- height: None,
99- duration_ms: None,
100- blurhash: None,
101- has_thumbnail: false,
102- };
103- let millis = |duration: std::time::Duration| u64::try_from(duration.as_millis()).ok();
104- Some(match message {
105- MessageType::Image(content) => {
106- let info = content.info.as_deref();
107- MediaDescriptor {
108- mimetype: info.and_then(|info| info.mimetype.clone()),
109- size_bytes: info.and_then(|info| info.size).map(u64::from),
110- width: info.and_then(|info| info.width).map(u64::from),
111- height: info.and_then(|info| info.height).map(u64::from),
112- blurhash: info.and_then(|info| info.blurhash.clone()),
113- has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
114- ..descriptor(MediaKind::Image, content.filename(), content.caption())
115- }
116- }
117- MessageType::Video(content) => {
118- let info = content.info.as_deref();
119- MediaDescriptor {
120- mimetype: info.and_then(|info| info.mimetype.clone()),
121- size_bytes: info.and_then(|info| info.size).map(u64::from),
122- width: info.and_then(|info| info.width).map(u64::from),
123- height: info.and_then(|info| info.height).map(u64::from),
124- duration_ms: info.and_then(|info| info.duration).and_then(millis),
125- blurhash: info.and_then(|info| info.blurhash.clone()),
126- has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
127- ..descriptor(MediaKind::Video, content.filename(), content.caption())
128- }
129- }
130- MessageType::Audio(content) => {
131- let info = content.info.as_deref();
132- MediaDescriptor {
133- mimetype: info.and_then(|info| info.mimetype.clone()),
134- size_bytes: info.and_then(|info| info.size).map(u64::from),
135- duration_ms: info.and_then(|info| info.duration).and_then(millis),
136- ..descriptor(MediaKind::Audio, content.filename(), content.caption())
137- }
138- }
139- MessageType::File(content) => {
140- let info = content.info.as_deref();
141- MediaDescriptor {
142- mimetype: info.and_then(|info| info.mimetype.clone()),
143- size_bytes: info.and_then(|info| info.size).map(u64::from),
144- has_thumbnail: info.is_some_and(|info| info.thumbnail_source.is_some()),
145- ..descriptor(MediaKind::File, content.filename(), content.caption())
146- }
147- }
148- _ => return None,
149- })
150-}
151-
152-pub(super) fn media_descriptor_for_sticker(
153- identity: &str,
154- content: &StickerEventContent,
155-) -> MediaDescriptor {
156- MediaDescriptor {
157- id: media_id(identity, "sticker"),
158- kind: MediaKind::Sticker,
159- filename: content.body.clone(),
160- caption: None,
161- mimetype: content.info.mimetype.clone(),
162- size_bytes: content.info.size.map(u64::from),
163- width: content.info.width.map(u64::from),
164- height: content.info.height.map(u64::from),
165- duration_ms: None,
166- blurhash: content.info.blurhash.clone(),
167- has_thumbnail: content.info.thumbnail_source.is_some(),
168- }
169-}
170-
171-pub(super) fn media_id(identity: &str, suffix: &str) -> MediaId {
172- MediaId::new(format!("{identity}#{suffix}"))
173-}
174-
175-fn media_sticker_registration(
176- identity: &str,
177- content: &StickerEventContent,
178-) -> Option<MediaRegistration> {
179- Some(MediaRegistration {
180- id: media_id(identity, "sticker"),
181- kind: MediaKind::Sticker,
182- full: MediaSourceInfo {
183- source: content.source()?,
184- mimetype: content.info.mimetype.clone(),
185- declared_size: content.info.size.map(u64::from),
186- },
187- thumbnail: thumbnail_media_source(
188- content.info.thumbnail_source.clone(),
189- content.info.thumbnail_info.as_deref(),
190- ),
191- })
192-}
193-
194-fn media_registration(identity: &str, message: &MessageType) -> Option<MediaRegistration> {
195- match message {
196- MessageType::Image(content) => media_registration_with_info(
197- identity,
198- MediaKind::Image,
199- content.source(),
200- content.info.as_deref(),
201- ),
202- MessageType::Video(content) => media_registration_with_info(
203- identity,
204- MediaKind::Video,
205- content.source(),
206- content.info.as_deref(),
207- ),
208- MessageType::File(content) => media_registration_with_info(
209- identity,
210- MediaKind::File,
211- content.source(),
212- content.info.as_deref(),
213- ),
214- MessageType::Audio(content) => media_registration_with_parts(
215- identity,
216- MediaKind::Audio,
217- content.source(),
218- content.info.as_ref().and_then(|info| info.mimetype.clone()),
219- content
220- .info
221- .as_ref()
222- .and_then(|info| info.size)
223- .map(u64::from),
224- None,
225- ),
226- _ => None,
227- }
228-}
229-
230-fn media_registration_with_info<T: MediaInfo>(
231- identity: &str,
232- kind: MediaKind,
233- source: Option<MediaSource>,
234- info: Option<&T>,
235-) -> Option<MediaRegistration> {
236- media_registration_with_parts(
237- identity,
238- kind,
239- source,
240- info.and_then(MediaInfo::mimetype),
241- info.and_then(MediaInfo::declared_size),
242- info.and_then(MediaInfo::thumbnail),
243- )
244-}
245-
246-fn media_registration_with_parts(
247- identity: &str,
248- kind: MediaKind,
249- source: Option<MediaSource>,
250- mimetype: Option<String>,
251- declared_size: Option<u64>,
252- thumbnail: Option<MediaSourceInfo>,
253-) -> Option<MediaRegistration> {
254- Some(MediaRegistration {
255- id: media_id(identity, "media"),
256- kind,
257- full: MediaSourceInfo {
258- source: source?,
259- mimetype,
260- declared_size,
261- },
262- thumbnail,
263- })
264-}
265-
266-trait MediaInfo {
267- fn mimetype(&self) -> Option<String>;
268- fn declared_size(&self) -> Option<u64>;
269- fn thumbnail(&self) -> Option<MediaSourceInfo>;
270-}
271-
272-macro_rules! impl_media_info {
273- ($type:ty) => {
274- impl MediaInfo for $type {
275- fn mimetype(&self) -> Option<String> {
276- self.mimetype.clone()
277- }
278-
279- fn declared_size(&self) -> Option<u64> {
280- self.size.map(u64::from)
281- }
282-
283- fn thumbnail(&self) -> Option<MediaSourceInfo> {
284- thumbnail_media_source(
285- self.thumbnail_source.clone(),
286- self.thumbnail_info.as_deref(),
287- )
288- }
289- }
290- };
291-}
292-
293-impl_media_info!(ImageInfo);
294-impl_media_info!(VideoInfo);
295-impl_media_info!(FileInfo);
296-
297-fn thumbnail_media_source(
298- source: Option<MediaSource>,
299- info: Option<&ThumbnailInfo>,
300-) -> Option<MediaSourceInfo> {
301- source.map(|source| MediaSourceInfo {
302- source,
303- mimetype: info.and_then(|info| info.mimetype.clone()),
304- declared_size: info.and_then(|info| info.size).map(u64::from),
305- })
306-}
307-
308-#[cfg(test)]
309-mod tests {
310- use matrix_sdk::ruma::{
311- events::room::{ImageInfo, message::ImageMessageEventContent},
312- mxc_uri,
313- };
314-
315- use super::*;
316-
317- #[test]
318- fn image_messages_keep_media_metadata() {
319- let mut info = ImageInfo::new();
320- info.mimetype = Some("image/png".to_owned());
321- info.size = Some(65_536_u32.into());
322- info.width = Some(640_u32.into());
323- info.height = Some(480_u32.into());
324- let content = ImageMessageEventContent::plain(
325- "screen.png".to_owned(),
326- mxc_uri!("mxc://example.org/screen").to_owned(),
327- )
328- .info(Box::new(info));
329-
330- let media = media_from_message("$event:example.org", &MessageType::Image(content)).unwrap();
331- assert_eq!(media.id.as_str(), "$event:example.org#media");
332- assert_eq!(media.kind, MediaKind::Image);
333- assert_eq!(media.filename, "screen.png");
334- assert_eq!(media.caption, None);
335- assert_eq!(media.mimetype.as_deref(), Some("image/png"));
336- assert_eq!(media.size_bytes, Some(65_536));
337- assert_eq!((media.width, media.height), (Some(640), Some(480)));
338- }
339-}
Deleted file: vendor/macltok/src/normalise/member.rs−25 deletions
@@ -1,25 +0,0 @@
1-use matrix_sdk::room::RoomMember;
2-use ruma::events::room::member::MembershipState;
3-
4-use crate::{MediaId, MemberMembership, MemberSummary};
5-
6-pub(crate) fn member_summary(member: RoomMember) -> MemberSummary {
7- MemberSummary {
8- user_id: member.user_id().to_owned(),
9- name: member.name().to_owned(),
10- display_name: member.display_name().map(ToOwned::to_owned),
11- avatar: member.avatar_url().map(MediaId::plain),
12- membership: match member.membership() {
13- MembershipState::Join => MemberMembership::Joined,
14- MembershipState::Invite => MemberMembership::Invited,
15- MembershipState::Leave => MemberMembership::Left,
16- MembershipState::Knock => MemberMembership::Knocked,
17- MembershipState::Ban => MemberMembership::Banned,
18- value => MemberMembership::Custom(value.to_string()),
19- },
20- own: member.is_account_user(),
21- name_ambiguous: member.name_ambiguous(),
22- ignored: member.is_ignored(),
23- service: member.is_service_member(),
24- }
25-}
Deleted file: vendor/macltok/src/normalise/mod.rs−14 deletions
@@ -1,14 +0,0 @@
1-mod image_pack;
2-mod link_preview;
3-mod media;
4-mod member;
5-mod room;
6-mod timeline;
7-
8-pub(crate) use image_pack::{ENABLED_ROOMS, ROOM_PACKS, USER_PACK, image_packs};
9-pub(crate) use link_preview::{ROOM_PREVIEW_URLS, server_preview, url_previews_allowed};
10-pub(crate) use media::{MediaRegistration, MediaScope, MediaSourceInfo};
11-pub(crate) use member::member_summary;
12-pub(crate) use room::notification_mode;
13-pub(crate) use room::{room_summaries, space_child};
14-pub(crate) use timeline::{message_kind, normalise_timeline, normalise_timeline_diffs};
Deleted file: vendor/macltok/src/normalise/room.rs−225 deletions
@@ -1,225 +0,0 @@
1-use futures_util::{StreamExt, stream};
2-use matrix_sdk::{
3- Client, Room, RoomState,
4- deserialized_responses::SyncOrStrippedState,
5- latest_events::LatestEventValue,
6- ruma::events::{
7- AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncStateEvent,
8- room::message::{Relation, RoomMessageEventContent},
9- space::child::SpaceChildEventContent,
10- },
11-};
12-use matrix_sdk_ui::spaces::{SpaceRoom, SpaceService};
13-use ruma::OwnedRoomId;
14-
15-use super::message_kind;
16-use crate::{
17- LatestEvent, MediaId, MessageKind, RoomHero, RoomMembership, RoomNotificationMode, RoomSummary,
18- SendState, SpaceChild,
19-};
20-
21-pub(crate) async fn room_summaries(client: &Client, spaces: &SpaceService) -> Vec<RoomSummary> {
22- let mut summaries = stream::iter(client.rooms())
23- .map(|room| async move { room_summary(&room, spaces).await })
24- .buffer_unordered(16)
25- .collect::<Vec<_>>()
26- .await;
27- summaries.sort_by(|left, right| {
28- right
29- .latest_event_timestamp_ms
30- .cmp(&left.latest_event_timestamp_ms)
31- .then_with(|| left.name.to_lowercase().cmp(&right.name.to_lowercase()))
32- .then_with(|| left.room_id.cmp(&right.room_id))
33- });
34- summaries
35-}
36-
37-async fn room_summary(room: &Room, spaces: &SpaceService) -> RoomSummary {
38- let name = room
39- .display_name()
40- .await
41- .map(|name| name.to_string())
42- .unwrap_or_else(|_| room.room_id().to_string());
43- let custom_notification_mode = room.user_defined_notification_mode().await;
44- let effective_notification_mode = match custom_notification_mode {
45- Some(mode) => Some(mode),
46- None => room.notification_mode().await,
47- };
48- let inviter = match room.state() {
49- RoomState::Invited => room
50- .invite_details()
51- .await
52- .ok()
53- .map(|invite| invite.inviter_id),
54- _ => None,
55- };
56- let children = if room.is_space() {
57- space_children(room).await
58- } else {
59- Vec::new()
60- };
61- RoomSummary {
62- room_id: room.room_id().to_owned(),
63- name,
64- membership: membership(room.state()),
65- unread_messages: room.num_unread_messages(),
66- unread_mentions: room.num_unread_mentions(),
67- unread_notifications: room.num_unread_notifications(),
68- encrypted: room.encryption_state().is_encrypted(),
69- space: room.is_space(),
70- direct: room.is_direct().await.unwrap_or(false),
71- favourite: room.is_favourite(),
72- low_priority: room.is_low_priority(),
73- avatar: room.avatar_url().as_deref().map(MediaId::plain),
74- latest_event_timestamp_ms: room
75- .latest_event_timestamp()
76- .map(|value| value.get().into()),
77- latest_event: latest_event(room),
78- notification_mode: effective_notification_mode.map(notification_mode),
79- notification_mode_custom: custom_notification_mode.is_some(),
80- topic: room.topic(),
81- canonical_alias: room.canonical_alias(),
82- joined_members: room.joined_members_count(),
83- invited_members: room.invited_members_count(),
84- heroes: room
85- .heroes()
86- .into_iter()
87- .map(|hero| RoomHero {
88- user_id: hero.user_id,
89- display_name: hero.display_name,
90- avatar: hero.avatar_url.as_deref().map(MediaId::plain),
91- })
92- .collect(),
93- successor: room.successor_room().map(|successor| successor.room_id),
94- inviter,
95- parent_spaces: spaces
96- .joined_parents_of_child(room.room_id())
97- .await
98- .into_iter()
99- .map(|parent| parent.room_id)
100- .collect(),
101- children,
102- }
103-}
104-
105-const fn membership(state: RoomState) -> RoomMembership {
106- match state {
107- RoomState::Joined => RoomMembership::Joined,
108- RoomState::Invited => RoomMembership::Invited,
109- RoomState::Left => RoomMembership::Left,
110- RoomState::Knocked => RoomMembership::Knocked,
111- RoomState::Banned => RoomMembership::Banned,
112- }
113-}
114-
115-/// what the space itself lists, straight from local state. redacted children don't count
116-async fn space_children(room: &Room) -> Vec<OwnedRoomId> {
117- let Ok(events) = room
118- .get_state_events_static::<SpaceChildEventContent>()
119- .await
120- else {
121- return Vec::new();
122- };
123- events
124- .into_iter()
125- .filter_map(|event| match event.deserialize().ok()? {
126- SyncOrStrippedState::Sync(SyncStateEvent::Original(event)) => Some(event.state_key),
127- SyncOrStrippedState::Stripped(event) => Some(event.state_key),
128- SyncOrStrippedState::Sync(SyncStateEvent::Redacted(_)) => None,
129- })
130- .collect()
131-}
132-
133-fn latest_event(room: &Room) -> Option<LatestEvent> {
134- let own = || Some(room.own_user_id().to_owned());
135- let local = |local: &matrix_sdk::latest_events::LocalLatestEventValue| {
136- let preview = match local.content.deserialize() {
137- Ok(AnyMessageLikeEventContent::RoomMessage(content)) => preview(&content),
138- _ => (None, None),
139- };
140- (preview, Some(local.timestamp))
141- };
142- let (sender, (kind, body), timestamp, send_state) = match room.latest_event() {
143- LatestEventValue::None => return None,
144- LatestEventValue::Remote(event) => {
145- let preview = match event.raw().deserialize() {
146- Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
147- message,
148- ))) => message
149- .as_original()
150- .map_or((None, None), |message| preview(&message.content)),
151- _ => (None, None),
152- };
153- (event.sender(), preview, event.timestamp(), SendState::Sent)
154- }
155- LatestEventValue::RemoteInvite {
156- timestamp, inviter, ..
157- } => (inviter, (None, None), Some(timestamp), SendState::Sent),
158- LatestEventValue::LocalIsSending(value) => {
159- let (preview, timestamp) = local(&value);
160- (own(), preview, timestamp, SendState::Sending)
161- }
162- LatestEventValue::LocalHasBeenSent { value, .. } => {
163- let (preview, timestamp) = local(&value);
164- (own(), preview, timestamp, SendState::Sent)
165- }
166- // the sdk won't say whether it'll retry, so assume the user has to
167- LatestEventValue::LocalCannotBeSent(value) => {
168- let (preview, timestamp) = local(&value);
169- (
170- own(),
171- preview,
172- timestamp,
173- SendState::Failed { will_retry: false },
174- )
175- }
176- };
177- Some(LatestEvent {
178- sender,
179- kind,
180- body,
181- timestamp_ms: timestamp.map(|timestamp| timestamp.get().into()),
182- send_state,
183- })
184-}
185-
186-/// the latest event of an edited message is the edit, whose own body is the `* fallback`
187-fn preview(content: &RoomMessageEventContent) -> (Option<MessageKind>, Option<String>) {
188- let msgtype = match &content.relates_to {
189- Some(Relation::Replacement(replacement)) => &replacement.new_content.msgtype,
190- _ => &content.msgtype,
191- };
192- (Some(message_kind(msgtype)), Some(msgtype.body().to_owned()))
193-}
194-
195-pub(crate) fn space_child(room: SpaceRoom) -> SpaceChild {
196- SpaceChild {
197- name: room.display_name,
198- room_id: room.room_id,
199- topic: room.topic,
200- avatar: room.avatar_url.as_deref().map(MediaId::plain),
201- canonical_alias: room.canonical_alias,
202- joined_members: room.num_joined_members,
203- space: room.room_type == Some(ruma::room::RoomType::Space),
204- children_count: room.children_count,
205- membership: room.state.map(membership),
206- via: room.via,
207- suggested: room.suggested,
208- world_readable: room.world_readable,
209- guest_can_join: room.guest_can_join,
210- }
211-}
212-
213-pub(crate) const fn notification_mode(
214- mode: matrix_sdk::notification_settings::RoomNotificationMode,
215-) -> RoomNotificationMode {
216- match mode {
217- matrix_sdk::notification_settings::RoomNotificationMode::AllMessages => {
218- RoomNotificationMode::AllMessages
219- }
220- matrix_sdk::notification_settings::RoomNotificationMode::MentionsAndKeywordsOnly => {
221- RoomNotificationMode::MentionsAndKeywordsOnly
222- }
223- matrix_sdk::notification_settings::RoomNotificationMode::Mute => RoomNotificationMode::Mute,
224- }
225-}
Deleted file: vendor/macltok/src/normalise/timeline.rs−451 deletions
@@ -1,451 +0,0 @@
1-use std::sync::Arc;
2-
3-use matrix_sdk_crypto::types::events::UtdCause as SdkUtdCause;
4-use matrix_sdk_ui::{
5- eyeball_im::VectorDiff,
6- timeline::{
7- AnyOtherStateEventContentChange, EncryptedMessage, EventSendState, EventTimelineItem,
8- MembershipChange as SdkMembershipChange, MsgLikeContent, MsgLikeKind, Profile,
9- TimelineDetails, TimelineEventItemId, TimelineItem as SdkTimelineItem, TimelineItemContent,
10- TimelineItemKind, VirtualTimelineItem,
11- },
12-};
13-use ruma::{
14- MxcUri, OwnedEventId, UserId,
15- events::{
16- StateEventContentChange,
17- room::message::{MessageFormat, MessageType},
18- },
19-};
20-
21-use crate::{
22- Change, MediaId, MembershipChange, Message, MessageKind, MessageMentions, Reaction,
23- ReplyDetails, ReplyReference, SendState, Sender, ThreadSummary, TimelineContent, TimelineDiff,
24- TimelineEvent, TimelineItem, TimelineItemId, UtdCause,
25-};
26-
27-use super::link_preview::sender_previews;
28-use super::media::{
29- MediaRegistration, MediaScope, media_descriptor_for_sticker, media_from_message,
30- media_identity, media_registrations_for_item,
31-};
32-
33-#[derive(Default)]
34-pub(crate) struct NormalisedTimeline {
35- pub(crate) items: imbl::Vector<TimelineItem>,
36- pub(crate) media: Vec<MediaRegistration>,
37- /// replies whose target isn't loaded, so somebody should go and ask for it
38- pub(crate) missing_replies: Vec<OwnedEventId>,
39-}
40-
41-pub(crate) struct NormalisedTimelineDiffs {
42- pub(crate) diffs: Vec<TimelineDiff>,
43- pub(crate) media: Vec<MediaRegistration>,
44- pub(crate) missing_replies: Vec<OwnedEventId>,
45-}
46-
47-struct Normaliser<'a> {
48- current_user_id: Option<&'a UserId>,
49- scope: &'a MediaScope,
50- media: Vec<MediaRegistration>,
51- missing_replies: Vec<OwnedEventId>,
52-}
53-
54-impl Normaliser<'_> {
55- fn item(&mut self, item: &SdkTimelineItem) -> TimelineItem {
56- self.media
57- .extend(media_registrations_for_item(item, self.scope));
58- match item.kind() {
59- TimelineItemKind::Event(event) => {
60- TimelineItem::Event(Box::new(self.event(item, event)))
61- }
62- TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(timestamp)) => {
63- TimelineItem::DayDivider {
64- timestamp_ms: timestamp.get().into(),
65- }
66- }
67- TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => TimelineItem::ReadMarker,
68- TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {
69- TimelineItem::TimelineStart
70- }
71- }
72- }
73-
74- fn event(&mut self, item: &SdkTimelineItem, event: &EventTimelineItem) -> TimelineEvent {
75- let msg_like = match event.content() {
76- TimelineItemContent::MsgLike(content) => Some(content),
77- _ => None,
78- };
79- let reply_to = msg_like
80- .and_then(|content| content.in_reply_to.as_ref())
81- .map(|reply| {
82- let details = match &reply.event {
83- TimelineDetails::Ready(embedded) => ReplyDetails::Ready {
84- sender: sender(&embedded.sender, &embedded.sender_profile),
85- content: Box::new(content(&embedded.content, None)),
86- timestamp_ms: embedded.timestamp.get().into(),
87- },
88- TimelineDetails::Unavailable => {
89- self.missing_replies.push(reply.event_id.clone());
90- ReplyDetails::Pending
91- }
92- TimelineDetails::Pending => ReplyDetails::Pending,
93- TimelineDetails::Error(_) => ReplyDetails::Unavailable,
94- };
95- ReplyReference {
96- event_id: reply.event_id.clone(),
97- details,
98- }
99- });
100- let mut read_by: Vec<_> = event
101- .read_receipts()
102- .keys()
103- .filter(|user_id| *user_id != event.sender())
104- .cloned()
105- .collect();
106- read_by.sort();
107- TimelineEvent {
108- item_id: TimelineItemId::new(item.unique_id().0.clone()),
109- event_id: event.event_id().map(ToOwned::to_owned),
110- transaction_id: event.transaction_id().map(ToOwned::to_owned),
111- sender: sender(event.sender(), event.sender_profile()),
112- timestamp_ms: event.timestamp().get().into(),
113- own: event.is_own(),
114- send_state: send_state(event.send_state()),
115- content: content(event.content(), Some(&media_identity(item, self.scope))),
116- reply_to,
117- thread_root: msg_like.and_then(|content| content.thread_root.clone()),
118- thread_summary: msg_like.and_then(thread_summary),
119- reactions: reactions(event, self.current_user_id),
120- read_by,
121- editable: event.is_editable(),
122- }
123- }
124-}
125-
126-pub(crate) fn normalise_timeline<'a>(
127- items: impl Iterator<Item = &'a Arc<SdkTimelineItem>>,
128- current_user_id: Option<&UserId>,
129- scope: &MediaScope,
130-) -> NormalisedTimeline {
131- let mut normaliser = Normaliser {
132- current_user_id,
133- scope,
134- media: Vec::new(),
135- missing_replies: Vec::new(),
136- };
137- let items = items.map(|item| normaliser.item(item)).collect();
138- NormalisedTimeline {
139- items,
140- media: normaliser.media,
141- missing_replies: normaliser.missing_replies,
142- }
143-}
144-
145-pub(crate) fn normalise_timeline_diffs(
146- diffs: Vec<VectorDiff<Arc<SdkTimelineItem>>>,
147- current_user_id: Option<&UserId>,
148- scope: &MediaScope,
149-) -> NormalisedTimelineDiffs {
150- let mut normaliser = Normaliser {
151- current_user_id,
152- scope,
153- media: Vec::new(),
154- missing_replies: Vec::new(),
155- };
156- let diffs = diffs
157- .into_iter()
158- .map(|diff| match diff.map(|item| normaliser.item(&item)) {
159- VectorDiff::Append { values } => TimelineDiff::Append {
160- values: values.into_iter().collect(),
161- },
162- VectorDiff::Clear => TimelineDiff::Clear,
163- VectorDiff::PushFront { value } => TimelineDiff::PushFront { value },
164- VectorDiff::PushBack { value } => TimelineDiff::PushBack { value },
165- VectorDiff::PopFront => TimelineDiff::PopFront,
166- VectorDiff::PopBack => TimelineDiff::PopBack,
167- VectorDiff::Insert { index, value } => TimelineDiff::Insert { index, value },
168- VectorDiff::Set { index, value } => TimelineDiff::Set { index, value },
169- VectorDiff::Remove { index } => TimelineDiff::Remove { index },
170- VectorDiff::Truncate { length } => TimelineDiff::Truncate { length },
171- VectorDiff::Reset { values } => TimelineDiff::Reset {
172- values: values.into_iter().collect(),
173- },
174- })
175- .collect();
176- NormalisedTimelineDiffs {
177- diffs,
178- media: normaliser.media,
179- missing_replies: normaliser.missing_replies,
180- }
181-}
182-
183-/// `media_identity` is `None` for embedded reply content, which never gets fetchable media of its own
184-fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> TimelineContent {
185- match content {
186- TimelineItemContent::MsgLike(content) => msg_like(content, media_identity),
187- TimelineItemContent::MembershipChange(change) => TimelineContent::Membership {
188- user_id: change.user_id().to_owned(),
189- display_name: change.display_name(),
190- change: change
191- .change()
192- .map_or(MembershipChange::Other, membership_change),
193- },
194- TimelineItemContent::ProfileChange(change) => TimelineContent::Profile {
195- user_id: change.user_id().to_owned(),
196- display_name: change.displayname_change().map(|change| Change {
197- old: change.old.clone(),
198- new: change.new.clone(),
199- }),
200- avatar: change.avatar_url_change().map(|change| Change {
201- old: change.old.as_deref().map(MediaId::plain),
202- new: change.new.as_deref().map(MediaId::plain),
203- }),
204- },
205- TimelineItemContent::OtherState(state) => match state.content() {
206- AnyOtherStateEventContentChange::RoomName(change) => TimelineContent::RoomName(
207- original(change).and_then(|content| non_empty(&content.name)),
208- ),
209- AnyOtherStateEventContentChange::RoomTopic(change) => TimelineContent::RoomTopic(
210- original(change).and_then(|content| non_empty(&content.topic)),
211- ),
212- AnyOtherStateEventContentChange::RoomAvatar(change) => TimelineContent::RoomAvatar(
213- original(change).and_then(|content| content.url.as_deref().map(MediaId::plain)),
214- ),
215- AnyOtherStateEventContentChange::RoomEncryption(_) => {
216- TimelineContent::EncryptionEnabled
217- }
218- AnyOtherStateEventContentChange::RoomCreate(_) => TimelineContent::RoomCreated,
219- AnyOtherStateEventContentChange::RoomTombstone(change) => match original(change) {
220- Some(content) => TimelineContent::RoomUpgraded {
221- successor: content.replacement_room.clone(),
222- },
223- None => other_state(state.content().event_type().to_string(), state.state_key()),
224- },
225- other => other_state(other.event_type().to_string(), state.state_key()),
226- },
227- TimelineItemContent::FailedToParseMessageLike { event_type, .. } => {
228- TimelineContent::Other {
229- event_type: event_type.to_string(),
230- }
231- }
232- TimelineItemContent::FailedToParseState {
233- event_type,
234- state_key,
235- ..
236- } => other_state(event_type.to_string(), state_key),
237- TimelineItemContent::CallInvite => TimelineContent::Other {
238- event_type: "m.call.invite".to_owned(),
239- },
240- TimelineItemContent::RtcNotification { .. } => TimelineContent::Other {
241- event_type: "org.matrix.msc4075.rtc.notification".to_owned(),
242- },
243- }
244-}
245-
246-/// a cleared name or topic arrives as an empty string
247-fn non_empty(value: &str) -> Option<String> {
248- (!value.is_empty()).then(|| value.to_owned())
249-}
250-
251-fn other_state(event_type: String, state_key: &str) -> TimelineContent {
252- TimelineContent::OtherState {
253- event_type,
254- state_key: state_key.to_owned(),
255- }
256-}
257-
258-fn original<C>(change: &StateEventContentChange<C>) -> Option<&C>
259-where
260- C: ruma::events::StaticStateEventContent + ruma::events::RedactContent,
261-{
262- match change {
263- StateEventContentChange::Original { content, .. } => Some(content),
264- StateEventContentChange::Redacted(_) => None,
265- }
266-}
267-
268-fn msg_like(content: &MsgLikeContent, media_identity: Option<&str>) -> TimelineContent {
269- match &content.kind {
270- MsgLikeKind::Message(message) => TimelineContent::Message(Message {
271- kind: message_kind(message.msgtype()),
272- body: message.body().to_owned(),
273- geo_uri: match message.msgtype() {
274- MessageType::Location(location) => Some(location.geo_uri.clone()),
275- _ => None,
276- },
277- formatted_body: formatted_body(message.msgtype()),
278- mentions: message.mentions().map(|mentions| MessageMentions {
279- user_ids: mentions.user_ids.iter().cloned().collect(),
280- room: mentions.room,
281- }),
282- edited: message.is_edited(),
283- media: media_identity
284- .and_then(|identity| media_from_message(identity, message.msgtype())),
285- link_previews: sender_previews(media_identity, message.msgtype())
286- .into_iter()
287- .map(|(preview, _)| preview)
288- .collect(),
289- }),
290- MsgLikeKind::Sticker(sticker) => {
291- let sticker = sticker.content();
292- match media_identity {
293- Some(identity) => TimelineContent::Sticker {
294- body: sticker.body.clone(),
295- media: media_descriptor_for_sticker(identity, sticker),
296- },
297- None => TimelineContent::Other {
298- event_type: "m.sticker".to_owned(),
299- },
300- }
301- }
302- MsgLikeKind::Poll(poll) => TimelineContent::Poll {
303- question: poll.results().question,
304- },
305- MsgLikeKind::Redacted => TimelineContent::Redacted,
306- MsgLikeKind::UnableToDecrypt(encrypted) => {
307- TimelineContent::UnableToDecrypt(match encrypted {
308- EncryptedMessage::MegolmV1AesSha2 { cause, .. } => utd_cause(*cause),
309- _ => UtdCause::Unknown,
310- })
311- }
312- MsgLikeKind::Other(other) => TimelineContent::Other {
313- event_type: other.event_type().to_string(),
314- },
315- MsgLikeKind::LiveLocation(_) => TimelineContent::Other {
316- event_type: "m.beacon_info".to_owned(),
317- },
318- }
319-}
320-
321-pub(crate) fn message_kind(message: &MessageType) -> MessageKind {
322- match message {
323- MessageType::Text(_) => MessageKind::Text,
324- MessageType::Emote(_) => MessageKind::Emote,
325- MessageType::Notice(_) => MessageKind::Notice,
326- MessageType::ServerNotice(_) => MessageKind::ServerNotice,
327- MessageType::Image(_) => MessageKind::Image,
328- MessageType::Video(_) => MessageKind::Video,
329- MessageType::Audio(_) => MessageKind::Audio,
330- MessageType::File(_) => MessageKind::File,
331- MessageType::Location(_) => MessageKind::Location,
332- MessageType::VerificationRequest(_) => MessageKind::VerificationRequest,
333- other => MessageKind::Other(other.msgtype().to_owned()),
334- }
335-}
336-
337-const fn membership_change(change: SdkMembershipChange) -> MembershipChange {
338- match change {
339- SdkMembershipChange::Joined => MembershipChange::Joined,
340- SdkMembershipChange::Left => MembershipChange::Left,
341- SdkMembershipChange::Banned => MembershipChange::Banned,
342- SdkMembershipChange::Unbanned => MembershipChange::Unbanned,
343- SdkMembershipChange::Kicked => MembershipChange::Kicked,
344- SdkMembershipChange::KickedAndBanned => MembershipChange::KickedAndBanned,
345- SdkMembershipChange::Invited => MembershipChange::Invited,
346- SdkMembershipChange::InvitationAccepted => MembershipChange::InvitationAccepted,
347- SdkMembershipChange::InvitationRejected => MembershipChange::InvitationRejected,
348- SdkMembershipChange::InvitationRevoked => MembershipChange::InvitationRevoked,
349- SdkMembershipChange::Knocked => MembershipChange::Knocked,
350- SdkMembershipChange::KnockAccepted => MembershipChange::KnockAccepted,
351- SdkMembershipChange::KnockRetracted => MembershipChange::KnockRetracted,
352- SdkMembershipChange::KnockDenied => MembershipChange::KnockDenied,
353- SdkMembershipChange::None
354- | SdkMembershipChange::Error
355- | SdkMembershipChange::NotImplemented => MembershipChange::Other,
356- }
357-}
358-
359-const fn utd_cause(cause: SdkUtdCause) -> UtdCause {
360- match cause {
361- SdkUtdCause::Unknown => UtdCause::Unknown,
362- SdkUtdCause::SentBeforeWeJoined => UtdCause::SentBeforeWeJoined,
363- SdkUtdCause::VerificationViolation => UtdCause::VerificationViolation,
364- SdkUtdCause::UnsignedDevice => UtdCause::UnsignedDevice,
365- SdkUtdCause::UnknownDevice => UtdCause::UnknownDevice,
366- SdkUtdCause::HistoricalMessageAndBackupIsDisabled => {
367- UtdCause::HistoricalMessageAndBackupIsDisabled
368- }
369- SdkUtdCause::WithheldForUnverifiedOrInsecureDevice => {
370- UtdCause::WithheldForUnverifiedOrInsecureDevice
371- }
372- SdkUtdCause::WithheldBySender => UtdCause::WithheldBySender,
373- SdkUtdCause::HistoricalMessageAndDeviceIsUnverified => {
374- UtdCause::HistoricalMessageAndDeviceIsUnverified
375- }
376- }
377-}
378-
379-fn sender(user_id: &UserId, profile: &TimelineDetails<Profile>) -> Sender {
380- let profile = match profile {
381- TimelineDetails::Ready(profile) => Some(profile),
382- _ => None,
383- };
384- Sender {
385- user_id: user_id.to_owned(),
386- display_name: profile.and_then(|profile| profile.display_name.clone()),
387- avatar: profile.and_then(|profile| profile.avatar_url.as_deref().map(MediaId::plain)),
388- name_ambiguous: profile.is_some_and(|profile| profile.display_name_ambiguous),
389- }
390-}
391-
392-fn send_state(state: Option<&EventSendState>) -> SendState {
393- match state {
394- None | Some(EventSendState::Sent { .. }) => SendState::Sent,
395- Some(EventSendState::NotSentYet { .. }) => SendState::Sending,
396- // recoverable means the sdk parks the queue and we unpark it, nothing for the user to do
397- Some(EventSendState::SendingFailed { is_recoverable, .. }) => SendState::Failed {
398- will_retry: *is_recoverable,
399- },
400- }
401-}
402-
403-fn thread_summary(content: &MsgLikeContent) -> Option<ThreadSummary> {
404- content.thread_summary.as_ref().map(|summary| {
405- let latest_event = match &summary.latest_event {
406- TimelineDetails::Ready(event) => Some(event.as_ref()),
407- _ => None,
408- };
409- ThreadSummary {
410- reply_count: summary.num_replies,
411- latest_event_id: latest_event.and_then(|event| match &event.identifier {
412- TimelineEventItemId::EventId(event_id) => Some(event_id.clone()),
413- TimelineEventItemId::TransactionId(_) => None,
414- }),
415- latest_event_timestamp_ms: latest_event.map(|event| event.timestamp.get().into()),
416- }
417- })
418-}
419-
420-fn reactions(event: &EventTimelineItem, current_user_id: Option<&UserId>) -> Vec<Reaction> {
421- event
422- .content()
423- .reactions()
424- .into_iter()
425- .flat_map(|reactions| reactions.iter())
426- .map(|(key, senders)| Reaction {
427- key: key.clone(),
428- shortcode: senders.values().find_map(|info| info.shortcode.clone()),
429- image: <&MxcUri>::from(key.as_str())
430- .is_valid()
431- .then(|| MediaId::plain(key)),
432- count: senders.len(),
433- own: current_user_id.is_some_and(|user_id| senders.contains_key(user_id)),
434- senders: senders.keys().cloned().collect(),
435- })
436- .collect()
437-}
438-
439-fn formatted_body(message: &MessageType) -> Option<String> {
440- let formatted = match message {
441- MessageType::Text(content) => content.formatted.as_ref(),
442- MessageType::Emote(content) => content.formatted.as_ref(),
443- MessageType::Notice(content) => content.formatted.as_ref(),
444- MessageType::Audio(content) => content.formatted_caption(),
445- MessageType::File(content) => content.formatted_caption(),
446- MessageType::Image(content) => content.formatted_caption(),
447- MessageType::Video(content) => content.formatted_caption(),
448- _ => None,
449- }?;
450- (formatted.format == MessageFormat::Html).then(|| formatted.body.clone())
451-}
Deleted file: vendor/macltok/src/session_store.rs−566 deletions
@@ -1,566 +0,0 @@
1-#[cfg(feature = "sqlite")]
2-use std::path::{Path, PathBuf};
3-
4-use matrix_sdk::{BoxFuture, SendOutsideWasm, SyncOutsideWasm};
5-use matrix_sdk_store_encryption::StoreCipher;
6-
7-use crate::{AccountError, SessionRecord, StoreConfig, account::account_error};
8-
9-#[cfg(feature = "sqlite")]
10-type RecordLocation = PathBuf;
11-#[cfg(feature = "indexeddb")]
12-type RecordLocation = String;
13-
14-#[cfg(not(target_family = "wasm"))]
15-type Lock<T> = std::sync::Mutex<T>;
16-#[cfg(target_family = "wasm")]
17-type Lock<T> = tokio::sync::Mutex<T>;
18-
19-/// one login's record and the matrix store it belongs to. the engine rotates refresh tokens
20-/// and each works once, so a `save` has to land before the next refresh or the login is gone
21-pub trait SessionStore: SendOutsideWasm + SyncOutsideWasm {
22- fn store(&self) -> &StoreConfig;
23-
24- fn load(&self) -> BoxFuture<'_, Result<Option<SessionRecord>, AccountError>>;
25-
26- /// replace the saved record, all or nothing
27- fn save<'a>(&'a self, record: &'a SessionRecord) -> BoxFuture<'a, Result<(), AccountError>>;
28-
29- /// `save`, but without awaiting. the sdk calls it from inside a refresh, before the new
30- /// refresh token is ever used, so a store that can write synchronously closes the gap
31- /// a crash would otherwise lose the login through. `None` leaves the refresh to `save`
32- #[cfg(not(target_family = "wasm"))]
33- fn save_blocking(&self, _record: &SessionRecord) -> Option<Result<(), AccountError>> {
34- None
35- }
36-
37- /// swap the record for one that only says who to log in again as, keeping the store.
38- /// `Session::restore` then answers `Restore::SoftLoggedOut`, even after a restart
39- fn soft_log_out(&self) -> BoxFuture<'_, Result<(), AccountError>>;
40-
41- /// drop the record and delete the store, once nothing has the store open
42- fn discard(&self) -> BoxFuture<'_, Result<(), AccountError>>;
43-}
44-
45-/// keeps the record sealed with the store's passphrase, in a file natively and in an
46-/// IndexedDB database of its own in a browser
47-pub struct EncryptedSessionStore {
48- record: RecordLocation,
49- store: StoreConfig,
50- sealer: Lock<Option<Sealer>>,
51-}
52-
53-struct Sealer {
54- cipher: StoreCipher,
55- exported: Vec<u8>,
56-}
57-
58-impl EncryptedSessionStore {
59- #[cfg(feature = "sqlite")]
60- #[must_use]
61- pub fn file(record: impl Into<PathBuf>, store: StoreConfig) -> Self {
62- Self::new(record.into(), store)
63- }
64-
65- /// the record lives beside the store, in `<name>::macltok-session`
66- #[cfg(feature = "indexeddb")]
67- #[must_use]
68- pub fn indexeddb(store: StoreConfig) -> Self {
69- let record = format!("{}::macltok-session", store.indexeddb_name());
70- Self::new(record, store)
71- }
72-
73- fn new(record: RecordLocation, store: StoreConfig) -> Self {
74- Self {
75- record,
76- store,
77- sealer: Lock::new(None),
78- }
79- }
80-
81- fn open(
82- &self,
83- sealer: &mut Option<Sealer>,
84- sealed: Option<Vec<u8>>,
85- ) -> Result<Option<SessionRecord>, AccountError> {
86- let Some(sealed) = sealed else {
87- return Ok(None);
88- };
89- let (opened, record) = unseal(self.store.passphrase(), &sealed)?;
90- *sealer = Some(opened);
91- Ok(Some(record))
92- }
93-
94- fn seal(
95- &self,
96- sealer: &mut Option<Sealer>,
97- record: &SessionRecord,
98- ) -> Result<Vec<u8>, AccountError> {
99- let sealer = match sealer {
100- Some(sealer) => sealer,
101- empty => empty.insert(Sealer::new(self.store.passphrase())?),
102- };
103- sealer.seal(record)
104- }
105-}
106-
107-#[cfg(not(target_family = "wasm"))]
108-impl EncryptedSessionStore {
109- fn lock(&self) -> std::sync::MutexGuard<'_, Option<Sealer>> {
110- self.sealer
111- .lock()
112- .unwrap_or_else(std::sync::PoisonError::into_inner)
113- }
114-
115- fn read(&self, sealer: &mut Option<Sealer>) -> Result<Option<SessionRecord>, AccountError> {
116- let sealed = backend::read(&self.record)
117- .map_err(|error| account_error("reading the session record", error))?;
118- self.open(sealer, sealed)
119- }
120-
121- fn write(
122- &self,
123- sealer: &mut Option<Sealer>,
124- record: &SessionRecord,
125- ) -> Result<(), AccountError> {
126- let sealed = self.seal(sealer, record)?;
127- backend::write(&self.record, &sealed)
128- .map_err(|error| account_error("writing the session record", error))
129- }
130-
131- fn load_record(&self) -> Result<Option<SessionRecord>, AccountError> {
132- self.read(&mut self.lock())
133- }
134-
135- fn save_record(&self, record: &SessionRecord) -> Result<(), AccountError> {
136- self.write(&mut self.lock(), record)
137- }
138-
139- fn soft_log_out_record(&self) -> Result<(), AccountError> {
140- let mut sealer = self.lock();
141- match self.read(&mut sealer)? {
142- Some(record) => self.write(&mut sealer, &record.soft_logged_out()),
143- None => Ok(()),
144- }
145- }
146-
147- fn forget_record(&self) -> Result<(), AccountError> {
148- let mut sealer = self.lock();
149- backend::remove(&self.record)
150- .map_err(|error| account_error("removing the session record", error))?;
151- *sealer = None;
152- Ok(())
153- }
154-}
155-
156-#[cfg(not(target_family = "wasm"))]
157-impl SessionStore for EncryptedSessionStore {
158- fn store(&self) -> &StoreConfig {
159- &self.store
160- }
161-
162- fn load(&self) -> BoxFuture<'_, Result<Option<SessionRecord>, AccountError>> {
163- Box::pin(async move { self.load_record() })
164- }
165-
166- fn save<'a>(&'a self, record: &'a SessionRecord) -> BoxFuture<'a, Result<(), AccountError>> {
167- Box::pin(async move { self.save_record(record) })
168- }
169-
170- /// blocks the calling worker for the write and its fsyncs, about a millisecond
171- fn save_blocking(&self, record: &SessionRecord) -> Option<Result<(), AccountError>> {
172- Some(self.save_record(record))
173- }
174-
175- fn soft_log_out(&self) -> BoxFuture<'_, Result<(), AccountError>> {
176- Box::pin(async move { self.soft_log_out_record() })
177- }
178-
179- fn discard(&self) -> BoxFuture<'_, Result<(), AccountError>> {
180- Box::pin(async move {
181- self.forget_record()?;
182- self.store.delete().await
183- })
184- }
185-}
186-
187-#[cfg(target_family = "wasm")]
188-impl EncryptedSessionStore {
189- async fn read(
190- &self,
191- sealer: &mut Option<Sealer>,
192- ) -> Result<Option<SessionRecord>, AccountError> {
193- let sealed = backend::read(&self.record)
194- .await
195- .map_err(|error| account_error("reading the session record", error))?;
196- self.open(sealer, sealed)
197- }
198-
199- async fn write(
200- &self,
201- sealer: &mut Option<Sealer>,
202- record: &SessionRecord,
203- ) -> Result<(), AccountError> {
204- let sealed = self.seal(sealer, record)?;
205- backend::write(&self.record, &sealed)
206- .await
207- .map_err(|error| account_error("writing the session record", error))
208- }
209-
210- async fn load_record(&self) -> Result<Option<SessionRecord>, AccountError> {
211- self.read(&mut *self.sealer.lock().await).await
212- }
213-
214- async fn save_record(&self, record: &SessionRecord) -> Result<(), AccountError> {
215- self.write(&mut *self.sealer.lock().await, record).await
216- }
217-
218- async fn soft_log_out_record(&self) -> Result<(), AccountError> {
219- let mut sealer = self.sealer.lock().await;
220- match self.read(&mut sealer).await? {
221- Some(record) => self.write(&mut sealer, &record.soft_logged_out()).await,
222- None => Ok(()),
223- }
224- }
225-
226- async fn discard_all(&self) -> Result<(), AccountError> {
227- let mut sealer = self.sealer.lock().await;
228- backend::remove(&self.record)
229- .await
230- .map_err(|error| error.into_account_error("removing the session record"))?;
231- *sealer = None;
232- self.store.delete().await
233- }
234-}
235-
236-#[cfg(target_family = "wasm")]
237-impl SessionStore for EncryptedSessionStore {
238- fn store(&self) -> &StoreConfig {
239- &self.store
240- }
241-
242- fn load(&self) -> BoxFuture<'_, Result<Option<SessionRecord>, AccountError>> {
243- Box::pin(self.load_record())
244- }
245-
246- fn save<'a>(&'a self, record: &'a SessionRecord) -> BoxFuture<'a, Result<(), AccountError>> {
247- Box::pin(self.save_record(record))
248- }
249-
250- fn soft_log_out(&self) -> BoxFuture<'_, Result<(), AccountError>> {
251- Box::pin(self.soft_log_out_record())
252- }
253-
254- fn discard(&self) -> BoxFuture<'_, Result<(), AccountError>> {
255- Box::pin(self.discard_all())
256- }
257-}
258-
259-impl Sealer {
260- fn new(passphrase: &str) -> Result<Self, AccountError> {
261- let cipher = StoreCipher::new()
262- .map_err(|error| account_error("creating the session record cipher", error))?;
263- let exported = cipher
264- .export(passphrase)
265- .map_err(|error| account_error("sealing the session record cipher", error))?;
266- Ok(Self { cipher, exported })
267- }
268-
269- fn seal(&self, record: &SessionRecord) -> Result<Vec<u8>, AccountError> {
270- let value = self
271- .cipher
272- .encrypt_value(record)
273- .map_err(|error| account_error("sealing the session record", error))?;
274- let length = u32::try_from(self.exported.len())
275- .expect("an exported store cipher is a few hundred bytes");
276- let mut sealed = Vec::with_capacity(4 + self.exported.len() + value.len());
277- sealed.extend_from_slice(&length.to_be_bytes());
278- sealed.extend_from_slice(&self.exported);
279- sealed.extend_from_slice(&value);
280- Ok(sealed)
281- }
282-}
283-
284-fn unseal(passphrase: &str, sealed: &[u8]) -> Result<(Sealer, SessionRecord), AccountError> {
285- let truncated = || {
286- account_error(
287- "opening the session record",
288- std::io::Error::from(std::io::ErrorKind::UnexpectedEof),
289- )
290- };
291- let (length, rest) = sealed.split_first_chunk::<4>().ok_or_else(truncated)?;
292- let (exported, value) = rest
293- .split_at_checked(u32::from_be_bytes(*length) as usize)
294- .ok_or_else(truncated)?;
295- let cipher = StoreCipher::import(passphrase, exported)
296- .map_err(|error| account_error("opening the session record cipher", error))?;
297- let record = cipher
298- .decrypt_value(value)
299- .map_err(|error| account_error("opening the session record", error))?;
300- let sealer = Sealer {
301- cipher,
302- exported: exported.to_vec(),
303- };
304- Ok((sealer, record))
305-}
306-
307-#[cfg(feature = "sqlite")]
308-mod backend {
309- use std::{
310- ffi::OsString,
311- fs::{self, File, OpenOptions},
312- io::{self, ErrorKind, Write},
313- path::{Path, PathBuf},
314- sync::atomic::{AtomicU64, Ordering},
315- };
316-
317- static WRITES: AtomicU64 = AtomicU64::new(0);
318-
319- pub(super) fn read(path: &Path) -> io::Result<Option<Vec<u8>>> {
320- match fs::read(path) {
321- Ok(sealed) => Ok(Some(sealed)),
322- Err(error) if error.kind() == ErrorKind::NotFound => Ok(None),
323- Err(error) => Err(error),
324- }
325- }
326-
327- pub(super) fn write(path: &Path, sealed: &[u8]) -> io::Result<()> {
328- let parent = match path.parent() {
329- Some(parent) if !parent.as_os_str().is_empty() => parent,
330- _ => Path::new("."),
331- };
332- fs::create_dir_all(parent)?;
333- let (temporary, mut file) = temporary_file(path)?;
334- let written = replace(path, &temporary, &mut file, parent, sealed);
335- if written.is_err() {
336- let _ = fs::remove_file(&temporary);
337- }
338- written
339- }
340-
341- fn temporary_file(path: &Path) -> io::Result<(PathBuf, File)> {
342- loop {
343- let mut name = OsString::from(path.as_os_str());
344- let write = WRITES.fetch_add(1, Ordering::Relaxed);
345- name.push(format!(".{}.{write}.tmp", std::process::id()));
346- let temporary = PathBuf::from(name);
347- match private_file(&temporary) {
348- Ok(file) => return Ok((temporary, file)),
349- Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
350- Err(error) => return Err(error),
351- }
352- }
353- }
354-
355- fn replace(
356- path: &Path,
357- temporary: &Path,
358- file: &mut File,
359- parent: &Path,
360- sealed: &[u8],
361- ) -> io::Result<()> {
362- file.write_all(sealed)?;
363- file.sync_all()?;
364- fs::rename(temporary, path)?;
365- sync_directory(parent)
366- }
367-
368- pub(super) fn remove(path: &Path) -> io::Result<()> {
369- match fs::remove_file(path) {
370- Err(error) if error.kind() != ErrorKind::NotFound => Err(error),
371- _ => Ok(()),
372- }
373- }
374-
375- /// `create_new` is `O_EXCL`, which posix says never follows a symlink, even a dangling one
376- #[cfg(unix)]
377- fn private_file(path: &Path) -> io::Result<File> {
378- use std::os::unix::fs::OpenOptionsExt;
379- OpenOptions::new()
380- .write(true)
381- .create_new(true)
382- .mode(0o600)
383- .open(path)
384- }
385-
386- #[cfg(not(unix))]
387- fn private_file(path: &Path) -> io::Result<File> {
388- OpenOptions::new().write(true).create_new(true).open(path)
389- }
390-
391- /// a rename isn't durable until its directory is synced
392- #[cfg(unix)]
393- fn sync_directory(path: &Path) -> io::Result<()> {
394- File::open(path)?.sync_all()
395- }
396-
397- #[cfg(not(unix))]
398- fn sync_directory(_: &Path) -> io::Result<()> {
399- Ok(())
400- }
401-}
402-
403-#[cfg(feature = "indexeddb")]
404-mod backend {
405- use indexed_db_futures::{
406- Build,
407- database::Database,
408- error::{Error, OpenDbError},
409- prelude::*,
410- transaction::{Transaction, TransactionMode},
411- typed_array::{Uint8Array, Uint8ArraySlice},
412- };
413-
414- const RECORDS: &str = "records";
415- const KEY: &str = "record";
416-
417- async fn open(name: &str) -> Result<Database, OpenDbError> {
418- Database::open(name)
419- .with_version(1u32)
420- .with_on_upgrade_needed(|_, transaction: &Transaction<'_>| -> Result<(), Error> {
421- transaction.db().create_object_store(RECORDS).build()?;
422- Ok(())
423- })
424- .build()?
425- .await
426- }
427-
428- /// the crate's `Database` never closes itself on drop, and an open one blocks deleting it
429- pub(super) async fn read(name: &str) -> Result<Option<Vec<u8>>, OpenDbError> {
430- let database = open(name).await?;
431- let sealed = get(&database).await;
432- database.close();
433- Ok(sealed?.map(Into::into))
434- }
435-
436- pub(super) async fn write(name: &str, sealed: &[u8]) -> Result<(), OpenDbError> {
437- let database = open(name).await?;
438- let written = put(&database, sealed).await;
439- database.close();
440- Ok(written?)
441- }
442-
443- pub(super) async fn remove(name: &str) -> Result<(), super::DeleteError> {
444- super::delete_database(name).await
445- }
446-
447- async fn get(database: &Database) -> Result<Option<Uint8Array>, Error> {
448- database
449- .transaction(RECORDS)
450- .with_mode(TransactionMode::Readonly)
451- .build()?
452- .object_store(RECORDS)?
453- .get(KEY)
454- .await
455- }
456-
457- async fn put(database: &Database, sealed: &[u8]) -> Result<(), Error> {
458- let transaction = database
459- .transaction(RECORDS)
460- .with_mode(TransactionMode::Readwrite)
461- .build()?;
462- transaction
463- .object_store(RECORDS)?
464- .put(Uint8ArraySlice::new(sealed))
465- .with_key(KEY)
466- .build()?;
467- transaction.commit().await
468- }
469-}
470-
471-#[cfg(feature = "sqlite")]
472-pub(crate) fn delete_sqlite_store(path: &Path) -> std::io::Result<()> {
473- use std::{fs, io::ErrorKind};
474-
475- // matrix-sdk-sqlite 0.18's database names, which it keeps private
476- const DATABASES: [&str; 4] = [
477- "matrix-sdk-state.sqlite3",
478- "matrix-sdk-crypto.sqlite3",
479- "matrix-sdk-event-cache.sqlite3",
480- "matrix-sdk-media.sqlite3",
481- ];
482- for database in DATABASES {
483- for suffix in ["", "-wal", "-shm", "-journal"] {
484- match fs::remove_file(path.join(format!("{database}{suffix}"))) {
485- Err(error) if error.kind() != ErrorKind::NotFound => return Err(error),
486- _ => {}
487- }
488- }
489- }
490- match fs::remove_file(path.join(crate::store_lock::LOCK_FILE)) {
491- Err(error) if error.kind() != ErrorKind::NotFound => return Err(error),
492- _ => {}
493- }
494- match fs::remove_dir(path) {
495- Err(error) if error.kind() != ErrorKind::NotFound => Err(error),
496- _ => Ok(()),
497- }
498-}
499-
500-#[cfg(feature = "indexeddb")]
501-pub(crate) async fn delete_indexeddb_store(name: &str) -> Result<(), DeleteError> {
502- // the databases matrix-sdk-indexeddb 0.18 opens under one store name
503- for suffix in [
504- "",
505- "::matrix-sdk-state",
506- "::matrix-sdk-crypto",
507- "::matrix-sdk-crypto-meta",
508- "::event_cache",
509- "::media",
510- ] {
511- delete_database(&format!("{name}{suffix}")).await?;
512- }
513- Ok(())
514-}
515-
516-#[cfg(feature = "indexeddb")]
517-#[derive(Debug, thiserror::Error)]
518-pub(crate) enum DeleteError {
519- #[error("the database is still open somewhere, it goes once that closes")]
520- Blocked,
521- #[error("this global scope has no IndexedDB")]
522- Unavailable,
523- #[error("deleting the database failed, {0}")]
524- Failed(String),
525-}
526-
527-#[cfg(feature = "indexeddb")]
528-impl DeleteError {
529- pub(crate) fn into_account_error(self, context: &'static str) -> AccountError {
530- match self {
531- Self::Blocked => AccountError::StoreDeletePending,
532- error => account_error(context, error),
533- }
534- }
535-}
536-
537-/// matrix_indexed_db_futures 0.7 ignores `blocked` on a delete, which then waits forever
538-#[cfg(feature = "indexeddb")]
539-async fn delete_database(name: &str) -> Result<(), DeleteError> {
540- use wasm_bindgen::{JsCast, JsValue};
541-
542- const BLOCKED: &str = "blocked";
543- let global = js_sys::global();
544- let factory = if let Some(window) = global.dyn_ref::<web_sys::Window>() {
545- window.indexed_db()
546- } else if let Some(worker) = global.dyn_ref::<web_sys::WorkerGlobalScope>() {
547- worker.indexed_db()
548- } else {
549- return Err(DeleteError::Unavailable);
550- };
551- let factory = factory.ok().flatten().ok_or(DeleteError::Unavailable)?;
552- let request = factory
553- .delete_database(name)
554- .map_err(|error| DeleteError::Failed(format!("{error:?}")))?;
555- let settled = js_sys::Promise::new(&mut |resolve, reject| {
556- request.set_onsuccess(Some(&resolve));
557- request.set_onerror(Some(&reject));
558- let blocked = reject.bind1(&JsValue::NULL, &JsValue::from_str(BLOCKED));
559- request.set_onblocked(Some(blocked.unchecked_ref()));
560- });
561- match wasm_bindgen_futures::JsFuture::from(settled).await {
562- Ok(_) => Ok(()),
563- Err(reason) if reason.as_string().as_deref() == Some(BLOCKED) => Err(DeleteError::Blocked),
564- Err(_) => Err(DeleteError::Failed(format!("{:?}", request.error()))),
565- }
566-}
Deleted file: vendor/macltok/src/store_lock.rs−117 deletions
@@ -1,117 +0,0 @@
1-use crate::{AccountError, account::account_error};
2-
3-/// the platform's own exclusive lock on a store. matrix-sdk 0.18's cross process lease drops
4-/// itself silently after one failed renewal or a stall past 500ms, so it can't be what keeps a
5-/// second engine out
6-pub(crate) struct StoreLock {
7- #[cfg(feature = "sqlite")]
8- _file: std::fs::File,
9- #[cfg(feature = "indexeddb")]
10- release: js_sys::Function,
11-}
12-
13-#[cfg(feature = "sqlite")]
14-pub(crate) const LOCK_FILE: &str = "macltok.lock";
15-
16-#[cfg(feature = "sqlite")]
17-impl StoreLock {
18- pub(crate) fn file(directory: &std::path::Path) -> Result<Self, AccountError> {
19- use std::fs::{self, File, TryLockError};
20-
21- fs::create_dir_all(directory)
22- .map_err(|error| account_error("creating the Matrix store directory", error))?;
23- let file = File::options()
24- .create(true)
25- .truncate(false)
26- .write(true)
27- .open(directory.join(LOCK_FILE))
28- .map_err(|error| account_error("opening the Matrix store's lock file", error))?;
29- match file.try_lock() {
30- Ok(()) => Ok(Self { _file: file }),
31- Err(TryLockError::WouldBlock) => Err(AccountError::StoreInUse),
32- Err(TryLockError::Error(error)) => {
33- Err(account_error("locking the Matrix store", error))
34- }
35- }
36- }
37-}
38-
39-#[cfg(feature = "indexeddb")]
40-#[derive(Debug, thiserror::Error)]
41-enum WebLockError {
42- #[error("this global scope has no Web Locks, they need a secure context")]
43- Unavailable,
44- #[error("requesting the Web Lock failed, {0}")]
45- Failed(String),
46-}
47-
48-#[cfg(feature = "indexeddb")]
49-impl StoreLock {
50- /// `ifAvailable` answers at once. the lock is ours until the promise we hand back settles
51- pub(crate) async fn web(name: &str) -> Result<Self, AccountError> {
52- use futures_util::future::{Either, select};
53- use js_sys::{Function, Object, Promise, Reflect};
54- use tokio::sync::oneshot;
55- use wasm_bindgen::{JsCast, JsValue, closure::Closure};
56-
57- let failed = |error: JsValue| {
58- account_error(
59- "locking the Matrix store",
60- WebLockError::Failed(format!("{error:?}")),
61- )
62- };
63- let locks = Reflect::get(&js_sys::global(), &JsValue::from_str("navigator"))
64- .and_then(|navigator| Reflect::get(&navigator, &JsValue::from_str("locks")))
65- .map_err(failed)?;
66- if locks.is_undefined() {
67- return Err(account_error(
68- "locking the Matrix store",
69- WebLockError::Unavailable,
70- ));
71- }
72- let request: Function = Reflect::get(&locks, &JsValue::from_str("request"))
73- .map_err(failed)?
74- .dyn_into()
75- .map_err(failed)?;
76- let options = Object::new();
77- Reflect::set(&options, &JsValue::from_str("ifAvailable"), &JsValue::TRUE)
78- .map_err(failed)?;
79-
80- let (granted, grant) = oneshot::channel();
81- let callback = Closure::once(move |lock: JsValue| -> JsValue {
82- if lock.is_null() {
83- let _ = granted.send(None);
84- return JsValue::UNDEFINED;
85- }
86- let mut release = None;
87- let held = Promise::new(&mut |resolve, _| release = Some(resolve));
88- let _ = granted.send(release);
89- held.into()
90- });
91- let outcome: Promise = request
92- .call3(
93- &locks,
94- &JsValue::from_str(&format!("macltok:{name}")),
95- &options,
96- callback.as_ref(),
97- )
98- .map_err(failed)?
99- .dyn_into()
100- .map_err(failed)?;
101- match select(grant, wasm_bindgen_futures::JsFuture::from(outcome)).await {
102- Either::Left((Ok(Some(release)), _)) => Ok(Self { release }),
103- Either::Left((Ok(None), _)) => Err(AccountError::StoreInUse),
104- Either::Left((Err(_), _)) | Either::Right((Ok(_), _)) => {
105- Err(failed(JsValue::from_str("the lock callback never ran")))
106- }
107- Either::Right((Err(error), _)) => Err(failed(error)),
108- }
109- }
110-}
111-
112-#[cfg(feature = "indexeddb")]
113-impl Drop for StoreLock {
114- fn drop(&mut self) {
115- let _ = self.release.call0(&wasm_bindgen::JsValue::UNDEFINED);
116- }
117-}
Deleted file: vendor/macltok/src/transport.rs−1125 deletions
@@ -1,1125 +0,0 @@
1-use std::{
2- fmt,
3- future::Future,
4- pin::Pin,
5- sync::Arc,
6- task::{Context, Poll},
7-};
8-
9-use matrix_sdk::{Client, RoomMemberships};
10-use matrix_sdk_ui::{
11- notification_client::{
12- NotificationClient, NotificationProcessSetup, NotificationStatus, RawNotificationEvent,
13- },
14- spaces::{SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState},
15- sync_service::SyncService,
16-};
17-use ruma::{
18- EventId, OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedRoomOrAliasId,
19- OwnedServerName, OwnedTransactionId, OwnedUserId, RoomId, UserId,
20- events::{
21- AnyGlobalAccountDataEventContent, AnyRoomAccountDataEvent, AnyRoomAccountDataEventContent,
22- GlobalAccountDataEventType, RoomAccountDataEventType,
23- },
24- serde::Raw,
25-};
26-use tokio::sync::{OnceCell, mpsc, oneshot, watch};
27-
28-use crate::{
29- AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,
30- EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventRef, ImagePacks,
31- InteractiveAuth, InteractiveAuthOutcome, LifecycleCommand, LinkPreview, Media, MediaCommand,
32- MediaId, MediaLocation, MediaSize, MemberFilter, MemberSummary, MessageContent,
33- MessageEditContent, NotificationCommand, OwnDevice, PowerLevelChanges, Profile,
34- PushNotification, PushNotificationEvent, PushNotificationStatus, RoomCommand, RoomListFilter,
35- RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomSummary, SpaceChild,
36- TimelineCommand, TimelineId, VerificationAction, VerificationTarget,
37- encryption::encryption_state,
38- engine::{MediaService, StoreLock},
39- model::{PushNotificationRoom, PushNotificationSender},
40- normalise::{image_packs, member_summary, room_summaries, space_child, url_previews_allowed},
41-};
42-
43-#[derive(Clone)]
44-pub struct ClientHandle {
45- requests: mpsc::Sender<Request>,
46- client: Client,
47- connection: watch::Receiver<ConnectionState>,
48- notification_client: Arc<OnceCell<NotificationClient>>,
49- sync_service: Arc<SyncService>,
50- media: Arc<MediaService>,
51- spaces: Arc<OnceCell<SpaceService>>,
52- _store_lock: Option<Arc<StoreLock>>,
53-}
54-
55-impl fmt::Debug for ClientHandle {
56- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57- formatter
58- .debug_struct("ClientHandle")
59- .finish_non_exhaustive()
60- }
61-}
62-
63-/// a command's answer. drop it if you don't care, the command still runs
64-#[derive(Debug)]
65-#[must_use = "dropping it is fine but then you'll never know"]
66-pub struct Pending(oneshot::Receiver<Result<CommandOutput, Error>>);
67-
68-impl Future for Pending {
69- type Output = Result<CommandOutput, Error>;
70-
71- fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
72- Pin::new(&mut self.0)
73- .poll(context)
74- .map(|result| result.unwrap_or_else(|_| Err(Error::interrupted())))
75- }
76-}
77-
78-impl ClientHandle {
79- /// current credentials, it holds secrets. the engine saves it for you unless the
80- /// session was built `without_persistence`
81- #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
82- pub fn session_record(&self) -> Result<crate::SessionRecord, crate::AccountError> {
83- crate::account::session_record(&self.client)
84- }
85-
86- /// the bearer token. it rotates on every refresh so ask again after a 401 rather than
87- /// keeping it, and keep it in memory and away from all but the homeserver's media endpoints
88- pub fn access_token(&self) -> Option<String> {
89- self.client.access_token()
90- }
91-
92- /// queue a command, waiting for room in the queue if there isn't any
93- pub async fn send(&self, command: Command) -> Result<Pending, Error> {
94- let (request, pending) = Request::new(command);
95- self.requests
96- .send(request)
97- .await
98- .map_err(|_| Error::interrupted())?;
99- Ok(pending)
100- }
101-
102- /// queue a command or fail with `QueueFull` right now
103- pub fn try_send(&self, command: Command) -> Result<Pending, Error> {
104- let (request, pending) = Request::new(command);
105- self.requests
106- .try_send(request)
107- .map_err(|error| match error {
108- mpsc::error::TrySendError::Full(_) => {
109- Error::new(ErrorKind::QueueFull, "the command queue is full")
110- }
111- mpsc::error::TrySendError::Closed(_) => Error::interrupted(),
112- })?;
113- Ok(pending)
114- }
115-
116- /// queue a command and wait for its answer. timeline sends answer once the
117- /// sdk send queue has them, delivery shows up later as the item's send state
118- pub async fn execute(&self, command: Command) -> Result<CommandOutput, Error> {
119- self.send(command).await?.await
120- }
121-
122- async fn run(&self, command: Command) -> Result<(), Error> {
123- self.execute(command).await.map(drop)
124- }
125-
126- async fn run_for_room(&self, command: RoomCommand) -> Result<OwnedRoomId, Error> {
127- match self.execute(Command::Room(command)).await? {
128- CommandOutput::Room(room_id) => Ok(room_id),
129- output => Err(Error::new(
130- ErrorKind::Other,
131- format!("expected a room but got {output:?}"),
132- )),
133- }
134- }
135-
136- pub async fn shutdown(&self) -> Result<(), Error> {
137- self.run(Command::Lifecycle(LifecycleCommand::Shutdown))
138- .await
139- }
140-
141- pub async fn logout(&self) -> Result<(), Error> {
142- self.run(Command::Account(AccountCommand::Logout)).await
143- }
144-
145- pub async fn delete_devices(
146- &self,
147- device_ids: Vec<OwnedDeviceId>,
148- auth: Option<InteractiveAuth>,
149- ) -> Result<InteractiveAuthOutcome, Error> {
150- match self
151- .run(Command::Account(AccountCommand::DeleteDevices {
152- device_ids,
153- auth,
154- }))
155- .await
156- {
157- Ok(()) => Ok(InteractiveAuthOutcome::Completed),
158- Err(Error {
159- challenge: Some(challenge),
160- ..
161- }) => Ok(InteractiveAuthOutcome::Challenge(*challenge)),
162- Err(error) => Err(error),
163- }
164- }
165-
166- pub async fn rename_device(
167- &self,
168- device_id: OwnedDeviceId,
169- display_name: impl Into<String>,
170- ) -> Result<(), Error> {
171- self.run(Command::Account(AccountCommand::RenameDevice {
172- device_id,
173- display_name: display_name.into(),
174- }))
175- .await
176- }
177-
178- pub async fn set_global_account_data(
179- &self,
180- event_type: GlobalAccountDataEventType,
181- content: Raw<AnyGlobalAccountDataEventContent>,
182- ) -> Result<(), Error> {
183- self.run(Command::Account(AccountCommand::SetAccountData {
184- event_type,
185- content,
186- }))
187- .await
188- }
189-
190- pub async fn set_room_account_data(
191- &self,
192- room_id: OwnedRoomId,
193- event_type: RoomAccountDataEventType,
194- content: Raw<AnyRoomAccountDataEventContent>,
195- ) -> Result<(), Error> {
196- self.run(Command::Room(RoomCommand::SetAccountData {
197- room_id,
198- event_type,
199- content,
200- }))
201- .await
202- }
203-
204- pub async fn set_room_notification_mode(
205- &self,
206- room_id: OwnedRoomId,
207- mode: RoomNotificationMode,
208- ) -> Result<(), Error> {
209- self.run(Command::Notification(NotificationCommand::SetRoomMode {
210- room_id,
211- mode,
212- }))
213- .await
214- }
215-
216- pub async fn reset_room_notification_mode(&self, room_id: OwnedRoomId) -> Result<(), Error> {
217- self.run(Command::Notification(NotificationCommand::ResetRoomMode(
218- room_id,
219- )))
220- .await
221- }
222-
223- pub async fn set_default_notification_mode(
224- &self,
225- encrypted: bool,
226- one_to_one: bool,
227- mode: RoomNotificationMode,
228- ) -> Result<(), Error> {
229- self.run(Command::Notification(NotificationCommand::SetDefaultMode {
230- encrypted,
231- one_to_one,
232- mode,
233- }))
234- .await
235- }
236-
237- pub async fn add_notification_keyword(&self, keyword: impl Into<String>) -> Result<(), Error> {
238- self.run(Command::Notification(NotificationCommand::AddKeyword(
239- keyword.into(),
240- )))
241- .await
242- }
243-
244- pub async fn remove_notification_keyword(
245- &self,
246- keyword: impl Into<String>,
247- ) -> Result<(), Error> {
248- self.run(Command::Notification(NotificationCommand::RemoveKeyword(
249- keyword.into(),
250- )))
251- .await
252- }
253-
254- pub async fn open_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
255- self.run(Command::Room(RoomCommand::Open(room_id))).await
256- }
257-
258- pub async fn close_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
259- self.run(Command::Room(RoomCommand::Close(room_id))).await
260- }
261-
262- pub async fn open_thread(
263- &self,
264- room_id: OwnedRoomId,
265- root_event_id: OwnedEventId,
266- ) -> Result<(), Error> {
267- self.run(Command::Timeline(TimelineCommand::OpenThread {
268- room_id,
269- root_event_id,
270- }))
271- .await
272- }
273-
274- pub async fn close_thread(
275- &self,
276- room_id: OwnedRoomId,
277- root_event_id: OwnedEventId,
278- ) -> Result<(), Error> {
279- self.run(Command::Timeline(TimelineCommand::CloseThread {
280- room_id,
281- root_event_id,
282- }))
283- .await
284- }
285-
286- pub async fn resync(&self, timeline_id: TimelineId) -> Result<(), Error> {
287- self.run(Command::Timeline(TimelineCommand::Resync { timeline_id }))
288- .await
289- }
290-
291- pub async fn paginate(&self, timeline_id: TimelineId, limit: u16) -> Result<(), Error> {
292- self.run(Command::Timeline(TimelineCommand::Paginate {
293- timeline_id,
294- limit,
295- }))
296- .await
297- }
298-
299- pub async fn send_message(
300- &self,
301- timeline_id: TimelineId,
302- content: MessageContent,
303- ) -> Result<(), Error> {
304- self.run(Command::Timeline(TimelineCommand::Send {
305- timeline_id,
306- content,
307- }))
308- .await
309- }
310-
311- pub async fn edit_message(
312- &self,
313- timeline_id: TimelineId,
314- target: impl Into<EventRef>,
315- content: MessageEditContent,
316- ) -> Result<(), Error> {
317- self.run(Command::Timeline(TimelineCommand::Edit {
318- timeline_id,
319- target: target.into(),
320- content,
321- }))
322- .await
323- }
324-
325- pub async fn redact(
326- &self,
327- timeline_id: TimelineId,
328- target: impl Into<EventRef>,
329- reason: Option<String>,
330- ) -> Result<(), Error> {
331- self.run(Command::Timeline(TimelineCommand::Redact {
332- timeline_id,
333- target: target.into(),
334- reason,
335- }))
336- .await
337- }
338-
339- pub async fn toggle_reaction(
340- &self,
341- timeline_id: TimelineId,
342- target: impl Into<EventRef>,
343- key: impl Into<String>,
344- shortcode: Option<String>,
345- ) -> Result<(), Error> {
346- self.run(Command::Timeline(TimelineCommand::ToggleReaction {
347- timeline_id,
348- target: target.into(),
349- key: key.into(),
350- shortcode,
351- }))
352- .await
353- }
354-
355- pub async fn mark_read(
356- &self,
357- timeline_id: TimelineId,
358- event_id: OwnedEventId,
359- ) -> Result<(), Error> {
360- self.run(Command::Timeline(TimelineCommand::MarkRead {
361- timeline_id,
362- event_id,
363- }))
364- .await
365- }
366-
367- pub async fn set_typing(&self, room_id: OwnedRoomId, typing: bool) -> Result<(), Error> {
368- self.run(Command::Room(RoomCommand::SetTyping { room_id, typing }))
369- .await
370- }
371-
372- pub async fn prepare_to_send(&self, room_id: OwnedRoomId) -> Result<(), Error> {
373- self.run(Command::Room(RoomCommand::PrepareToSend(room_id)))
374- .await
375- }
376-
377- pub async fn join_room(
378- &self,
379- room: OwnedRoomOrAliasId,
380- via: Vec<OwnedServerName>,
381- ) -> Result<OwnedRoomId, Error> {
382- self.run_for_room(RoomCommand::Join { room, via }).await
383- }
384-
385- pub async fn leave_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
386- self.run(Command::Room(RoomCommand::Leave(room_id))).await
387- }
388-
389- pub async fn forget_room(&self, room_id: OwnedRoomId) -> Result<(), Error> {
390- self.run(Command::Room(RoomCommand::Forget(room_id))).await
391- }
392-
393- pub async fn accept_invite(&self, room_id: OwnedRoomId) -> Result<(), Error> {
394- self.run(Command::Room(RoomCommand::AcceptInvite(room_id)))
395- .await
396- }
397-
398- pub async fn reject_invite(&self, room_id: OwnedRoomId) -> Result<(), Error> {
399- self.run(Command::Room(RoomCommand::RejectInvite(room_id)))
400- .await
401- }
402-
403- pub async fn invite_user(
404- &self,
405- room_id: OwnedRoomId,
406- user_id: OwnedUserId,
407- ) -> Result<(), Error> {
408- self.run(Command::Room(RoomCommand::InviteUser { room_id, user_id }))
409- .await
410- }
411-
412- pub async fn kick_user(
413- &self,
414- room_id: OwnedRoomId,
415- user_id: OwnedUserId,
416- reason: Option<String>,
417- ) -> Result<(), Error> {
418- self.run(Command::Room(RoomCommand::KickUser {
419- room_id,
420- user_id,
421- reason,
422- }))
423- .await
424- }
425-
426- pub async fn ban_user(
427- &self,
428- room_id: OwnedRoomId,
429- user_id: OwnedUserId,
430- reason: Option<String>,
431- ) -> Result<(), Error> {
432- self.run(Command::Room(RoomCommand::BanUser {
433- room_id,
434- user_id,
435- reason,
436- }))
437- .await
438- }
439-
440- pub async fn unban_user(
441- &self,
442- room_id: OwnedRoomId,
443- user_id: OwnedUserId,
444- reason: Option<String>,
445- ) -> Result<(), Error> {
446- self.run(Command::Room(RoomCommand::UnbanUser {
447- room_id,
448- user_id,
449- reason,
450- }))
451- .await
452- }
453-
454- pub async fn set_power_levels(
455- &self,
456- room_id: OwnedRoomId,
457- changes: PowerLevelChanges,
458- ) -> Result<(), Error> {
459- self.run(Command::Room(RoomCommand::SetPowerLevels {
460- room_id,
461- changes,
462- }))
463- .await
464- }
465-
466- pub async fn set_user_power_level(
467- &self,
468- room_id: OwnedRoomId,
469- user_id: OwnedUserId,
470- level: i64,
471- ) -> Result<(), Error> {
472- self.run(Command::Room(RoomCommand::SetUserPowerLevel {
473- room_id,
474- user_id,
475- level,
476- }))
477- .await
478- }
479-
480- pub async fn create_room(&self, options: CreateRoomOptions) -> Result<OwnedRoomId, Error> {
481- self.run_for_room(RoomCommand::Create(options)).await
482- }
483-
484- /// hands back the existing dm with them if there is one
485- pub async fn create_direct(&self, user_id: OwnedUserId) -> Result<OwnedRoomId, Error> {
486- self.run_for_room(RoomCommand::CreateDirect(user_id)).await
487- }
488-
489- pub async fn cancel_send(
490- &self,
491- room_id: OwnedRoomId,
492- transaction_id: OwnedTransactionId,
493- ) -> Result<(), Error> {
494- self.run(Command::Room(RoomCommand::CancelSend {
495- room_id,
496- transaction_id,
497- }))
498- .await
499- }
500-
501- pub async fn retry_send(
502- &self,
503- room_id: OwnedRoomId,
504- transaction_id: OwnedTransactionId,
505- ) -> Result<(), Error> {
506- self.run(Command::Room(RoomCommand::RetrySend {
507- room_id,
508- transaction_id,
509- }))
510- .await
511- }
512-
513- pub async fn upload(&self, upload: AttachmentUpload) -> Result<(), Error> {
514- self.run(Command::Media(MediaCommand::Upload(upload))).await
515- }
516-
517- pub async fn recover(&self, recovery_key: impl Into<String>) -> Result<(), Error> {
518- self.run(Command::Encryption(EncryptionCommand::Recover(
519- recovery_key.into(),
520- )))
521- .await
522- }
523-
524- pub async fn create_key_backup(&self) -> Result<(), Error> {
525- self.run(Command::Encryption(EncryptionCommand::CreateKeyBackup))
526- .await
527- }
528-
529- pub async fn enable_key_backup(&self) -> Result<(), Error> {
530- self.run(Command::Encryption(EncryptionCommand::EnableKeyBackup))
531- .await
532- }
533-
534- pub async fn disable_key_backup(&self, delete_from_server: bool) -> Result<(), Error> {
535- self.run(Command::Encryption(EncryptionCommand::DisableKeyBackup {
536- delete_from_server,
537- }))
538- .await
539- }
540-
541- pub async fn recover_key_backup(&self, recovery_key: impl Into<String>) -> Result<(), Error> {
542- self.run(Command::Encryption(EncryptionCommand::RecoverKeyBackup(
543- recovery_key.into(),
544- )))
545- .await
546- }
547-
548- pub async fn start_verification(&self, target: VerificationTarget) -> Result<(), Error> {
549- self.run(Command::Encryption(EncryptionCommand::StartVerification(
550- target,
551- )))
552- .await
553- }
554-
555- pub async fn verify(&self, action: VerificationAction) -> Result<(), Error> {
556- self.run(Command::Encryption(EncryptionCommand::Verify(action)))
557- .await
558- }
559-
560- /// bytes for an avatar, or for media a retained timeline showed you. concurrent asks share
561- /// one download
562- pub async fn media(&self, media_id: &MediaId, size: MediaSize) -> Result<Media, Error> {
563- self.media.timeline_media(media_id, size).await
564- }
565-
566- /// where `media` would fetch from, for a host streaming big files itself. the byte cap
567- /// doesn't apply since nothing is buffered
568- pub async fn media_source(
569- &self,
570- media_id: &MediaId,
571- size: MediaSize,
572- ) -> Result<MediaLocation, Error> {
573- self.media.media_location(media_id, size).await
574- }
575-
576- /// the space graph builds on first use and then keeps itself current
577- async fn spaces(&self) -> &SpaceService {
578- self.spaces
579- .get_or_init(|| SpaceService::new(self.client.clone()))
580- .await
581- }
582-
583- pub async fn rooms(&self) -> Vec<RoomSummary> {
584- room_summaries(&self.client, self.spaces().await).await
585- }
586-
587- pub async fn image_packs(&self) -> Result<ImagePacks, Error> {
588- image_packs(&self.client).await
589- }
590-
591- /// the homeserver's preview of a page, `None` when it has none. the homeserver sees every
592- /// url asked about, so a host decides whether encrypted rooms ask at all
593- pub async fn url_preview(&self, url: &str) -> Result<Option<LinkPreview>, Error> {
594- self.media.url_preview(url).await
595- }
596-
597- /// whether the room and the user's settings let link previews show here
598- pub async fn url_previews_allowed(&self, room_id: &RoomId) -> Result<bool, Error> {
599- url_previews_allowed(&self.room(room_id)?).await
600- }
601-
602- /// browse a space the way the server sees it, rooms you haven't joined included
603- pub async fn space_hierarchy(&self, space_id: OwnedRoomId) -> SpaceHierarchy {
604- SpaceHierarchy(SpaceRoomList::new(self.client.clone(), space_id).await)
605- }
606-
607- pub async fn own_profile(&self) -> Result<Profile, Error> {
608- let user_id = self
609- .client
610- .user_id()
611- .ok_or_else(|| Error::new(ErrorKind::Unauthenticated, "the client is not logged in"))?
612- .to_owned();
613- let response = self
614- .client
615- .account()
616- .fetch_user_profile()
617- .await
618- .map_err(|source| Error::from_sdk("fetching the profile failed", source))?;
619- profile(user_id, &response)
620- }
621-
622- pub async fn user_profile(&self, user_id: &UserId) -> Result<Profile, Error> {
623- let response = self
624- .client
625- .account()
626- .fetch_user_profile_of(user_id)
627- .await
628- .map_err(|source| Error::from_sdk("fetching the profile failed", source))?;
629- profile(user_id.to_owned(), &response)
630- }
631-
632- pub async fn set_display_name(&self, name: Option<String>) -> Result<(), Error> {
633- self.run(Command::Account(AccountCommand::SetDisplayName(name)))
634- .await
635- }
636-
637- pub async fn set_avatar(&self, avatar: Option<OwnedMxcUri>) -> Result<(), Error> {
638- self.run(Command::Account(AccountCommand::SetAvatar(avatar)))
639- .await
640- }
641-
642- pub async fn upload_avatar(
643- &self,
644- mimetype: impl Into<String>,
645- bytes: Vec<u8>,
646- ) -> Result<(), Error> {
647- self.run(Command::Account(AccountCommand::UploadAvatar {
648- mimetype: mimetype.into(),
649- bytes,
650- }))
651- .await
652- }
653-
654- pub async fn rooms_filtered(&self, filter: &RoomListFilter) -> Vec<RoomSummary> {
655- let matcher = filter.matcher();
656- self.rooms()
657- .await
658- .into_iter()
659- .filter(|room| matcher.matches(room))
660- .collect()
661- }
662-
663- pub async fn devices(&self) -> Result<Vec<OwnDevice>, Error> {
664- let current_device_id = self.client.device_id();
665- Ok(self
666- .client
667- .devices()
668- .await
669- .map_err(|source| Error::from_http("listing devices failed", source))?
670- .devices
671- .into_iter()
672- .map(|device| OwnDevice {
673- current: current_device_id == Some(device.device_id.as_ref()),
674- device_id: device.device_id,
675- display_name: device.display_name,
676- last_seen_ip: device.last_seen_ip,
677- last_seen_timestamp_ms: device.last_seen_ts.map(|timestamp| timestamp.get().into()),
678- })
679- .collect())
680- }
681-
682- fn room(&self, room_id: &RoomId) -> Result<matrix_sdk::Room, Error> {
683- self.client
684- .get_room(room_id)
685- .ok_or_else(Error::room_unknown)
686- }
687-
688- pub async fn room_notification_settings(
689- &self,
690- room_id: &RoomId,
691- ) -> Result<Option<RoomNotificationSettings>, Error> {
692- let room = self.room(room_id)?;
693- let custom = room.user_defined_notification_mode().await;
694- let mode = match custom {
695- Some(mode) => mode,
696- None => match room.notification_mode().await {
697- Some(mode) => mode,
698- None => return Ok(None),
699- },
700- };
701- Ok(Some(RoomNotificationSettings {
702- mode: crate::normalise::notification_mode(mode),
703- custom: custom.is_some(),
704- }))
705- }
706-
707- pub async fn notification_keywords(&self) -> Vec<String> {
708- self.client
709- .notification_settings()
710- .await
711- .enabled_keywords()
712- .await
713- .into_iter()
714- .collect()
715- }
716-
717- pub async fn resolve_push_notification(
718- &self,
719- room_id: &RoomId,
720- event_id: &EventId,
721- ) -> Result<PushNotificationStatus, Error> {
722- let notification_failed = |source| {
723- Error::with_source(
724- ErrorKind::Other,
725- "resolving the push notification failed",
726- source,
727- )
728- };
729- let client = self
730- .notification_client
731- .get_or_try_init(|| async {
732- NotificationClient::new(
733- self.client.clone(),
734- NotificationProcessSetup::SingleProcess {
735- sync_service: self.sync_service.clone(),
736- },
737- )
738- .await
739- })
740- .await
741- .map_err(notification_failed)?;
742- let status = client
743- .get_notification(room_id, event_id)
744- .await
745- .map_err(notification_failed)?;
746- Ok(normalise_push_notification(room_id, event_id, status))
747- }
748-
749- pub async fn members(
750- &self,
751- room_id: &RoomId,
752- filter: MemberFilter,
753- ) -> Result<Vec<MemberSummary>, Error> {
754- let memberships = match filter {
755- MemberFilter::Active => RoomMemberships::ACTIVE,
756- MemberFilter::Joined => RoomMemberships::JOIN,
757- MemberFilter::Invited => RoomMemberships::INVITE,
758- MemberFilter::Banned => RoomMemberships::BAN,
759- MemberFilter::Left => RoomMemberships::LEAVE,
760- // An empty mask is the SDK's unfiltered wildcard: it matches every
761- // membership, including custom states that `all()` would exclude.
762- MemberFilter::All => RoomMemberships::empty(),
763- };
764- Ok(self
765- .room(room_id)?
766- .members(memberships)
767- .await
768- .map_err(|source| Error::from_sdk("listing members failed", source))?
769- .into_iter()
770- .map(member_summary)
771- .collect())
772- }
773-
774- pub async fn room_power_levels(&self, room_id: &RoomId) -> Result<RoomPowerLevels, Error> {
775- let levels = self
776- .room(room_id)?
777- .power_levels()
778- .await
779- .map_err(|source| Error::from_sdk("reading power levels failed", source.into()))?;
780- Ok(RoomPowerLevels {
781- ban: levels.ban.into(),
782- invite: levels.invite.into(),
783- kick: levels.kick.into(),
784- redact: levels.redact.into(),
785- events_default: levels.events_default.into(),
786- state_default: levels.state_default.into(),
787- users_default: levels.users_default.into(),
788- room_notification: levels.notifications.room.into(),
789- users: levels
790- .users
791- .into_iter()
792- .map(|(user_id, level)| (user_id, level.into()))
793- .collect(),
794- events: levels
795- .events
796- .into_iter()
797- .map(|(event_type, level)| (event_type.to_string(), level.into()))
798- .collect(),
799- creators: levels
800- .rules
801- .privileged_creators
802- .into_iter()
803- .flatten()
804- .collect(),
805- })
806- }
807-
808- pub async fn pending_send_ids(
809- &self,
810- room_id: &RoomId,
811- ) -> Result<Vec<OwnedTransactionId>, Error> {
812- let (echoes, _) = self
813- .room(room_id)?
814- .send_queue()
815- .subscribe()
816- .await
817- .map_err(|source| {
818- Error::with_source(ErrorKind::Store, "reading the send queue failed", source)
819- })?;
820- Ok(echoes
821- .into_iter()
822- .filter(|echo| {
823- matches!(
824- echo.content,
825- matrix_sdk::send_queue::LocalEchoContent::Event { .. }
826- )
827- })
828- .map(|echo| echo.transaction_id)
829- .collect())
830- }
831-
832- pub async fn global_account_data(
833- &self,
834- event_type: GlobalAccountDataEventType,
835- ) -> Result<Option<Raw<AnyGlobalAccountDataEventContent>>, Error> {
836- self.client
837- .account()
838- .account_data_raw(event_type)
839- .await
840- .map_err(|source| Error::from_sdk("reading account data failed", source))
841- }
842-
843- pub async fn room_account_data(
844- &self,
845- room_id: &RoomId,
846- event_type: RoomAccountDataEventType,
847- ) -> Result<Option<Raw<AnyRoomAccountDataEvent>>, Error> {
848- self.room(room_id)?
849- .account_data(event_type)
850- .await
851- .map_err(|source| Error::from_sdk("reading room account data failed", source))
852- }
853-
854- #[must_use]
855- pub fn connection_state(&self) -> watch::Receiver<ConnectionState> {
856- self.connection.clone()
857- }
858-
859- #[must_use]
860- pub fn encryption_state(&self) -> Option<EncryptionState> {
861- let device_id = self.client.device_id()?.to_owned();
862- let encryption = self.client.encryption();
863- Some(encryption_state(
864- device_id,
865- encryption.recovery().state(),
866- encryption.verification_state().get(),
867- encryption.backups().state(),
868- ))
869- }
870-}
871-
872-/// one space's hierarchy, a page at a time. hold on to it while you browse, it remembers where it got to
873-pub struct SpaceHierarchy(SpaceRoomList);
874-
875-impl fmt::Debug for SpaceHierarchy {
876- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
877- formatter
878- .debug_struct("SpaceHierarchy")
879- .finish_non_exhaustive()
880- }
881-}
882-
883-impl SpaceHierarchy {
884- /// fetches the next page. a no-op once the end is reached or while a page is in flight
885- pub async fn paginate(&self) -> Result<(), Error> {
886- self.0.paginate().await.map_err(|source| {
887- Error::with_source(
888- ErrorKind::Server,
889- "fetching the space hierarchy failed",
890- source,
891- )
892- })
893- }
894-
895- /// everything fetched so far, the space itself excluded
896- pub async fn rooms(&self) -> Vec<SpaceChild> {
897- self.0.rooms().await.into_iter().map(space_child).collect()
898- }
899-
900- #[must_use]
901- pub fn end_reached(&self) -> bool {
902- matches!(
903- self.0.pagination_state(),
904- SpaceRoomListPaginationState::Idle { end_reached: true }
905- )
906- }
907-}
908-
909-fn profile(
910- user_id: OwnedUserId,
911- response: &ruma::api::client::profile::get_profile::v3::Response,
912-) -> Result<Profile, Error> {
913- use ruma::api::client::profile::{AvatarUrl, DisplayName};
914- let malformed = |source| {
915- Error::with_source(
916- ErrorKind::Server,
917- "the server sent a malformed profile",
918- source,
919- )
920- };
921- Ok(Profile {
922- user_id,
923- display_name: response.get_static::<DisplayName>().map_err(malformed)?,
924- avatar: response
925- .get_static::<AvatarUrl>()
926- .map_err(malformed)?
927- .as_deref()
928- .map(MediaId::plain),
929- })
930-}
931-
932-#[derive(Debug)]
933-pub(crate) struct Request {
934- pub(crate) command: Command,
935- pub(crate) reply: Responder,
936-}
937-
938-impl Request {
939- fn new(command: Command) -> (Self, Pending) {
940- let (reply, pending) = oneshot::channel();
941- (
942- Self {
943- command,
944- reply: Responder(reply),
945- },
946- Pending(pending),
947- )
948- }
949-}
950-
951-/// whoever holds this owes the caller an answer. dropping it answers `Interrupted`
952-#[derive(Debug)]
953-pub(crate) struct Responder(oneshot::Sender<Result<CommandOutput, Error>>);
954-
955-impl Responder {
956- pub(crate) fn send(self, result: Result<CommandOutput, Error>) {
957- if let Err(error) = &result {
958- tracing::debug!(%error, kind = ?error.kind, "command failed");
959- }
960- let _ = self.0.send(result);
961- }
962-
963- pub(crate) fn done(self, result: Result<(), Error>) {
964- self.send(result.map(|()| CommandOutput::Done));
965- }
966-}
967-
968-#[derive(Debug)]
969-pub struct EventStream {
970- events: mpsc::Receiver<Event>,
971-}
972-
973-impl EventStream {
974- pub async fn recv(&mut self) -> Option<Event> {
975- self.events.recv().await
976- }
977-
978- pub fn try_recv(&mut self) -> Result<Event, mpsc::error::TryRecvError> {
979- self.events.try_recv()
980- }
981-}
982-
983-impl futures_util::Stream for EventStream {
984- type Item = Event;
985-
986- fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
987- self.events.poll_recv(context)
988- }
989-}
990-
991-#[derive(Debug)]
992-pub(crate) struct DriverPort {
993- requests: mpsc::Receiver<Request>,
994- events: mpsc::Sender<Event>,
995- connection: watch::Sender<ConnectionState>,
996-}
997-
998-#[derive(Clone, Debug)]
999-pub(crate) struct EventSender(mpsc::Sender<Event>);
1000-
1001-impl EventSender {
1002- pub(crate) async fn send(&self, event: Event) -> Result<(), SendEventError> {
1003- self.0.send(event).await.map_err(|_| SendEventError)
1004- }
1005-
1006- pub(crate) async fn reserve(&self) -> Result<mpsc::Permit<'_, Event>, SendEventError> {
1007- self.0.reserve().await.map_err(|_| SendEventError)
1008- }
1009-
1010- /// resolves once every `EventStream` is gone
1011- pub(crate) async fn closed(&self) {
1012- self.0.closed().await;
1013- }
1014-}
1015-
1016-impl DriverPort {
1017- pub(crate) fn event_sender(&self) -> EventSender {
1018- EventSender(self.events.clone())
1019- }
1020-
1021- pub(crate) fn connection_sender(&self) -> watch::Sender<ConnectionState> {
1022- self.connection.clone()
1023- }
1024-
1025- pub(crate) async fn recv(&mut self) -> Option<Request> {
1026- self.requests.recv().await
1027- }
1028-
1029- pub(crate) fn try_recv(&mut self) -> Option<Request> {
1030- self.requests.try_recv().ok()
1031- }
1032-
1033- pub(crate) fn close(&mut self) {
1034- self.requests.close();
1035- }
1036-}
1037-
1038-#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
1039-#[error("the event receiver has closed")]
1040-pub(crate) struct SendEventError;
1041-
1042-#[must_use]
1043-pub(crate) fn transport_pair(
1044- client: Client,
1045- sync_service: Arc<SyncService>,
1046- media: Arc<MediaService>,
1047- store_lock: Option<Arc<StoreLock>>,
1048- command_capacity: usize,
1049- event_capacity: usize,
1050-) -> (ClientHandle, DriverPort, EventStream) {
1051- let (request_tx, request_rx) = mpsc::channel(command_capacity);
1052- let (event_tx, event_rx) = mpsc::channel(event_capacity);
1053- let (connection_tx, connection_rx) = watch::channel(ConnectionState::Idle);
1054- (
1055- ClientHandle {
1056- requests: request_tx,
1057- client,
1058- connection: connection_rx,
1059- notification_client: Arc::new(OnceCell::new()),
1060- sync_service,
1061- media,
1062- spaces: Arc::new(OnceCell::new()),
1063- _store_lock: store_lock,
1064- },
1065- DriverPort {
1066- requests: request_rx,
1067- events: event_tx,
1068- connection: connection_tx,
1069- },
1070- EventStream { events: event_rx },
1071- )
1072-}
1073-
1074-fn normalise_push_notification(
1075- room_id: &RoomId,
1076- event_id: &EventId,
1077- status: NotificationStatus,
1078-) -> PushNotificationStatus {
1079- match status {
1080- NotificationStatus::Event(item) => {
1081- let event = match &item.raw_event {
1082- RawNotificationEvent::Timeline(event) => {
1083- PushNotificationEvent::Timeline(event.clone())
1084- }
1085- RawNotificationEvent::Invite(event) => PushNotificationEvent::Invite(event.clone()),
1086- };
1087- PushNotificationStatus::Event(Box::new(PushNotification {
1088- event_id: event_id.to_owned(),
1089- event,
1090- sender: PushNotificationSender {
1091- user_id: item.event.sender().to_owned(),
1092- display_name: item.sender_display_name,
1093- avatar_url: item.sender_avatar_url.as_deref().map(MediaId::plain),
1094- name_ambiguous: item.is_sender_name_ambiguous,
1095- },
1096- room: PushNotificationRoom {
1097- room_id: room_id.to_owned(),
1098- name: item.room_computed_display_name,
1099- avatar_url: item.room_avatar_url.as_deref().map(MediaId::plain),
1100- canonical_alias: item
1101- .room_canonical_alias
1102- .as_deref()
1103- .and_then(|alias| alias.parse().ok()),
1104- topic: item.room_topic,
1105- encrypted: item.is_room_encrypted,
1106- direct: item.room_is_dm,
1107- joined_members_count: item.joined_members_count,
1108- service_members: item
1109- .service_members
1110- .into_iter()
1111- .filter_map(|user_id| user_id.parse().ok())
1112- .collect(),
1113- active_service_members_count: item.active_service_members_count,
1114- space: item.is_space,
1115- },
1116- noisy: item.is_noisy,
1117- has_mention: item.has_mention,
1118- thread_id: item.thread_id,
1119- }))
1120- }
1121- NotificationStatus::EventNotFound => PushNotificationStatus::NotFound,
1122- NotificationStatus::EventFilteredOut => PushNotificationStatus::FilteredOut,
1123- NotificationStatus::EventRedacted => PushNotificationStatus::Redacted,
1124- }
1125-}
Deleted file: vendor/macltok/src/uiaa.rs−234 deletions
@@ -1,234 +0,0 @@
1-use std::fmt;
2-
3-use ruma::{UserId, api::client::uiaa, serde::JsonObject};
4-#[cfg(feature = "serde")]
5-use serde::{Deserialize, Serialize};
6-
7-#[derive(Eq, PartialEq)]
8-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9-#[non_exhaustive]
10-pub enum InteractiveAuth {
11- Password {
12- password: String,
13- session: Option<String>,
14- },
15- ReCaptcha {
16- response: String,
17- session: Option<String>,
18- },
19- Dummy {
20- session: Option<String>,
21- },
22- FallbackAcknowledgement {
23- session: String,
24- },
25- OAuth {
26- session: Option<String>,
27- },
28- Stage {
29- auth_type: String,
30- session: Option<String>,
31- data: JsonObject,
32- },
33-}
34-
35-impl InteractiveAuth {
36- #[must_use]
37- pub fn password(password: impl Into<String>, session: Option<String>) -> Self {
38- Self::Password {
39- password: password.into(),
40- session,
41- }
42- }
43-
44- #[must_use]
45- pub fn recaptcha(response: impl Into<String>, session: Option<String>) -> Self {
46- Self::ReCaptcha {
47- response: response.into(),
48- session,
49- }
50- }
51-
52- #[must_use]
53- pub fn dummy(session: Option<String>) -> Self {
54- Self::Dummy { session }
55- }
56-
57- #[must_use]
58- pub fn fallback_acknowledgement(session: impl Into<String>) -> Self {
59- Self::FallbackAcknowledgement {
60- session: session.into(),
61- }
62- }
63-
64- #[must_use]
65- pub fn oauth(session: Option<String>) -> Self {
66- Self::OAuth { session }
67- }
68-
69- #[must_use]
70- pub fn stage(auth_type: impl Into<String>, session: Option<String>, data: JsonObject) -> Self {
71- Self::Stage {
72- auth_type: auth_type.into(),
73- session,
74- data,
75- }
76- }
77-
78- pub(crate) fn into_sdk(self, user_id: &UserId) -> Result<uiaa::AuthData, String> {
79- match self {
80- Self::Password { password, session } => {
81- let mut password = uiaa::Password::new(user_id.to_owned().into(), password);
82- password.session = session;
83- Ok(uiaa::AuthData::Password(password))
84- }
85- Self::ReCaptcha { response, session } => {
86- let mut recaptcha = uiaa::ReCaptcha::new(response);
87- recaptcha.session = session;
88- Ok(uiaa::AuthData::ReCaptcha(recaptcha))
89- }
90- Self::Dummy { session } => {
91- let mut dummy = uiaa::Dummy::new();
92- dummy.session = session;
93- Ok(uiaa::AuthData::Dummy(dummy))
94- }
95- Self::FallbackAcknowledgement { session } => {
96- Ok(uiaa::AuthData::fallback_acknowledgement(session))
97- }
98- Self::OAuth { session } => {
99- let mut oauth = uiaa::OAuth::new();
100- oauth.session = session;
101- Ok(uiaa::AuthData::OAuth(oauth))
102- }
103- Self::Stage {
104- auth_type,
105- session,
106- data,
107- } => uiaa::AuthData::new(&auth_type, session, data).map_err(|error| error.to_string()),
108- }
109- }
110-}
111-
112-impl fmt::Debug for InteractiveAuth {
113- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114- match self {
115- Self::Password {
116- password: _,
117- session,
118- } => formatter
119- .debug_struct("Password")
120- .field("session", &session.as_ref().map(|_| "..."))
121- .finish_non_exhaustive(),
122- Self::ReCaptcha {
123- response: _,
124- session,
125- } => formatter
126- .debug_struct("ReCaptcha")
127- .field("session", &session.as_ref().map(|_| "..."))
128- .finish_non_exhaustive(),
129- Self::Dummy { session } => formatter
130- .debug_struct("Dummy")
131- .field("session", &session.as_ref().map(|_| "..."))
132- .finish(),
133- Self::FallbackAcknowledgement { session: _ } => formatter
134- .debug_struct("FallbackAcknowledgement")
135- .field("session", &"...")
136- .finish(),
137- Self::OAuth { session } => formatter
138- .debug_struct("OAuth")
139- .field("session", &session.as_ref().map(|_| "..."))
140- .finish(),
141- Self::Stage {
142- auth_type,
143- session,
144- data: _,
145- } => formatter
146- .debug_struct("Stage")
147- .field("auth_type", auth_type)
148- .field("session", &session.as_ref().map(|_| "..."))
149- .finish_non_exhaustive(),
150- }
151- }
152-}
153-
154-#[derive(Clone, Eq, PartialEq)]
155-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
156-#[non_exhaustive]
157-pub struct InteractiveAuthChallenge {
158- pub flows: Vec<InteractiveAuthFlow>,
159- pub completed: Vec<String>,
160- pub parameters_json: Option<String>,
161- pub session: Option<String>,
162- pub error: Option<InteractiveAuthFailure>,
163-}
164-
165-impl fmt::Debug for InteractiveAuthChallenge {
166- fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167- formatter
168- .debug_struct("InteractiveAuthChallenge")
169- .field("flows", &self.flows)
170- .field("completed", &self.completed)
171- .field(
172- "parameters_json",
173- &self.parameters_json.as_ref().map(|_| "..."),
174- )
175- .field("session", &self.session.as_ref().map(|_| "..."))
176- .field("error", &self.error)
177- .finish()
178- }
179-}
180-
181-impl From<&uiaa::UiaaInfo> for InteractiveAuthChallenge {
182- fn from(info: &uiaa::UiaaInfo) -> Self {
183- Self {
184- flows: info
185- .flows
186- .iter()
187- .map(|flow| InteractiveAuthFlow {
188- stages: flow
189- .stages
190- .iter()
191- .map(|stage| stage.as_str().to_owned())
192- .collect(),
193- })
194- .collect(),
195- completed: info
196- .completed
197- .iter()
198- .map(|stage| stage.as_str().to_owned())
199- .collect(),
200- parameters_json: info.params.as_ref().map(|params| params.get().to_owned()),
201- session: info.session.clone(),
202- error: info
203- .auth_error
204- .as_ref()
205- .map(|error| InteractiveAuthFailure {
206- code: error.kind.errcode().as_str().to_owned(),
207- message: error.message.clone(),
208- }),
209- }
210- }
211-}
212-
213-#[derive(Clone, Debug, Eq, PartialEq)]
214-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
215-#[non_exhaustive]
216-pub struct InteractiveAuthFlow {
217- pub stages: Vec<String>,
218-}
219-
220-#[derive(Clone, Debug, Eq, PartialEq)]
221-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
222-#[non_exhaustive]
223-pub struct InteractiveAuthFailure {
224- pub code: String,
225- pub message: String,
226-}
227-
228-#[derive(Clone, Debug, Eq, PartialEq)]
229-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
230-#[non_exhaustive]
231-pub enum InteractiveAuthOutcome {
232- Completed,
233- Challenge(InteractiveAuthChallenge),
234-}
Deleted file: vendor/macltok/src/upload.rs−168 deletions
@@ -1,168 +0,0 @@
1-use std::time::Duration;
2-
3-use matrix_sdk::{
4- attachment::{
5- AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
6- BaseVideoInfo, Thumbnail,
7- },
8- ruma::events::room::message::TextMessageEventContent,
9-};
10-
11-use crate::{AttachmentUpload, Error, ErrorKind, UploadDetails};
12-
13-pub(crate) fn prepare_upload(
14- mut upload: AttachmentUpload,
15- max_media_bytes: usize,
16-) -> Result<(AttachmentUpload, mime::Mime, AttachmentConfig), Error> {
17- if upload.bytes.len() > max_media_bytes {
18- return Err(Error::new(
19- ErrorKind::MediaTooLarge,
20- "attachment exceeds the configured byte limit",
21- ));
22- }
23- let mimetype: mime::Mime = upload
24- .mimetype
25- .parse()
26- .map_err(|_| Error::new(ErrorKind::Invalid, "media MIME type is invalid"))?;
27-
28- let size = ruma::UInt::try_from(upload.bytes.len()).map_err(|_| {
29- Error::new(
30- ErrorKind::MediaTooLarge,
31- "attachment size is not representable",
32- )
33- })?;
34- let info = match upload.details.as_ref() {
35- Some(UploadDetails::Image {
36- width,
37- height,
38- blurhash,
39- }) if mimetype.type_() == mime::IMAGE => AttachmentInfo::Image(BaseImageInfo {
40- width: width.map(Into::into),
41- height: height.map(Into::into),
42- size: Some(size),
43- blurhash: blurhash.clone(),
44- ..Default::default()
45- }),
46- Some(UploadDetails::Video {
47- width,
48- height,
49- duration_ms,
50- blurhash,
51- }) if mimetype.type_() == mime::VIDEO => AttachmentInfo::Video(BaseVideoInfo {
52- width: width.map(Into::into),
53- height: height.map(Into::into),
54- duration: duration_ms.map(Duration::from_millis),
55- size: Some(size),
56- blurhash: blurhash.clone(),
57- }),
58- Some(UploadDetails::Audio { duration_ms }) if mimetype.type_() == mime::AUDIO => {
59- AttachmentInfo::Audio(BaseAudioInfo {
60- duration: duration_ms.map(Duration::from_millis),
61- size: Some(size),
62- ..Default::default()
63- })
64- }
65- Some(UploadDetails::Voice {
66- duration_ms,
67- waveform,
68- }) if mimetype.type_() == mime::AUDIO => {
69- if waveform.iter().any(|sample| *sample > 1024) {
70- return Err(Error::new(
71- ErrorKind::Invalid,
72- "voice waveform samples must be at most 1024",
73- ));
74- }
75- AttachmentInfo::Voice(BaseAudioInfo {
76- duration: Some(Duration::from_millis(*duration_ms)),
77- size: Some(size),
78- waveform: Some(
79- waveform
80- .iter()
81- .map(|sample| f32::from(*sample) / 1024.0)
82- .collect(),
83- ),
84- })
85- }
86- Some(UploadDetails::File)
87- if mimetype.type_() != mime::IMAGE
88- && mimetype.type_() != mime::VIDEO
89- && mimetype.type_() != mime::AUDIO =>
90- {
91- AttachmentInfo::File(BaseFileInfo { size: Some(size) })
92- }
93- None => match mimetype.type_() {
94- mime::IMAGE => AttachmentInfo::Image(BaseImageInfo {
95- size: Some(size),
96- ..Default::default()
97- }),
98- mime::VIDEO => AttachmentInfo::Video(BaseVideoInfo {
99- size: Some(size),
100- ..Default::default()
101- }),
102- mime::AUDIO => AttachmentInfo::Audio(BaseAudioInfo {
103- size: Some(size),
104- ..Default::default()
105- }),
106- _ => AttachmentInfo::File(BaseFileInfo { size: Some(size) }),
107- },
108- _ => {
109- return Err(Error::new(
110- ErrorKind::Invalid,
111- "attachment metadata does not match its MIME type",
112- ));
113- }
114- };
115- if upload.thumbnail.is_some() && mimetype.type_() == mime::AUDIO {
116- return Err(Error::new(
117- ErrorKind::Invalid,
118- "audio attachments do not support thumbnails",
119- ));
120- }
121- let thumbnail = upload
122- .thumbnail
123- .take()
124- .map(|thumbnail| {
125- if thumbnail.bytes.len() > max_media_bytes
126- || thumbnail.width == 0
127- || thumbnail.height == 0
128- {
129- return Err(Error::new(
130- if thumbnail.bytes.len() > max_media_bytes {
131- ErrorKind::MediaTooLarge
132- } else {
133- ErrorKind::Invalid
134- },
135- "thumbnail size or dimensions are invalid",
136- ));
137- }
138- let content_type: mime::Mime = thumbnail
139- .mimetype
140- .parse()
141- .map_err(|_| Error::new(ErrorKind::Invalid, "thumbnail MIME type is invalid"))?;
142- if content_type.type_() != mime::IMAGE {
143- return Err(Error::new(
144- ErrorKind::Invalid,
145- "thumbnail MIME type must be an image",
146- ));
147- }
148- let size = ruma::UInt::try_from(thumbnail.bytes.len()).map_err(|_| {
149- Error::new(
150- ErrorKind::MediaTooLarge,
151- "thumbnail size is not representable",
152- )
153- })?;
154- Ok(Thumbnail {
155- data: thumbnail.bytes,
156- content_type,
157- width: thumbnail.width.into(),
158- height: thumbnail.height.into(),
159- size,
160- })
161- })
162- .transpose()?;
163- let config = AttachmentConfig::new()
164- .info(info)
165- .thumbnail(thumbnail)
166- .caption(upload.caption.clone().map(TextMessageEventContent::plain));
167- Ok((upload, mimetype, config))
168-}
Deleted file: vendor/macltok/src/wasm.rs−29 deletions
@@ -1,29 +0,0 @@
1-use std::cell::RefCell;
2-
3-type Handler = Box<dyn Fn(&str)>;
4-
5-thread_local! {
6- static HANDLER: RefCell<Option<Handler>> = const { RefCell::new(None) };
7-}
8-
9-/// a panic on wasm32 traps the task that raised it and nothing unwinds, so the engine
10-/// catches nothing. the rest of it keeps running in an undefined state and goes silent,
11-/// no `Failed` and no `Stopped`. `handler` runs from the panic hook with the message,
12-/// after whatever hook was there before, and is the host's one chance to react, say by
13-/// terminating the worker and starting a fresh one. a second call replaces the handler
14-pub fn on_panic(handler: impl Fn(&str) + 'static) {
15- let installed = HANDLER.with(|slot| slot.replace(Some(Box::new(handler))).is_some());
16- if installed {
17- return;
18- }
19- let previous = std::panic::take_hook();
20- std::panic::set_hook(Box::new(move |info| {
21- previous(info);
22- let message = info.to_string();
23- HANDLER.with(|slot| {
24- if let Some(handler) = &*slot.borrow() {
25- handler(&message);
26- }
27- });
28- }));
29-}
Modified file: web/vite.config.ts+1 additions−4 deletions
@@ -16,9 +16,7 @@ function precacheManifestPlugin(): import('vite').Plugin {
1616 name: 'malice-sw-precache-manifest',
1717 generateBundle(_options, bundle) {
1818 const urls = Object.keys(bundle)
19- .filter(
20- (fileName) => fileName !== 'sw.js' && fileName !== '.vite/manifest.json' && !fileName.startsWith('twemoji/')
21- )
19+ .filter((fileName) => fileName !== 'sw.js' && !fileName.startsWith('twemoji/'))
2220 .map((fileName) => `/${fileName}`);
2321 const swChunk = bundle['sw.js'];
2422 if (swChunk && swChunk.type === 'chunk') {
@@ -82,7 +80,6 @@ export default defineConfig(({ mode }) => {
8280 ? {
8381 outDir: 'dist-device',
8482 emptyOutDir: true,
85- manifest: true,
8683 rollupOptions: {
8784 input: {
8885 main: 'index.html',

show truncated diff