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−235 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_i

show full diff (262144 bytes truncated)