mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-08-07 02:49:45 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64a029ee09 | ||
|
|
a3f6971579 | ||
|
|
51681aec1b | ||
|
|
39c84fabb4 | ||
|
|
f77bd41837 | ||
|
|
6f34b8e9ca | ||
|
|
5051da493a | ||
|
|
ff0e007c45 | ||
|
|
b85fb5ea6f | ||
|
|
e905538269 | ||
|
|
6f672b7304 | ||
|
|
4363ed6ec3 | ||
|
|
dd50a4cb0b | ||
|
|
e0a997c227 | ||
|
|
e1f89b69ea | ||
|
|
888f72d8d0 | ||
|
|
06618eadab | ||
|
|
05390d6097 | ||
|
|
1f803fe3a9 | ||
|
|
1492d68e25 | ||
|
|
c1aa94fb91 | ||
|
|
7320d0a40b | ||
|
|
abded2d442 | ||
|
|
4afd6f347b | ||
|
|
6b8d6956a3 | ||
|
|
f59d62c01c | ||
|
|
a14556da97 | ||
|
|
8b1de3d8db | ||
|
|
240b498489 | ||
|
|
d680a6ba53 | ||
|
|
aa3f14cd57 | ||
|
|
15627bc8d0 | ||
|
|
084facf474 | ||
|
|
d24986edf1 | ||
|
|
ce1ac277a6 | ||
|
|
7aeed0a95a | ||
|
|
9265748a57 | ||
|
|
e85cfdf48a | ||
|
|
48923b3657 | ||
|
|
aedaf3f0c1 | ||
|
|
b24b59dc38 | ||
|
|
738b5e3fa5 | ||
|
|
1f0cfec5ca | ||
|
|
9c5caa3a5f | ||
|
|
1cf4a26ae9 | ||
|
|
3694ffbab3 | ||
|
|
af0e01e016 | ||
|
|
5e89f0acae | ||
|
|
563873af77 | ||
|
|
d2072080c9 | ||
|
|
e191730950 | ||
|
|
f660e00bb5 | ||
|
|
f613d0c2ad | ||
|
|
7596ad2019 | ||
|
|
384add9784 | ||
|
|
a234f019b1 | ||
|
|
e8a87bdfa3 | ||
|
|
6a4aff424f | ||
|
|
8959d9e2c1 | ||
|
|
a30c043386 | ||
|
|
a39ef994d2 | ||
|
|
b714f24029 | ||
|
|
2263f2e874 | ||
|
|
9abe9becd6 | ||
|
|
4b74c01895 | ||
|
|
1223763e2b | ||
|
|
9b64c1f105 | ||
|
|
212a8434a8 | ||
|
|
bfaac8b5a2 | ||
|
|
9af15ecbba | ||
|
|
f66a83763e | ||
|
|
718c3adcb2 | ||
|
|
af80482c04 | ||
|
|
a20ddcd586 | ||
|
|
b483306367 | ||
|
|
f5e98467be | ||
|
|
d44db45f83 | ||
|
|
0397bb8237 | ||
|
|
4010fc62bc | ||
|
|
0d823a2822 | ||
|
|
9cd175b125 | ||
|
|
050a1a350a | ||
|
|
ec0f872f8f | ||
|
|
024e8eae62 | ||
|
|
4fd60b2605 | ||
|
|
e53968d9eb | ||
|
|
7cbc2ee385 | ||
|
|
0df5e5e7ac | ||
|
|
312eb69450 | ||
|
|
5b620a2c37 | ||
|
|
3b4fbb8c1a | ||
|
|
158d44e1a9 | ||
|
|
d2aab468cf | ||
|
|
aa4486dfdf | ||
|
|
2a662445b6 | ||
|
|
5b3f0fde23 | ||
|
|
9640afebff | ||
|
|
dd5c5c7a4a | ||
|
|
fc0f04defa | ||
|
|
7d8f7cbe5d | ||
|
|
4e0249cd2f |
+11
-10
@@ -20,13 +20,13 @@ jobs:
|
||||
name: Init
|
||||
runs-on: ${{matrix.runner}}
|
||||
env:
|
||||
default_cargo_profiles: '["test", "release"]'
|
||||
default_cargo_profiles: '["test", "bench", "release"]'
|
||||
default_feat_sets: '["none", "default", "all"]'
|
||||
default_rust_toolchains: '["nightly", "stable"]'
|
||||
default_sys_names: '["debian"]'
|
||||
default_sys_versions: '["testing-slim"]'
|
||||
default_rust_targets: '["x86_64-unknown-linux-gnu"]'
|
||||
default_sys_targets: '["x86_64-v1-linux-gnu"]'
|
||||
default_sys_targets: '["x86_64-v1-linux-gnu", "x86_64-v3-linux-gnu"]'
|
||||
default_machines: '["X64"]'
|
||||
|
||||
outputs:
|
||||
@@ -38,17 +38,17 @@ jobs:
|
||||
sys_targets: ${{vars.SYS_TARGETS || env.default_sys_targets}}
|
||||
sys_versions: ${{vars.SYS_VERSIONS || env.default_sys_versions}}
|
||||
machines: ${{vars.MACHINES || env.default_machines}}
|
||||
package: ${{vars.PACKAGE || !contains(github.ref, 'refs/pull/')}}
|
||||
publish: ${{vars.PUBLISH || !contains(github.ref, 'refs/pull/')}}
|
||||
build_nix: ${{vars.BUILD_NIX || !contains(github.ref, 'refs/pull/')}}
|
||||
build_pkgs: ${{vars.BUILD_PKGS || github.ref == 'refs/heads/main' || contains(github.ref, 'tags/v')}}
|
||||
package: ${{vars.PACKAGE != 'false'}}
|
||||
publish: ${{vars.PUBLISH != 'false'}}
|
||||
build_nix: ${{vars.BUILD_NIX != 'false'}}
|
||||
build_pkgs: ${{vars.BUILD_PKGS || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/test' || contains(github.ref, 'tags/v')}}
|
||||
check_pkgs: ${{vars.CHECK_PKGS || 'false'}}
|
||||
complement: ${{vars.COMPLEMENT || 'true'}}
|
||||
complement: ${{vars.COMPLEMENT != 'false'}}
|
||||
complement_runner: 'het'
|
||||
docker_repo: ${{vars.DOCKER_REPO}}
|
||||
release_url: ${{steps.release.outputs.upload_url}}
|
||||
pages_url: 'https://matrix-construct.github.io/tuwunel/'
|
||||
rust_sdk_integration: ${{vars.RUST_SDK_INTEGRATION || 'true'}}
|
||||
rust_sdk_integ: ${{vars.RUST_SDK_INTEGRATION != 'false'}}
|
||||
head_msg: ${{github.event.head_commit.message || github.event.workflow_run.head_commit.message}}
|
||||
|
||||
strategy:
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
with:
|
||||
head_msg: ${{needs.init.outputs.head_msg}}
|
||||
build_nix: ${{fromJSON(needs.init.outputs.build_nix)}}
|
||||
rust_sdk_integration: ${{fromJSON(needs.init.outputs.rust_sdk_integration)}}
|
||||
rust_sdk_integ: ${{fromJSON(needs.init.outputs.rust_sdk_integ)}}
|
||||
complement: ${{fromJSON(needs.init.outputs.complement)}}
|
||||
complement_runner: ${{needs.init.outputs.complement_runner}}
|
||||
cargo_profiles: ${{needs.init.outputs.cargo_profiles}}
|
||||
@@ -189,7 +189,6 @@ jobs:
|
||||
machines: ${{needs.init.outputs.machines}}
|
||||
excludes: >
|
||||
[
|
||||
{"feat_set": "logging"},
|
||||
{"cargo_profile": "test", "rust_toolchain": "stable", "feat_set": "none"},
|
||||
{"cargo_profile": "test", "rust_target": "aarch64-unknown-linux-gnu"},
|
||||
{"cargo_profile": "release-debuginfo"},
|
||||
@@ -223,6 +222,7 @@ jobs:
|
||||
if: >
|
||||
!failure() && !cancelled()
|
||||
&& needs.init.outputs.package
|
||||
&& !contains(github.ref, 'refs/pull')
|
||||
&& !contains(needs.init.outputs.head_msg, '[ci only it]')
|
||||
&& !contains(needs.init.outputs.head_msg, '[ci no build]')
|
||||
&& !contains(needs.init.outputs.head_msg, '[ci no package]')
|
||||
@@ -286,6 +286,7 @@ jobs:
|
||||
if: >
|
||||
!failure() && !cancelled()
|
||||
&& needs.init.outputs.publish
|
||||
&& !contains(github.ref, 'refs/pull')
|
||||
&& !contains(needs.init.outputs.head_msg, '[ci only it]')
|
||||
&& !contains(needs.init.outputs.head_msg, '[ci no build]')
|
||||
&& !contains(needs.init.outputs.head_msg, '[ci no package]')
|
||||
|
||||
@@ -181,10 +181,10 @@ jobs:
|
||||
"mime": "application/x-rpm"
|
||||
},
|
||||
"nix": {
|
||||
"dst": "tuwunel.drv.tar.zst",
|
||||
"src": "/opt/tuwunel/tuwunel.drv",
|
||||
"dst": "tuwunel.nix.tar.zst",
|
||||
"src": "/opt/tuwunel.nix.tar",
|
||||
"mime": "application/zstd",
|
||||
"zstd": 6
|
||||
"zstd": 11
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
-38
@@ -38,12 +38,9 @@ on:
|
||||
complement:
|
||||
type: boolean
|
||||
default: true
|
||||
complement_feat_sets:
|
||||
type: string
|
||||
default: '["all"]'
|
||||
complement_runner:
|
||||
type: string
|
||||
rust_sdk_integration:
|
||||
rust_sdk_integ:
|
||||
type: boolean
|
||||
default: true
|
||||
build_nix:
|
||||
@@ -84,10 +81,10 @@ jobs:
|
||||
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
|
||||
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
|
||||
|
||||
name: Unit
|
||||
name: Module
|
||||
uses: ./.github/workflows/bake.yml
|
||||
with:
|
||||
bake_targets: '["unit", "integration"]'
|
||||
bake_targets: '["unit", "integ"]'
|
||||
cargo_profiles: '["test"]'
|
||||
feat_sets: '["all"]'
|
||||
rust_toolchains: ${{inputs.rust_toolchains}}
|
||||
@@ -110,7 +107,7 @@ jobs:
|
||||
name: Bench
|
||||
uses: ./.github/workflows/bake.yml
|
||||
with:
|
||||
bake_targets: '["unit", "integration"]'
|
||||
bake_targets: '["unit", "integ"]'
|
||||
cargo_profiles: '["bench"]'
|
||||
feat_sets: '["all"]'
|
||||
rust_toolchains: '["nightly"]'
|
||||
@@ -122,7 +119,7 @@ jobs:
|
||||
excludes: ${{inputs.excludes}}
|
||||
includes: ${{inputs.includes}}
|
||||
|
||||
valgrind:
|
||||
memcheck:
|
||||
if: >
|
||||
!contains(inputs.head_msg, '[ci only it]')
|
||||
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
|
||||
@@ -133,8 +130,9 @@ jobs:
|
||||
name: Memcheck
|
||||
uses: ./.github/workflows/bake.yml
|
||||
with:
|
||||
bake_targets: '["integration-valgrind"]'
|
||||
cargo_profiles: '["bench"]'
|
||||
#bake_targets: '["unit-valgrind", "integ-valgrind"]'
|
||||
bake_targets: '["integ-valgrind"]'
|
||||
cargo_profiles: '["bench"]' # use bench not release for debug syms
|
||||
feat_sets: '["all"]'
|
||||
rust_toolchains: '["nightly"]'
|
||||
sys_names: ${{inputs.sys_names}}
|
||||
@@ -142,8 +140,8 @@ jobs:
|
||||
rust_targets: ${{inputs.rust_targets}}
|
||||
sys_targets: '["x86_64-v3-linux-gnu"]'
|
||||
machines: ${{inputs.machines}}
|
||||
excludes: ${{inputs.excludes}}
|
||||
includes: ${{inputs.includes}}
|
||||
excludes: ${{inputs.excludes}}
|
||||
|
||||
smoke:
|
||||
if: >
|
||||
@@ -225,8 +223,9 @@ jobs:
|
||||
!failure() && !cancelled()
|
||||
&& !contains(inputs.head_msg, '[ci only it]')
|
||||
&& !contains(inputs.head_msg, '[ci no build]')
|
||||
&& !contains(github.ref, 'refs/pull')
|
||||
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["release"]')[0])
|
||||
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
|
||||
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
|
||||
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["stable"]')[0])
|
||||
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
|
||||
&& fromJSON(inputs.build_nix)
|
||||
@@ -246,61 +245,78 @@ jobs:
|
||||
excludes: ${{inputs.excludes}}
|
||||
includes: ${{inputs.includes}}
|
||||
|
||||
matrix-rust-sdk-integration:
|
||||
rust-sdk-integ:
|
||||
if: >
|
||||
!failure() && !cancelled()
|
||||
&& !contains(inputs.head_msg, '[ci no build]')
|
||||
&& inputs.rust_sdk_integration
|
||||
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
|
||||
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
|
||||
&& inputs.rust_sdk_integ
|
||||
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
|
||||
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
|
||||
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
|
||||
&& contains(fromJSON(inputs.rust_targets), fromJSON('["x86_64-unknown-linux-gnu"]')[0])
|
||||
|
||||
name: Matrix SDK Integration
|
||||
needs: [smoke]
|
||||
uses: ./.github/workflows/bake.yml
|
||||
with:
|
||||
bake_targets: '["matrix-rust-sdk-integration"]'
|
||||
cargo_profiles: '["test"]'
|
||||
feat_sets: ${{inputs.complement_feat_sets}}
|
||||
#bake_targets: '["rust-sdk-integ", "rust-sdk-valgrind"]'
|
||||
bake_targets: '["rust-sdk-integ"]'
|
||||
cargo_profiles: ${{inputs.cargo_profiles}}
|
||||
feat_sets: '["all"]'
|
||||
rust_toolchains: '["nightly"]'
|
||||
sys_names: ${{inputs.sys_names}}
|
||||
sys_versions: ${{inputs.sys_versions}}
|
||||
rust_targets: ${{inputs.rust_targets}}
|
||||
sys_targets: '["x86_64-v1-linux-gnu"]'
|
||||
machines: ${{inputs.machines}}
|
||||
rust_targets: '["x86_64-unknown-linux-gnu"]'
|
||||
sys_targets: ${{inputs.sys_targets}}
|
||||
machines: '["X64"]'
|
||||
runner: ${{inputs.complement_runner}}
|
||||
excludes: ${{inputs.excludes}}
|
||||
includes: ${{inputs.includes}}
|
||||
artifact: >
|
||||
{
|
||||
"matrix-rust-sdk-integration": {
|
||||
"rust-sdk-integ": {
|
||||
"src": "/var/log/tuwunel.log",
|
||||
"dst": "matrix-rust-sdk-integration.tuwunel.log",
|
||||
"dst": "rust-sdk-integ.tuwunel.log",
|
||||
},
|
||||
"rust-sdk-valgrind": {
|
||||
"src": "/var/log/tuwunel.log",
|
||||
"dst": "rust-sdk-valgrind.tuwunel.log",
|
||||
}
|
||||
}
|
||||
excludes: >
|
||||
[
|
||||
{"bake_target": "rust-sdk-valgrind", "cargo_profile": "test"},
|
||||
{"feat_set": "none"},
|
||||
{"feat_set": "logging"},
|
||||
{"cargo_profile": "release"},
|
||||
{"cargo_profile": "release-debuginfo"},
|
||||
{"cargo_profile": "release-native"},
|
||||
{"cargo_profile": "test", "sys_target": "x86_64-v2-linux-gnu"},
|
||||
{"cargo_profile": "test", "sys_target": "x86_64-v3-linux-gnu"},
|
||||
{"cargo_profile": "bench", "sys_target": "x86_64-v1-linux-gnu"},
|
||||
{"cargo_profile": "bench", "sys_target": "x86_64-v2-linux-gnu"},
|
||||
{"rust_target": "aarch64-unknown-linux-gnu"},
|
||||
{"sys_target": "aarch64-v8-linux-gnu"},
|
||||
]
|
||||
|
||||
complement:
|
||||
if: >
|
||||
!failure() && !cancelled()
|
||||
&& !contains(inputs.head_msg, '[ci no build]')
|
||||
&& inputs.complement
|
||||
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
|
||||
&& contains(fromJSON(inputs.complement_feat_sets), fromJSON('["all"]')[0])
|
||||
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
|
||||
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
|
||||
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
|
||||
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
|
||||
|
||||
name: Complement
|
||||
uses: ./.github/workflows/bake.yml
|
||||
with:
|
||||
bake_targets: '["complement-tester", "complement-testee"]'
|
||||
cargo_profiles: '["test"]'
|
||||
feat_sets: ${{inputs.complement_feat_sets}}
|
||||
cargo_profiles: '["bench"]'
|
||||
feat_sets: '["logging"]'
|
||||
rust_toolchains: '["nightly"]'
|
||||
sys_names: ${{inputs.sys_names}}
|
||||
sys_versions: ${{inputs.sys_versions}}
|
||||
rust_targets: ${{inputs.rust_targets}}
|
||||
sys_targets: '["x86_64-v1-linux-gnu"]'
|
||||
sys_targets: '["x86_64-v3-linux-gnu"]'
|
||||
machines: ${{inputs.machines}}
|
||||
runner: ${{inputs.complement_runner}}
|
||||
excludes: ${{inputs.excludes}}
|
||||
@@ -312,10 +328,9 @@ jobs:
|
||||
&& !contains(inputs.head_msg, '[ci no build]')
|
||||
&& inputs.complement
|
||||
&& inputs.machines
|
||||
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["test"]')[0])
|
||||
&& contains(fromJSON(inputs.feat_sets), fromJSON('["all"]')[0])
|
||||
&& contains(fromJSON(inputs.cargo_profiles), fromJSON('["bench"]')[0])
|
||||
&& contains(fromJSON(inputs.rust_toolchains), fromJSON('["nightly"]')[0])
|
||||
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v1-linux-gnu"]')[0])
|
||||
&& contains(fromJSON(inputs.sys_targets), fromJSON('["x86_64-v3-linux-gnu"]')[0])
|
||||
|
||||
name: Matrix Compliance
|
||||
needs: [complement, smoke]
|
||||
@@ -327,13 +342,13 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cargo_profile: ${{fromJSON('["test"]')}}
|
||||
feat_set: ${{fromJSON(inputs.complement_feat_sets)}}
|
||||
cargo_profile: ${{fromJSON('["bench"]')}}
|
||||
feat_set: ${{fromJSON('["logging"]')}}
|
||||
rust_toolchain: ${{fromJSON('["nightly"]')}}
|
||||
sys_name: ${{fromJSON(inputs.sys_names)}}
|
||||
sys_version: ${{fromJSON(inputs.sys_versions)}}
|
||||
rust_target: ${{fromJSON(inputs.rust_targets)}}
|
||||
sys_target: ${{fromJSON('["x86_64-v1-linux-gnu"]')}}
|
||||
sys_target: ${{fromJSON('["x86_64-v3-linux-gnu"]')}}
|
||||
machine: ${{fromJSON(inputs.machines)}}
|
||||
exclude: ${{fromJSON(inputs.excludes)}}
|
||||
include: ${{fromJSON(inputs.includes)}}
|
||||
|
||||
Generated
+321
-471
File diff suppressed because it is too large
Load Diff
+25
-18
@@ -28,8 +28,8 @@ keywords = [
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-construct/tuwunel"
|
||||
rust-version = "1.88.0"
|
||||
version = "1.4.3"
|
||||
rust-version = "1.89.0"
|
||||
version = "1.4.6"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "tuwunel"
|
||||
@@ -104,7 +104,7 @@ features = [
|
||||
version = "1.10"
|
||||
|
||||
[workspace.dependencies.bytesize]
|
||||
version = "2.0"
|
||||
version = "2.1"
|
||||
|
||||
[workspace.dependencies.cargo_toml]
|
||||
version = "0.22"
|
||||
@@ -192,7 +192,7 @@ version = "1.3"
|
||||
version = "0.1"
|
||||
|
||||
[workspace.dependencies.hyper]
|
||||
version = "1.6"
|
||||
version = "1.7"
|
||||
default-features = false
|
||||
features = [
|
||||
"server",
|
||||
@@ -262,7 +262,7 @@ version = "0.1"
|
||||
version = "1.0"
|
||||
|
||||
[workspace.dependencies.minicbor]
|
||||
version = "2.0"
|
||||
version = "2.1"
|
||||
features = ["std"]
|
||||
|
||||
[workspace.dependencies.minicbor-serde]
|
||||
@@ -281,7 +281,7 @@ features = [
|
||||
version = "0.2"
|
||||
|
||||
[workspace.dependencies.opentelemetry]
|
||||
version = "0.30.0"
|
||||
version = "0.31"
|
||||
|
||||
# Disabled until they move to opentelemetry 0.30
|
||||
#[workspace.dependencies.opentelemetry-jaeger]
|
||||
@@ -289,7 +289,7 @@ version = "0.30.0"
|
||||
#features = ["rt-tokio"]
|
||||
|
||||
[workspace.dependencies.opentelemetry_sdk]
|
||||
version = "0.30"
|
||||
version = "0.31"
|
||||
features = ["rt-tokio"]
|
||||
|
||||
[workspace.dependencies.proc-macro2]
|
||||
@@ -302,7 +302,7 @@ version = "1.0"
|
||||
version = "0.8"
|
||||
|
||||
[workspace.dependencies.regex]
|
||||
version = "1.11"
|
||||
version = "1.12"
|
||||
|
||||
[workspace.dependencies.reqwest]
|
||||
version = "0.12"
|
||||
@@ -320,7 +320,7 @@ default-features = false
|
||||
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://github.com/matrix-construct/ruma"
|
||||
rev = "3c26be1e4d670c2c71e2bc84b3f09c67c4188612"
|
||||
rev = "db4bf59eaa1757735f38489f8924348ae05a6a17"
|
||||
features = [
|
||||
"__compat",
|
||||
"appservice-api-c",
|
||||
@@ -339,6 +339,7 @@ features = [
|
||||
"unstable-msc2870",
|
||||
"unstable-msc3026",
|
||||
"unstable-msc3061",
|
||||
"unstable-msc3814",
|
||||
"unstable-msc3245",
|
||||
"unstable-msc3381", # polls
|
||||
"unstable-msc3489", # beacon / live location
|
||||
@@ -348,6 +349,7 @@ features = [
|
||||
"unstable-msc4121",
|
||||
"unstable-msc4125",
|
||||
"unstable-msc4133",
|
||||
"unstable-msc4143",
|
||||
"unstable-msc4186",
|
||||
"unstable-msc4203", # sending to-device events to appservices
|
||||
"unstable-msc4310",
|
||||
@@ -384,7 +386,7 @@ version = "0.4"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.sentry]
|
||||
version = "0.42"
|
||||
version = "0.45"
|
||||
default-features = false
|
||||
features = [
|
||||
"backtrace",
|
||||
@@ -400,10 +402,10 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.sentry-tower]
|
||||
version = "0.42"
|
||||
version = "0.45"
|
||||
|
||||
[workspace.dependencies.sentry-tracing]
|
||||
version = "0.42"
|
||||
version = "0.45"
|
||||
|
||||
[workspace.dependencies.serde]
|
||||
version = "1.0"
|
||||
@@ -498,7 +500,7 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.tokio]
|
||||
version = "1.47"
|
||||
version = "1.48"
|
||||
default-features = false
|
||||
features = [
|
||||
"fs",
|
||||
@@ -551,7 +553,7 @@ default-features = false
|
||||
version = "0.2"
|
||||
|
||||
[workspace.dependencies.tracing-opentelemetry]
|
||||
version = "0.31"
|
||||
version = "0.32"
|
||||
|
||||
[workspace.dependencies.tracing-subscriber]
|
||||
version = "0.3"
|
||||
@@ -702,8 +704,8 @@ inherits = "release-native.build-override"
|
||||
#]
|
||||
|
||||
[profile.bench]
|
||||
debug = false
|
||||
strip = false
|
||||
debug = "limited"
|
||||
strip = "none"
|
||||
#rustflags = [
|
||||
# "-Cremark=all",
|
||||
# '-Ztime-passes',
|
||||
@@ -723,7 +725,7 @@ strip = false
|
||||
# and can be raised if build times are tolerable.
|
||||
|
||||
[profile.dev]
|
||||
debug = "full"
|
||||
debug = 0
|
||||
#rustflags = [
|
||||
# '--cfg', 'tuwunel_mods',
|
||||
# '-Ztime-passes',
|
||||
@@ -783,7 +785,7 @@ inherits = "dev"
|
||||
|
||||
[profile.dev.package.'*']
|
||||
inherits = "dev"
|
||||
debug = "limited"
|
||||
debug = 0
|
||||
incremental = false
|
||||
codegen-units = 1
|
||||
opt-level = 'z'
|
||||
@@ -800,6 +802,11 @@ opt-level = 'z'
|
||||
# '-Clink-arg=-Wl,-z,nodelete',
|
||||
#]
|
||||
|
||||
# same as dev but slower.
|
||||
[profile.dbg]
|
||||
inherits = "dev"
|
||||
debug = "full"
|
||||
|
||||
# primarily used for CI
|
||||
[profile.test]
|
||||
debug = "limited"
|
||||
|
||||
+26
-29
@@ -1,50 +1,47 @@
|
||||
# Tuwunel 1.4.3
|
||||
# Tuwunel 1.4.6
|
||||
|
||||
October 10, 2025
|
||||
November 6, 2025
|
||||
|
||||
### Featured
|
||||
### New Features
|
||||
|
||||
**Sync Tokens have been eliminated** now with stateless sync. Users should notice a reduction in database size after running this version. Long-time users, including from conduwuit and even Conduit will benefit the most. New users may not notice reductions, but nevertheless will be preventing database growth going forward. With the prior addition of room deletions courtesy of @dasha-uwu, only enhanced media retention remains between Tuwunel and sustainable cost-of-ownership.
|
||||
- Element Call discovery support was implemented by @tototomate123 in (#209). Adding a `[[global.well_known.rtc_transports]]` section in your [config file](https://github.com/matrix-construct/tuwunel/blob/e1f89b69ea117f166be423f035a5a34f4c0e7366/tuwunel-example.toml#L1835-L1851) enables discovery. More information on setting up Element Call can be found at [Spaetzblog](https://sspaeth.de/2024/11/sfu/), skipping step one, and performing step three in your Tuwunel config.
|
||||
|
||||
**Sliding Sync has been fixed (Element X)** after a third pass was made to install an entirely new architecture based on the latest evolution of the highly active MSC4186. For background, the first work done earlier this year was for optimization without logical changes to what was inherited from conduwuit. The existing implementation worked by all appearances, but didn't meet specific production quality demands. The second pass made last month to rapidly prepare this passé implementation for production use against highly developed modern clients did not turn out well. More tests passed; fewer things worked. This time the core logic had been rewritten. These three iterations have now modernized the entire module to keep up with the final stages of the specification's development. It is still not perfect, so your input and issue reports are greatly appreciated as always.
|
||||
- Dehydrated Device support (MSC3814) is now available (#200). This feature allows users to receive encrypted messages without being logged in. Supporting clients will setup the dehydrated device automatically and it will "just work" behind the scenes; in fact, these clients will also hide it from the sessions list to avoid confusion. Support is not widespread yet but it has been tested with matrix-js-sdk clients such as Element-web. This feature was commissioned and made public by an enterprise sponsor.
|
||||
|
||||
- Notification panel (the 🔔 button) has been implemented in (#201). Even though Element-web now requires enabling it in the Labs menu, the underlying support (`GET /_matrix/client/v3/notifications`) enhances the push-notification handling of other clients.
|
||||
|
||||
- Live room previews are now available. This support (`GET /_matrix/client/v3/events`) allows users, including guests, to sync updates for a public room without joining (4afd6f347b1).
|
||||
|
||||
- Thanks to a suggestion by @cyberdoors in (#29), the configuration option `encryption_enabled_by_default_for_room_type` is now available. This feature can enable encryption for a room even when the client does not. The values `all` or `invite` are accepted, the latter roughly meaning DM's only. Neither are enabled by default.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Configuration options passed after arguments `--option` or `-O` now accept dots to address the TOML tables found in the config file. Thanks to the suggestion by @lucat1 while trying to configure `ldap.bind_password_file` from the command line (#162). This was separately uncovered by @andrewerf trying to configure the `tls` section (#167).
|
||||
- Thank you @tototomate123 for improving the reverse-proxy docs, adding dedicated pages for both [Caddy](https://github.com/matrix-construct/tuwunel/blob/e0a997c22784b453735b24907e607412b153ba56/docs/deploying/reverse-proxy-caddy.md) and [Nginx](https://github.com/matrix-construct/tuwunel/blob/e0a997c22784b453735b24907e607412b153ba56/docs/deploying/reverse-proxy-nginx.md) in (#209). Thanks to @tycode for pointing out the docs were missing for alternative proxies in (#197).
|
||||
|
||||
- Configuring `log_to_stderr` now provides an explicit way to redirect logging and tracing to stderr. This is often requested for use with systemd.
|
||||
- Thanks to an observation by @iwalkalone69 in (#40), the last-seen time for a device in the session list is now updated acceptably. This function piggybacks on the presence system to prevent writing too frequently; testing has never shown it more than a minute or few out of date.
|
||||
|
||||
- The `!admin query raw` commands no longer require the redundant `raw-` prefix on every command name.
|
||||
- Thanks to an inquiry by @EntityinArray in (#189) guest-accounts can now be enabled while registration tokens are also enabled to prevent fully open account registration. Note that registration tokens don't apply to guest-accounts and those are still fully open.
|
||||
|
||||
- Thanks to @SophiaH67 for pointing out that our new verbose-logging docker images aren't very useful without being pushed to registries, then taking the initiative to host it themselves until we corrected that.
|
||||
|
||||
- After a report by @munkinasack in (#186), @dasha-uwu determined we could solve a lot of recurring network issues by clearing the well-known cache entry for a destination that becomes unreachable.
|
||||
|
||||
- Thanks to @dasha-uwu for general improvements, refactoring and fixes for the room alias and presence subsystems.
|
||||
|
||||
- Thanks to a report by @ohitsdylan a cryptic error message from the DNS resolver has now been caught and reworded to indicate a missing or empty `/etc/resolv.conf` (#179).
|
||||
|
||||
- Inspired by @boarfish offering due confusion about our many build variants in (#175), some documentation about `x86_64-v1` `-v2` and `-v3` has been improved. Special thanks to @Hashbrown777 for providing a quick one-liner shell script which made its way to the documentation.
|
||||
|
||||
- Various performance improvements took place in s2s request handlers, and the ubiquitous matrix event `Pdu` structure.
|
||||
|
||||
- Nix builds have been added to CI.
|
||||
- Courtesy of @dasha-uwu the list of servers attempted when joining a room is now properly shuffled to increase the odds of finding a viable server, especially if an additional join attempt is made.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Thanks to @harharlinks for reporting the Code of Conduct link on our github-pages was 404. Also thanks to @Tronde for reporting additional broken links in (#165). This helped discover pages had stopped deploying at some point and needed fixing.
|
||||
- Special thanks to @BVollmerhaus for finding the TURN secret file configured by `turn_secret_file` was broken in (#211), forcing users to configure `turn_secret` directly. Thank you for fixing this in (#212).
|
||||
|
||||
- Thanks to @agx for contributing a fix for the systemd units on multiple platforms which contained unknown or deprecated keys (#168). And thanks again for adding missing documentation to the well-known sections of the example configuration which many users will greatly appreciate (#173).
|
||||
- Thank you @scvalex for updating the nix build for Tuwunel's integration tests and re-enabling all checks. (#215)
|
||||
|
||||
- @mitch9911:matrix.org reported the `device_signing/upload` endpoint was omitted when adding JWT as a UIAA flow to other API's. This was subsequently patched (#169).
|
||||
- Thanks to a report by @Anagastes in (#146) **Nheko and NeoChat users can now enjoy properly verified devices.** Special thanks for the assistance of @deepbluev7 with diagnosing the cross-signing signature issue.
|
||||
|
||||
- The report by @orhtej2 of an invalid `?via` parameter sent by FluffyChat when joining a version 12 room was greatly appreciated, even though there was nothing more we could do on our end (#171). Thanks to @dasha-uwu for triaging and investigating this issue to conclusion.
|
||||
- Database columns intended for deletion, notably `roomsynctoken_shortstatehash`, never had the deletion command actually invoked on them 😭 explaining the lack of enthusiasm after the 1.4.3 release introduced stateless sync. **Users will now see the free disk space they were promised.** This was uncovered during an unrelated issue investigation courtesy of @frebib.
|
||||
|
||||
- Thanks to a follow-up by @alaviss in (#176) the DNS-passthru feature was found to still be enforcing a large minimum-TTL for its DNS cache. This was subsequently corrected.
|
||||
- Thanks to investigation by @dasha-uwu the pagination tokens in the `/relations` endpoint were buggy and now operate correctly.
|
||||
|
||||
- Thank you @canarysnort01 for the apropos and rather surgical off-by-one fix to sliding-sync range selection in (#188). Unfortunately this entire unit had to be rewritten for the latest iteration of sliding-sync, but the fix carried value in any case to improve the rewrite.
|
||||
- Thanks to @Polve for identifying the `DynamicUser=yes` directive in the systemd files was invalid and advising a replacement in (#207).
|
||||
|
||||
### Notices
|
||||
- Thanks to @daudix for reporting an edge-case where the server will refuse to start rather than robustly reporting errors during startup checks and recreate a missing media directory (#213).
|
||||
|
||||
It has come to our attention courtesy of @andrewerf that the Arch packages are built with `--no-default-features`. This may be a problem for an ideal experience. The backstory is that conduwuit underwent a "feature skew" over its lifetime which still remains today: our default-features are basically minimal requirements, while `--all-features` should be default features. Let us first take a moment to reiterate our gratitude to AUR package maintainers @drrossum and @kimiblock who have supported this project from the first hours of its existence. No action is required on their part as the plan now is to remove several optional features by the next release to make `--no-default-features` viable. We still recommend default features in general unless this conflicts with AUR policies or philosophies.
|
||||
- Push rule evaluation was never implemented for invites arriving over federation. Notifications are now properly sent in this case.
|
||||
|
||||
- Sliding-sync handlers were susceptible to errors under rare circumstances escaping to cause an HTTP 500, which wreaks havoc on the rust-sdk. This has now been prevented.
|
||||
|
||||
- Federating with Conduit over several non-essential endpoints was broken. It is unclear whether this affected an actual Conduit release version, but thanks to @kladki a fix is scheduled and we have included a workaround now on this end.
|
||||
|
||||
Vendored
-1
@@ -5,7 +5,6 @@ After=network-online.target
|
||||
Documentation=https://tuwunel.chat/
|
||||
|
||||
[Service]
|
||||
DynamicUser=yes
|
||||
User=tuwunel
|
||||
Group=tuwunel
|
||||
Type=notify
|
||||
|
||||
+11
-8
@@ -12,6 +12,7 @@ ARG CARGO_TARGET_DIR
|
||||
ARG cargo_target_profile
|
||||
ARG cargo_target_artifact
|
||||
ARG cargo_target_share
|
||||
ARG cargo_share
|
||||
ARG CARGO_TERM_VERBOSE=0
|
||||
ARG RUST_BACKTRACE
|
||||
ARG JEMALLOC_OVERRIDE
|
||||
@@ -33,6 +34,8 @@ ARG color_args="--color always"
|
||||
ARG recipe_args=""
|
||||
ARG cargo_args=""
|
||||
ARG git_checkout
|
||||
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
|
||||
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
@@ -43,6 +46,7 @@ WORKDIR /usr/lib/${sys_triple}
|
||||
COPY --link --from=rocksdb . .
|
||||
|
||||
WORKDIR /usr/src/tuwunel
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
ENV PATH="${CARGO_HOME}/bin:$PATH"
|
||||
ENV CARGO_TERM_VERBOSE="${CARGO_TERM_VERBOSE}"
|
||||
ENV RUST_BACKTRACE="${RUST_BACKTRACE}"
|
||||
@@ -58,17 +62,15 @@ ENV CARGO_PROFILE_RELEASE_DEBUGINFO_DEBUG="${CARGO_PROFILE_RELEASE_DEBUGINFO_DEB
|
||||
ENV CARGO_PROFILE_RELEASE_DEBUGINFO_LTO="${CARGO_PROFILE_RELEASE_DEBUGINFO_LTO}"
|
||||
ENV CARGO_BUILD_RUSTFLAGS="${CARGO_BUILD_RUSTFLAGS}"
|
||||
ENV CARGO_TARGET_DIR="${CARGO_TARGET_DIR}"
|
||||
ENV targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
|
||||
ENV targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
|
||||
RUN \
|
||||
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
|
||||
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
|
||||
@@ -77,7 +79,8 @@ RUN \
|
||||
<<EOF
|
||||
set -eux
|
||||
ulimit -n 65535
|
||||
rustup run ${rust_toolchain} \
|
||||
|
||||
rustup run "${rust_toolchain}" \
|
||||
cargo ${cargo_cmd} \
|
||||
--verbose \
|
||||
--locked \
|
||||
|
||||
@@ -10,9 +10,12 @@ ARG CARGO_TARGET_DIR
|
||||
ARG cargo_target_profile
|
||||
ARG cargo_target_artifact
|
||||
ARG cargo_target_share
|
||||
ARG cargo_share
|
||||
ARG cargo_profile
|
||||
ARG cargo_features
|
||||
ARG cargo_spec_features
|
||||
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
|
||||
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
|
||||
ARG pkg_dir
|
||||
ARG deb_args=""
|
||||
|
||||
@@ -20,17 +23,15 @@ WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
WORKDIR /usr/src/tuwunel
|
||||
ENV targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
|
||||
ENV targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
|
||||
RUN \
|
||||
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
|
||||
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# syntax = docker/dockerfile:1.11-labs
|
||||
|
||||
FROM input AS build-rpm
|
||||
ARG rust_target
|
||||
ARG rust_toolchain
|
||||
ARG RUSTUP_HOME
|
||||
ARG CARGO_HOME
|
||||
@@ -9,9 +10,12 @@ ARG CARGO_TARGET_DIR
|
||||
ARG cargo_target_profile
|
||||
ARG cargo_target_artifact
|
||||
ARG cargo_target_share
|
||||
ARG cargo_share
|
||||
ARG cargo_profile
|
||||
ARG cargo_features
|
||||
ARG cargo_spec_features
|
||||
ARG targ_dir="${CARGO_TARGET_DIR}/${cargo_target_profile}"
|
||||
ARG targ_targ_dir="${CARGO_TARGET_DIR}/${rust_target}/${cargo_target_profile}"
|
||||
ARG pkg_dir
|
||||
ARG gen_rpm_args=""
|
||||
|
||||
@@ -23,11 +27,11 @@ RUN \
|
||||
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
|
||||
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_target_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_target_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/deps,id=${cargo_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/build,id=${cargo_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/examples,id=${cargo_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/incremental,id=${cargo_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${cargo_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${cargo_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/build,id=${cargo_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${cargo_target_share}/examples,sharing=locked \
|
||||
|
||||
@@ -103,9 +103,6 @@ EOF
|
||||
|
||||
FROM input AS complement-testee
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
EXPOSE 8008 8448
|
||||
RUN mkdir /database
|
||||
COPY --from=complement-config * /complement/
|
||||
@@ -115,9 +112,6 @@ ENTRYPOINT tuwunel -Oserver_name=\""$SERVER_NAME\""
|
||||
|
||||
FROM input AS complement-testee-valgrind
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
EXPOSE 8008 8448
|
||||
RUN mkdir /database
|
||||
COPY --from=complement-config * /complement/
|
||||
@@ -134,12 +128,13 @@ ENTRYPOINT valgrind \
|
||||
FROM input AS complement-base
|
||||
ARG var_cache
|
||||
ARG var_lib_apt
|
||||
ARG complement_ref="426bc3458e5287398ec39ab7d2b8f2e3103dcf39"
|
||||
ARG complement_tags="conduwuit_blacklist"
|
||||
ARG complement_tests="./tests/..."
|
||||
ARG complement_run=".*"
|
||||
|
||||
WORKDIR /usr/src
|
||||
ADD https://github.com/matrix-construct/complement.git#403840348f6bcc9cc8ed1671dc2f638c2b1ce4ac complement
|
||||
ADD https://github.com/matrix-construct/complement.git#${complement_ref} complement
|
||||
|
||||
WORKDIR /usr/src/complement
|
||||
ENV COMPLEMENT_BASE_IMAGE="complement-testee"
|
||||
@@ -155,8 +150,9 @@ EOF
|
||||
FROM input AS complement-tester
|
||||
ARG complement_verbose=0
|
||||
ARG complement_debug=0
|
||||
ARG complement_dirty=0
|
||||
ARG complement_count=1
|
||||
ARG complement_parallel=16
|
||||
ARG complement_parallel=1
|
||||
ARG complement_shuffle=1337
|
||||
ARG complement_timeout="1h"
|
||||
ARG complement_run=".*"
|
||||
@@ -165,9 +161,6 @@ ARG complement_tags="conduwuit_blacklist"
|
||||
ARG complement_tests="./tests/..."
|
||||
ARG complement_base_image
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
WORKDIR /usr/src/complement
|
||||
ENV COMPLEMENT_DEBUG=$complement_debug
|
||||
ENV complement_parallel="$complement_parallel"
|
||||
@@ -179,6 +172,7 @@ ENV complement_tests="$complement_tests"
|
||||
ENV complement_skip="$complement_skip"
|
||||
ENV complement_run="$complement_run"
|
||||
ENV complement_tests="$complement_tests"
|
||||
ENV COMPLEMENT_ENABLE_DIRTY_RUNS="$complement_dirty"
|
||||
ENV COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS="$complement_verbose"
|
||||
ENV COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT="host.docker.internal"
|
||||
ENV COMPLEMENT_HOST_MOUNTS="/var/run/docker.sock:/var/run/docker.sock"
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
# syntax = docker/dockerfile:1.11-labs
|
||||
|
||||
FROM input AS matrix-rust-sdk-integration
|
||||
FROM input AS rust-sdk-integration
|
||||
ARG sys_name
|
||||
ARG sys_version
|
||||
ARG rust_toolchain
|
||||
ARG feat_set
|
||||
ARG rust_target
|
||||
ARG rust_toolchain
|
||||
ARG cargo_profile
|
||||
ARG RUSTUP_HOME
|
||||
ARG CARGO_HOME
|
||||
ARG CARGO_TARGET
|
||||
ARG MRSDK_TARGET_DIR="/usr/src/matrix-rust-sdk"
|
||||
ARG mrsdk_profile="release"
|
||||
ARG mrsdk_target_share="${MRSDK_TARGET_DIR}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${mrsdk_profile}/_shared_cache"
|
||||
ARG MRSDK_TARGET_DIR="/usr/src/matrix-rust-sdk/target"
|
||||
ARG mrsdk_target_share
|
||||
#ARG mrsdk_ref="integration"
|
||||
ARG mrsdk_ref="tuwunel-changes"
|
||||
ARG mrsdk_test_args=""
|
||||
ARG mrsdk_test_opts=""
|
||||
#TODO!!!
|
||||
ARG mrsdk_skip_list="--skip test_history_share_on_invite_pin_violation --skip test_room_notification_count"
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
ARG mrsdk_skip_list=""
|
||||
ARG mrsdk_parallel=2
|
||||
ARG mrsdk_startup_delay="10s"
|
||||
ARG mrsdk_testee="/usr/bin/tuwunel"
|
||||
|
||||
WORKDIR /usr/src
|
||||
ADD --link https://github.com/matrix-construct/matrix-rust-sdk.git matrix-rust-sdk
|
||||
ADD --link https://github.com/matrix-construct/matrix-rust-sdk.git#${mrsdk_ref} matrix-rust-sdk
|
||||
|
||||
WORKDIR /etc
|
||||
COPY <<EOF tuwunel.toml
|
||||
@@ -60,49 +62,39 @@ WORKDIR /usr/bin
|
||||
COPY --link --from=install /usr/bin/tuwunel .
|
||||
|
||||
WORKDIR /usr/src/matrix-rust-sdk
|
||||
ENV targ_dir="${MRSDK_TARGET_DIR}/${mrsdk_profile}"
|
||||
ENV targ_targ_dir="${MRSDK_TARGET_DIR}/${rust_target}/${mrsdk_profile}"
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
ENV RUST_BACKTRACE="full"
|
||||
ENV TUWUNEL_CONFIG="/etc/tuwunel.toml"
|
||||
ENV TUWUNEL_DATABASE_PATH="/var/db/tuwunel"
|
||||
ENV TUWUNEL_SERVER_NAME="localhost:6167"
|
||||
ENV TUWUNEL_PORT="[6167]"
|
||||
ENV HOMESERVER_URL="http://localhost:6167"
|
||||
ENV TUWUNEL_SERVER_NAME="localhost"
|
||||
ENV TUWUNEL_PORT="[8448]"
|
||||
ENV HOMESERVER_URL="http://localhost:8448"
|
||||
RUN \
|
||||
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/registry,sharing=shared \
|
||||
--mount=type=cache,dst=${CARGO_HOME}/git,sharing=shared \
|
||||
--mount=type=cache,dst=${targ_dir}/deps,id=${mrsdk_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/build,id=${mrsdk_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/examples,id=${mrsdk_target_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/incremental,id=${mrsdk_target_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_dir}/.fingerprint,id=${mrsdk_target_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/deps,id=${mrsdk_target_share}/deps,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/build,id=${mrsdk_target_share}/build,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/examples,id=${mrsdk_target_share}/examples,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/incremental,id=${mrsdk_target_share}/incremental,sharing=locked \
|
||||
--mount=type=cache,dst=${targ_targ_dir}/.fingerprint,id=${mrsdk_target_share}/fingerprint,sharing=locked \
|
||||
--mount=type=cache,dst=${MRSDK_TARGET_DIR},id=${mrsdk_target_share},sharing=locked \
|
||||
<<EOF
|
||||
set -eux
|
||||
|
||||
nohup /usr/bin/tuwunel 1> /var/log/tuwunel.log &
|
||||
PID=$!; trap "sleep 10s; set +ex; kill -QUIT ${PID}; wait ${PID}" EXIT
|
||||
sleep 3s
|
||||
nohup ${mrsdk_testee[@]} 1> /var/log/tuwunel.log &
|
||||
PID=$!; trap "sleep 10s; set +e; kill -QUIT ${PID}; wait ${PID}" EXIT
|
||||
sleep "${mrsdk_startup_delay}"
|
||||
|
||||
rustup run ${rust_toolchain} \
|
||||
cargo test \
|
||||
--locked \
|
||||
--release \
|
||||
--color always \
|
||||
--features default \
|
||||
--target "${rust_target}" \
|
||||
--target-dir "${MRSDK_TARGET_DIR}" \
|
||||
--package matrix-sdk-integration-testing \
|
||||
${mrsdk_test_args} \
|
||||
"--color=always" \
|
||||
"--features=default" \
|
||||
"--target=${rust_target}" \
|
||||
"--target-dir=${MRSDK_TARGET_DIR}" \
|
||||
"--package=matrix-sdk-integration-testing" \
|
||||
${mrsdk_test_args[@]} \
|
||||
-- \
|
||||
--test-threads 1 \
|
||||
--color always \
|
||||
${mrsdk_skip_list} \
|
||||
${mrsdk_test_opts} \
|
||||
"--color=always" \
|
||||
"--test-threads=${mrsdk_parallel}" \
|
||||
${mrsdk_skip_list[@]} \
|
||||
${mrsdk_test_opts[@]} \
|
||||
;
|
||||
EOF
|
||||
|
||||
+26
-24
@@ -1,12 +1,10 @@
|
||||
# syntax = docker/dockerfile:1.11-labs
|
||||
|
||||
FROM input AS nix-base
|
||||
ARG sys_name
|
||||
ARG sys_version
|
||||
ARG sys_target
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
RUN \
|
||||
--mount=type=cache,dst=/nix,sharing=shared \
|
||||
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
|
||||
@@ -20,9 +18,6 @@ EOF
|
||||
|
||||
|
||||
FROM nix-base AS build-nix
|
||||
ARG sys_name
|
||||
ARG sys_version
|
||||
ARG sys_target
|
||||
|
||||
WORKDIR /usr/src/tuwunel
|
||||
COPY --link --from=source /usr/src/tuwunel .
|
||||
@@ -44,15 +39,13 @@ RUN \
|
||||
EOF
|
||||
|
||||
|
||||
FROM build-nix AS smoke-nix
|
||||
ARG sys_name
|
||||
ARG sys_version
|
||||
ARG sys_target
|
||||
FROM input AS smoke-nix
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=build-nix . .
|
||||
COPY --link --from=nix-base . .
|
||||
|
||||
WORKDIR /opt/tuwunel
|
||||
WORKDIR /usr/src/tuwunel
|
||||
COPY --link --from=source /usr/src/tuwunel .
|
||||
ENV TUWUNEL_DATABASE_PATH="/tmp/tuwunel/smoketest.db"
|
||||
ENV TUWUNEL_LOG="info"
|
||||
RUN \
|
||||
@@ -61,29 +54,38 @@ RUN \
|
||||
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
|
||||
<<EOF
|
||||
set -eux
|
||||
alias nix="nix --extra-experimental-features nix-command --extra-experimental-features flakes"
|
||||
|
||||
bin/tuwunel \
|
||||
-Otest='["smoke", "fresh"]' \
|
||||
-Oserver_name=\"localhost\" \
|
||||
nix run \
|
||||
--verbose \
|
||||
--cores 0 \
|
||||
--max-jobs $(nproc) \
|
||||
--log-format raw \
|
||||
.#all-features \
|
||||
-- \
|
||||
-Otest='["smoke", "fresh"]' \
|
||||
-Oserver_name=\"localhost\" \
|
||||
EOF
|
||||
|
||||
|
||||
FROM build-nix AS nix-pkg
|
||||
ARG sys_name
|
||||
ARG sys_version
|
||||
ARG sys_target
|
||||
FROM input AS nix-pkg
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=build-nix . .
|
||||
COPY --link --from=nix-base . .
|
||||
|
||||
WORKDIR /usr/src/tuwunel
|
||||
COPY --link --from=source /usr/src/tuwunel .
|
||||
RUN \
|
||||
--mount=type=cache,dst=/nix,sharing=shared \
|
||||
--mount=type=cache,dst=/root/.cache/nix,sharing=shared \
|
||||
--mount=type=cache,dst=/root/.local/state/nix,sharing=shared \
|
||||
<<EOF
|
||||
set -eux
|
||||
#TODO: extract derivation?
|
||||
mkdir -p /opt/tuwunel
|
||||
touch /opt/tuwunel/tuwunel.drv
|
||||
set -eux
|
||||
alias nix="nix --extra-experimental-features nix-command --extra-experimental-features flakes"
|
||||
|
||||
ID=$(nix-store --realise $(nix path-info --derivation))
|
||||
|
||||
mkdir -p tuwunel
|
||||
nix-store --export $ID > tuwunel/tuwunel.drv
|
||||
tar -cvf /opt/tuwunel.nix.tar tuwunel
|
||||
EOF
|
||||
|
||||
@@ -6,9 +6,6 @@ ARG rust_target
|
||||
ARG rustup_version="1.28.2"
|
||||
ARG rustup_profile="minimal"
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
WORKDIR ${RUST_HOME}
|
||||
RUN <<EOF
|
||||
set -eux
|
||||
@@ -32,9 +29,6 @@ ARG CARGO_TERM_VERBOSE
|
||||
ARG rustup_components
|
||||
ARG cargo_installs
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
WORKDIR ${RUST_HOME}
|
||||
ENV CARGO_TARGET="${rust_target}"
|
||||
ENV RUSTUP_HOME="${RUSTUP_HOME}"
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
FROM input AS source
|
||||
ARG git_checkout
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
ADD --link --keep-git-dir . /usr/src/tuwunel
|
||||
WORKDIR /usr/src/tuwunel
|
||||
RUN <<EOF
|
||||
@@ -30,7 +27,6 @@ ARG JEMALLOC_OVERRIDE
|
||||
ARG ROCKSDB_LIB_DIR
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
COPY --link --from=rust ${RUST_HOME} ${RUST_HOME}
|
||||
COPY --link --from=source /usr/src/tuwunel /usr/src/tuwunel
|
||||
|
||||
@@ -67,9 +63,6 @@ ARG RUSTUP_HOME
|
||||
ARG CARGO_HOME
|
||||
ARG CARGO_TARGET
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
WORKDIR /usr/src/tuwunel
|
||||
RUN \
|
||||
--mount=type=cache,dst=${RUSTUP_HOME}/downloads,sharing=locked \
|
||||
|
||||
@@ -12,9 +12,6 @@ ARG var_lib_apt
|
||||
ARG packages
|
||||
ARG DEBIAN_FRONTEND
|
||||
|
||||
WORKDIR /
|
||||
COPY --link --from=input . .
|
||||
|
||||
ENV DEBIAN_FRONTEND="${DEBIAN_FRONTEND}"
|
||||
RUN \
|
||||
--mount=type=cache,dst=/var/cache,id=${var_cache},sharing=locked \
|
||||
|
||||
+74
-39
@@ -1,4 +1,4 @@
|
||||
variable "CI" {}
|
||||
|
||||
variable "GITHUB_ACTOR" {}
|
||||
variable "GITHUB_REPOSITORY" {}
|
||||
variable "GITHUB_REF" {}
|
||||
@@ -247,7 +247,7 @@ group "tests" {
|
||||
group "matrix-compliance" {
|
||||
targets = [
|
||||
"complement",
|
||||
"matrix-rust-sdk-integration",
|
||||
"rust-sdk-integ",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -440,61 +440,96 @@ target "complement-config" {
|
||||
# Integration tests
|
||||
#
|
||||
|
||||
group "integrations" {
|
||||
group "integration" {
|
||||
targets = [
|
||||
"matrix-rust-sdk-integration",
|
||||
"integration",
|
||||
"integ",
|
||||
"rust-sdk-integ",
|
||||
]
|
||||
}
|
||||
|
||||
target "matrix-rust-sdk-integration" {
|
||||
name = elem("matrix-rust-sdk-integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
variable "valgrind_flags" {
|
||||
default = "--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no"
|
||||
}
|
||||
|
||||
target "rust-sdk-valgrind" {
|
||||
name = elem("rust-sdk-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
tags = [
|
||||
elem_tag("matrix-rust-sdk-integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
elem_tag("rust-sdk-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
]
|
||||
output = ["type=docker,compression=zstd,mode=max,compression-level=${zstd_image_compress_level}"]
|
||||
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
|
||||
target = "matrix-rust-sdk-integration"
|
||||
dockerfile = "${docker_dir}/Dockerfile.matrix-rust-sdk"
|
||||
matrix = cargo_rust_feat_sys
|
||||
inherits = [
|
||||
elem("rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
|
||||
elem("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
]
|
||||
contexts = {
|
||||
input = elem("target:rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
|
||||
install = elem("target:install", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
}
|
||||
args = {
|
||||
mrsdk_test_args=""
|
||||
mrsdk_test_opts=""
|
||||
VALGRINDFLAGS = "${valgrind_flags}"
|
||||
mrsdk_testee = "valgrind ${valgrind_flags} /usr/bin/tuwunel"
|
||||
mrsdk_test_args = ""
|
||||
mrsdk_startup_delay = "30s"
|
||||
mrsdk_skip_list =<<EOF
|
||||
--skip test_delayed_invite_response_and_sent_message_decryption
|
||||
--skip test_history_share_on_invite_pin_violation
|
||||
EOF
|
||||
}
|
||||
}
|
||||
|
||||
target "integration-valgrind" {
|
||||
name = elem("integration-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
target "rust-sdk-integ" {
|
||||
name = elem("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
tags = [
|
||||
elem_tag("integration-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
elem_tag("rust-sdk-integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
]
|
||||
output = ["type=docker,compression=zstd,mode=max,compression-level=${zstd_image_compress_level}"]
|
||||
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
|
||||
target = "rust-sdk-integration"
|
||||
dockerfile = "${docker_dir}/Dockerfile.matrix-rust-sdk"
|
||||
matrix = cargo_rust_feat_sys
|
||||
inherits = [
|
||||
elem("rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target]),
|
||||
elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
|
||||
]
|
||||
contexts = {
|
||||
input = elem("target:rust", [rust_toolchain, rust_target, sys_name, sys_version, sys_target])
|
||||
install = elem("target:install", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
}
|
||||
args = {
|
||||
mrsdk_target_share = "/usr/src/matrix-rust-sdk/target/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/_shared_cache"
|
||||
|
||||
mrsdk_testee = "/usr/bin/tuwunel"
|
||||
mrsdk_test_args = "--no-fail-fast"
|
||||
|
||||
mrsdk_skip_list =<<EOF
|
||||
--skip test_delayed_invite_response_and_sent_message_decryption
|
||||
EOF
|
||||
}
|
||||
}
|
||||
|
||||
target "integ-valgrind" {
|
||||
name = elem("integ-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
tags = [
|
||||
elem_tag("integ-valgrind", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
]
|
||||
matrix = cargo_rust_feat_sys
|
||||
inherits = [
|
||||
elem("integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
|
||||
elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
|
||||
]
|
||||
contexts = {
|
||||
input = elem("target:integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
input = elem("target:build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
}
|
||||
args = {
|
||||
VALGRINDFLAGS = "--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no"
|
||||
VALGRINDFLAGS = "${valgrind_flags}"
|
||||
cargo_cmd = "valgrind test"
|
||||
cargo_args = "--test=*"
|
||||
}
|
||||
}
|
||||
|
||||
target "integration" {
|
||||
name = elem("integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
target "integ" {
|
||||
name = elem("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
tags = [
|
||||
elem_tag("integration", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
elem_tag("integ", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
]
|
||||
target = "cargo"
|
||||
matrix = cargo_rust_feat_sys
|
||||
inherits = [
|
||||
elem("build-tests", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
|
||||
@@ -504,7 +539,6 @@ target "integration" {
|
||||
}
|
||||
args = {
|
||||
TUWUNEL_DATABASE_PATH = "/tmp/integration.test.db"
|
||||
|
||||
cargo_cmd = (cargo_profile == "bench"? "bench": "test")
|
||||
cargo_args = (cargo_profile == "bench"?
|
||||
"--no-fail-fast --bench=*": "--no-fail-fast --test=*"
|
||||
@@ -527,16 +561,16 @@ group "smoke" {
|
||||
}
|
||||
|
||||
target "smoke-nix" {
|
||||
name = elem("smoke-nix", [sys_name, sys_version, sys_target])
|
||||
name = elem("smoke-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
tags = [
|
||||
elem_tag("smoke-nix", [sys_name, sys_version, sys_target], "latest"),
|
||||
elem_tag("smoke-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
]
|
||||
output = ["type=cacheonly,compression=zstd,mode=min,compression-level=${cache_compress_level}"]
|
||||
dockerfile = "${docker_dir}/Dockerfile.nix"
|
||||
target = "smoke-nix"
|
||||
matrix = sys
|
||||
matrix = cargo_rust_feat_sys
|
||||
inherits = [
|
||||
elem("build-nix", [sys_name, sys_version, sys_target]),
|
||||
elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -630,7 +664,7 @@ target "unit-valgrind" {
|
||||
input = elem("target:unit", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
}
|
||||
args = {
|
||||
VALGRINDFLAGS = "--error-exitcode=1 --exit-on-first-error=yes --undef-value-errors=no --leak-check=no"
|
||||
VALGRINDFLAGS = "${valgrind_flags}"
|
||||
cargo_cmd = "valgrind test"
|
||||
cargo_args = "--lib --bins"
|
||||
}
|
||||
@@ -961,20 +995,20 @@ target "nix" {
|
||||
target = "nix-pkg"
|
||||
matrix = cargo_rust_feat_sys
|
||||
inherits = [
|
||||
elem("build-nix", [sys_name, sys_version, sys_target]),
|
||||
elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target]),
|
||||
]
|
||||
}
|
||||
|
||||
target "build-nix" {
|
||||
name = elem("build-nix", [sys_name, sys_version, sys_target])
|
||||
name = elem("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target])
|
||||
tags = [
|
||||
elem_tag("build-nix", [sys_name, sys_version, sys_target], "latest"),
|
||||
elem_tag("build-nix", [cargo_profile, rust_toolchain, rust_target, feat_set, sys_name, sys_version, sys_target], "latest"),
|
||||
]
|
||||
output = ["type=cacheonly,compression=zstd,mode=min,compression-level=${cache_compress_level}"]
|
||||
cache_to = ["type=local,compression=zstd,mode=max,compression-level=${cache_compress_level}"]
|
||||
dockerfile = "${docker_dir}/Dockerfile.nix"
|
||||
target = "build-nix"
|
||||
matrix = sys
|
||||
matrix = cargo_rust_feat_sys
|
||||
inherits = [
|
||||
elem("builder", [sys_name, sys_version, sys_target]),
|
||||
elem("source", [sys_name, sys_version, sys_target]),
|
||||
@@ -1006,7 +1040,6 @@ target "book" {
|
||||
}
|
||||
dockerfile-inline =<<EOF
|
||||
FROM input AS book
|
||||
COPY --link --from=input . .
|
||||
RUN ["mdbook", "build", "-d", "/book", "/usr/src/tuwunel"]
|
||||
EOF
|
||||
}
|
||||
@@ -1352,6 +1385,8 @@ target "deps-base" {
|
||||
# cache key for unique artifact area
|
||||
cargo_target_artifact = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${cargo_profile}/${feat_set}/${git_ref_sha}"
|
||||
# cache key for hashed subdirs
|
||||
cargo_share = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_toolchain}/${cargo_profile}/_shared_cache"
|
||||
# cache key for hashed subdirs
|
||||
cargo_target_share = "${cargo_tgt_dir_base}/${sys_name}/${sys_version}/${rust_target}/${rust_toolchain}/${cargo_profile}/_shared_cache"
|
||||
# cased name of profile subdir within target complex
|
||||
cargo_target_profile = (
|
||||
@@ -1390,7 +1425,7 @@ target "deps-base" {
|
||||
"-C link-arg=-l:libgcc.a": "",
|
||||
]):
|
||||
|
||||
(cargo_profile == "release" || cargo_profile == "bench") && rust_toolchain == "nightly"?
|
||||
(cargo_profile == "release" || cargo_profile == "bench") && substr(rust_toolchain, 0, 7) == "nightly"?
|
||||
join(" ", [
|
||||
join(" ", rustflags),
|
||||
join(" ", nightly_rustflags),
|
||||
@@ -1432,7 +1467,7 @@ target "deps-base" {
|
||||
"-C link-arg=-l:libgcc.a": "",
|
||||
]):
|
||||
|
||||
rust_toolchain == "stable"?
|
||||
substr(rust_toolchain, 0, 6) == "stable"?
|
||||
join(" ", [
|
||||
join(" ", rustflags),
|
||||
join(" ", static_rustflags),
|
||||
@@ -1452,7 +1487,7 @@ target "deps-base" {
|
||||
"-C link-arg=-l:libgcc.a": "",
|
||||
]):
|
||||
|
||||
rust_toolchain == "nightly"?
|
||||
substr(rust_toolchain, 0, 7) == "nightly"?
|
||||
join(" ", [
|
||||
join(" ", rustflags),
|
||||
join(" ", nightly_rustflags),
|
||||
|
||||
@@ -16,6 +16,7 @@ default_sys_target="x86_64-v1-linux-gnu"
|
||||
default_sys_version="testing-slim"
|
||||
|
||||
default_complement_verbose=0
|
||||
default_complement_dirty=0
|
||||
default_complement_count=1
|
||||
default_complement_parallel=1
|
||||
default_complement_shuffle=0
|
||||
@@ -31,9 +32,7 @@ skip="${skip}|TestRoomCreate/Parallel/POST_/createRoom_makes_a_room_with_a_topic
|
||||
skip="${skip}|TestLogin/parallel/POST_/"
|
||||
skip="${skip}|TestUnbanViaInvite"
|
||||
skip="${skip}|TestRoomState/Parallel/GET_/publicRooms_lists_newly-created_room"
|
||||
# flakes due to timeout in debug-mode
|
||||
skip="${skip}|TestMSC4297StateResolutionV2_1_starts_from_empty_set"
|
||||
skip="${skip}|TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph"
|
||||
skip="${skip}|TestThreadReceiptsInSyncMSC4102"
|
||||
|
||||
set -a
|
||||
cargo_profile="${cargo_profile:-$default_cargo_profile}"
|
||||
@@ -53,6 +52,7 @@ set +a
|
||||
envs=""
|
||||
envs="$envs -e complement_verbose=${complement_verbose:-$default_complement_verbose}"
|
||||
envs="$envs -e complement_count=${complement_count:-$default_complement_count}"
|
||||
envs="$envs -e complement_dirty=${complement_dirty:-$default_complement_dirty}"
|
||||
envs="$envs -e complement_parallel=${complement_parallel:-$default_complement_parallel}"
|
||||
envs="$envs -e complement_shuffle=${complement_shuffle:-$default_complement_shuffle}"
|
||||
envs="$envs -e complement_timeout=${complement_timeout:-$default_complement_timeout}"
|
||||
|
||||
@@ -5,6 +5,8 @@ # Summary
|
||||
- [Examples](configuration/examples.md)
|
||||
- [Deploying](deploying.md)
|
||||
- [Generic](deploying/generic.md)
|
||||
- [Reverse Proxy - Caddy](deploying/reverse-proxy-caddy.md)
|
||||
- [Reverse Proxy - Nginx](deploying/reverse-proxy-nginx.md)
|
||||
- [NixOS](deploying/nixos.md)
|
||||
- [Docker](deploying/docker.md)
|
||||
- [Kubernetes](deploying/kubernetes.md)
|
||||
|
||||
+23
-44
@@ -141,46 +141,31 @@ ## Setting the correct file permissions
|
||||
|
||||
## Setting up the Reverse Proxy
|
||||
|
||||
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc transparently with proper defaults.
|
||||
For other software, please refer to their respective documentation or online guides.
|
||||
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults. However, Nginx is also well-supported and widely used.
|
||||
|
||||
### Caddy
|
||||
**Choose your reverse proxy:**
|
||||
|
||||
After installing Caddy via your preferred method, create `/etc/caddy/conf.d/tuwunel_caddyfile`
|
||||
and enter this (substitute for your server name).
|
||||
- **[Caddy Setup Guide](reverse-proxy-caddy.md)** - Recommended for ease of use and automatic TLS
|
||||
- **[Nginx Setup Guide](reverse-proxy-nginx.md)** - Popular choice with extensive documentation
|
||||
|
||||
```caddyfile
|
||||
your.server.name, your.server.name:8448 {
|
||||
# TCP reverse_proxy
|
||||
reverse_proxy localhost:8008
|
||||
# UNIX socket
|
||||
#reverse_proxy unix//run/tuwunel/tuwunel.sock
|
||||
}
|
||||
```
|
||||
### Quick Overview
|
||||
|
||||
That's it! Just start and enable the service and you're set.
|
||||
Regardless of which reverse proxy you choose, you will need to:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now caddy
|
||||
```
|
||||
1. **Reverse proxy the following routes:**
|
||||
- `/_matrix/` - core Matrix C-S and S-S APIs
|
||||
- `/_tuwunel/` - ad-hoc Tuwunel routes such as `/local_user_count` and `/server_version`
|
||||
|
||||
### Other Reverse Proxies
|
||||
2. **Optionally reverse proxy (recommended):**
|
||||
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using Tuwunel to perform delegation (see the `[global.well_known]` config section)
|
||||
- `/.well-known/matrix/support` if using Tuwunel to send the homeserver admin contact and support page (formerly known as MSC1929)
|
||||
- `/` if you would like to see `hewwo from tuwunel woof!` at the root
|
||||
|
||||
As we would prefer our users to use Caddy, we will not provide configuration files for other proxies.
|
||||
3. **Handle ports:**
|
||||
- Port 443 (HTTPS) for client-server API
|
||||
- Port 8448 for federation (if federating with other homeservers)
|
||||
|
||||
You will need to reverse proxy everything under following routes:
|
||||
- `/_matrix/` - core Matrix C-S and S-S APIs
|
||||
- `/_tuwunel/` - ad-hoc Tuwunel routes such as `/local_user_count` and
|
||||
`/server_version`
|
||||
|
||||
You can optionally reverse proxy the following individual routes:
|
||||
- `/.well-known/matrix/client` and `/.well-known/matrix/server` if using
|
||||
Tuwunel to perform delegation (see the `[global.well_known]` config section)
|
||||
- `/.well-known/matrix/support` if using Tuwunel to send the homeserver admin
|
||||
contact and support page (formerly known as MSC1929)
|
||||
- `/` if you would like to see `hewwo from tuwunel woof!` at the root
|
||||
|
||||
See the following spec pages for more details on these files:
|
||||
See the following spec pages for more details on well-known files:
|
||||
- [`/.well-known/matrix/server`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixserver)
|
||||
- [`/.well-known/matrix/client`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient)
|
||||
- [`/.well-known/matrix/support`](https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixsupport)
|
||||
@@ -189,21 +174,15 @@ ### Other Reverse Proxies
|
||||
- <https://matrix.org/.well-known/matrix/server>
|
||||
- <https://matrix.org/.well-known/matrix/client>
|
||||
|
||||
For Apache and Nginx there are many examples available online.
|
||||
### Other Reverse Proxies
|
||||
|
||||
Lighttpd is not supported as it seems to mess with the `X-Matrix` Authorization
|
||||
header, making federation non-functional. If a workaround is found, feel free to share to get it added to the documentation here.
|
||||
_Specific contributions for other proxies are welcome!_
|
||||
|
||||
If using Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from messing with the `X-Matrix` header (note that Apache isn't very good as a general reverse proxy and we discourage the usage of it if you can).
|
||||
**Not Recommended:**
|
||||
- **Apache**: While possible, Apache requires special configuration (`nocanon` in `ProxyPass`) to prevent corruption of the `X-Matrix` header.
|
||||
- **Lighttpd**: Its proxy module alters the `X-Matrix` authorization header, breaking federation functionality.
|
||||
|
||||
If using Nginx, you need to give Tuwunel the request URI using `$request_uri`, or like so:
|
||||
- `proxy_pass http://127.0.0.1:6167$request_uri;`
|
||||
- `proxy_pass http://127.0.0.1:6167;`
|
||||
|
||||
Nginx users need to increase `client_max_body_size` (default is 1M) to match
|
||||
`max_request_size` defined in tuwunel.toml.
|
||||
|
||||
## You're done
|
||||
## You are done
|
||||
|
||||
Now you can start Tuwunel with:
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Reverse Proxy Setup - Caddy
|
||||
|
||||
[<= Back to Generic Deployment Guide](generic.md#setting-up-the-reverse-proxy)
|
||||
|
||||
We recommend Caddy as a reverse proxy, as it is trivial to use, handling TLS certificates, reverse proxy headers, etc. transparently with proper defaults.
|
||||
|
||||
## Installation
|
||||
|
||||
Install Caddy via your preferred method. Refer to the [official Caddy installation guide](https://caddyserver.com/docs/install) for your distribution.
|
||||
|
||||
## Configuration
|
||||
|
||||
After installing Caddy, create `/etc/caddy/conf.d/tuwunel_caddyfile` and enter this (substitute `your.server.name` with your actual server name):
|
||||
|
||||
```caddyfile
|
||||
your.server.name, your.server.name:8448 {
|
||||
# TCP reverse_proxy
|
||||
reverse_proxy localhost:8008
|
||||
# UNIX socket (alternative - comment out the line above and uncomment this)
|
||||
#reverse_proxy unix//run/tuwunel/tuwunel.sock
|
||||
}
|
||||
```
|
||||
|
||||
### What this does
|
||||
|
||||
- Handles both port 443 (HTTPS) and port 8448 (Matrix federation) automatically
|
||||
- Automatically provisions and renews TLS certificates via Let's Encrypt
|
||||
- Sets all necessary reverse proxy headers correctly
|
||||
- Routes all traffic to Tuwunel listening on `localhost:8008`
|
||||
|
||||
That's it! Just start and enable the service and you're set.
|
||||
|
||||
```bash
|
||||
sudo systemctl enable --now caddy
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After starting Caddy, verify it's working by checking:
|
||||
|
||||
```bash
|
||||
curl https://your.server.name/_tuwunel/server_version
|
||||
curl https://your.server.name:8448/_tuwunel/server_version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
[=> Continue with "You're Done"](generic.md#you-are-done)
|
||||
@@ -0,0 +1,164 @@
|
||||
# Reverse Proxy Setup - Nginx
|
||||
|
||||
[<= Back to Generic Deployment Guide](generic.md#setting-up-the-reverse-proxy)
|
||||
|
||||
This guide shows you how to configure Nginx as a reverse proxy for Tuwunel with TLS support.
|
||||
|
||||
## Installation
|
||||
|
||||
Install Nginx via your preferred method. Most distributions include Nginx in their package repositories:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install nginx
|
||||
|
||||
# Red Hat/Fedora
|
||||
sudo dnf install nginx
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S nginx
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a new configuration file at `/etc/nginx/sites-available/tuwunel` (or `/etc/nginx/conf.d/tuwunel.conf` on some distributions):
|
||||
|
||||
```nginx
|
||||
# Client-Server API over HTTPS (port 443)
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name matrix.example.com;
|
||||
|
||||
# Nginx standard body size is 1MB, which is quite small for media uploads
|
||||
# Increase this to match the max_request_size in your tuwunel.toml
|
||||
client_max_body_size 100M;
|
||||
|
||||
# Forward requests to Tuwunel (listening on 127.0.0.1:8008)
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8008;
|
||||
|
||||
# Preserve host and scheme - critical for proper Matrix operation
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
|
||||
# TLS configuration (Let's Encrypt example using certbot)
|
||||
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
|
||||
}
|
||||
|
||||
# Matrix Federation over HTTPS (port 8448)
|
||||
# Only needed if you want to federate with other homeservers
|
||||
# Don't forget to open port 8448 in your firewall!
|
||||
server {
|
||||
listen 8448 ssl http2;
|
||||
listen [::]:8448 ssl http2;
|
||||
server_name matrix.example.com;
|
||||
|
||||
# Same body size increase for larger files
|
||||
client_max_body_size 100M;
|
||||
|
||||
# Forward to the same local port as client-server API
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8008;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
|
||||
# TLS configuration (same certificates as above)
|
||||
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
|
||||
}
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Replace `matrix.example.com`** with your actual server name
|
||||
- **`client_max_body_size`**: Must match or exceed `max_request_size` in your `tuwunel.toml`
|
||||
- **Do NOT use `$request_uri`** in `proxy_pass` - while some guides suggest this, it's not necessary for Tuwunel and can cause issues
|
||||
- **IPv6**: The `listen [::]:443` and `listen [::]:8448` lines enable IPv6 support. Remove them if you don't need IPv6
|
||||
|
||||
### TLS Certificates
|
||||
|
||||
The example above uses Let's Encrypt certificates via certbot. To obtain certificates:
|
||||
|
||||
```bash
|
||||
sudo certbot certonly --nginx -d matrix.example.com
|
||||
```
|
||||
|
||||
Certbot will automatically handle renewal. Make sure to reload Nginx after certificate renewal:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Optional: Timeout Configuration
|
||||
|
||||
The default Nginx timeouts are usually sufficient for Matrix operations. Element's long-polling `/sync` requests typically run for 30 seconds, which is within Nginx's default timeouts.
|
||||
|
||||
However, if you experience federation retries or dropped long-poll connections, you can extend the timeouts by adding these lines inside your `location /` blocks:
|
||||
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8008;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# Optional: Extend timeouts if experiencing issues
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
```
|
||||
|
||||
## Enable the Configuration
|
||||
|
||||
If using sites-available/sites-enabled structure:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/tuwunel /etc/nginx/sites-enabled/
|
||||
```
|
||||
|
||||
Test the configuration:
|
||||
|
||||
```bash
|
||||
sudo nginx -t
|
||||
```
|
||||
|
||||
If the test passes, reload Nginx:
|
||||
|
||||
```bash
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Enable Nginx to start on boot:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable nginx
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After configuring Nginx, verify it's working by checking:
|
||||
|
||||
```bash
|
||||
curl https://matrix.example.com/_tuwunel/server_version
|
||||
curl https://matrix.example.com:8448/_tuwunel/server_version
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Apache Compatibility Note
|
||||
|
||||
If you're considering Apache instead of Nginx: Apache is not well-suited as a reverse proxy for Matrix homeservers. If you must use Apache, you need to use `nocanon` in your `ProxyPass` directive to prevent httpd from corrupting the `X-Matrix` authorization header, which will break federation.
|
||||
|
||||
### Lighttpd is Not Supported
|
||||
|
||||
Lighttpd has known issues with the `X-Matrix` authorization header, making federation non-functional. We do not recommend using Lighttpd with Tuwunel.
|
||||
|
||||
---
|
||||
|
||||
[=> Continue with "You're Done"](generic.md#you-are-done)
|
||||
Generated
+43
-39
@@ -32,11 +32,11 @@
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1756385612,
|
||||
"narHash": "sha256-+NU5MMhuPHHRyvZZWNFG7zt+leRSPsJu1MwhOUzkPUk=",
|
||||
"lastModified": 1761150191,
|
||||
"narHash": "sha256-fPQmV+K836jR/Tu6slfMdm9HL3p0IEPB5x5I7Zc6v5E=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "dc24688cd67518c3711d511fa369c0f5a131063a",
|
||||
"rev": "5629a7d84c91df9c06650b18eb428cce4166c4fc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -68,11 +68,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748883665,
|
||||
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
|
||||
"lastModified": 1752264895,
|
||||
"narHash": "sha256-1zBPE/PNAkPNUsOWFET4J0cjlvziH8DOekesDmjND+w=",
|
||||
"owner": "cachix",
|
||||
"repo": "cachix",
|
||||
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
|
||||
"rev": "47053aef762f452e816e44eb9a23fbc3827b241a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -116,11 +116,11 @@
|
||||
},
|
||||
"crane_2": {
|
||||
"locked": {
|
||||
"lastModified": 1758758545,
|
||||
"narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=",
|
||||
"lastModified": 1760924934,
|
||||
"narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364",
|
||||
"rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -137,6 +137,7 @@
|
||||
"cachix",
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-parts": "flake-parts_2",
|
||||
"git-hooks": [
|
||||
"cachix",
|
||||
"git-hooks"
|
||||
@@ -148,11 +149,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1754404745,
|
||||
"narHash": "sha256-BdbW/iTImczgcuATgQIa9sPGuYIBxVq2xqcvICsa2AQ=",
|
||||
"lastModified": 1760560333,
|
||||
"narHash": "sha256-goJQdVl9oDgCxF9CggPUw1DvB4gsot1jzMmz9px8Du8=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "6563b21105168f90394dfaf58284b078af2d7275",
|
||||
"rev": "0a4043938f540027e562c5a0feebbe6be872c3ea",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -169,11 +170,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1759251341,
|
||||
"narHash": "sha256-0vt4IQnTLyAhaeom3h9GOCpO2+av+wK4zP7O8BSKr6I=",
|
||||
"lastModified": 1761547629,
|
||||
"narHash": "sha256-4OH1CVm2PdjKRqEJ3RLfkQMDSBdn7VId6iyYCwKOK+U=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "af8a7505a1c62fab493157ea380a05c094bb63af",
|
||||
"rev": "d82a7c64ea441e397914577c9a18f2867e5b364b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -258,16 +259,15 @@
|
||||
"nixpkgs-lib": [
|
||||
"cachix",
|
||||
"devenv",
|
||||
"nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1733312601,
|
||||
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
|
||||
"lastModified": 1756770412,
|
||||
"narHash": "sha256-+uWLQZccFHwqpGqr2Yt5VsW/PbeJVTn9Dk6SHWhNRPw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
|
||||
"rev": "4524271976b625a4a605beefd893f270620fd751",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -308,11 +308,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1750779888,
|
||||
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
|
||||
"lastModified": 1760392170,
|
||||
"narHash": "sha256-WftxJgr2MeDDFK47fQKywzC72L2jRc/PWcyGdjaDzkw=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
|
||||
"rev": "46d55f0aeb1d567a78223e69729734f3dca25a85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -346,11 +346,11 @@
|
||||
"liburing": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1759268303,
|
||||
"narHash": "sha256-nT3JWey0P2lPWlnwN57XHmuzkeW8Mo795+Ahf8S5TMI=",
|
||||
"lastModified": 1761487364,
|
||||
"narHash": "sha256-PmRdt9dQUFGcVrX7XCh7z2FpRm1SqKgiYbIwpkJPjRU=",
|
||||
"owner": "axboe",
|
||||
"repo": "liburing",
|
||||
"rev": "1671cb952a8f680214b04ae9546d4f649f1b367b",
|
||||
"rev": "65ec4223032a38870dde2bce646ba4de13d2feff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -367,7 +367,11 @@
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-parts": "flake-parts_2",
|
||||
"flake-parts": [
|
||||
"cachix",
|
||||
"devenv",
|
||||
"flake-parts"
|
||||
],
|
||||
"git-hooks-nix": [
|
||||
"cachix",
|
||||
"devenv",
|
||||
@@ -388,16 +392,16 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1752773918,
|
||||
"narHash": "sha256-dOi/M6yNeuJlj88exI+7k154z+hAhFcuB8tZktiW7rg=",
|
||||
"lastModified": 1758763079,
|
||||
"narHash": "sha256-Bx1A+lShhOWwMuy3uDzZQvYiBKBFcKwy6G6NEohhv6A=",
|
||||
"owner": "cachix",
|
||||
"repo": "nix",
|
||||
"rev": "031c3cf42d2e9391eee373507d8c12e0f9606779",
|
||||
"rev": "6f0140527c2b0346df4afad7497baa08decb929f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "devenv-2.30",
|
||||
"ref": "devenv-2.30.5",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -473,11 +477,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1754214453,
|
||||
"narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=",
|
||||
"lastModified": 1760524057,
|
||||
"narHash": "sha256-EVAqOteLBFmd7pKkb0+FIUyzTF61VKi7YmvP1tw4nEw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376",
|
||||
"rev": "544961dfcce86422ba200ed9a0b00dd4b1486ec5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -489,11 +493,11 @@
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1759070547,
|
||||
"narHash": "sha256-JVZl8NaVRYb0+381nl7LvPE+A774/dRpif01FKLrYFQ=",
|
||||
"lastModified": 1761349956,
|
||||
"narHash": "sha256-tH3wHnOJms+U4k/rK2Nn1RfBrhffX92jLP/2VndSn0w=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "647e5c14cbd5067f44ac86b74f014962df460840",
|
||||
"rev": "02f2cb8e0feb4596d20cc52fda73ccee960e3538",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -538,11 +542,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1759134797,
|
||||
"narHash": "sha256-YPi+jL3tx/yC5J5l7/OB7Lnlr9BMTzYnZtm7tRJzUNg=",
|
||||
"lastModified": 1761500479,
|
||||
"narHash": "sha256-syeBTCCU96qPJHcVpwHeCwmPCiLTDHHgYQYhpZ0iwLo=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "062ac7a5451e8e92a32e22a60d86882d6a034f3f",
|
||||
"rev": "049767e6faa84b2d1a951d8f227e6ebd99d728a2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
file = ./rust-toolchain.toml;
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
sha256 = "sha256-Qxt8XAuaUR2OMdKbN4u8dBJOhSHxS+uS06Wl9+flVEk=";
|
||||
sha256 = "sha256-+9FmLhAOezBZCOziO0Qct1NOrfpjNsXxc/8I0c7BdKE=";
|
||||
};
|
||||
|
||||
mkScope = pkgs: pkgs.lib.makeScope pkgs.newScope (self: {
|
||||
|
||||
@@ -202,8 +202,25 @@ craneLib.buildPackage ( commonAttrs // {
|
||||
env = buildDepsOnlyEnv;
|
||||
});
|
||||
|
||||
# Disabled due to integration test failing to find /etc/resolv.conf
|
||||
doCheck = false;
|
||||
nativeCheckInputs = [
|
||||
pkgsBuildHost.libredirect.hook
|
||||
];
|
||||
|
||||
preCheck =
|
||||
let
|
||||
fakeResolvConf = pkgsBuildHost.writeTextFile {
|
||||
name = "resolv.conf";
|
||||
text = ''
|
||||
nameserver 0.0.0.0
|
||||
'';
|
||||
};
|
||||
in
|
||||
''
|
||||
export NIX_REDIRECTS="/etc/resolv.conf=${fakeResolvConf}"
|
||||
export TUWUNEL_DATABASE_PATH="$(mktemp -d)/smoketest.db"
|
||||
'';
|
||||
doCheck = true;
|
||||
|
||||
doBenchmark = false;
|
||||
|
||||
cargoExtraArgs = "--no-default-features --locked "
|
||||
|
||||
@@ -5,7 +5,6 @@ After=network-online.target
|
||||
Documentation=https://tuwunel.chat/
|
||||
|
||||
[Service]
|
||||
DynamicUser=yes
|
||||
User=tuwunel
|
||||
Group=tuwunel
|
||||
Type=notify
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
# If you're having trouble making the relevant changes, bug a maintainer.
|
||||
|
||||
[toolchain]
|
||||
channel = "1.88.0"
|
||||
channel = "1.89.0"
|
||||
profile = "minimal"
|
||||
components = [
|
||||
# For rust-analyzer
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedRoomId, OwnedUserId};
|
||||
use tuwunel_core::Result;
|
||||
use tuwunel_database::Deserialized;
|
||||
|
||||
use crate::{admin_command, admin_command_dispatch};
|
||||
|
||||
@@ -46,7 +47,7 @@ async fn changes_since(
|
||||
.await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
|
||||
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:?}\n```"))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -58,11 +59,12 @@ async fn account_data_get(
|
||||
room_id: Option<OwnedRoomId>,
|
||||
) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = self
|
||||
let results: serde_json::Value = self
|
||||
.services
|
||||
.account_data
|
||||
.get_raw(room_id.as_deref(), &user_id, &kind)
|
||||
.await;
|
||||
.await
|
||||
.deserialized()?;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```"))
|
||||
|
||||
+42
-10
@@ -37,15 +37,6 @@ pub(crate) enum RawCommand {
|
||||
base64: bool,
|
||||
},
|
||||
|
||||
/// - Raw database delete (for string keys)
|
||||
Del {
|
||||
/// Map name
|
||||
map: String,
|
||||
|
||||
/// Key
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// - Raw database keys iteration
|
||||
Keys {
|
||||
/// Map name
|
||||
@@ -135,7 +126,26 @@ pub(crate) enum RawCommand {
|
||||
prefix: Option<String>,
|
||||
},
|
||||
|
||||
/// - Compact database
|
||||
/// - Raw database delete (for string keys) DANGER!!!
|
||||
Del {
|
||||
/// Map name
|
||||
map: String,
|
||||
|
||||
/// Key
|
||||
key: String,
|
||||
},
|
||||
|
||||
/// - Clear database table DANGER!!!
|
||||
Clear {
|
||||
/// Map name
|
||||
map: String,
|
||||
|
||||
/// Confirm
|
||||
#[arg(long)]
|
||||
confirm: bool,
|
||||
},
|
||||
|
||||
/// - Compact database DANGER!!!
|
||||
Compact {
|
||||
#[arg(short, long, alias("column"))]
|
||||
map: Option<Vec<String>>,
|
||||
@@ -425,6 +435,28 @@ pub(super) async fn raw_del(&self, map: String, key: String) -> Result {
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn raw_clear(&self, map: String, confirm: bool) -> Result {
|
||||
let map = self.services.db.get(&map)?;
|
||||
|
||||
if !confirm {
|
||||
return Err!("Are you really sure you want to clear all data? Add the --confirm option.");
|
||||
}
|
||||
|
||||
let timer = Instant::now();
|
||||
let cork = self.services.db.cork();
|
||||
map.raw_keys()
|
||||
.ignore_err()
|
||||
.ready_for_each(|key| map.remove(&key))
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
drop(cork);
|
||||
let query_time = timer.elapsed();
|
||||
self.write_str(&format!("Operation completed in {query_time:?}"))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn raw_get(&self, map: String, key: String, base64: bool) -> Result {
|
||||
let map = self.services.db.get(&map)?;
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
#[derive(Debug, Subcommand)]
|
||||
/// All the getters and iterators from src/database/key_value/rooms/alias.rs
|
||||
pub(crate) enum RoomAliasCommand {
|
||||
/// - Resolve any local or remote alias.
|
||||
ResolveAlias {
|
||||
/// Full room alias
|
||||
alias: OwnedRoomAliasId,
|
||||
},
|
||||
|
||||
/// - Resolve an alias on this server.
|
||||
ResolveLocalAlias {
|
||||
/// Full room alias
|
||||
alias: OwnedRoomAliasId,
|
||||
@@ -28,6 +35,13 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
|
||||
let services = context.services;
|
||||
|
||||
match subcommand {
|
||||
| RoomAliasCommand::ResolveAlias { alias } => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.alias.resolve_alias(&alias).await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
write!(context, "Query completed in {query_time:?}:\n\n```rs\n{results:#?}\n```")
|
||||
},
|
||||
| RoomAliasCommand::ResolveLocalAlias { alias } => {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let results = services.alias.resolve_local_alias(&alias).await;
|
||||
|
||||
+14
-7
@@ -29,7 +29,7 @@ pub(crate) enum SyncCommand {
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn list_connections(&self) -> Result {
|
||||
let connections = self.services.sync.list_connections();
|
||||
let connections = self.services.sync.list_loaded_connections().await;
|
||||
|
||||
for connection_key in connections {
|
||||
self.write_str(&format!("{connection_key:?}\n"))
|
||||
@@ -47,7 +47,11 @@ pub(super) async fn show_connection(
|
||||
conn_id: Option<String>,
|
||||
) -> Result {
|
||||
let key = into_connection_key(user_id, device_id, conn_id);
|
||||
let cache = self.services.sync.find_connection(&key)?;
|
||||
let cache = self
|
||||
.services
|
||||
.sync
|
||||
.get_loaded_connection(&key)
|
||||
.await?;
|
||||
|
||||
let out;
|
||||
{
|
||||
@@ -65,11 +69,14 @@ pub(super) async fn drop_connections(
|
||||
device_id: Option<OwnedDeviceId>,
|
||||
conn_id: Option<String>,
|
||||
) -> Result {
|
||||
self.services.sync.clear_connections(
|
||||
user_id.as_deref(),
|
||||
device_id.as_deref(),
|
||||
conn_id.map(Into::into).as_ref(),
|
||||
);
|
||||
self.services
|
||||
.sync
|
||||
.clear_connections(
|
||||
user_id.as_deref(),
|
||||
device_id.as_deref(),
|
||||
conn_id.map(Into::into).as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use ruma::{
|
||||
Int, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
|
||||
Int, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId,
|
||||
events::{
|
||||
RoomAccountDataEventType, StateEventType,
|
||||
room::{
|
||||
@@ -224,6 +224,25 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn delete_device(
|
||||
&self,
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
) -> Result {
|
||||
if !self.services.globals.user_is_local(&user_id) {
|
||||
return Err!("Cannot delete device of remote user");
|
||||
}
|
||||
|
||||
self.services
|
||||
.users
|
||||
.remove_device(&user_id, &device_id)
|
||||
.await;
|
||||
|
||||
self.write_str(&format!("User {user_id}'s device {device_id} removed."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod commands;
|
||||
|
||||
use clap::Subcommand;
|
||||
use ruma::{OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId};
|
||||
use ruma::{OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId};
|
||||
use tuwunel_core::Result;
|
||||
|
||||
use crate::admin_command_dispatch;
|
||||
@@ -59,6 +59,12 @@ pub(super) enum UserCommand {
|
||||
force: bool,
|
||||
},
|
||||
|
||||
/// - Deletes a user's device.
|
||||
DeleteDevice {
|
||||
user_id: OwnedUserId,
|
||||
device_id: OwnedDeviceId,
|
||||
},
|
||||
|
||||
/// - List local users in the database
|
||||
#[clap(alias = "list")]
|
||||
ListUsers,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use futures::StreamExt;
|
||||
use ruma::api::client::dehydrated_device::{
|
||||
delete_dehydrated_device::unstable as delete_dehydrated_device,
|
||||
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
|
||||
put_dehydrated_device::unstable as put_dehydrated_device,
|
||||
};
|
||||
use tuwunel_core::{Err, Result, at, utils::result::IsErrOr};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
const MAX_BATCH_EVENTS: usize = 50;
|
||||
|
||||
/// # `PUT /_matrix/client/../dehydrated_device`
|
||||
///
|
||||
/// Creates or overwrites the user's dehydrated device.
|
||||
#[tracing::instrument(skip_all, fields(%client))]
|
||||
pub(crate) async fn put_dehydrated_device_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<put_dehydrated_device::Request>,
|
||||
) -> Result<put_dehydrated_device::Response> {
|
||||
let sender_user = body
|
||||
.sender_user
|
||||
.as_deref()
|
||||
.expect("AccessToken authentication required");
|
||||
|
||||
let device_id = body.body.device_id.clone();
|
||||
|
||||
services
|
||||
.users
|
||||
.set_dehydrated_device(sender_user, body.body)
|
||||
.await?;
|
||||
|
||||
Ok(put_dehydrated_device::Response { device_id })
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/../dehydrated_device`
|
||||
///
|
||||
/// Deletes the user's dehydrated device without replacement.
|
||||
#[tracing::instrument(skip_all, fields(%client))]
|
||||
pub(crate) async fn delete_dehydrated_device_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<delete_dehydrated_device::Request>,
|
||||
) -> Result<delete_dehydrated_device::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
let device_id = services
|
||||
.users
|
||||
.get_dehydrated_device_id(sender_user)
|
||||
.await?;
|
||||
|
||||
services
|
||||
.users
|
||||
.remove_device(sender_user, &device_id)
|
||||
.await;
|
||||
|
||||
Ok(delete_dehydrated_device::Response { device_id })
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/../dehydrated_device`
|
||||
///
|
||||
/// Gets the user's dehydrated device
|
||||
#[tracing::instrument(skip_all, fields(%client))]
|
||||
pub(crate) async fn get_dehydrated_device_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_dehydrated_device::Request>,
|
||||
) -> Result<get_dehydrated_device::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
let device = services
|
||||
.users
|
||||
.get_dehydrated_device(sender_user)
|
||||
.await?;
|
||||
|
||||
Ok(get_dehydrated_device::Response {
|
||||
device_id: device.device_id,
|
||||
device_data: device.device_data,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
|
||||
///
|
||||
/// Paginates the events of the dehydrated device.
|
||||
#[tracing::instrument(skip_all, fields(%client))]
|
||||
pub(crate) async fn get_dehydrated_events_route(
|
||||
State(services): State<crate::State>,
|
||||
InsecureClientIp(client): InsecureClientIp,
|
||||
body: Ruma<get_events::Request>,
|
||||
) -> Result<get_events::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
let device_id = &body.body.device_id;
|
||||
let existing_id = services
|
||||
.users
|
||||
.get_dehydrated_device_id(sender_user)
|
||||
.await;
|
||||
|
||||
if existing_id
|
||||
.as_ref()
|
||||
.is_err_or(|existing_id| existing_id != device_id)
|
||||
{
|
||||
return Err!(Request(Forbidden("Not the dehydrated device_id.")));
|
||||
}
|
||||
|
||||
let since: Option<u64> = body
|
||||
.body
|
||||
.next_batch
|
||||
.as_deref()
|
||||
.map(str::parse)
|
||||
.transpose()?;
|
||||
|
||||
let mut next_batch: Option<u64> = None;
|
||||
let events = services
|
||||
.users
|
||||
.get_to_device_events(sender_user, device_id, since, None)
|
||||
.take(MAX_BATCH_EVENTS)
|
||||
.inspect(|&(count, _)| {
|
||||
next_batch.replace(count);
|
||||
})
|
||||
.map(at!(1))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
Ok(get_events::Response {
|
||||
events,
|
||||
next_batch: next_batch.as_ref().map(ToString::to_string),
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
||||
},
|
||||
};
|
||||
use tuwunel_core::{Err, Result, debug, err, utils};
|
||||
use tuwunel_core::{Err, Result, debug, err, utils, utils::string::to_small_string};
|
||||
|
||||
use crate::{Ruma, client::DEVICE_ID_LENGTH, router::auth_uiaa};
|
||||
|
||||
@@ -61,18 +61,21 @@ pub(crate) async fn update_device_route(
|
||||
.await
|
||||
{
|
||||
| Ok(mut device) => {
|
||||
let notify = device.display_name != body.display_name;
|
||||
device.display_name.clone_from(&body.display_name);
|
||||
|
||||
device
|
||||
.last_seen_ip
|
||||
.clone_from(&Some(client.to_string()));
|
||||
.clone_from(&Some(to_small_string(client)));
|
||||
|
||||
device
|
||||
.last_seen_ts
|
||||
.clone_from(&Some(MilliSecondsSinceUnixEpoch::now()));
|
||||
|
||||
assert_eq!(device.device_id, body.device_id, "device_id mismatch");
|
||||
services
|
||||
.users
|
||||
.update_device_metadata(sender_user, &body.device_id, &device)
|
||||
.await?;
|
||||
.put_device_metadata(sender_user, notify, &device);
|
||||
|
||||
Ok(update_device::v3::Response {})
|
||||
},
|
||||
@@ -80,6 +83,7 @@ pub(crate) async fn update_device_route(
|
||||
let Some(appservice) = appservice else {
|
||||
return Err!(Request(NotFound("Device not found.")));
|
||||
};
|
||||
|
||||
if !appservice.registration.device_management {
|
||||
return Err!(Request(NotFound("Device not found.")));
|
||||
}
|
||||
@@ -97,7 +101,7 @@ pub(crate) async fn update_device_route(
|
||||
.create_device(
|
||||
sender_user,
|
||||
&device_id,
|
||||
(&appservice.registration.as_token, None),
|
||||
(Some(&appservice.registration.as_token), None),
|
||||
None,
|
||||
None,
|
||||
Some(client.to_string()),
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
use std::iter::once;
|
||||
|
||||
use axum::extract::State;
|
||||
use futures::StreamExt;
|
||||
use ruma::api::client::peeking::listen_to_new_events::v3::{Request, Response};
|
||||
use tokio::time::{Duration, Instant, timeout_at};
|
||||
use tuwunel_core::{
|
||||
Err, Event, Result, at,
|
||||
matrix::PduCount,
|
||||
utils::{
|
||||
BoolExt,
|
||||
result::FlatOk,
|
||||
stream::{IterStream, ReadyExt},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
const EVENT_LIMIT: usize = 50;
|
||||
|
||||
/// GET `/_matrix/client/v3/events`
|
||||
pub(crate) async fn events_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<Request>,
|
||||
) -> Result<Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
let from = body
|
||||
.body
|
||||
.from
|
||||
.as_deref()
|
||||
.map(str::parse)
|
||||
.flat_ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let timeout = body
|
||||
.body
|
||||
.timeout
|
||||
.as_ref()
|
||||
.map(Duration::as_millis)
|
||||
.map(TryInto::try_into)
|
||||
.flat_ok()
|
||||
.unwrap_or(services.config.client_sync_timeout_default)
|
||||
.max(services.config.client_sync_timeout_min)
|
||||
.min(services.config.client_sync_timeout_max);
|
||||
|
||||
let Some(room_id) = body.room_id.as_deref() else {
|
||||
//TODO: upgrade ruma
|
||||
return Err!(Request(InvalidParam("Missing RoomId parameter.")));
|
||||
};
|
||||
|
||||
if !services
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, room_id)
|
||||
.await
|
||||
{
|
||||
return Err!(Request(Forbidden("No room preview available.")));
|
||||
}
|
||||
|
||||
let stop_at = Instant::now()
|
||||
.checked_add(Duration::from_millis(timeout))
|
||||
.expect("configuration must limit maximum timeout");
|
||||
|
||||
loop {
|
||||
let watchers = services
|
||||
.sync
|
||||
.watch(sender_user, sender_device, once(room_id).stream());
|
||||
|
||||
let next_batch = services.globals.wait_pending().await?;
|
||||
|
||||
let events = services
|
||||
.timeline
|
||||
.pdus(Some(sender_user), room_id, Some(PduCount::Normal(from)))
|
||||
.ready_filter_map(Result::ok)
|
||||
.ready_take_while(|(count, _)| PduCount::Normal(next_batch).ge(count))
|
||||
.take(EVENT_LIMIT)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
if !events.is_empty() {
|
||||
return Ok(Response {
|
||||
start: events
|
||||
.first()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
|
||||
end: events
|
||||
.last()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
|
||||
chunk: events
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(Event::into_format)
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
||||
if timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
|
||||
return Ok(Response {
|
||||
chunk: Default::default(),
|
||||
start: body.body.from,
|
||||
end: services
|
||||
.server
|
||||
.is_stopping()
|
||||
.is_false()
|
||||
.then_some(next_batch)
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-22
@@ -37,30 +37,16 @@ pub(crate) async fn upload_keys_route(
|
||||
) -> Result<upload_keys::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
for (key_id, one_time_key) in body
|
||||
let one_time_keys = body
|
||||
.one_time_keys
|
||||
.iter()
|
||||
.take(services.config.one_time_key_limit)
|
||||
{
|
||||
if one_time_key
|
||||
.deserialize()
|
||||
.inspect_err(|e| {
|
||||
debug_warn!(
|
||||
?key_id,
|
||||
?one_time_key,
|
||||
"Invalid one time key JSON submitted by client, skipping: {e}"
|
||||
);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
.map(|(id, val)| (id.as_ref(), val));
|
||||
|
||||
services
|
||||
.users
|
||||
.add_one_time_key(sender_user, sender_device, key_id, one_time_key)
|
||||
.await?;
|
||||
}
|
||||
services
|
||||
.users
|
||||
.add_one_time_keys(sender_user, sender_device, one_time_keys)
|
||||
.await?;
|
||||
|
||||
if let Some(device_keys) = &body.device_keys {
|
||||
let deser_device_keys = device_keys.deserialize().map_err(|e| {
|
||||
@@ -85,8 +71,11 @@ pub(crate) async fn upload_keys_route(
|
||||
.users
|
||||
.get_device_keys(sender_user, sender_device)
|
||||
.await
|
||||
.and_then(|keys| keys.deserialize().map_err(Into::into))
|
||||
{
|
||||
if existing_keys.json().get() == device_keys.json().get() {
|
||||
// NOTE: also serves as a workaround for a nheko bug which omits cross-signing
|
||||
// NOTE: signatures when re-uploading the same DeviceKeys.
|
||||
if existing_keys.keys == deser_device_keys.keys {
|
||||
debug!(
|
||||
?sender_user,
|
||||
?sender_device,
|
||||
@@ -585,7 +574,7 @@ fn add_unsigned_device_display_name(
|
||||
.or_insert_with(|| CanonicalJsonObject::default().into())
|
||||
{
|
||||
let display_name = if include_display_names {
|
||||
CanonicalJsonValue::String(display_name)
|
||||
CanonicalJsonValue::String(display_name.to_string())
|
||||
} else {
|
||||
CanonicalJsonValue::String(metadata.device_id.into())
|
||||
};
|
||||
|
||||
@@ -210,11 +210,11 @@ async fn knock_room_helper_local(
|
||||
})?;
|
||||
|
||||
knock_event_stub.insert(
|
||||
"origin".to_owned(),
|
||||
"origin".into(),
|
||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
||||
);
|
||||
knock_event_stub.insert(
|
||||
"origin_server_ts".to_owned(),
|
||||
"origin_server_ts".into(),
|
||||
CanonicalJsonValue::Integer(
|
||||
utils::millis_since_unix_epoch()
|
||||
.try_into()
|
||||
@@ -222,7 +222,7 @@ async fn knock_room_helper_local(
|
||||
),
|
||||
);
|
||||
knock_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
"content".into(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
@@ -244,7 +244,7 @@ async fn knock_room_helper_local(
|
||||
|
||||
// Add event_id
|
||||
knock_event_stub
|
||||
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
|
||||
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
|
||||
|
||||
// It has enough fields to be called a proper event now
|
||||
let knock_event = knock_event_stub;
|
||||
@@ -346,11 +346,11 @@ async fn knock_room_helper_remote(
|
||||
})?;
|
||||
|
||||
knock_event_stub.insert(
|
||||
"origin".to_owned(),
|
||||
"origin".into(),
|
||||
CanonicalJsonValue::String(services.globals.server_name().as_str().to_owned()),
|
||||
);
|
||||
knock_event_stub.insert(
|
||||
"origin_server_ts".to_owned(),
|
||||
"origin_server_ts".into(),
|
||||
CanonicalJsonValue::Integer(
|
||||
utils::millis_since_unix_epoch()
|
||||
.try_into()
|
||||
@@ -358,7 +358,7 @@ async fn knock_room_helper_remote(
|
||||
),
|
||||
);
|
||||
knock_event_stub.insert(
|
||||
"content".to_owned(),
|
||||
"content".into(),
|
||||
to_canonical_value(RoomMemberEventContent {
|
||||
displayname: services.users.displayname(sender_user).await.ok(),
|
||||
avatar_url: services.users.avatar_url(sender_user).await.ok(),
|
||||
@@ -380,7 +380,7 @@ async fn knock_room_helper_remote(
|
||||
|
||||
// Add event_id
|
||||
knock_event_stub
|
||||
.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.clone().into()));
|
||||
.insert("event_id".into(), CanonicalJsonValue::String(event_id.clone().into()));
|
||||
|
||||
// It has enough fields to be called a proper event now
|
||||
let knock_event = knock_event_stub;
|
||||
|
||||
@@ -195,12 +195,10 @@ async fn get_join_params(
|
||||
);
|
||||
|
||||
// shuffle additionals, append to base servers
|
||||
additional_servers.sort_unstable();
|
||||
additional_servers.dedup();
|
||||
shuffle(&mut additional_servers);
|
||||
servers.append(&mut additional_servers);
|
||||
servers.sort_unstable();
|
||||
servers.dedup();
|
||||
servers.append(&mut additional_servers);
|
||||
shuffle(&mut servers);
|
||||
|
||||
// sort deprioritized servers last
|
||||
servers.sort_by(|a, b| {
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
pub(super) mod backup;
|
||||
pub(super) mod capabilities;
|
||||
pub(super) mod context;
|
||||
pub(super) mod dehydrated_device;
|
||||
pub(super) mod device;
|
||||
pub(super) mod directory;
|
||||
pub(super) mod events;
|
||||
pub(super) mod filter;
|
||||
pub(super) mod keys;
|
||||
pub(super) mod media;
|
||||
@@ -49,8 +51,10 @@
|
||||
pub(super) use backup::*;
|
||||
pub(super) use capabilities::*;
|
||||
pub(super) use context::*;
|
||||
pub(super) use dehydrated_device::*;
|
||||
pub(super) use device::*;
|
||||
pub(super) use directory::*;
|
||||
pub(super) use events::*;
|
||||
pub(super) use filter::*;
|
||||
pub(super) use keys::*;
|
||||
pub(super) use media::*;
|
||||
|
||||
@@ -49,7 +49,7 @@ pub(crate) async fn set_displayname_route(
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
|
||||
.await?;
|
||||
|
||||
Ok(set_display_name::v3::Response {})
|
||||
@@ -149,7 +149,7 @@ pub(crate) async fn set_avatar_url_route(
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
|
||||
+108
-5
@@ -1,10 +1,11 @@
|
||||
use axum::extract::State;
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue,
|
||||
CanonicalJsonObject, CanonicalJsonValue, MilliSecondsSinceUnixEpoch,
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
push::{
|
||||
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions,
|
||||
delete_pushrule, get_notifications, get_pushers, get_pushrule, get_pushrule_actions,
|
||||
get_pushrule_enabled, get_pushrules_all, get_pushrules_global_scope, set_pusher,
|
||||
set_pushrule, set_pushrule_actions, set_pushrule_enabled,
|
||||
},
|
||||
@@ -14,15 +15,117 @@
|
||||
push_rules::{PushRulesEvent, PushRulesEventContent},
|
||||
},
|
||||
push::{
|
||||
InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId,
|
||||
Action, InsertPushRuleError, PredefinedContentRuleId, PredefinedOverrideRuleId,
|
||||
RemovePushRuleError, Ruleset,
|
||||
},
|
||||
};
|
||||
use tuwunel_core::{Err, Error, Result, err};
|
||||
use tuwunel_core::{
|
||||
Err, Error, Result, at, err,
|
||||
matrix::{Event, PduId},
|
||||
utils::{
|
||||
stream::{ReadyExt, WidebandExt},
|
||||
string::to_small_string,
|
||||
},
|
||||
};
|
||||
use tuwunel_service::Services;
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/r0/notifications/`
|
||||
///
|
||||
/// Paginate through the list of events the user has been, or would have been
|
||||
/// notified about.
|
||||
pub(crate) async fn get_notifications_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_notifications::v3::Request>,
|
||||
) -> Result<get_notifications::v3::Response> {
|
||||
use get_notifications::v3::Notification;
|
||||
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
let from = body
|
||||
.body
|
||||
.from
|
||||
.as_deref()
|
||||
.map(str::parse)
|
||||
.transpose()
|
||||
.map_err(|e| err!(Request(InvalidParam("Invalid `from' parameter: {e}"))))?;
|
||||
|
||||
let limit: usize = body
|
||||
.body
|
||||
.limit
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?
|
||||
.unwrap_or(50)
|
||||
.clamp(1, 100);
|
||||
|
||||
let only_highlight = body
|
||||
.body
|
||||
.only
|
||||
.as_deref()
|
||||
.is_some_and(|only| only.contains("highlight"));
|
||||
|
||||
let mut next_token: Option<u64> = None;
|
||||
let notifications = services
|
||||
.pusher
|
||||
.get_notifications(sender_user, from)
|
||||
.ready_filter(|(_, notify)| {
|
||||
if only_highlight && !notify.actions.iter().any(Action::is_highlight) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.wide_filter_map(async |(count, notify)| {
|
||||
let pdu_id = PduId {
|
||||
shortroomid: notify.sroomid,
|
||||
count: count.into(),
|
||||
};
|
||||
|
||||
let event = services
|
||||
.timeline
|
||||
.get_pdu_from_id(&pdu_id.into())
|
||||
.await
|
||||
.ok()
|
||||
.filter(|event| !event.is_redacted())?;
|
||||
|
||||
let read = services
|
||||
.pusher
|
||||
.last_notification_read(sender_user, event.room_id())
|
||||
.await
|
||||
.is_ok_and(|last_read| last_read.ge(&count));
|
||||
|
||||
let ts = notify
|
||||
.ts
|
||||
.try_into()
|
||||
.map(MilliSecondsSinceUnixEpoch)
|
||||
.ok()?;
|
||||
|
||||
let notification = Notification {
|
||||
room_id: event.room_id().into(),
|
||||
event: event.into_format(),
|
||||
ts,
|
||||
read,
|
||||
profile_tag: notify.tag,
|
||||
actions: notify.actions,
|
||||
};
|
||||
|
||||
Some((count, notification))
|
||||
})
|
||||
.take(limit)
|
||||
.inspect(|(count, _)| {
|
||||
next_token.replace(*count);
|
||||
})
|
||||
.map(at!(1))
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
Ok(get_notifications::v3::Response {
|
||||
next_token: next_token.map(to_small_string),
|
||||
notifications,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/`
|
||||
///
|
||||
/// Retrieves the push rules event for this user.
|
||||
@@ -335,7 +438,7 @@ pub(crate) async fn set_pushrule_actions_route(
|
||||
if account_data
|
||||
.content
|
||||
.global
|
||||
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
|
||||
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone().into())
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(NotFound("Push rule not found.")));
|
||||
|
||||
@@ -26,6 +26,12 @@ pub(crate) async fn set_read_marker_route(
|
||||
) -> Result<set_read_marker::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
|
||||
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
|
||||
services
|
||||
.pusher
|
||||
.reset_notification_counts(sender_user, &body.room_id);
|
||||
}
|
||||
|
||||
if let Some(event) = &body.fully_read {
|
||||
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
|
||||
content: ruma::events::fully_read::FullyReadEventContent { event_id: event.clone() },
|
||||
@@ -39,21 +45,29 @@ pub(crate) async fn set_read_marker_route(
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event)?,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
|
||||
if let Some(event) = &body.private_read_receipt {
|
||||
let count = services
|
||||
.timeline
|
||||
.get_pdu_count(event)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
|
||||
|
||||
let PduCount::Normal(count) = count else {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Event is a backfilled PDU and cannot be marked as read."
|
||||
)));
|
||||
};
|
||||
|
||||
services
|
||||
.user
|
||||
.reset_notification_counts(sender_user, &body.room_id);
|
||||
.read_receipt
|
||||
.private_read_set(&body.room_id, sender_user, count);
|
||||
}
|
||||
|
||||
if let Some(event) = &body.read_receipt {
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(sender_user, &ruma::presence::PresenceState::Online)
|
||||
.await?;
|
||||
|
||||
let receipt_content = BTreeMap::from_iter([(
|
||||
event.to_owned(),
|
||||
BTreeMap::from_iter([(
|
||||
@@ -76,24 +90,16 @@ pub(crate) async fn set_read_marker_route(
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(event) = &body.private_read_receipt {
|
||||
let count = services
|
||||
.timeline
|
||||
.get_pdu_count(event)
|
||||
.await
|
||||
.map_err(|_| err!(Request(NotFound("Event not found."))))?;
|
||||
|
||||
let PduCount::Normal(count) = count else {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Event is a backfilled PDU and cannot be marked as read."
|
||||
)));
|
||||
};
|
||||
|
||||
services
|
||||
.read_receipt
|
||||
.private_read_set(&body.room_id, sender_user, count);
|
||||
.presence
|
||||
.maybe_ping_presence(
|
||||
sender_user,
|
||||
body.sender_device.as_deref(),
|
||||
&ruma::presence::PresenceState::Online,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(set_read_marker::v3::Response {})
|
||||
@@ -113,7 +119,7 @@ pub(crate) async fn create_receipt_route(
|
||||
create_receipt::v3::ReceiptType::Read | create_receipt::v3::ReceiptType::ReadPrivate
|
||||
) {
|
||||
services
|
||||
.user
|
||||
.pusher
|
||||
.reset_notification_counts(sender_user, &body.room_id);
|
||||
}
|
||||
|
||||
@@ -135,11 +141,6 @@ pub(crate) async fn create_receipt_route(
|
||||
.await?;
|
||||
},
|
||||
| create_receipt::v3::ReceiptType::Read => {
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(sender_user, &ruma::presence::PresenceState::Online)
|
||||
.await?;
|
||||
|
||||
let receipt_content = BTreeMap::from_iter([(
|
||||
body.event_id.clone(),
|
||||
BTreeMap::from_iter([(
|
||||
@@ -165,6 +166,16 @@ pub(crate) async fn create_receipt_route(
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(
|
||||
sender_user,
|
||||
body.sender_device.as_deref(),
|
||||
&ruma::presence::PresenceState::Online,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
},
|
||||
| create_receipt::v3::ReceiptType::ReadPrivate => {
|
||||
let count = services
|
||||
|
||||
+23
-17
@@ -16,7 +16,9 @@
|
||||
events::GlobalAccountDataEventType,
|
||||
push,
|
||||
};
|
||||
use tuwunel_core::{Err, Error, Result, debug_info, error, info, is_equal_to, utils, warn};
|
||||
use tuwunel_core::{
|
||||
Err, Error, Result, debug_info, debug_warn, error, info, is_equal_to, utils, warn,
|
||||
};
|
||||
use tuwunel_service::users::device::generate_refresh_token;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH};
|
||||
@@ -181,31 +183,33 @@ pub(crate) async fn register_route(
|
||||
return Err!(Request(Forbidden("Registration has been disabled.")));
|
||||
}
|
||||
|
||||
if is_guest
|
||||
&& (!services.config.allow_guest_registration
|
||||
|| (services.config.allow_registration
|
||||
&& services.globals.registration_token.is_some()))
|
||||
{
|
||||
info!(
|
||||
if is_guest && !services.config.allow_guest_registration {
|
||||
let display_name = body
|
||||
.initial_device_display_name
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
|
||||
debug_warn!(
|
||||
"Guest registration disabled / registration enabled with token configured, \
|
||||
rejecting guest registration attempt, initial device name: \"{}\"",
|
||||
body.initial_device_display_name
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
rejecting guest registration attempt, initial device name: \"{display_name}\""
|
||||
);
|
||||
|
||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||
}
|
||||
|
||||
// forbid guests from registering if there is not a real admin user yet. give
|
||||
// generic user error.
|
||||
if is_guest && services.users.count().await < 2 {
|
||||
let display_name = body
|
||||
.initial_device_display_name
|
||||
.as_deref()
|
||||
.unwrap_or("");
|
||||
|
||||
warn!(
|
||||
"Guest account attempted to register before a real admin user has been registered, \
|
||||
rejecting registration. Guest's initial device name: \"{}\"",
|
||||
body.initial_device_display_name
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
rejecting registration. Guest's initial device name: \"{display_name}\""
|
||||
);
|
||||
|
||||
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
|
||||
}
|
||||
|
||||
@@ -309,7 +313,7 @@ pub(crate) async fn register_route(
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo;
|
||||
let skip_auth = if services.globals.registration_token.is_some() {
|
||||
let skip_auth = if services.globals.registration_token.is_some() && !is_guest {
|
||||
// Registration token required
|
||||
uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
@@ -320,6 +324,7 @@ pub(crate) async fn register_route(
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
body.appservice_info.is_some()
|
||||
} else {
|
||||
// No registration token necessary, but clients must still go through the flow
|
||||
@@ -330,6 +335,7 @@ pub(crate) async fn register_route(
|
||||
session: None,
|
||||
auth_error: None,
|
||||
};
|
||||
|
||||
body.appservice_info.is_some() || is_guest
|
||||
};
|
||||
|
||||
@@ -446,7 +452,7 @@ pub(crate) async fn register_route(
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
(&access_token, expires_in),
|
||||
(Some(&access_token), expires_in),
|
||||
refresh_token.as_deref(),
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
|
||||
@@ -154,13 +154,11 @@ async fn paginate_relations_with_filter(
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let next_batch = match dir {
|
||||
| Direction::Forward => events.last(),
|
||||
| Direction::Backward => events.first(),
|
||||
}
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
let next_batch = events
|
||||
.last()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
Ok(get_relating_events::v1::Response {
|
||||
next_batch,
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
use axum::extract::State;
|
||||
use futures::{FutureExt, future::OptionFuture};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
|
||||
CanonicalJsonObject, EventEncryptionAlgorithm, Int, OwnedRoomAliasId, OwnedRoomId,
|
||||
OwnedUserId, RoomId, RoomVersionId,
|
||||
api::client::room::{
|
||||
self, create_room,
|
||||
create_room::v3::{CreationContent, RoomPreset},
|
||||
@@ -13,6 +14,7 @@
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
encryption::RoomEncryptionEventContent,
|
||||
guest_access::{GuestAccess, RoomGuestAccessEventContent},
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
@@ -262,6 +264,7 @@ pub(crate) async fn create_room_route(
|
||||
.await?;
|
||||
|
||||
// 6. Events listed in initial_state
|
||||
let mut is_encrypted = false;
|
||||
for event in &body.initial_state {
|
||||
let mut pdu_builder = event
|
||||
.deserialize_as_unchecked::<PduBuilder>()
|
||||
@@ -292,6 +295,10 @@ pub(crate) async fn create_room_route(
|
||||
continue;
|
||||
}
|
||||
|
||||
if pdu_builder.event_type == TimelineEventType::RoomEncryption {
|
||||
is_encrypted = true;
|
||||
}
|
||||
|
||||
services
|
||||
.timeline
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
|
||||
@@ -299,6 +306,33 @@ pub(crate) async fn create_room_route(
|
||||
.await?;
|
||||
}
|
||||
|
||||
if services.config.allow_encryption && !is_encrypted {
|
||||
use RoomPreset::*;
|
||||
|
||||
let config = services
|
||||
.config
|
||||
.encryption_enabled_by_default_for_room_type
|
||||
.as_deref()
|
||||
.unwrap_or("off");
|
||||
|
||||
let invite = matches!(config, "invite");
|
||||
let always = matches!(config, "all" | "invite");
|
||||
if always || (invite && matches!(preset, PrivateChat | TrustedPrivateChat)) {
|
||||
let algorithm = EventEncryptionAlgorithm::MegolmV1AesSha2;
|
||||
let content = RoomEncryptionEventContent::new(algorithm);
|
||||
services
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(String::new(), &content),
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Events implied by name and topic
|
||||
if let Some(name) = &body.name {
|
||||
services
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
use axum::extract::State;
|
||||
use futures::{FutureExt, TryStreamExt, future::try_join4};
|
||||
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::try_join5};
|
||||
use ruma::{
|
||||
api::client::room::initial_sync::v3::{PaginationChunk, Request, Response},
|
||||
events::AnyRawAccountDataEvent,
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Err, Event, Result, at,
|
||||
utils::{BoolExt, stream::TryTools},
|
||||
Err, Event, Result, at, extract_variant,
|
||||
matrix::PduCount,
|
||||
utils::stream::{ReadyExt, TryTools},
|
||||
};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
const LIMIT_MAX: usize = 100;
|
||||
const LIMIT_MAX: usize = 50;
|
||||
|
||||
/// GET `/_matrix/client/v3/rooms/{roomId}/initialSync`
|
||||
pub(crate) async fn room_initial_sync_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<Request>,
|
||||
@@ -24,13 +29,15 @@ pub(crate) async fn room_initial_sync_route(
|
||||
return Err!(Request(Forbidden("No room preview available.")));
|
||||
}
|
||||
|
||||
let next_batch = services.globals.current_count();
|
||||
|
||||
let visibility = services.directory.visibility(room_id).map(Ok);
|
||||
|
||||
let membership = services
|
||||
.state_cache
|
||||
.user_membership(body.sender_user(), room_id)
|
||||
.map(Ok);
|
||||
|
||||
let visibility = services.directory.visibility(room_id).map(Ok);
|
||||
|
||||
let state = services
|
||||
.state_accessor
|
||||
.room_state_full_pdus(room_id)
|
||||
@@ -40,42 +47,52 @@ pub(crate) async fn room_initial_sync_route(
|
||||
let limit = LIMIT_MAX;
|
||||
let events = services
|
||||
.timeline
|
||||
.pdus_rev(None, room_id, None)
|
||||
.pdus_rev(None, room_id, Some(PduCount::Normal(next_batch).saturating_add(1)))
|
||||
.try_take(limit)
|
||||
.try_collect::<Vec<_>>();
|
||||
.try_collect()
|
||||
.map_ok(|mut vec: Vec<_>| {
|
||||
vec.reverse();
|
||||
vec
|
||||
});
|
||||
|
||||
let (membership, visibility, state, events) =
|
||||
try_join4(membership, visibility, state, events)
|
||||
let account_data = services
|
||||
.account_data
|
||||
.changes_since(Some(room_id), body.sender_user(), 0, Some(next_batch))
|
||||
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
|
||||
.collect::<Vec<_>>()
|
||||
.map(Ok);
|
||||
|
||||
let (membership, visibility, state, events, account_data) =
|
||||
try_join5(membership, visibility, state, events, account_data)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
let messages = PaginationChunk {
|
||||
start: events
|
||||
.last()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
|
||||
end: events
|
||||
.first()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
|
||||
chunk: events
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(Event::into_format)
|
||||
.collect(),
|
||||
};
|
||||
|
||||
Ok(Response {
|
||||
room_id: room_id.to_owned(),
|
||||
account_data: None,
|
||||
state: state.into(),
|
||||
messages: messages.chunk.is_empty().or_some(messages),
|
||||
visibility: visibility.into(),
|
||||
membership,
|
||||
visibility: visibility.into(),
|
||||
account_data: Some(account_data),
|
||||
state: state.into(),
|
||||
messages: PaginationChunk {
|
||||
start: events
|
||||
.first()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string),
|
||||
|
||||
end: events
|
||||
.last()
|
||||
.map(at!(0))
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_default(),
|
||||
|
||||
chunk: events
|
||||
.into_iter()
|
||||
.map(at!(1))
|
||||
.map(Event::into_format)
|
||||
.collect(),
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ pub(crate) async fn login_route(
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
(&access_token, expires_in),
|
||||
(Some(&access_token), expires_in),
|
||||
refresh_token.as_deref(),
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod v3;
|
||||
mod v5;
|
||||
|
||||
use futures::{StreamExt, pin_mut};
|
||||
use futures::{FutureExt, StreamExt, pin_mut};
|
||||
use ruma::{RoomId, UserId};
|
||||
use tuwunel_core::{
|
||||
Error, PduCount, Result,
|
||||
@@ -42,10 +42,12 @@ async fn load_timeline(
|
||||
.by_ref()
|
||||
.take(limit)
|
||||
.collect()
|
||||
.map(|mut pdus: Vec<_>| {
|
||||
pdus.reverse();
|
||||
pdus
|
||||
})
|
||||
.await;
|
||||
|
||||
let timeline_pdus: Vec<_> = timeline_pdus.into_iter().rev().collect();
|
||||
|
||||
// They /sync response doesn't always return all messages, so we say the output
|
||||
// is limited unless there are events in non_timeline_pdus
|
||||
let limited = non_timeline_pdus.next().await.is_some();
|
||||
|
||||
+212
-147
@@ -10,7 +10,7 @@
|
||||
pin_mut,
|
||||
};
|
||||
use ruma::{
|
||||
DeviceId, EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
DeviceId, EventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
|
||||
api::client::{
|
||||
filter::FilterDefinition,
|
||||
sync::sync_events::{
|
||||
@@ -21,7 +21,6 @@
|
||||
State as RoomState, StateEvents, Timeline, ToDevice,
|
||||
},
|
||||
},
|
||||
uiaa::UiaaResponse,
|
||||
},
|
||||
events::{
|
||||
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, StateEventType,
|
||||
@@ -38,8 +37,8 @@
|
||||
use tuwunel_core::{
|
||||
Error, Result, at,
|
||||
debug::INFO_SPAN_LEVEL,
|
||||
err, error,
|
||||
error::inspect_debug_log,
|
||||
err,
|
||||
error::{inspect_debug_log, inspect_log},
|
||||
extract_variant, is_equal_to,
|
||||
matrix::{
|
||||
Event,
|
||||
@@ -53,10 +52,10 @@
|
||||
self, BoolExt, FutureBoolExt, IterStream, ReadyExt, TryFutureExtExt,
|
||||
future::{OptionStream, ReadyEqExt},
|
||||
math::ruma_from_u64,
|
||||
result::LogErr,
|
||||
result::MapExpect,
|
||||
stream::{BroadbandExt, Tools, TryExpect, WidebandExt},
|
||||
string::to_small_string,
|
||||
},
|
||||
warn,
|
||||
};
|
||||
use tuwunel_service::{
|
||||
Services,
|
||||
@@ -68,7 +67,7 @@
|
||||
};
|
||||
|
||||
use super::{load_timeline, share_encrypted_room};
|
||||
use crate::{Ruma, RumaResponse, client::ignored_filter};
|
||||
use crate::{Ruma, client::ignored_filter};
|
||||
|
||||
#[derive(Default)]
|
||||
struct StateChanges {
|
||||
@@ -126,18 +125,36 @@ struct StateChanges {
|
||||
pub(crate) async fn sync_events_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<sync_events::v3::Request>,
|
||||
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
|
||||
) -> Result<sync_events::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
services
|
||||
let filter: OptionFuture<_> = body
|
||||
.body
|
||||
.filter
|
||||
.as_ref()
|
||||
.map(async |filter| match filter {
|
||||
| Filter::FilterDefinition(filter) => filter.clone(),
|
||||
| Filter::FilterId(filter_id) => services
|
||||
.users
|
||||
.get_filter(sender_user, filter_id)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
.into();
|
||||
|
||||
let filter = filter.map(Option::unwrap_or_default);
|
||||
let full_state = body.body.full_state;
|
||||
let set_presence = &body.body.set_presence;
|
||||
let ping_presence = services
|
||||
.presence
|
||||
.maybe_ping_presence(sender_user, &body.body.set_presence)
|
||||
.await
|
||||
.log_err()
|
||||
.maybe_ping_presence(sender_user, body.sender_device.as_deref(), set_presence)
|
||||
.inspect_err(inspect_log)
|
||||
.ok();
|
||||
|
||||
// Record user as actively syncing for push suppression heuristic.
|
||||
services.presence.note_sync(sender_user).await;
|
||||
let note_sync = services.presence.note_sync(sender_user);
|
||||
|
||||
let (filter, ..) = join3(filter, ping_presence, note_sync).await;
|
||||
|
||||
let mut since = body
|
||||
.body
|
||||
@@ -163,7 +180,11 @@ pub(crate) async fn sync_events_route(
|
||||
.expect("configuration must limit maximum timeout");
|
||||
|
||||
loop {
|
||||
let watch_rooms = services.state_cache.rooms_joined(sender_user);
|
||||
let watch_rooms = services
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.chain(services.state_cache.rooms_invited(sender_user));
|
||||
|
||||
let watchers = services
|
||||
.sync
|
||||
.watch(sender_user, sender_device, watch_rooms);
|
||||
@@ -171,22 +192,31 @@ pub(crate) async fn sync_events_route(
|
||||
let next_batch = services.globals.wait_pending().await?;
|
||||
debug_assert!(since <= next_batch, "next_batch is monotonic");
|
||||
|
||||
if since < next_batch || body.body.full_state {
|
||||
let response = build_sync_events(&services, &body, since, next_batch).await?;
|
||||
if since < next_batch || full_state {
|
||||
let response = build_sync_events(
|
||||
&services,
|
||||
body.sender(),
|
||||
since,
|
||||
next_batch,
|
||||
full_state,
|
||||
&filter,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let empty = response.rooms.is_empty()
|
||||
&& response.presence.is_empty()
|
||||
&& response.account_data.is_empty()
|
||||
&& response.device_lists.is_empty()
|
||||
&& response.to_device.is_empty();
|
||||
|
||||
if !empty || body.body.full_state {
|
||||
if !empty || full_state {
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for activity
|
||||
if time::timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
|
||||
let response = build_empty_response(&services, &body, next_batch).await;
|
||||
let response = build_empty_response(&services, body.sender(), next_batch).await;
|
||||
trace!(since, next_batch, "empty response");
|
||||
return Ok(response);
|
||||
}
|
||||
@@ -205,16 +235,16 @@ pub(crate) async fn sync_events_route(
|
||||
|
||||
async fn build_empty_response(
|
||||
services: &Services,
|
||||
body: &Ruma<sync_events::v3::Request>,
|
||||
(sender_user, sender_device): (&UserId, &DeviceId),
|
||||
next_batch: u64,
|
||||
) -> sync_events::v3::Response {
|
||||
sync_events::v3::Response {
|
||||
device_one_time_keys_count: services
|
||||
.users
|
||||
.count_one_time_keys(body.sender_user(), body.sender_device())
|
||||
.count_one_time_keys(sender_user, sender_device)
|
||||
.await,
|
||||
|
||||
..sync_events::v3::Response::new(next_batch.to_string())
|
||||
..sync_events::v3::Response::new(to_small_string(next_batch))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,23 +261,12 @@ async fn build_empty_response(
|
||||
)]
|
||||
async fn build_sync_events(
|
||||
services: &Services,
|
||||
body: &Ruma<sync_events::v3::Request>,
|
||||
(sender_user, sender_device): (&UserId, &DeviceId),
|
||||
since: u64,
|
||||
next_batch: u64,
|
||||
full_state: bool,
|
||||
filter: &FilterDefinition,
|
||||
) -> Result<sync_events::v3::Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
|
||||
let full_state = body.body.full_state;
|
||||
let filter = match body.body.filter.as_ref() {
|
||||
| None => FilterDefinition::default(),
|
||||
| Some(Filter::FilterDefinition(filter)) => filter.clone(),
|
||||
| Some(Filter::FilterId(filter_id)) => services
|
||||
.users
|
||||
.get_filter(sender_user, filter_id)
|
||||
.await
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
let joined_rooms = services
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
@@ -262,7 +281,7 @@ async fn build_sync_events(
|
||||
since,
|
||||
next_batch,
|
||||
full_state,
|
||||
&filter,
|
||||
filter,
|
||||
)
|
||||
.map_ok(move |(joined_room, dlu, jeu)| (room_id, joined_room, dlu, jeu))
|
||||
.ok()
|
||||
@@ -293,7 +312,7 @@ async fn build_sync_events(
|
||||
sender_user,
|
||||
next_batch,
|
||||
full_state,
|
||||
&filter,
|
||||
filter,
|
||||
)
|
||||
.map_ok(move |left_room| (room_id, left_room))
|
||||
.ok()
|
||||
@@ -352,7 +371,7 @@ async fn build_sync_events(
|
||||
let presence_updates: OptionFuture<_> = services
|
||||
.config
|
||||
.allow_local_presence
|
||||
.then(|| process_presence_updates(services, since, next_batch, sender_user, &filter))
|
||||
.then(|| process_presence_updates(services, since, next_batch, sender_user, filter))
|
||||
.into();
|
||||
|
||||
let account_data = services
|
||||
@@ -371,6 +390,7 @@ async fn build_sync_events(
|
||||
let to_device_events = services
|
||||
.users
|
||||
.get_to_device_events(sender_user, sender_device, Some(since), Some(next_batch))
|
||||
.map(at!(1))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let device_one_time_keys_count = services
|
||||
@@ -437,7 +457,7 @@ async fn build_sync_events(
|
||||
device_one_time_keys_count,
|
||||
// Fallback keys are not yet supported
|
||||
device_unused_fallback_key_types: None,
|
||||
next_batch: next_batch.to_string(),
|
||||
next_batch: to_small_string(next_batch),
|
||||
presence: Presence { events: presence_events },
|
||||
rooms: Rooms {
|
||||
leave: left_rooms,
|
||||
@@ -502,28 +522,20 @@ async fn handle_left_room(
|
||||
.state_cache
|
||||
.get_left_count(room_id, sender_user)
|
||||
.await
|
||||
.ok();
|
||||
.unwrap_or(0);
|
||||
|
||||
let filter_exclude = filter
|
||||
.room
|
||||
.not_rooms
|
||||
.iter()
|
||||
.any(is_equal_to!(room_id));
|
||||
if left_count == 0 || left_count > next_batch {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let filter_include = filter
|
||||
.room
|
||||
.rooms
|
||||
.as_ref()
|
||||
.is_some_and(|rooms| rooms.iter().any(is_equal_to!(room_id)));
|
||||
let include_leave = filter.room.include_leave;
|
||||
if since == 0 && !include_leave {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let too_soon = Some(next_batch) < left_count;
|
||||
let too_late = Some(since) >= left_count;
|
||||
let initial_sync = since == 0;
|
||||
let include_leave =
|
||||
filter.room.include_leave && !filter_exclude && (filter_include || initial_sync);
|
||||
|
||||
// Left before last sync or after cutoff for next sync
|
||||
if (too_late && !include_leave) || too_soon {
|
||||
// Cannot sync unless the event falls within the snapshot. The room is only
|
||||
// sync'ed once to the client, after that it's too late.
|
||||
if since != 0 && left_count <= since {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -535,125 +547,176 @@ async fn handle_left_room(
|
||||
|
||||
pin_mut!(is_not_found, is_disabled, is_banned);
|
||||
if is_not_found.or(is_disabled).or(is_banned).await {
|
||||
// This is just a rejected invite, not a room we know
|
||||
// Insert a leave event anyways for the client
|
||||
// For rejected invites, deleted, missing, or broken room state this is the last
|
||||
// resort to convey a the minimum of information to the client.
|
||||
let event = PduEvent {
|
||||
event_id: EventId::new(services.globals.server_name()),
|
||||
sender: sender_user.to_owned(),
|
||||
origin: None,
|
||||
origin_server_ts: utils::millis_since_unix_epoch().try_into()?,
|
||||
kind: RoomMember,
|
||||
content: serde_json::from_str(r#"{"membership":"leave"}"#)?,
|
||||
state_key: Some(sender_user.as_str().into()),
|
||||
unsigned: None,
|
||||
sender: sender_user.to_owned(),
|
||||
content: serde_json::from_str(r#"{"membership":"leave"}"#)?,
|
||||
// The following keys are dropped on conversion
|
||||
room_id: room_id.clone(),
|
||||
prev_events: Default::default(),
|
||||
auth_events: Default::default(),
|
||||
depth: uint!(1),
|
||||
origin: None,
|
||||
unsigned: None,
|
||||
redacts: None,
|
||||
hashes: EventHash::default(),
|
||||
auth_events: Default::default(),
|
||||
prev_events: Default::default(),
|
||||
signatures: None,
|
||||
};
|
||||
|
||||
return Ok(Some(LeftRoom {
|
||||
account_data: RoomAccountData { events: Vec::new() },
|
||||
account_data: RoomAccountData::default(),
|
||||
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
|
||||
timeline: Timeline {
|
||||
limited: false,
|
||||
prev_batch: Some(next_batch.to_string()),
|
||||
events: Vec::new(),
|
||||
events: Default::default(),
|
||||
prev_batch: Some(left_count.to_string()),
|
||||
},
|
||||
state: RoomState::Before(StateEvents { events: vec![event.into_format()] }),
|
||||
}));
|
||||
}
|
||||
|
||||
let mut left_state_events = Vec::new();
|
||||
load_left_room(services, sender_user, room_id, since, left_count, full_state, filter).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "load", level = "debug", skip_all)]
|
||||
async fn load_left_room(
|
||||
services: &Services,
|
||||
sender_user: &UserId,
|
||||
room_id: &RoomId,
|
||||
since: u64,
|
||||
left_count: u64,
|
||||
full_state: bool,
|
||||
filter: &FilterDefinition,
|
||||
) -> Result<Option<LeftRoom>> {
|
||||
let initial = since == 0;
|
||||
let timeline_limit: usize = filter
|
||||
.room
|
||||
.timeline
|
||||
.limit
|
||||
.map(TryInto::try_into)
|
||||
.map_expect("UInt to usize")
|
||||
.unwrap_or(10)
|
||||
.min(100);
|
||||
|
||||
let (timeline_pdus, limited, _) = load_timeline(
|
||||
services,
|
||||
sender_user,
|
||||
room_id,
|
||||
PduCount::Normal(since),
|
||||
Some(PduCount::Normal(left_count)),
|
||||
timeline_limit.max(1),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let since_shortstatehash = services
|
||||
.timeline
|
||||
.prev_shortstatehash(room_id, PduCount::Normal(since).saturating_add(1))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let since_state_ids: HashMap<_, OwnedEventId> = since_shortstatehash
|
||||
.map(|since_shortstatehash| {
|
||||
let horizon_shortstatehash: OptionFuture<_> = timeline_pdus
|
||||
.first()
|
||||
.map(at!(0))
|
||||
.map(|count| {
|
||||
services
|
||||
.state_accessor
|
||||
.state_full_ids(since_shortstatehash)
|
||||
.timeline
|
||||
.get_shortstatehash(room_id, count)
|
||||
.inspect_err(inspect_debug_log)
|
||||
.ok()
|
||||
})
|
||||
.into();
|
||||
|
||||
let left_shortstatehash = services
|
||||
.timeline
|
||||
.get_shortstatehash(room_id, PduCount::Normal(left_count))
|
||||
.inspect_err(inspect_debug_log)
|
||||
.or_else(|_| services.state.get_room_shortstatehash(room_id))
|
||||
.map_err(|_| err!(Database(error!("Room {room_id} has no state"))));
|
||||
|
||||
let (since_shortstatehash, horizon_shortstatehash, left_shortstatehash) =
|
||||
join3(since_shortstatehash, horizon_shortstatehash, left_shortstatehash)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
let StateChanges { state_events, .. } = calculate_state_changes(
|
||||
services,
|
||||
sender_user,
|
||||
room_id,
|
||||
full_state || initial,
|
||||
since_shortstatehash,
|
||||
horizon_shortstatehash.flatten(),
|
||||
left_shortstatehash?,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
let is_sender_membership = |event: &PduEvent| {
|
||||
*event.kind() == RoomMember && event.state_key() == Some(sender_user.as_str())
|
||||
};
|
||||
|
||||
let timeline_sender_member = timeline_limit
|
||||
.eq(&0)
|
||||
.then(|| timeline_pdus.last().map(ref_at!(1)).cloned())
|
||||
.into_iter()
|
||||
.flat_map(Option::into_iter);
|
||||
|
||||
let state_events = state_events
|
||||
.into_iter()
|
||||
.filter(|pdu| filter.room.state.matches(pdu))
|
||||
.filter(|pdu| timeline_limit > 0 || !is_sender_membership(pdu))
|
||||
.chain(timeline_sender_member)
|
||||
.map(Event::into_format)
|
||||
.collect();
|
||||
|
||||
let left_prev_batch = timeline_limit
|
||||
.eq(&0)
|
||||
.then_some(left_count)
|
||||
.map(PduCount::Normal);
|
||||
|
||||
let prev_batch = timeline_pdus
|
||||
.first()
|
||||
.filter(|_| timeline_limit > 0)
|
||||
.map(at!(0))
|
||||
.or(left_prev_batch)
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
let timeline_events = timeline_pdus
|
||||
.into_iter()
|
||||
.stream()
|
||||
.flatten()
|
||||
.collect()
|
||||
.wide_filter_map(|item| ignored_filter(services, item, sender_user))
|
||||
.map(at!(1))
|
||||
.ready_filter(|pdu| filter.room.timeline.matches(pdu))
|
||||
.take(timeline_limit)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let account_data_events = services
|
||||
.account_data
|
||||
.changes_since(Some(room_id), sender_user, since, None)
|
||||
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
|
||||
.collect();
|
||||
|
||||
let (account_data_events, timeline_events) = join(account_data_events, timeline_events)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
let Ok(left_event_id): Result<OwnedEventId> = services
|
||||
.state_accessor
|
||||
.room_state_get_id(room_id, &StateEventType::RoomMember, sender_user.as_str())
|
||||
.await
|
||||
else {
|
||||
warn!("Left {room_id} but no left state event");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let Ok(left_shortstatehash) = services
|
||||
.state
|
||||
.pdu_shortstatehash(&left_event_id)
|
||||
.await
|
||||
else {
|
||||
warn!(event_id = %left_event_id, "Leave event has no state in {room_id}");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut left_state_ids: HashMap<_, _> = services
|
||||
.state_accessor
|
||||
.state_full_ids(left_shortstatehash)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let leave_shortstatekey = services
|
||||
.short
|
||||
.get_or_create_shortstatekey(&StateEventType::RoomMember, sender_user.as_str())
|
||||
.await;
|
||||
|
||||
left_state_ids.insert(leave_shortstatekey, left_event_id);
|
||||
|
||||
for (shortstatekey, event_id) in left_state_ids {
|
||||
if full_state || since_state_ids.get(&shortstatekey) != Some(&event_id) {
|
||||
let (event_type, state_key) = services
|
||||
.short
|
||||
.get_statekey_from_short(shortstatekey)
|
||||
.await?;
|
||||
|
||||
if filter.room.state.lazy_load_options.is_enabled()
|
||||
&& event_type == StateEventType::RoomMember
|
||||
&& !full_state
|
||||
&& state_key
|
||||
.as_str()
|
||||
.try_into()
|
||||
.is_ok_and(|user_id: &UserId| sender_user != user_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(pdu) = services.timeline.get_pdu(&event_id).await else {
|
||||
error!("Pdu in state not found: {event_id}");
|
||||
continue;
|
||||
};
|
||||
|
||||
left_state_events.push(pdu.into_format());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(LeftRoom {
|
||||
account_data: RoomAccountData { events: Vec::new() },
|
||||
account_data: RoomAccountData { events: account_data_events },
|
||||
state: RoomState::Before(StateEvents { events: state_events }),
|
||||
timeline: Timeline {
|
||||
// TODO: support left timeline events so we dont need to set limited to true
|
||||
limited: true,
|
||||
prev_batch: Some(next_batch.to_string()),
|
||||
events: Vec::new(), // and so we dont need to set this to empty vec
|
||||
prev_batch,
|
||||
limited: limited || timeline_limit == 0,
|
||||
events: timeline_events
|
||||
.into_iter()
|
||||
.map(Event::into_format)
|
||||
.collect(),
|
||||
},
|
||||
state: RoomState::Before(StateEvents { events: left_state_events }),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -804,8 +867,9 @@ async fn load_joined_room(
|
||||
.is_empty()
|
||||
.then(|| {
|
||||
services
|
||||
.user
|
||||
.pusher
|
||||
.last_notification_read(sender_user, room_id)
|
||||
.ok()
|
||||
})
|
||||
.into();
|
||||
|
||||
@@ -866,13 +930,14 @@ async fn load_joined_room(
|
||||
.map(Into::into)
|
||||
});
|
||||
|
||||
let send_notification_counts =
|
||||
last_notification_read.is_none_or(|last_count| last_count.gt(&since));
|
||||
let send_notification_counts = last_notification_read
|
||||
.flatten()
|
||||
.is_none_or(|last_count| last_count.gt(&since));
|
||||
|
||||
let notification_count: OptionFuture<_> = send_notification_counts
|
||||
.then(|| {
|
||||
services
|
||||
.user
|
||||
.pusher
|
||||
.notification_count(sender_user, room_id)
|
||||
.map(TryInto::try_into)
|
||||
.unwrap_or(uint!(0))
|
||||
@@ -882,7 +947,7 @@ async fn load_joined_room(
|
||||
let highlight_count: OptionFuture<_> = send_notification_counts
|
||||
.then(|| {
|
||||
services
|
||||
.user
|
||||
.pusher
|
||||
.highlight_count(sender_user, room_id)
|
||||
.map(TryInto::try_into)
|
||||
.unwrap_or(uint!(0))
|
||||
|
||||
+96
-248
@@ -1,50 +1,35 @@
|
||||
mod account_data;
|
||||
mod e2ee;
|
||||
mod extensions;
|
||||
mod filter;
|
||||
mod receipts;
|
||||
mod room;
|
||||
mod rooms;
|
||||
mod selector;
|
||||
mod to_device;
|
||||
mod typing;
|
||||
|
||||
use std::{collections::BTreeMap, fmt::Debug, time::Duration};
|
||||
|
||||
use axum::extract::State;
|
||||
use futures::{
|
||||
FutureExt, TryFutureExt, TryStreamExt,
|
||||
future::{OptionFuture, join, join5, try_join},
|
||||
FutureExt, TryFutureExt,
|
||||
future::{join, try_join},
|
||||
};
|
||||
use ruma::{
|
||||
DeviceId, OwnedRoomId, RoomId, UserId,
|
||||
api::client::sync::sync_events::v5::{
|
||||
ListId, Request, Response, request::ExtensionRoomConfig, response,
|
||||
},
|
||||
DeviceId, OwnedRoomId, UserId,
|
||||
api::client::sync::sync_events::v5::{ListId, Request, Response, response},
|
||||
events::room::member::MembershipState,
|
||||
};
|
||||
use tokio::time::{Instant, timeout_at};
|
||||
use tuwunel_core::{
|
||||
Result, apply, at,
|
||||
Err, Result, debug,
|
||||
debug::INFO_SPAN_LEVEL,
|
||||
err,
|
||||
debug_warn,
|
||||
error::inspect_log,
|
||||
extract_variant,
|
||||
smallvec::SmallVec,
|
||||
trace,
|
||||
utils::{
|
||||
BoolExt, IterStream, TryFutureExtExt,
|
||||
result::FlatOk,
|
||||
stream::{TryBroadbandExt, TryReadyExt},
|
||||
},
|
||||
utils::{TryFutureExtExt, result::FlatOk},
|
||||
};
|
||||
use tuwunel_service::{
|
||||
Services,
|
||||
sync::{Connection, into_connection_key},
|
||||
};
|
||||
|
||||
use self::{
|
||||
filter::{filter_room, filter_room_meta},
|
||||
selector::selector,
|
||||
};
|
||||
use super::share_encrypted_room;
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -53,7 +38,6 @@ struct SyncInfo<'a> {
|
||||
services: &'a Services,
|
||||
sender_user: &'a UserId,
|
||||
sender_device: &'a DeviceId,
|
||||
request: &'a Request,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -87,63 +71,77 @@ struct WindowRoom {
|
||||
user_id = %body.sender_user().localpart(),
|
||||
device_id = %body.sender_device(),
|
||||
conn_id = ?body.body.conn_id.clone().unwrap_or_default(),
|
||||
since = ?body.body.pos.clone().or_else(|| body.body.pos_qrs_.clone()).unwrap_or_default(),
|
||||
since = ?body.body.pos.clone().unwrap_or_default(),
|
||||
)
|
||||
)]
|
||||
pub(crate) async fn sync_events_v5_route(
|
||||
State(ref services): State<crate::State>,
|
||||
body: Ruma<Request>,
|
||||
) -> Result<Response> {
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
let request = &body.body;
|
||||
let since = request
|
||||
.pos
|
||||
.as_ref()
|
||||
.or(request.pos_qrs_.as_ref())
|
||||
.and_then(|string| string.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let (sender_user, sender_device) = body.sender();
|
||||
let timeout = request
|
||||
.timeout
|
||||
.as_ref()
|
||||
.map(Duration::as_millis)
|
||||
.map(TryInto::try_into)
|
||||
.flat_ok()
|
||||
.map(|timeout: u64| {
|
||||
timeout
|
||||
.max(services.config.client_sync_timeout_min)
|
||||
.min(services.config.client_sync_timeout_max)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let conn_key = into_connection_key(sender_user, sender_device, request.conn_id.as_deref());
|
||||
let conn_val = since
|
||||
.ne(&0)
|
||||
.then(|| services.sync.find_connection(&conn_key))
|
||||
.unwrap_or_else(|| Ok(services.sync.init_connection(&conn_key)))
|
||||
.map_err(|_| err!(Request(UnknownPos("Connection lost; restarting sync stream."))))?;
|
||||
let conn_val = services
|
||||
.sync
|
||||
.load_or_init_connection(&conn_key)
|
||||
.await;
|
||||
|
||||
let conn = conn_val.lock();
|
||||
let ping_presence = services
|
||||
.presence
|
||||
.maybe_ping_presence(sender_user, &request.set_presence)
|
||||
.maybe_ping_presence(sender_user, Some(sender_device), &request.set_presence)
|
||||
.inspect_err(inspect_log)
|
||||
.ok();
|
||||
|
||||
let (mut conn, _) = join(conn, ping_presence).await;
|
||||
|
||||
if since != 0 && conn.next_batch == 0 {
|
||||
return Err!(Request(UnknownPos(warn!("Connection lost; restarting sync stream."))));
|
||||
}
|
||||
|
||||
if since == 0 {
|
||||
*conn = Connection::default();
|
||||
conn.store(&services.sync, &conn_key);
|
||||
debug_warn!(?conn_key, "Client cleared cache and reloaded.");
|
||||
}
|
||||
|
||||
let advancing = since == conn.next_batch;
|
||||
let retarding = since != 0 && since <= conn.globalsince;
|
||||
if !advancing && !retarding {
|
||||
return Err!(Request(UnknownPos(warn!(
|
||||
"Requesting unknown or invalid stream position."
|
||||
))));
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
advancing || retarding,
|
||||
"Request should either be advancing or replaying the since token."
|
||||
);
|
||||
|
||||
// Update parameters regardless of replay or advance
|
||||
conn.next_batch = services.globals.wait_pending().await?;
|
||||
conn.globalsince = since.min(conn.next_batch);
|
||||
conn.update_cache(request);
|
||||
conn.globalsince = since;
|
||||
conn.next_batch = since;
|
||||
|
||||
let sync_info = SyncInfo {
|
||||
services,
|
||||
sender_user,
|
||||
sender_device,
|
||||
request,
|
||||
};
|
||||
|
||||
let timeout = request
|
||||
.timeout
|
||||
.as_ref()
|
||||
.or(request.timeout_qrs_.as_ref())
|
||||
.map(Duration::as_millis)
|
||||
.map(TryInto::try_into)
|
||||
.flat_ok()
|
||||
.unwrap_or(services.config.client_sync_timeout_default)
|
||||
.max(services.config.client_sync_timeout_min)
|
||||
.min(services.config.client_sync_timeout_max);
|
||||
|
||||
let stop_at = Instant::now()
|
||||
.checked_add(Duration::from_millis(timeout))
|
||||
.expect("configuration must limit maximum timeout");
|
||||
conn.update_rooms_prologue(retarding.then_some(since));
|
||||
|
||||
let mut response = Response {
|
||||
txn_id: request.txn_id.clone(),
|
||||
@@ -153,54 +151,66 @@ pub(crate) async fn sync_events_v5_route(
|
||||
extensions: Default::default(),
|
||||
};
|
||||
|
||||
loop {
|
||||
let window;
|
||||
(window, response.lists) = selector(&mut conn, sync_info).boxed().await;
|
||||
let watch_rooms = window.keys().map(AsRef::as_ref).stream();
|
||||
let watchers = services
|
||||
.sync
|
||||
.watch(sender_user, sender_device, watch_rooms);
|
||||
let stop_at = Instant::now()
|
||||
.checked_add(Duration::from_millis(timeout))
|
||||
.expect("configuration must limit maximum timeout");
|
||||
|
||||
conn.next_batch = services.globals.wait_pending().await?;
|
||||
let sync_info = SyncInfo { services, sender_user, sender_device };
|
||||
loop {
|
||||
debug_assert!(
|
||||
conn.globalsince <= conn.next_batch,
|
||||
"next_batch should not be greater than since."
|
||||
"since should not be greater than next_batch."
|
||||
);
|
||||
|
||||
if conn.globalsince < conn.next_batch {
|
||||
let rooms =
|
||||
handle_rooms(sync_info, &conn, &window).map_ok(|rooms| response.rooms = rooms);
|
||||
let window;
|
||||
let watchers = services.sync.watch(
|
||||
sender_user,
|
||||
sender_device,
|
||||
services.state_cache.rooms_joined(sender_user),
|
||||
);
|
||||
|
||||
let extensions = handle_extensions(sync_info, &conn, &window)
|
||||
.map_ok(|extensions| response.extensions = extensions);
|
||||
conn.next_batch = services.globals.wait_pending().await?;
|
||||
(window, response.lists) = selector::selector(&mut conn, sync_info)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
if conn.globalsince < conn.next_batch {
|
||||
let rooms = rooms::handle(sync_info, &conn, &window)
|
||||
.map_ok(|response_rooms| response.rooms = response_rooms);
|
||||
|
||||
let extensions = extensions::handle(sync_info, &conn, &window)
|
||||
.map_ok(|response_extensions| response.extensions = response_extensions);
|
||||
|
||||
try_join(rooms, extensions).boxed().await?;
|
||||
|
||||
for room_id in window.keys() {
|
||||
conn.rooms
|
||||
.entry(room_id.into())
|
||||
.or_default()
|
||||
.roomsince = conn.next_batch;
|
||||
}
|
||||
conn.update_rooms_epilogue(window.keys().map(AsRef::as_ref));
|
||||
|
||||
if !is_empty_response(&response) {
|
||||
response.pos = conn.next_batch.to_string().into();
|
||||
trace!(conn.globalsince, conn.next_batch, "response {response:?}");
|
||||
conn.store(&services.sync, &conn_key);
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
if timeout_at(stop_at, watchers).await.is_err() || services.server.is_stopping() {
|
||||
if timeout == 0
|
||||
|| services.server.is_stopping()
|
||||
|| timeout_at(stop_at, watchers)
|
||||
.boxed()
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
response.pos = conn.next_batch.to_string().into();
|
||||
trace!(conn.globalsince, conn.next_batch, "timeout; empty response");
|
||||
trace!(conn.globalsince, conn.next_batch, "timeout; empty response {response:?}");
|
||||
conn.store(&services.sync, &conn_key);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
trace!(
|
||||
conn.globalsince,
|
||||
last_batch = ?conn.next_batch,
|
||||
count = ?services.globals.pending_count(),
|
||||
stop_at = ?stop_at,
|
||||
debug!(
|
||||
?timeout,
|
||||
last_since = conn.globalsince,
|
||||
last_batch = conn.next_batch,
|
||||
pend_count = ?services.globals.pending_count(),
|
||||
"notified by watcher"
|
||||
);
|
||||
|
||||
@@ -209,167 +219,5 @@ pub(crate) async fn sync_events_v5_route(
|
||||
}
|
||||
|
||||
fn is_empty_response(response: &Response) -> bool {
|
||||
response.extensions.is_empty()
|
||||
&& response
|
||||
.rooms
|
||||
.iter()
|
||||
.all(|(_, room)| room.timeline.is_empty() && room.invite_state.is_none())
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "rooms",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
next_batch = conn.next_batch,
|
||||
window = window.len(),
|
||||
)
|
||||
)]
|
||||
async fn handle_rooms(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
window: &Window,
|
||||
) -> Result<BTreeMap<OwnedRoomId, response::Room>> {
|
||||
window
|
||||
.iter()
|
||||
.try_stream()
|
||||
.broad_and_then(async |(room_id, room)| {
|
||||
room::handle(sync_info, conn, room)
|
||||
.map_ok(|room| (room_id, room))
|
||||
.await
|
||||
})
|
||||
.ready_try_filter_map(|(room_id, room)| Ok(room.map(|room| (room_id, room))))
|
||||
.map_ok(|(room_id, room)| (room_id.to_owned(), room))
|
||||
.try_collect()
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "extensions",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
next_batch = conn.next_batch,
|
||||
window = window.len(),
|
||||
rooms = conn.rooms.len(),
|
||||
subs = conn.subscriptions.len(),
|
||||
)
|
||||
)]
|
||||
async fn handle_extensions(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
window: &Window,
|
||||
) -> Result<response::Extensions> {
|
||||
let SyncInfo { .. } = sync_info;
|
||||
|
||||
let account_data: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.account_data
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| account_data::collect(sync_info, conn, window))
|
||||
.into();
|
||||
|
||||
let receipts: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.receipts
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| receipts::collect(sync_info, conn, window))
|
||||
.into();
|
||||
|
||||
let typing: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.typing
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| typing::collect(sync_info, conn, window))
|
||||
.into();
|
||||
|
||||
let to_device: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.to_device
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| to_device::collect(sync_info, conn))
|
||||
.into();
|
||||
|
||||
let e2ee: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.e2ee
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| e2ee::collect(sync_info, conn))
|
||||
.into();
|
||||
|
||||
let (account_data, receipts, typing, to_device, e2ee) =
|
||||
join5(account_data, receipts, typing, to_device, e2ee)
|
||||
.map(apply!(5, |t: Option<_>| t.unwrap_or(Ok(Default::default()))))
|
||||
.await;
|
||||
|
||||
Ok(response::Extensions {
|
||||
account_data: account_data?,
|
||||
receipts: receipts?,
|
||||
typing: typing?,
|
||||
to_device: to_device?,
|
||||
e2ee: e2ee?,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "selector",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(?implicit, ?explicit),
|
||||
)]
|
||||
fn extension_rooms_selector<'a, ListIter, SubsIter>(
|
||||
SyncInfo { .. }: SyncInfo<'a>,
|
||||
conn: &'a Connection,
|
||||
window: &'a Window,
|
||||
implicit: Option<ListIter>,
|
||||
explicit: Option<SubsIter>,
|
||||
) -> impl Iterator<Item = &'a RoomId> + Send + Sync + 'a
|
||||
where
|
||||
ListIter: Iterator<Item = &'a ListId> + Clone + Debug + Send + Sync + 'a,
|
||||
SubsIter: Iterator<Item = &'a ExtensionRoomConfig> + Clone + Debug + Send + Sync + 'a,
|
||||
{
|
||||
let has_all_subscribed = explicit
|
||||
.clone()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|erc| matches!(erc, ExtensionRoomConfig::AllSubscribed));
|
||||
|
||||
let all_subscribed = has_all_subscribed
|
||||
.then(|| conn.subscriptions.keys())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(AsRef::as_ref);
|
||||
|
||||
let rooms_explicit = has_all_subscribed
|
||||
.is_false()
|
||||
.then(move || {
|
||||
explicit
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
|
||||
.map(AsRef::as_ref)
|
||||
})
|
||||
.into_iter()
|
||||
.flatten();
|
||||
|
||||
let rooms_selected = window
|
||||
.iter()
|
||||
.filter(move |(_, room)| {
|
||||
implicit.as_ref().is_none_or(|lists| {
|
||||
lists
|
||||
.clone()
|
||||
.any(|list| room.lists.contains(list))
|
||||
})
|
||||
})
|
||||
.map(at!(0))
|
||||
.map(AsRef::as_ref);
|
||||
|
||||
all_subscribed
|
||||
.chain(rooms_explicit)
|
||||
.chain(rooms_selected)
|
||||
response.extensions.is_empty() && response.rooms.is_empty()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
mod account_data;
|
||||
mod e2ee;
|
||||
mod receipts;
|
||||
mod to_device;
|
||||
mod typing;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use futures::{
|
||||
FutureExt,
|
||||
future::{OptionFuture, join5},
|
||||
};
|
||||
use ruma::{
|
||||
RoomId,
|
||||
api::client::sync::sync_events::v5::{ListId, request::ExtensionRoomConfig, response},
|
||||
};
|
||||
use tuwunel_core::{Result, apply, at, extract_variant, utils::BoolExt};
|
||||
use tuwunel_service::sync::Connection;
|
||||
|
||||
use super::{SyncInfo, Window, share_encrypted_room};
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "extensions",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
next_batch = conn.next_batch,
|
||||
window = window.len(),
|
||||
rooms = conn.rooms.len(),
|
||||
subs = conn.subscriptions.len(),
|
||||
)
|
||||
)]
|
||||
pub(super) async fn handle(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
window: &Window,
|
||||
) -> Result<response::Extensions> {
|
||||
let SyncInfo { .. } = sync_info;
|
||||
|
||||
let account_data: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.account_data
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| account_data::collect(sync_info, conn, window))
|
||||
.into();
|
||||
|
||||
let receipts: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.receipts
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| receipts::collect(sync_info, conn, window))
|
||||
.into();
|
||||
|
||||
let typing: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.typing
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| typing::collect(sync_info, conn, window))
|
||||
.into();
|
||||
|
||||
let to_device: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.to_device
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| to_device::collect(sync_info, conn))
|
||||
.into();
|
||||
|
||||
let e2ee: OptionFuture<_> = conn
|
||||
.extensions
|
||||
.e2ee
|
||||
.enabled
|
||||
.unwrap_or(false)
|
||||
.then(|| e2ee::collect(sync_info, conn))
|
||||
.into();
|
||||
|
||||
let (account_data, receipts, typing, to_device, e2ee) =
|
||||
join5(account_data, receipts, typing, to_device, e2ee)
|
||||
.map(apply!(5, |t: Option<_>| t.unwrap_or(Ok(Default::default()))))
|
||||
.await;
|
||||
|
||||
Ok(response::Extensions {
|
||||
account_data: account_data?,
|
||||
receipts: receipts?,
|
||||
typing: typing?,
|
||||
to_device: to_device?,
|
||||
e2ee: e2ee?,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "selector",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(?implicit, ?explicit),
|
||||
)]
|
||||
fn selector<'a, ListIter, SubsIter>(
|
||||
SyncInfo { .. }: SyncInfo<'a>,
|
||||
conn: &'a Connection,
|
||||
window: &'a Window,
|
||||
implicit: Option<ListIter>,
|
||||
explicit: Option<SubsIter>,
|
||||
) -> impl Iterator<Item = &'a RoomId> + Send + Sync + 'a
|
||||
where
|
||||
ListIter: Iterator<Item = &'a ListId> + Clone + Debug + Send + Sync + 'a,
|
||||
SubsIter: Iterator<Item = &'a ExtensionRoomConfig> + Clone + Debug + Send + Sync + 'a,
|
||||
{
|
||||
let has_all_subscribed = explicit
|
||||
.clone()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.any(|erc| matches!(erc, ExtensionRoomConfig::AllSubscribed));
|
||||
|
||||
let all_subscribed = has_all_subscribed
|
||||
.then(|| conn.subscriptions.keys())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(AsRef::as_ref);
|
||||
|
||||
let rooms_explicit = has_all_subscribed
|
||||
.is_false()
|
||||
.then(move || {
|
||||
explicit
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|erc| extract_variant!(erc, ExtensionRoomConfig::Room))
|
||||
.map(AsRef::as_ref)
|
||||
})
|
||||
.into_iter()
|
||||
.flatten();
|
||||
|
||||
let rooms_selected = window
|
||||
.iter()
|
||||
.filter(move |(_, room)| {
|
||||
implicit.as_ref().is_none_or(|lists| {
|
||||
lists
|
||||
.clone()
|
||||
.any(|list| room.lists.contains(list))
|
||||
})
|
||||
})
|
||||
.map(at!(0))
|
||||
.map(AsRef::as_ref);
|
||||
|
||||
all_subscribed
|
||||
.chain(rooms_explicit)
|
||||
.chain(rooms_selected)
|
||||
}
|
||||
+3
-3
@@ -6,9 +6,9 @@
|
||||
};
|
||||
use tuwunel_service::sync::Room;
|
||||
|
||||
use super::{Connection, SyncInfo, Window, extension_rooms_selector};
|
||||
use super::{Connection, SyncInfo, Window, selector};
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[tracing::instrument(name = "account_data", level = "trace", skip_all)]
|
||||
pub(super) async fn collect(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
@@ -30,7 +30,7 @@ pub(super) async fn collect(
|
||||
.as_deref()
|
||||
.map(<[_]>::iter);
|
||||
|
||||
let rooms = extension_rooms_selector(sync_info, conn, window, implicit, explicit)
|
||||
let rooms = selector(sync_info, conn, window, implicit, explicit)
|
||||
.stream()
|
||||
.broad_filter_map(async |room_id| {
|
||||
let &Room { roomsince, .. } = conn.rooms.get(room_id)?;
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
use super::{SyncInfo, share_encrypted_room};
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[tracing::instrument(name = "e2ee", level = "trace", skip_all)]
|
||||
pub(super) async fn collect(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
@@ -94,7 +94,7 @@ pub(super) async fn collect(
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, fields(room_id))]
|
||||
#[tracing::instrument(level = "trace", skip_all, fields(room_id), ret)]
|
||||
async fn collect_room(
|
||||
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
@@ -11,9 +11,9 @@
|
||||
};
|
||||
use tuwunel_service::{rooms::read_receipt::pack_receipts, sync::Room};
|
||||
|
||||
use super::{Connection, SyncInfo, Window, extension_rooms_selector};
|
||||
use super::{Connection, SyncInfo, Window, selector};
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[tracing::instrument(name = "receipts", level = "trace", skip_all)]
|
||||
pub(super) async fn collect(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
@@ -35,7 +35,7 @@ pub(super) async fn collect(
|
||||
.as_deref()
|
||||
.map(<[_]>::iter);
|
||||
|
||||
let rooms = extension_rooms_selector(sync_info, conn, window, implicit, explicit)
|
||||
let rooms = selector(sync_info, conn, window, implicit, explicit)
|
||||
.stream()
|
||||
.broad_filter_map(|room_id| collect_room(sync_info, conn, window, room_id))
|
||||
.collect()
|
||||
@@ -44,7 +44,7 @@ pub(super) async fn collect(
|
||||
Ok(response::Receipts { rooms })
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, fields(room_id))]
|
||||
#[tracing::instrument(level = "trace", skip_all, fields(room_id), ret)]
|
||||
async fn collect_room(
|
||||
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
+3
-2
@@ -1,10 +1,10 @@
|
||||
use futures::StreamExt;
|
||||
use ruma::api::client::sync::sync_events::v5::response;
|
||||
use tuwunel_core::{self, Result};
|
||||
use tuwunel_core::{self, Result, at};
|
||||
|
||||
use super::{Connection, SyncInfo};
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[tracing::instrument(name = "to_device", level = "trace", skip_all, ret)]
|
||||
pub(super) async fn collect(
|
||||
SyncInfo { services, sender_user, sender_device, .. }: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
@@ -17,6 +17,7 @@ pub(super) async fn collect(
|
||||
let events: Vec<_> = services
|
||||
.users
|
||||
.get_to_device_events(sender_user, sender_device, None, Some(conn.next_batch))
|
||||
.map(at!(1))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
utils::{IterStream, ReadyExt},
|
||||
};
|
||||
|
||||
use super::{Connection, SyncInfo, Window, extension_rooms_selector};
|
||||
use super::{Connection, SyncInfo, Window, selector};
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
#[tracing::instrument(name = "typing", level = "trace", skip_all, ret)]
|
||||
pub(super) async fn collect(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
@@ -37,7 +37,7 @@ pub(super) async fn collect(
|
||||
.as_deref()
|
||||
.map(<[_]>::iter);
|
||||
|
||||
extension_rooms_selector(sync_info, conn, window, implicit, explicit)
|
||||
selector(sync_info, conn, window, implicit, explicit)
|
||||
.stream()
|
||||
.filter_map(async |room_id| {
|
||||
services
|
||||
@@ -7,7 +7,7 @@
|
||||
is_equal_to, is_true,
|
||||
utils::{
|
||||
BoolExt, FutureBoolExt, IterStream, ReadyExt,
|
||||
future::{OptionExt, ReadyEqExt},
|
||||
future::{self, OptionExt, ReadyEqExt},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -115,19 +115,16 @@ pub(super) async fn filter_room(
|
||||
})
|
||||
.into();
|
||||
|
||||
match_encrypted
|
||||
.is_none_or(is_true!())
|
||||
.and3(
|
||||
match_invite.is_none_or(is_true!()),
|
||||
match_direct.is_none_or(is_true!()),
|
||||
match_direct_member.is_none_or(is_true!()),
|
||||
)
|
||||
.and3(
|
||||
match_space_child.is_none_or(is_true!()),
|
||||
match_room_type.is_none_or(is_true!()),
|
||||
match_room_tag.is_none_or(is_true!()),
|
||||
)
|
||||
.await
|
||||
future::and7(
|
||||
match_invite.is_none_or(is_true!()),
|
||||
match_encrypted.is_none_or(is_true!()),
|
||||
match_direct.is_none_or(is_true!()),
|
||||
match_direct_member.is_none_or(is_true!()),
|
||||
match_space_child.is_none_or(is_true!()),
|
||||
match_room_type.is_none_or(is_true!()),
|
||||
match_room_tag.is_none_or(is_true!()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "filter_meta", level = "trace", skip_all)]
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::{cmp::Ordering, collections::HashSet};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, HashSet},
|
||||
};
|
||||
|
||||
use futures::{
|
||||
FutureExt, StreamExt, TryFutureExt, TryStreamExt,
|
||||
future::{OptionFuture, join, join3, join4},
|
||||
};
|
||||
use ruma::{
|
||||
JsOption, MxcUri, OwnedMxcUri, RoomId, UInt, UserId,
|
||||
JsOption, MxcUri, OwnedMxcUri, OwnedRoomId, RoomId, UserId,
|
||||
api::client::sync::sync_events::{
|
||||
UnreadNotificationsCount,
|
||||
v5::{DisplayName, response},
|
||||
v5::{DisplayName, response, response::Heroes},
|
||||
},
|
||||
events::{
|
||||
StateEventType,
|
||||
@@ -19,7 +22,7 @@
|
||||
},
|
||||
};
|
||||
use tuwunel_core::{
|
||||
Result, at, debug_error, err, is_equal_to,
|
||||
Result, at, err, error, is_equal_to,
|
||||
matrix::{Event, StateKey, pdu::PduCount},
|
||||
ref_at,
|
||||
utils::{
|
||||
@@ -29,12 +32,41 @@
|
||||
};
|
||||
use tuwunel_service::{Services, sync::Room};
|
||||
|
||||
use super::{super::load_timeline, Connection, SyncInfo, WindowRoom};
|
||||
use super::{super::load_timeline, Connection, SyncInfo, Window, WindowRoom};
|
||||
use crate::client::ignored_filter;
|
||||
|
||||
static DEFAULT_BUMP_TYPES: [TimelineEventType; 6] =
|
||||
[CallInvite, PollStart, Beacon, RoomEncrypted, RoomMessage, Sticker];
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "rooms",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(
|
||||
next_batch = conn.next_batch,
|
||||
window = window.len(),
|
||||
)
|
||||
)]
|
||||
pub(super) async fn handle(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
window: &Window,
|
||||
) -> Result<BTreeMap<OwnedRoomId, response::Room>> {
|
||||
window
|
||||
.iter()
|
||||
.stream()
|
||||
.broad_filter_map(async |(room_id, room)| {
|
||||
handle_room(sync_info, conn, room)
|
||||
.map_ok(move |room| (room_id.clone(), room))
|
||||
.inspect_err(|e| error!(?room_id, "sync handler: {e:?}"))
|
||||
.await
|
||||
.ok()
|
||||
})
|
||||
.collect()
|
||||
.map(Ok)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "room",
|
||||
level = "debug",
|
||||
@@ -42,18 +74,47 @@
|
||||
fields(room_id, roomsince)
|
||||
)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn handle(
|
||||
async fn handle_room(
|
||||
SyncInfo { services, sender_user, .. }: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
WindowRoom { lists, membership, room_id, .. }: &WindowRoom,
|
||||
) -> Result<Option<response::Room>> {
|
||||
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted");
|
||||
WindowRoom {
|
||||
lists, membership, room_id, last_count, ..
|
||||
}: &WindowRoom,
|
||||
) -> Result<response::Room> {
|
||||
debug_assert!(
|
||||
DEFAULT_BUMP_TYPES.is_sorted(),
|
||||
"DEFAULT_BUMP_TYPES must be sorted for binary search"
|
||||
);
|
||||
|
||||
let &Room { roomsince } = conn
|
||||
let &Room { roomsince, .. } = conn
|
||||
.rooms
|
||||
.get(room_id)
|
||||
.ok_or_else(|| err!("Missing connection state for {room_id}"))?;
|
||||
|
||||
debug_assert!(
|
||||
*last_count > roomsince || *last_count == 0 || roomsince == 0,
|
||||
"Stale room shouldn't be in the window"
|
||||
);
|
||||
|
||||
if *membership == Some(MembershipState::Leave) {
|
||||
return Ok(response::Room {
|
||||
initial: roomsince.eq(&0).then_some(true),
|
||||
lists: lists.clone(),
|
||||
membership: membership.clone(),
|
||||
prev_batch: Some(conn.next_batch.to_string().into()),
|
||||
limited: true,
|
||||
required_state: vec![
|
||||
services
|
||||
.state_accessor
|
||||
.room_state_get(room_id, &StateEventType::RoomMember, sender_user.as_str())
|
||||
.map_ok(Event::into_format)
|
||||
.await?,
|
||||
],
|
||||
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let is_invite = *membership == Some(MembershipState::Invite);
|
||||
let default_details = (0_usize, HashSet::new());
|
||||
let (timeline_limit, required_state) = lists
|
||||
@@ -84,41 +145,55 @@ pub(super) async fn handle(
|
||||
})
|
||||
.into();
|
||||
|
||||
let Ok(timeline) = timeline.await.transpose() else {
|
||||
debug_error!(?room_id, "Missing timeline.");
|
||||
return Ok(None);
|
||||
};
|
||||
let (timeline_pdus, limited, _lastcount) = timeline
|
||||
.await
|
||||
.flat_ok()
|
||||
.unwrap_or_else(|| (Vec::new(), true, PduCount::default()));
|
||||
|
||||
let (timeline_pdus, limited, _lastcount) =
|
||||
timeline.unwrap_or_else(|| (Vec::new(), true, PduCount::default()));
|
||||
|
||||
if roomsince != 0 && timeline_pdus.is_empty() && !is_invite {
|
||||
return Ok(None);
|
||||
}
|
||||
let required_state = required_state
|
||||
.into_iter()
|
||||
.filter(|_| !timeline_pdus.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let prev_batch = timeline_pdus
|
||||
.first()
|
||||
.map(at!(0))
|
||||
.map(PduCount::into_unsigned)
|
||||
.or_else(|| roomsince.ne(&0).then_some(roomsince))
|
||||
.as_ref()
|
||||
.map(ToString::to_string);
|
||||
|
||||
let bump_stamp = timeline_pdus
|
||||
.iter()
|
||||
.filter(|(_, pdu)| {
|
||||
if *pdu.event_type() == TimelineEventType::RoomMember {
|
||||
return pdu
|
||||
.state_key()
|
||||
.is_some_and(is_equal_to!(sender_user.as_str()));
|
||||
}
|
||||
|
||||
DEFAULT_BUMP_TYPES
|
||||
.binary_search(pdu.event_type())
|
||||
.is_ok()
|
||||
})
|
||||
.fold(Option::<UInt>::None, |mut bump_stamp, (_, pdu)| {
|
||||
let ts = pdu.origin_server_ts().0;
|
||||
if bump_stamp.is_none_or(|bump_stamp| bump_stamp < ts) {
|
||||
bump_stamp.replace(ts);
|
||||
}
|
||||
.filter(|(_, pdu)| !pdu.is_redacted())
|
||||
.map(at!(0))
|
||||
.map(PduCount::into_signed)
|
||||
.max()
|
||||
.map(TryInto::try_into)
|
||||
.flat_ok();
|
||||
|
||||
bump_stamp
|
||||
});
|
||||
let num_live: OptionFuture<_> = roomsince
|
||||
.ne(&0)
|
||||
.and_is(limited || timeline_pdus.len() >= timeline_limit)
|
||||
.then(|| {
|
||||
services
|
||||
.timeline
|
||||
.pdus(None, room_id, Some(roomsince.into()))
|
||||
.count()
|
||||
.map(TryInto::try_into)
|
||||
.map(Result::ok)
|
||||
})
|
||||
.into();
|
||||
|
||||
let lazy = required_state
|
||||
.iter()
|
||||
@@ -183,14 +258,6 @@ pub(super) async fn handle(
|
||||
})
|
||||
.into();
|
||||
|
||||
let timeline = timeline_pdus
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|item| ignored_filter(services, item.clone(), sender_user))
|
||||
.map(at!(1))
|
||||
.map(Event::into_format)
|
||||
.collect();
|
||||
|
||||
let room_name = services
|
||||
.state_accessor
|
||||
.get_name(room_id)
|
||||
@@ -205,13 +272,13 @@ pub(super) async fn handle(
|
||||
.map(Option::flatten);
|
||||
|
||||
let highlight_count = services
|
||||
.user
|
||||
.pusher
|
||||
.highlight_count(sender_user, room_id)
|
||||
.map(TryInto::try_into)
|
||||
.map(Result::ok);
|
||||
|
||||
let notification_count = services
|
||||
.user
|
||||
.pusher
|
||||
.notification_count(sender_user, room_id)
|
||||
.map(TryInto::try_into)
|
||||
.map(Result::ok);
|
||||
@@ -235,15 +302,27 @@ pub(super) async fn handle(
|
||||
.is_direct(room_id, sender_user)
|
||||
.map(|is_dm| is_dm.then_some(is_dm));
|
||||
|
||||
let last_read_count = services
|
||||
.pusher
|
||||
.last_notification_read(sender_user, room_id);
|
||||
|
||||
let timeline = timeline_pdus
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(|item| ignored_filter(services, item.clone(), sender_user))
|
||||
.map(at!(1))
|
||||
.map(Event::into_format)
|
||||
.collect();
|
||||
|
||||
let meta = join3(room_name, room_avatar, is_dm);
|
||||
let events = join3(timeline, required_state, invite_state);
|
||||
let events = join4(timeline, num_live, required_state, invite_state);
|
||||
let member_counts = join(joined_count, invited_count);
|
||||
let notification_counts = join(highlight_count, notification_count);
|
||||
let notification_counts = join3(highlight_count, notification_count, last_read_count);
|
||||
let (
|
||||
(room_name, room_avatar, is_dm),
|
||||
(timeline, required_state, invite_state),
|
||||
(timeline, num_live, required_state, invite_state),
|
||||
(joined_count, invited_count),
|
||||
(highlight_count, notification_count),
|
||||
(highlight_count, notification_count, _last_notification_read),
|
||||
) = join4(meta, events, member_counts, notification_counts)
|
||||
.boxed()
|
||||
.await;
|
||||
@@ -257,27 +336,25 @@ pub(super) async fn handle(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let num_live = None; // Count events in timeline greater than global sync counter
|
||||
|
||||
Ok(Some(response::Room {
|
||||
initial: Some(roomsince == 0),
|
||||
Ok(response::Room {
|
||||
initial: roomsince.eq(&0).then_some(true),
|
||||
lists: lists.clone(),
|
||||
membership: membership.clone(),
|
||||
name: room_name.or(hero_name),
|
||||
avatar: JsOption::from_option(room_avatar.or(heroes_avatar)),
|
||||
is_dm,
|
||||
heroes,
|
||||
required_state,
|
||||
invite_state: invite_state.flatten(),
|
||||
prev_batch: prev_batch.as_deref().map(Into::into),
|
||||
num_live: num_live.flatten(),
|
||||
limited,
|
||||
timeline,
|
||||
bump_stamp,
|
||||
heroes,
|
||||
num_live,
|
||||
joined_count,
|
||||
invited_count,
|
||||
unread_notifications: UnreadNotificationsCount { highlight_count, notification_count },
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "heroes", level = "trace", skip_all)]
|
||||
@@ -288,9 +365,9 @@ async fn calculate_heroes(
|
||||
room_id: &RoomId,
|
||||
room_name: Option<&DisplayName>,
|
||||
room_avatar: Option<&MxcUri>,
|
||||
) -> Result<(Option<Vec<response::Hero>>, Option<DisplayName>, Option<OwnedMxcUri>)> {
|
||||
) -> Result<(Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>)> {
|
||||
const MAX_HEROES: usize = 5;
|
||||
let heroes: Vec<_> = services
|
||||
let heroes: Heroes = services
|
||||
.state_cache
|
||||
.room_members(room_id)
|
||||
.ready_filter(|&member| member != sender_user)
|
||||
+153
-146
@@ -2,7 +2,7 @@
|
||||
|
||||
use futures::{
|
||||
FutureExt, StreamExt, TryFutureExt,
|
||||
future::{OptionFuture, join3},
|
||||
future::{OptionFuture, join5},
|
||||
};
|
||||
use ruma::{OwnedRoomId, UInt, events::room::member::MembershipState, uint};
|
||||
use tuwunel_core::{
|
||||
@@ -10,8 +10,7 @@
|
||||
matrix::PduCount,
|
||||
trace,
|
||||
utils::{
|
||||
BoolExt,
|
||||
future::TryExtExt,
|
||||
BoolExt, TryFutureExtExt,
|
||||
math::usize_from_ruma,
|
||||
stream::{BroadbandExt, IterStream},
|
||||
},
|
||||
@@ -19,7 +18,8 @@
|
||||
use tuwunel_service::sync::Connection;
|
||||
|
||||
use super::{
|
||||
ListIds, ResponseLists, SyncInfo, Window, WindowRoom, filter_room, filter_room_meta,
|
||||
ListIds, ResponseLists, SyncInfo, Window, WindowRoom,
|
||||
filter::{filter_room, filter_room_meta},
|
||||
};
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
@@ -29,16 +29,13 @@ pub(super) async fn selector(
|
||||
) -> (Window, ResponseLists) {
|
||||
use MembershipState::*;
|
||||
|
||||
let SyncInfo { services, sender_user, request, .. } = sync_info;
|
||||
let SyncInfo { services, sender_user, .. } = sync_info;
|
||||
|
||||
trace!(?request);
|
||||
let mut rooms = services
|
||||
.state_cache
|
||||
.user_memberships(sender_user, Some(&[Join, Invite, Knock]))
|
||||
.map(|(membership, room_id)| (room_id.to_owned(), Some(membership)))
|
||||
.broad_filter_map(|(room_id, membership)| {
|
||||
match_lists_for_room(sync_info, conn, room_id, membership)
|
||||
})
|
||||
.broad_filter_map(|(room_id, membership)| matcher(sync_info, conn, room_id, membership))
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
@@ -48,25 +45,133 @@ pub(super) async fn selector(
|
||||
.enumerate()
|
||||
.for_each(|(i, room)| {
|
||||
room.ranked = i;
|
||||
|
||||
conn.rooms
|
||||
.entry(room.room_id.clone())
|
||||
.or_default();
|
||||
});
|
||||
|
||||
trace!(?rooms);
|
||||
let lists = response_lists(rooms.iter());
|
||||
|
||||
trace!(?lists);
|
||||
let window = select_window(sync_info, conn, rooms.iter(), &lists).await;
|
||||
let window = window(sync_info, conn, rooms.iter(), &lists).await;
|
||||
|
||||
trace!(?window);
|
||||
for room in &rooms {
|
||||
conn.rooms
|
||||
.entry(room.room_id.clone())
|
||||
.or_default();
|
||||
}
|
||||
|
||||
(window, lists)
|
||||
}
|
||||
|
||||
async fn select_window<'a, Rooms>(
|
||||
#[tracing::instrument(
|
||||
name = "matcher",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(?room_id, ?membership)
|
||||
)]
|
||||
async fn matcher(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
room_id: OwnedRoomId,
|
||||
membership: Option<MembershipState>,
|
||||
) -> Option<WindowRoom> {
|
||||
let SyncInfo { services, sender_user, .. } = sync_info;
|
||||
|
||||
let (matched, lists) = conn
|
||||
.lists
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(async |(id, list)| {
|
||||
let filter: OptionFuture<_> = list
|
||||
.filters
|
||||
.clone()
|
||||
.map(async |filters| {
|
||||
filter_room(sync_info, &filters, &room_id, membership.as_ref()).await
|
||||
})
|
||||
.into();
|
||||
|
||||
filter
|
||||
.await
|
||||
.is_none_or(is_true!())
|
||||
.then(|| id.clone())
|
||||
})
|
||||
.collect::<ListIds>()
|
||||
.map(|lists| (lists.is_empty().is_false(), lists))
|
||||
.await;
|
||||
|
||||
let last_notification: OptionFuture<_> = matched
|
||||
.then(|| {
|
||||
services
|
||||
.pusher
|
||||
.last_notification_read(sender_user, &room_id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.into();
|
||||
|
||||
let last_privateread: OptionFuture<_> = matched
|
||||
.then(|| {
|
||||
services
|
||||
.read_receipt
|
||||
.last_privateread_update(sender_user, &room_id)
|
||||
})
|
||||
.into();
|
||||
|
||||
let last_receipt: OptionFuture<_> = matched
|
||||
.then(|| {
|
||||
services
|
||||
.read_receipt
|
||||
.last_receipt_count(&room_id, sender_user.into(), None)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.into();
|
||||
|
||||
let last_account: OptionFuture<_> = matched
|
||||
.then(|| {
|
||||
services
|
||||
.account_data
|
||||
.last_count(Some(room_id.as_ref()), sender_user, Some(conn.next_batch))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.into();
|
||||
|
||||
let last_timeline: OptionFuture<_> = matched
|
||||
.then(|| {
|
||||
services
|
||||
.timeline
|
||||
.last_timeline_count(None, &room_id, Some(conn.next_batch.into()))
|
||||
.map_ok(PduCount::into_unsigned)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.into();
|
||||
|
||||
let (last_timeline, last_notification, last_account, last_receipt, last_privateread) =
|
||||
join5(last_timeline, last_notification, last_account, last_receipt, last_privateread)
|
||||
.await;
|
||||
|
||||
Some(WindowRoom {
|
||||
room_id: room_id.clone(),
|
||||
membership,
|
||||
lists,
|
||||
ranked: 0,
|
||||
last_count: [
|
||||
last_timeline,
|
||||
last_notification,
|
||||
last_account,
|
||||
last_receipt,
|
||||
last_privateread,
|
||||
]
|
||||
.into_iter()
|
||||
.map(Option::unwrap_or_default)
|
||||
.filter(|count| conn.next_batch.ge(count))
|
||||
.max()
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(rooms = rooms.clone().count())
|
||||
)]
|
||||
async fn window<'a, Rooms>(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
rooms: Rooms,
|
||||
@@ -77,6 +182,8 @@ async fn select_window<'a, Rooms>(
|
||||
{
|
||||
static FULL_RANGE: (UInt, UInt) = (UInt::MIN, UInt::MAX);
|
||||
|
||||
let SyncInfo { services, sender_user, .. } = sync_info;
|
||||
|
||||
let selections = lists
|
||||
.keys()
|
||||
.cloned()
|
||||
@@ -95,135 +202,46 @@ async fn select_window<'a, Rooms>(
|
||||
.map(move |range| (id.clone(), range))
|
||||
})
|
||||
.flat_map(|(id, (start, end))| {
|
||||
let list = rooms
|
||||
rooms
|
||||
.clone()
|
||||
.filter(move |&room| room.lists.contains(&id));
|
||||
|
||||
let cycled = list.clone().all(|room| {
|
||||
conn.rooms
|
||||
.get(&room.room_id)
|
||||
.is_some_and(|room| room.roomsince != 0)
|
||||
});
|
||||
|
||||
list.enumerate()
|
||||
.skip_while(move |&(i, room)| {
|
||||
i < start
|
||||
|| conn
|
||||
.rooms
|
||||
.get(&room.room_id)
|
||||
.is_some_and(|conn_room| {
|
||||
conn_room.roomsince >= room.last_count
|
||||
|| (!cycled && conn_room.roomsince != 0)
|
||||
})
|
||||
.filter(move |&room| room.lists.contains(&id))
|
||||
.filter(|&room| {
|
||||
conn.rooms
|
||||
.get(&room.room_id)
|
||||
.is_some_and(|conn_room| {
|
||||
conn_room.roomsince == 0 || room.last_count > conn_room.roomsince
|
||||
})
|
||||
})
|
||||
.enumerate()
|
||||
.skip_while(move |&(i, _)| i < start)
|
||||
.take(end.saturating_add(1).saturating_sub(start))
|
||||
.map(|(_, room)| (room.room_id.clone(), room.clone()))
|
||||
});
|
||||
})
|
||||
.stream();
|
||||
|
||||
conn.subscriptions
|
||||
let subscriptions = conn
|
||||
.subscriptions
|
||||
.iter()
|
||||
.stream()
|
||||
.broad_filter_map(async |(room_id, _)| {
|
||||
filter_room_meta(sync_info, room_id)
|
||||
.await
|
||||
.then(|| WindowRoom {
|
||||
room_id: room_id.clone(),
|
||||
membership: None,
|
||||
lists: Default::default(),
|
||||
ranked: usize::MAX,
|
||||
last_count: 0,
|
||||
})
|
||||
.into_option()?;
|
||||
|
||||
Some(WindowRoom {
|
||||
room_id: room_id.clone(),
|
||||
lists: Default::default(),
|
||||
ranked: usize::MAX,
|
||||
last_count: 0,
|
||||
membership: services
|
||||
.state_cache
|
||||
.user_membership(sender_user, room_id)
|
||||
.await,
|
||||
})
|
||||
})
|
||||
.map(|room| (room.room_id.clone(), room))
|
||||
.chain(selections.stream())
|
||||
.collect()
|
||||
.await
|
||||
}
|
||||
.map(|room| (room.room_id.clone(), room));
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "matcher",
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(?room_id, ?membership)
|
||||
)]
|
||||
async fn match_lists_for_room(
|
||||
sync_info: SyncInfo<'_>,
|
||||
conn: &Connection,
|
||||
room_id: OwnedRoomId,
|
||||
membership: Option<MembershipState>,
|
||||
) -> Option<WindowRoom> {
|
||||
let SyncInfo { services, sender_user, .. } = sync_info;
|
||||
|
||||
let lists = conn
|
||||
.lists
|
||||
.iter()
|
||||
.stream()
|
||||
.filter_map(async |(id, list)| {
|
||||
let filter: OptionFuture<_> = list
|
||||
.filters
|
||||
.clone()
|
||||
.map(async |filters| {
|
||||
filter_room(sync_info, &filters, &room_id, membership.as_ref()).await
|
||||
})
|
||||
.into();
|
||||
|
||||
filter
|
||||
.await
|
||||
.is_none_or(is_true!())
|
||||
.then(|| id.clone())
|
||||
})
|
||||
.collect::<ListIds>()
|
||||
.await;
|
||||
|
||||
let last_timeline_count: OptionFuture<_> = lists
|
||||
.is_empty()
|
||||
.is_false()
|
||||
.then(|| {
|
||||
services
|
||||
.timeline
|
||||
.last_timeline_count(None, &room_id, None)
|
||||
.map_ok(PduCount::into_unsigned)
|
||||
.ok()
|
||||
})
|
||||
.into();
|
||||
|
||||
let last_account_count: OptionFuture<_> = lists
|
||||
.is_empty()
|
||||
.is_false()
|
||||
.then(|| {
|
||||
services
|
||||
.account_data
|
||||
.last_count(Some(room_id.as_ref()), sender_user, conn.next_batch)
|
||||
.ok()
|
||||
})
|
||||
.into();
|
||||
|
||||
let last_receipt_count: OptionFuture<_> = lists
|
||||
.is_empty()
|
||||
.is_false()
|
||||
.then(|| {
|
||||
services
|
||||
.read_receipt
|
||||
.last_receipt_count(&room_id, sender_user.into(), conn.globalsince.into())
|
||||
.map(Result::ok)
|
||||
})
|
||||
.into();
|
||||
|
||||
let (last_timeline_count, last_account_count, last_receipt_count) =
|
||||
join3(last_timeline_count, last_account_count, last_receipt_count).await;
|
||||
|
||||
Some(WindowRoom {
|
||||
room_id: room_id.clone(),
|
||||
membership,
|
||||
lists,
|
||||
ranked: 0,
|
||||
last_count: [last_timeline_count, last_account_count, last_receipt_count]
|
||||
.into_iter()
|
||||
.map(Option::flatten)
|
||||
.map(Option::unwrap_or_default)
|
||||
.max()
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
subscriptions.chain(selections).collect().await
|
||||
}
|
||||
|
||||
fn response_lists<'a, Rooms>(rooms: Rooms) -> ResponseLists
|
||||
@@ -243,15 +261,4 @@ fn response_lists<'a, Rooms>(rooms: Rooms) -> ResponseLists
|
||||
})
|
||||
}
|
||||
|
||||
fn room_sort(a: &WindowRoom, b: &WindowRoom) -> Ordering {
|
||||
if a.membership != b.membership {
|
||||
if a.membership == Some(MembershipState::Invite) {
|
||||
return Ordering::Less;
|
||||
}
|
||||
if b.membership == Some(MembershipState::Invite) {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
}
|
||||
|
||||
b.last_count.cmp(&a.last_count)
|
||||
}
|
||||
fn room_sort(a: &WindowRoom, b: &WindowRoom) -> Ordering { b.last_count.cmp(&a.last_count) }
|
||||
|
||||
@@ -66,7 +66,11 @@ pub(crate) async fn create_typing_event_route(
|
||||
// ping presence
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(&body.user_id, &ruma::presence::PresenceState::Online)
|
||||
.maybe_ping_presence(
|
||||
&body.user_id,
|
||||
body.sender_device.as_deref(),
|
||||
&ruma::presence::PresenceState::Online,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(create_typing_event::v3::Response {})
|
||||
|
||||
@@ -76,7 +76,7 @@ pub(crate) async fn delete_timezone_key_route(
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
|
||||
.await?;
|
||||
|
||||
Ok(delete_timezone_key::unstable::Response {})
|
||||
@@ -104,7 +104,7 @@ pub(crate) async fn set_timezone_key_route(
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
|
||||
.await?;
|
||||
|
||||
Ok(set_timezone_key::unstable::Response {})
|
||||
@@ -170,7 +170,7 @@ pub(crate) async fn set_profile_field_route(
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
|
||||
.await?;
|
||||
|
||||
Ok(set_profile_field::v3::Response {})
|
||||
@@ -224,7 +224,7 @@ pub(crate) async fn delete_profile_field_route(
|
||||
// Presence update
|
||||
services
|
||||
.presence
|
||||
.maybe_ping_presence(&body.user_id, &PresenceState::Online)
|
||||
.maybe_ping_presence(&body.user_id, body.sender_device.as_deref(), &PresenceState::Online)
|
||||
.await?;
|
||||
|
||||
Ok(delete_profile_field::v3::Response {})
|
||||
|
||||
@@ -42,17 +42,19 @@ pub(crate) async fn get_supported_versions_route(
|
||||
unstable_features: BTreeMap::from_iter([
|
||||
("org.matrix.e2e_cross_signing".to_owned(), true),
|
||||
("org.matrix.msc2285.stable".to_owned(), true), /* private read receipts (https://github.com/matrix-org/matrix-spec-proposals/pull/2285) */
|
||||
("fi.mau.msc2659.stable".to_owned(), true), /* appservice ping https://github.com/matrix-org/matrix-spec-proposals/pull/2659) */
|
||||
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true), /* query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666) */
|
||||
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
||||
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
||||
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
||||
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
||||
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
||||
("org.matrix.msc3575".to_owned(), true), /* sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1588877046) */
|
||||
("org.matrix.msc3814".to_owned(), true), /* dehydrated devices */
|
||||
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
||||
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
|
||||
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
|
||||
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
||||
("uk.tcpip.msc4133".to_owned(), true), /* Extending User Profile API with Key:Value Pairs (https://github.com/matrix-org/matrix-spec-proposals/pull/4133) */
|
||||
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
|
||||
("org.matrix.msc4180".to_owned(), true), /* stable flag for 3916 (https://github.com/matrix-org/matrix-spec-proposals/pull/4180) */
|
||||
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
|
||||
]),
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ pub(crate) async fn turn_server_route(
|
||||
return Err!(Request(NotFound("Not Found")));
|
||||
}
|
||||
|
||||
let turn_secret = &services.config.turn_secret;
|
||||
let turn_secret = &services.globals.turn_secret;
|
||||
|
||||
let (username, password) = if !turn_secret.is_empty() {
|
||||
let expiry = SecondsSinceUnixEpoch::from_system_time(
|
||||
|
||||
@@ -1,28 +1,59 @@
|
||||
use axum::{Json, extract::State, response::IntoResponse};
|
||||
use ruma::api::client::discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo},
|
||||
discover_homeserver::{self, HomeserverInfo, RtcFocusInfo},
|
||||
discover_support::{self, Contact},
|
||||
};
|
||||
use tuwunel_core::{Err, Result};
|
||||
use serde_json::Value as JsonValue;
|
||||
use tuwunel_core::{Err, Result, err, error::inspect_log};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /.well-known/matrix/client`
|
||||
///
|
||||
/// Returns the .well-known URL if it is configured, otherwise returns 404.
|
||||
/// Also includes RTC transport configuration for Element Call (MSC4143).
|
||||
pub(crate) async fn well_known_client(
|
||||
State(services): State<crate::State>,
|
||||
_body: Ruma<discover_homeserver::Request>,
|
||||
) -> Result<discover_homeserver::Response> {
|
||||
let client_url = match services.server.config.well_known.client.as_ref() {
|
||||
| Some(url) => url.to_string(),
|
||||
| None => return Err!(Request(NotFound("Not found."))),
|
||||
let homeserver = HomeserverInfo {
|
||||
base_url: match services.server.config.well_known.client.as_ref() {
|
||||
| Some(url) => url.to_string(),
|
||||
| None => return Err!(Request(NotFound("Not found."))),
|
||||
},
|
||||
};
|
||||
|
||||
// Add RTC transport configuration if available (MSC4143 / Element Call)
|
||||
// Element Call has evolved through several versions with different field
|
||||
// expectations
|
||||
let rtc_foci = services
|
||||
.server
|
||||
.config
|
||||
.well_known
|
||||
.rtc_transports
|
||||
.iter()
|
||||
.map(|transport| {
|
||||
let focus_type = transport
|
||||
.get("type")
|
||||
.and_then(JsonValue::as_str)
|
||||
.ok_or_else(|| err!("`type` is not a valid string"))?;
|
||||
|
||||
let transport = transport
|
||||
.as_object()
|
||||
.cloned()
|
||||
.ok_or_else(|| err!("`rtc_transport` is not a valid object"))?;
|
||||
|
||||
RtcFocusInfo::new(focus_type, transport).map_err(Into::into)
|
||||
})
|
||||
.collect::<Result<_>>()
|
||||
.map_err(|e| {
|
||||
err!(Config("global.well_known.rtc_transports", "Malformed value(s): {e:?}"))
|
||||
})
|
||||
.inspect_err(inspect_log)?;
|
||||
|
||||
Ok(discover_homeserver::Response {
|
||||
homeserver: HomeserverInfo { base_url: client_url },
|
||||
identity_server: None,
|
||||
tile_server: None,
|
||||
rtc_foci,
|
||||
..discover_homeserver::Response::new(homeserver)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#![type_length_limit = "589824"] //TODO: REDUCE ME
|
||||
#![type_length_limit = "262144"] //TODO: REDUCE ME
|
||||
#![allow(clippy::toplevel_ref_arg)]
|
||||
|
||||
pub mod client;
|
||||
|
||||
@@ -47,6 +47,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::request_3pid_management_token_via_email_route)
|
||||
.ruma_route(&client::request_3pid_management_token_via_msisdn_route)
|
||||
.ruma_route(&client::check_registration_token_validity)
|
||||
.ruma_route(&client::get_notifications_route)
|
||||
.ruma_route(&client::get_capabilities_route)
|
||||
.ruma_route(&client::get_pushrules_all_route)
|
||||
.ruma_route(&client::get_pushrules_global_route)
|
||||
@@ -145,6 +146,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
get(client::get_state_events_for_empty_key_route)
|
||||
.put(client::send_state_event_for_empty_key_route),
|
||||
)
|
||||
.ruma_route(&client::events_route)
|
||||
.ruma_route(&client::sync_events_route)
|
||||
.ruma_route(&client::sync_events_v5_route)
|
||||
.ruma_route(&client::get_context_route)
|
||||
@@ -163,6 +165,10 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::update_device_route)
|
||||
.ruma_route(&client::delete_device_route)
|
||||
.ruma_route(&client::delete_devices_route)
|
||||
.ruma_route(&client::put_dehydrated_device_route)
|
||||
.ruma_route(&client::delete_dehydrated_device_route)
|
||||
.ruma_route(&client::get_dehydrated_device_route)
|
||||
.ruma_route(&client::get_dehydrated_events_route)
|
||||
.ruma_route(&client::get_tags_route)
|
||||
.ruma_route(&client::update_tag_route)
|
||||
.ruma_route(&client::delete_tag_route)
|
||||
|
||||
+5
-11
@@ -6,7 +6,7 @@
|
||||
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
||||
OwnedUserId, ServerName, UserId, api::IncomingRequest,
|
||||
};
|
||||
use tuwunel_core::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
|
||||
use tuwunel_core::{Error, Result, debug_warn, err, trace, utils::string::EMPTY};
|
||||
use tuwunel_service::{Services, appservice::RegistrationInfo};
|
||||
|
||||
use super::{auth, auth::Auth, request, request::Request};
|
||||
@@ -95,6 +95,7 @@ async fn from_request(
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
let mut request = request::from(services, request).await?;
|
||||
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&request.body).ok();
|
||||
trace!(?request);
|
||||
|
||||
// while very unusual and really shouldn't be recommended, Synapse accepts POST
|
||||
// requests with a completely empty body. very old clients, libraries, and some
|
||||
@@ -104,13 +105,13 @@ async fn from_request(
|
||||
&& request.parts.method == http::Method::POST
|
||||
&& !request.parts.uri.path().contains("/media/")
|
||||
{
|
||||
trace!("json_body from_request: {:?}", json_body.clone());
|
||||
debug_warn!(
|
||||
"received a POST request with an empty body, defaulting/assuming to {{}} like \
|
||||
Synapse does"
|
||||
);
|
||||
json_body = Some(CanonicalJsonValue::Object(CanonicalJsonObject::new()));
|
||||
}
|
||||
|
||||
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
|
||||
Ok(Self {
|
||||
body: make_body::<T>(services, &mut request, json_body.as_mut(), &auth)?,
|
||||
@@ -147,16 +148,9 @@ fn into_http_request(request: &Request, body: Bytes) -> hyper::Request<Bytes> {
|
||||
.headers_mut()
|
||||
.expect("mutable http headers") = request.parts.headers.clone();
|
||||
|
||||
let http_request = http_request
|
||||
.body(body)
|
||||
.expect("http request body");
|
||||
|
||||
let headers = http_request.headers();
|
||||
let method = http_request.method();
|
||||
let uri = http_request.uri();
|
||||
debug!("{method:?} {uri:?} {headers:?}");
|
||||
|
||||
http_request
|
||||
.body(body)
|
||||
.expect("http request body")
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(super) async fn auth_appservice(
|
||||
let Ok(user_id) = request
|
||||
.query
|
||||
.user_id
|
||||
.clone()
|
||||
.as_deref()
|
||||
.map_or_else(user_id_default, OwnedUserId::parse)
|
||||
else {
|
||||
return Err!(Request(InvalidUsername("Username is invalid.")));
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use axum::RequestPartsExt;
|
||||
use axum_extra::{TypedHeader, headers::Authorization, typed_header::TypedHeaderRejectionReason};
|
||||
use ruma::{CanonicalJsonObject, CanonicalJsonValue, api::federation::authentication::XMatrix};
|
||||
use ruma::{
|
||||
CanonicalJsonName, CanonicalJsonObject, CanonicalJsonValue,
|
||||
api::federation::authentication::XMatrix,
|
||||
};
|
||||
use tuwunel_core::{Err, Result, debug_error, err, warn};
|
||||
use tuwunel_service::{
|
||||
Services,
|
||||
@@ -14,7 +17,7 @@ pub(super) async fn auth_server(
|
||||
request: &mut Request,
|
||||
body: Option<&CanonicalJsonValue>,
|
||||
) -> Result<Auth> {
|
||||
type Member = (String, CanonicalJsonValue);
|
||||
type Member = (CanonicalJsonName, CanonicalJsonValue);
|
||||
type Object = CanonicalJsonObject;
|
||||
type Value = CanonicalJsonValue;
|
||||
|
||||
@@ -66,7 +69,7 @@ pub(super) async fn auth_server(
|
||||
err!(Request(Forbidden(debug_warn!("Failed to fetch signing keys: {e}"))))
|
||||
})?;
|
||||
|
||||
let keys: PubKeys = [(x_matrix.key.to_string(), key.key)].into();
|
||||
let keys: PubKeys = [(x_matrix.key.as_str().into(), key.key)].into();
|
||||
let keys: PubKeyMap = [(origin.as_str().into(), keys)].into();
|
||||
if let Err(e) = ruma::signatures::verify_json(&keys, &authorization) {
|
||||
debug_error!("Failed to verify federation request from {origin}: {e}");
|
||||
|
||||
@@ -41,7 +41,7 @@ impl<Err, Req, Fut, Fun, $($tx,)*> RumaHandler<($($tx,)* Ruma<Req>,)> for Fun
|
||||
Fun: Fn($($tx,)* Ruma<Req>,) -> Fut + Send + Sync + 'static,
|
||||
Fut: Future<Output = Result<Req::OutgoingResponse, Err>> + Send,
|
||||
Req: IncomingRequest + Debug + Send + Sync + 'static,
|
||||
Err: IntoResponse + Send,
|
||||
Err: IntoResponse + Debug + Send,
|
||||
<Req as IncomingRequest>::OutgoingResponse: Send,
|
||||
$( $tx: FromRequestParts<State> + Send + Sync + 'static, )*
|
||||
{
|
||||
|
||||
@@ -4,22 +4,28 @@
|
||||
use bytes::Bytes;
|
||||
use http::request::Parts;
|
||||
use serde::Deserialize;
|
||||
use tuwunel_core::{Result, err};
|
||||
use tuwunel_core::{Result, err, smallstr::SmallString, smallvec::SmallVec};
|
||||
use tuwunel_service::Services;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(super) struct QueryParams {
|
||||
pub(super) access_token: Option<String>,
|
||||
pub(super) user_id: Option<String>,
|
||||
pub(super) user_id: Option<UserId>,
|
||||
}
|
||||
|
||||
pub(super) type UserId = SmallString<[u8; 48]>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Request {
|
||||
pub(super) path: Path<Vec<String>>,
|
||||
pub(super) path: Path<PathParams>,
|
||||
pub(super) query: QueryParams,
|
||||
pub(super) body: Bytes,
|
||||
pub(super) parts: Parts,
|
||||
}
|
||||
|
||||
pub(super) type PathParams = SmallVec<[PathParam; 8]>;
|
||||
pub(super) type PathParam = SmallString<[u8; 32]>;
|
||||
|
||||
pub(super) async fn from(
|
||||
services: &Services,
|
||||
request: hyper::Request<axum::body::Body>,
|
||||
@@ -27,7 +33,7 @@ pub(super) async fn from(
|
||||
let limited = request.with_limited_body();
|
||||
let (mut parts, body) = limited.into_parts();
|
||||
|
||||
let path: Path<Vec<String>> = parts.extract().await?;
|
||||
let path: Path<PathParams> = parts.extract().await?;
|
||||
let query = parts.uri.query().unwrap_or_default();
|
||||
let query = serde_html_form::from_str(query)
|
||||
.map_err(|e| err!(Request(Unknown("Failed to read query parameters: {e}"))))?;
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
use axum::extract::State;
|
||||
use axum_client_ip::InsecureClientIp;
|
||||
use base64::{Engine as _, engine::general_purpose};
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
CanonicalJsonValue, OwnedUserId, UserId,
|
||||
api::{
|
||||
client::error::ErrorKind,
|
||||
federation::membership::{RawStrippedState, create_invite},
|
||||
},
|
||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
||||
events::{
|
||||
GlobalAccountDataEventType,
|
||||
push_rules::PushRulesEvent,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
push,
|
||||
serde::JsonObject,
|
||||
};
|
||||
use tuwunel_core::{
|
||||
@@ -100,7 +106,7 @@ pub(crate) async fn create_invite_route(
|
||||
let event_id = gen_event_id(&signed_event, &body.room_version)?;
|
||||
|
||||
// Add event_id back
|
||||
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
|
||||
signed_event.insert("event_id".into(), CanonicalJsonValue::String(event_id.to_string()));
|
||||
|
||||
let sender: &UserId = signed_event
|
||||
.get("sender")
|
||||
@@ -127,7 +133,7 @@ pub(crate) async fn create_invite_route(
|
||||
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
||||
|
||||
event.insert("event_id".to_owned(), "$placeholder".into());
|
||||
event.insert("event_id".into(), "$placeholder".into());
|
||||
|
||||
let pdu: PduEvent = serde_json::from_value(event.into())
|
||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
||||
@@ -159,6 +165,36 @@ pub(crate) async fn create_invite_route(
|
||||
.await?;
|
||||
drop(count);
|
||||
|
||||
services
|
||||
.pusher
|
||||
.get_pushkeys(&invited_user)
|
||||
.map(ToOwned::to_owned)
|
||||
.for_each(async |pushkey| {
|
||||
let Ok(pusher) = services
|
||||
.pusher
|
||||
.get_pusher(&invited_user, &pushkey)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let ruleset = services
|
||||
.account_data
|
||||
.get_global(&invited_user, GlobalAccountDataEventType::PushRules)
|
||||
.await
|
||||
.map_or_else(
|
||||
|_| push::Ruleset::server_default(&invited_user),
|
||||
|ev: PushRulesEvent| ev.content.global,
|
||||
);
|
||||
|
||||
services
|
||||
.pusher
|
||||
.send_push_notice(&invited_user, &pusher, &ruleset, &pdu)
|
||||
.await
|
||||
.ok();
|
||||
})
|
||||
.await;
|
||||
|
||||
for appservice in services.appservice.read().await.values() {
|
||||
if appservice.is_user_match(&invited_user) {
|
||||
services
|
||||
|
||||
+69
-60
@@ -1,13 +1,17 @@
|
||||
use axum::extract::State;
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt};
|
||||
use ruma::api::{
|
||||
client::error::ErrorKind,
|
||||
federation::{
|
||||
device::get_devices::{self, v1::UserDevice},
|
||||
keys::{claim_keys, get_keys},
|
||||
use futures::{FutureExt, StreamExt, TryFutureExt, future::join4};
|
||||
use ruma::{
|
||||
UserId,
|
||||
api::{
|
||||
client::device::Device,
|
||||
federation::{
|
||||
device::get_devices::{self, v1::UserDevice},
|
||||
keys::{claim_keys, get_keys},
|
||||
},
|
||||
},
|
||||
uint,
|
||||
};
|
||||
use tuwunel_core::{Error, Result};
|
||||
use tuwunel_core::{Err, Result, utils::future::TryExtExt};
|
||||
|
||||
use crate::{
|
||||
Ruma,
|
||||
@@ -21,54 +25,65 @@ pub(crate) async fn get_devices_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<get_devices::v1::Request>,
|
||||
) -> Result<get_devices::v1::Response> {
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Tried to access user from other server.",
|
||||
));
|
||||
let user_id = &body.user_id;
|
||||
if !services.globals.user_is_local(user_id) {
|
||||
return Err!(Request(InvalidParam("Tried to access user from other server.")));
|
||||
}
|
||||
|
||||
let user_id = &body.user_id;
|
||||
Ok(get_devices::v1::Response {
|
||||
user_id: user_id.clone(),
|
||||
stream_id: services
|
||||
.users
|
||||
.get_devicelist_version(user_id)
|
||||
.await
|
||||
.unwrap_or(0)
|
||||
.try_into()?,
|
||||
devices: services
|
||||
.users
|
||||
.all_devices_metadata(user_id)
|
||||
.filter_map(async |metadata| {
|
||||
let device_id = metadata.device_id.clone();
|
||||
let device_id_clone = device_id.clone();
|
||||
let device_id_string = device_id.as_str().to_owned();
|
||||
let device_display_name = if services.config.allow_device_name_federation {
|
||||
metadata.display_name.clone()
|
||||
} else {
|
||||
Some(device_id_string)
|
||||
};
|
||||
let allowed_signatures = |u: &UserId| u.server_name() == body.origin();
|
||||
|
||||
services
|
||||
.users
|
||||
.get_device_keys(user_id, &device_id_clone)
|
||||
.map_ok(|keys| UserDevice { device_id, keys, device_display_name })
|
||||
.map(Result::ok)
|
||||
.await
|
||||
})
|
||||
.collect()
|
||||
.await,
|
||||
master_key: services
|
||||
.users
|
||||
.get_master_key(None, &body.user_id, &|u| u.server_name() == body.origin())
|
||||
.await
|
||||
.ok(),
|
||||
self_signing_key: services
|
||||
.users
|
||||
.get_self_signing_key(None, &body.user_id, &|u| u.server_name() == body.origin())
|
||||
.await
|
||||
.ok(),
|
||||
let master_key = services
|
||||
.users
|
||||
.get_master_key(None, user_id, &allowed_signatures)
|
||||
.ok();
|
||||
|
||||
let self_signing_key = services
|
||||
.users
|
||||
.get_self_signing_key(None, user_id, &allowed_signatures)
|
||||
.ok();
|
||||
|
||||
let stream_id = services
|
||||
.users
|
||||
.get_devicelist_version(user_id)
|
||||
.map_ok(TryInto::try_into)
|
||||
.map_ok(Result::ok)
|
||||
.ok();
|
||||
|
||||
let devices = services
|
||||
.users
|
||||
.all_devices_metadata(user_id)
|
||||
.filter_map(async |Device { device_id, display_name, .. }: Device| {
|
||||
let device_display_name = services
|
||||
.config
|
||||
.allow_device_name_federation
|
||||
.then_some(display_name)
|
||||
.flatten()
|
||||
.or_else(|| Some(device_id.as_str().into()));
|
||||
|
||||
services
|
||||
.users
|
||||
.get_device_keys(user_id, &device_id)
|
||||
.map_ok(|keys| UserDevice {
|
||||
device_id: device_id.clone(),
|
||||
device_display_name,
|
||||
keys,
|
||||
})
|
||||
.map(Result::ok)
|
||||
.await
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (stream_id, master_key, self_signing_key, devices) =
|
||||
join4(stream_id, master_key, self_signing_key, devices)
|
||||
.boxed()
|
||||
.await;
|
||||
|
||||
Ok(get_devices::v1::Response {
|
||||
user_id: body.body.user_id,
|
||||
stream_id: stream_id.flatten().unwrap_or_else(|| uint!(0)),
|
||||
devices,
|
||||
self_signing_key,
|
||||
master_key,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -84,10 +99,7 @@ pub(crate) async fn get_keys_route(
|
||||
.iter()
|
||||
.any(|(u, _)| !services.globals.user_is_local(u))
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"User does not belong to this server.",
|
||||
));
|
||||
return Err!(Request(InvalidParam("User does not belong to this server.")));
|
||||
}
|
||||
|
||||
let result = get_keys_helper(
|
||||
@@ -118,10 +130,7 @@ pub(crate) async fn claim_keys_route(
|
||||
.iter()
|
||||
.any(|(u, _)| !services.globals.user_is_local(u))
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Tried to access user from other server.",
|
||||
));
|
||||
return Err!(Request(InvalidParam("Tried to access user from other server.")));
|
||||
}
|
||||
|
||||
let result = claim_keys_helper(&services, &body.one_time_keys).await?;
|
||||
|
||||
+60
-12
@@ -590,6 +590,17 @@ pub struct Config {
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_encryption: bool,
|
||||
|
||||
/// Controls whether locally-created rooms should be end-to-end encrypted by
|
||||
/// default. This option is equivalent to the one found in Synapse.
|
||||
///
|
||||
/// Options:
|
||||
/// - "all": All created rooms are encrypted.
|
||||
/// - "invite": Any room created with `private_chat` or
|
||||
/// `trusted_private_chat` presets.
|
||||
/// - Other values default to no effect.
|
||||
#[serde(default)]
|
||||
pub encryption_enabled_by_default_for_room_type: Option<String>,
|
||||
|
||||
/// Controls whether federation is allowed or not. It is not recommended to
|
||||
/// disable this after installation due to potential federation breakage but
|
||||
/// this is technically not a permanent setting.
|
||||
@@ -1277,26 +1288,28 @@ pub struct Config {
|
||||
#[serde(default = "default_rocksdb_stats_level")]
|
||||
pub rocksdb_stats_level: u8,
|
||||
|
||||
/// Erases data no longer reachable in the current schema. The developers
|
||||
/// expect this to be set to true which simplifies the schema and prevents
|
||||
/// accumulation of old schemas remaining in the codebase forever. If this
|
||||
/// is set to false, old columns which are not described in the current
|
||||
/// schema will be ignored rather than erased, leaking their space.
|
||||
/// Ignores the list of dropped columns set by developers.
|
||||
///
|
||||
/// This can be set to false when moving between versions in ways which are
|
||||
/// not recommended or otherwise forbidden, or for diagnostic and
|
||||
/// development purposes; requiring preservation across such movements.
|
||||
/// This should be set to true when knowingly moving between versions in
|
||||
/// ways which are not recommended or otherwise forbidden, or for
|
||||
/// diagnostic and development purposes; requiring preservation across such
|
||||
/// movements.
|
||||
///
|
||||
/// default: true
|
||||
#[serde(default = "true_fn")]
|
||||
pub rocksdb_drop_missing_columns: bool,
|
||||
/// The developer's list of dropped columns is meant to safely reduce space
|
||||
/// by erasing data no longer in use. If this is set to true that storage
|
||||
/// will not be reclaimed as intended.
|
||||
///
|
||||
/// default: false
|
||||
#[serde(default)]
|
||||
pub rocksdb_never_drop_columns: bool,
|
||||
|
||||
/// This is a password that can be configured that will let you login to the
|
||||
/// server bot account (currently `@conduit`) for emergency troubleshooting
|
||||
/// purposes such as recovering/recreating your admin room, or inviting
|
||||
/// yourself back.
|
||||
///
|
||||
/// See https://tuwunel.chat/troubleshooting.html#lost-access-to-admin-room for other ways to get back into your admin room.
|
||||
/// See https://tuwunel.chat/troubleshooting.html#lost-access-to-admin-room
|
||||
/// for other ways to get back into your admin room.
|
||||
///
|
||||
/// Once this password is unset, all sessions will be logged out for
|
||||
/// security purposes.
|
||||
@@ -1310,6 +1323,19 @@ pub struct Config {
|
||||
#[serde(default = "default_notification_push_path")]
|
||||
pub notification_push_path: String,
|
||||
|
||||
/// For compatibility and special purpose use only. Setting this option to
|
||||
/// true will not filter messages sent to pushers based on rules or actions.
|
||||
/// Everything will be sent to the pusher. This option is offered for
|
||||
/// several reasons, but should not be necessary:
|
||||
/// - Bypass to workaround bugs or outdated server-side ruleset support.
|
||||
/// - Allow clients to evaluate pushrules themselves (due to the above).
|
||||
/// - Hosting or companies which have custom pushers and internal needs.
|
||||
///
|
||||
/// Note that setting this option to true will not affect the record of
|
||||
/// notifications found in the notifications pane.
|
||||
#[serde(default)]
|
||||
pub push_everything: bool,
|
||||
|
||||
/// Allow local (your server only) presence updates/requests.
|
||||
///
|
||||
/// Note that presence on tuwunel is very fast unlike Synapse's. If using
|
||||
@@ -2134,6 +2160,28 @@ pub struct WellKnownConfig {
|
||||
///
|
||||
/// example "@admin:example.com"
|
||||
pub support_mxid: Option<OwnedUserId>,
|
||||
|
||||
/// Element Call / MatrixRTC configuration (MSC4143).
|
||||
/// Configures the LiveKit SFU server for voice/video calls.
|
||||
///
|
||||
/// Requires a LiveKit server with JWT authentication.
|
||||
/// The `livekit_service_url` should point to your LiveKit JWT endpoint.
|
||||
///
|
||||
/// Note: You must also set `client` above to your homeserver URL.
|
||||
///
|
||||
/// Example:
|
||||
/// ```toml
|
||||
/// [global.well_known]
|
||||
/// client = "https://matrix.yourdomain.com"
|
||||
///
|
||||
/// [[global.well_known.rtc_transports]]
|
||||
/// type = "livekit"
|
||||
/// livekit_service_url = "https://livekit.yourdomain.com"
|
||||
/// ```
|
||||
///
|
||||
/// default: []
|
||||
#[serde(default)]
|
||||
pub rtc_transports: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Default)]
|
||||
|
||||
@@ -62,7 +62,7 @@ fn mutate_outgoing_reference_format(value: &mut CanonicalJsonValue) {
|
||||
if let Some(event_id) = value.as_str().map(ToOwned::to_owned) {
|
||||
*value = CanonicalJsonValue::Array(vec![
|
||||
CanonicalJsonValue::String(event_id),
|
||||
CanonicalJsonValue::Object([(String::new(), "".into())].into()),
|
||||
CanonicalJsonValue::Object([(Default::default(), "".into())].into()),
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -92,7 +92,7 @@ pub fn from_incoming_federation(
|
||||
}
|
||||
|
||||
if !room_rules.event_format.require_event_id {
|
||||
pdu_json.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.into()));
|
||||
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(event_id.into()));
|
||||
}
|
||||
|
||||
PduEvent::from_val(pdu_json)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Err, Result, debug, err, is_equal_to,
|
||||
Err, Result, err, is_equal_to,
|
||||
matrix::{Event, StateKey},
|
||||
};
|
||||
|
||||
@@ -45,8 +45,6 @@ pub(super) async fn check_room_member<Fetch, Fut, Pdu>(
|
||||
Fut: Future<Output = Result<Pdu>> + Send,
|
||||
Pdu: Event,
|
||||
{
|
||||
debug!("starting m.room.member check");
|
||||
|
||||
// Since v1, if there is no state_key property, or no membership property in
|
||||
// content, reject.
|
||||
let Some(state_key) = room_member_event.state_key() else {
|
||||
|
||||
@@ -364,7 +364,7 @@ fn check_pdu_format_pdu_too_big() {
|
||||
.unwrap();
|
||||
|
||||
let long_string = repeat_n('a', 66_000).collect::<String>();
|
||||
content.insert("big_data".to_owned(), long_string.into());
|
||||
content.insert("big_data".into(), long_string.into());
|
||||
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ fn check_pdu_format_strings_too_big() {
|
||||
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
|
||||
let mut pdu = pdu_v1();
|
||||
let value = repeat_n('a', 300).collect::<String>();
|
||||
pdu.insert((*field).to_owned(), value.into());
|
||||
pdu.insert((*field).into(), value.into());
|
||||
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
|
||||
}
|
||||
}
|
||||
@@ -393,7 +393,7 @@ fn check_pdu_format_strings_too_big() {
|
||||
fn check_pdu_format_strings_wrong_format() {
|
||||
for field in &["event_id", "sender", "room_id", "type", "state_key"] {
|
||||
let mut pdu = pdu_v1();
|
||||
pdu.insert((*field).to_owned(), true.into());
|
||||
pdu.insert((*field).into(), true.into());
|
||||
check_pdu_format(&pdu, &EventFormatRules::V1).unwrap_err();
|
||||
}
|
||||
}
|
||||
@@ -405,7 +405,7 @@ fn check_pdu_format_arrays_too_big() {
|
||||
let value: Vec<_> =
|
||||
repeat_n(CanonicalJsonValue::from("$eventid".to_owned()), 30).collect();
|
||||
|
||||
pdu.insert((*field).to_owned(), value.into());
|
||||
pdu.insert((*field).into(), value.into());
|
||||
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
|
||||
}
|
||||
}
|
||||
@@ -414,7 +414,7 @@ fn check_pdu_format_arrays_too_big() {
|
||||
fn check_pdu_format_arrays_wrong_format() {
|
||||
for field in &["prev_events", "auth_events"] {
|
||||
let mut pdu = pdu_v3();
|
||||
pdu.insert((*field).to_owned(), true.into());
|
||||
pdu.insert((*field).into(), true.into());
|
||||
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
|
||||
}
|
||||
}
|
||||
@@ -422,7 +422,7 @@ fn check_pdu_format_arrays_wrong_format() {
|
||||
#[test]
|
||||
fn check_pdu_format_negative_depth() {
|
||||
let mut pdu = pdu_v3();
|
||||
pdu.insert("depth".to_owned(), int!(-1).into())
|
||||
pdu.insert("depth".into(), int!(-1).into())
|
||||
.unwrap();
|
||||
|
||||
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
|
||||
@@ -431,7 +431,7 @@ fn check_pdu_format_negative_depth() {
|
||||
#[test]
|
||||
fn check_pdu_format_depth_wrong_format() {
|
||||
let mut pdu = pdu_v3();
|
||||
pdu.insert("depth".to_owned(), true.into());
|
||||
pdu.insert("depth".into(), true.into());
|
||||
check_pdu_format(&pdu, &EventFormatRules::V3).unwrap_err();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
use crate::{
|
||||
Result, debug,
|
||||
matrix::{Event, TypeStateKey},
|
||||
trace,
|
||||
utils::stream::{BroadbandExt, IterStream},
|
||||
};
|
||||
|
||||
@@ -87,7 +88,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
|
||||
// Split the unconflicted state map and the conflicted state set.
|
||||
let (unconflicted_state, conflicted_states) = split_conflicted_state(state_maps).await;
|
||||
|
||||
debug!(?unconflicted_state, unconflicted = unconflicted_state.len(), "unresolved state");
|
||||
trace!(?unconflicted_state, unconflicted = unconflicted_state.len(), "unresolved state");
|
||||
debug!(?conflicted_states, conflicted = conflicted_states.len(), "unresolved states");
|
||||
|
||||
if conflicted_states.is_empty() {
|
||||
@@ -120,7 +121,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
|
||||
.chain(conflicted_subgraph)
|
||||
.collect::<AuthSet<_>>()
|
||||
.inspect(|set| debug!(count = set.len(), "full conflicted set"))
|
||||
.inspect(|set| debug!(?set, "full conflicted set"))
|
||||
.inspect(|set| trace!(?set, "full conflicted set"))
|
||||
.await;
|
||||
|
||||
// 1. Select the set X of all power events that appear in the full conflicted
|
||||
@@ -129,7 +130,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
|
||||
// a list using the reverse topological power ordering.
|
||||
let sorted_power_events: Vec<_> = power_sort(rules, &full_conflicted_set, fetch)
|
||||
.inspect_ok(|list| debug!(count = list.len(), "sorted power events"))
|
||||
.inspect_ok(|list| debug!(?list, "sorted power events"))
|
||||
.inspect_ok(|list| trace!(?list, "sorted power events"))
|
||||
.await?;
|
||||
|
||||
let sorted_power_events_set: AuthSet<_> = sorted_power_events.iter().collect();
|
||||
@@ -154,7 +155,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
|
||||
let partially_resolved_state =
|
||||
iterative_auth_check(rules, sorted_power_events, initial_state, fetch)
|
||||
.inspect_ok(|map| debug!(count = map.len(), "partially resolved power state"))
|
||||
.inspect_ok(|map| debug!(?map, "partially resolved power state"))
|
||||
.inspect_ok(|map| trace!(?map, "partially resolved power state"))
|
||||
.await?;
|
||||
|
||||
// This "epochs" power level event
|
||||
@@ -168,7 +169,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
|
||||
.collect();
|
||||
|
||||
debug!(count = remaining_events.len(), "remaining events");
|
||||
debug!(list = ?remaining_events, "remaining events");
|
||||
trace!(list = ?remaining_events, "remaining events");
|
||||
|
||||
let have_remaining_events = !remaining_events.is_empty();
|
||||
let remaining_events = remaining_events
|
||||
@@ -188,7 +189,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
|
||||
.unwrap_or(Ok(Vec::new()))?;
|
||||
|
||||
debug!(count = sorted_remaining_events.len(), "sorted remaining events");
|
||||
debug!(list = ?sorted_remaining_events, "sorted remaining events");
|
||||
trace!(list = ?sorted_remaining_events, "sorted remaining events");
|
||||
|
||||
let sorted_remaining_events = sorted_remaining_events
|
||||
.iter()
|
||||
@@ -207,7 +208,7 @@ pub async fn resolve<States, AuthSets, FetchExists, ExistsFut, FetchEvent, Event
|
||||
resolved_state.extend(unconflicted_state);
|
||||
|
||||
debug!(resolved_state = resolved_state.len(), "resolved state");
|
||||
debug!(?resolved_state, "resolved state");
|
||||
trace!(?resolved_state, "resolved state");
|
||||
|
||||
Ok(resolved_state)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
pub trait BoolExt {
|
||||
fn and<T>(self, t: Option<T>) -> Option<T>;
|
||||
|
||||
#[must_use]
|
||||
fn and_is(self, b: bool) -> bool;
|
||||
|
||||
#[must_use]
|
||||
fn and_if<F: FnOnce() -> bool>(self, f: F) -> bool;
|
||||
|
||||
fn and_then<T, F: FnOnce() -> Option<T>>(self, f: F) -> Option<T>;
|
||||
|
||||
#[must_use]
|
||||
@@ -55,6 +61,12 @@ impl BoolExt for bool {
|
||||
#[inline]
|
||||
fn and<T>(self, t: Option<T>) -> Option<T> { self.then_some(t).flatten() }
|
||||
|
||||
#[inline]
|
||||
fn and_if<F: FnOnce() -> Self>(self, f: F) -> Self { self.and_is(f()) }
|
||||
|
||||
#[inline]
|
||||
fn and_is(self, b: Self) -> Self { self && b }
|
||||
|
||||
#[inline]
|
||||
fn and_then<T, F: FnOnce() -> Option<T>>(self, f: F) -> Option<T> { self.then(f).flatten() }
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Extended external extensions to futures::FutureExt
|
||||
#![allow(clippy::many_single_char_names, clippy::impl_trait_in_params)]
|
||||
|
||||
use std::marker::Unpin;
|
||||
|
||||
@@ -16,8 +17,6 @@ pub trait BoolExt
|
||||
where
|
||||
Self: Future<Output = bool> + Send,
|
||||
{
|
||||
type Result;
|
||||
|
||||
fn or<B>(self, b: B) -> impl Future<Output = bool> + Send
|
||||
where
|
||||
B: Future<Output = bool> + Send + Unpin,
|
||||
@@ -46,15 +45,11 @@ impl<Fut> BoolExt for Fut
|
||||
where
|
||||
Fut: Future<Output = bool> + Send,
|
||||
{
|
||||
type Result = crate::Result<(), ()>;
|
||||
|
||||
fn or<B>(self, b: B) -> impl Future<Output = bool> + Send
|
||||
where
|
||||
B: Future<Output = bool> + Send + Unpin,
|
||||
Self: Sized + Unpin,
|
||||
{
|
||||
let test = |test: bool| test.ok_or(Self::Result::Err(()));
|
||||
|
||||
select_ok([Left(self.map(test)), Right(b.map(test))]).map(|res| res.is_ok())
|
||||
}
|
||||
|
||||
@@ -63,8 +58,6 @@ fn and<B>(self, b: B) -> impl Future<Output = bool> + Send
|
||||
B: Future<Output = bool> + Send,
|
||||
Self: Sized,
|
||||
{
|
||||
let test = |test: bool| test.ok_or(Self::Result::Err(()));
|
||||
|
||||
try_join(self.map(test), b.map(test)).map(|res| res.is_ok())
|
||||
}
|
||||
|
||||
@@ -74,8 +67,6 @@ fn and2<B, C>(self, b: B, c: C) -> impl Future<Output = bool> + Send
|
||||
C: Future<Output = bool> + Send,
|
||||
Self: Sized,
|
||||
{
|
||||
let test = |test: bool| test.ok_or(Self::Result::Err(()));
|
||||
|
||||
try_join3(self.map(test), b.map(test), c.map(test)).map(|res| res.is_ok())
|
||||
}
|
||||
|
||||
@@ -86,8 +77,6 @@ fn and3<B, C, D>(self, b: B, c: C, d: D) -> impl Future<Output = bool> + Send
|
||||
D: Future<Output = bool> + Send,
|
||||
Self: Sized,
|
||||
{
|
||||
let test = |test: bool| test.ok_or(Self::Result::Err(()));
|
||||
|
||||
try_join4(self.map(test), b.map(test), c.map(test), d.map(test)).map(|res| res.is_ok())
|
||||
}
|
||||
}
|
||||
@@ -97,9 +86,7 @@ pub fn and<I, F>(args: I) -> impl Future<Output = bool> + Send
|
||||
I: Iterator<Item = F> + Send,
|
||||
F: Future<Output = bool> + Send,
|
||||
{
|
||||
type Result = crate::Result<(), ()>;
|
||||
|
||||
let args = args.map(|a| a.map(|a| a.ok_or(Result::Err(()))));
|
||||
let args = args.map(|a| a.map(test));
|
||||
|
||||
try_join_all(args).map(|res| res.is_ok())
|
||||
}
|
||||
@@ -109,9 +96,51 @@ pub fn or<I, F>(args: I) -> impl Future<Output = bool> + Send
|
||||
I: Iterator<Item = F> + Send,
|
||||
F: Future<Output = bool> + Send + Unpin,
|
||||
{
|
||||
type Result = crate::Result<(), ()>;
|
||||
|
||||
let args = args.map(|a| a.map(|a| a.ok_or(Result::Err(()))));
|
||||
let args = args.map(|a| a.map(test));
|
||||
|
||||
select_ok(args).map(|res| res.is_ok())
|
||||
}
|
||||
|
||||
pub fn and4(
|
||||
a: impl Future<Output = bool> + Send,
|
||||
b: impl Future<Output = bool> + Send,
|
||||
c: impl Future<Output = bool> + Send,
|
||||
d: impl Future<Output = bool> + Send,
|
||||
) -> impl Future<Output = bool> + Send {
|
||||
a.and3(b, c, d)
|
||||
}
|
||||
|
||||
pub fn and5(
|
||||
a: impl Future<Output = bool> + Send,
|
||||
b: impl Future<Output = bool> + Send,
|
||||
c: impl Future<Output = bool> + Send,
|
||||
d: impl Future<Output = bool> + Send,
|
||||
e: impl Future<Output = bool> + Send,
|
||||
) -> impl Future<Output = bool> + Send {
|
||||
a.and2(b, c).and2(d, e)
|
||||
}
|
||||
|
||||
pub fn and6(
|
||||
a: impl Future<Output = bool> + Send,
|
||||
b: impl Future<Output = bool> + Send,
|
||||
c: impl Future<Output = bool> + Send,
|
||||
d: impl Future<Output = bool> + Send,
|
||||
e: impl Future<Output = bool> + Send,
|
||||
f: impl Future<Output = bool> + Send,
|
||||
) -> impl Future<Output = bool> + Send {
|
||||
a.and3(b, c, d).and2(e, f)
|
||||
}
|
||||
|
||||
pub fn and7(
|
||||
a: impl Future<Output = bool> + Send,
|
||||
b: impl Future<Output = bool> + Send,
|
||||
c: impl Future<Output = bool> + Send,
|
||||
d: impl Future<Output = bool> + Send,
|
||||
e: impl Future<Output = bool> + Send,
|
||||
f: impl Future<Output = bool> + Send,
|
||||
g: impl Future<Output = bool> + Send,
|
||||
) -> impl Future<Output = bool> + Send {
|
||||
a.and3(b, c, d).and3(e, f, g)
|
||||
}
|
||||
|
||||
fn test(test: bool) -> crate::Result<(), ()> { test.ok_or(()) }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
mod ready_eq_ext;
|
||||
mod try_ext_ext;
|
||||
|
||||
pub use bool_ext::{BoolExt, and, or};
|
||||
pub use bool_ext::{BoolExt, and, and4, and5, and6, and7, or};
|
||||
pub use ext_ext::ExtExt;
|
||||
pub use option_ext::OptionExt;
|
||||
pub use option_stream::OptionStream;
|
||||
|
||||
@@ -6,6 +6,14 @@ pub trait OptionExt<T> {
|
||||
fn is_none_or(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = bool> + Send;
|
||||
|
||||
fn is_some_and(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = bool> + Send;
|
||||
|
||||
fn unwrap_or(self, t: T) -> impl Future<Output = T> + Send;
|
||||
|
||||
fn unwrap_or_else(self, f: impl FnOnce() -> T + Send) -> impl Future<Output = T> + Send;
|
||||
|
||||
fn unwrap_or_default(self) -> impl Future<Output = T> + Send
|
||||
where
|
||||
T: Default;
|
||||
}
|
||||
|
||||
impl<T, Fut> OptionExt<T> for OptionFuture<Fut>
|
||||
@@ -22,4 +30,20 @@ fn is_none_or(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = b
|
||||
fn is_some_and(self, f: impl FnOnce(&T) -> bool + Send) -> impl Future<Output = bool> + Send {
|
||||
self.map(|o| o.as_ref().is_some_and(f))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unwrap_or(self, t: T) -> impl Future<Output = T> + Send { self.map(|o| o.unwrap_or(t)) }
|
||||
|
||||
#[inline]
|
||||
fn unwrap_or_else(self, f: impl FnOnce() -> T + Send) -> impl Future<Output = T> + Send {
|
||||
self.map(|o| o.unwrap_or_else(f))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn unwrap_or_default(self) -> impl Future<Output = T> + Send
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
self.map(Option::unwrap_or_default)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
mod flat_ok;
|
||||
mod inspect_log;
|
||||
mod into_is_ok;
|
||||
mod is_err_or;
|
||||
mod log_debug_err;
|
||||
mod log_err;
|
||||
mod map_expect;
|
||||
@@ -19,6 +20,7 @@
|
||||
flat_ok::FlatOk,
|
||||
inspect_log::{ErrDebugLog, ErrLog},
|
||||
into_is_ok::IntoIsOk,
|
||||
is_err_or::IsErrOr,
|
||||
log_debug_err::LogDebugErr,
|
||||
log_err::LogErr,
|
||||
map_expect::MapExpect,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#![allow(clippy::wrong_self_convention)]
|
||||
|
||||
use super::Result;
|
||||
|
||||
pub trait IsErrOr<T> {
|
||||
fn is_err_or<F: FnOnce(T) -> bool>(self, f: F) -> bool;
|
||||
}
|
||||
|
||||
impl<T, E> IsErrOr<T> for Result<T, E> {
|
||||
#[inline]
|
||||
fn is_err_or<F>(self, f: F) -> bool
|
||||
where
|
||||
F: FnOnce(T) -> bool,
|
||||
{
|
||||
if let Ok(t) = self { f(t) } else { true }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
mod unquoted;
|
||||
|
||||
pub use self::{between::Between, split::SplitInfallible, unquote::Unquote, unquoted::Unquoted};
|
||||
use crate::{Result, utils::exchange};
|
||||
use crate::{Result, smallstr::SmallString, utils::exchange};
|
||||
|
||||
pub const EMPTY: &str = "";
|
||||
|
||||
@@ -93,18 +93,34 @@ pub fn camel_to_snake_case<I, O>(output: &mut O, input: I) -> Result
|
||||
/// ```
|
||||
#[must_use]
|
||||
#[allow(clippy::string_slice)]
|
||||
pub fn common_prefix<'a>(choice: &'a [&str]) -> &'a str {
|
||||
pub fn common_prefix<T: AsRef<str>>(choice: &[T]) -> &str {
|
||||
choice.first().map_or(EMPTY, move |best| {
|
||||
choice.iter().skip(1).fold(*best, |best, choice| {
|
||||
&best[0..choice
|
||||
.char_indices()
|
||||
.zip(best.char_indices())
|
||||
.take_while(|&(a, b)| a == b)
|
||||
.count()]
|
||||
})
|
||||
choice
|
||||
.iter()
|
||||
.skip(1)
|
||||
.fold(best.as_ref(), |best, choice| {
|
||||
&best[0..choice
|
||||
.as_ref()
|
||||
.char_indices()
|
||||
.zip(best.char_indices())
|
||||
.take_while(|&(a, b)| a == b)
|
||||
.count()]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_small_string<const CAP: usize, T>(t: T) -> SmallString<[u8; CAP]>
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut ret = SmallString::<[u8; CAP]>::new();
|
||||
write!(&mut ret, "{t}").expect("Failed to Display type in SmallString");
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
/// Parses the bytes into a string.
|
||||
pub fn string_from_bytes(bytes: &[u8]) -> Result<String> {
|
||||
let str: &str = str_from_bytes(bytes)?;
|
||||
|
||||
@@ -16,7 +16,7 @@ fn common_prefix_empty() {
|
||||
|
||||
#[test]
|
||||
fn common_prefix_none() {
|
||||
let input = [];
|
||||
let input: [&str; 0] = [];
|
||||
let output = super::common_prefix(&input);
|
||||
assert_eq!(output, "");
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ async-channel.workspace = true
|
||||
const-str.workspace = true
|
||||
ctor.workspace = true
|
||||
futures.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
minicbor.workspace = true
|
||||
minicbor-serde.workspace = true
|
||||
|
||||
+1
-1
@@ -274,7 +274,7 @@ fn deserialize_newtype_struct<V>(self, name: &'static str, visitor: V) -> Result
|
||||
| "$serde_json::private::RawValue" => visitor.visit_map(self),
|
||||
| "Cbor" => visitor
|
||||
.visit_newtype_struct(&mut minicbor_serde::Deserializer::new(self.record_trail()))
|
||||
.map_err(|e| Self::Error::SerdeDe(e.to_string().into())),
|
||||
.map_err(|e| Self::Error::SerdeDe(format!("{name}: {e}").into())),
|
||||
|
||||
| _ => visitor.visit_newtype_struct(self),
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ fn descriptor_cf_options(
|
||||
opts.set_max_bytes_for_level_multiplier(1.0);
|
||||
opts.set_max_bytes_for_level_multiplier_additional(&desc.level_shape);
|
||||
|
||||
opts.set_disable_auto_compactions(desc.ignored);
|
||||
opts.set_compaction_style(desc.compaction);
|
||||
opts.set_compaction_pri(desc.compaction_pri);
|
||||
opts.set_universal_compaction_options(&uc_options(&desc));
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct Descriptor {
|
||||
pub(crate) name: &'static str,
|
||||
pub(crate) ignored: bool,
|
||||
pub(crate) dropped: bool,
|
||||
pub(crate) cache_disp: CacheDisp,
|
||||
pub(crate) key_size_hint: Option<usize>,
|
||||
@@ -52,6 +53,7 @@ pub(crate) enum CacheDisp {
|
||||
/// Base descriptor supplying common defaults to all derived descriptors.
|
||||
static BASE: Descriptor = Descriptor {
|
||||
name: EMPTY,
|
||||
ignored: false,
|
||||
dropped: false,
|
||||
cache_disp: CacheDisp::Shared,
|
||||
key_size_hint: None,
|
||||
@@ -83,8 +85,14 @@ pub(crate) enum CacheDisp {
|
||||
auto_readahead_max: 1024 * 1024 * 2,
|
||||
};
|
||||
|
||||
/// Placeholder descriptor for existing columns which have no description.
|
||||
/// Automatically generated on db open; should not appear in any schema.
|
||||
pub(crate) static IGNORED: Descriptor = Descriptor { ignored: true, ..BASE };
|
||||
|
||||
/// Tombstone descriptor for columns which have been or will be deleted.
|
||||
pub(crate) static DROPPED: Descriptor = Descriptor { dropped: true, ..BASE };
|
||||
/// Descriptors of this kind are explicitly set to delete data. Care should be
|
||||
/// taken when using this as it inhibits downgrading and other migrations.
|
||||
pub(crate) static DROPPED: Descriptor = Descriptor { dropped: true, ..IGNORED };
|
||||
|
||||
/// Descriptor for large datasets with random updates across the keyspace.
|
||||
pub(crate) static RANDOM: Descriptor = Descriptor {
|
||||
|
||||
+92
-33
@@ -4,16 +4,13 @@
|
||||
sync::{Arc, atomic::AtomicU32},
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
use rocksdb::{ColumnFamilyDescriptor, Options};
|
||||
use tuwunel_core::{Result, debug, implement, info, warn};
|
||||
use tuwunel_core::{Result, debug, debug_warn, implement, info, trace, warn};
|
||||
|
||||
use super::{
|
||||
Db, Engine,
|
||||
cf_opts::cf_options,
|
||||
context,
|
||||
db_opts::db_options,
|
||||
descriptor::{self, Descriptor},
|
||||
repair::repair,
|
||||
Db, Engine, cf_opts::cf_options, context, db_opts::db_options, descriptor,
|
||||
descriptor::Descriptor, repair::repair,
|
||||
};
|
||||
use crate::{Context, or_else};
|
||||
|
||||
@@ -31,7 +28,7 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
|
||||
&ctx.row_cache.lock().expect("row cache locked"),
|
||||
)?;
|
||||
|
||||
let cfds = Self::configure_cfds(&ctx, &db_opts, desc)?;
|
||||
let (cfds, dropped) = Self::configure_cfds(&ctx, &db_opts, desc)?;
|
||||
let num_cfds = cfds.len();
|
||||
debug!("Configured {num_cfds} column descriptors...");
|
||||
|
||||
@@ -50,6 +47,13 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
|
||||
}
|
||||
.or_else(or_else)?;
|
||||
|
||||
if !config.rocksdb_read_only && !config.rocksdb_secondary {
|
||||
for name in &dropped {
|
||||
debug!("Deleting dropped column {name:?} ...");
|
||||
db.drop_cf(name).or_else(or_else)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
columns = num_cfds,
|
||||
sequence = %db.latest_sequence_number(),
|
||||
@@ -74,59 +78,114 @@ fn configure_cfds(
|
||||
ctx: &Arc<Context>,
|
||||
db_opts: &Options,
|
||||
desc: &[Descriptor],
|
||||
) -> Result<Vec<ColumnFamilyDescriptor>> {
|
||||
) -> Result<(Vec<ColumnFamilyDescriptor>, Vec<String>)> {
|
||||
let server = &ctx.server;
|
||||
let config = &server.config;
|
||||
let path = &config.database_path;
|
||||
let existing = Self::discover_cfs(path, db_opts);
|
||||
|
||||
let creating = desc
|
||||
.iter()
|
||||
.filter(|desc| !existing.contains(desc.name));
|
||||
|
||||
// Found columns which are not described.
|
||||
let missing = existing
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.filter(|&name| name != "default")
|
||||
.filter(|&name| !desc.iter().any(|desc| desc.name == name));
|
||||
|
||||
// Described columns which are not found.
|
||||
let creating = desc
|
||||
.iter()
|
||||
.filter(|desc| !desc.dropped)
|
||||
.filter(|desc| !existing.contains(desc.name));
|
||||
|
||||
// Found columns which are described as dropped.
|
||||
let dropping = desc
|
||||
.iter()
|
||||
.filter(|desc| desc.dropped)
|
||||
.filter(|desc| existing.contains(desc.name))
|
||||
.filter(|_| !config.rocksdb_never_drop_columns);
|
||||
|
||||
// Described dropped columns which are no longer found.
|
||||
let dropped = desc
|
||||
.iter()
|
||||
.filter(|desc| desc.dropped)
|
||||
.filter(|desc| !existing.contains(desc.name));
|
||||
|
||||
debug!(
|
||||
existing = existing.len(),
|
||||
described = desc.len(),
|
||||
missing = missing.clone().count(),
|
||||
dropped = dropped.clone().count(),
|
||||
creating = creating.clone().count(),
|
||||
dropping = dropping.clone().count(),
|
||||
"Discovered database columns"
|
||||
);
|
||||
|
||||
missing.clone().for_each(|name| {
|
||||
debug!("Found unrecognized column {name:?} in existing database.");
|
||||
debug_warn!("Found undescribed column {name:?} in existing database.");
|
||||
});
|
||||
|
||||
creating.map(|desc| desc.name).for_each(|name| {
|
||||
debug!("Creating new column {name:?} not previously found in existing database.");
|
||||
});
|
||||
|
||||
let missing_descriptors = missing
|
||||
dropped
|
||||
.clone()
|
||||
.filter(|_| config.rocksdb_drop_missing_columns)
|
||||
.map(|_| descriptor::DROPPED);
|
||||
.map(|desc| desc.name)
|
||||
.for_each(|name| {
|
||||
debug!("Previously dropped column {name:?} no longer found in database.");
|
||||
});
|
||||
|
||||
let cfopts: Vec<_> = desc
|
||||
creating
|
||||
.clone()
|
||||
.map(|desc| desc.name)
|
||||
.for_each(|name| {
|
||||
debug!("Creating new column {name:?} not previously found in existing database.");
|
||||
});
|
||||
|
||||
dropping
|
||||
.clone()
|
||||
.map(|desc| desc.name)
|
||||
.for_each(|name| {
|
||||
warn!(
|
||||
"Column {name:?} has been scheduled for deletion. Storage may not appear \
|
||||
reclaimed until further restart or compaction."
|
||||
);
|
||||
});
|
||||
|
||||
let not_dropped = |desc: &&Descriptor| {
|
||||
!dropped
|
||||
.clone()
|
||||
.any(|dropped| desc.name == dropped.name)
|
||||
};
|
||||
|
||||
let dropping = dropping
|
||||
.map(|desc| desc.name)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect();
|
||||
|
||||
let cfnames = desc
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(missing_descriptors)
|
||||
.map(|ref desc| cf_options(ctx, db_opts.clone(), desc))
|
||||
.collect::<Result<_>>()?;
|
||||
.filter(not_dropped)
|
||||
.map(|desc| desc.name)
|
||||
.chain(missing.clone());
|
||||
|
||||
let cfds: Vec<_> = desc
|
||||
.iter()
|
||||
.map(|desc| desc.name)
|
||||
.map(ToOwned::to_owned)
|
||||
.chain(missing.cloned())
|
||||
.zip(cfopts.into_iter())
|
||||
.map(|(name, opts)| ColumnFamilyDescriptor::new(name, opts))
|
||||
.collect();
|
||||
.filter(not_dropped)
|
||||
.copied()
|
||||
.chain(missing.map(|_| descriptor::IGNORED))
|
||||
.zip(cfnames)
|
||||
.inspect(|&(desc, name)| {
|
||||
assert!(
|
||||
desc.ignored || desc.name == name,
|
||||
"{name:?} does not match descriptor {:?}",
|
||||
desc.name
|
||||
);
|
||||
})
|
||||
.inspect(|&(_, name)| debug!(name, "Described column"))
|
||||
.map(|(desc, name)| (desc, name.to_owned()))
|
||||
.map(|(desc, name)| Ok((name, cf_options(ctx, db_opts.clone(), &desc)?)))
|
||||
.map_ok(|(name, opts)| ColumnFamilyDescriptor::new(name, opts))
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(cfds)
|
||||
trace!(?dropping);
|
||||
Ok((cfds, dropping))
|
||||
}
|
||||
|
||||
#[implement(Engine)]
|
||||
|
||||
@@ -20,4 +20,6 @@ pub fn remove<K>(&self, key: &K)
|
||||
if !self.engine.corked() {
|
||||
self.engine.flush().expect("database flush error");
|
||||
}
|
||||
|
||||
self.notify(key.as_ref());
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
use futures::pin_mut;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::watch::{Sender, channel};
|
||||
use tuwunel_core::implement;
|
||||
use tuwunel_core::{debug, implement, smallvec::SmallVec};
|
||||
|
||||
use crate::keyval::{KeyBuf, serialize_key};
|
||||
|
||||
type Watchers = Mutex<BTreeMap<KeyBuf, Sender<()>>>;
|
||||
type KeyVec = SmallVec<[KeyBuf; 1]>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Watch {
|
||||
@@ -57,6 +58,14 @@ pub fn watch_raw_prefix<'a, K>(&self, prefix: &'a K) -> impl Future<Output = ()>
|
||||
}
|
||||
|
||||
#[implement(super::Map)]
|
||||
#[tracing::instrument(
|
||||
level = "trace",
|
||||
skip_all,
|
||||
fields(
|
||||
map = self.name(),
|
||||
key = str::from_utf8(key.as_ref()).unwrap_or("<binary>"),
|
||||
)
|
||||
)]
|
||||
pub(crate) fn notify<K>(&self, key: &K)
|
||||
where
|
||||
K: AsRef<[u8]> + Ord + ?Sized,
|
||||
@@ -65,15 +74,20 @@ pub(crate) fn notify<K>(&self, key: &K)
|
||||
|
||||
let mut watchers = self.watch.watchers.lock().expect("locked");
|
||||
|
||||
watchers
|
||||
let num_notified = watchers
|
||||
.range(range)
|
||||
.rev()
|
||||
.take_while(|(k, _)| key.as_ref().starts_with(k))
|
||||
.filter_map(|(k, tx)| tx.send(()).is_err().then_some(k))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<KeyVec>()
|
||||
.into_iter()
|
||||
.for_each(|k| {
|
||||
watchers.remove(&k);
|
||||
.fold(0_usize, |num_notified, key| {
|
||||
watchers.remove(&key);
|
||||
num_notified.saturating_add(1)
|
||||
});
|
||||
|
||||
if num_notified > 0 {
|
||||
debug!(watchers = watchers.len(), num_notified, "notified");
|
||||
}
|
||||
}
|
||||
|
||||
+27
-4
@@ -16,6 +16,7 @@ pub(super) fn open(engine: &Arc<Engine>) -> Result<Maps> { open_list(engine, MAP
|
||||
#[tracing::instrument(name = "maps", level = "debug", skip_all)]
|
||||
pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
maps.iter()
|
||||
.filter(|desc| !desc.dropped)
|
||||
.map(|desc| Ok((desc.name, Map::open(engine, desc.name)?)))
|
||||
.collect()
|
||||
}
|
||||
@@ -165,6 +166,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
|
||||
name: "roomid_joinedcount",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomid_maxremotepowerlevel",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomid_pduleaves",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -183,6 +188,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
|
||||
name: "roomserverids",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomsynctoken_shortstatehash",
|
||||
..descriptor::DROPPED
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomuserdataid_accountdata",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -200,6 +209,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
|
||||
name: "roomuserid_lastprivatereadupdate",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomuserid_lastnotificationread",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomuserid_leftcount",
|
||||
val_size_hint: Some(8),
|
||||
@@ -337,6 +350,11 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
|
||||
name: "userdeviceid_metadata",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userdeviceconnid_conn",
|
||||
block_size: 1024 * 16,
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userdeviceid_refresh",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -365,6 +383,10 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
|
||||
name: "userid_blurhash",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_dehydrateddevice",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_devicelistversion",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -401,6 +423,11 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
|
||||
name: "userid_usersigningkeyid",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "useridcount_notification",
|
||||
limit_size: 1024 * 1024 * 256,
|
||||
..descriptor::RANDOM_SMALL_CACHE
|
||||
},
|
||||
Descriptor {
|
||||
name: "useridprofilekey_value",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -429,8 +456,4 @@ pub(super) fn open_list(engine: &Arc<Engine>, maps: &[Descriptor]) -> Result<Map
|
||||
name: "userroomid_notificationcount",
|
||||
..descriptor::RANDOM
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomid_maxremotepowerlevel",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
];
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#![type_length_limit = "8192"]
|
||||
#![type_length_limit = "65536"]
|
||||
|
||||
extern crate rust_rocksdb as rocksdb;
|
||||
|
||||
|
||||
+48
-1
@@ -9,7 +9,7 @@
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Ignore, Interfix, de, ser,
|
||||
Cbor, Ignore, Interfix, de, ser,
|
||||
ser::{Json, serialize_to_vec},
|
||||
};
|
||||
|
||||
@@ -204,6 +204,53 @@ fn ser_json_raw_json() {
|
||||
assert_eq!(&s, r#"{"event_fields":["content.body"]}"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ser_cbor() {
|
||||
use tuwunel_core::ruma::api::client::filter::FilterDefinition;
|
||||
|
||||
let filter = FilterDefinition {
|
||||
event_fields: Some(vec!["content.body".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let serialized = serialize_to_vec(Cbor(&filter)).expect("failed to serialize cbor");
|
||||
let deserialized: FilterDefinition = de::from_slice::<Cbor<_>>(&serialized)
|
||||
.expect("failed to deserialize cbor")
|
||||
.0;
|
||||
|
||||
assert_eq!(filter.event_fields, deserialized.event_fields);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(disable)]
|
||||
fn ser_cbor_ruma_raw() {
|
||||
use serde_json::value::RawValue;
|
||||
use tuwunel_core::ruma::api::client::filter::FilterDefinition;
|
||||
|
||||
struct Foo {
|
||||
a: String,
|
||||
b: Box<RawValue>,
|
||||
}
|
||||
|
||||
let filter = FilterDefinition {
|
||||
event_fields: Some(vec!["content.body".to_owned()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let foo = Foo {
|
||||
a: "test".into(),
|
||||
b: serde_json::value::to_raw_value(&filter).expect("failed to serialize to raw value"),
|
||||
};
|
||||
|
||||
let serialized = serialize_to_vec(Cbor(&foo)).expect("failed to serialize cbor");
|
||||
let deserialized: Foo = de::from_slice::<Cbor<_>>(&serialized)
|
||||
.expect("failed to deserialize cbor")
|
||||
.0;
|
||||
|
||||
assert_eq!(foo.a, deserialized.a);
|
||||
assert_eq!(foo.a.get(), deserialized.b.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn de_tuple() {
|
||||
let user_id: &UserId = "@user:example.com".try_into().unwrap();
|
||||
|
||||
@@ -169,10 +169,11 @@ pub async fn last_count<'a>(
|
||||
&'a self,
|
||||
room_id: Option<&'a RoomId>,
|
||||
user_id: &'a UserId,
|
||||
upper: u64,
|
||||
upper: Option<u64>,
|
||||
) -> Result<u64> {
|
||||
type Key<'a> = (Option<&'a RoomId>, &'a UserId, u64, Ignore);
|
||||
|
||||
let upper = upper.unwrap_or(u64::MAX);
|
||||
let key = (room_id, user_id, upper, Interfix);
|
||||
self.db
|
||||
.roomuserdataid_accountdata
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
use ruma::{UserId, events::TimelineEventType};
|
||||
use tuwunel_core::{
|
||||
Result, error, implement,
|
||||
matrix::{
|
||||
event::Event,
|
||||
pdu::{Pdu, RawPduId},
|
||||
},
|
||||
utils::ReadyExt,
|
||||
};
|
||||
|
||||
use super::{NamespaceRegex, RegistrationInfo};
|
||||
|
||||
/// Called by timeline::append() after accepting new PDU.
|
||||
#[implement(super::Service)]
|
||||
#[tracing::instrument(name = "append", level = "debug", skip_all)]
|
||||
pub(crate) async fn append_pdu(&self, pdu_id: RawPduId, pdu: &Pdu) -> Result {
|
||||
for appservice in self.read().await.values() {
|
||||
self.append_pdu_to(appservice, pdu_id, pdu)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
error!(
|
||||
event_id = %pdu.event_id(),
|
||||
appservice = ?appservice.registration.id,
|
||||
"Failed to send PDU to appservice: {e}"
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[implement(super::Service)]
|
||||
#[tracing::instrument(
|
||||
name = "append_to",
|
||||
level = "debug",
|
||||
skip_all,
|
||||
fields(id = %appservice.registration.id),
|
||||
)]
|
||||
async fn append_pdu_to(
|
||||
&self,
|
||||
appservice: &RegistrationInfo,
|
||||
pdu_id: RawPduId,
|
||||
pdu: &Pdu,
|
||||
) -> Result {
|
||||
if self
|
||||
.services
|
||||
.state_cache
|
||||
.appservice_in_room(pdu.room_id(), appservice)
|
||||
.await
|
||||
{
|
||||
self.services
|
||||
.sending
|
||||
.send_pdu_appservice(appservice.registration.id.clone(), pdu_id)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If the RoomMember event has a non-empty state_key, it is targeted at someone.
|
||||
// If it is our appservice user, we send this PDU to it.
|
||||
if *pdu.kind() == TimelineEventType::RoomMember {
|
||||
if let Some(state_key_uid) = &pdu
|
||||
.state_key
|
||||
.as_ref()
|
||||
.and_then(|state_key| UserId::parse(state_key.as_str()).ok())
|
||||
{
|
||||
let appservice_uid = appservice.registration.sender_localpart.as_str();
|
||||
if state_key_uid == &appservice_uid {
|
||||
self.services
|
||||
.sending
|
||||
.send_pdu_appservice(appservice.registration.id.clone(), pdu_id)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let matching_users = |users: &NamespaceRegex| {
|
||||
appservice.users.is_match(pdu.sender().as_str())
|
||||
|| *pdu.kind() == TimelineEventType::RoomMember
|
||||
&& pdu
|
||||
.state_key
|
||||
.as_ref()
|
||||
.is_some_and(|state_key| users.is_match(state_key))
|
||||
};
|
||||
let matching_aliases = |aliases: NamespaceRegex| {
|
||||
self.services
|
||||
.alias
|
||||
.local_aliases_for_room(pdu.room_id())
|
||||
.ready_any(move |room_alias| aliases.is_match(room_alias.as_str()))
|
||||
};
|
||||
|
||||
if matching_aliases(appservice.aliases.clone()).await
|
||||
|| appservice.rooms.is_match(pdu.room_id().as_str())
|
||||
|| matching_users(&appservice.users)
|
||||
{
|
||||
self.services
|
||||
.sending
|
||||
.send_pdu_appservice(appservice.registration.id.clone(), pdu_id)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod append;
|
||||
mod namespace_regex;
|
||||
mod registration_info;
|
||||
pub(crate) mod request;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
@@ -14,6 +16,7 @@
|
||||
use tuwunel_core::{Err, Result, debug, err, utils::stream::IterStream};
|
||||
use tuwunel_database::Map;
|
||||
|
||||
pub(crate) use self::request::send_request;
|
||||
pub use self::{namespace_regex::NamespaceRegex, registration_info::RegistrationInfo};
|
||||
|
||||
pub struct Service {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user