Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f92ccd8d40 | |||
| 2c2e0f1b15 | |||
| f314578df5 | |||
| 739289cd82 | |||
| 1d7d6c943b | |||
| 917f9ee298 | |||
| c7d44ddad3 | |||
| 1a170ddf90 | |||
| 696d1cacbe | |||
| c6d1cf20b6 | |||
| 38f34e66eb | |||
| 8fa411aaa0 | |||
| b4b3b5258c | |||
| fd6cb6647f | |||
| 103ff7d3ec | |||
| efa28a1ffd | |||
| c4640897d4 | |||
| 0487a143ed | |||
| 7ab97d59b8 | |||
| 940862ebed | |||
| e507eaabf6 | |||
| a2bbc33920 | |||
| db787c28b8 | |||
| a3d6114ddd | |||
| 58a139bf96 | |||
| 012bc03014 | |||
| 38adf952bd | |||
| 99d675d85a | |||
| be5eebaa35 | |||
| cf4d3cb492 | |||
| ad5241b2e5 | |||
| d083c56ccb | |||
| eb493de1b8 | |||
| 6aa4fdaed4 | |||
| 9c6bbd98a7 | |||
| 8b14a8920c | |||
| 98b36fe5e0 | |||
| a1f32d741f | |||
| ce01854078 | |||
| 7f8c9b0244 | |||
| 3621acb445 | |||
| 3789374951 | |||
| e54a36a1ca | |||
| cf95c3f4cc | |||
| 6ccfc92139 | |||
| 4ee20befa3 | |||
| 4348d9de23 | |||
| 477ee70e1c | |||
| 83989c030a | |||
| 717b6371f8 | |||
| 6fa832cfc8 | |||
| 1bff8cbe5d | |||
| 9bb67e8422 | |||
| c962a52f73 | |||
| 4d20b481c1 | |||
| a5136c7384 | |||
| 0e120549ce | |||
| 1e74c48351 | |||
| 9e242abda9 | |||
| 62ff895ce8 | |||
| d0d22ece78 | |||
| d49d86e8b0 | |||
| b226191bf4 | |||
| 21d7cd1b9d | |||
| b066260554 | |||
| dbc6e3c3aa | |||
| 3293c94451 | |||
| 53385798f6 | |||
| d49736455b | |||
| e278a99a20 | |||
| d5c7c886a1 | |||
| b9b40c0ef2 | |||
| 99d60bcc70 | |||
| 64d602141a | |||
| 59d2f8d4af | |||
| 80a253ada9 | |||
| ec5082bb9c | |||
| 63f5be6935 | |||
| dab87b7731 | |||
| 63f33a081f | |||
| 8b762b04df | |||
| a9ef675b1c | |||
| e69bb96144 | |||
| 63b8fc8b47 | |||
| d68879d0aa | |||
| 1afebd6d4e | |||
| db4c1f8dbd | |||
| c1b89a2751 | |||
| 9d39e4bae8 | |||
| 936f173e6f | |||
| 9eb4a5a24c | |||
| 49bba4d471 | |||
| f23deacec4 | |||
| 9c28ee7041 | |||
| 5d420b683f | |||
| 2d5b95e45e | |||
| 3d29db5ebb | |||
| 8e041d6958 | |||
| c02d0f1d01 | |||
| 2da262e56c | |||
| 31e4657718 | |||
| 5b2fe3cf47 | |||
| 49d7d4528d | |||
| 3d7e505530 | |||
| 56d4163982 | |||
| 4cafc48cc4 | |||
| 8ea55b30bc | |||
| d438ee2ca0 | |||
| a3fe9eef12 | |||
| 60056ba205 | |||
| ff62ed667c | |||
| 4168362912 | |||
| 96d63b7c8d | |||
| 260eaeea2b | |||
| f7442fa279 | |||
| da3cf3d54b | |||
| 872861e90e | |||
| 36cca4e444 | |||
| bb6202c76b | |||
| 5458a5afd2 | |||
| 45fa860b6b | |||
| 4ad25e32af | |||
| 423ebf9502 | |||
| 7fbb8cf9e5 | |||
| e0cac4eb7a | |||
| aa9cc67f32 | |||
| 7d6569ff27 | |||
| 4d7b19999b | |||
| d47306045f | |||
| ba461598b2 | |||
| 0233d03d4e | |||
| 725f74cbe8 | |||
| 98aceb40c2 | |||
| 34a35ad32a | |||
| 99971a2347 | |||
| b5f5a9c90e | |||
| ec4e228620 | |||
| a2c254d7da | |||
| 557c0698ba | |||
| 99a5568dbf | |||
| 94d8674709 | |||
| dfc4e03d5b | |||
| 14b8248e4b | |||
| 447c89aed4 | |||
| 894f94c383 | |||
| 1f3dea778b | |||
| b07069170f | |||
| f685feed40 | |||
| 610d4ca4e5 | |||
| 41edb08283 | |||
| 44aad14732 | |||
| b1fbf31c2e | |||
| 2bcab72dbb | |||
| 3bac5d250d | |||
| 4ec75b5b5c | |||
| c8c9a94995 | |||
| 08b80c533a | |||
| 0e679904e3 | |||
| 89c50c7ecd | |||
| d2857ee20a | |||
| c1274cea14 | |||
| 12cd1effdc | |||
| 3a14b73258 | |||
| d97f4ab8b3 | |||
| 3539487c49 | |||
| 526f35e3d3 | |||
| 6b4e2655cc | |||
| 30de5372eb | |||
| ba451b67c9 | |||
| a65fec3175 | |||
| 6298b5c176 | |||
| 0283ae14d9 | |||
| a1157d2c38 | |||
| fcce02fc11 | |||
| 7cea5be9e2 | |||
| 32e9626e0f | |||
| e376670af3 | |||
| 7f08793360 | |||
| 1fc7b34016 | |||
| 37447e16e5 | |||
| 782355b556 | |||
| eb51c862ce | |||
| 676c81ca80 | |||
| 644c5e8a4c | |||
| 34d5e6da2a | |||
| 5936a7285f | |||
| 62d58a21ab | |||
| 2e334fafa2 | |||
| e66967c46f | |||
| c237659aca | |||
| 8cb6f74996 | |||
| 0afd7c9528 | |||
| 388ced09a6 | |||
| 4071a55cd7 | |||
| c04a97f706 | |||
| 5b0c6bbafc | |||
| 1fb0f7f56a | |||
| ee9a05defe | |||
| c0f746f88f | |||
| 839786f810 | |||
| 4e4b508c38 | |||
| 313e634996 | |||
| ae72fcf663 | |||
| 12e9a2a159 | |||
| 16c1b9b57f | |||
| 06e6dd05c3 | |||
| 1cf84e203e | |||
| 9913cce946 | |||
| 1ee88b176c | |||
| f0193cc8ac | |||
| 5d2eab119d | |||
| ca91b6a278 | |||
| 260fec8750 | |||
| 950fcd289d | |||
| bdd0162831 | |||
| 752695c6ff | |||
| 552639763e | |||
| d151340882 | |||
| 27415dc64d | |||
| 4feeaf0cf5 | |||
| 87a2f8fab7 | |||
| 871cb2221b | |||
| d1d174d137 | |||
| bd7dca45ef | |||
| 856b2be992 | |||
| 161750f6d0 | |||
| c4a95f9bbe | |||
| ac5552807b | |||
| ff8b27f99b | |||
| bf9ffd7d09 | |||
| 9f18d76355 |
@@ -97,7 +97,7 @@ jobs:
|
||||
run: cargo codspeed build -p benchmarks --bench ${{ matrix.benchmark }} --features codspeed
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab
|
||||
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
mode: simulation
|
||||
|
||||
@@ -38,12 +38,14 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -51,12 +53,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -101,7 +103,9 @@ jobs:
|
||||
ndk-version: r27
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -109,12 +113,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -149,12 +153,14 @@ jobs:
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install aarch64-apple-ios target
|
||||
run: rustup target install aarch64-apple-ios
|
||||
@@ -165,12 +171,12 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-macos }}"
|
||||
@@ -206,12 +212,14 @@ jobs:
|
||||
|
||||
# install protoc in case we end up rebuilding opentelemetry-proto
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Add rust targets
|
||||
run: |
|
||||
@@ -223,7 +231,7 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
|
||||
+48
-29
@@ -37,7 +37,6 @@ jobs:
|
||||
- no-encryption-and-sqlite
|
||||
- sqlite-cryptostore
|
||||
- experimental-encrypted-state-events
|
||||
- native-tls
|
||||
- markdown
|
||||
- socks
|
||||
- sso-login
|
||||
@@ -50,7 +49,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install libsqlite
|
||||
run: |
|
||||
@@ -58,7 +59,7 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -69,10 +70,12 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -94,18 +97,22 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -132,20 +139,23 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -182,7 +192,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
@@ -193,17 +203,19 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
@@ -256,8 +268,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: wasm32-unknown-unknown
|
||||
components: clippy
|
||||
|
||||
@@ -268,7 +281,7 @@ jobs:
|
||||
version: v0.13.1
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
# use a separate cache for each job to work around
|
||||
# https://github.com/Swatinem/rust-cache/issues/124
|
||||
@@ -279,10 +292,12 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -308,7 +323,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@631208b7aac2daa8b707f55e7331f9112b0e062d # v1.44.0
|
||||
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
@@ -322,23 +337,23 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: nightly-2026-02-26
|
||||
components: clippy, rustfmt
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -388,15 +403,19 @@ jobs:
|
||||
sudo apt-get install libsqlite3-dev
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest
|
||||
|
||||
- name: Test
|
||||
env:
|
||||
|
||||
@@ -26,6 +26,9 @@ jobs:
|
||||
name: Code Coverage
|
||||
needs: xtask
|
||||
runs-on: "ubuntu-latest"
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
# run several docker containers with the same networking stack so the hostname 'synapse'
|
||||
# maps to the synapse container, etc.
|
||||
@@ -112,7 +115,9 @@ jobs:
|
||||
sudo rm -rf /var/lib/apt/lists/*
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
# Cargo config can screw with caching and is only used for alias config
|
||||
# and extra lints, which we don't care about here
|
||||
@@ -120,19 +125,18 @@ jobs:
|
||||
run: rm .cargo/config.toml
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
prefix-key: "coverage"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@8a8ecf49de4feac160d13668bc406fe17413c1bf # cargo-llvm-cov
|
||||
|
||||
- name: Install nextest
|
||||
uses: taiki-e/install-action@e28ca663369ecd0d4acc114be0b55092dcd84136 # nextest
|
||||
- name: Install nextest and llvm-cov
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v 2.75.10
|
||||
with:
|
||||
tool: nextest,cargo-llvm-cov
|
||||
|
||||
- name: Get xtask
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
key: "${{ needs.xtask.outputs.cachekey-linux }}"
|
||||
@@ -151,35 +155,14 @@ jobs:
|
||||
HOMESERVER_URL: "http://localhost:8008"
|
||||
HOMESERVER_DOMAIN: "synapse"
|
||||
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/test.yml
|
||||
- name: Get PR number and commit SHA
|
||||
run: |
|
||||
echo "Storing PR number ${{ github.event.number }}"
|
||||
echo "${{ github.event.number }}" > pr_number.txt
|
||||
|
||||
echo "Storing commit SHA ${{ github.event.pull_request.head.sha }}"
|
||||
echo "${{ github.event.pull_request.head.sha }}" > commit_sha.txt
|
||||
|
||||
- name: Move the JUnit file into the root directory
|
||||
shell: bash
|
||||
run: |
|
||||
mv target/nextest/ci/junit.xml ./junit.xml
|
||||
|
||||
# This stores the coverage report and metadata in artifacts.
|
||||
# The actual upload to Codecov is executed by a different workflow `upload_coverage.yml`.
|
||||
# The reason for this split is because `on.pull_request` workflows don't have access to secrets.
|
||||
- name: Store coverage report in artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
|
||||
with:
|
||||
name: codecov_report
|
||||
path: |
|
||||
coverage.xml
|
||||
junit.xml
|
||||
pr_number.txt
|
||||
commit_sha.txt
|
||||
if-no-files-found: error
|
||||
use_oidc: true
|
||||
|
||||
- run: |
|
||||
echo 'The coverage report was stored in Github artifacts.'
|
||||
echo 'It will be uploaded to Codecov using `upload_coverage.yml` workflow shortly.'
|
||||
- name: Upload test results to Codecov
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2
|
||||
with:
|
||||
use_oidc: true
|
||||
report_type: "test_results"
|
||||
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2
|
||||
- uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Machete
|
||||
uses: bnjbvr/cargo-machete@b81ce1560c5fbd0210cb66d88bf210329ff04266
|
||||
uses: bnjbvr/cargo-machete@ac30a525c0a8d163a92d727b3ff079ee3f6ecb08
|
||||
|
||||
@@ -26,12 +26,12 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install protoc
|
||||
uses: taiki-e/install-action@64c5c20c872907b6f7cd50994ac189e7274160f2 # v2
|
||||
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10
|
||||
with:
|
||||
tool: protoc@3.20.3
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # master
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: nightly-2026-02-26
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
node-version: 20
|
||||
|
||||
- name: Load cache
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
|
||||
@@ -54,11 +54,11 @@ jobs:
|
||||
|
||||
- name: Upload artifact
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: './target/doc/'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
|
||||
@@ -16,5 +16,7 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: taiki-e/install-action@4146cc238a18f9b3f3427572fd4585d2d2d78c88 # cargo-hack
|
||||
- uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # cargo-hack
|
||||
with:
|
||||
tool: cargo-hack
|
||||
- run: cargo hack check --rust-version --workspace --all-targets
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# Copied with minimal adjustments, source:
|
||||
# https://github.com/google/mdbook-i18n-helpers/blob/2168b9cea1f4f76b55426591a9bcc308a620194f/.github/workflows/coverage-report.yml
|
||||
name: Upload code coverage
|
||||
|
||||
on:
|
||||
# This workflow is triggered after every successful execution
|
||||
# of `coverage` workflow.
|
||||
workflow_run:
|
||||
workflows: ["Code Coverage"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Upload coverage report
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: 'Fetch coverage report from artifacts'
|
||||
id: prepare_report
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
script: |
|
||||
var fs = require('fs');
|
||||
|
||||
// List artifacts of the workflow run that triggered this workflow
|
||||
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
let codecovReport = artifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "codecov_report";
|
||||
});
|
||||
|
||||
if (codecovReport.length != 1) {
|
||||
throw new Error("Unexpected number of {codecov_report} artifacts: " + codecovReport.length);
|
||||
}
|
||||
|
||||
var download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: codecovReport[0].id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
fs.writeFileSync('codecov_report.zip', Buffer.from(download.data));
|
||||
|
||||
- id: parse_previous_artifacts
|
||||
run: |
|
||||
unzip codecov_report.zip
|
||||
|
||||
echo "Detected PR is: $(<pr_number.txt)"
|
||||
echo "Detected commit_sha is: $(<commit_sha.txt)"
|
||||
|
||||
# Make the params available as step output
|
||||
echo "override_pr=$(<pr_number.txt)" >> "$GITHUB_OUTPUT"
|
||||
echo "override_commit=$(<commit_sha.txt)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
ref: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
path: repo_root
|
||||
persist-credentials: false
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
# Manual overrides for these parameters are needed because automatic detection
|
||||
# in codecov-action does not work for non-`pull_request` workflows.
|
||||
# In `main` branch push, these default to empty strings since we want to run
|
||||
# the analysis on HEAD.
|
||||
override_commit: ${{ steps.parse_previous_artifacts.outputs.override_commit || '' }}
|
||||
override_pr: ${{ steps.parse_previous_artifacts.outputs.override_pr || '' }}
|
||||
working-directory: ${{ github.workspace }}/repo_root
|
||||
|
||||
# Location where coverage report files are searched for
|
||||
directory: ${{ github.workspace }}
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
echo "cachekey-${{ matrix.cachekey-id }}=xtask-${{ matrix.cachekey-id }}-${{ hashFiles('Cargo.toml', 'xtask/**') }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check xtask cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: xtask-cache
|
||||
with:
|
||||
path: target/debug/xtask
|
||||
@@ -68,7 +68,9 @@ jobs:
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Build
|
||||
if: steps.xtask-cache.outputs.cache-hit != 'true'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Lint GHA workflows with zizmor
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
zizmor:
|
||||
name: Run zizmor
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
|
||||
+1
-1
@@ -7,4 +7,4 @@ group_imports = "StdExternalCrate"
|
||||
format_code_in_doc_comments = true
|
||||
doc_comment_code_block_width = 80
|
||||
# Workaround for https://github.com/rust-lang/rust.vim/issues/464
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
rules:
|
||||
unpinned-uses:
|
||||
severity: high
|
||||
+15
-25
@@ -81,10 +81,10 @@ contributors' pull requests and pushes.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Ideally, a PR should have a *proper title*, with *atomic logical commits*, and
|
||||
each commit should have a *good commit message*.
|
||||
Ideally, a PR should have a _proper title_, with _atomic logical commits_, and
|
||||
each commit should have a _good commit message_.
|
||||
|
||||
A *proper PR title* would be a one-liner summary of the changes in the PR,
|
||||
A _proper PR title_ would be a one-liner summary of the changes in the PR,
|
||||
following the same guidelines of a good commit message, including the
|
||||
area/feature prefix. Something like `FFI: Allow logs files to be pruned.` would
|
||||
be a good PR title.
|
||||
@@ -126,10 +126,10 @@ A good example of a changelog entry could look like the following:
|
||||
For security-related changelog entries, please include the following additional
|
||||
details alongside the pull request number:
|
||||
|
||||
* Impact: Clearly describe the issue's potential impact on users or systems.
|
||||
* CVE Number: If available, include the CVE (Common Vulnerabilities and
|
||||
- Impact: Clearly describe the issue's potential impact on users or systems.
|
||||
- CVE Number: If available, include the CVE (Common Vulnerabilities and
|
||||
Exposures) identifier.
|
||||
* GitHub Advisory Link: Provide a link to the corresponding GitHub security
|
||||
- GitHub Advisory Link: Provide a link to the corresponding GitHub security
|
||||
advisory for further context.
|
||||
|
||||
```markdown
|
||||
@@ -156,12 +156,12 @@ Conventional Commits are structured as follows:
|
||||
The type of changes which will be included in changelogs is one of the
|
||||
following:
|
||||
|
||||
* `feat`: A new feature
|
||||
* `fix`: A bugfix
|
||||
* `doc`: Documentation changes
|
||||
* `refactor`: Code refactoring
|
||||
* `perf`: Performance improvements
|
||||
* `ci`: Changes to CI configuration files and scripts
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bugfix
|
||||
- `doc`: Documentation changes
|
||||
- `refactor`: Code refactoring
|
||||
- `perf`: Performance improvements
|
||||
- `ci`: Changes to CI configuration files and scripts
|
||||
|
||||
The scope is optional and can specify the area of the codebase affected (e.g.,
|
||||
olm, cipher).
|
||||
@@ -174,10 +174,10 @@ changelog entry.
|
||||
|
||||
The metadata must be included in the following git-trailers:
|
||||
|
||||
* `Security-Impact`: The magnitude of harm that can be expected, i.e.
|
||||
- `Security-Impact`: The magnitude of harm that can be expected, i.e.
|
||||
low/moderate/high/critical.
|
||||
* `CVE`: The CVE that was assigned to this issue.
|
||||
* `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
- `CVE`: The CVE that was assigned to this issue.
|
||||
- `GitHub-Advisory`: The GitHub advisory identifier.
|
||||
|
||||
Please include all the fields that are available.
|
||||
|
||||
@@ -334,16 +334,6 @@ on Git 2.17+ you can mass signoff using rebase:
|
||||
git rebase --signoff origin/main
|
||||
```
|
||||
|
||||
## Tips for working on the `matrix-rust-sdk` with specific IDEs
|
||||
|
||||
* [RustRover](https://www.jetbrains.com/rust/) will attempt to sync the project
|
||||
with all features enabled, causing an error in `matrix-sdk` ("only one of the
|
||||
features `native-tls` or `rustls-tls` can be enabled"). To work around this,
|
||||
open `crates/matrix-sdk/Cargo.toml` in RustRover and uncheck one of the
|
||||
`native-tls` or `rustls-tls` feature definitions:
|
||||
|
||||

|
||||
|
||||
## AI policy
|
||||
|
||||
This policy is a copy of the [Forgejo's AI agreement][Forgejo].
|
||||
|
||||
Generated
+359
-388
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -28,7 +28,7 @@ assert_matches2 = { version = "0.1.2", default-features = false }
|
||||
async_cell = { version = "0.2.3", default-features = false }
|
||||
async-compat = { version = "0.2.5", default-features = false }
|
||||
async-once-cell = { version = "0.5.4", default-features = false }
|
||||
async-rx = { version = "0.1.3", default-features = false }
|
||||
async-rx = { version = "0.2.0", default-features = false }
|
||||
# Bumping this to 0.3.6 produces a test failure because the semantic between the
|
||||
# versions changed subtly: https://github.com/matrix-org/matrix-rust-sdk/issues/4599
|
||||
async-stream = { version = "0.3.6", default-features = false }
|
||||
@@ -46,7 +46,7 @@ eyeball-im-util = { version = "0.10.0", default-features = false }
|
||||
futures-core = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
futures-executor = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
getrandom = { version = "0.3.4", default-features = false }
|
||||
getrandom = { version = "0.4.2", default-features = false }
|
||||
gloo-timers = { version = "0.3.0", default-features = false }
|
||||
gloo-utils = { version = "0.2.0", default-features = false, features = ["serde"] }
|
||||
growable-bloom-filter = { version = "2.1.1", default-features = false }
|
||||
@@ -67,11 +67,11 @@ pin-project-lite = { version = "0.2.16", default-features = false }
|
||||
proc-macro2 = { version = "1.0.106", default-features = false }
|
||||
proptest = { version = "1.9.0", default-features = false, features = ["std"] }
|
||||
quote = { version = "1.0.37", default-features = false }
|
||||
rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] }
|
||||
rand = { version = "0.10.1", default-features = false, features = ["std", "std_rng", "thread_rng"] }
|
||||
regex = { version = "1.12.2", default-features = false }
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rmp-serde = { version = "1.3.0", default-features = false }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "9666e207f2cd1a8b26e49bbdf656ad32e4639c7b", features = [
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "7680eebd9586669e1a4e5b1fd1c2c691221369d4", features = [
|
||||
"client-api-c",
|
||||
"compat-unset-avatar",
|
||||
"compat-upload-signatures",
|
||||
@@ -119,7 +119,7 @@ uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["ca
|
||||
url = { version = "2.5.7", default-features = false }
|
||||
uuid = { version = "1.18.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
vodozemac = { version = "0.9.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption"] }
|
||||
vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] }
|
||||
wasm-bindgen = { version = "0.2.105", default-features = false }
|
||||
wasm-bindgen-test = { version = "0.3.55", default-features = false, features = ["std"] }
|
||||
web-sys = { version = "0.3.82", default-features = false }
|
||||
|
||||
@@ -17,7 +17,7 @@ codspeed = []
|
||||
assert_matches.workspace = true
|
||||
criterion = { version = "4.2.1", features = ["async", "async_tokio", "html_reports"], package = "codspeed-criterion-compat" }
|
||||
futures-util.workspace = true
|
||||
matrix-sdk = { workspace = true, features = ["rustls-tls", "e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk = { workspace = true, features = ["e2e-encryption", "sqlite", "testing"] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-crypto.workspace = true
|
||||
matrix-sdk-sqlite = { workspace = true, features = ["crypto-store"] }
|
||||
|
||||
@@ -6,7 +6,7 @@ use matrix_sdk_test::{JoinedRoomBuilder, base64_sha256_hash, event_factory::Even
|
||||
use matrix_sdk_ui::{
|
||||
RoomListService, eyeball_im::VectorDiff, room_list_service::filters::new_filter_non_left,
|
||||
};
|
||||
use rand::{distributions::Uniform, prelude::Distribution};
|
||||
use rand::{distr::Uniform, prelude::Distribution};
|
||||
use ruma::{OwnedRoomId, RoomId, owned_user_id};
|
||||
use tokio::runtime::Builder;
|
||||
|
||||
@@ -26,8 +26,8 @@ pub fn create(c: &mut Criterion) {
|
||||
});
|
||||
|
||||
let sender_id = owned_user_id!("@mnt_io:matrix.org");
|
||||
let mut rand = rand::thread_rng();
|
||||
let server_ts_range = Uniform::from(100..1000);
|
||||
let mut rand = rand::rng();
|
||||
let server_ts_range = Uniform::try_from(100..1000).unwrap();
|
||||
|
||||
for room_nth in 0..NUMBER_OF_ROOMS {
|
||||
// Synapse's room IDs for rooms v1 to v11 have an 18 characters localpart.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:5.6
|
||||
// swift-tools-version:5.7
|
||||
|
||||
// A package manifest for local development. This file will be copied
|
||||
// into the root of the repo when generating an XCFramework.
|
||||
@@ -8,7 +8,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "MatrixRustSDK",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.iOS(.v16),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version: 5.6
|
||||
// swift-tools-version: 5.7
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
@@ -6,7 +6,7 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "MatrixRustSDK",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.iOS(.v16),
|
||||
.macOS(.v12)
|
||||
],
|
||||
products: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "matrix-sdk-crypto-ffi"
|
||||
version = "0.1.0"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
description = "Uniffi based bindings for the Rust SDK crypto crate"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
|
||||
@@ -3,10 +3,10 @@ use std::{collections::HashMap, iter, ops::DerefMut, sync::Arc};
|
||||
use hmac::Hmac;
|
||||
use matrix_sdk_crypto::{
|
||||
backups::DecryptionError,
|
||||
store::{types::BackupDecryptionKey, CryptoStoreError as InnerStoreError},
|
||||
store::{CryptoStoreError as InnerStoreError, types::BackupDecryptionKey},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
use rand::{RngExt, distr::Alphanumeric, rng};
|
||||
use sha2::Sha512;
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
@@ -75,11 +75,7 @@ impl BackupRecoveryKey {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[uniffi::constructor]
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
inner: BackupDecryptionKey::new()
|
||||
.expect("Can't gather enough randomness to create a recovery key"),
|
||||
passphrase_info: None,
|
||||
})
|
||||
Arc::new(Self { inner: BackupDecryptionKey::new(), passphrase_info: None })
|
||||
}
|
||||
|
||||
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string.
|
||||
@@ -97,7 +93,7 @@ impl BackupRecoveryKey {
|
||||
/// Create a new [`BackupRecoveryKey`] from the given passphrase.
|
||||
#[uniffi::constructor]
|
||||
pub fn new_from_passphrase(passphrase: String) -> Arc<Self> {
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
let salt: String = iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
|
||||
@@ -2,14 +2,14 @@ use std::{mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
DecryptionSettings,
|
||||
dehydrated_devices::{
|
||||
DehydratedDevice as InnerDehydratedDevice, DehydratedDevices as InnerDehydratedDevices,
|
||||
RehydratedDevice as InnerRehydratedDevice,
|
||||
},
|
||||
store::types::DehydratedDeviceKey as InnerDehydratedDeviceKey,
|
||||
DecryptionSettings,
|
||||
};
|
||||
use ruma::{api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw, OwnedDeviceId};
|
||||
use ruma::{OwnedDeviceId, api::client::dehydrated_device, events::AnyToDeviceEvent, serde::Raw};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{CryptoStoreError, DehydratedDeviceKey};
|
||||
@@ -29,8 +29,6 @@ pub enum DehydrationError {
|
||||
Store(#[from] matrix_sdk_crypto::CryptoStoreError),
|
||||
#[error("The pickle key has an invalid length, expected 32 bytes, got {0}")]
|
||||
PickleKeyLength(usize),
|
||||
#[error(transparent)]
|
||||
Rand(#[from] rand::Error),
|
||||
}
|
||||
|
||||
impl From<matrix_sdk_crypto::dehydrated_devices::DehydrationError> for DehydrationError {
|
||||
@@ -227,13 +225,11 @@ impl From<dehydrated_device::put_dehydrated_device::unstable::Request>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{dehydrated_devices::DehydrationError, DehydratedDeviceKey};
|
||||
use crate::{DehydratedDeviceKey, dehydrated_devices::DehydrationError};
|
||||
|
||||
#[test]
|
||||
fn test_creating_dehydrated_key() {
|
||||
let result = DehydratedDeviceKey::new();
|
||||
assert!(result.is_ok());
|
||||
let dehydrated_device_key = result.unwrap();
|
||||
let dehydrated_device_key = DehydratedDeviceKey::new();
|
||||
let base_64 = dehydrated_device_key.to_base64();
|
||||
let inner_bytes = dehydrated_device_key.inner;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use matrix_sdk_crypto::{
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
KeyExportError, MegolmError, OlmError, SecretImportError as RustSecretImportError,
|
||||
SignatureError as InnerSignatureError,
|
||||
store::{CryptoStoreError as InnerStoreError, DehydrationError as InnerDehydrationError},
|
||||
};
|
||||
use matrix_sdk_sqlite::OpenStoreError;
|
||||
use ruma::{IdParseError, OwnedUserId};
|
||||
|
||||
@@ -31,22 +31,22 @@ pub use error::{
|
||||
CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError,
|
||||
};
|
||||
use js_int::UInt;
|
||||
pub use logger::{set_logger, Logger};
|
||||
pub use logger::{Logger, set_logger};
|
||||
pub use machine::{KeyRequestPair, OlmMachine, SignatureVerification};
|
||||
use matrix_sdk_common::deserialized_responses::{ShieldState as RustShieldState, ShieldStateCode};
|
||||
use matrix_sdk_crypto::{
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
olm::{IdentityKeys, InboundGroupSession, SenderData, Session},
|
||||
store::{
|
||||
CryptoStore,
|
||||
types::{
|
||||
Changes, DehydratedDeviceKey as InnerDehydratedDeviceKey, PendingChanges,
|
||||
RoomSettings as RustRoomSettings,
|
||||
},
|
||||
CryptoStore,
|
||||
},
|
||||
types::{
|
||||
DeviceKey, DeviceKeys, EventEncryptionAlgorithm as RustEventEncryptionAlgorithm, SigningKey,
|
||||
},
|
||||
CollectStrategy, EncryptionSettings as RustEncryptionSettings,
|
||||
};
|
||||
use matrix_sdk_sqlite::SqliteCryptoStore;
|
||||
pub use responses::{
|
||||
@@ -54,9 +54,9 @@ pub use responses::{
|
||||
Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest,
|
||||
};
|
||||
use ruma::{
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId,
|
||||
RoomId, SecondsSinceUnixEpoch, UserId,
|
||||
events::room::history_visibility::HistoryVisibility as RustHistoryVisibility,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::runtime::Runtime;
|
||||
@@ -850,9 +850,9 @@ pub struct DehydratedDeviceKey {
|
||||
|
||||
impl DehydratedDeviceKey {
|
||||
/// Generates a new random pickle key.
|
||||
pub fn new() -> Result<Self, DehydrationError> {
|
||||
let inner = InnerDehydratedDeviceKey::new()?;
|
||||
Ok(inner.into())
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
InnerDehydratedDeviceKey::new().into()
|
||||
}
|
||||
|
||||
/// Creates a new dehydration pickle key from the given slice.
|
||||
@@ -1015,18 +1015,18 @@ impl PkEncryption {
|
||||
}
|
||||
|
||||
/// Encrypt a message using this [`PkEncryption`] object.
|
||||
pub fn encrypt(&self, plaintext: &str) -> PkMessage {
|
||||
pub fn encrypt(&self, plaintext: &str) -> Option<PkMessage> {
|
||||
use vodozemac::base64_encode;
|
||||
|
||||
let message = self.inner.encrypt(plaintext.as_ref());
|
||||
let message = self.inner.encrypt(plaintext.as_ref()).ok()?;
|
||||
|
||||
let vodozemac::pk_encryption::Message { ciphertext, mac, ephemeral_key } = message;
|
||||
|
||||
PkMessage {
|
||||
Some(PkMessage {
|
||||
ciphertext: base64_encode(ciphertext),
|
||||
mac: base64_encode(mac),
|
||||
ephemeral_key: ephemeral_key.to_base64(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,11 +1049,11 @@ uniffi::setup_scaffolding!();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use tempfile::tempdir;
|
||||
|
||||
use super::MigrationData;
|
||||
use crate::{migrate, EventEncryptionAlgorithm, OlmMachine, RoomSettings};
|
||||
use crate::{EventEncryptionAlgorithm, OlmMachine, RoomSettings, migrate};
|
||||
|
||||
#[test]
|
||||
fn android_migration() -> Result<()> {
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
use tracing_subscriber::{EnvFilter, fmt::MakeWriter};
|
||||
|
||||
/// Trait that can be used to forward Rust logs over FFI to a language specific
|
||||
/// logger.
|
||||
|
||||
@@ -10,6 +10,8 @@ use std::{
|
||||
use js_int::UInt;
|
||||
use matrix_sdk_common::deserialized_responses::AlgorithmInfo;
|
||||
use matrix_sdk_crypto::{
|
||||
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
backups::{
|
||||
MegolmV1BackupKey as RustBackupKey, SignatureState,
|
||||
SignatureVerification as RustSignatureCheckResult,
|
||||
@@ -18,11 +20,12 @@ use matrix_sdk_crypto::{
|
||||
olm::ExportedRoomKey,
|
||||
store::types::{BackupDecryptionKey, Changes},
|
||||
types::requests::ToDeviceRequest,
|
||||
CollectStrategy, DecryptionSettings, LocalTrust, OlmMachine as InnerMachine,
|
||||
UserIdentity as SdkUserIdentity,
|
||||
};
|
||||
use ruma::{
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
api::{
|
||||
IncomingResponse,
|
||||
client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
@@ -32,38 +35,35 @@ use ruma::{
|
||||
upload_signatures::v3::Response as SignatureUploadResponse,
|
||||
},
|
||||
message::send_message_event::v3::Response as RoomMessageResponse,
|
||||
sync::sync_events::{v3::ToDevice, DeviceLists as RumaDeviceLists},
|
||||
sync::sync_events::{DeviceLists as RumaDeviceLists, v3::ToDevice},
|
||||
to_device::send_event_to_device::v3::Response as ToDeviceResponse,
|
||||
},
|
||||
IncomingResponse,
|
||||
},
|
||||
events::{
|
||||
key::verification::VerificationMethod, room::message::MessageType, AnyMessageLikeEvent,
|
||||
AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
AnyMessageLikeEvent, AnySyncMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
|
||||
key::verification::VerificationMethod, room::message::MessageType,
|
||||
},
|
||||
serde::Raw,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
DeviceKeyAlgorithm, EventId, OneTimeKeyAlgorithm, OwnedTransactionId, OwnedUserId, RoomId,
|
||||
UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use serde_json::{Value, value::RawValue};
|
||||
use tokio::runtime::Runtime;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
|
||||
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
|
||||
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
|
||||
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
|
||||
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
|
||||
dehydrated_devices::DehydratedDevices,
|
||||
error::{
|
||||
CryptoStoreError, DecryptionError, SecretImportError, SecretsBundleExportError,
|
||||
SignatureError,
|
||||
},
|
||||
parse_user_id,
|
||||
responses::{response_from_string, OwnedResponse},
|
||||
BackupKeys, BackupRecoveryKey, BootstrapCrossSigningResult, CrossSigningKeyExport,
|
||||
CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, EncryptionSettings,
|
||||
EventEncryptionAlgorithm, KeyImportError, KeysImportResult, MegolmV1BackupKey,
|
||||
ProgressListener, Request, RequestType, RequestVerificationResult, RoomKeyCounts, RoomSettings,
|
||||
Sas, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest,
|
||||
responses::{OwnedResponse, response_from_string},
|
||||
};
|
||||
|
||||
/// The return value for the [`OlmMachine::receive_sync_changes()`] method.
|
||||
@@ -913,22 +913,19 @@ impl OlmMachine {
|
||||
&decryption_settings,
|
||||
))?;
|
||||
|
||||
if handle_verification_events {
|
||||
if let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize() {
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(
|
||||
original_event,
|
||||
)) => {
|
||||
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype
|
||||
{
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
if handle_verification_events
|
||||
&& let Ok(AnyTimelineEvent::MessageLike(e)) = decrypted.event.deserialize()
|
||||
{
|
||||
match &e {
|
||||
AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(original_event)) => {
|
||||
if let MessageType::VerificationRequest(_) = &original_event.content.msgtype {
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
_ if e.event_type().to_string().starts_with("m.key.verification") => {
|
||||
self.runtime.block_on(self.inner.receive_verification_event(&e))?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@ use std::collections::HashMap;
|
||||
|
||||
use http::Response;
|
||||
use matrix_sdk_crypto::{
|
||||
CrossSigningBootstrapRequests,
|
||||
types::requests::{
|
||||
AnyIncomingResponse, KeysBackupRequest, OutgoingRequest,
|
||||
OutgoingVerificationRequest as SdkVerificationRequest, RoomMessageRequest, ToDeviceRequest,
|
||||
UploadSigningKeysRequest as RustUploadSigningKeysRequest,
|
||||
},
|
||||
CrossSigningBootstrapRequests,
|
||||
};
|
||||
use ruma::{
|
||||
OwnedTransactionId, UserId,
|
||||
api::client::{
|
||||
backup::add_backup_keys::v3::Response as KeysBackupResponse,
|
||||
keys::{
|
||||
@@ -28,7 +29,6 @@ use ruma::{
|
||||
},
|
||||
assign,
|
||||
events::MessageLikeEventContent,
|
||||
OwnedTransactionId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use matrix_sdk_crypto::{types::CrossSigningKey, UserIdentity as SdkUserIdentity};
|
||||
use matrix_sdk_crypto::{UserIdentity as SdkUserIdentity, types::CrossSigningKey};
|
||||
|
||||
use crate::CryptoStoreError;
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ use std::sync::Arc;
|
||||
use futures_util::{Stream, StreamExt};
|
||||
use matrix_sdk_common::executor::Handle;
|
||||
use matrix_sdk_crypto::{
|
||||
matrix_sdk_qrcode::QrVerificationData, CancelInfo as RustCancelInfo, QrVerification as InnerQr,
|
||||
QrVerificationState, Sas as InnerSas, SasState as RustSasState,
|
||||
Verification as InnerVerification, VerificationRequest as InnerVerificationRequest,
|
||||
CancelInfo as RustCancelInfo, QrVerification as InnerQr, QrVerificationState, Sas as InnerSas,
|
||||
SasState as RustSasState, Verification as InnerVerification,
|
||||
VerificationRequest as InnerVerificationRequest,
|
||||
VerificationRequestState as RustVerificationRequestState,
|
||||
matrix_sdk_qrcode::QrVerificationData,
|
||||
};
|
||||
use ruma::events::key::verification::VerificationMethod;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
description = "Helper macros to write FFI bindings"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma"]
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -27,18 +27,18 @@ pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
}
|
||||
} else if let Item::Impl(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let ImplItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
if let ImplItem::Fn(fun) = item
|
||||
&& fun.sig.asyncness.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else if let Item::Trait(blk) = &item {
|
||||
for item in &blk.items {
|
||||
if let TraitItem::Fn(fun) = item {
|
||||
if fun.sig.asyncness.is_some() {
|
||||
return true;
|
||||
}
|
||||
if let TraitItem::Fn(fun) = item
|
||||
&& fun.sig.asyncness.is_some()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Add `Client::set_avatar_url` to manually set the avatar URL of the user to a provided MXC one.
|
||||
- Allow setting a custom Sliding Sync connection ID and timeline limit on `RoomListService`.
|
||||
([#6289](https://github.com/matrix-org/matrix-rust-sdk/pull/6289))
|
||||
- Fix devices on Android 11 crashing because the SDK could not be initialized using `libloading`
|
||||
to get a reference to the JVM. Replaced `libloading` with `jvm-getter`, which works like a
|
||||
compatibility layer. ([#6370](https://github.com/matrix-org/matrix-rust-sdk/pull/6370))
|
||||
@@ -41,12 +44,37 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Features
|
||||
|
||||
- Add `Client::get_dm_rooms` function to get a list with the DMs for the provided user id.
|
||||
([#6487](https://github.com/matrix-org/matrix-rust-sdk/pull/6487))
|
||||
- Expose `ffi::NotificationRoomInfo::service_members` so clients can use the list of service
|
||||
members to calculate if a room is a DM from the notification info.
|
||||
([#6474](https://github.com/matrix-org/matrix-rust-sdk/pull/6474))
|
||||
- Enable `experimental-push-secrets` feature by default.
|
||||
([#6473](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
|
||||
- Add new high-level search helpers `RoomSearchIterator` and `GlobalSearchIterator` to perform
|
||||
searches for messages in a room or across all rooms.
|
||||
([6394](https://github.com/matrix-org/matrix-rust-sdk/pull/6394))
|
||||
- Added the `Client.request_openid_token()` method.
|
||||
([#6458](https://github.com/matrix-org/matrix-rust-sdk/pull/6458))
|
||||
- Added the `Client::import_secrets_bundle` method.
|
||||
([#6212](https://github.com/matrix-org/matrix-rust-sdk/pull/6212))
|
||||
- [**breaking**] Remove support for `native-tls` and remove all feature
|
||||
flags for selecting TLS backend, as `rustls` is the now the only supported
|
||||
TLS backend.
|
||||
([#6409](https://github.com/matrix-org/matrix-rust-sdk/pull/6409))
|
||||
- Expose `event_type_raw` and `latest_json()` on `EventTimelineItem`,
|
||||
allowing clients to access the raw event type string and full event JSON for
|
||||
custom event handling without pattern-matching through nested enums.
|
||||
([#6387](https://github.com/matrix-org/matrix-rust-sdk/pull/6387))
|
||||
([#6424](https://github.com/matrix-org/matrix-rust-sdk/pull/6424))
|
||||
- Expose sync v2 API through FFI via `Client.sync_v2()` and
|
||||
`Client.sync_once_v2()`, enabling mobile clients to sync without
|
||||
requiring Sliding Sync support on the homeserver. `Client.sync_v2()`
|
||||
accepts a `SyncListenerV2` callback that receives a `SyncResponseV2`
|
||||
after each successful sync.
|
||||
([#6359](https://github.com/matrix-org/matrix-rust-sdk/pull/6359))
|
||||
- Added `HomeserverCapabilities` and `Client::homeserver_capabilities()` to get the capabilities
|
||||
of the homeserver. ([#6371](https://github.com/matrix-org/matrix-rust-sdk/pull/6371))
|
||||
- Expose `Room.send_state_event_raw()` for sending arbitrary state events
|
||||
through the FFI layer.
|
||||
([#6350](https://github.com/matrix-org/matrix-rust-sdk/pull/6350))
|
||||
@@ -133,6 +161,19 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `Room::observe_live_location_shares` has been replaced by
|
||||
`Room::live_location_shares`. Call [`LiveLocationShares::subscribe`] on it to
|
||||
receive an initial snapshot and a stream of incremental updates.The stream is seeded from the event cache
|
||||
on creation and includes the own user's shares (previously excluded). `LiveLocationShare.is_live`
|
||||
has been removed; instead `ts` (start timestamp) and `timeout` (duration in milliseconds) are now
|
||||
exposed so clients can compute liveness themselves via `current_time < ts + timeout`. Non-live
|
||||
shares are automatically removed from the list. A new `LiveLocationShareListener` callback
|
||||
interface must be implemented and passed to the method.
|
||||
([#6385](https://github.com/matrix-org/matrix-rust-sdk/pull/6385))
|
||||
- [**breaking**] The `RoomAliases` variants of `StateEventContent`, `StateEventType` and
|
||||
`OtherState` was removed. This state event type was removed from the Matrix specification a while
|
||||
ago, and support for it has been removed in Ruma.
|
||||
([#6414](https://github.com/matrix-org/matrix-rust-sdk/pull/6414))
|
||||
- `Client::new` no longer unnecessarily instantiates an `OAuth` component if `CrossProcessLockConfig::SingleProcess`
|
||||
is used. ([#6293](https://github.com/matrix-org/matrix-rust-sdk/pull/6293))
|
||||
- [**breaking**] `Room::report_content()` no longer takes a `score` argument, because it was
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "matrix-sdk-ffi"
|
||||
version = "0.16.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ffi"]
|
||||
license = "Apache-2.0"
|
||||
@@ -24,7 +24,7 @@ crate-type = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "rustls-tls"]
|
||||
default = ["bundled-sqlite", "unstable-msc4274", "experimental-element-recent-emojis", "experimental-push-secrets"]
|
||||
# Use SQLite for the session storage.
|
||||
sqlite = ["matrix-sdk/sqlite"]
|
||||
# Use an embedded version of SQLite.
|
||||
@@ -34,14 +34,11 @@ indexeddb = ["matrix-sdk/indexeddb"]
|
||||
unstable-msc4274 = ["matrix-sdk-ui/unstable-msc4274"]
|
||||
# Required when targeting a Javascript environment, like Wasm in a browser.
|
||||
js = ["matrix-sdk-ui/js"]
|
||||
# Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
native-tls = ["matrix-sdk/native-tls", "sentry?/native-tls"]
|
||||
# Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
rustls-tls = ["matrix-sdk/rustls-tls", "sentry?/rustls"]
|
||||
# Enable sentry error monitoring, not compatible with Wasm platforms.
|
||||
sentry = ["dep:sentry", "dep:sentry-tracing"]
|
||||
|
||||
experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-emojis"]
|
||||
experimental-push-secrets = ["matrix-sdk/experimental-push-secrets"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -59,6 +56,7 @@ matrix-sdk = { workspace = true, features = [
|
||||
"socks",
|
||||
"uniffi",
|
||||
"federation-api",
|
||||
"experimental-search"
|
||||
] }
|
||||
matrix-sdk-base.workspace = true
|
||||
matrix-sdk-common.workspace = true
|
||||
|
||||
@@ -6,11 +6,6 @@ This uses [`uniffi`](https://mozilla.github.io/uniffi-rs/Overview.html) to build
|
||||
|
||||
Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
### Platform specific
|
||||
|
||||
- `rustls-tls`: Use Rustls as the TLS implementation, necessary on Android platforms.
|
||||
- `native-tls`: Use the TLS implementation provided by the host system, necessary on iOS and Wasm platforms.
|
||||
|
||||
### Functionality
|
||||
|
||||
- `sentry`: Enable error monitoring using Sentry, not supports on Wasm platforms.
|
||||
@@ -24,11 +19,11 @@ Given the number of platforms targeted, we have broken out a number of features
|
||||
|
||||
## Platforms
|
||||
|
||||
Each supported target should use features to select the relevant TLS system. Here are some suggested feature flags for the major platforms:
|
||||
Each supported target should use features to build the relevant system. Here are some suggested feature flags for the major platforms:
|
||||
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,rustls-tls,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,native-tls,sentry"`
|
||||
- JavaScript/Wasm: `"indexeddb,unstable-msc4274,native-tls"`
|
||||
- Android: `"bundled-sqlite,unstable-msc4274,sentry"`
|
||||
- iOS: `"bundled-sqlite,unstable-msc4274,sentry"`
|
||||
- JavaScript/Wasm: `"indexeddb,unstable-msc4274"`
|
||||
|
||||
### Swift/iOS sync
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ use std::{
|
||||
};
|
||||
|
||||
use matrix_sdk::{
|
||||
Error,
|
||||
authentication::oauth::{
|
||||
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
|
||||
error::OAuthAuthorizationCodeError,
|
||||
registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
|
||||
ClientId, ClientRegistrationData, OAuthError as SdkOAuthError,
|
||||
},
|
||||
Error,
|
||||
};
|
||||
use ruma::serde::Raw;
|
||||
use url::Url;
|
||||
@@ -101,6 +101,7 @@ impl SsoHandler {
|
||||
let builder =
|
||||
auth.login_with_sso_callback(url.into()).map_err(|_| SsoError::CallbackUrlInvalid)?;
|
||||
builder.await.map_err(|_| SsoError::LoginWithTokenFailed)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,45 +20,47 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context as _};
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use futures_util::pin_mut;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
#[cfg(feature = "sqlite")]
|
||||
use matrix_sdk::STATE_STORE_DATABASE_NAME;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle;
|
||||
use matrix_sdk::{
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
|
||||
authentication::oauth::{ClientId, OAuthAuthorizationData, OAuthError, OAuthSession},
|
||||
deserialized_responses::RawAnySyncOrStrippedTimelineEvent,
|
||||
executor::AbortOnDrop,
|
||||
media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings},
|
||||
ruma::{
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
api::client::{
|
||||
account::request_openid_token,
|
||||
discovery::{
|
||||
discover_homeserver::RtcFocusInfo,
|
||||
get_authorization_server_metadata::v1::Prompt as RumaOidcPrompt,
|
||||
},
|
||||
push::{EmailPusherData, PusherIds, PusherInit, PusherKind as RumaPusherKind},
|
||||
room::{create_room, Visibility},
|
||||
room::{Visibility, create_room},
|
||||
session::get_login_types,
|
||||
user_directory::search_users,
|
||||
},
|
||||
events::{
|
||||
AnyInitialStateEvent, InitialStateEvent,
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent, encryption::RoomEncryptionEventContent,
|
||||
message::MessageType,
|
||||
},
|
||||
AnyInitialStateEvent, InitialStateEvent,
|
||||
},
|
||||
serde::Raw,
|
||||
EventEncryptionAlgorithm, RoomId, TransactionId, UInt, UserId,
|
||||
},
|
||||
sliding_sync::Version as SdkSlidingSyncVersion,
|
||||
store::RoomLoadSettings as SdkRoomLoadSettings,
|
||||
sync::Notification,
|
||||
task_monitor::BackgroundTaskFailureReason,
|
||||
Account, AuthApi, AuthSession, Client as MatrixClient, Error, SessionChange, SessionTokens,
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
cross_process_lock::CrossProcessLockConfig, stream::StreamExt, SendOutsideWasm, SyncOutsideWasm,
|
||||
SendOutsideWasm, SyncOutsideWasm, cross_process_lock::CrossProcessLockConfig, stream::StreamExt,
|
||||
};
|
||||
use matrix_sdk_ui::{
|
||||
notification_client::{
|
||||
@@ -71,17 +73,23 @@ use matrix_sdk_ui::{
|
||||
use mime::Mime;
|
||||
use oauth2::Scope;
|
||||
use ruma::{
|
||||
api::client::{
|
||||
alias::get_alias,
|
||||
discovery::get_authorization_server_metadata::v1::{
|
||||
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
|
||||
OwnedDeviceId, OwnedMxcUri, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
api::{
|
||||
client::{
|
||||
alias::get_alias,
|
||||
discovery::get_authorization_server_metadata::v1::{
|
||||
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
|
||||
},
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
room::create_room::{RoomPowerLevelsContentOverride, v3::CreationContent},
|
||||
uiaa::{EmailUserIdentifier, UserIdentifier},
|
||||
},
|
||||
error::ErrorKind,
|
||||
profile::{AvatarUrl, DisplayName},
|
||||
room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
events::{
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
@@ -100,25 +108,22 @@ use ruma::{
|
||||
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
|
||||
},
|
||||
tag::TagEventContent,
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
},
|
||||
push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat},
|
||||
room::RoomType,
|
||||
OwnedDeviceId, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
|
||||
use super::{
|
||||
room::{room_info::RoomInfo, Room},
|
||||
room::{Room, room_info::RoomInfo},
|
||||
session_verification::SessionVerificationController,
|
||||
};
|
||||
use crate::{
|
||||
ClientError,
|
||||
authentication::{HomeserverLoginDetails, OidcConfiguration, OidcError, SsoError, SsoHandler},
|
||||
client,
|
||||
encryption::Encryption,
|
||||
@@ -142,7 +147,6 @@ use crate::{
|
||||
task_handle::TaskHandle,
|
||||
utd::{UnableToDecryptDelegate, UtdHook},
|
||||
utils::AsyncRuntimeDropped,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -313,6 +317,28 @@ impl From<matrix_sdk::TransmissionProgress> for TransmissionProgress {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct OpenIdToken {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
pub matrix_server_name: String,
|
||||
pub expires_in_seconds: u64,
|
||||
}
|
||||
|
||||
impl From<request_openid_token::v3::Response> for OpenIdToken {
|
||||
fn from(value: request_openid_token::v3::Response) -> Self {
|
||||
Self {
|
||||
access_token: value.access_token,
|
||||
token_type: serde_json::to_value(&value.token_type)
|
||||
.ok()
|
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_else(|| format!("{:?}", value.token_type)),
|
||||
matrix_server_name: value.matrix_server_name.to_string(),
|
||||
expires_in_seconds: value.expires_in.as_secs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegateData {
|
||||
/// The delegate itself, that will receive the callbacks.
|
||||
delegate: Arc<dyn ClientDelegate>,
|
||||
@@ -365,12 +391,12 @@ impl Client {
|
||||
|
||||
let controller = session_verification_controller.clone();
|
||||
sdk_client.add_event_handler(move |event: OriginalSyncRoomMessageEvent| async move {
|
||||
if let MessageType::VerificationRequest(_) = &event.content.msgtype {
|
||||
if let Some(session_verification_controller) = &*controller.clone().read().await {
|
||||
session_verification_controller
|
||||
.process_incoming_verification_request(&event.sender, event.event_id)
|
||||
.await;
|
||||
}
|
||||
if let MessageType::VerificationRequest(_) = &event.content.msgtype
|
||||
&& let Some(session_verification_controller) = &*controller.clone().read().await
|
||||
{
|
||||
session_verification_controller
|
||||
.process_incoming_verification_request(&event.sender, event.event_id)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -488,13 +514,17 @@ impl Client {
|
||||
device_id: Option<String>,
|
||||
) -> Result<(), ClientError> {
|
||||
let mut builder = self.inner.matrix_auth().login_username(&username, &password);
|
||||
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
}
|
||||
|
||||
if let Some(device_id) = device_id.as_ref() {
|
||||
builder = builder.device_id(device_id);
|
||||
}
|
||||
|
||||
builder.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -520,6 +550,7 @@ impl Client {
|
||||
}
|
||||
|
||||
builder.send().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -534,7 +565,7 @@ impl Client {
|
||||
let mut builder = self
|
||||
.inner
|
||||
.matrix_auth()
|
||||
.login_identifier(UserIdentifier::Email { address: email }, &password);
|
||||
.login_identifier(UserIdentifier::Email(EmailUserIdentifier::new(email)), &password);
|
||||
|
||||
if let Some(initial_device_name) = initial_device_name.as_ref() {
|
||||
builder = builder.initial_device_display_name(initial_device_name);
|
||||
@@ -951,139 +982,7 @@ impl Client {
|
||||
let listener = Arc::new(listener);
|
||||
self.inner
|
||||
.register_notification_handler(move |notification, room, _client| {
|
||||
let listener = listener.clone();
|
||||
let room_id = room.room_id().to_string();
|
||||
|
||||
async move {
|
||||
// Extract information about the actions
|
||||
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
|
||||
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
|
||||
|
||||
// Convert SDK actions to FFI type
|
||||
let actions: Vec<crate::notification_settings::Action> = notification
|
||||
.actions
|
||||
.into_iter()
|
||||
.filter_map(|action| action.try_into().ok())
|
||||
.collect();
|
||||
|
||||
// Convert SDK event to FFI type
|
||||
let (sender, event, thread_id, raw_event) = match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let thread_id = match &deserialized {
|
||||
AnySyncTimelineEvent::MessageLike(event) => {
|
||||
match event.original_content() {
|
||||
Some(AnyMessageLikeEventContent::RoomMessage(
|
||||
content,
|
||||
)) => match content.relates_to {
|
||||
Some(Relation::Thread(thread)) => {
|
||||
Some(thread.event_id.to_string())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let event = NotificationEvent::Timeline {
|
||||
event: Arc::new(crate::event::TimelineEvent(Box::new(
|
||||
deserialized,
|
||||
))),
|
||||
};
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to deserialize timeline event: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let event =
|
||||
NotificationEvent::Invite { sender: sender.to_string() };
|
||||
let thread_id = None;
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to deserialize stripped state event: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Compile sender info
|
||||
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
|
||||
let sender_info = if let Some(sender) = sender.as_ref() {
|
||||
NotificationSenderInfo {
|
||||
display_name: sender.display_name().map(|name| name.to_owned()),
|
||||
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
|
||||
is_name_ambiguous: sender.name_ambiguous(),
|
||||
}
|
||||
} else {
|
||||
NotificationSenderInfo {
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
is_name_ambiguous: false,
|
||||
}
|
||||
};
|
||||
|
||||
// Compile room info
|
||||
let display_name = match room.display_name().await {
|
||||
Ok(name) => name.to_string(),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to calculate the room's display name: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let is_direct = match room.is_direct().await {
|
||||
Ok(is_direct) => is_direct,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to determine if room is direct or not: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let room_info = NotificationRoomInfo {
|
||||
display_name,
|
||||
avatar_url: room.avatar_url().map(Into::into),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
topic: room.topic(),
|
||||
join_rule: room
|
||||
.join_rule()
|
||||
.map(TryInto::try_into)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
is_encrypted: Some(room.encryption_state().is_encrypted()),
|
||||
is_direct,
|
||||
is_space: room.is_space(),
|
||||
};
|
||||
|
||||
listener.on_notification(
|
||||
NotificationItem {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
has_mention: Some(has_mention),
|
||||
thread_id,
|
||||
actions: Some(actions),
|
||||
},
|
||||
room_id,
|
||||
);
|
||||
}
|
||||
notification_handler(notification, room, listener.clone())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
@@ -1125,10 +1024,7 @@ impl Client {
|
||||
pub async fn reset_well_known(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.reset_well_known().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Retrieves a media file from the media source
|
||||
///
|
||||
/// Not available on Wasm platforms, due to lack of accessible file system.
|
||||
@@ -1195,10 +1091,7 @@ impl Client {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// The sliding sync version.
|
||||
pub fn sliding_sync_version(&self) -> SlidingSyncVersion {
|
||||
self.inner.sliding_sync_version().into()
|
||||
@@ -1345,12 +1238,28 @@ impl Client {
|
||||
Ok(display_name)
|
||||
}
|
||||
|
||||
pub async fn request_openid_token(&self) -> Result<OpenIdToken, ClientError> {
|
||||
Ok(self.inner.account().request_openid_token().await?.into())
|
||||
}
|
||||
|
||||
pub async fn upload_avatar(&self, mime_type: String, data: Vec<u8>) -> Result<(), ClientError> {
|
||||
let mime: Mime = mime_type.parse()?;
|
||||
self.inner.account().upload_avatar(&mime, data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the user's avatar using the provided MXC url.
|
||||
pub async fn set_avatar_url(&self, url: String) -> Result<(), ClientError> {
|
||||
// MxcUri can't just be instantiated, serde deserialization seems to be the only
|
||||
// way
|
||||
let mxc = serde_json::from_str::<OwnedMxcUri>(&url)?;
|
||||
// Validate the newly generated MxcUri
|
||||
mxc.validate().map_err(ClientError::from_err)?;
|
||||
|
||||
self.inner.account().set_avatar_url(Some(&mxc)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_avatar(&self) -> Result<(), ClientError> {
|
||||
self.inner.account().set_avatar_url(None).await?;
|
||||
Ok(())
|
||||
@@ -1574,6 +1483,7 @@ impl Client {
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
/// Get the first existing DM room with the given user, if any.
|
||||
pub fn get_dm_room(&self, user_id: String) -> Result<Option<Arc<Room>>, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let sdk_room = self.inner.get_dm_room(&user_id);
|
||||
@@ -1582,6 +1492,16 @@ impl Client {
|
||||
Ok(dm)
|
||||
}
|
||||
|
||||
/// Get an iterator with the existing DM rooms for the given user.
|
||||
pub fn get_dm_rooms(&self, user_id: String) -> Result<Vec<Arc<Room>>, ClientError> {
|
||||
let user_id = UserId::parse(user_id)?;
|
||||
let sdk_rooms = self.inner.get_dm_rooms(&user_id);
|
||||
let dms = sdk_rooms
|
||||
.map(|room| Arc::new(Room::new(room, self.utd_hook_manager.get().cloned())))
|
||||
.collect();
|
||||
Ok(dms)
|
||||
}
|
||||
|
||||
pub async fn search_users(
|
||||
&self,
|
||||
search_term: String,
|
||||
@@ -2130,10 +2050,10 @@ impl Client {
|
||||
let room_id = RoomId::parse(room_id)?;
|
||||
|
||||
// Emit the initial event, if present
|
||||
if let Some(room) = self.inner.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
if let Some(room) = self.inner.get_room(&room_id)
|
||||
&& let Ok(room_info) = RoomInfo::new(&room).await
|
||||
{
|
||||
listener.call(room_info);
|
||||
}
|
||||
|
||||
Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn({
|
||||
@@ -2145,15 +2065,150 @@ impl Client {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(room) = client.get_room(&room_id) {
|
||||
if let Ok(room_info) = RoomInfo::new(&room).await {
|
||||
listener.call(room_info);
|
||||
}
|
||||
if let Some(room) = client.get_room(&room_id)
|
||||
&& let Ok(room_info) = RoomInfo::new(&room).await
|
||||
{
|
||||
listener.call(room_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Whether to enable automatic backpagination under certain conditions
|
||||
/// (e.g. when processing read receipts).
|
||||
///
|
||||
/// This is an experimental feature, and might cause performance issues on
|
||||
/// large accounts. Use with caution.
|
||||
///
|
||||
/// This must be called after creating a client, but before subscribing to
|
||||
/// the event cache (so, before spawning a sync service or a timeline).
|
||||
pub fn enable_automatic_backpagination(&self) {
|
||||
self.inner.event_cache().config_mut().experimental_auto_backpagination = true;
|
||||
}
|
||||
|
||||
pub fn homeserver_capabilities(&self) -> HomeserverCapabilities {
|
||||
HomeserverCapabilities::new(self.inner.homeserver_capabilities())
|
||||
}
|
||||
}
|
||||
|
||||
async fn notification_handler(
|
||||
notification: Notification,
|
||||
room: matrix_sdk::Room,
|
||||
listener: Arc<Box<dyn SyncNotificationListener>>,
|
||||
) {
|
||||
let room_id = room.room_id().to_string();
|
||||
|
||||
// Extract information about the actions
|
||||
let is_noisy = notification.actions.iter().any(|a| a.sound().is_some());
|
||||
let has_mention = notification.actions.iter().any(|a| a.is_highlight());
|
||||
|
||||
// Convert SDK actions to FFI type
|
||||
let actions: Vec<crate::notification_settings::Action> =
|
||||
notification.actions.into_iter().filter_map(|action| action.try_into().ok()).collect();
|
||||
|
||||
// Convert SDK event to FFI type
|
||||
let (sender, event, thread_id, raw_event) = match notification.event {
|
||||
RawAnySyncOrStrippedTimelineEvent::Sync(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let thread_id = if let AnySyncTimelineEvent::MessageLike(event) = &deserialized
|
||||
&& let Some(AnyMessageLikeEventContent::RoomMessage(content)) =
|
||||
event.original_content()
|
||||
&& let Some(Relation::Thread(thread)) = content.relates_to
|
||||
{
|
||||
Some(thread.event_id.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let event = NotificationEvent::Timeline {
|
||||
event: Arc::new(crate::event::TimelineEvent(Box::new(deserialized))),
|
||||
};
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to deserialize timeline event: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
RawAnySyncOrStrippedTimelineEvent::Stripped(raw) => {
|
||||
let raw_event = raw.json().get().to_owned();
|
||||
match raw.deserialize() {
|
||||
Ok(deserialized) => {
|
||||
let sender = deserialized.sender().to_owned();
|
||||
let event = NotificationEvent::Invite { sender: sender.to_string() };
|
||||
let thread_id = None;
|
||||
(sender, event, thread_id, raw_event)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to deserialize stripped state event: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Compile sender info
|
||||
let sender = room.get_member_no_sync(&sender).await.ok().flatten();
|
||||
let sender_info = if let Some(sender) = sender.as_ref() {
|
||||
NotificationSenderInfo {
|
||||
display_name: sender.display_name().map(|name| name.to_owned()),
|
||||
avatar_url: sender.avatar_url().map(|uri| uri.to_string()),
|
||||
is_name_ambiguous: sender.name_ambiguous(),
|
||||
}
|
||||
} else {
|
||||
NotificationSenderInfo { display_name: None, avatar_url: None, is_name_ambiguous: false }
|
||||
};
|
||||
|
||||
// Compile room info
|
||||
let display_name = match room.display_name().await {
|
||||
Ok(name) => name.to_string(),
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to calculate the room's display name: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let is_direct = match room.is_direct().await {
|
||||
Ok(is_direct) => is_direct,
|
||||
Err(err) => {
|
||||
tracing::warn!("Failed to determine if room is direct or not: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let room_info = NotificationRoomInfo {
|
||||
display_name,
|
||||
avatar_url: room.avatar_url().map(Into::into),
|
||||
canonical_alias: room.canonical_alias().map(Into::into),
|
||||
topic: room.topic(),
|
||||
join_rule: room.join_rule().map(TryInto::try_into).transpose().ok().flatten(),
|
||||
joined_members_count: room.joined_members_count(),
|
||||
service_members: room
|
||||
.service_members()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
is_encrypted: Some(room.encryption_state().is_encrypted()),
|
||||
is_direct,
|
||||
is_space: room.is_space(),
|
||||
};
|
||||
|
||||
listener.on_notification(
|
||||
NotificationItem {
|
||||
event,
|
||||
raw_event,
|
||||
sender_info,
|
||||
room_info,
|
||||
is_noisy: Some(is_noisy),
|
||||
has_mention: Some(has_mention),
|
||||
thread_id,
|
||||
actions: Some(actions),
|
||||
},
|
||||
room_id,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-element-recent-emojis")]
|
||||
@@ -3039,16 +3094,88 @@ impl From<matrix_sdk::StoreSizes> for StoreSizes {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct HomeserverCapabilities {
|
||||
inner: matrix_sdk::HomeserverCapabilities,
|
||||
}
|
||||
|
||||
impl HomeserverCapabilities {
|
||||
pub(crate) fn new(capabilities: matrix_sdk::HomeserverCapabilities) -> Self {
|
||||
Self { inner: capabilities }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl HomeserverCapabilities {
|
||||
pub async fn refresh(&self) -> Result<(), ClientError> {
|
||||
Ok(self.inner.refresh().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_password(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_password().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_displayname(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_displayname().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_avatar(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_avatar().await?)
|
||||
}
|
||||
|
||||
pub async fn can_change_thirdparty_ids(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_change_thirdparty_ids().await?)
|
||||
}
|
||||
|
||||
pub async fn can_get_login_token(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.can_get_login_token().await?)
|
||||
}
|
||||
|
||||
pub async fn extended_profile_fields(&self) -> Result<ExtendedProfileFields, ClientError> {
|
||||
let profile_fields = self.inner.extended_profile_fields().await?;
|
||||
Ok(ExtendedProfileFields {
|
||||
enabled: profile_fields.enabled,
|
||||
allowed: profile_fields
|
||||
.allowed
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
disallowed: profile_fields
|
||||
.disallowed
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn forgets_room_when_leaving(&self) -> Result<bool, ClientError> {
|
||||
Ok(self.inner.forgets_room_when_leaving().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct ExtendedProfileFields {
|
||||
pub enabled: bool,
|
||||
pub allowed: Vec<String>,
|
||||
pub disallowed: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use ruma::{
|
||||
api::client::room::{create_room, Visibility},
|
||||
ServerName,
|
||||
api::client::room::{Visibility, create_room},
|
||||
authentication::TokenType,
|
||||
events::StateEventType,
|
||||
room::RoomType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
client::{CreateRoomParameters, JoinRule, RoomPreset, RoomVisibility},
|
||||
client::{CreateRoomParameters, JoinRule, OpenIdToken, RoomPreset, RoomVisibility},
|
||||
room::RoomHistoryVisibility,
|
||||
};
|
||||
|
||||
@@ -3087,9 +3214,9 @@ mod tests {
|
||||
assert_eq!(request.invite.len(), 1);
|
||||
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomAvatar));
|
||||
assert!(initial_state.iter().any(|e| e.event_type() == StateEventType::RoomJoinRules));
|
||||
assert!(initial_state
|
||||
.iter()
|
||||
.any(|e| e.event_type() == StateEventType::RoomHistoryVisibility));
|
||||
assert!(
|
||||
initial_state.iter().any(|e| e.event_type() == StateEventType::RoomHistoryVisibility)
|
||||
);
|
||||
assert_eq!(request.room_alias_name, Some("#a-room:example.com".to_owned()));
|
||||
|
||||
let room_type = request
|
||||
@@ -3100,4 +3227,21 @@ mod tests {
|
||||
.room_type;
|
||||
assert_eq!(room_type, Some(RoomType::Space));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_openid_token_mapping() {
|
||||
let response = ruma::api::client::account::request_openid_token::v3::Response::new(
|
||||
"open-id-token".to_owned(),
|
||||
TokenType::Bearer,
|
||||
ServerName::parse("example.com").expect("valid server name"),
|
||||
Duration::from_secs(3_600),
|
||||
);
|
||||
|
||||
let token: OpenIdToken = response.into();
|
||||
|
||||
assert_eq!(token.access_token, "open-id-token");
|
||||
assert_eq!(token.token_type, "Bearer");
|
||||
assert_eq!(token.matrix_server_name, "example.com");
|
||||
assert_eq!(token.expires_in_seconds, 3_600);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,21 +15,22 @@
|
||||
// Allow UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{num::NonZeroUsize, sync::Arc, time::Duration};
|
||||
use std::{fs, num::NonZeroUsize, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
#[cfg(not(any(target_family = "wasm", target_os = "android")))]
|
||||
use matrix_sdk::reqwest::Certificate;
|
||||
use matrix_sdk::{
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, ThreadingSupport,
|
||||
cross_process_lock::CrossProcessLockConfig as SdkCrossProcessLockConfig,
|
||||
encryption::{BackupDownloadStrategy, EncryptionSettings},
|
||||
event_cache::EventCacheError,
|
||||
ruma::{ServerName, UserId},
|
||||
search_index::SearchIndexStoreKind,
|
||||
sliding_sync::{
|
||||
Error as MatrixSlidingSyncError, VersionBuilder as MatrixSlidingSyncVersionBuilder,
|
||||
VersionBuilderError,
|
||||
},
|
||||
Client as MatrixClient, ClientBuildError as MatrixClientBuildError, HttpError, IdParseError,
|
||||
RumaApiError, ThreadingSupport,
|
||||
};
|
||||
use matrix_sdk_base::crypto::{CollectStrategy, DecryptionSettings, TrustRequirement};
|
||||
use ruma::api::error::{DeserializationError, FromHttpResponseError};
|
||||
@@ -137,6 +138,7 @@ pub struct ClientBuilder {
|
||||
decryption_settings: DecryptionSettings,
|
||||
enable_share_history_on_invite: bool,
|
||||
request_config: Option<RequestConfig>,
|
||||
search_index_store: Option<SearchIndexStoreKind>,
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
user_agent: Option<String>,
|
||||
@@ -193,6 +195,7 @@ impl ClientBuilder {
|
||||
enable_share_history_on_invite: false,
|
||||
request_config: Default::default(),
|
||||
threading_support: ThreadingSupport::Disabled,
|
||||
search_index_store: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -357,6 +360,37 @@ impl ClientBuilder {
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
/// Set up the search index store for this client, which is used to store
|
||||
/// the message search index locally.
|
||||
///
|
||||
/// As soon as this is enabled, messages will start to be indexed, and can
|
||||
/// be later queried for search.
|
||||
///
|
||||
/// `path` is the directory where the search index will be stored. It must
|
||||
/// be unique per session.
|
||||
///
|
||||
/// `password` is an optional password to encrypt the search index at rest.
|
||||
/// If `None`, the search index will be stored unencrypted.
|
||||
pub fn with_search_index_store(
|
||||
self: Arc<Self>,
|
||||
path: String,
|
||||
password: Option<String>,
|
||||
) -> Arc<Self> {
|
||||
let mut builder = unwrap_or_clone_arc(self);
|
||||
|
||||
// Note: creation of the path is deferred to later.
|
||||
let path = PathBuf::from(path);
|
||||
|
||||
let kind = if let Some(password) = password {
|
||||
SearchIndexStoreKind::EncryptedDirectory(path, password)
|
||||
} else {
|
||||
SearchIndexStoreKind::UnencryptedDirectory(path)
|
||||
};
|
||||
|
||||
builder.search_index_store = Some(kind);
|
||||
Arc::new(builder)
|
||||
}
|
||||
|
||||
pub async fn build(self: Arc<Self>) -> Result<Arc<Client>, ClientBuildError> {
|
||||
let builder = unwrap_or_clone_arc(self);
|
||||
let mut inner_builder = MatrixClient::builder()
|
||||
@@ -385,6 +419,20 @@ impl ClientBuilder {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(search_index_store) = builder.search_index_store {
|
||||
// Create the search index directory.
|
||||
match search_index_store {
|
||||
SearchIndexStoreKind::UnencryptedDirectory(ref path)
|
||||
| SearchIndexStoreKind::EncryptedDirectory(ref path, _) => {
|
||||
fs::create_dir_all(path)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Configure the inner builder to use the search index store.
|
||||
inner_builder = inner_builder.search_index_store(search_index_store);
|
||||
}
|
||||
|
||||
// Determine server either from URL, server name or user ID.
|
||||
inner_builder = match builder.homeserver_cfg {
|
||||
Some(HomeserverConfig::Url(url)) => inner_builder.homeserver_url(url),
|
||||
@@ -490,12 +538,11 @@ impl ClientBuilder {
|
||||
updated_config = updated_config.timeout(Duration::from_millis(timeout));
|
||||
}
|
||||
updated_config = updated_config.read_timeout(DEFAULT_READ_TIMEOUT);
|
||||
if let Some(max_concurrent_requests) = config.max_concurrent_requests {
|
||||
if max_concurrent_requests > 0 {
|
||||
updated_config = updated_config.max_concurrent_requests(NonZeroUsize::new(
|
||||
max_concurrent_requests as usize,
|
||||
));
|
||||
}
|
||||
if let Some(max_concurrent_requests) = config.max_concurrent_requests
|
||||
&& max_concurrent_requests > 0
|
||||
{
|
||||
updated_config = updated_config
|
||||
.max_concurrent_requests(NonZeroUsize::new(max_concurrent_requests as usize));
|
||||
}
|
||||
if let Some(max_retry_time) = config.max_retry_time {
|
||||
updated_config =
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
encryption,
|
||||
encryption::{backups, recovery},
|
||||
};
|
||||
use matrix_sdk::encryption::{self, backups, recovery};
|
||||
use matrix_sdk_base::crypto::types::{BackupSecrets, RoomKeyBackupInfo};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::OwnedUserId;
|
||||
use serde::de::Error;
|
||||
use thiserror::Error;
|
||||
use tracing::{error, info};
|
||||
use zeroize::Zeroize;
|
||||
@@ -238,6 +238,225 @@ impl From<encryption::VerificationState> for VerificationState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct containing the bundle of secrets to fully activate a new device for
|
||||
/// end-to-end encryption.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct SecretsBundleWithUserId {
|
||||
user_id: OwnedUserId,
|
||||
inner: matrix_sdk_base::crypto::types::SecretsBundle,
|
||||
}
|
||||
|
||||
/// Result for the check if a store has a valid secrets bundle.
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum DetectedSecretsBundle {
|
||||
/// The store doesn't contain a secrets bundle at all.
|
||||
None,
|
||||
/// The store contains a bundle without a backup.
|
||||
WithoutBackup,
|
||||
/// The store contains a bundle with an unused backup, the backup key in the
|
||||
/// bundle isn't used on the homeserver.
|
||||
UnusedBackup,
|
||||
/// The store contains a complete secrets bundle.
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// Error type describing failures that can happen while exporting a
|
||||
/// [`SecretsBundle`] from a SQLite store.
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
pub enum BundleExportError {
|
||||
/// The SQLite store couldn't be opened.
|
||||
#[error("the store couldn't be opened: {msg}")]
|
||||
OpenStoreError { msg: String },
|
||||
/// Data from the SQLite store couldn't be exported.
|
||||
#[error("the bundle couldn't be exported due to a storage error: {msg}")]
|
||||
StoreError { msg: String },
|
||||
/// The store doesn't contain a secrets bundle or it couldn't be read from
|
||||
/// the store.
|
||||
#[error("the bundle couldn't be exported: {msg}")]
|
||||
SecretError { msg: String },
|
||||
/// The store is empty and doesn't contain a secrets bundle.
|
||||
#[error("the store is completely empty")]
|
||||
StoreEmpty,
|
||||
/// A JSON object couldn't be deserialized while the secrets bundle was
|
||||
/// exported.
|
||||
#[error("Couldn't deserialize a JSON value: {msg}")]
|
||||
Json { msg: String },
|
||||
/// Error returned when the secrets bundle is missing a backup key or
|
||||
/// includes one that doesn’t match the key configured for the active backup
|
||||
/// version.
|
||||
#[error(
|
||||
"The bundle is missing a backup key or has one that isn't the one that's currently used"
|
||||
)]
|
||||
InvalidBackup,
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
impl From<matrix_sdk::encryption::BundleExportError> for BundleExportError {
|
||||
fn from(value: matrix_sdk::encryption::BundleExportError) -> Self {
|
||||
match value {
|
||||
matrix_sdk::encryption::BundleExportError::OpenStoreError(e) => {
|
||||
BundleExportError::OpenStoreError { msg: e.to_string() }
|
||||
}
|
||||
matrix_sdk::encryption::BundleExportError::StoreError(e) => {
|
||||
BundleExportError::StoreError { msg: e.to_string() }
|
||||
}
|
||||
matrix_sdk::encryption::BundleExportError::SecretExport(e) => {
|
||||
BundleExportError::SecretError { msg: e.to_string() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for BundleExportError {
|
||||
fn from(value: serde_json::Error) -> Self {
|
||||
Self::Json { msg: value.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SecretsBundleWithUserId {
|
||||
/// Attempt to export a [`SecretsBundle`] from a crypto store.
|
||||
///
|
||||
/// This method can be used to retrieve a [`SecretsBundle`] from an existing
|
||||
/// `matrix-sdk`-based client in order to import the [`SecretsBundle`] in
|
||||
/// another [`Client`] instance.
|
||||
///
|
||||
/// This can be useful for migration purposes or to allow existing client
|
||||
/// instances create new ones that will be fully verified.
|
||||
#[uniffi::constructor]
|
||||
pub async fn from_database(
|
||||
database_path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
backup_info: &str,
|
||||
) -> Result<Arc<Self>, BundleExportError> {
|
||||
let backup_info = serde_json::from_str(backup_info)?;
|
||||
|
||||
let ret = if let Some((user_id, bundle)) =
|
||||
matrix_sdk::encryption::export_secrets_bundle_from_store(
|
||||
database_path,
|
||||
passphrase.as_deref(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let is_backup_ok =
|
||||
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
|
||||
|
||||
if is_backup_ok {
|
||||
Ok(SecretsBundleWithUserId { user_id, inner: bundle }.into())
|
||||
} else {
|
||||
Err(BundleExportError::InvalidBackup)
|
||||
}
|
||||
} else {
|
||||
Err(BundleExportError::StoreEmpty)
|
||||
};
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl SecretsBundleWithUserId {
|
||||
/// Attempt to create a [`SecretsBundle`] from a previously JSON serialized
|
||||
/// bundle.
|
||||
#[uniffi::constructor]
|
||||
pub fn from_str(
|
||||
user_id: &str,
|
||||
bundle: &str,
|
||||
backup_info: &str,
|
||||
) -> Result<Arc<Self>, BundleExportError> {
|
||||
let user_id =
|
||||
OwnedUserId::from_str(user_id).map_err(|e| serde_json::Error::custom(e.to_string()))?;
|
||||
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
|
||||
let backup_info = serde_json::from_str(backup_info)?;
|
||||
|
||||
let is_backup_ok =
|
||||
bundle.backup.as_ref().is_some_and(|backup| is_valid_backup(backup, &backup_info));
|
||||
|
||||
if is_backup_ok {
|
||||
Ok(Self { user_id, inner: bundle }.into())
|
||||
} else {
|
||||
Err(BundleExportError::InvalidBackup)
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the bundle contain a backup key.
|
||||
///
|
||||
/// Since enabling a backup is optional, the backup key might be missing
|
||||
/// from the bundle. Returns `false` if the backup key is missing,
|
||||
/// otherwise `true`.
|
||||
pub fn contains_backup_key(&self) -> bool {
|
||||
self.inner.backup.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_backup(secrets: &BackupSecrets, info: &RoomKeyBackupInfo) -> bool {
|
||||
match secrets {
|
||||
BackupSecrets::MegolmBackupV1Curve25519AesSha2(secrets) => {
|
||||
secrets.key.backup_key_matches(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_bundle_and_info(
|
||||
bundle: &matrix_sdk_base::crypto::types::SecretsBundle,
|
||||
info: Option<&RoomKeyBackupInfo>,
|
||||
) -> DetectedSecretsBundle {
|
||||
match (&bundle.backup, info) {
|
||||
(None, None) => DetectedSecretsBundle::WithoutBackup,
|
||||
(None, Some(_)) => DetectedSecretsBundle::WithoutBackup,
|
||||
(Some(_), None) => DetectedSecretsBundle::UnusedBackup,
|
||||
(Some(backup), Some(info)) => {
|
||||
if is_valid_backup(backup, info) {
|
||||
DetectedSecretsBundle::Complete
|
||||
} else {
|
||||
DetectedSecretsBundle::UnusedBackup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a JSON encoded string contains a valid [`SecretsBundle`].
|
||||
#[uniffi::export]
|
||||
pub fn json_string_contains_secrets_bundle(
|
||||
bundle: &str,
|
||||
backup_info: Option<String>,
|
||||
) -> Result<DetectedSecretsBundle, ClientError> {
|
||||
let info: Option<RoomKeyBackupInfo> =
|
||||
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
|
||||
|
||||
let bundle: matrix_sdk_base::crypto::types::SecretsBundle = serde_json::from_str(bundle)?;
|
||||
|
||||
Ok(check_bundle_and_info(&bundle, info.as_ref()))
|
||||
}
|
||||
|
||||
/// Check if a crypto store contains a valid [`SecretsBundle`].
|
||||
#[cfg(feature = "sqlite")]
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
pub async fn database_contains_secrets_bundle(
|
||||
database_path: &str,
|
||||
mut passphrase: Option<String>,
|
||||
backup_info: Option<String>,
|
||||
) -> Result<DetectedSecretsBundle, BundleExportError> {
|
||||
let info: Option<RoomKeyBackupInfo> =
|
||||
backup_info.map(|info| serde_json::from_str(&info)).transpose()?;
|
||||
|
||||
let maybe_bundle = matrix_sdk::encryption::export_secrets_bundle_from_store(
|
||||
database_path,
|
||||
passphrase.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
passphrase.zeroize();
|
||||
|
||||
Ok(match maybe_bundle {
|
||||
Some((_, bundle)) => check_bundle_and_info(&bundle, info.as_ref()),
|
||||
None => DetectedSecretsBundle::None,
|
||||
})
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Encryption {
|
||||
/// Get the public ed25519 key of our own device. This is usually what is
|
||||
@@ -505,6 +724,43 @@ impl Encryption {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// This method will import all the private cross-signing keys and
|
||||
/// the private part of a backup key and its accompanying version into the
|
||||
/// store.
|
||||
///
|
||||
/// Importing all the secrets will mark the device as verified and enable
|
||||
/// backups.
|
||||
///
|
||||
/// **Warning**: Only import this from a trusted source, i.e. if an existing
|
||||
/// device is sharing this with a new device.
|
||||
///
|
||||
/// **Warning*: Only call this method right after logging in and before the
|
||||
/// initial sync has been started.
|
||||
pub async fn import_secrets_bundle(
|
||||
&self,
|
||||
secrets_bundle: &SecretsBundleWithUserId,
|
||||
) -> Result<(), ClientError> {
|
||||
let user_id = self._client.inner.user_id().expect(
|
||||
"We should have a user ID available now, this is only called once we're logged in",
|
||||
);
|
||||
|
||||
if user_id == secrets_bundle.user_id {
|
||||
self.inner
|
||||
.import_secrets_bundle(&secrets_bundle.inner)
|
||||
.await
|
||||
.map_err(ClientError::from_err)?;
|
||||
|
||||
self.inner.wait_for_e2ee_initialization_tasks().await;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ClientError::Generic {
|
||||
msg: "Secrets bundle does not belong to the user which was logged in".to_owned(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The E2EE identity of a user.
|
||||
|
||||
@@ -15,21 +15,20 @@
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
authentication::oauth::OAuthError,
|
||||
encryption::{identities::RequestVerificationError, CryptoStoreError},
|
||||
encryption::{CryptoStoreError, identities::RequestVerificationError},
|
||||
event_cache::EventCacheError,
|
||||
reqwest,
|
||||
room::{calls::CallError, edit::EditError},
|
||||
send_queue::RoomSendQueueError,
|
||||
HttpError, IdParseError, NotificationSettingsError as SdkNotificationSettingsError,
|
||||
QueueWedgeError as SdkQueueWedgeError, StoreError,
|
||||
};
|
||||
use matrix_sdk_ui::{encryption_sync_service, notification_client, spaces, sync_service, timeline};
|
||||
use ruma::{
|
||||
api::client::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
api::error::{ErrorBody, ErrorKind as RumaApiErrorKind, RetryAfter, StandardErrorBody},
|
||||
};
|
||||
use tracing::warn;
|
||||
use uniffi::UnexpectedUniFFICallbackError;
|
||||
|
||||
use crate::{room_list::RoomListError, timeline::FocusEventError};
|
||||
@@ -44,7 +43,6 @@ pub enum ClientError {
|
||||
|
||||
impl ClientError {
|
||||
pub(crate) fn from_str<E: Display>(error: E, details: Option<String>) -> Self {
|
||||
warn!("Error: {error}");
|
||||
Self::Generic { msg: error.to_string(), details }
|
||||
}
|
||||
|
||||
@@ -77,22 +75,21 @@ impl From<matrix_sdk::Error> for ClientError {
|
||||
fn from(e: matrix_sdk::Error) -> Self {
|
||||
match e {
|
||||
matrix_sdk::Error::Http(http_error) => {
|
||||
if let Some(api_error) = http_error.as_client_api_error() {
|
||||
if let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
|
||||
if let Some(api_error) = http_error.as_client_api_error()
|
||||
&& let ErrorBody::Standard(StandardErrorBody { kind, message, .. }) =
|
||||
&api_error.body
|
||||
{
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return (*http_error).into();
|
||||
};
|
||||
return Self::MatrixApi {
|
||||
kind,
|
||||
code,
|
||||
msg: message.to_owned(),
|
||||
details: Some(format!("{api_error:?}")),
|
||||
};
|
||||
}
|
||||
{
|
||||
let code = kind.errcode().to_string();
|
||||
let Ok(kind) = kind.to_owned().try_into() else {
|
||||
// We couldn't parse the API error, so we return a generic one instead
|
||||
return (*http_error).into();
|
||||
};
|
||||
return Self::MatrixApi {
|
||||
kind,
|
||||
code,
|
||||
msg: message.to_owned(),
|
||||
details: Some(format!("{api_error:?}")),
|
||||
};
|
||||
}
|
||||
(*http_error).into()
|
||||
}
|
||||
@@ -348,6 +345,39 @@ pub enum RoomError {
|
||||
FailedSendingAttachment,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum LiveLocationError {
|
||||
#[error("Network error")]
|
||||
Network,
|
||||
#[error("Existing beacon information not found")]
|
||||
NotFound,
|
||||
#[error("Beacon event is redacted and cannot be processed")]
|
||||
Redacted,
|
||||
#[error("Must join the room to access beacon information")]
|
||||
Stripped,
|
||||
#[error("The beacon event has expired")]
|
||||
NotLive,
|
||||
#[error("Deserialization error")]
|
||||
Deserialization,
|
||||
#[error("Other error: {msg}")]
|
||||
Other { msg: String },
|
||||
}
|
||||
|
||||
impl From<matrix_sdk::BeaconError> for LiveLocationError {
|
||||
fn from(value: matrix_sdk::BeaconError) -> Self {
|
||||
match value {
|
||||
matrix_sdk::BeaconError::Network(_) => Self::Network,
|
||||
matrix_sdk::BeaconError::NotFound => Self::NotFound,
|
||||
matrix_sdk::BeaconError::Redacted => Self::Redacted,
|
||||
matrix_sdk::BeaconError::Stripped => Self::Stripped,
|
||||
matrix_sdk::BeaconError::Deserialization(_) => Self::Deserialization,
|
||||
matrix_sdk::BeaconError::NotLive => Self::NotLive,
|
||||
matrix_sdk::BeaconError::Other(err) => Self::Other { msg: err.to_string() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
#[uniffi(flat_error)]
|
||||
pub enum MediaInfoError {
|
||||
|
||||
@@ -12,29 +12,29 @@
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use anyhow::{Context, bail};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
use ruma::{
|
||||
EventId,
|
||||
events::{
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
TimelineEventType as RumaTimelineEventType,
|
||||
room::{
|
||||
encrypted,
|
||||
message::{MessageType as RumaMessageType, Relation},
|
||||
redaction::SyncRoomRedactionEvent,
|
||||
},
|
||||
AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
|
||||
MessageLikeEventContent as RumaMessageLikeEventContent, RedactContent,
|
||||
RedactedStateEventContent, StaticStateEventContent, SyncMessageLikeEvent, SyncStateEvent,
|
||||
TimelineEventType as RumaTimelineEventType,
|
||||
},
|
||||
EventId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ClientError,
|
||||
room_member::MembershipState,
|
||||
ruma::{MessageType, RtcCallIntent, RtcNotificationType},
|
||||
utils::Timestamp,
|
||||
ClientError,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
@@ -89,6 +89,7 @@ impl From<AnyTimelineEvent> for TimelineEvent {
|
||||
|
||||
/// The timeline event type.
|
||||
#[derive(Clone, uniffi::Enum, PartialEq, Eq, Hash)]
|
||||
#[uniffi::export(Eq, Hash)]
|
||||
pub enum TimelineEventType {
|
||||
/// The event is a message-like one and should be displayed as such.
|
||||
MessageLike { value: MessageLikeEventType },
|
||||
@@ -224,9 +225,6 @@ impl From<RumaTimelineEventType> for TimelineEventType {
|
||||
RumaTimelineEventType::PolicyRuleUser => {
|
||||
Self::State { value: StateEventType::PolicyRuleUser }
|
||||
}
|
||||
RumaTimelineEventType::RoomAliases => {
|
||||
Self::State { value: StateEventType::RoomAliases }
|
||||
}
|
||||
RumaTimelineEventType::RoomAvatar => Self::State { value: StateEventType::RoomAvatar },
|
||||
RumaTimelineEventType::RoomCanonicalAlias => {
|
||||
Self::State { value: StateEventType::RoomCanonicalAlias }
|
||||
@@ -306,7 +304,6 @@ pub enum StateEventContent {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
@@ -334,7 +331,6 @@ impl TryFrom<AnySyncStateEvent> for StateEventContent {
|
||||
AnySyncStateEvent::PolicyRuleRoom(_) => StateEventContent::PolicyRuleRoom,
|
||||
AnySyncStateEvent::PolicyRuleServer(_) => StateEventContent::PolicyRuleServer,
|
||||
AnySyncStateEvent::PolicyRuleUser(_) => StateEventContent::PolicyRuleUser,
|
||||
AnySyncStateEvent::RoomAliases(_) => StateEventContent::RoomAliases,
|
||||
AnySyncStateEvent::RoomAvatar(_) => StateEventContent::RoomAvatar,
|
||||
AnySyncStateEvent::RoomCanonicalAlias(_) => StateEventContent::RoomCanonicalAlias,
|
||||
AnySyncStateEvent::RoomCreate(_) => StateEventContent::RoomCreate,
|
||||
@@ -526,7 +522,6 @@ pub enum StateEventType {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar,
|
||||
RoomCanonicalAlias,
|
||||
RoomCreate,
|
||||
@@ -558,7 +553,6 @@ impl From<StateEventType> for ruma::events::StateEventType {
|
||||
StateEventType::PolicyRuleRoom => Self::PolicyRuleRoom,
|
||||
StateEventType::PolicyRuleServer => Self::PolicyRuleServer,
|
||||
StateEventType::PolicyRuleUser => Self::PolicyRuleUser,
|
||||
StateEventType::RoomAliases => Self::RoomAliases,
|
||||
StateEventType::RoomAvatar => Self::RoomAvatar,
|
||||
StateEventType::RoomCanonicalAlias => Self::RoomCanonicalAlias,
|
||||
StateEventType::RoomCreate => Self::RoomCreate,
|
||||
|
||||
@@ -24,6 +24,7 @@ mod room_member;
|
||||
mod room_preview;
|
||||
mod ruma;
|
||||
mod runtime;
|
||||
mod search;
|
||||
mod session_verification;
|
||||
mod spaces;
|
||||
mod store;
|
||||
|
||||
@@ -12,22 +12,153 @@
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::ruma::LocationContent;
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::StreamExt as _;
|
||||
use matrix_sdk::live_location_share::{
|
||||
LiveLocationShare as SdkLiveLocationShare, LiveLocationShares as SdkLiveLocationShares,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
|
||||
use crate::{ruma::LocationContent, runtime::get_runtime_handle, task_handle::TaskHandle};
|
||||
|
||||
/// Details of the last known location beacon.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
/// The most recent location content of the user.
|
||||
/// The most recent location content shared for this asset.
|
||||
pub location: LocationContent,
|
||||
/// A timestamp in milliseconds since Unix Epoch on that day in local
|
||||
/// time.
|
||||
/// The timestamp of when the location was updated.
|
||||
pub ts: u64,
|
||||
}
|
||||
/// Details of a users live location share.
|
||||
|
||||
/// Details of a user's live location share.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LiveLocationShare {
|
||||
/// The user's last known location.
|
||||
pub last_location: LastLocation,
|
||||
/// The live status of the live location share.
|
||||
pub(crate) is_live: bool,
|
||||
/// The asset's last known location.
|
||||
pub last_location: Option<LastLocation>,
|
||||
/// The user ID of the person sharing their live location.
|
||||
pub user_id: String,
|
||||
/// The time when location sharing started.
|
||||
pub start_ts: u64,
|
||||
/// The duration that the location sharing will be live.
|
||||
/// Meaning that the location will stop being shared at ts + timeout.
|
||||
pub timeout: u64,
|
||||
}
|
||||
|
||||
/// An update to the list of active live location shares.
|
||||
///
|
||||
/// Corresponds to a [`VectorDiff`] on the underlying [`ObservableVector`].
|
||||
///
|
||||
/// [`ObservableVector`]: eyeball_im::ObservableVector
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum LiveLocationShareUpdate {
|
||||
Append { values: Vec<LiveLocationShare> },
|
||||
Clear,
|
||||
PushFront { value: LiveLocationShare },
|
||||
PushBack { value: LiveLocationShare },
|
||||
PopFront,
|
||||
PopBack,
|
||||
Insert { index: u32, value: LiveLocationShare },
|
||||
Set { index: u32, value: LiveLocationShare },
|
||||
Remove { index: u32 },
|
||||
Truncate { length: u32 },
|
||||
Reset { values: Vec<LiveLocationShare> },
|
||||
}
|
||||
|
||||
/// Listener for live location share updates.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: SendOutsideWasm + SyncOutsideWasm + Debug {
|
||||
/// Called with a batch of [`LiveLocationShareUpdate`]s whenever the list
|
||||
/// of active shares changes.
|
||||
fn on_update(&self, updates: Vec<LiveLocationShareUpdate>);
|
||||
}
|
||||
|
||||
/// Tracks active live location shares in a room.
|
||||
///
|
||||
/// Holds the SDK [`SdkLiveLocationShares`] which keeps the beacon and
|
||||
/// beacon_info event handlers registered for as long as this object is alive.
|
||||
/// Call [`LiveLocationShares::subscribe`] to start receiving updates.
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct LiveLocationShares {
|
||||
inner: SdkLiveLocationShares,
|
||||
}
|
||||
|
||||
impl LiveLocationShares {
|
||||
pub fn new(inner: SdkLiveLocationShares) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl LiveLocationShares {
|
||||
/// Subscribe to changes in the list of active live location shares.
|
||||
///
|
||||
/// Immediately calls `listener` with a `Reset` update containing the
|
||||
/// current snapshot (if non-empty), then calls it again for every
|
||||
/// subsequent change that arrives from sync.
|
||||
///
|
||||
/// Returns a [`TaskHandle`] that, when dropped, stops the listener.
|
||||
/// The event handlers remain registered for as long as this
|
||||
/// [`LiveLocationShares`] object is alive.
|
||||
pub fn subscribe(&self, listener: Box<dyn LiveLocationShareListener>) -> Arc<TaskHandle> {
|
||||
let (initial_values, mut stream) = self.inner.subscribe();
|
||||
|
||||
if !initial_values.is_empty() {
|
||||
listener.on_update(vec![LiveLocationShareUpdate::Reset {
|
||||
values: initial_values.into_iter().map(Into::into).collect(),
|
||||
}]);
|
||||
}
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
while let Some(diffs) = stream.next().await {
|
||||
listener.on_update(diffs.into_iter().map(Into::into).collect());
|
||||
}
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SdkLiveLocationShare> for LiveLocationShare {
|
||||
fn from(share: SdkLiveLocationShare) -> Self {
|
||||
let start_ts = share.beacon_info.ts.0.into();
|
||||
let timeout = share.beacon_info.timeout.as_millis() as u64;
|
||||
let asset = share.beacon_info.asset.type_.into();
|
||||
let last_location = share.last_location.map(|l| LastLocation {
|
||||
location: LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: l.location.uri.to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset,
|
||||
},
|
||||
ts: l.ts.0.into(),
|
||||
});
|
||||
LiveLocationShare { user_id: share.user_id.to_string(), last_location, start_ts, timeout }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VectorDiff<SdkLiveLocationShare>> for LiveLocationShareUpdate {
|
||||
fn from(diff: VectorDiff<SdkLiveLocationShare>) -> Self {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
Self::Append { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
VectorDiff::Clear => Self::Clear,
|
||||
VectorDiff::PushFront { value } => Self::PushFront { value: value.into() },
|
||||
VectorDiff::PushBack { value } => Self::PushBack { value: value.into() },
|
||||
VectorDiff::PopFront => Self::PopFront,
|
||||
VectorDiff::PopBack => Self::PopBack,
|
||||
VectorDiff::Insert { index, value } => {
|
||||
Self::Insert { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
Self::Set { index: index as u32, value: value.into() }
|
||||
}
|
||||
VectorDiff::Remove { index } => Self::Remove { index: index as u32 },
|
||||
VectorDiff::Truncate { length } => Self::Truncate { length: length as u32 },
|
||||
VectorDiff::Reset { values } => {
|
||||
Self::Reset { values: values.into_iter().map(Into::into).collect() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ pub struct NotificationRoomInfo {
|
||||
pub topic: Option<String>,
|
||||
pub join_rule: Option<JoinRule>,
|
||||
pub joined_members_count: u64,
|
||||
pub service_members: Vec<String>,
|
||||
pub is_encrypted: Option<bool>,
|
||||
pub is_direct: bool,
|
||||
pub is_space: bool,
|
||||
@@ -106,6 +107,7 @@ impl NotificationItem {
|
||||
topic: item.room_topic,
|
||||
join_rule: item.room_join_rule.map(TryInto::try_into).transpose().ok().flatten(),
|
||||
joined_members_count: item.joined_members_count,
|
||||
service_members: item.service_members,
|
||||
is_encrypted: item.is_room_encrypted,
|
||||
is_direct: item.is_direct_message_room,
|
||||
is_space: item.is_space,
|
||||
|
||||
@@ -15,23 +15,26 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use matrix_sdk::{
|
||||
Client as MatrixClient,
|
||||
event_handler::EventHandlerHandle,
|
||||
notification_settings::{
|
||||
NotificationSettings as SdkNotificationSettings,
|
||||
RoomNotificationMode as SdkRoomNotificationMode,
|
||||
},
|
||||
ruma::events::push_rules::PushRulesEvent,
|
||||
Client as MatrixClient,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::{
|
||||
Int, RoomId, UInt,
|
||||
events::push_rules::PushRulesEventContent,
|
||||
push::{
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, PredefinedOverrideRuleId,
|
||||
PredefinedUnderrideRuleId, PushCondition as SdkPushCondition, RoomMemberCountIs,
|
||||
RuleKind as SdkRuleKind, ScalarJsonValue as SdkJsonValue, Tweak as SdkTweak,
|
||||
Action as SdkAction, ComparisonOperator as SdkComparisonOperator, EventMatchConditionData,
|
||||
EventPropertyContainsConditionData, EventPropertyIsConditionData, HighlightTweakValue,
|
||||
PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition as SdkPushCondition,
|
||||
RoomMemberCountConditionData, RoomMemberCountIs, RuleKind as SdkRuleKind,
|
||||
ScalarJsonValue as SdkJsonValue, SenderNotificationPermissionConditionData,
|
||||
Tweak as SdkTweak,
|
||||
},
|
||||
Int, RoomId, UInt,
|
||||
};
|
||||
use tokio::sync::RwLock as AsyncRwLock;
|
||||
|
||||
@@ -181,20 +184,22 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
|
||||
fn try_from(value: SdkPushCondition) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkPushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
SdkPushCondition::EventMatch(data) => {
|
||||
Self::EventMatch { key: data.key, pattern: data.pattern }
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
SdkPushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
SdkPushCondition::RoomMemberCount { is } => {
|
||||
Self::RoomMemberCount { prefix: is.prefix.into(), count: is.count.into() }
|
||||
SdkPushCondition::RoomMemberCount(data) => {
|
||||
Self::RoomMemberCount { prefix: data.is.prefix.into(), count: data.is.count.into() }
|
||||
}
|
||||
SdkPushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key: key.to_string() }
|
||||
SdkPushCondition::SenderNotificationPermission(data) => {
|
||||
Self::SenderNotificationPermission { key: data.key.to_string() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
SdkPushCondition::EventPropertyIs(data) => {
|
||||
Self::EventPropertyIs { key: data.key, value: data.value.into() }
|
||||
}
|
||||
SdkPushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
SdkPushCondition::EventPropertyContains(data) => {
|
||||
Self::EventPropertyContains { key: data.key, value: data.value.into() }
|
||||
}
|
||||
_ => return Err("Unsupported condition type".to_owned()),
|
||||
})
|
||||
@@ -204,24 +209,28 @@ impl TryFrom<SdkPushCondition> for PushCondition {
|
||||
impl From<PushCondition> for SdkPushCondition {
|
||||
fn from(value: PushCondition) -> Self {
|
||||
match value {
|
||||
PushCondition::EventMatch { key, pattern } => Self::EventMatch { key, pattern },
|
||||
PushCondition::EventMatch { key, pattern } => {
|
||||
Self::EventMatch(EventMatchConditionData::new(key, pattern))
|
||||
}
|
||||
#[allow(deprecated)]
|
||||
PushCondition::ContainsDisplayName => Self::ContainsDisplayName,
|
||||
PushCondition::RoomMemberCount { prefix, count } => Self::RoomMemberCount {
|
||||
is: RoomMemberCountIs {
|
||||
PushCondition::RoomMemberCount { prefix, count } => {
|
||||
Self::RoomMemberCount(RoomMemberCountConditionData::new(RoomMemberCountIs {
|
||||
prefix: prefix.into(),
|
||||
count: UInt::new(count).unwrap_or_default(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
}
|
||||
PushCondition::SenderNotificationPermission { key } => {
|
||||
Self::SenderNotificationPermission { key: key.into() }
|
||||
Self::SenderNotificationPermission(SenderNotificationPermissionConditionData::new(
|
||||
key.into(),
|
||||
))
|
||||
}
|
||||
PushCondition::EventPropertyIs { key, value } => {
|
||||
Self::EventPropertyIs { key, value: value.into() }
|
||||
}
|
||||
PushCondition::EventPropertyContains { key, value } => {
|
||||
Self::EventPropertyContains { key, value: value.into() }
|
||||
Self::EventPropertyIs(EventPropertyIsConditionData::new(key, value.into()))
|
||||
}
|
||||
PushCondition::EventPropertyContains { key, value } => Self::EventPropertyContains(
|
||||
EventPropertyContainsConditionData::new(key, value.into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,16 +312,19 @@ impl TryFrom<SdkTweak> for Tweak {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: SdkTweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
SdkTweak::Sound(sound) => Self::Sound { value: sound },
|
||||
SdkTweak::Highlight(highlight) => Self::Highlight { value: highlight },
|
||||
SdkTweak::Custom { name, value } => {
|
||||
let json_string = serde_json::to_string(&value)
|
||||
.map_err(|e| format!("Failed to serialize custom tweak value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value: json_string }
|
||||
Ok(match &value {
|
||||
SdkTweak::Sound(sound) => Self::Sound { value: sound.to_string() },
|
||||
SdkTweak::Highlight(highlight) => {
|
||||
Self::Highlight { value: matches!(highlight, HighlightTweakValue::Yes) }
|
||||
}
|
||||
_ => {
|
||||
let json_string = value
|
||||
.custom_value()
|
||||
.ok_or_else(|| "Unsupported tweak type".to_owned())?
|
||||
.to_string();
|
||||
|
||||
Self::Custom { name: value.set_tweak().to_owned(), value: json_string }
|
||||
}
|
||||
_ => return Err("Unsupported tweak type".to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -322,16 +334,16 @@ impl TryFrom<Tweak> for SdkTweak {
|
||||
|
||||
fn try_from(value: Tweak) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
Tweak::Sound { value } => Self::Sound(value),
|
||||
Tweak::Highlight { value } => Self::Highlight(value),
|
||||
Tweak::Custom { name, value } => {
|
||||
let json_value: serde_json::Value = serde_json::from_str(&value)
|
||||
.map_err(|e| format!("Failed to deserialize custom tweak value: {e}"))?;
|
||||
let value = serde_json::from_value(json_value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?;
|
||||
|
||||
Self::Custom { name, value }
|
||||
}
|
||||
Tweak::Sound { value } => Self::Sound(value.into()),
|
||||
Tweak::Highlight { value } => Self::Highlight(value.into()),
|
||||
Tweak::Custom { name, value } => Self::new(
|
||||
name,
|
||||
Some(
|
||||
serde_json::value::RawValue::from_string(value)
|
||||
.map_err(|e| format!("Failed to convert JSON value: {e}"))?,
|
||||
),
|
||||
)
|
||||
.map_err(|e| format!("Failed to convert custom tweak: {e}"))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::{error::Error, mem::MaybeUninit};
|
||||
|
||||
use jni::{
|
||||
errors::JniError,
|
||||
sys::{JavaVM as RawJavaVM, JNI_OK},
|
||||
sys::{JNI_OK, JavaVM as RawJavaVM},
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
@@ -70,8 +70,8 @@ fn init_rustls_platform_verifier(env: &mut jni::JNIEnv<'_>) -> jni::errors::Resu
|
||||
}
|
||||
|
||||
/// Attach the current thread to a JVM one.
|
||||
pub(crate) fn android_attach_current_thread_permanently(
|
||||
) -> jni::errors::Result<jni::JNIEnv<'static>> {
|
||||
pub(crate) fn android_attach_current_thread_permanently()
|
||||
-> jni::errors::Result<jni::JNIEnv<'static>> {
|
||||
ANDROID_JVM
|
||||
.get()
|
||||
.ok_or_else(|| jni::errors::Error::JniCall(JniError::Unknown))?
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
|
||||
use ::tracing::info;
|
||||
#[cfg(feature = "sentry")]
|
||||
@@ -24,18 +24,17 @@ use tracing_appender::rolling::Rotation;
|
||||
use tracing_core::Level;
|
||||
use tracing_core::Subscriber;
|
||||
use tracing_subscriber::{
|
||||
EnvFilter, Layer, Registry,
|
||||
field::RecordFields,
|
||||
fmt::{
|
||||
self,
|
||||
self, FormatEvent, FormatFields, FormattedFields,
|
||||
format::{DefaultFields, Writer},
|
||||
time::FormatTime,
|
||||
FormatEvent, FormatFields, FormattedFields,
|
||||
},
|
||||
layer::{Layered, SubscriberExt as _},
|
||||
registry::LookupSpan,
|
||||
reload::{self, Handle},
|
||||
util::SubscriberInitExt as _,
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
use crate::error::ClientError;
|
||||
@@ -54,9 +53,9 @@ mod android_platform;
|
||||
|
||||
use rolling_writer::SizeAndDateRollingWriter;
|
||||
|
||||
use self::tracing::LogLevel;
|
||||
#[cfg(feature = "sentry")]
|
||||
use self::tracing::BRIDGE_SPAN_NAME;
|
||||
use self::tracing::LogLevel;
|
||||
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
@@ -150,10 +149,10 @@ where
|
||||
|
||||
write!(writer, "{}", span.name())?;
|
||||
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>() {
|
||||
if !fields.is_empty() {
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
if let Some(fields) = &span.extensions().get::<FormattedFields<N>>()
|
||||
&& !fields.is_empty()
|
||||
{
|
||||
write!(writer, "{{{fields}}}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,7 +513,12 @@ impl TracingConfiguration {
|
||||
#[cfg_attr(not(feature = "sentry"), allow(unused_mut))]
|
||||
fn build(mut self) -> LoggingCtx {
|
||||
// Show full backtraces, if we run into panics.
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
//
|
||||
// FIXME: Use safe API for this once stable. Tracking issue:
|
||||
// https://github.com/rust-lang/rust/issues/93346
|
||||
unsafe {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
}
|
||||
|
||||
// Log panics.
|
||||
log_panics::init();
|
||||
@@ -533,11 +537,7 @@ impl TracingConfiguration {
|
||||
sentry::ClientOptions {
|
||||
traces_sampler: Some(Arc::new(|ctx| {
|
||||
// Make sure bridge spans are always uploaded
|
||||
if ctx.name() == BRIDGE_SPAN_NAME {
|
||||
1.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if ctx.name() == BRIDGE_SPAN_NAME { 1.0 } else { 0.0 }
|
||||
})),
|
||||
attach_stacktrace: true,
|
||||
release: Some(env!("VERGEN_GIT_SHA").into()),
|
||||
|
||||
@@ -237,12 +237,11 @@ impl SizeAndDateRollingWriter {
|
||||
check_conditions: bool,
|
||||
) -> io::Result<()> {
|
||||
// Check if rotation is needed (skip for uninitialized state)
|
||||
if check_conditions {
|
||||
if let Some(state) = state.as_ref() {
|
||||
if !Self::should_rotate_by_time(config, &state.current_path) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if check_conditions
|
||||
&& let Some(state) = state.as_ref()
|
||||
&& !Self::should_rotate_by_time(config, &state.current_path)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let time_str = Self::format_rotation_timestamp(config);
|
||||
@@ -312,10 +311,10 @@ impl SizeAndDateRollingWriter {
|
||||
}
|
||||
|
||||
// Check if file is older than max age
|
||||
if let Ok(duration) = now.duration_since(modified) {
|
||||
if duration.as_secs() > config.max_age_seconds {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
if let Ok(duration) = now.duration_since(modified)
|
||||
&& duration.as_secs() > config.max_age_seconds
|
||||
{
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,11 +402,7 @@ impl<'a> Write for SizeAndDateRollingWriterHandle<'a> {
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let Some(s) = state.as_mut() {
|
||||
s.current_file.flush()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
if let Some(s) = state.as_mut() { s.current_file.flush() } else { Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,11 +653,7 @@ mod tests {
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
path.file_name()?.to_str().map(|s| s.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if path.is_file() { path.file_name()?.to_str().map(|s| s.to_owned()) } else { None }
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -876,10 +867,10 @@ mod tests {
|
||||
std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter(|entry| {
|
||||
if let Ok(entry) = entry {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
return name.starts_with("multi");
|
||||
}
|
||||
if let Ok(entry) = entry
|
||||
&& let Some(name) = entry.file_name().to_str()
|
||||
{
|
||||
return name.starts_with("multi");
|
||||
}
|
||||
false
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@ use std::{
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use tracing::{callsite::DefaultCallsite, debug, error, field::FieldSet, Callsite};
|
||||
use tracing::{Callsite, callsite::DefaultCallsite, debug, error, field::FieldSet};
|
||||
use tracing_core::{identify_callsite, metadata::Kind as MetadataKind};
|
||||
|
||||
/// Log an event.
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
OAuth,
|
||||
qrcode::{
|
||||
self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError,
|
||||
DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress,
|
||||
},
|
||||
OAuth,
|
||||
};
|
||||
use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent};
|
||||
use matrix_sdk_common::{stream::StreamExt, SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm, stream::StreamExt};
|
||||
|
||||
use crate::{
|
||||
authentication::OidcConfiguration, runtime::get_runtime_handle, task_handle::TaskHandle,
|
||||
|
||||
@@ -15,62 +15,61 @@
|
||||
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{StreamExt, pin_mut};
|
||||
use matrix_sdk::{
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
edit::EditedContent, power_levels::RoomPowerLevelChanges, Room as SdkRoom, RoomMemberRole,
|
||||
},
|
||||
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
|
||||
ComposerDraft as SdkComposerDraft, ComposerDraftType as SdkComposerDraftType,
|
||||
DraftAttachment as SdkDraftAttachment, DraftAttachmentContent, DraftThumbnail, EncryptionState,
|
||||
PredecessorRoom as SdkPredecessorRoom, RoomHero as SdkRoomHero, RoomMemberships, RoomState,
|
||||
SuccessorRoom as SdkSuccessorRoom,
|
||||
encryption::LocalTrust,
|
||||
room::{
|
||||
Room as SdkRoom, RoomMemberRole, edit::EditedContent, power_levels::RoomPowerLevelChanges,
|
||||
},
|
||||
send_queue::RoomSendQueueUpdate as SdkRoomSendQueueUpdate,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
timeline::{default_event_filter, RoomExt, TimelineBuilder},
|
||||
timeline::{RoomExt, TimelineBuilder, default_event_filter},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
use mime::Mime;
|
||||
use ruma::{
|
||||
assign,
|
||||
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId, assign,
|
||||
events::{
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
receipt::ReceiptThread,
|
||||
room::{
|
||||
avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
MediaSource as RumaMediaSource, avatar::ImageInfo as RumaAvatarImageInfo,
|
||||
history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
join_rules::JoinRule as RumaJoinRule, message::RoomMessageEventContentWithoutRelation,
|
||||
MediaSource as RumaMediaSource,
|
||||
},
|
||||
AnyMessageLikeEventContent, AnySyncTimelineEvent,
|
||||
},
|
||||
EventId, Int, OwnedDeviceId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomAliasId,
|
||||
ServerName, UserId,
|
||||
};
|
||||
use tracing::{error, warn};
|
||||
use tracing::error;
|
||||
|
||||
use self::{power_levels::RoomPowerLevels, room_info::RoomInfo};
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
chunk_iterator::ChunkIterator,
|
||||
client::{JoinRule, RoomVisibility},
|
||||
error::{ClientError, MediaInfoError, NotYetImplemented, QueueWedgeError, RoomError},
|
||||
error::{
|
||||
ClientError, LiveLocationError, MediaInfoError, NotYetImplemented, QueueWedgeError,
|
||||
RoomError,
|
||||
},
|
||||
event::TimelineEvent,
|
||||
identity_status_change::IdentityStatusChange,
|
||||
live_location_share::{LastLocation, LiveLocationShare},
|
||||
live_location_share::LiveLocationShares,
|
||||
room_member::{RoomMember, RoomMemberWithSenderInfo},
|
||||
room_preview::RoomPreview,
|
||||
ruma::{
|
||||
AudioInfo, FileInfo, ImageInfo, LocationContent, MediaSource, ThumbnailInfo, VideoInfo,
|
||||
},
|
||||
ruma::{AudioInfo, FileInfo, ImageInfo, MediaSource, ThumbnailInfo, VideoInfo},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{
|
||||
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
|
||||
configuration::{TimelineConfiguration, TimelineFilter},
|
||||
threads::{ThreadListService, ThreadSubscription},
|
||||
AbstractProgress, LatestEventValue, ReceiptType, SendHandle, Timeline, UploadSource,
|
||||
},
|
||||
utils::{u64_to_uint, AsyncRuntimeDropped},
|
||||
TaskHandle,
|
||||
utils::{AsyncRuntimeDropped, u64_to_uint},
|
||||
};
|
||||
|
||||
mod power_levels;
|
||||
@@ -1088,17 +1087,14 @@ impl Room {
|
||||
}
|
||||
|
||||
/// Stop the current users live location share in the room.
|
||||
pub async fn stop_live_location_share(&self) -> Result<(), ClientError> {
|
||||
self.inner.stop_live_location_share().await.expect("Unable to stop live location share");
|
||||
pub async fn stop_live_location_share(&self) -> Result<(), LiveLocationError> {
|
||||
self.inner.stop_live_location_share().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the current users live location beacon in the room.
|
||||
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), ClientError> {
|
||||
self.inner
|
||||
.send_location_beacon(geo_uri)
|
||||
.await
|
||||
.expect("Unable to send live location beacon");
|
||||
pub async fn send_live_location(&self, geo_uri: String) -> Result<(), LiveLocationError> {
|
||||
self.inner.send_location_beacon(geo_uri).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1140,46 +1136,16 @@ impl Room {
|
||||
}))))
|
||||
}
|
||||
|
||||
/// Subscribes to live location shares in this room, using a `listener` to
|
||||
/// be notified of the changes.
|
||||
/// Returns the active live location shares for this room.
|
||||
///
|
||||
/// The current live location shares will be emitted immediately when
|
||||
/// subscribing, along with a [`TaskHandle`] to cancel the subscription.
|
||||
pub fn subscribe_to_live_location_shares(
|
||||
self: Arc<Self>,
|
||||
listener: Box<dyn LiveLocationShareListener>,
|
||||
) -> Arc<TaskHandle> {
|
||||
let room = self.inner.clone();
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
let subscription = room.observe_live_location_shares();
|
||||
let stream = subscription.subscribe();
|
||||
let mut pinned_stream = pin!(stream);
|
||||
|
||||
while let Some(event) = pinned_stream.next().await {
|
||||
let last_location = LocationContent {
|
||||
body: "".to_owned(),
|
||||
geo_uri: event.last_location.location.uri.clone().to_string(),
|
||||
description: None,
|
||||
zoom_level: None,
|
||||
asset: event.beacon_info.as_ref().map(|b| b.asset.type_.clone()).into(),
|
||||
};
|
||||
|
||||
let Some(beacon_info) = event.beacon_info else {
|
||||
warn!("Live location share is missing the associated beacon_info state, skipping event.");
|
||||
continue;
|
||||
};
|
||||
|
||||
listener.call(vec![LiveLocationShare {
|
||||
last_location: LastLocation {
|
||||
location: last_location,
|
||||
ts: event.last_location.ts.0.into(),
|
||||
},
|
||||
is_live: beacon_info.is_live(),
|
||||
user_id: event.user_id.to_string(),
|
||||
}])
|
||||
}
|
||||
})))
|
||||
/// The returned [`LiveLocationShares`] object tracks which users are
|
||||
/// currently sharing their live location. It keeps the underlying event
|
||||
/// handlers registered — and therefore the share list up-to-date — for as
|
||||
/// long as it is alive. Call [`LiveLocationShares::subscribe`] on it to
|
||||
/// receive an initial snapshot and a stream of incremental updates.
|
||||
pub async fn live_location_shares(&self) -> Arc<LiveLocationShares> {
|
||||
let inner = self.inner.live_location_shares().await;
|
||||
Arc::new(LiveLocationShares::new(inner))
|
||||
}
|
||||
|
||||
/// Forget this room.
|
||||
@@ -1214,12 +1180,11 @@ impl Room {
|
||||
|
||||
// If no server names are provided and the room's membership is invited,
|
||||
// add the server name from the sender's user id as a fallback value
|
||||
if server_names.is_empty() {
|
||||
if let Ok(invite_details) = self.inner.invite_details().await {
|
||||
if let Some(inviter) = invite_details.inviter {
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
}
|
||||
if server_names.is_empty()
|
||||
&& let Ok(invite_details) = self.inner.invite_details().await
|
||||
&& let Some(inviter) = invite_details.inviter
|
||||
{
|
||||
server_names.push(inviter.user_id().server_name().to_owned());
|
||||
}
|
||||
|
||||
let room_preview = client.get_room_preview(&room_or_alias_id, server_names).await?;
|
||||
@@ -1303,12 +1268,6 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
/// A listener for receiving new live location shares in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait LiveLocationShareListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
fn call(&self, live_location_shares: Vec<LiveLocationShare>);
|
||||
}
|
||||
|
||||
/// A listener for receiving call decline events in a room.
|
||||
#[matrix_sdk_ffi_macros::export(callback_interface)]
|
||||
pub trait CallDeclineListener: SyncOutsideWasm + SendOutsideWasm {
|
||||
@@ -1675,23 +1634,30 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
type Error = ClientError;
|
||||
|
||||
fn try_from(value: DraftAttachment) -> Result<Self, Self::Error> {
|
||||
fn draft_thumbnail(
|
||||
thumbnail_info: Option<ThumbnailInfo>,
|
||||
thumbnail_source: Option<UploadSource>,
|
||||
) -> Result<Option<DraftThumbnail>, ClientError> {
|
||||
if let Some(info) = thumbnail_info
|
||||
&& let Some(source) = thumbnail_source
|
||||
{
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Ok(Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
match value {
|
||||
DraftAttachment::Image { image_info, source, thumbnail_source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
let thumbnail = match (image_info.thumbnail_info, thumbnail_source) {
|
||||
(Some(info), Some(source)) => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Image {
|
||||
@@ -1701,26 +1667,12 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
width: image_info.width,
|
||||
height: image_info.height,
|
||||
blurhash: image_info.blurhash,
|
||||
thumbnail,
|
||||
thumbnail: draft_thumbnail(image_info.thumbnail_info, thumbnail_source)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
DraftAttachment::Video { video_info, source, thumbnail_source, .. } => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
let thumbnail = match (video_info.thumbnail_info, thumbnail_source) {
|
||||
(Some(info), Some(source)) => {
|
||||
let (data, filename) = read_upload_source(source)?;
|
||||
Some(DraftThumbnail {
|
||||
filename,
|
||||
data,
|
||||
mimetype: info.mimetype,
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
size: info.size,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Ok(Self {
|
||||
filename,
|
||||
content: DraftAttachmentContent::Video {
|
||||
@@ -1731,7 +1683,7 @@ impl TryFrom<DraftAttachment> for SdkDraftAttachment {
|
||||
height: video_info.height,
|
||||
duration: video_info.duration,
|
||||
blurhash: video_info.blurhash,
|
||||
thumbnail,
|
||||
thumbnail: draft_thumbnail(video_info.thumbnail_info, thumbnail_source)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use ruma::{
|
||||
events::{room::power_levels::RoomPowerLevels as RumaPowerLevels, TimelineEventType},
|
||||
OwnedUserId, UserId,
|
||||
events::{TimelineEventType, room::power_levels::RoomPowerLevels as RumaPowerLevels},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::{
|
||||
error::ClientError,
|
||||
notification_settings::RoomNotificationMode,
|
||||
room::{
|
||||
power_levels::RoomPowerLevels, Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom,
|
||||
Membership, RoomHero, RoomHistoryVisibility, SuccessorRoom, power_levels::RoomPowerLevels,
|
||||
},
|
||||
room_member::RoomMember,
|
||||
ruma::RtcCallIntent,
|
||||
|
||||
@@ -17,30 +17,30 @@
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{StreamExt, pin_mut};
|
||||
use matrix_sdk::{
|
||||
ruma::{
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
RoomId,
|
||||
},
|
||||
Room as SdkRoom,
|
||||
ruma::{
|
||||
RoomId,
|
||||
api::client::sync::sync_events::UnreadNotificationsCount as RumaUnreadNotificationsCount,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::{
|
||||
room_list_service::filters::{
|
||||
new_filter_all, new_filter_any, new_filter_category, new_filter_deduplicate_versions,
|
||||
new_filter_favourite, new_filter_fuzzy_match_room_name, new_filter_identifiers,
|
||||
new_filter_invite, new_filter_joined, new_filter_low_priority, new_filter_non_left,
|
||||
new_filter_none, new_filter_normalized_match_room_name, new_filter_not, new_filter_space,
|
||||
new_filter_unread, BoxedFilterFn, RoomCategory,
|
||||
BoxedFilterFn, RoomCategory, new_filter_all, new_filter_any, new_filter_category,
|
||||
new_filter_deduplicate_versions, new_filter_favourite, new_filter_fuzzy_match_room_name,
|
||||
new_filter_identifiers, new_filter_invite, new_filter_joined, new_filter_low_priority,
|
||||
new_filter_non_left, new_filter_none, new_filter_normalized_match_room_name,
|
||||
new_filter_not, new_filter_space, new_filter_unread,
|
||||
},
|
||||
unable_to_decrypt_hook::UtdHookManager,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
room::{Membership, Room},
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
#[derive(Debug, thiserror::Error, uniffi::Error)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use matrix_sdk::room::{RoomMember as SdkRoomMember, RoomMemberRole};
|
||||
use ruma::{events::room::power_levels::UserPowerLevel, UserId};
|
||||
use ruma::{UserId, events::room::power_levels::UserPowerLevel};
|
||||
|
||||
use crate::error::{ClientError, NotYetImplemented};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use matrix_sdk::{Client, room_preview::RoomPreview as SdkRoomPreview};
|
||||
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -21,8 +21,13 @@ use std::{
|
||||
use extension_trait::extension_trait;
|
||||
use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo};
|
||||
use ruma::{
|
||||
assign,
|
||||
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
|
||||
OwnedRoomId, OwnedUserId, UInt, UserId, assign,
|
||||
events::{
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
RoomAccountDataEventType as RumaRoomAccountDataEventType,
|
||||
direct::DirectEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
identity_server::IdentityServerEventContent,
|
||||
@@ -36,6 +41,8 @@ use ruma::{
|
||||
poll::start::PollKind as RumaPollKind,
|
||||
push_rules::PushRulesEventContent,
|
||||
room::{
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
message::{
|
||||
AudioInfo as RumaAudioInfo,
|
||||
AudioMessageEventContent as RumaAudioMessageEventContent,
|
||||
@@ -53,8 +60,6 @@ use ruma::{
|
||||
VideoInfo as RumaVideoInfo,
|
||||
VideoMessageEventContent as RumaVideoMessageEventContent,
|
||||
},
|
||||
ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource,
|
||||
ThumbnailInfo as RumaThumbnailInfo,
|
||||
},
|
||||
rtc::notification::{
|
||||
CallIntent as RumaCallIntent, NotificationType as RumaNotificationType,
|
||||
@@ -72,10 +77,6 @@ use ruma::{
|
||||
TagEventContent, TagInfo as RumaTagInfo, TagName as RumaTagName,
|
||||
UserTagName as RumaUserTagName,
|
||||
},
|
||||
GlobalAccountDataEvent as RumaGlobalAccountDataEvent,
|
||||
GlobalAccountDataEventType as RumaGlobalAccountDataEventType,
|
||||
RoomAccountDataEvent as RumaRoomAccountDataEvent,
|
||||
RoomAccountDataEventType as RumaRoomAccountDataEventType,
|
||||
},
|
||||
matrix_uri::MatrixId as RumaMatrixId,
|
||||
push::{
|
||||
@@ -83,8 +84,6 @@ use ruma::{
|
||||
Ruleset as RumaRuleset, SimplePushRule as RumaSimplePushRule,
|
||||
},
|
||||
serde::JsonObject,
|
||||
KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri,
|
||||
OwnedRoomId, OwnedUserId, UInt, UserId,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
@@ -391,11 +390,7 @@ pub enum MessageType {
|
||||
/// is its own field.
|
||||
/// - if a media only has a filename, then body is the filename.
|
||||
fn get_body_and_filename(filename: String, caption: Option<String>) -> (String, Option<String>) {
|
||||
if let Some(caption) = caption {
|
||||
(caption, Some(filename))
|
||||
} else {
|
||||
(filename, None)
|
||||
}
|
||||
if let Some(caption) = caption { (caption, Some(filename)) } else { (filename, None) }
|
||||
}
|
||||
|
||||
impl TryFrom<MessageType> for RumaMessageType {
|
||||
@@ -1714,6 +1709,7 @@ pub enum RoomAccountDataEvent {
|
||||
|
||||
/// The name of a tag.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, uniffi::Enum)]
|
||||
#[uniffi::export(Eq, Hash)]
|
||||
pub enum TagName {
|
||||
/// `m.favourite`: The user's favorite rooms.
|
||||
Favorite,
|
||||
|
||||
@@ -39,7 +39,7 @@ mod sys {
|
||||
mod sys {
|
||||
use std::future::Future;
|
||||
|
||||
use matrix_sdk_common::executor::{spawn, JoinHandle};
|
||||
use matrix_sdk_common::executor::{JoinHandle, spawn};
|
||||
|
||||
/// A dummy guard that does nothing when dropped.
|
||||
/// This is used for the Wasm implementation to match
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::{
|
||||
deserialized_responses::TimelineEvent,
|
||||
message_search::{
|
||||
GlobalSearchIterator as SdkGlobalSearchIterator,
|
||||
RoomSearchIterator as SdkRoomSearchIterator, SearchError as SdkSearchError,
|
||||
},
|
||||
};
|
||||
use matrix_sdk_ui::timeline::TimelineDetails;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::{
|
||||
client::Client,
|
||||
error::ClientError,
|
||||
room::Room,
|
||||
timeline::{ProfileDetails, TimelineItemContent},
|
||||
utils::Timestamp,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Error, thiserror::Error, Debug)]
|
||||
pub enum SearchError {
|
||||
#[error("Failed to search through the index: {0}")]
|
||||
IndexError(String),
|
||||
#[error("Failed to load event content for search result: {0}")]
|
||||
EventLoadError(String),
|
||||
}
|
||||
|
||||
impl From<SdkSearchError> for SearchError {
|
||||
fn from(err: SdkSearchError) -> Self {
|
||||
match err {
|
||||
SdkSearchError::IndexError(err) => SearchError::IndexError(err.to_string()),
|
||||
SdkSearchError::EventLoadError(err) => SearchError::EventLoadError(err.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Room {
|
||||
/// Search for messages in this room matching the given query, returning an
|
||||
/// iterator over the results that yields `num_results_per_batch` results at
|
||||
/// a time.
|
||||
pub fn search_messages(&self, query: String, num_results_per_batch: u32) -> RoomSearchIterator {
|
||||
RoomSearchIterator {
|
||||
sdk_room: self.inner.clone(),
|
||||
inner: Mutex::new(self.inner.search_messages(query, num_results_per_batch as usize)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct RoomSearchIterator {
|
||||
sdk_room: matrix_sdk::room::Room,
|
||||
inner: Mutex<SdkRoomSearchIterator>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl RoomSearchIterator {
|
||||
/// Return a list of events for the next batch of search results, or `None`
|
||||
/// if there are no more results.
|
||||
pub async fn next_events(&self) -> Result<Option<Vec<RoomSearchResult>>, SearchError> {
|
||||
let Some(events) = self.inner.lock().await.next_events().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut results = Vec::with_capacity(events.len());
|
||||
|
||||
for event in events {
|
||||
if let Some(result) = RoomSearchResult::from(&self.sdk_room, event).await {
|
||||
results.push(result);
|
||||
}
|
||||
}
|
||||
|
||||
results.shrink_to_fit();
|
||||
|
||||
Ok(Some(results))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct RoomSearchResult {
|
||||
event_id: String,
|
||||
sender: String,
|
||||
sender_profile: ProfileDetails,
|
||||
content: TimelineItemContent,
|
||||
timestamp: Timestamp,
|
||||
}
|
||||
|
||||
impl RoomSearchResult {
|
||||
async fn from(room: &matrix_sdk::room::Room, event: TimelineEvent) -> Option<Self> {
|
||||
let sender = event.sender()?;
|
||||
|
||||
let event_id = event.event_id().unwrap().to_string();
|
||||
let timestamp =
|
||||
event.timestamp().unwrap_or_else(ruma::MilliSecondsSinceUnixEpoch::now).into();
|
||||
|
||||
let content = matrix_sdk_ui::timeline::TimelineItemContent::from_event(room, event).await?;
|
||||
let profile = TimelineDetails::from_initial_value(
|
||||
matrix_sdk_ui::timeline::Profile::load(room, &sender).await,
|
||||
);
|
||||
|
||||
Some(Self {
|
||||
event_id,
|
||||
sender: sender.to_string(),
|
||||
sender_profile: ProfileDetails::from(profile),
|
||||
content: TimelineItemContent::from(content),
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum SearchRoomFilter {
|
||||
/// All the joined rooms (= DMs + non-DMs).
|
||||
Rooms,
|
||||
/// Only joined DM rooms.
|
||||
Dms,
|
||||
/// Only joined non-DM (group) rooms.
|
||||
NonDms,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl Client {
|
||||
/// Search across all all rooms for the given query, returning an iterator
|
||||
/// over the results.
|
||||
pub async fn search_messages(
|
||||
&self,
|
||||
query: String,
|
||||
filter: SearchRoomFilter,
|
||||
num_results_per_batch: u32,
|
||||
) -> Result<GlobalSearchIterator, ClientError> {
|
||||
let sdk_client = (*self.inner).clone();
|
||||
let mut search = sdk_client.search_messages(query, num_results_per_batch as usize);
|
||||
|
||||
match filter {
|
||||
SearchRoomFilter::Rooms => {}
|
||||
SearchRoomFilter::Dms => search = search.only_dm_rooms().await?,
|
||||
SearchRoomFilter::NonDms => search = search.no_dms().await?,
|
||||
}
|
||||
|
||||
Ok(GlobalSearchIterator { sdk_client, inner: Mutex::new(search.build()) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct GlobalSearchResult {
|
||||
room_id: String,
|
||||
result: RoomSearchResult,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct GlobalSearchIterator {
|
||||
sdk_client: matrix_sdk::Client,
|
||||
inner: Mutex<SdkGlobalSearchIterator>,
|
||||
}
|
||||
|
||||
#[matrix_sdk_ffi_macros::export]
|
||||
impl GlobalSearchIterator {
|
||||
/// Return a list of events for the next batch of search results, or `None`
|
||||
/// if there are no more results.
|
||||
pub async fn next_events(&self) -> Result<Option<Vec<GlobalSearchResult>>, SearchError> {
|
||||
let Some(events) = self.inner.lock().await.next_events().await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut results = Vec::with_capacity(events.len());
|
||||
|
||||
for (room_id, event) in events {
|
||||
let Some(room) = self.sdk_client.get_room(&room_id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(result) = RoomSearchResult::from(&room, event).await {
|
||||
results.push(GlobalSearchResult { room_id: room_id.to_string(), result });
|
||||
}
|
||||
}
|
||||
|
||||
results.shrink_to_fit();
|
||||
|
||||
Ok(Some(results))
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,13 @@ use std::sync::{Arc, RwLock};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use matrix_sdk::{
|
||||
Account,
|
||||
encryption::{
|
||||
Encryption,
|
||||
identities::UserIdentity,
|
||||
verification::{SasState, SasVerification, VerificationRequest, VerificationRequestState},
|
||||
Encryption,
|
||||
},
|
||||
ruma::events::key::verification::VerificationMethod,
|
||||
Account,
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use ruma::UserId;
|
||||
@@ -246,16 +246,15 @@ impl SessionVerificationController {
|
||||
sender: &UserId,
|
||||
flow_id: impl AsRef<str>,
|
||||
) {
|
||||
if sender != self.user_identity.user_id() {
|
||||
if let Some(status) = self.encryption.cross_signing_status().await {
|
||||
if !status.is_complete() {
|
||||
warn!(
|
||||
"Cannot verify other users until our own device's cross-signing status \
|
||||
is complete: {status:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if sender != self.user_identity.user_id()
|
||||
&& let Some(status) = self.encryption.cross_signing_status().await
|
||||
&& !status.is_complete()
|
||||
{
|
||||
warn!(
|
||||
"Cannot verify other users until our own device's cross-signing status \
|
||||
is complete: {status:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(request) = self.encryption.get_verification_request(sender, flow_id).await else {
|
||||
@@ -290,15 +289,10 @@ impl SessionVerificationController {
|
||||
) -> Result<(), ClientError> {
|
||||
if let Some(ongoing_verification_request) =
|
||||
self.verification_request.read().unwrap().clone()
|
||||
&& !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
if !ongoing_verification_request.is_done()
|
||||
&& !ongoing_verification_request.is_cancelled()
|
||||
{
|
||||
return Err(ClientError::from_str(
|
||||
"There is another verification flow ongoing.",
|
||||
None,
|
||||
));
|
||||
}
|
||||
return Err(ClientError::from_str("There is another verification flow ongoing.", None));
|
||||
}
|
||||
|
||||
*self.verification_request.write().unwrap() = Some(verification_request.clone());
|
||||
|
||||
@@ -15,23 +15,23 @@
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
use eyeball_im::VectorDiff;
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use futures_util::{StreamExt, pin_mut};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::spaces::{
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
SpaceFilter as UISpaceFilter, SpaceRoom as UISpaceRoom, SpaceRoomList as UISpaceRoomList,
|
||||
SpaceService as UISpaceService,
|
||||
leave::{LeaveSpaceHandle as UILeaveSpaceHandle, LeaveSpaceRoom as UILeaveSpaceRoom},
|
||||
room_list::SpaceRoomListPaginationState,
|
||||
};
|
||||
use ruma::RoomId;
|
||||
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
client::JoinRule,
|
||||
error::ClientError,
|
||||
room::{Membership, RoomHero},
|
||||
room_preview::RoomType,
|
||||
runtime::get_runtime_handle,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
/// The main entry point into the Spaces facilities.
|
||||
|
||||
@@ -26,8 +26,8 @@ use matrix_sdk_ui::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
|
||||
runtime::get_runtime_handle, TaskHandle,
|
||||
TaskHandle, error::ClientError, helpers::unwrap_or_clone_arc, room_list::RoomListService,
|
||||
runtime::get_runtime_handle,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
@@ -128,6 +128,28 @@ impl SyncServiceBuilder {
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
/// Set a custom Sliding Sync connection ID for the room list service.
|
||||
///
|
||||
/// By default [`matrix_sdk_ui::room_list_service::DEFAULT_CONNECTION_ID`]
|
||||
/// is used. Set a different value for secondary processes such as iOS
|
||||
/// Share Extensions that are not meant to reuse the main app's
|
||||
/// connection.
|
||||
pub fn with_room_list_connection_id(self: Arc<Self>, connection_id: String) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_room_list_conn_id(connection_id);
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
/// Set a custom timeline limit for the room list service.
|
||||
///
|
||||
/// When set, overrides the default timeline limit of
|
||||
/// [`matrix_sdk_ui::room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`].
|
||||
pub fn with_room_list_timeline_limit(self: Arc<Self>, limit: u32) -> Arc<Self> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
let builder = this.builder.with_room_list_timeline_limit(limit);
|
||||
Arc::new(Self { builder, ..this })
|
||||
}
|
||||
|
||||
pub async fn finish(self: Arc<Self>) -> Result<Arc<SyncService>, ClientError> {
|
||||
let this = unwrap_or_clone_arc(self);
|
||||
Ok(Arc::new(SyncService {
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::{
|
||||
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
|
||||
TimelineEventFocusThreadMode, TimelineReadReceiptTracking,
|
||||
event_filter::{TimelineEventCondition, TimelineEventFilter as InnerTimelineEventFilter},
|
||||
};
|
||||
use ruma::{
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
EventId,
|
||||
events::{AnySyncTimelineEvent, TimelineEventType},
|
||||
};
|
||||
|
||||
use super::FocusEventError;
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::collections::HashMap;
|
||||
use matrix_sdk::room::power_levels::power_level_user_changes;
|
||||
use matrix_sdk_ui::timeline::RoomPinnedEventsChange;
|
||||
use ruma::events::{
|
||||
room::history_visibility::HistoryVisibility as RumaHistoryVisibility, StateEventContentChange,
|
||||
StateEventContentChange, room::history_visibility::HistoryVisibility as RumaHistoryVisibility,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -40,7 +40,9 @@ impl From<matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent
|
||||
|
||||
Content::CallInvite => TimelineItemContent::CallInvite,
|
||||
|
||||
Content::RtcNotification => TimelineItemContent::RtcNotification,
|
||||
Content::RtcNotification { call_intent } => TimelineItemContent::RtcNotification {
|
||||
call_intent: call_intent.map(|s| s.to_string()),
|
||||
},
|
||||
|
||||
Content::MembershipChange(membership) => {
|
||||
let reason = match membership.content() {
|
||||
@@ -159,7 +161,9 @@ pub enum TimelineItemContent {
|
||||
content: MsgLikeContent,
|
||||
},
|
||||
CallInvite,
|
||||
RtcNotification,
|
||||
RtcNotification {
|
||||
call_intent: Option<String>,
|
||||
},
|
||||
RoomMembership {
|
||||
user_id: String,
|
||||
user_display_name: Option<String>,
|
||||
@@ -265,7 +269,6 @@ pub enum OtherState {
|
||||
PolicyRuleRoom,
|
||||
PolicyRuleServer,
|
||||
PolicyRuleUser,
|
||||
RoomAliases,
|
||||
RoomAvatar {
|
||||
url: Option<String>,
|
||||
},
|
||||
@@ -363,7 +366,6 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherStateEventContentChange> for OtherSt
|
||||
Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
|
||||
Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
|
||||
Content::PolicyRuleUser(_) => Self::PolicyRuleUser,
|
||||
Content::RoomAliases(_) => Self::RoomAliases,
|
||||
Content::RoomAvatar(c) => {
|
||||
let url = match c {
|
||||
FullContent::Original { content, .. } => {
|
||||
|
||||
@@ -38,8 +38,9 @@ use matrix_sdk_ui::timeline::{
|
||||
use mime::Mime;
|
||||
use reply::{EmbeddedEventDetails, InReplyToDetails};
|
||||
use ruma::{
|
||||
assign,
|
||||
EventId, UInt, assign,
|
||||
events::{
|
||||
AnyMessageLikeEventContent,
|
||||
location::{AssetType as RumaAssetType, LocationContent, ZoomLevel},
|
||||
poll::{
|
||||
unstable_end::UnstablePollEndEventContent,
|
||||
@@ -53,9 +54,7 @@ use ruma::{
|
||||
LocationMessageEventContent, MessageType, RoomMessageEventContentWithoutRelation,
|
||||
TextMessageEventContent,
|
||||
},
|
||||
AnyMessageLikeEventContent,
|
||||
},
|
||||
EventId, UInt,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{error, warn};
|
||||
@@ -284,8 +283,17 @@ impl Timeline {
|
||||
// be that the listener be called before the initial items have been
|
||||
// handled by the caller. See #3535 for details.
|
||||
|
||||
// First, pass all the items as a reset update.
|
||||
listener.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
|
||||
// Note we pass initial items as a reset update, as a way to give the callers a
|
||||
// unified way to handle the initial batch of items as well as other
|
||||
// batches, instead of having a separate callback for the initial items.
|
||||
//
|
||||
// Start with passing all the items of a *non-empty* timeline as a reset update
|
||||
// (if the initial items are empty, then the timeline would transition
|
||||
// from empty to empty, which is a no-op).
|
||||
if !timeline_items.is_empty() {
|
||||
listener
|
||||
.on_update(vec![TimelineDiff::new(VectorDiff::Reset { values: timeline_items })]);
|
||||
}
|
||||
|
||||
Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move {
|
||||
pin_mut!(timeline_stream);
|
||||
@@ -1012,6 +1020,9 @@ pub struct EventTimelineItem {
|
||||
is_own: bool,
|
||||
is_editable: bool,
|
||||
content: TimelineItemContent,
|
||||
/// The raw Matrix event type string (e.g. `"m.room.message"`), or `None`
|
||||
/// when the original type is not available (e.g. redacted events).
|
||||
event_type_raw: Option<String>,
|
||||
timestamp: Timestamp,
|
||||
local_send_state: Option<EventSendState>,
|
||||
local_created_at: Option<u64>,
|
||||
@@ -1037,6 +1048,7 @@ impl From<matrix_sdk_ui::timeline::EventTimelineItem> for EventTimelineItem {
|
||||
is_own: item.is_own(),
|
||||
is_editable: item.is_editable(),
|
||||
content: item.content().clone().into(),
|
||||
event_type_raw: item.content().event_type_str(),
|
||||
timestamp: item.timestamp().into(),
|
||||
local_send_state: item.send_state().map(|s| s.into()),
|
||||
local_created_at: item.local_created_at().map(|t| t.0.into()),
|
||||
@@ -1314,6 +1326,13 @@ impl LazyTimelineItemProvider {
|
||||
fn contains_only_emojis(&self) -> bool {
|
||||
self.0.contains_only_emojis()
|
||||
}
|
||||
|
||||
/// Returns the full raw JSON string of the latest version of the event
|
||||
/// (including edits). Returns `None` for local echoes that haven't been
|
||||
/// echoed back by the server yet.
|
||||
fn latest_json(&self) -> Option<String> {
|
||||
Some(self.0.latest_json()?.json().get().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// Mimic the [`UiLatestEventValue`] type.
|
||||
@@ -1387,7 +1406,7 @@ mod galleries {
|
||||
use matrix_sdk_common::executor::{AbortHandle, JoinHandle};
|
||||
use matrix_sdk_ui::timeline::GalleryConfig;
|
||||
use mime::Mime;
|
||||
use ruma::{assign, events::room::message::TextMessageEventContent, EventId};
|
||||
use ruma::{EventId, assign, events::room::message::TextMessageEventContent};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
@@ -1395,7 +1414,7 @@ mod galleries {
|
||||
error::RoomError,
|
||||
ruma::{AudioInfo, FileInfo, FormattedBody, ImageInfo, Mentions, VideoInfo},
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{build_thumbnail_info, Timeline, UploadSource},
|
||||
timeline::{Timeline, UploadSource, build_thumbnail_info},
|
||||
};
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_base::crypto::types::events::UtdCause;
|
||||
use ruma::events::{room::MediaSource as RumaMediaSource, MessageLikeEventContent};
|
||||
use ruma::events::{MessageLikeEventContent, room::MediaSource as RumaMediaSource};
|
||||
|
||||
use super::{
|
||||
content::{BeaconInfo, LiveLocationContent, Reaction},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
use matrix_sdk_ui::timeline::{EmbeddedEvent, TimelineDetails};
|
||||
|
||||
use super::{content::TimelineItemContent, ProfileDetails};
|
||||
use super::{ProfileDetails, content::TimelineItemContent};
|
||||
use crate::{event::EventOrTransactionId, utils::Timestamp};
|
||||
|
||||
#[derive(Clone, uniffi::Object)]
|
||||
|
||||
@@ -21,22 +21,22 @@ use matrix_sdk::room::{
|
||||
};
|
||||
use matrix_sdk_common::{SendOutsideWasm, SyncOutsideWasm};
|
||||
use matrix_sdk_ui::timeline::{
|
||||
RoomExt,
|
||||
thread_list_service::{
|
||||
ThreadListItem as UIThreadListItem, ThreadListItemEvent as UIThreadListItemEvent,
|
||||
ThreadListPaginationState as UIThreadListPaginationState,
|
||||
ThreadListService as UIThreadListService,
|
||||
ThreadListServiceError as UIThreadListServiceError,
|
||||
},
|
||||
RoomExt,
|
||||
};
|
||||
use ruma::api::client::threads::get_threads::v1::IncludeThreads as SdkIncludeThreads;
|
||||
|
||||
use crate::{
|
||||
TaskHandle,
|
||||
error::ClientError,
|
||||
runtime::get_runtime_handle,
|
||||
timeline::{ProfileDetails, TimelineItemContent},
|
||||
utils::Timestamp,
|
||||
TaskHandle,
|
||||
};
|
||||
|
||||
/// A thread subscription (MSC4306).
|
||||
|
||||
@@ -41,10 +41,10 @@ impl UnableToDecryptHook for UtdHook {
|
||||
|
||||
// UTDs that have been decrypted in the `IGNORE_UTD_PERIOD` are just ignored and
|
||||
// not considered UTDs.
|
||||
if let Some(duration) = &info.time_to_decrypt {
|
||||
if *duration < IGNORE_UTD_PERIOD {
|
||||
return;
|
||||
}
|
||||
if let Some(duration) = &info.time_to_decrypt
|
||||
&& *duration < IGNORE_UTD_PERIOD
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Report the UTD to the client.
|
||||
|
||||
@@ -266,6 +266,7 @@ pub fn get_element_call_required_permissions(
|
||||
requires_client: true,
|
||||
update_delayed_event: true,
|
||||
send_delayed_event: true,
|
||||
download_files: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +332,8 @@ pub struct WidgetCapabilities {
|
||||
pub update_delayed_event: bool,
|
||||
/// This allows the widget to send events with a delay.
|
||||
pub send_delayed_event: bool,
|
||||
/// This allows the widget to download files (avatars)
|
||||
pub download_files: bool,
|
||||
}
|
||||
|
||||
impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
|
||||
@@ -341,6 +344,7 @@ impl From<WidgetCapabilities> for matrix_sdk::widget::Capabilities {
|
||||
requires_client: value.requires_client,
|
||||
update_delayed_event: value.update_delayed_event,
|
||||
send_delayed_event: value.send_delayed_event,
|
||||
download_file: value.download_files,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -353,6 +357,7 @@ impl From<matrix_sdk::widget::Capabilities> for WidgetCapabilities {
|
||||
requires_client: value.requires_client,
|
||||
update_delayed_event: value.update_delayed_event,
|
||||
send_delayed_event: value.send_delayed_event,
|
||||
download_files: value.download_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -553,5 +558,8 @@ mod tests {
|
||||
// RTC decline
|
||||
cap_assert("org.matrix.msc2762.receive.event:org.matrix.msc4310.rtc.decline");
|
||||
cap_assert("org.matrix.msc2762.send.event:org.matrix.msc4310.rtc.decline");
|
||||
|
||||
// Download avatars
|
||||
cap_assert("org.matrix.msc4039.download_file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] `TtlStoreValue` was moved and renamed to `matrix_sdk_common::ttl_cache::TtlValue`.
|
||||
- [**breaking**] `Gap::prev_token` has been renamed to `Gap::token` since it's now used for both
|
||||
the previous batch token and the next batch token.
|
||||
([#6236](https://github.com/matrix-org/matrix-rust-sdk/pull/6236))
|
||||
|
||||
@@ -40,6 +40,12 @@ experimental-encrypted-state-events = [
|
||||
"matrix-sdk-crypto?/experimental-encrypted-state-events"
|
||||
]
|
||||
|
||||
# Enable experimental support for pushing secrets
|
||||
experimental-push-secrets = [
|
||||
"e2e-encryption",
|
||||
"matrix-sdk-crypto?/experimental-push-secrets"
|
||||
]
|
||||
|
||||
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]
|
||||
|
||||
# Private feature, see
|
||||
@@ -113,6 +119,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
wasm-bindgen-test.workspace = true
|
||||
getrandom3 = { version = "0.3.4", package = "getrandom", default-features = false, features = ["wasm_js"] }
|
||||
gloo-timers = { workspace = true, features = ["futures"] }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -202,7 +202,6 @@ fn update_push_room_context(
|
||||
|
||||
push_rules.member_count = UInt::new(room_info.active_members_count()).unwrap_or(UInt::MAX);
|
||||
|
||||
// TODO: Use if let chain once stable
|
||||
if let Some(member) = context.state_changes.member(room_id, user_id) {
|
||||
push_rules.user_display_name =
|
||||
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
@@ -230,7 +229,6 @@ pub async fn get_push_room_context(
|
||||
|
||||
let member_count = room_info.active_members_count();
|
||||
|
||||
// TODO: Use if let chain once stable
|
||||
let user_display_name = if let Some(member) = context.state_changes.member(room_id, user_id) {
|
||||
member.content.displayname.unwrap_or_else(|| user_id.localpart().to_owned())
|
||||
} else if let Some(member) = Box::pin(room.get_member(user_id)).await? {
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::{
|
||||
use assert_matches::assert_matches;
|
||||
use assert_matches2::assert_let;
|
||||
use growable_bloom_filter::GrowableBloomBuilder;
|
||||
use matrix_sdk_common::ttl_cache::TtlValue;
|
||||
use matrix_sdk_test::{TestResult, event_factory::EventFactory};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
@@ -46,7 +47,7 @@ use serde_json::json;
|
||||
|
||||
use super::{
|
||||
DependentQueuedRequestKind, DisplayName, DynStateStore, RoomLoadSettings,
|
||||
SupportedVersionsResponse, TtlStoreValue, WellKnownResponse, send_queue::SentRequestKey,
|
||||
SupportedVersionsResponse, WellKnownResponse, send_queue::SentRequestKey,
|
||||
};
|
||||
use crate::{
|
||||
RoomInfo, RoomMemberships, RoomState, StateChanges, StateStoreDataKey, StateStoreDataValue,
|
||||
@@ -531,7 +532,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::SupportedVersions,
|
||||
StateStoreDataValue::SupportedVersions(TtlStoreValue::new(supported_versions.clone())),
|
||||
StateStoreDataValue::SupportedVersions(TtlValue::new(supported_versions.clone())),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -539,7 +540,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(Some(StateStoreDataValue::SupportedVersions(stored_supported_versions))) =
|
||||
self.get_kv_data(StateStoreDataKey::SupportedVersions).await
|
||||
);
|
||||
assert_let!(Some(stored_supported_versions) = stored_supported_versions.into_data());
|
||||
let stored_supported_versions = stored_supported_versions.into_data();
|
||||
assert_eq!(supported_versions, stored_supported_versions);
|
||||
|
||||
let stored_supported = stored_supported_versions.supported_versions();
|
||||
@@ -563,7 +564,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::WellKnown,
|
||||
StateStoreDataValue::WellKnown(TtlStoreValue::new(Some(well_known.clone()))),
|
||||
StateStoreDataValue::WellKnown(TtlValue::new(Some(well_known.clone()))),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -571,7 +572,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
|
||||
self.get_kv_data(StateStoreDataKey::WellKnown).await
|
||||
);
|
||||
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
|
||||
let stored_well_known = stored_well_known.into_data();
|
||||
assert_eq!(stored_well_known, Some(well_known));
|
||||
|
||||
self.remove_kv_data(StateStoreDataKey::WellKnown).await?;
|
||||
@@ -579,7 +580,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
|
||||
self.set_kv_data(
|
||||
StateStoreDataKey::WellKnown,
|
||||
StateStoreDataValue::WellKnown(TtlStoreValue::new(None)),
|
||||
StateStoreDataValue::WellKnown(TtlValue::new(None)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -587,7 +588,7 @@ impl StateStoreIntegrationTests for DynStateStore {
|
||||
Ok(Some(StateStoreDataValue::WellKnown(stored_well_known))) =
|
||||
self.get_kv_data(StateStoreDataKey::WellKnown).await
|
||||
);
|
||||
assert_let!(Some(stored_well_known) = stored_well_known.into_data());
|
||||
let stored_well_known = stored_well_known.into_data();
|
||||
assert_eq!(stored_well_known, None);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -20,10 +20,11 @@ use std::{
|
||||
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloom;
|
||||
use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK};
|
||||
use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, ttl_cache::TtlValue};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri,
|
||||
OwnedRoomId, OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
api::client::discovery::get_capabilities::v3::Capabilities,
|
||||
canonical_json::{RedactedBecause, redact},
|
||||
events::{
|
||||
AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
|
||||
@@ -40,7 +41,7 @@ use tracing::{debug, instrument, warn};
|
||||
use super::{
|
||||
DependentQueuedRequest, DependentQueuedRequestKind, QueuedRequestKind, Result, RoomInfo,
|
||||
RoomLoadSettings, StateChanges, StateStore, StoreError, SupportedVersionsResponse,
|
||||
TtlStoreValue, WellKnownResponse,
|
||||
WellKnownResponse,
|
||||
send_queue::{ChildTransactionId, QueuedRequest, SentRequestKey},
|
||||
traits::ComposerDraft,
|
||||
};
|
||||
@@ -60,8 +61,8 @@ struct MemoryStoreInner {
|
||||
composer_drafts: HashMap<(OwnedRoomId, Option<OwnedEventId>), ComposerDraft>,
|
||||
user_avatar_url: HashMap<OwnedUserId, OwnedMxcUri>,
|
||||
sync_token: Option<String>,
|
||||
supported_versions: Option<TtlStoreValue<SupportedVersionsResponse>>,
|
||||
well_known: Option<TtlStoreValue<Option<WellKnownResponse>>>,
|
||||
supported_versions: Option<TtlValue<SupportedVersionsResponse>>,
|
||||
well_known: Option<TtlValue<Option<WellKnownResponse>>>,
|
||||
filters: HashMap<String, String>,
|
||||
utd_hook_manager_data: Option<GrowableBloom>,
|
||||
one_time_key_uploaded_error: bool,
|
||||
@@ -92,6 +93,7 @@ struct MemoryStoreInner {
|
||||
seen_knock_requests: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, OwnedUserId>>,
|
||||
thread_subscriptions: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, StoredThreadSubscription>>,
|
||||
thread_subscriptions_catchup_tokens: Option<Vec<ThreadSubscriptionCatchupToken>>,
|
||||
homeserver_capabilities: Option<TtlValue<Capabilities>>,
|
||||
}
|
||||
|
||||
/// In-memory, non-persistent implementation of the `StateStore`.
|
||||
@@ -195,6 +197,10 @@ impl StateStore for MemoryStore {
|
||||
.thread_subscriptions_catchup_tokens
|
||||
.clone()
|
||||
.map(StateStoreDataValue::ThreadSubscriptionsCatchupTokens),
|
||||
StateStoreDataKey::HomeserverCapabilities => inner
|
||||
.homeserver_capabilities
|
||||
.clone()
|
||||
.map(StateStoreDataValue::HomeserverCapabilities),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -270,6 +276,13 @@ impl StateStore for MemoryStore {
|
||||
"Session data is not a list of thread subscription catchup tokens",
|
||||
));
|
||||
}
|
||||
StateStoreDataKey::HomeserverCapabilities => {
|
||||
inner.homeserver_capabilities = Some(
|
||||
value
|
||||
.into_homeserver_capabilities()
|
||||
.expect("Session data is not a homeserver capabilities"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -304,6 +317,7 @@ impl StateStore for MemoryStore {
|
||||
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => {
|
||||
inner.thread_subscriptions_catchup_tokens = None;
|
||||
}
|
||||
StateStoreDataKey::HomeserverCapabilities => inner.homeserver_capabilities = None,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ pub use self::{
|
||||
traits::{
|
||||
ComposerDraft, ComposerDraftType, DraftAttachment, DraftAttachmentContent, DraftThumbnail,
|
||||
DynStateStore, IntoStateStore, StateStore, StateStoreDataKey, StateStoreDataValue,
|
||||
StateStoreExt, SupportedVersionsResponse, ThreadSubscriptionCatchupToken, TtlStoreValue,
|
||||
StateStoreExt, SupportedVersionsResponse, ThreadSubscriptionCatchupToken,
|
||||
WellKnownResponse,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -117,6 +117,14 @@ pub enum QueuedRequestKind {
|
||||
#[serde(default)]
|
||||
accumulated: Vec<AccumulatedSentMediaInfo>,
|
||||
},
|
||||
|
||||
/// A redaction of another event to send.
|
||||
Redaction {
|
||||
/// The ID of the event to redact.
|
||||
redacts: OwnedEventId,
|
||||
/// The reason for the event being redacted.
|
||||
reason: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<SerializableEventContent> for QueuedRequestKind {
|
||||
@@ -421,12 +429,27 @@ pub enum SentRequestKey {
|
||||
|
||||
/// The parent transaction returned an uploaded resource URL.
|
||||
Media(SentMediaInfo),
|
||||
|
||||
/// The parent transaction returned a redaction event when it succeeded.
|
||||
Redaction {
|
||||
/// The event ID returned by the server.
|
||||
event_id: OwnedEventId,
|
||||
|
||||
/// The ID of the redacted event.
|
||||
redacts: OwnedEventId,
|
||||
|
||||
/// The reason for the event being redacted.
|
||||
reason: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl SentRequestKey {
|
||||
/// Converts the current parent key into an event id, if possible.
|
||||
pub fn into_event_id(self) -> Option<OwnedEventId> {
|
||||
as_variant!(self, Self::Event { event_id, .. } => event_id)
|
||||
match self {
|
||||
Self::Event { event_id, .. } | Self::Redaction { event_id, .. } => Some(event_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the current parent key into information about a sent media, if
|
||||
|
||||
@@ -22,14 +22,17 @@ use std::{
|
||||
use as_variant::as_variant;
|
||||
use async_trait::async_trait;
|
||||
use growable_bloom_filter::GrowableBloom;
|
||||
use matrix_sdk_common::AsyncTraitDeps;
|
||||
use matrix_sdk_common::{AsyncTraitDeps, ttl_cache::TtlValue};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId,
|
||||
OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId,
|
||||
api::{
|
||||
SupportedVersions,
|
||||
client::discovery::discover_homeserver::{
|
||||
self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo,
|
||||
MatrixVersion, SupportedVersions,
|
||||
client::discovery::{
|
||||
discover_homeserver::{
|
||||
self, HomeserverInfo, IdentityServerInfo, RtcFocusInfo, TileServerInfo,
|
||||
},
|
||||
get_capabilities::v3::Capabilities,
|
||||
},
|
||||
},
|
||||
events::{
|
||||
@@ -41,7 +44,6 @@ use ruma::{
|
||||
receipt::{Receipt, ReceiptThread, ReceiptType},
|
||||
},
|
||||
serde::Raw,
|
||||
time::SystemTime,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -258,7 +260,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
event_type: RoomAccountDataEventType,
|
||||
) -> Result<Option<Raw<AnyRoomAccountDataEvent>>, Self::Error>;
|
||||
|
||||
/// Get an event out of the user room receipt store.
|
||||
/// Get a user's read receipt for a given room and receipt type and thread.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -269,7 +271,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
///
|
||||
/// * `thread` - The thread containing this receipt.
|
||||
///
|
||||
/// * `user_id` - The id of the user for who the receipt should be fetched.
|
||||
/// * `user_id` - The id of the user for whom the receipt should be fetched.
|
||||
async fn get_user_room_receipt_event(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
@@ -278,7 +280,7 @@ pub trait StateStore: AsyncTraitDeps {
|
||||
user_id: &UserId,
|
||||
) -> Result<Option<(OwnedEventId, Receipt)>, Self::Error>;
|
||||
|
||||
/// Get events out of the event room receipt store.
|
||||
/// Get an event's read receipts for a given room, receipt type, and thread.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -1041,37 +1043,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A TTL value in the store whose data can only be accessed before it expires.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TtlStoreValue<T> {
|
||||
/// The data of the item.
|
||||
#[serde(flatten)]
|
||||
data: T,
|
||||
|
||||
/// Last time we fetched this data from the server, in milliseconds since
|
||||
/// epoch.
|
||||
last_fetch_ts: f64,
|
||||
}
|
||||
|
||||
impl<T> TtlStoreValue<T> {
|
||||
/// The number of milliseconds after which the data is considered stale.
|
||||
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24 * 7) as _; // seven days
|
||||
|
||||
/// Construct a new `TtlStoreValue` with the given data.
|
||||
pub fn new(data: T) -> Self {
|
||||
Self { data, last_fetch_ts: now_timestamp_ms() }
|
||||
}
|
||||
|
||||
/// Get the data of this value, if it hasn't expired.
|
||||
pub fn into_data(self) -> Option<T> {
|
||||
if now_timestamp_ms() - self.last_fetch_ts >= Self::STALE_THRESHOLD {
|
||||
None
|
||||
} else {
|
||||
Some(self.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialisable representation of get_supported_versions::Response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SupportedVersionsResponse {
|
||||
@@ -1089,7 +1060,16 @@ impl SupportedVersionsResponse {
|
||||
/// Note: Matrix versions and features that Ruma cannot parse, or does not
|
||||
/// know about, are discarded.
|
||||
pub fn supported_versions(&self) -> SupportedVersions {
|
||||
SupportedVersions::from_parts(&self.versions, &self.unstable_features)
|
||||
let mut supported_versions =
|
||||
SupportedVersions::from_parts(&self.versions, &self.unstable_features);
|
||||
|
||||
// We need at least one supported version to be able to make requests, so we
|
||||
// default to Matrix 1.0.
|
||||
if supported_versions.versions.is_empty() {
|
||||
supported_versions.versions.insert(MatrixVersion::V1_0);
|
||||
}
|
||||
|
||||
supported_versions
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1120,15 +1100,6 @@ impl From<discover_homeserver::Response> for WellKnownResponse {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
|
||||
fn now_timestamp_ms() -> f64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("System clock was before 1970.")
|
||||
.as_secs_f64()
|
||||
* 1000.0
|
||||
}
|
||||
|
||||
/// A value for key-value data that should be persisted into the store.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StateStoreDataValue {
|
||||
@@ -1136,10 +1107,10 @@ pub enum StateStoreDataValue {
|
||||
SyncToken(String),
|
||||
|
||||
/// The supported versions of the server.
|
||||
SupportedVersions(TtlStoreValue<SupportedVersionsResponse>),
|
||||
SupportedVersions(TtlValue<SupportedVersionsResponse>),
|
||||
|
||||
/// The well-known information of the server.
|
||||
WellKnown(TtlStoreValue<Option<WellKnownResponse>>),
|
||||
WellKnown(TtlValue<Option<WellKnownResponse>>),
|
||||
|
||||
/// A filter with the given ID.
|
||||
Filter(String),
|
||||
@@ -1172,6 +1143,9 @@ pub enum StateStoreDataValue {
|
||||
/// See documentation of [`ThreadSubscriptionCatchupToken`] for more
|
||||
/// details.
|
||||
ThreadSubscriptionsCatchupTokens(Vec<ThreadSubscriptionCatchupToken>),
|
||||
|
||||
/// The capabilities the homeserver supports or disables.
|
||||
HomeserverCapabilities(TtlValue<Capabilities>),
|
||||
}
|
||||
|
||||
/// Tokens to use when catching up on thread subscriptions.
|
||||
@@ -1352,12 +1326,12 @@ impl StateStoreDataValue {
|
||||
}
|
||||
|
||||
/// Get this value if it is the supported versions metadata.
|
||||
pub fn into_supported_versions(self) -> Option<TtlStoreValue<SupportedVersionsResponse>> {
|
||||
pub fn into_supported_versions(self) -> Option<TtlValue<SupportedVersionsResponse>> {
|
||||
as_variant!(self, Self::SupportedVersions)
|
||||
}
|
||||
|
||||
/// Get this value if it is the well-known metadata.
|
||||
pub fn into_well_known(self) -> Option<TtlStoreValue<Option<WellKnownResponse>>> {
|
||||
pub fn into_well_known(self) -> Option<TtlValue<Option<WellKnownResponse>>> {
|
||||
as_variant!(self, Self::WellKnown)
|
||||
}
|
||||
|
||||
@@ -1373,6 +1347,12 @@ impl StateStoreDataValue {
|
||||
) -> Option<Vec<ThreadSubscriptionCatchupToken>> {
|
||||
as_variant!(self, Self::ThreadSubscriptionsCatchupTokens)
|
||||
}
|
||||
|
||||
/// Get this value if it is the data for the capabilities the homeserver
|
||||
/// supports or disables.
|
||||
pub fn into_homeserver_capabilities(self) -> Option<TtlValue<Capabilities>> {
|
||||
as_variant!(self, Self::HomeserverCapabilities)
|
||||
}
|
||||
}
|
||||
|
||||
/// A key for key-value data.
|
||||
@@ -1415,6 +1395,9 @@ pub enum StateStoreDataKey<'a> {
|
||||
|
||||
/// A list of thread subscriptions catchup tokens.
|
||||
ThreadSubscriptionsCatchupTokens,
|
||||
|
||||
/// A list of capabilities that the homeserver supports.
|
||||
HomeserverCapabilities,
|
||||
}
|
||||
|
||||
impl StateStoreDataKey<'_> {
|
||||
@@ -1460,6 +1443,9 @@ impl StateStoreDataKey<'_> {
|
||||
/// [`ThreadSubscriptionsCatchupTokens`][Self::ThreadSubscriptionsCatchupTokens] variant.
|
||||
pub const THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS: &'static str =
|
||||
"thread_subscriptions_catchup_tokens";
|
||||
|
||||
/// Key prefix to use for the homeserver's [`Capabilities`].
|
||||
pub const HOMESERVER_CAPABILITIES: &'static str = "homeserver_capabilities";
|
||||
}
|
||||
|
||||
/// Compare two thread subscription changes bump stamps, given a fixed room and
|
||||
@@ -1492,47 +1478,3 @@ pub fn compare_thread_subscription_bump_stamps(
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{SupportedVersionsResponse, TtlStoreValue, now_timestamp_ms};
|
||||
|
||||
#[test]
|
||||
fn test_stale_ttl_store_value() {
|
||||
// Definitely stale.
|
||||
let ttl_value = TtlStoreValue {
|
||||
data: (),
|
||||
last_fetch_ts: now_timestamp_ms() - TtlStoreValue::<()>::STALE_THRESHOLD - 1.0,
|
||||
};
|
||||
assert!(ttl_value.into_data().is_none());
|
||||
|
||||
// Definitely not stale.
|
||||
let ttl_value = TtlStoreValue::new(());
|
||||
assert!(ttl_value.into_data().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stale_ttl_store_value_serialize_roundtrip() {
|
||||
let server_info = SupportedVersionsResponse {
|
||||
versions: vec!["1.2".to_owned(), "1.3".to_owned(), "1.4".to_owned()],
|
||||
unstable_features: [("org.matrix.msc3916.stable".to_owned(), true)].into(),
|
||||
};
|
||||
let ttl_value = TtlStoreValue { data: server_info.clone(), last_fetch_ts: 1000.0 };
|
||||
let json = json!({
|
||||
"versions": ["1.2", "1.3", "1.4"],
|
||||
"unstable_features": {
|
||||
"org.matrix.msc3916.stable": true,
|
||||
},
|
||||
"last_fetch_ts": 1000.0,
|
||||
});
|
||||
|
||||
assert_eq!(serde_json::to_value(&ttl_value).unwrap(), json);
|
||||
|
||||
let deserialized =
|
||||
serde_json::from_value::<TtlStoreValue<SupportedVersionsResponse>>(json).unwrap();
|
||||
assert_eq!(deserialized.data, server_info);
|
||||
assert!(deserialized.last_fetch_ts - ttl_value.last_fetch_ts < 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,23 @@ All notable changes to this project will be documented in this file.
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] Change to the stable identifiers for `m.history_not_shared`.
|
||||
We still support reading the unstable identifier.
|
||||
([#6467](https://github.com/matrix-org/matrix-rust-sdk/pull/6467))
|
||||
- Add a method to check the validity of edits.
|
||||
([#6454](https://github.com/matrix-org/matrix-rust-sdk/pull/6454))
|
||||
- A background task monitor has been added, that can spawn background tasks and monitor their
|
||||
execution on a separate channel. Such tasks can run forever, or they can run for one-shot jobs.
|
||||
([#6075](https://github.com/matrix-org/matrix-rust-sdk/pull/6075) &&
|
||||
[#6421](https://github.com/matrix-org/matrix-rust-sdk/pull/6421))
|
||||
- Add `AcquireCrossProcessLockResult` and `AcquireCrossProcessLockFn`
|
||||
for convenience in generalizing cross-process lock acquisition.
|
||||
([#6326](https://github.com/matrix-org/matrix-rust-sdk/pull/6326))
|
||||
- Add support in the `MemoryStore`'s implementation of `EventCacheStore` for
|
||||
having duplicate events in a room, where each duplicate is in a different
|
||||
`LinkedChunk`. This is useful, e.g., when an event is in a room and a
|
||||
thread in that room.
|
||||
|
||||
|
||||
- [**breaking**] In order to support having duplicate events in the same room (in different `LinkedChunk`'s) a few
|
||||
functions were changed in `RelationalLinkedChunk`. The items in the `Iterator` returned by `RelationalLinkedChunk::items`
|
||||
now also include the `LinkedChunkId` in which the `Item` was found. Additionally, `RelationalLinkedChunk::save_item`
|
||||
|
||||
@@ -66,6 +66,7 @@ wasm-bindgen-test.workspace = true
|
||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||
# Enable the JS feature for getrandom.
|
||||
getrandom = { workspace = true, default-features = false, features = ["wasm_js"] }
|
||||
getrandom3 = { version = "0.3.4", package = "getrandom", default-features = false, features = ["wasm_js"] }
|
||||
js-sys.workspace = true
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -62,6 +62,31 @@ use crate::{
|
||||
/// This is used to know if a lock has been dirtied.
|
||||
pub type CrossProcessLockGeneration = u64;
|
||||
|
||||
/// A trait that represents any function which can be used to
|
||||
/// acquire the underlying lock of a [`CrossProcessLock`].
|
||||
///
|
||||
/// For example, this can be useful when writing a function which
|
||||
/// is parameterized to acquire the underlying lock through either
|
||||
/// [`CrossProcessLock::spin_lock`] or [`CrossProcessLock::try_lock_once`].
|
||||
pub trait AcquireCrossProcessLockFn<L>
|
||||
where
|
||||
Self: AsyncFn(&CrossProcessLock<L>) -> AcquireCrossProcessLockResult<L::LockError>,
|
||||
L: TryLock + Clone + SendOutsideWasm + 'static,
|
||||
{
|
||||
}
|
||||
|
||||
impl<L, T> AcquireCrossProcessLockFn<L> for T
|
||||
where
|
||||
T: AsyncFn(&CrossProcessLock<L>) -> AcquireCrossProcessLockResult<L::LockError>,
|
||||
L: TryLock + Clone + SendOutsideWasm + 'static,
|
||||
{
|
||||
}
|
||||
|
||||
/// A convenience type for the [`Result`] returned from calling
|
||||
/// or [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
|
||||
pub type AcquireCrossProcessLockResult<E> =
|
||||
Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, E>;
|
||||
|
||||
/// Trait used to try to take a lock. Foundation of [`CrossProcessLock`].
|
||||
pub trait TryLock {
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -276,10 +301,8 @@ where
|
||||
///
|
||||
/// The lock can be obtained but it can be dirty. In all cases, the renew
|
||||
/// task will run in the background.
|
||||
#[instrument(skip(self), fields(?self.lock_key, ?self.config))]
|
||||
pub async fn try_lock_once(
|
||||
&self,
|
||||
) -> Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, L::LockError> {
|
||||
#[instrument(skip(self), fields(?self.lock_key, ?self.config, ?self.generation))]
|
||||
pub async fn try_lock_once(&self) -> AcquireCrossProcessLockResult<L::LockError> {
|
||||
// If it's not `MultiProcess`, this behaves as a no-op
|
||||
let CrossProcessLockConfig::MultiProcess { holder_name } = &self.config else {
|
||||
let guard = CrossProcessLockGuard::new(self.num_holders.clone(), self.is_dirty.clone());
|
||||
@@ -447,7 +470,7 @@ where
|
||||
pub async fn spin_lock(
|
||||
&self,
|
||||
max_backoff: Option<u32>,
|
||||
) -> Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, L::LockError> {
|
||||
) -> AcquireCrossProcessLockResult<L::LockError> {
|
||||
// If there is no holder, this behaves as a no-op
|
||||
let max_backoff = max_backoff.unwrap_or(MAX_BACKOFF_MS);
|
||||
|
||||
|
||||
@@ -1249,7 +1249,7 @@ pub enum WithheldCode {
|
||||
/// that the session was not marked as "shared_history".
|
||||
///
|
||||
/// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
|
||||
#[ruma_enum(rename = "io.element.msc4268.history_not_shared", alias = "m.history_not_shared")]
|
||||
#[ruma_enum(rename = "m.history_not_shared", alias = "io.element.msc4268.history_not_shared")]
|
||||
HistoryNotShared,
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use ruma::{events::AnySyncTimelineEvent, serde::Raw};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::deserialized_responses::EncryptionInfo;
|
||||
|
||||
/// Represents all possible validation errors that can occur when processing
|
||||
/// an event edit.
|
||||
///
|
||||
/// These errors ensure that a replacement event complies with the rules
|
||||
/// required to safely and correctly modify an existing event.
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum EditValidityError {
|
||||
/// Occurs when the sender of the replacement event does not match
|
||||
/// the sender of the original event.
|
||||
///
|
||||
/// Only the original sender is allowed to edit their own event.
|
||||
#[error(
|
||||
"the sender of the original event isn't the same as the sender of the replacement event"
|
||||
)]
|
||||
InvalidSender,
|
||||
|
||||
/// Occurs when either the original event or the replacement event contains
|
||||
/// a state key.
|
||||
///
|
||||
/// State events are not allowed to be edited.
|
||||
#[error("the original event or the replacement event contains a state key")]
|
||||
StateKeyPresent,
|
||||
|
||||
/// Occurs when the content type of the original event differs from
|
||||
/// that of the replacement event.
|
||||
///
|
||||
/// Edits must not change the event’s content type, as this would
|
||||
/// introduce semantic inconsistencies.
|
||||
#[error(
|
||||
"the content type of the original event is `{content_type}` while the replacement is a `{replacement_type}`"
|
||||
)]
|
||||
MismatchContentType {
|
||||
/// The content type of the original event.
|
||||
content_type: String,
|
||||
|
||||
/// The content type of the replacement event.
|
||||
replacement_type: String,
|
||||
},
|
||||
|
||||
/// Occurs when the original event is itself already a replacement (edit).
|
||||
#[error("the original event is an edit as well")]
|
||||
OriginalEventIsReplacement,
|
||||
|
||||
/// Occurs when the replacement event is not a replacement for the original
|
||||
/// event.
|
||||
#[error("the replacement event is not a replacement for the original event")]
|
||||
NotReplacement,
|
||||
|
||||
/// Occurs when a required field is missing from either the original
|
||||
/// or the replacement event.
|
||||
///
|
||||
/// The event is considered malformed and cannot be validated.
|
||||
#[error("the event was encrypted, as such it should have an `m.new_content` field")]
|
||||
MissingNewContent,
|
||||
|
||||
#[error(transparent)]
|
||||
InvalidJson(#[from] serde_json::Error),
|
||||
|
||||
/// Occurs when the original event is encrypted but the replacement
|
||||
/// event is not.
|
||||
#[error("the original event was encrypted while the replacement is not")]
|
||||
ReplacementNotEncrypted,
|
||||
}
|
||||
|
||||
/// This implements the Matrix spec rule set for validity of replacement events
|
||||
/// (edits). Invalid replacements must be ignored.
|
||||
///
|
||||
/// This function implements the steps documented in the [spec] with one
|
||||
/// exception, the step to check if the room IDs match isn't done. The JSON of
|
||||
/// the event might not contain the room ID if it wasn received over a `/sync`
|
||||
/// request.
|
||||
///
|
||||
/// *Warning*: Callers must ensure that the original event and replacement event
|
||||
/// belong to the same room, that is, they have the same room ID.
|
||||
///
|
||||
/// [spec]: https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events
|
||||
pub fn check_validity_of_replacement_events(
|
||||
original_json: &Raw<AnySyncTimelineEvent>,
|
||||
original_encryption_info: Option<&EncryptionInfo>,
|
||||
replacement_json: &Raw<AnySyncTimelineEvent>,
|
||||
replacement_encryption_info: Option<&EncryptionInfo>,
|
||||
) -> Result<(), EditValidityError> {
|
||||
const REPLACEMENT_REL_TYPE: &str = "m.replace";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MinimalEvent<'a> {
|
||||
sender: &'a str,
|
||||
event_id: &'a str,
|
||||
#[serde(rename = "type")]
|
||||
event_type: &'a str,
|
||||
state_key: Option<&'a str>,
|
||||
content: MinimalContent<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MinimalContent<'a> {
|
||||
#[serde(borrow, rename = "m.relates_to")]
|
||||
relates_to: Option<MinimalRelatesTo<'a>>,
|
||||
#[serde(rename = "m.new_content")]
|
||||
new_content: Option<MinimalNewContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MinimalNewContent {}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MinimalRelatesTo<'a> {
|
||||
rel_type: Option<&'a str>,
|
||||
event_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
let original_event = original_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
|
||||
let replacement_event = replacement_json.deserialize_as_unchecked::<MinimalEvent<'_>>()?;
|
||||
|
||||
// We don't check the room ID here since this event might have been received
|
||||
// over /sync, in this case the JSON likely won't contain the room ID field.
|
||||
|
||||
// The original event and replacement event must have the same sender (i.e. you
|
||||
// cannot edit someone else’s messages).
|
||||
if original_event.sender != replacement_event.sender {
|
||||
return Err(EditValidityError::InvalidSender);
|
||||
}
|
||||
|
||||
// This check isn't part of the list in the spec, but it makes sense to check if
|
||||
// the replacement event is has the correct rel_type and if it's an edit for the
|
||||
// original event.
|
||||
if let Some(relates_to) = replacement_event.content.relates_to {
|
||||
if relates_to.rel_type != Some(REPLACEMENT_REL_TYPE)
|
||||
|| relates_to.event_id != Some(original_event.event_id)
|
||||
{
|
||||
return Err(EditValidityError::NotReplacement);
|
||||
}
|
||||
} else {
|
||||
return Err(EditValidityError::NotReplacement);
|
||||
}
|
||||
|
||||
// The replacement and original events must have the same type (i.e. you cannot
|
||||
// change the original event’s type).
|
||||
if original_event.event_type != replacement_event.event_type {
|
||||
return Err(EditValidityError::MismatchContentType {
|
||||
content_type: original_event.event_type.to_owned(),
|
||||
replacement_type: replacement_event.event_type.to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
// The replacement and original events must not have a state_key property (i.e.
|
||||
// you cannot edit state events at all).
|
||||
if original_event.state_key.is_some() || replacement_event.state_key.is_some() {
|
||||
return Err(EditValidityError::StateKeyPresent);
|
||||
}
|
||||
|
||||
// The original event must not, itself, have a rel_type of m.replace (i.e. you
|
||||
// cannot edit an edit — though you can send multiple edits for a single
|
||||
// original event).
|
||||
if let Some(relates_to) = original_event.content.relates_to
|
||||
&& relates_to.rel_type == Some(REPLACEMENT_REL_TYPE)
|
||||
{
|
||||
return Err(EditValidityError::OriginalEventIsReplacement);
|
||||
}
|
||||
|
||||
// The replacement event (once decrypted, if appropriate) must have an
|
||||
// m.new_content property.
|
||||
if replacement_encryption_info.is_some() && replacement_event.content.new_content.is_none() {
|
||||
return Err(EditValidityError::MissingNewContent);
|
||||
}
|
||||
|
||||
// If the original event was encrypted, the replacement should be too.
|
||||
if original_encryption_info.is_some() && replacement_encryption_info.is_none() {
|
||||
return Err(EditValidityError::ReplacementNotEncrypted);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
use std::pin::Pin;
|
||||
|
||||
use ruma::{RoomVersionId, room_version_rules::RoomVersionRules};
|
||||
|
||||
#[cfg(test)]
|
||||
matrix_sdk_test_utils::init_tracing_for_tests!();
|
||||
|
||||
@@ -27,6 +29,7 @@ pub use ruma;
|
||||
pub mod cross_process_lock;
|
||||
pub mod debug;
|
||||
pub mod deserialized_responses;
|
||||
mod edit_validation;
|
||||
pub mod executor;
|
||||
pub mod failures_cache;
|
||||
pub mod linked_chunk;
|
||||
@@ -47,7 +50,7 @@ pub mod ttl_cache;
|
||||
pub mod js_tracing;
|
||||
|
||||
pub use cross_process_lock::LEASE_DURATION_MS;
|
||||
use ruma::{RoomVersionId, room_version_rules::RoomVersionRules};
|
||||
pub use edit_validation::*;
|
||||
|
||||
/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on
|
||||
/// wasm.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
//! let mut failures = monitor.subscribe();
|
||||
//!
|
||||
//! // Spawn a monitored background task
|
||||
//! let handle = monitor.spawn_background_task("my_task", async {
|
||||
//! let handle = monitor.spawn_infinite_task("my_task", async {
|
||||
//! loop {
|
||||
//! // Do background work...
|
||||
//! matrix_sdk_common::sleep::sleep(std::time::Duration::from_secs(1))
|
||||
@@ -183,7 +183,7 @@ const FAILURE_CHANNEL_CAPACITY: usize = 8;
|
||||
/// let mut failures = monitor.subscribe();
|
||||
///
|
||||
/// // Spawn a task that runs indefinitely
|
||||
/// let _handle = monitor.spawn_background_task("worker", async {
|
||||
/// let _handle = monitor.spawn_infinite_task("worker", async {
|
||||
/// loop {
|
||||
/// // Do work...
|
||||
/// matrix_sdk_common::sleep::sleep(std::time::Duration::from_secs(1))
|
||||
@@ -223,7 +223,10 @@ impl TaskMonitor {
|
||||
self.failure_sender.subscribe()
|
||||
}
|
||||
|
||||
/// Spawn a background task that is expected to run indefinitely.
|
||||
/// Spawn a background task that is expected to **run forever**.
|
||||
///
|
||||
/// For one-off background tasks that are expected to complete successfully,
|
||||
/// use [`Self::spawn_finite_task`] instead.
|
||||
///
|
||||
/// If the task completes (whether successfully or by panicking), it will be
|
||||
/// reported as a [`BackgroundTaskFailure`] report through the broadcast
|
||||
@@ -242,10 +245,46 @@ impl TaskMonitor {
|
||||
///
|
||||
/// A [`BackgroundTaskHandle`] that can be used to abort the task or check
|
||||
/// if it has finished. This is the equivalent of tokio's `JoinHandle`.
|
||||
pub fn spawn_background_task<F>(
|
||||
pub fn spawn_infinite_task<F>(&self, name: impl Into<String>, future: F) -> BackgroundTaskHandle
|
||||
where
|
||||
F: Future<Output = ()> + SendOutsideWasm + 'static,
|
||||
{
|
||||
self.spawn_task_internal(name, future, true)
|
||||
}
|
||||
|
||||
/// Spawn a background job that is expected to run once and complete
|
||||
/// successfully in the background.
|
||||
///
|
||||
/// For long-term background jobs that are expected to run forever, use
|
||||
/// [`Self::spawn_infinite_task`] instead.
|
||||
///
|
||||
/// If the task completes (by panicking), it will be reported as a
|
||||
/// [`BackgroundTaskFailure`] report through the broadcast channel.
|
||||
///
|
||||
/// Use this for one-shot background tasks that should complete under normal
|
||||
/// operation.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `name` - A human-readable name for the task (for debugging purposes).
|
||||
/// * `future` - The async task to run.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A [`BackgroundTaskHandle`] that can be used to abort the task or check
|
||||
/// if it has finished. This is the equivalent of tokio's `JoinHandle`.
|
||||
pub fn spawn_finite_task<F>(&self, name: impl Into<String>, future: F) -> BackgroundTaskHandle
|
||||
where
|
||||
F: Future<Output = ()> + SendOutsideWasm + 'static,
|
||||
{
|
||||
self.spawn_task_internal(name, future, false)
|
||||
}
|
||||
|
||||
fn spawn_task_internal<F>(
|
||||
&self,
|
||||
name: impl Into<String>,
|
||||
future: F,
|
||||
runs_forever: bool,
|
||||
) -> BackgroundTaskHandle
|
||||
where
|
||||
F: Future<Output = ()> + SendOutsideWasm + 'static,
|
||||
@@ -274,8 +313,14 @@ impl TaskMonitor {
|
||||
|
||||
let failure_reason = match result {
|
||||
Ok(()) => {
|
||||
// The task ended, this is considered an early termination.
|
||||
BackgroundTaskFailureReason::EarlyTermination
|
||||
if runs_forever {
|
||||
// The background forever task ended, this is considered an early
|
||||
// termination.
|
||||
BackgroundTaskFailureReason::EarlyTermination
|
||||
} else {
|
||||
// The task ended successfully, no failure to report.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Err(panic_payload) => BackgroundTaskFailureReason::Panic {
|
||||
@@ -474,7 +519,13 @@ fn extract_panic_message(payload: &Box<dyn Any + Send>) -> Option<String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use matrix_sdk_test_macros::async_test;
|
||||
@@ -488,7 +539,7 @@ mod tests {
|
||||
let mut failures = monitor.subscribe();
|
||||
|
||||
// Spawn a task that completes immediately.
|
||||
let _handle = monitor.spawn_background_task("test_task", async {
|
||||
let _handle = monitor.spawn_infinite_task("test_task", async {
|
||||
// Completes immediately: this is an "early termination".
|
||||
});
|
||||
|
||||
@@ -509,7 +560,7 @@ mod tests {
|
||||
let mut failures = monitor.subscribe();
|
||||
|
||||
// Spawn a task that panics.
|
||||
let _handle = monitor.spawn_background_task("panicking_task", async {
|
||||
let _handle = monitor.spawn_infinite_task("panicking_task", async {
|
||||
panic!("test panic message");
|
||||
});
|
||||
|
||||
@@ -573,7 +624,7 @@ mod tests {
|
||||
let mut failures = monitor.subscribe();
|
||||
|
||||
// Spawn a long-running task.
|
||||
let handle = monitor.spawn_background_task("aborted_task", async {
|
||||
let handle = monitor.spawn_infinite_task("aborted_task", async {
|
||||
loop {
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
@@ -599,7 +650,7 @@ mod tests {
|
||||
|
||||
// Spawn a long-running task.
|
||||
let handle = monitor
|
||||
.spawn_background_task("aborted_task", async {
|
||||
.spawn_infinite_task("aborted_task", async {
|
||||
loop {
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
@@ -616,4 +667,31 @@ mod tests {
|
||||
let result = timeout(failures.recv(), Duration::from_millis(100)).await;
|
||||
assert!(result.is_err(), "should timeout, no failure expected for abort");
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_spawn_finite_task() {
|
||||
let monitor = TaskMonitor::new();
|
||||
let mut failures = monitor.subscribe();
|
||||
|
||||
let successful_completion = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// Spawn a one-off background job that completes successfully.
|
||||
let successful_completion_clone = successful_completion.clone();
|
||||
let _handle = monitor.spawn_finite_task("one-shot job", async move {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
successful_completion_clone.store(true, Ordering::SeqCst);
|
||||
});
|
||||
|
||||
// Give the task time to finish.
|
||||
sleep(Duration::from_millis(20)).await;
|
||||
|
||||
// Should NOT receive a failure for successful completion.
|
||||
let result = timeout(failures.recv(), Duration::from_millis(100)).await;
|
||||
assert!(result.is_err(), "should timeout, no failure expected for abort");
|
||||
|
||||
assert!(
|
||||
successful_completion.load(Ordering::SeqCst),
|
||||
"background job should have completed successfully"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
|
||||
use std::{borrow::Borrow, collections::HashMap, hash::Hash, time::Duration};
|
||||
|
||||
use ruma::time::Instant;
|
||||
use ruma::time::{Instant, SystemTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// One day is the default lifetime.
|
||||
const DEFAULT_LIFETIME: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
@@ -123,10 +124,101 @@ impl<K: Eq + Hash, V: Clone> Default for TtlCache<K, V> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A value that expires after some time.
|
||||
///
|
||||
/// This value is (de)serializable so it can be persisted in a store.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TtlValue<T> {
|
||||
/// The data of the item.
|
||||
#[serde(flatten)]
|
||||
data: T,
|
||||
|
||||
/// Last time we fetched this data from the server, in milliseconds since
|
||||
/// UNIX epoch.
|
||||
///
|
||||
/// When this field is missing during deserialization, it defaults to `0.0`,
|
||||
/// which means that the data is always expired. This allows to be
|
||||
/// compatible with data that was persisted before deciding to add an
|
||||
/// expiration time.
|
||||
#[serde(default = "default_timestamp")]
|
||||
last_fetch_ts: Option<f64>,
|
||||
}
|
||||
|
||||
impl<T> TtlValue<T> {
|
||||
/// The number of milliseconds after which the data is considered stale.
|
||||
///
|
||||
/// This matches 1 day.
|
||||
pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24) as _;
|
||||
|
||||
/// Construct a new `TtlValue` with the given data.
|
||||
pub fn new(data: T) -> Self {
|
||||
Self { data, last_fetch_ts: Some(now_timestamp_ms()) }
|
||||
}
|
||||
|
||||
/// Construct a new `TtlValue` with the given data that never expires.
|
||||
pub fn without_expiry(data: T) -> Self {
|
||||
Self { data, last_fetch_ts: None }
|
||||
}
|
||||
|
||||
/// Converts from `&TtlValue<T>` to `TtlValue<&T>`.
|
||||
pub fn as_ref(&self) -> TtlValue<&T> {
|
||||
TtlValue { data: &self.data, last_fetch_ts: self.last_fetch_ts }
|
||||
}
|
||||
|
||||
/// Transform the data of this `TtlValue` with the given function.
|
||||
pub fn map<U, F>(self, f: F) -> TtlValue<U>
|
||||
where
|
||||
F: FnOnce(T) -> U,
|
||||
{
|
||||
TtlValue { data: f(self.data), last_fetch_ts: self.last_fetch_ts }
|
||||
}
|
||||
|
||||
/// Whether this value has expired.
|
||||
pub fn has_expired(&self) -> bool {
|
||||
self.last_fetch_ts.is_some_and(|ts| now_timestamp_ms() - ts >= Self::STALE_THRESHOLD)
|
||||
}
|
||||
|
||||
/// Mark this value has expired.
|
||||
pub fn expire(&mut self) {
|
||||
// We assume that the system time is always correct and we are far from the UNIX
|
||||
// epoch so a timestamp of 0 should always be expired.
|
||||
self.last_fetch_ts = Some(0.0)
|
||||
}
|
||||
|
||||
/// Get a reference to the data of this value.
|
||||
pub fn data(&self) -> &T {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Get the data of this value.
|
||||
pub fn into_data(self) -> T {
|
||||
self.data
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
|
||||
fn now_timestamp_ms() -> f64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("System clock was before 1970.")
|
||||
.as_secs_f64()
|
||||
* 1000.0
|
||||
}
|
||||
|
||||
/// The default timestamp if it is missing during deserialization.
|
||||
///
|
||||
/// We expect that a value that was serialized always has an expiry time, so the
|
||||
/// default is `Some(0.0)`.
|
||||
fn default_timestamp() -> Option<f64> {
|
||||
Some(0.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use super::TtlCache;
|
||||
use super::{TtlCache, TtlValue, now_timestamp_ms};
|
||||
|
||||
#[test]
|
||||
fn test_ttl_cache_insertion() {
|
||||
@@ -144,4 +236,52 @@ mod tests {
|
||||
assert!(!cache.contains("A"));
|
||||
assert!(cache.get("A").is_none(), "The item should have been removed from the cache");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ttl_value_expiry() {
|
||||
// Definitely stale.
|
||||
let ttl_value = TtlValue {
|
||||
data: (),
|
||||
last_fetch_ts: Some(now_timestamp_ms() - TtlValue::<()>::STALE_THRESHOLD - 1.0),
|
||||
};
|
||||
assert!(ttl_value.has_expired());
|
||||
|
||||
// Definitely not stale.
|
||||
let ttl_value = TtlValue::new(());
|
||||
assert!(!ttl_value.has_expired());
|
||||
|
||||
// Cannot be stale.
|
||||
let ttl_value = TtlValue::without_expiry(());
|
||||
assert!(!ttl_value.has_expired());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ttl_value_serialize_roundtrip() {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct Data {
|
||||
foo: String,
|
||||
}
|
||||
|
||||
let data = Data { foo: "bar".to_owned() };
|
||||
|
||||
// With timestamp.
|
||||
let ttl_value = TtlValue { data: data.clone(), last_fetch_ts: Some(1000.0) };
|
||||
let json = json!({
|
||||
"foo": "bar",
|
||||
"last_fetch_ts": 1000.0,
|
||||
});
|
||||
assert_eq!(serde_json::to_value(&ttl_value).unwrap(), json);
|
||||
|
||||
let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
|
||||
assert_eq!(deserialized.data, data);
|
||||
assert!(deserialized.last_fetch_ts.unwrap() - ttl_value.last_fetch_ts.unwrap() < 0.0001);
|
||||
|
||||
// Without timestamp the value is always expired in theory.
|
||||
let json = json!({
|
||||
"foo": "bar",
|
||||
});
|
||||
let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
|
||||
assert_eq!(deserialized.data, data);
|
||||
assert!(deserialized.last_fetch_ts.unwrap() - 0.0 < 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Features
|
||||
|
||||
- [**breaking**] Change to the stable identifiers for `m.room_key_bundle`,
|
||||
`m.history_not_shared` and `m.shared_history`. We still support reading the
|
||||
unstable identifiers.
|
||||
([#6467](https://github.com/matrix-org/matrix-rust-sdk/pull/6467))
|
||||
- Add support for MSC4385.
|
||||
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
|
||||
- Add new method `OlmMachine::push_secret_to_verified_devices`.
|
||||
- Pushed secrets that we receive from verified devices are added to the
|
||||
secrets inbox.
|
||||
- Add `Store::{store,clear}_room_pending_key_bundle`,
|
||||
`CryptoStore::get_pending_key_bundle_details_for_room` and
|
||||
`CryptoStore::get_all_rooms_pending_key_bundle`, which can be used by
|
||||
@@ -15,10 +24,8 @@ All notable changes to this project will be documented in this file.
|
||||
[MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268) key
|
||||
bundle.
|
||||
([#6199](https://github.com/matrix-org/matrix-rust-sdk/pull/6199)), ([#6233](https://github.com/matrix-org/matrix-rust-sdk/pull/6233)),
|
||||
|
||||
- Add MSC4388 support to the QrcodeData struct.
|
||||
([#6089](https://github.com/matrix-org/matrix-rust-sdk/pull/6089))
|
||||
|
||||
- Improved logging when we are sending secrets in `GossipMachine`.
|
||||
([#6074](https://github.com/matrix-org/matrix-rust-sdk/pull/6074))
|
||||
([#6083](https://github.com/matrix-org/matrix-rust-sdk/pull/6083))
|
||||
@@ -36,6 +43,20 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Refactor
|
||||
|
||||
- [**breaking**] The `MegolmV1BackupKey::encrypt` now returns a `Result`
|
||||
([#6477](https://github.com/matrix-org/matrix-rust-sdk/pull/6477))
|
||||
- [**breaking**] `CryptoStore::get_secrets_from_inbox` now returns a `Vec` of
|
||||
the secrets as strings, rather than a `Vec` of `GossippedSecret` structs.
|
||||
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
|
||||
- [**breaking**] `store::types::Changes::sessions` now stores a `Vec` of
|
||||
`SecretsInboxItem`.
|
||||
([#6164](https://github.com/matrix-org/matrix-rust-sdk/pull/6164))
|
||||
- **breaking** The `BackupDecryptionKey::new` and `DehydratedDeviceKey::new`
|
||||
methods became infallible, they don't return a `Result` anymore.
|
||||
([#5502](https://github.com/matrix-org/matrix-rust-sdk/pull/5502))
|
||||
- [**breaking**] Remove cross-process lock generation logic from `OlmMachine`, which is now
|
||||
implemented more generally in `matrix_sdk_common::cross_process_lock::CrossProcessLock`.
|
||||
([#6326](https://github.com/matrix-org/matrix-rust-sdk/pull/6326))
|
||||
- [**breaking**] The `MediaEncryptionInfo` fields changed to match the new fields of `EncryptedFile`
|
||||
from Ruma. The serialized JSON format did not change and still matches the format of
|
||||
`EncryptedFile` defined in the spec, without the `url` field. The `DecryptorError::KeyNonceLength`
|
||||
@@ -50,7 +71,6 @@ All notable changes to this project will be documented in this file.
|
||||
returns an MSC-specific struct now. The `rendezvous_url()` method has been
|
||||
removed.
|
||||
([#6081](https://github.com/matrix-org/matrix-rust-sdk/pull/6081))
|
||||
|
||||
- [**breaking**] The `message-ids` feature has been removed. It was already a no-op and has now
|
||||
been eliminated entirely.
|
||||
([#5963](https://github.com/matrix-org/matrix-rust-sdk/pull/5963))
|
||||
|
||||
@@ -40,6 +40,10 @@ test-send-sync = []
|
||||
# Testing helpers for implementations based upon this
|
||||
testing = ["matrix-sdk-test"]
|
||||
|
||||
# Enable experimental support for pushing secrets; see
|
||||
# https://github.com/matrix-org/matrix-spec-proposals/pull/4385
|
||||
experimental-push-secrets = []
|
||||
|
||||
[dependencies]
|
||||
aes = { version = "0.8.4", default-features = false }
|
||||
aquamarine.workspace = true
|
||||
|
||||
@@ -101,7 +101,10 @@ impl MegolmV1BackupKey {
|
||||
|
||||
/// Export the given inbound group session, and encrypt the data, ready for
|
||||
/// writing to the backup.
|
||||
pub async fn encrypt(&self, session: InboundGroupSession) -> KeyBackupData {
|
||||
pub async fn encrypt(
|
||||
&self,
|
||||
session: InboundGroupSession,
|
||||
) -> Result<KeyBackupData, vodozemac::pk_encryption::Error> {
|
||||
let pk = PkEncryption::from_key(self.inner.key);
|
||||
|
||||
// The forwarding chains don't mean much, we only care whether we received the
|
||||
@@ -117,7 +120,7 @@ impl MegolmV1BackupKey {
|
||||
let key =
|
||||
Zeroizing::new(serde_json::to_vec(&key).expect("Can't serialize exported room key"));
|
||||
|
||||
let message = pk.encrypt(&key);
|
||||
let message = pk.encrypt(&key)?;
|
||||
|
||||
let session_data = EncryptedSessionDataInit {
|
||||
ephemeral: Base64::new(message.ephemeral_key.to_vec()),
|
||||
@@ -126,7 +129,7 @@ impl MegolmV1BackupKey {
|
||||
}
|
||||
.into();
|
||||
|
||||
KeyBackupDataInit {
|
||||
Ok(KeyBackupDataInit {
|
||||
first_message_index,
|
||||
forwarded_count,
|
||||
// TODO: is this actually used anywhere? seems to be completely
|
||||
@@ -136,6 +139,6 @@ impl MegolmV1BackupKey {
|
||||
is_verified: false,
|
||||
session_data,
|
||||
}
|
||||
.into()
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn base64_decoding() -> Result<(), DecodeError> {
|
||||
let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
|
||||
let key = BackupDecryptionKey::new();
|
||||
|
||||
let base64 = key.to_base64();
|
||||
let decoded_key = BackupDecryptionKey::from_base64(&base64)?;
|
||||
@@ -316,7 +316,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn base58_decoding() -> Result<(), DecodeError> {
|
||||
let key = BackupDecryptionKey::new().expect("Can't create a new recovery key");
|
||||
let key = BackupDecryptionKey::new();
|
||||
|
||||
let base64 = key.to_base58();
|
||||
let decoded_key = BackupDecryptionKey::from_base58(&base64)?;
|
||||
@@ -394,10 +394,10 @@ mod tests {
|
||||
async fn test_encryption_cycle() {
|
||||
let session = InboundGroupSession::from_export(&room_key()).unwrap();
|
||||
|
||||
let decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
let decryption_key = BackupDecryptionKey::new();
|
||||
let encryption_key = decryption_key.megolm_v1_public_key();
|
||||
|
||||
let encrypted = encryption_key.encrypt(session).await;
|
||||
let encrypted = encryption_key.encrypt(session).await.unwrap();
|
||||
|
||||
let _ = decryption_key
|
||||
.decrypt_session_data(encrypted.session_data)
|
||||
@@ -406,7 +406,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn key_matches() {
|
||||
let decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
let decryption_key = BackupDecryptionKey::new();
|
||||
|
||||
let key_info = decryption_key.to_backup_info();
|
||||
|
||||
|
||||
@@ -534,7 +534,7 @@ impl BackupMachine {
|
||||
}
|
||||
|
||||
let key_count = sessions.len();
|
||||
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await;
|
||||
let (backup, session_record) = Self::backup_keys(sessions, backup_key).await?;
|
||||
|
||||
info!(
|
||||
key_count = key_count,
|
||||
@@ -556,10 +556,13 @@ impl BackupMachine {
|
||||
async fn backup_keys(
|
||||
sessions: Vec<InboundGroupSession>,
|
||||
backup_key: &MegolmV1BackupKey,
|
||||
) -> (
|
||||
BTreeMap<OwnedRoomId, RoomKeyBackup>,
|
||||
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
|
||||
) {
|
||||
) -> Result<
|
||||
(
|
||||
BTreeMap<OwnedRoomId, RoomKeyBackup>,
|
||||
BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
|
||||
),
|
||||
vodozemac::pk_encryption::Error,
|
||||
> {
|
||||
let mut backup: BTreeMap<OwnedRoomId, RoomKeyBackup> = BTreeMap::new();
|
||||
let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>> =
|
||||
BTreeMap::new();
|
||||
@@ -568,7 +571,7 @@ impl BackupMachine {
|
||||
let room_id = session.room_id().to_owned();
|
||||
let session_id = session.session_id().to_owned();
|
||||
let sender_key = session.sender_key().to_owned();
|
||||
let session = backup_key.encrypt(session).await;
|
||||
let session = backup_key.encrypt(session).await?;
|
||||
|
||||
session_record
|
||||
.entry(room_id.to_owned())
|
||||
@@ -586,7 +589,7 @@ impl BackupMachine {
|
||||
.insert(session_id, session);
|
||||
}
|
||||
|
||||
(backup, session_record)
|
||||
Ok((backup, session_record))
|
||||
}
|
||||
|
||||
/// Import the given room keys into our store.
|
||||
@@ -701,7 +704,7 @@ mod tests {
|
||||
assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
|
||||
assert_eq!(counts.backed_up, 0, "No room keys have been backed up yet");
|
||||
|
||||
let decryption_key = BackupDecryptionKey::new().expect("Can't create new recovery key");
|
||||
let decryption_key = BackupDecryptionKey::new();
|
||||
let backup_key = decryption_key.megolm_v1_public_key();
|
||||
backup_key.set_version("1".to_owned());
|
||||
|
||||
@@ -838,7 +841,7 @@ mod tests {
|
||||
let backup_machine = machine.backup_machine();
|
||||
|
||||
// We set up a backup key, so that we can test `backup_machine.backup()` later.
|
||||
let decryption_key = BackupDecryptionKey::new().expect("Couldn't create new recovery key");
|
||||
let decryption_key = BackupDecryptionKey::new();
|
||||
let backup_key = decryption_key.megolm_v1_public_key();
|
||||
backup_key.set_version("1".to_owned());
|
||||
backup_machine.enable_backup_v1(backup_key).await.expect("Couldn't enable backup");
|
||||
@@ -886,7 +889,7 @@ mod tests {
|
||||
let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
|
||||
let backup_machine = machine.backup_machine();
|
||||
|
||||
let decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
let decryption_key = BackupDecryptionKey::new();
|
||||
let mut backup_info = decryption_key.to_backup_info();
|
||||
|
||||
let result = backup_machine.verify_backup(backup_info.to_owned(), false).await.unwrap();
|
||||
@@ -904,7 +907,7 @@ mod tests {
|
||||
async fn test_fix_backup_key_mismatch() {
|
||||
let store = MemoryStore::new();
|
||||
|
||||
let backup_decryption_key = BackupDecryptionKey::new().unwrap();
|
||||
let backup_decryption_key = BackupDecryptionKey::new();
|
||||
|
||||
store
|
||||
.save_changes(Changes {
|
||||
|
||||
@@ -23,7 +23,7 @@ use hmac::{
|
||||
digest::{FixedOutput, MacError},
|
||||
};
|
||||
use pbkdf2::pbkdf2;
|
||||
use rand::{RngCore, thread_rng};
|
||||
use rand::{Rng, rng};
|
||||
use sha2::{Sha256, Sha512};
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
@@ -247,7 +247,7 @@ impl AesHmacSha2Key {
|
||||
/// The initialization vector will be clamped and will be used to encrypt
|
||||
/// the ciphertext.
|
||||
fn generate_iv() -> [u8; IV_SIZE] {
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
let mut iv = [0u8; IV_SIZE];
|
||||
|
||||
rng.fill_bytes(&mut iv);
|
||||
|
||||
@@ -343,7 +343,7 @@ impl DehydratedDevice {
|
||||
/// async fn example() -> anyhow::Result<()> {
|
||||
/// # let machine: OlmMachine = unimplemented!();
|
||||
/// // Create a new random key
|
||||
/// let pickle_key = DehydratedDeviceKey::new()?;
|
||||
/// let pickle_key = DehydratedDeviceKey::new();
|
||||
///
|
||||
/// // Create the dehydrated device.
|
||||
/// let device = machine.dehydrated_devices().create().await?;
|
||||
@@ -612,7 +612,7 @@ mod tests {
|
||||
let stored_key = dehydrated_manager.get_dehydrated_device_pickle_key().await.unwrap();
|
||||
assert!(stored_key.is_none());
|
||||
|
||||
let pickle_key = DehydratedDeviceKey::new().unwrap();
|
||||
let pickle_key = DehydratedDeviceKey::new();
|
||||
|
||||
dehydrated_manager.save_dehydrated_device_pickle_key(&pickle_key).await.unwrap();
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ pub enum OlmError {
|
||||
)]
|
||||
MissingSession,
|
||||
|
||||
/// Encrypting of an Olm message failed because of a low-level cryptographic
|
||||
/// issue occurred.
|
||||
#[error(transparent)]
|
||||
Encryption(#[from] vodozemac::olm::EncryptionError),
|
||||
|
||||
/// Encryption failed due to an error collecting the recipient devices.
|
||||
#[error("encryption failed due to an error collecting the recipient devices: {0}")]
|
||||
SessionRecipientCollectionError(SessionRecipientCollectionError),
|
||||
@@ -444,3 +449,15 @@ pub enum SessionRecipientCollectionError {
|
||||
#[error("Encryption failed because your device is not verified")]
|
||||
SendingFromUnverifiedDevice,
|
||||
}
|
||||
|
||||
/// Error representing a problem when pushing a secret
|
||||
#[derive(Error, Debug)]
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
pub enum SecretPushError {
|
||||
#[error("The requested secret is not available")]
|
||||
MissingSecret,
|
||||
|
||||
/// The storage layer returned an error.
|
||||
#[error(transparent)]
|
||||
StoreError(#[from] CryptoStoreError),
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use aes::{
|
||||
Aes256,
|
||||
cipher::{KeyIvInit, StreamCipher},
|
||||
};
|
||||
use rand::{RngCore, thread_rng};
|
||||
use rand::{Rng, rng};
|
||||
use ruma::{
|
||||
events::room::{
|
||||
EncryptedFile, EncryptedFileHash, EncryptedFileHashAlgorithm, EncryptedFileHashes,
|
||||
@@ -216,7 +216,7 @@ impl<'a, R: Read + ?Sized + 'a> AttachmentEncryptor<'a, R> {
|
||||
let mut key = [0u8; KEY_SIZE];
|
||||
let mut iv = [0u8; IV_SIZE];
|
||||
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
|
||||
rng.fill_bytes(&mut key);
|
||||
// Only populate the first 8 bytes with randomness, the rest is 0
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use rand::{RngCore, thread_rng};
|
||||
use rand::{Rng, rng};
|
||||
use serde_json::Error as SerdeError;
|
||||
use thiserror::Error;
|
||||
use vodozemac::{base64_decode, base64_encode};
|
||||
@@ -149,7 +149,7 @@ pub fn encrypt_room_key_export(
|
||||
|
||||
fn encrypt_helper(plaintext: &[u8], passphrase: &str, rounds: u32) -> String {
|
||||
let mut salt = [0u8; SALT_SIZE];
|
||||
let mut rng = thread_rng();
|
||||
let mut rng = rng();
|
||||
|
||||
rng.fill_bytes(&mut salt);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2020, 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,6 +20,8 @@
|
||||
// If we don't trust the device store an object that remembers the request and
|
||||
// let the users introspect that object.
|
||||
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, btree_map::Entry},
|
||||
mem,
|
||||
@@ -61,6 +63,11 @@ use crate::{
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
},
|
||||
};
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
use crate::{
|
||||
error::SecretPushError,
|
||||
types::events::{olm_v1::DecryptedSecretPushEvent, secret_push::SecretPushContent},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct GossipMachine {
|
||||
@@ -288,6 +295,98 @@ impl GossipMachine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a secret to all of our other verified devices.
|
||||
///
|
||||
/// This function assumes that we already have Olm sessions with the other
|
||||
/// devices. This can be done by calling
|
||||
/// [`OlmMachine::get_missing_sessions()`].
|
||||
///
|
||||
/// * `secret_name` - The name of the secret to push
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
pub async fn push_secret_to_verified_devices(
|
||||
&self,
|
||||
secret_name: SecretName,
|
||||
) -> Result<HashMap<OwnedDeviceId, OlmError>, SecretPushError> {
|
||||
let content = if let Some(secret) = self.inner.store.export_secret(&secret_name).await? {
|
||||
SecretPushContent::new(secret_name.clone(), secret)
|
||||
} else {
|
||||
info!(?secret_name, "Can't push a secret, secret isn't found");
|
||||
return Err(SecretPushError::MissingSecret);
|
||||
};
|
||||
|
||||
let devices = self.inner.store.get_user_devices(self.user_id()).await?;
|
||||
let mut errors = HashMap::new();
|
||||
|
||||
for device in devices.devices() {
|
||||
if !device.is_our_own_device() && device.is_verified() {
|
||||
let event_type = content.event_type().to_owned();
|
||||
match device.encrypt(&event_type, content.clone()).await {
|
||||
Ok((_used_session, content, message_id)) => {
|
||||
let encrypted_event_type = content.event_type().to_owned();
|
||||
let request = ToDeviceRequest::new(
|
||||
device.user_id(),
|
||||
device.device_id().to_owned(),
|
||||
&encrypted_event_type,
|
||||
content.cast(),
|
||||
);
|
||||
let request = OutgoingRequest {
|
||||
request_id: request.txn_id.clone(),
|
||||
request: Arc::new(request.into()),
|
||||
};
|
||||
debug!(
|
||||
device = ?device.device_id(),
|
||||
event_type,
|
||||
request_id = ?request.request_id,
|
||||
?secret_name,
|
||||
?message_id,
|
||||
"Creating outgoing secret push to-device request",
|
||||
);
|
||||
self.inner
|
||||
.outgoing_requests
|
||||
.write()
|
||||
.insert(request.request_id.clone(), request);
|
||||
}
|
||||
Err(err) => {
|
||||
info!(?secret_name, device_id = ?device.device_id(), ?err, "Can't push secret to device");
|
||||
errors.insert(device.device_id().to_owned(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(errors)
|
||||
}
|
||||
|
||||
/// Handle a received secret push event.
|
||||
///
|
||||
/// Checks that the sender device is verified, then adds it to the changes.
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
pub async fn receive_secret_push_event(
|
||||
&self,
|
||||
sender_key: &Curve25519PublicKey,
|
||||
event: &DecryptedSecretPushEvent,
|
||||
changes: &mut Changes,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
// Only accept events from verified own-devices
|
||||
let sender = &event.sender;
|
||||
if sender != self.user_id() {
|
||||
// Ignore if sent from a different user
|
||||
warn!(?sender, "Received secret push from a different user");
|
||||
return Ok(());
|
||||
}
|
||||
let Some(device) = self.inner.store.get_device_from_curve_key(sender, *sender_key).await?
|
||||
else {
|
||||
warn!(?sender, ?sender_key, "Received secret push from unknown device");
|
||||
return Ok(());
|
||||
};
|
||||
if !device.is_verified() {
|
||||
warn!(?sender, device_id = ?device.device_id(), "Received secret push from unverified device");
|
||||
return Ok(());
|
||||
}
|
||||
changes.secrets.push(event.content.clone().into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_secret_request(
|
||||
&self,
|
||||
cache: &StoreCache,
|
||||
@@ -917,7 +1016,7 @@ impl GossipMachine {
|
||||
// So we put the secret into our inbox. Later users can inspect the contents of
|
||||
// the inbox and decide if they want to activate the backup.
|
||||
info!("Received a backup decryption key, storing it into the secret inbox.");
|
||||
changes.secrets.push(secret);
|
||||
changes.secrets.push(secret.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1112,6 +1211,8 @@ impl GossipMachine {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "automatic-room-key-forwarding")]
|
||||
@@ -2060,7 +2161,7 @@ mod tests {
|
||||
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
|
||||
bob_machine.store().save_device_data(&[alice_device.inner]).await.unwrap();
|
||||
|
||||
let decryption_key = crate::store::types::BackupDecryptionKey::new().unwrap();
|
||||
let decryption_key = crate::store::types::BackupDecryptionKey::new();
|
||||
alice_machine
|
||||
.backup_machine()
|
||||
.save_decryption_key(Some(decryption_key), None)
|
||||
@@ -2240,4 +2341,237 @@ mod tests {
|
||||
|
||||
assert_eq!(session.session_id(), group_session.session_id())
|
||||
}
|
||||
|
||||
/// Set up OlmMachines for the secret-pushing tests
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
async fn set_up_secret_push() -> (
|
||||
crate::machine::OlmMachine,
|
||||
crate::identities::device::Device,
|
||||
crate::machine::OlmMachine,
|
||||
crate::identities::device::Device,
|
||||
crate::store::types::BackupDecryptionKey,
|
||||
) {
|
||||
use crate::machine::test_helpers::get_machine_pair_with_setup_sessions_test_helper;
|
||||
|
||||
let alice_id = user_id!("@alice:localhost");
|
||||
|
||||
let (alice_machine, bob_machine) =
|
||||
get_machine_pair_with_setup_sessions_test_helper(alice_id, alice_id, false).await;
|
||||
|
||||
let bob_device = alice_machine
|
||||
.get_device(alice_id, bob_machine.device_id(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let alice_device = bob_machine
|
||||
.get_device(alice_id, alice_machine.device_id(), None)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let decryption_key = crate::store::types::BackupDecryptionKey::new();
|
||||
alice_machine
|
||||
.backup_machine()
|
||||
.save_decryption_key(Some(decryption_key.clone()), None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(alice_machine, alice_device, bob_machine, bob_device, decryption_key)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
async fn test_secret_pushing() {
|
||||
let (alice_machine, _alice_device, _bob_machine, bob_device, _decryption_key) =
|
||||
set_up_secret_push().await;
|
||||
|
||||
// try to push a secret, but the other device isn't verified, so nothing
|
||||
// should happen
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.push_secret_to_verified_devices(SecretName::RecoveryKey)
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let alice_cache = alice_machine.store().cache().await.unwrap();
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.collect_incoming_key_requests(&alice_cache)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let requests =
|
||||
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
|
||||
|
||||
assert_eq!(requests.len(), 0);
|
||||
|
||||
// Now the device is trusted, so the secret should be pushed
|
||||
bob_device.set_trust_state(LocalTrust::Verified);
|
||||
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
|
||||
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.push_secret_to_verified_devices(SecretName::RecoveryKey)
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let alice_cache = alice_machine.store().cache().await.unwrap();
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.collect_incoming_key_requests(&alice_cache)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let requests =
|
||||
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
|
||||
|
||||
assert_eq!(requests.len(), 1);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
async fn test_secret_push_receive() {
|
||||
use futures_util::{FutureExt, pin_mut};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::EncryptionSyncChanges;
|
||||
|
||||
let (alice_machine, alice_device, bob_machine, bob_device, decryption_key) =
|
||||
set_up_secret_push().await;
|
||||
|
||||
// Push the secret to Bob
|
||||
bob_device.set_trust_state(LocalTrust::Verified);
|
||||
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
|
||||
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.push_secret_to_verified_devices(SecretName::RecoveryKey)
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let alice_cache = alice_machine.store().cache().await.unwrap();
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.collect_incoming_key_requests(&alice_cache)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let requests =
|
||||
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
|
||||
|
||||
assert_eq!(requests.len(), 1);
|
||||
let request = requests.first().expect("We should have an outgoing to-device request");
|
||||
|
||||
// Since Alice is trusted, we should get the secret
|
||||
alice_device.set_trust_state(LocalTrust::Verified);
|
||||
bob_machine.store().save_device_data(&[alice_device.inner]).await.unwrap();
|
||||
let event: EncryptedToDeviceEvent =
|
||||
request_to_event(bob_machine.user_id(), alice_machine.user_id(), request);
|
||||
let event = Raw::from_json(to_raw_value(&event).unwrap());
|
||||
|
||||
let stream = bob_machine.store().secrets_stream();
|
||||
pin_mut!(stream);
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
|
||||
bob_machine
|
||||
.receive_sync_changes(
|
||||
EncryptionSyncChanges {
|
||||
to_device_events: vec![event],
|
||||
changed_devices: &Default::default(),
|
||||
one_time_keys_counts: &Default::default(),
|
||||
unused_fallback_keys: None,
|
||||
next_batch_token: None,
|
||||
},
|
||||
&decryption_settings,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let secret = stream
|
||||
.next()
|
||||
.now_or_never()
|
||||
.flatten()
|
||||
.expect("The broadcaster should have sent out the secret");
|
||||
|
||||
assert_eq!(secret.secret.deref(), &decryption_key.to_base64())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
async fn test_secret_push_receive_untrusted() {
|
||||
use futures_util::{FutureExt, pin_mut};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::EncryptionSyncChanges;
|
||||
|
||||
let (alice_machine, _alice_device, bob_machine, bob_device, _decryption_key) =
|
||||
set_up_secret_push().await;
|
||||
|
||||
// Push the secret to Bob
|
||||
bob_device.set_trust_state(LocalTrust::Verified);
|
||||
alice_machine.store().save_device_data(&[bob_device.inner]).await.unwrap();
|
||||
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.push_secret_to_verified_devices(SecretName::RecoveryKey)
|
||||
.await
|
||||
.unwrap();
|
||||
{
|
||||
let alice_cache = alice_machine.store().cache().await.unwrap();
|
||||
alice_machine
|
||||
.inner
|
||||
.key_request_machine
|
||||
.collect_incoming_key_requests(&alice_cache)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let requests =
|
||||
alice_machine.inner.key_request_machine.outgoing_to_device_requests().await.unwrap();
|
||||
|
||||
assert_eq!(requests.len(), 1);
|
||||
let request = requests.first().expect("We should have an outgoing to-device request");
|
||||
|
||||
// Test receiving the event. Alice isn't trusted, so the secret will be
|
||||
// dropped
|
||||
let event: EncryptedToDeviceEvent =
|
||||
request_to_event(bob_machine.user_id(), alice_machine.user_id(), request);
|
||||
let event = Raw::from_json(to_raw_value(&event).unwrap());
|
||||
|
||||
let stream = bob_machine.store().secrets_stream();
|
||||
pin_mut!(stream);
|
||||
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
|
||||
bob_machine
|
||||
.receive_sync_changes(
|
||||
EncryptionSyncChanges {
|
||||
to_device_events: vec![event.clone()],
|
||||
changed_devices: &Default::default(),
|
||||
one_time_keys_counts: &Default::default(),
|
||||
unused_fallback_keys: None,
|
||||
next_batch_token: None,
|
||||
},
|
||||
&decryption_settings,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(stream.next().now_or_never().flatten().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
// Copyright 2020, 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -66,6 +66,8 @@ use tracing::{
|
||||
};
|
||||
use vodozemac::{Curve25519PublicKey, Ed25519Signature, megolm::DecryptionError};
|
||||
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
use crate::error::SecretPushError;
|
||||
#[cfg(feature = "experimental-send-custom-to-device")]
|
||||
use crate::session_manager::split_devices_for_share_strategy;
|
||||
use crate::{
|
||||
@@ -174,7 +176,6 @@ impl std::fmt::Debug for OlmMachine {
|
||||
}
|
||||
|
||||
impl OlmMachine {
|
||||
const CURRENT_GENERATION_STORE_KEY: &'static str = "generation-counter";
|
||||
const HAS_MIGRATED_VERIFICATION_LATCH: &'static str = "HAS_MIGRATED_VERIFICATION_LATCH";
|
||||
|
||||
/// Create a new memory based OlmMachine.
|
||||
@@ -959,8 +960,7 @@ impl OlmMachine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a received, decrypted, `io.element.msc4268.room_key_bundle`
|
||||
/// to-device event.
|
||||
/// Handle a received, decrypted, `m.room_key_bundle` to-device event.
|
||||
#[instrument()]
|
||||
async fn receive_room_key_bundle_data(
|
||||
&self,
|
||||
@@ -1409,6 +1409,13 @@ impl OlmMachine {
|
||||
debug!("Received a room key bundle event {:?}", e);
|
||||
self.receive_room_key_bundle_data(decrypted.result.sender_key, e, changes).await?;
|
||||
}
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
AnyDecryptedOlmEvent::SecretPush(e) => {
|
||||
self.inner
|
||||
.key_request_machine
|
||||
.receive_secret_push_event(&decrypted.result.sender_key, e, changes)
|
||||
.await?;
|
||||
}
|
||||
AnyDecryptedOlmEvent::Custom(_) => {
|
||||
warn!("Received an unexpected encrypted to-device event");
|
||||
}
|
||||
@@ -2013,6 +2020,21 @@ impl OlmMachine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a secret to all of our other verified devices.
|
||||
///
|
||||
/// This function assumes that we already have Olm sessions with the other
|
||||
/// devices. This can be done by calling
|
||||
/// [`OlmMachine::get_missing_sessions()`].
|
||||
///
|
||||
/// * `secret_name` - The name of the secret to push
|
||||
#[cfg(feature = "experimental-push-secrets")]
|
||||
pub async fn push_secret_to_verified_devices(
|
||||
&self,
|
||||
secret_name: SecretName,
|
||||
) -> Result<HashMap<OwnedDeviceId, OlmError>, SecretPushError> {
|
||||
self.inner.key_request_machine.push_secret_to_verified_devices(secret_name).await
|
||||
}
|
||||
|
||||
/// Get some metadata pertaining to a given group session.
|
||||
///
|
||||
/// This includes the session owner's Matrix user ID, their device ID, info
|
||||
@@ -2823,127 +2845,6 @@ impl OlmMachine {
|
||||
&self.inner.backup_machine
|
||||
}
|
||||
|
||||
/// Syncs the database and in-memory generation counter.
|
||||
///
|
||||
/// This requires that the crypto store lock has been acquired already.
|
||||
pub async fn initialize_crypto_store_generation(
|
||||
&self,
|
||||
generation: &Mutex<Option<u64>>,
|
||||
) -> StoreResult<()> {
|
||||
// Avoid reentrant initialization by taking the lock for the entire's function
|
||||
// scope.
|
||||
let mut gen_guard = generation.lock().await;
|
||||
|
||||
let prev_generation =
|
||||
self.inner.store.get_custom_value(Self::CURRENT_GENERATION_STORE_KEY).await?;
|
||||
|
||||
let generation = match prev_generation {
|
||||
Some(val) => {
|
||||
// There was a value in the store. We need to signal that we're a different
|
||||
// process, so we don't just reuse the value but increment it.
|
||||
u64::from_le_bytes(val.try_into().map_err(|_| {
|
||||
CryptoStoreError::InvalidLockGeneration("invalid format".to_owned())
|
||||
})?)
|
||||
.wrapping_add(1)
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
tracing::debug!("Initialising crypto store generation at {generation}");
|
||||
|
||||
self.inner
|
||||
.store
|
||||
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, generation.to_le_bytes().to_vec())
|
||||
.await?;
|
||||
|
||||
*gen_guard = Some(generation);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If needs be, update the local and on-disk crypto store generation.
|
||||
///
|
||||
/// ## Requirements
|
||||
///
|
||||
/// - This assumes that `initialize_crypto_store_generation` has been called
|
||||
/// beforehand.
|
||||
/// - This requires that the crypto store lock has been acquired.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `generation` - The in-memory generation counter (or rather, the
|
||||
/// `Mutex` wrapping it). This defines the "expected" generation on entry,
|
||||
/// and, if we determine an update is needed, is updated to hold the "new"
|
||||
/// generation.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A tuple containing:
|
||||
///
|
||||
/// * A `bool`, set to `true` if another process has updated the generation
|
||||
/// number in the `Store` since our expected value, and as such we've
|
||||
/// incremented and updated it in the database. Otherwise, `false`.
|
||||
///
|
||||
/// * The (possibly updated) generation counter.
|
||||
pub async fn maintain_crypto_store_generation(
|
||||
&'_ self,
|
||||
generation: &Mutex<Option<u64>>,
|
||||
) -> StoreResult<(bool, u64)> {
|
||||
let mut gen_guard = generation.lock().await;
|
||||
|
||||
// The database value must be there:
|
||||
// - either we could initialize beforehand, thus write into the database,
|
||||
// - or we couldn't, and then another process was holding onto the database's
|
||||
// lock, thus
|
||||
// has written a generation counter in there.
|
||||
let actual_gen = self
|
||||
.inner
|
||||
.store
|
||||
.get_custom_value(Self::CURRENT_GENERATION_STORE_KEY)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
CryptoStoreError::InvalidLockGeneration("counter missing in store".to_owned())
|
||||
})?;
|
||||
|
||||
let actual_gen =
|
||||
u64::from_le_bytes(actual_gen.try_into().map_err(|_| {
|
||||
CryptoStoreError::InvalidLockGeneration("invalid format".to_owned())
|
||||
})?);
|
||||
|
||||
let new_gen = match gen_guard.as_ref() {
|
||||
Some(expected_gen) => {
|
||||
if actual_gen == *expected_gen {
|
||||
return Ok((false, actual_gen));
|
||||
}
|
||||
// Increment the biggest, and store it everywhere.
|
||||
actual_gen.max(*expected_gen).wrapping_add(1)
|
||||
}
|
||||
None => {
|
||||
// Some other process hold onto the lock when initializing, so we must reload.
|
||||
// Increment database value, and store it everywhere.
|
||||
actual_gen.wrapping_add(1)
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
"Crypto store generation mismatch: previously known was {:?}, actual is {:?}, next is {}",
|
||||
*gen_guard,
|
||||
actual_gen,
|
||||
new_gen
|
||||
);
|
||||
|
||||
// Update known value.
|
||||
*gen_guard = Some(new_gen);
|
||||
|
||||
// Update value in database.
|
||||
self.inner
|
||||
.store
|
||||
.set_custom_value(Self::CURRENT_GENERATION_STORE_KEY, new_gen.to_le_bytes().to_vec())
|
||||
.await?;
|
||||
|
||||
Ok((true, new_gen))
|
||||
}
|
||||
|
||||
/// Manage dehydrated devices.
|
||||
pub fn dehydrated_devices(&self) -> DehydratedDevices {
|
||||
DehydratedDevices { inner: self.to_owned() }
|
||||
|
||||
@@ -272,7 +272,7 @@ pub async fn build_encrypted_to_device_content_without_sender_data(
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let ciphertext = olm_session.encrypt_helper(&plaintext).await;
|
||||
let ciphertext = olm_session.encrypt_helper(&plaintext).await.unwrap();
|
||||
let content =
|
||||
olm_session.build_encrypted_event(ciphertext, None).await.expect("could not encrypt");
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ async fn create_and_share_session_without_sender_data(
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let ciphertext = olm_session.encrypt_helper(&plaintext).await;
|
||||
let ciphertext = olm_session.encrypt_helper(&plaintext).await.unwrap();
|
||||
ToDeviceEvent::new(
|
||||
alice.user_id().to_owned(),
|
||||
olm_session.build_encrypted_event(ciphertext, None).await.unwrap(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user