Compare commits

..

2 Commits

Author SHA1 Message Date
timedout 0514491189 chore: Bump version & cut changelog 2026-05-07 21:19:40 +01:00
timedout a30c355c03 fix: Restore event auth check 4 in v12 rooms
Reviewed-By: Jacob Taylor <jacob@explodie.org>
2026-05-07 21:11:54 +01:00
279 changed files with 8502 additions and 7042 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ runs:
- name: Install timelord-cli and git-warp-time
if: steps.check-binaries.outputs.need-install == 'true'
uses: https://github.com/taiki-e/install-action@b5fddbb5361bce8a06fb168c9d403a6cc552b084 # v2
uses: https://github.com/taiki-e/install-action@787505cde8a44ea468a00478fe52baf23b15bccd # v2
with:
tool: git-warp-time,timelord-cli@3.0.1
+2
View File
@@ -45,6 +45,7 @@ If you aren't sure about some, feel free to ask for clarification in #dev:contin
- [ ] I have [tested my contribution][c1t] (or proof-read it for documentation-only changes)
myself, if applicable. This includes ensuring code compiles.
- [ ] My commit messages follow the [commit message format][c1cm] and are descriptive.
- [ ] I have written a [news fragment][n1] for this PR, if applicable<!--(can be done after hitting open!)-->.
<!--
Notes on these requirements:
@@ -78,3 +79,4 @@ Notes on these requirements:
[c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks
[c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally
[c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
[n1]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
+1 -1
View File
@@ -24,7 +24,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/crate-ci/typos
rev: v1.46.0
rev: v1.45.2
hooks:
- id: typos
- id: typos
+11
View File
@@ -1,3 +1,14 @@
# Continuwuity 0.5.9 (2026-05-07)
## Security
- Fixed a bug that caused the server to drop events during processing if several events for the same room were sent in a
singular transaction. Contributed by @nex. (#1711)
- Fixed a bug that caused the server to panic while handling transactions with malformed events, resulting in a
deadlock that prevented the remote server from federating with us until the server was restarted. Contributed by @nex.
- Fixed a bug that resulted in the `m.federate` field of `m.room.create` being ignored in v12 rooms, allowing remote
servers to participate in local-only rooms. Contributed by @nex, reported by [@mat:emzee.ee](matrix:u/mat:emzee.ee?action=chat).
# Continuwuity 0.5.8 (2026-04-24)
## Features
Generated
+479 -290
View File
File diff suppressed because it is too large Load Diff
+36 -37
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "0.5.8"
version = "0.5.9"
[workspace.metadata.crane]
name = "conduwuit"
@@ -39,10 +39,7 @@ features = ["ffi", "std", "union"]
version = "1.1.0"
[workspace.dependencies.ctor]
version = "0.13.0"
[workspace.dependencies.dtor]
version = "0.13.0"
version = "0.10.0"
[workspace.dependencies.cargo_toml]
version = "0.22"
@@ -71,7 +68,7 @@ default-features = false
version = "0.1.3"
[workspace.dependencies.rand]
version = "0.10.1"
version = "0.10.0"
# Used for the http request / response body type for Ruma endpoints used with reqwest
[workspace.dependencies.bytes]
@@ -164,7 +161,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.25"
version = "0.0.24"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -345,50 +342,51 @@ version = "0.1.88"
[workspace.dependencies.lru-cache]
version = "0.1.2"
[workspace.dependencies.assign]
version = "1.1.1"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
# version = "0.14.1"
git = "https://github.com/ruma/ruma.git"
rev = "5742fec0021b85fedbf5cd1f59c50a00bb5b9f7c"
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes"
rev = "d00b51a8669b21689c4eb47fb81f3a8b27c3e371"
features = [
"compat",
"rand",
"appservice-api-c",
"client-api",
"federation-api",
"push-gateway-api-c",
"state-res",
"rand",
"markdown",
"push-gateway-api-c",
"unstable-exhaustive-types",
"ring-compat",
"compat-upload-signatures",
"compat-optional-txn-pdus",
"identifiers-validation",
"unstable-unspecified",
"unstable-msc2448",
"unstable-msc2666",
"unstable-msc2867",
"unstable-msc2870",
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3814",
"unstable-msc3245",
"unstable-msc3381",
"unstable-msc3489",
"unstable-msc3930",
"unstable-msc3266",
"unstable-msc3381", # polls
"unstable-msc3489", # beacon / live location
"unstable-msc3575",
"unstable-msc3930", # polls push rules
"unstable-msc4075",
"unstable-msc4095",
"unstable-msc4121",
"unstable-msc4125",
"unstable-msc4155",
"unstable-msc4186",
"unstable-msc4195",
"unstable-msc4203",
"unstable-msc4310",
"unstable-msc4373",
"unstable-msc4380",
"unstable-msc4143",
"unstable-msc4293",
"unstable-msc4406",
"unstable-msc4439",
"unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions
"unstable-extensible-events",
"unstable-pdu",
"unstable-msc4155",
"unstable-msc4143", # livekit well_known response
"unstable-msc4284",
"unstable-msc4439", # pgp_key in .well_known/matrix/support
]
[workspace.dependencies.rust-rocksdb]
@@ -433,7 +431,7 @@ features = ["http", "grpc-tonic", "trace", "logs", "metrics"]
# optional sentry metrics for crash/panic reporting
[workspace.dependencies.sentry]
version = "0.48.0"
version = "0.47.0"
default-features = false
features = [
"backtrace",
@@ -448,9 +446,9 @@ features = [
]
[workspace.dependencies.sentry-tracing]
version = "0.48.0"
version = "0.47.0"
[workspace.dependencies.sentry-tower]
version = "0.48.0"
version = "0.47.0"
# jemalloc usage
[workspace.dependencies.tikv-jemalloc-sys]
@@ -549,11 +547,16 @@ features = ["std"]
[workspace.dependencies.maplit]
version = "1.0.2"
[workspace.dependencies.ldap3]
version = "0.12.0"
default-features = false
features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.yansi]
version = "1.0.1"
[workspace.dependencies.askama]
version = "0.16.0"
version = "0.15.0"
[workspace.dependencies.lettre]
version = "0.11.19"
@@ -655,10 +658,6 @@ default-features = false
package = "conduwuit"
path = "src/main"
[workspace.dependencies.ruminuwuity]
package = "ruminuwuity"
path = "src/ruminuwuity"
###############################################################################
#
# Release profiles
-1
View File
@@ -1 +0,0 @@
The invite recipient's membership event is now included in invite stripped state, which should fix flaky invite display in some clients. Contributed by @ginger
-1
View File
@@ -1 +0,0 @@
Switched from Continuwuity's fork of Ruma back to upstream Ruma. Contributed by @ginger.
-1
View File
@@ -1 +0,0 @@
Removed support for LDAP.
-1
View File
@@ -1 +0,0 @@
Clarified in the config that `max_request_size` affects federated media as well.
-1
View File
@@ -1 +0,0 @@
Added support for fallback encryption keys.
-1
View File
@@ -1 +0,0 @@
Fixed a bug that caused the server to drop events during processing if several events for the same room were sent in a singular transaction. Contributed by @nex.
+108 -1
View File
@@ -291,7 +291,6 @@
#ip_lookup_strategy = 5
# Max request size for file uploads in bytes. Defaults to 20MB.
# Also limits incoming federated media.
#
#max_request_size = 20971520
@@ -574,6 +573,18 @@
#
#allow_public_room_directory_over_federation = false
# Allow guests/unauthenticated users to access TURN credentials.
#
# This is the equivalent of Synapse's `turn_allow_guests` config option.
# This allows any unauthenticated user to call the endpoint
# `/_matrix/client/v3/voip/turnServer`.
#
# It is unlikely you need to enable this as all major clients support
# authentication for this endpoint and prevents misuse of your TURN server
# from potential bots.
#
#turn_allow_guests = false
# Set this to true to lock down your server's public room directory and
# only allow admins to publish rooms to the room directory. Unpublishing
# is still allowed by all users with this enabled.
@@ -1934,6 +1945,102 @@
#
#foci = []
[global.ldap]
# Whether to enable LDAP login.
#
# example: "true"
#
#enable = false
# Whether to force LDAP authentication or authorize classical password
# login.
#
# example: "true"
#
#ldap_only = false
# URI of the LDAP server.
#
# example: "ldap://ldap.example.com:389"
#
#uri = ""
# StartTLS for LDAP connections.
#
#use_starttls = false
# Skip TLS certificate verification, possibly dangerous.
#
#disable_tls_verification = false
# Root of the searches.
#
# example: "ou=users,dc=example,dc=org"
#
#base_dn = ""
# Bind DN if anonymous search is not enabled.
#
# You can use the variable `{username}` that will be replaced by the
# entered username. In such case, the password used to bind will be the
# one provided for the login and not the one given by
# `bind_password_file`. Beware: automatically granting admin rights will
# not work if you use this direct bind instead of a LDAP search.
#
# example: "cn=ldap-reader,dc=example,dc=org" or
# "cn={username},ou=users,dc=example,dc=org"
#
#bind_dn = ""
# Path to a file on the system that contains the password for the
# `bind_dn`.
#
# The server must be able to access the file, and it must not be empty.
#
#bind_password_file = ""
# Search filter to limit user searches.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# example: "(&(objectClass=person)(memberOf=matrix))"
#
#filter = "(objectClass=*)"
# Attribute to use to uniquely identify the user.
#
# example: "uid" or "cn"
#
#uid_attribute = "uid"
# Attribute containing the display name of the user.
#
# example: "givenName" or "sn"
#
#name_attribute = "givenName"
# Root of the searches for admin users.
#
# Defaults to `base_dn` if empty.
#
# example: "ou=admins,dc=example,dc=org"
#
#admin_base_dn = ""
# The LDAP search filter to find administrative users for continuwuity.
#
# If left blank, administrative state must be configured manually for each
# user.
#
# You can use the variable `{username}` that will be replaced by the
# entered username for more complex filters.
#
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
#
#admin_filter = ""
#[global.antispam]
#[global.antispam.meowlnir]
+1 -1
View File
@@ -50,7 +50,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.19.0
ENV BINSTALL_VERSION=1.18.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+1 -1
View File
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.19.0
ENV BINSTALL_VERSION=1.18.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+2
View File
@@ -81,6 +81,8 @@ changes.
All forked dependencies are maintained under the
[continuwuation organization on Forgejo](https://forgejo.ellis.link/continuwuation):
- [ruwuma][continuwuation-ruwuma] - Fork of [ruma/ruma][ruma] with various
performance improvements, more features and better client/server interop
- [rocksdb][continuwuation-rocksdb] - Fork of [facebook/rocksdb][rocksdb] via
[`@zaidoon1`][8] with liburing build fixes and GCC debug build fixes
- [jemallocator][continuwuation-jemallocator] - Fork of
Generated
+15 -15
View File
@@ -3,11 +3,11 @@
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1777645914,
"narHash": "sha256-P1T7QVQS13OvkXEuEhI91CLaQfyv6iqV9vW8IBLLDYg=",
"lastModified": 1775907537,
"narHash": "sha256-vbeLNgmsx1Z6TwnlDV0dKyeBCcon3UpkV9yLr/yc6HM=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "d6ba1f7070ba91f45efe372d68eb648be67d0417",
"rev": "d99f7b9eb81731bddebf80a355f8be7b2f8b1b28",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"crane": {
"locked": {
"lastModified": 1777335812,
"narHash": "sha256-bEg5xoAxAwsyfnGhkEX7RJViTIBIYPd8ISg4O1c0HFc=",
"lastModified": 1775839657,
"narHash": "sha256-SPm9ck7jh3Un9nwPuMGbRU04UroFmOHjLP56T10MOeM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "5e0fb2f64edff2822249f21293b8304dedaaf676",
"rev": "7cf72d978629469c4bd4206b95c402514c1f6000",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1777624102,
"narHash": "sha256-thSyElkje577x/kAbP72nHlfiFc1a+tCudskLPHXe9s=",
"lastModified": 1775891769,
"narHash": "sha256-EOfVlTKw2n8w1uhfh46GS4hEGnQ7oWrIWQfIY6utIkI=",
"owner": "nix-community",
"repo": "fenix",
"rev": "4d81601e0b73f20d81d066754ad0e7d1e7f75a06",
"rev": "6fbc54dde15aee725bdc7aae5e478849685d5f56",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"lastModified": 1775710090,
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1777583169,
"narHash": "sha256-dVJ4+wrRKc8oIgp3rLOFSq1obt/sCKlXy3h47qof/w0=",
"lastModified": 1775843361,
"narHash": "sha256-j53ZgyDvmYf3Sjh1IPvvTjqa614qUfVQSzj59+MpzkY=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "aa64e4828a2bbba44463c1229a81c748d3cce583",
"rev": "9eb97ea96d8400e8957ddd56702e962614296583",
"type": "github"
},
"original": {
+132 -111
View File
@@ -16,24 +16,26 @@
}
},
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
"integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
"integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -45,6 +47,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -106,9 +109,9 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
"integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -125,13 +128,13 @@
}
},
"node_modules/@rsbuild/core": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.3.tgz",
"integrity": "sha512-2myp7jUgGen50saxW8OJD/eMVKp7HnuBN5MUzwRb6mDbRZZVpoorfI4LQqiGSBNjGLB6jltvx/R2yHmcmnchwg==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rsbuild/core/-/core-2.0.0-rc.1.tgz",
"integrity": "sha512-eqxtRlQiFSm/ibCNGiPj8ozsGSNK91NY+GksmPuTCPmWQExGtPqM1V+s13UYeWZS6fYbMRs7NlQKD896e0QkKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/core": "~2.0.1",
"@rspack/core": "2.0.0-rc.1",
"@swc/helpers": "^0.5.21"
},
"bin": {
@@ -150,17 +153,17 @@
}
},
"node_modules/@rsbuild/plugin-react": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-2.0.0.tgz",
"integrity": "sha512-/1gzt39EGUSFEqB83g46QoOwsgv172HI18i6au1b6lgIaX4sv9stuX4ijdHbHCp8PqYEq+MyQ99jIQMO6I+etg==",
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@rsbuild/plugin-react/-/plugin-react-1.4.6.tgz",
"integrity": "sha512-LAT6xHlEyZKA0VjF/ph5d50iyG+WSmBx+7g98HNZUwb94VeeTMZFB8qVptTkbIRMss3BNKOXmHOu71Lhsh9oEw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/plugin-react-refresh": "2.0.0",
"@rspack/plugin-react-refresh": "^1.6.1",
"react-refresh": "^0.18.0"
},
"peerDependencies": {
"@rsbuild/core": "^2.0.0-0"
"@rsbuild/core": "^1.0.0 || ^2.0.0-0"
},
"peerDependenciesMeta": {
"@rsbuild/core": {
@@ -169,28 +172,28 @@
}
},
"node_modules/@rspack/binding": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.1.tgz",
"integrity": "sha512-ynV1gw4KqFtQ0P+ZZh76SUj49wBb2FuHW3zSmHverHWuxBhzvrZS6/dZ+fCFQG8bTTPtrPz0RQUTN3uEDbPVBQ==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.0.0-rc.1.tgz",
"integrity": "sha512-rhJqtbyiRPOjTAZW0xTZFbOrS5yP5yL1SF0DPE9kvFfzePz30IqjMDMxL0KuhkDZd/M1eUINJyoqd8NTbR9wHw==",
"dev": true,
"license": "MIT",
"optionalDependencies": {
"@rspack/binding-darwin-arm64": "2.0.1",
"@rspack/binding-darwin-x64": "2.0.1",
"@rspack/binding-linux-arm64-gnu": "2.0.1",
"@rspack/binding-linux-arm64-musl": "2.0.1",
"@rspack/binding-linux-x64-gnu": "2.0.1",
"@rspack/binding-linux-x64-musl": "2.0.1",
"@rspack/binding-wasm32-wasi": "2.0.1",
"@rspack/binding-win32-arm64-msvc": "2.0.1",
"@rspack/binding-win32-ia32-msvc": "2.0.1",
"@rspack/binding-win32-x64-msvc": "2.0.1"
"@rspack/binding-darwin-arm64": "2.0.0-rc.1",
"@rspack/binding-darwin-x64": "2.0.0-rc.1",
"@rspack/binding-linux-arm64-gnu": "2.0.0-rc.1",
"@rspack/binding-linux-arm64-musl": "2.0.0-rc.1",
"@rspack/binding-linux-x64-gnu": "2.0.0-rc.1",
"@rspack/binding-linux-x64-musl": "2.0.0-rc.1",
"@rspack/binding-wasm32-wasi": "2.0.0-rc.1",
"@rspack/binding-win32-arm64-msvc": "2.0.0-rc.1",
"@rspack/binding-win32-ia32-msvc": "2.0.0-rc.1",
"@rspack/binding-win32-x64-msvc": "2.0.0-rc.1"
}
},
"node_modules/@rspack/binding-darwin-arm64": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.1.tgz",
"integrity": "sha512-CGFO5zmajD1Itch1lxAI7+gvKiagzyqXopHv/jHG9Su2WWQ2/Nhn2/rkSpdp6ptE9ri6+6tCOOahf099/v/Xog==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-2.0.0-rc.1.tgz",
"integrity": "sha512-fYbeDDDg6QKZzXYt/J0/j0Qhr01wQLuISUsYnNhu5MLwdXVUSVcqz+CTqgF3d0EQVVn6FqLV63lbNRzUGfSq9g==",
"cpu": [
"arm64"
],
@@ -202,9 +205,9 @@
]
},
"node_modules/@rspack/binding-darwin-x64": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.1.tgz",
"integrity": "sha512-2vvBNBoS09/PurupBwSrlTZd8283o00B8v20ncsNUdEff41uCR/hzIrYoTIVWnVST+Gt5O1+cfcfORp397lajg==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-2.0.0-rc.1.tgz",
"integrity": "sha512-MvXi9kr8xXn1y0PD1WI/4YphRNOdbykJjKdEsAG4JxEVoERmhIHOTwKvUqlejajizAwlVZcxQl/FacoPLsKN5Q==",
"cpu": [
"x64"
],
@@ -216,9 +219,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-gnu": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.1.tgz",
"integrity": "sha512-uvNXk6ahE3AH3h2avnd1Mgno68YQpS4cfX1OkOGWIC/roL+NrOP2XVXV4yfVAoydPALDO7AfbIfN0QdmBK3rsA==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-2.0.0-rc.1.tgz",
"integrity": "sha512-j6WsHEwGSdUoiy4BsQBW0RjFl+MBzozdybSYhkiyVSoHlbm7CPt3XaaS3elH5YcwuLHORmVHPP91QhwWl9UFJg==",
"cpu": [
"arm64"
],
@@ -233,9 +236,9 @@
]
},
"node_modules/@rspack/binding-linux-arm64-musl": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.1.tgz",
"integrity": "sha512-S/a6uN9PiZ5O/PjSqyIXhuRC1lVzeJkJV69NeLk5sIEUiDQ/aQGZG97uN+tluwpbo1tPbLJkdHYETfjspOX4Pg==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-2.0.0-rc.1.tgz",
"integrity": "sha512-MPoZE0aS8oH+Wr0R5tIYch8gbUwYYf4LsiGdP6enMKMTrmpJyOVGlhPHVSwsrFgBg7fjTGOuxHuibtsvDUdLOQ==",
"cpu": [
"arm64"
],
@@ -250,9 +253,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-gnu": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.1.tgz",
"integrity": "sha512-C13Kk0OkZiocZVj187Sf753UH6pDXnuEu6vzUvi3qv9ltibG1ki0H2Y8isXBYL2cHQOV+hk0g1S6/4z3TTB97A==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-2.0.0-rc.1.tgz",
"integrity": "sha512-gOlPCwtIg9GsFG/8ZdUyV5SyXDaGq2kmtXmyyFU7RO33MaalltNEBMf2hevRPj9z39eSzxwgJDonMOdx5Fo0Og==",
"cpu": [
"x64"
],
@@ -267,9 +270,9 @@
]
},
"node_modules/@rspack/binding-linux-x64-musl": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.1.tgz",
"integrity": "sha512-TQsiBFpEDGkuvK9tNdGj/Uc+AIytzqhxXH/1jKU6M24cWB1DTw/Cx7DdrkCBDyq3129K3POLdujvbWCGqBzQUw==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-2.0.0-rc.1.tgz",
"integrity": "sha512-K6Swk1rfP4z4b6bp84NlikGlUWMOPpIWCtlPr/W0TWgc2C/cd844oHdoIu7WtmOH7y9AwB5UG2bWpgFAVwykCw==",
"cpu": [
"x64"
],
@@ -284,9 +287,9 @@
]
},
"node_modules/@rspack/binding-wasm32-wasi": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.1.tgz",
"integrity": "sha512-wk3gyUgBW/ayP49bI54bkY8+EQnfBHxdoe9dz3oobSTZQc8AOWwmUUDEPltW8rUvPOM6dfHECTOUMnfaf2f5yA==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-2.0.0-rc.1.tgz",
"integrity": "sha512-aa9oUTqOb1QjwsHVlMr5sV+7mcBI4MLQ/xhFO2CIEcfVnJIPl8XpKUbDEgqMwcFlzcgzKmHg5cVmIvd82BLgow==",
"cpu": [
"wasm32"
],
@@ -294,15 +297,13 @@
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "1.10.0",
"@emnapi/runtime": "1.10.0",
"@napi-rs/wasm-runtime": "1.1.4"
"@napi-rs/wasm-runtime": "1.1.2"
}
},
"node_modules/@rspack/binding-win32-arm64-msvc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.1.tgz",
"integrity": "sha512-rHjLcy3VcAC3+x+PxH+gwhwv6tPe0JdXTNT5eAOs9wgZIM6T9p4wre49+K4Qy98+Fb7TTbLX0ObUitlOkGwTSA==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-2.0.0-rc.1.tgz",
"integrity": "sha512-+UxF0c7E9bE3siFbMHi+mmoeQJzcTKl1j3x+Y6MY/PJ3V70cU23wOaxMvmSsCyq2JNJBT2RCNZ9HaL+o3kReug==",
"cpu": [
"arm64"
],
@@ -314,9 +315,9 @@
]
},
"node_modules/@rspack/binding-win32-ia32-msvc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.1.tgz",
"integrity": "sha512-Ad1vVqMBBnd4T8rsORngu9sl2kyRTlS4kMlvFudjzl1X2UFArEDBe0YVGNN7ZvahM12CErUx2WiN8Sd8pb+qXQ==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-2.0.0-rc.1.tgz",
"integrity": "sha512-gc0JdkdxSWo+o/b1qTCT6mZ3DrlGe32eW+Ps3xInxcG4UHjUG7hTDgFtOgVQ6VhQ8WMUXG+TQOz0CySVpYjsoQ==",
"cpu": [
"ia32"
],
@@ -328,9 +329,9 @@
]
},
"node_modules/@rspack/binding-win32-x64-msvc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.1.tgz",
"integrity": "sha512-oPM2Jtm7HOlmxl/aBfleAVlL6t9VeHx6WvEets7BBJMInemFXAQd4CErRqybf7rXutACzLeUWBOue4Jpd1/ykw==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-2.0.0-rc.1.tgz",
"integrity": "sha512-Dnj0jthyVUikf65MGEyZy3akshtSmR1xsp/Xr0h/NWTo5JFWHKAFNYFE+jFfY0uzC8e4IDcLQLYoFomqV1DsEg==",
"cpu": [
"x64"
],
@@ -342,13 +343,13 @@
]
},
"node_modules/@rspack/core": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.1.tgz",
"integrity": "sha512-lgfZiExh8kDR/3obgi3RQKwKG5av1Xf5qDN1aVde777W9pbmx0Pqvrww1qtNvJ+gobEjbrrn5HEZWYGe0VLmcA==",
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/@rspack/core/-/core-2.0.0-rc.1.tgz",
"integrity": "sha512-OIfkYn05/IWtVIdZ8Y/a0y/k4ipzqfApxIZqnJM59G/bGwQKMBrLHpOMGgV2Wmq1j9UMXzF7ZtsFMUbYBhFb9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rspack/binding": "2.0.1"
"@rspack/binding": "2.0.0-rc.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
@@ -367,33 +368,36 @@
}
},
"node_modules/@rspack/plugin-react-refresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-2.0.0.tgz",
"integrity": "sha512-Cf6CxBStNDJbiXMc/GmsvG1G8PRlUpa0MSfWsMTI+e8npzuTN/p8nwLs3shriBZOLciqgkSZpBtPTd10BLpj1g==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.2.tgz",
"integrity": "sha512-k+/VrfTNgo+KirjI6V+8CWRj6y+DH9jOUWv8JorYY4vKf/9xfnZ8xHzuB4iqCwTtoZl9YnxOaOuoyjJipc2tiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"error-stack-parser": "^2.1.4"
},
"peerDependencies": {
"@rspack/core": "^2.0.0-0",
"react-refresh": ">=0.10.0 <1.0.0"
"react-refresh": ">=0.10.0 <1.0.0",
"webpack-hot-middleware": "2.x"
},
"peerDependenciesMeta": {
"@rspack/core": {
"webpack-hot-middleware": {
"optional": true
}
}
},
"node_modules/@rspress/core": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.10.tgz",
"integrity": "sha512-DvoV7YUW538x0CVAGyYPKfjUHgEuq7Z8LZq1cpfUgBpA1DynFUK3Ls6spvdoAHAl3l0AN+xxOHpu/sRVhzqi/A==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.9.tgz",
"integrity": "sha512-cfbqqbWtdimrWIsfeyPnQOTKwJpdNLr8VnwLIL4JYC2ZcRq+xcInpszLXVpV86nONL6qI19usr2Or7uzZJ+ynA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@rsbuild/core": "^2.0.2",
"@rsbuild/plugin-react": "~2.0.0",
"@rspress/shared": "2.0.10",
"@rsbuild/core": "2.0.0-rc.1",
"@rsbuild/plugin-react": "~1.4.6",
"@rspress/shared": "2.0.9",
"@shikijs/rehype": "^4.0.2",
"@types/unist": "^3.0.3",
"@unhead/react": "^2.1.13",
@@ -407,8 +411,8 @@
"mdast-util-mdxjs-esm": "^2.0.1",
"medium-zoom": "1.1.0",
"nprogress": "^0.2.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-lazy-with-preload": "^2.2.1",
"react-reconciler": "0.33.0",
"react-render-to-markdown": "19.0.1",
@@ -436,39 +440,39 @@
}
},
"node_modules/@rspress/plugin-client-redirects": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.10.tgz",
"integrity": "sha512-ImOm3h/cbXiJXIvpwv3Wn9rM91xgdhKbD2WX+WlMlWO4AtQfKR4XFrVhIZZAkrt09eeotRIklA7nu8Nuzzzbsw==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.9.tgz",
"integrity": "sha512-r2GyHzOSt8CeS4UIsy/cPM5Zotekt1JVQFmgOYGapvll5ktUlVcd77HLtXDbZjtpgtj0XlaMLrXueOpV2gsBoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rspress/core": "^2.0.10"
"@rspress/core": "^2.0.9"
}
},
"node_modules/@rspress/plugin-sitemap": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.10.tgz",
"integrity": "sha512-PZLig9+OlnyLcy6x9BlEqWSRef6TzDWB6Dlh2/hY41FtKlhyb7d7U56RGlLselWaQV54SHVa6H/y611A56ZI2g==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.9.tgz",
"integrity": "sha512-GTuXuySaeaazUZoUxdk2vZ8p0ehIgulPjCP9C7gDg6lIh5JGpUbcjG4def4tWHsxUoKp2rIwu/93bHwKb8T0Mw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rspress/core": "^2.0.10"
"@rspress/core": "^2.0.9"
}
},
"node_modules/@rspress/shared": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.10.tgz",
"integrity": "sha512-Kx10OAHWqi2jvW7ScmBUbkGjnwv4E6rEoelUchcL8It8nQ4nAVk0xvvES7m64knEon55zDbs8JQumCjbHu801Q==",
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.9.tgz",
"integrity": "sha512-G48n3pC7AVAR58pLqwClUCYj5Nt7ZgYEStR8VTBGFuPgXtzb3+KPfo/gz0hb6wxdKJ1cL5ohPsZ6EXqllu6lew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rsbuild/core": "^2.0.2",
"@rsbuild/core": "2.0.0-rc.1",
"@shikijs/rehype": "^4.0.2",
"unified": "^11.0.5"
}
@@ -968,6 +972,16 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/esast-util-from-estree": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz",
@@ -1399,9 +1413,9 @@
}
},
"node_modules/hookable": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz",
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.0.tgz",
"integrity": "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw==",
"dev": true,
"license": "MIT"
},
@@ -2683,20 +2697,20 @@
"license": "MIT"
},
"node_modules/oniguruma-parser": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz",
"integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==",
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz",
"integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==",
"dev": true,
"license": "MIT"
},
"node_modules/oniguruma-to-es": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz",
"integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==",
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz",
"integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"oniguruma-parser": "^0.12.2",
"oniguruma-parser": "^0.12.1",
"regex": "^6.1.0",
"regex-recursion": "^6.0.2"
}
@@ -2822,9 +2836,9 @@
}
},
"node_modules/react-router": {
"version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz",
"integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2845,13 +2859,13 @@
}
},
"node_modules/react-router-dom": {
"version": "7.14.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz",
"integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"react-router": "7.14.2"
"react-router": "7.14.0"
},
"engines": {
"node": ">=20.0.0"
@@ -3204,6 +3218,13 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
"dev": true,
"license": "MIT"
},
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
-2
View File
@@ -81,11 +81,9 @@ conduwuit-macros.workspace = true
conduwuit-service.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
lettre.workspace = true
log.workspace = true
assign.workspace = true
ruma.workspace = true
serde_json.workspace = true
serde-saphyr.workspace = true
+1 -1
View File
@@ -7,7 +7,7 @@ use crate::Context;
#[implement(Context, params = "<'_>")]
pub(super) async fn check_all_users(&self) -> Result {
let timer = tokio::time::Instant::now();
let users = self.services.users.stream().collect::<Vec<_>>().await;
let users = self.services.users.iter().collect::<Vec<_>>().await;
let query_time = timer.elapsed();
let total = users.len();
+24 -37
View File
@@ -79,14 +79,12 @@ pub(super) async fn parse_pdu(&self) -> Result {
}
let string = self.body[1..self.body.len().saturating_sub(1)].join("\n");
let room_version_rules = RoomVersionId::V12.rules().unwrap();
match serde_json::from_str(&string) {
| Err(e) => return Err!("Invalid json in command body: {e}"),
| Ok(value) => match ruma::signatures::reference_hash(&value, &room_version_rules) {
| Ok(value) => match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
| Err(e) => return Err!("Could not parse PDU JSON: {e:?}"),
| Ok(hash) => {
let event_id = EventId::parse(format!("${hash}"));
let event_id = OwnedEventId::parse(format!("${hash}"));
match serde_json::from_value::<PduEvent>(serde_json::to_value(value)?) {
| Err(e) => return Err!("EventId: {event_id:?}\nCould not parse event: {e}"),
| Ok(pdu) => write!(self, "EventId: {event_id:?}\n{pdu:#?}"),
@@ -121,7 +119,7 @@ pub(super) async fn get_pdu(&self, event_id: OwnedEventId) -> Result {
} else {
"PDU found in our database"
};
write!(self, "{msg}\n```json\n{text}\n```")
write!(self, "{msg}\n```json\n{text}\n```",)
},
}
.await
@@ -189,7 +187,10 @@ pub(super) async fn get_remote_pdu_list(&self, server: OwnedServerName, force: b
for event_id in list {
if force {
match self.get_remote_pdu(event_id.clone(), server.clone()).await {
match self
.get_remote_pdu(event_id.to_owned(), server.clone())
.await
{
| Err(e) => {
failed_count = failed_count.saturating_add(1);
self.services
@@ -204,7 +205,7 @@ pub(super) async fn get_remote_pdu_list(&self, server: OwnedServerName, force: b
},
}
} else {
self.get_remote_pdu(event_id.clone(), server.clone())
self.get_remote_pdu(event_id.to_owned(), server.clone())
.await?;
success_count = success_count.saturating_add(1);
}
@@ -236,10 +237,10 @@ pub(super) async fn get_remote_pdu(
match self
.services
.sending
.send_federation_request(
&server,
ruma::api::federation::event::get_event::v1::Request::new(event_id.clone()),
)
.send_federation_request(&server, ruma::api::federation::event::get_event::v1::Request {
event_id: event_id.clone(),
include_unredacted_content: None,
})
.await
{
| Err(e) => {
@@ -329,9 +330,9 @@ pub(super) async fn ping(&self, server: OwnedServerName) -> Result {
match self
.services
.sending
.send_unauthenticated_request(
.send_federation_request(
&server,
ruma::api::federation::discovery::get_server_version::v1::Request::new(),
ruma::api::federation::discovery::get_server_version::v1::Request {},
)
.await
{
@@ -360,7 +361,7 @@ pub(super) async fn force_device_list_updates(&self) -> Result {
self.services
.users
.stream()
.for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await)
.for_each(|user_id| self.services.users.mark_device_key_update(user_id))
.await;
write!(self, "Marked all devices for all users as having new keys to update").await
@@ -429,16 +430,9 @@ pub(super) async fn verify_json(&self) -> Result {
}
let string = self.body[1..self.body.len().checked_sub(1).unwrap()].join("\n");
let room_version_rules = RoomVersionId::V12.rules().unwrap();
match serde_json::from_str::<CanonicalJsonObject>(&string) {
| Err(e) => return Err!("Invalid json: {e}"),
| Ok(value) => match self
.services
.server_keys
.verify_json(&value, &room_version_rules)
.await
{
| Ok(value) => match self.services.server_keys.verify_json(&value, None).await {
| Err(e) => return Err!("Signature verification failed: {e}"),
| Ok(()) => write!(self, "Signature correct"),
},
@@ -451,15 +445,9 @@ pub(super) async fn verify_pdu(&self, event_id: OwnedEventId) -> Result {
use ruma::signatures::Verified;
let mut event = self.services.rooms.timeline.get_pdu_json(&event_id).await?;
let room_version_rules = RoomVersionId::V12.rules().unwrap();
event.remove("event_id");
let msg = match self
.services
.server_keys
.verify_event(&event, &room_version_rules)
.await
{
let msg = match self.services.server_keys.verify_event(&event, None).await {
| Err(e) => return Err(e),
| Ok(Verified::Signatures) => "signatures OK, but content hash failed (redaction).",
| Ok(Verified::All) => "signatures and hashes OK.",
@@ -556,17 +544,16 @@ pub(super) async fn force_set_room_state_from_server(
};
let room_version = self.services.rooms.state.get_room_version(&room_id).await?;
let room_version_rules = room_version.rules().unwrap();
let mut state: HashMap<u64, OwnedEventId> = HashMap::new();
let remote_state_response = self
.services
.sending
.send_federation_request(
&server_name,
get_room_state::v1::Request::new(at_event_id, room_id.clone()),
)
.send_federation_request(&server_name, get_room_state::v1::Request {
room_id: room_id.clone(),
event_id: at_event_id,
})
.await?;
for pdu in remote_state_response.pdus.clone() {
@@ -589,7 +576,7 @@ pub(super) async fn force_set_room_state_from_server(
for result in remote_state_response.pdus.iter().map(|pdu| {
self.services
.server_keys
.validate_and_add_event_id(pdu, &room_version_rules)
.validate_and_add_event_id(pdu, &room_version)
}) {
let Ok((event_id, value)) = result.await else {
continue;
@@ -621,7 +608,7 @@ pub(super) async fn force_set_room_state_from_server(
for result in remote_state_response.auth_chain.iter().map(|pdu| {
self.services
.server_keys
.validate_and_add_event_id(pdu, &room_version_rules)
.validate_and_add_event_id(pdu, &room_version)
}) {
let Ok((event_id, value)) = result.await else {
continue;
@@ -638,7 +625,7 @@ pub(super) async fn force_set_room_state_from_server(
.services
.rooms
.event_handler
.resolve_state(&room_id, &room_version_rules, state)
.resolve_state(&room_id, &room_version, state)
.await?;
info!("Compressing new room state");
+2 -2
View File
@@ -111,7 +111,7 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result
.rooms
.state_cache
.rooms_joined(&user_id)
.then(async |room_id| get_room_info(self.services, &room_id).await)
.then(|room_id| get_room_info(self.services, room_id))
.collect()
.await;
@@ -129,6 +129,6 @@ pub(super) async fn remote_user_in_rooms(&self, user_id: OwnedUserId) -> Result
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```"))
self.write_str(&format!("Rooms {user_id} shares with us ({num}):\n```\n{body}\n```",))
.await
}
+4 -5
View File
@@ -6,8 +6,7 @@ use conduwuit::{
warn,
};
use conduwuit_service::media::Dim;
use ruma::{OwnedEventId, OwnedMxcUri, OwnedServerName};
use service::media::mxc::Mxc;
use ruma::{Mxc, OwnedEventId, OwnedMxcUri, OwnedServerName};
use crate::{admin_command, utils::parse_local_user_id};
@@ -262,7 +261,7 @@ pub(super) async fn delete_past_remote_media(
)
.await?;
self.write_str(&format!("Deleted {deleted_count} total files."))
self.write_str(&format!("Deleted {deleted_count} total files.",))
.await
}
@@ -272,7 +271,7 @@ pub(super) async fn delete_all_from_user(&self, username: String) -> Result {
let deleted_count = self.services.media.delete_from_user(&user_id).await?;
self.write_str(&format!("Deleted {deleted_count} total files."))
self.write_str(&format!("Deleted {deleted_count} total files.",))
.await
}
@@ -331,7 +330,7 @@ pub(super) async fn delete_all_from_server(
}
}
self.write_str(&format!("Deleted {deleted_count} total files."))
self.write_str(&format!("Deleted {deleted_count} total files.",))
.await
}
+5 -5
View File
@@ -16,8 +16,8 @@ use futures::{AsyncWriteExt, future::FutureExt, io::BufWriter};
use ruma::{
EventId,
events::{
relation::{InReplyTo, Reply},
room::message::{Relation, RoomMessageEventContent},
relation::InReplyTo,
room::message::{Relation::Reply, RoomMessageEventContent},
},
};
use service::{
@@ -38,7 +38,6 @@ pub(super) fn dispatch(services: Arc<Services>, command: CommandInput) -> Proces
}
#[tracing::instrument(skip_all, name = "admin", level = "info")]
#[allow(clippy::result_large_err)]
async fn handle_command(services: Arc<Services>, command: CommandInput) -> ProcessorResult {
AssertUnwindSafe(Box::pin(process_command(services, &command)))
.catch_unwind()
@@ -278,8 +277,9 @@ fn reply(
mut content: RoomMessageEventContent,
reply_id: Option<&EventId>,
) -> RoomMessageEventContent {
content.relates_to =
reply_id.map(|event_id| Relation::Reply(Reply::new(InReplyTo::new(event_id.to_owned()))));
content.relates_to = reply_id.map(|event_id| Reply {
in_reply_to: InReplyTo { event_id: event_id.to_owned() },
});
content
}
+2 -2
View File
@@ -50,7 +50,7 @@ async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Resu
while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
if let Some(server_name) = server_name.as_ref() {
if name != *server_name {
if name != server_name {
continue;
}
}
@@ -76,7 +76,7 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
overrides.next().await
{
if let Some(server_name) = server_name.as_ref() {
if name != *server_name {
if name != server_name {
continue;
}
}
+2 -1
View File
@@ -41,6 +41,7 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -53,7 +54,7 @@ pub(super) async fn process(subcommand: RoomAliasCommand, context: &Context<'_>)
.rooms
.alias
.all_local_aliases()
.map(|(room_id, alias)| (room_id, alias.to_owned()))
.map(|(room_id, alias)| (room_id.to_owned(), alias.to_owned()))
.collect::<Vec<_>>()
.await;
let query_time = timer.elapsed();
+8
View File
@@ -101,6 +101,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_servers(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -117,6 +118,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.server_rooms(&server)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -133,6 +135,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -149,6 +152,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.local_users_in_room(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -165,6 +169,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -207,6 +212,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_useroncejoined(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -223,6 +229,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.room_members_invited(&room_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -269,6 +276,7 @@ pub(super) async fn process(subcommand: RoomStateCacheCommand, context: &Context
.rooms
.state_cache
.rooms_joined(&user_id)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
+4 -1
View File
@@ -104,6 +104,7 @@ async fn get_shared_rooms(&self, user_a: OwnedUserId, user_b: OwnedUserId) -> Re
.rooms
.state_cache
.get_shared_rooms(&user_a, &user_b)
.map(ToOwned::to_owned)
.collect()
.await;
let query_time = timer.elapsed();
@@ -216,7 +217,8 @@ async fn iter_users2(&self) -> Result {
let result: Vec<_> = self.services.users.stream().collect().await;
let result: Vec<_> = result
.into_iter()
.map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned())
.map(ruma::UserId::as_bytes)
.map(String::from_utf8_lossy)
.collect();
let query_time = timer.elapsed();
@@ -252,6 +254,7 @@ async fn list_devices(&self, user_id: OwnedUserId) -> Result {
.services
.users
.all_device_ids(&user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await;
+3 -3
View File
@@ -3,7 +3,7 @@ use std::fmt::Write;
use clap::Subcommand;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::{OwnedRoomAliasId, OwnedRoomId, RoomAliasId};
use ruma::{OwnedRoomAliasId, OwnedRoomId};
use crate::Context;
@@ -52,7 +52,7 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
| RoomAliasCommand::Which { ref room_alias_localpart } => {
let room_alias_str =
format!("#{}:{}", room_alias_localpart, services.globals.server_name());
let room_alias = match RoomAliasId::parse(room_alias_str) {
let room_alias = match OwnedRoomAliasId::parse(room_alias_str) {
| Ok(alias) => alias,
| Err(err) => {
return Err!("Failed to parse alias: {err}");
@@ -139,7 +139,7 @@ pub(super) async fn process(command: RoomAliasCommand, context: &Context<'_>) ->
.rooms
.alias
.all_local_aliases()
.map(|(room_id, localpart)| (room_id, localpart.into()))
.map(|(room_id, localpart)| (room_id.into(), localpart.into()))
.collect::<Vec<(OwnedRoomId, String)>>()
.await;
+4 -4
View File
@@ -22,14 +22,14 @@ pub(super) async fn list_rooms(
.metadata
.iter_ids()
.filter_map(|room_id| async move {
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(&room_id).await)
(!exclude_disabled || !self.services.rooms.metadata.is_disabled(room_id).await)
.then_some(room_id)
})
.filter_map(|room_id| async move {
(!exclude_banned || !self.services.rooms.metadata.is_banned(&room_id).await)
(!exclude_banned || !self.services.rooms.metadata.is_banned(room_id).await)
.then_some(room_id)
})
.then(async |room_id| get_room_info(self.services, &room_id).await)
.then(|room_id| get_room_info(self.services, room_id))
.then(|(room_id, total_members, name)| async move {
let local_members: Vec<_> = self
.services
@@ -72,7 +72,7 @@ pub(super) async fn list_rooms(
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len()))
self.write_str(&format!("Rooms ({}):\n```\n{body}\n```", rooms.len(),))
.await
}
+2 -2
View File
@@ -43,7 +43,7 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Context<'_>
.rooms
.directory
.public_rooms()
.then(async |room_id| get_room_info(services, &room_id).await)
.then(|room_id| get_room_info(services, room_id))
.collect()
.await;
@@ -67,7 +67,7 @@ pub(super) async fn process(command: RoomDirectoryCommand, context: &Context<'_>
.join("\n");
context
.write_str(&format!("Rooms (page {page}):\n```\n{body}\n```"))
.write_str(&format!("Rooms (page {page}):\n```\n{body}\n```",))
.await
},
}
+2 -1
View File
@@ -46,6 +46,7 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R
.then(|| self.services.globals.user_is_local(user_id))
.unwrap_or(true)
})
.map(ToOwned::to_owned)
.filter_map(|user_id| async move {
Some((
self.services
@@ -66,7 +67,7 @@ async fn list_joined_members(&self, room_id: OwnedRoomId, local_only: bool) -> R
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```"))
self.write_str(&format!("{num} Members in Room \"{room_name}\":\n```\n{body}\n```",))
.await
}
+14 -10
View File
@@ -71,7 +71,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
debug!("Room specified is a room ID, banning room ID");
room_id.clone()
room_id.to_owned()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
@@ -89,7 +89,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
locally, if not using get_alias_helper to fetch room ID remotely"
);
match self.services.rooms.alias.resolve_alias(&room_alias).await {
match self.services.rooms.alias.resolve_alias(room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
@@ -116,6 +116,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
@@ -139,6 +140,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(|local_alias| async move {
self.services
.rooms
@@ -203,7 +205,7 @@ async fn ban_list_of_rooms(&self) -> Result {
},
};
room_ids.push(room_id.clone());
room_ids.push(room_id.to_owned());
}
if room_alias_or_id.is_room_alias_id() {
@@ -213,7 +215,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.services
.rooms
.alias
.resolve_local_alias(&room_alias)
.resolve_local_alias(room_alias)
.await
{
| Ok(room_id) => room_id,
@@ -227,7 +229,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.services
.rooms
.alias
.resolve_alias(&room_alias)
.resolve_alias(room_alias)
.await
{
| Ok((room_id, servers)) => {
@@ -282,6 +284,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.rooms
.state_cache
.room_members(&room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| self.services.globals.user_is_local(user))
.boxed();
@@ -306,6 +309,7 @@ async fn ban_list_of_rooms(&self) -> Result {
.rooms
.alias
.local_aliases_for_room(&room_id)
.map(ToOwned::to_owned)
.for_each(|local_alias| async move {
self.services
.rooms
@@ -344,9 +348,9 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
};
debug!("Room specified is a room ID, unbanning room ID");
self.services.rooms.metadata.ban_room(&room_id, false);
self.services.rooms.metadata.ban_room(room_id, false);
room_id.clone()
room_id.to_owned()
} else if room.is_room_alias_id() {
let room_alias = match RoomAliasId::parse(&room) {
| Ok(room_alias) => room_alias,
@@ -368,7 +372,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
.services
.rooms
.alias
.resolve_local_alias(&room_alias)
.resolve_local_alias(room_alias)
.await
{
| Ok(room_id) => room_id,
@@ -378,7 +382,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
room ID over federation"
);
match self.services.rooms.alias.resolve_alias(&room_alias).await {
match self.services.rooms.alias.resolve_alias(room_alias).await {
| Ok((room_id, servers)) => {
debug!(
%room_id,
@@ -449,6 +453,6 @@ async fn list_banned_rooms(&self, no_details: bool) -> Result {
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```"))
self.write_str(&format!("Rooms Banned ({num}):\n```\n{body}\n```",))
.await
}
+2 -2
View File
@@ -159,8 +159,8 @@ pub(super) async fn list_features(&self) -> Result {
let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES
.lock()
.expect("locked")
.values()
.flat_map(|f| f.iter())
.iter()
.flat_map(|(_, f)| f.iter())
.collect::<Vec<_>>();
enabled_features.sort_unstable();
+64 -56
View File
@@ -9,18 +9,20 @@ use api::client::{
};
use conduwuit::{
Err, Result, debug_warn, error, info,
matrix::{Event, pdu::PartialPdu},
matrix::{Event, pdu::PduBuilder},
utils::{self, ReadyExt},
warn,
};
use futures::{FutureExt, StreamExt};
use lettre::Address;
use ruma::{
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, ServerName,
UserId, assign,
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
events::{
RoomAccountDataEventType,
room::{power_levels::RoomPowerLevelsEventContent, redaction::RoomRedactionEventContent},
RoomAccountDataEventType, StateEventType,
room::{
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
redaction::RoomRedactionEventContent,
},
tag::{TagEvent, TagEventContent, TagInfo},
},
};
@@ -39,7 +41,7 @@ pub(super) async fn list_users(&self) -> Result {
.services
.users
.list_local_users()
.map(|id| id.as_str().to_owned())
.map(ToString::to_string)
.collect()
.await;
@@ -70,7 +72,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// Create user
self.services
.users
.create(&user_id, Some(password.as_str()))
.create(&user_id, Some(password.as_str()), None)
.await?;
// Default to pretty displayname
@@ -101,12 +103,11 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
ruma::events::GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent::new(
ruma::events::push_rules::PushRulesEventContent::new(
ruma::push::Ruleset::server_default(&user_id),
),
))
.unwrap(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: ruma::push::Ruleset::server_default(&user_id),
},
})?,
)
.await?;
@@ -291,12 +292,7 @@ pub(super) async fn reset_password(
self.services
.users
.all_device_ids(&user_id)
.for_each(async |device_id| {
self.services
.users
.remove_device(&user_id, &device_id)
.await;
})
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
write!(self, "\nAll existing sessions have been logged out.").await?;
}
@@ -441,7 +437,7 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
.rooms
.state_cache
.rooms_joined(&user_id)
.then(async |room_id| get_room_info(self.services, &room_id).await)
.then(|room_id| get_room_info(self.services, room_id))
.collect()
.await;
@@ -458,7 +454,7 @@ pub(super) async fn list_joined_rooms(&self, user_id: String) -> Result {
.collect::<Vec<_>>()
.join("\n");
self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len()))
self.write_str(&format!("Rooms {user_id} Joined ({}):\n```\n{body}\n```", rooms.len(),))
.await
}
@@ -510,7 +506,7 @@ pub(super) async fn force_join_list_of_local_users(
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id))
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.await
{
return Err!("There is not a single server admin in the room.",);
@@ -624,7 +620,7 @@ pub(super) async fn force_join_all_local_users(
.rooms
.state_cache
.room_members(&room_id)
.ready_any(|user_id| server_admins.contains(&user_id))
.ready_any(|user_id| server_admins.contains(&user_id.to_owned()))
.await
{
return Err!("There is not a single server admin in the room.",);
@@ -637,6 +633,7 @@ pub(super) async fn force_join_all_local_users(
.services
.users
.list_local_users()
.map(UserId::to_owned)
.collect::<Vec<_>>()
.await
{
@@ -687,7 +684,7 @@ pub(super) async fn force_join_room(
);
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, &None).await?;
self.write_str(&format!("{user_id} has been joined to {room_id}."))
self.write_str(&format!("{user_id} has been joined to {room_id}.",))
.await
}
@@ -719,7 +716,7 @@ pub(super) async fn force_leave_room(
.boxed()
.await?;
self.write_str(&format!("{user_id} has left {room_id}."))
self.write_str(&format!("{user_id} has left {room_id}.",))
.await
}
@@ -733,34 +730,42 @@ pub(super) async fn force_demote(&self, user_id: String, room_id: OwnedRoomOrAli
"Parsed user_id must be a local user"
);
let state_lock = self.services.rooms.state.mutex.lock(room_id.as_str()).await;
let state_lock = self.services.rooms.state.mutex.lock(&room_id).await;
let mut room_power_levels = self
let room_power_levels: Option<RoomPowerLevelsEventContent> = self
.services
.rooms
.state_accessor
.get_room_power_levels(&room_id)
.await;
.room_state_get_content(&room_id, &StateEventType::RoomPowerLevels, "")
.await
.ok();
let user_can_demote_self =
room_power_levels.user_can_change_user_power_level(&user_id, &user_id);
let user_can_demote_self = room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(&user_id, &user_id)
}) || self
.services
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
if !user_can_demote_self {
return Err!("User is not allowed to modify their own power levels in the room.",);
}
room_power_levels.users.remove(&user_id);
let mut power_levels_content = room_power_levels.unwrap_or_default();
power_levels_content.users.remove(&user_id);
let event_id = self
.services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
String::new(),
&RoomPowerLevelsEventContent::try_from(room_power_levels)
.expect("PLs should be valid for room version"),
),
PduBuilder::state(String::new(), &power_levels_content),
&user_id,
Some(&room_id),
&state_lock,
@@ -788,7 +793,7 @@ pub(super) async fn make_user_admin(&self, user_id: String) -> Result {
.boxed()
.await?;
self.write_str(&format!("{user_id} has been granted admin privileges."))
self.write_str(&format!("{user_id} has been granted admin privileges.",))
.await
}
@@ -806,7 +811,9 @@ pub(super) async fn put_room_tag(
.account_data
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
tags_event
.content
@@ -843,7 +850,9 @@ pub(super) async fn delete_room_tag(
.account_data
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
tags_event.content.tags.remove(&tag.clone().into());
@@ -873,7 +882,9 @@ pub(super) async fn get_room_tags(&self, user_id: String, room_id: OwnedRoomId)
.account_data
.get_room(&room_id, &user_id, RoomAccountDataEventType::Tag)
.await
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
self.write_str(&format!("```\n{:#?}\n```", tags_event.content.tags))
.await
@@ -910,19 +921,19 @@ pub(super) async fn redact_event(&self, event_id: OwnedEventId) -> Result {
.rooms
.state
.mutex
.lock(event.room_id_or_hash().as_str())
.lock(&event.room_id_or_hash())
.await;
self.services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
redacts: Some(event.event_id().to_owned()),
..PartialPdu::timeline(&assign!(RoomRedactionEventContent::new_v1(), {
..PduBuilder::timeline(&RoomRedactionEventContent {
redacts: Some(event.event_id().to_owned()),
reason: Some(reason),
}))
})
},
event.sender(),
Some(&event.room_id_or_hash()),
@@ -952,7 +963,7 @@ pub(super) async fn force_leave_remote_room(
.resolve_with_servers(
&room_id,
if let Some(v) = via.clone() {
Some(vec![ServerName::parse(v)?])
Some(vec![OwnedServerName::parse(v)?])
} else {
None
},
@@ -965,7 +976,7 @@ pub(super) async fn force_leave_remote_room(
);
let mut vias: HashSet<OwnedServerName> = HashSet::new();
if let Some(via) = via {
vias.insert(ServerName::parse(via)?);
vias.insert(OwnedServerName::parse(via)?);
}
for server in vias_raw {
vias.insert(server);
@@ -1040,12 +1051,7 @@ pub(super) async fn logout(&self, user_id: String) -> Result {
self.services
.users
.all_device_ids(&user_id)
.for_each(async |device_id| {
self.services
.users
.remove_device(&user_id, &device_id)
.await;
})
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
self.write_str(&format!("User {user_id} has been logged out from all devices."))
.await
@@ -1123,9 +1129,11 @@ pub(super) async fn get_user_by_email(&self, email: String) -> Result {
match self.services.threepid.get_localpart_for_email(&email).await {
| Some(localpart) => {
let user_id =
UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name()))
.unwrap();
let user_id = OwnedUserId::parse(format!(
"@{localpart}:{}",
self.services.globals.server_name()
))
.unwrap();
self.write_str(&format!("{email} belongs to {user_id}."))
.await
+3 -3
View File
@@ -48,6 +48,9 @@ jemalloc_stats = [
"conduwuit-core/jemalloc_stats",
"conduwuit-service/jemalloc_stats",
]
ldap = [
"conduwuit-service/ldap"
]
release_max_log_level = [
"conduwuit-core/release_max_log_level",
"conduwuit-service/release_max_log_level",
@@ -74,7 +77,6 @@ conduwuit-macros.workspace = true
conduwuit-service.workspace = true
const-str.workspace = true
ctor.workspace = true
dtor.workspace = true
futures.workspace = true
hmac.workspace = true
http.workspace = true
@@ -86,9 +88,7 @@ lettre.workspace = true
log.workspace = true
rand.workspace = true
reqwest.workspace = true
assign.workspace = true
ruma.workspace = true
ruminuwuity.workspace = true
serde_html_form.workspace = true
serde_json.workspace = true
serde.workspace = true
+7 -4
View File
@@ -1,8 +1,10 @@
use axum::extract::State;
use conduwuit::{Err, Result, info, utils::ReadyExt, warn};
use futures::{FutureExt, StreamExt};
use ruma::{OwnedRoomAliasId, events::room::message::RoomMessageEventContent};
use ruminuwuity::admin::continuwuity::rooms;
use ruma::{
OwnedRoomAliasId, continuwuity_admin_api::rooms,
events::room::message::RoomMessageEventContent,
};
use crate::{Ruma, client::leave_room};
@@ -34,6 +36,7 @@ pub(crate) async fn ban_room(
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.ready_filter(|user| services.globals.user_is_local(user))
.boxed();
let mut evicted = Vec::new();
@@ -60,9 +63,9 @@ pub(crate) async fn ban_room(
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.collect()
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await;
for alias in &aliases {
info!("Removing alias {} for banned room {}", alias, body.room_id);
services
+3 -4
View File
@@ -1,8 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::OwnedRoomId;
use ruminuwuity::admin::continuwuity::rooms;
use ruma::{OwnedRoomId, continuwuity_admin_api::rooms};
use crate::Ruma;
@@ -23,8 +22,8 @@ pub(crate) async fn list_rooms(
.metadata
.iter_ids()
.filter_map(|room_id| async move {
if !services.rooms.metadata.is_banned(&room_id).await {
Some(room_id.clone())
if !services.rooms.metadata.is_banned(room_id).await {
Some(room_id.to_owned())
} else {
None
}
+67 -43
View File
@@ -1,15 +1,15 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err, info,
pdu::PartialPdu,
Err, Event, Result, err, info,
pdu::PduBuilder,
utils::{ReadyExt, stream::BroadbandExt},
};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt};
use lettre::{Address, message::Mailbox};
use ruma::{
OwnedRoomId, UserId,
OwnedRoomId, OwnedUserId, UserId,
api::client::{
account::{
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
@@ -18,10 +18,12 @@ use ruma::{
},
uiaa::{AuthFlow, AuthType},
},
assign,
events::room::{
member::{MembershipState, RoomMemberEventContent},
power_levels::RoomPowerLevelsEventContent,
events::{
StateEventType,
room::{
member::{MembershipState, RoomMemberEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
},
};
use service::{mailer::messages, uiaa::Identity};
@@ -85,7 +87,7 @@ pub(crate) async fn get_register_available_route(
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
Ok(get_username_availability::v3::Response::new(true))
Ok(get_username_availability::v3::Response { available: true })
}
/// # `POST /_matrix/client/r0/account/password`
@@ -141,7 +143,7 @@ pub(crate) async fn change_password_route(
.await?
};
let sender_user = UserId::parse(format!(
let sender_user = OwnedUserId::parse(format!(
"@{}:{}",
identity.localpart.expect("localpart should be known"),
services.globals.server_name()
@@ -159,7 +161,7 @@ pub(crate) async fn change_password_route(
.users
.all_device_ids(&sender_user)
.ready_filter(|id| *id != body.sender_device())
.for_each(async |id| services.users.remove_device(&sender_user, &id).await)
.for_each(|id| services.users.remove_device(&sender_user, id))
.await;
// Remove all pushers except the ones associated with this session
@@ -173,8 +175,8 @@ pub(crate) async fn change_password_route(
.get_pusher_device(&pushkey)
.await
.ok()
.as_ref()
.is_some_and(|pusher_device| pusher_device != body.sender_device())
.filter(|pusher_device| pusher_device != body.sender_device())
.is_some()
.then_some(pushkey)
})
.for_each(async |pushkey| {
@@ -192,7 +194,7 @@ pub(crate) async fn change_password_route(
.await;
}
Ok(change_password::v3::Response::new())
Ok(change_password::v3::Response {})
}
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
@@ -213,7 +215,7 @@ pub(crate) async fn request_password_change_token_via_email_route(
};
let user_id =
UserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
OwnedUserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
let display_name = services.users.displayname(&user_id).await.ok();
let session = services
@@ -249,10 +251,11 @@ pub(crate) async fn whoami_route(
.map_err(|_| {
err!(Request(Forbidden("Application service has not registered this user.")))
})? && body.appservice_info.is_none();
Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), is_guest), {
Ok(whoami::v3::Response {
user_id: body.sender_user().to_owned(),
device_id: body.sender_device.clone(),
}))
is_guest,
})
}
/// # `POST /_matrix/client/r0/account/deactivate`
@@ -307,7 +310,9 @@ pub(crate) async fn deactivate_route(
.await;
}
Ok(deactivate::v3::Response::new(ThirdPartyIdRemovalStatus::Success))
Ok(deactivate::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
})
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
@@ -325,7 +330,7 @@ pub(crate) async fn check_registration_token_validity(
.await
.is_some();
Ok(check_registration_token_validity::v1::Response::new(valid))
Ok(check_registration_token_validity::v1::Response { valid })
}
/// Runs through all the deactivation steps:
@@ -349,7 +354,13 @@ pub async fn full_user_deactivate(
.await;
}
services.users.clear_profile(user_id).await;
services
.users
.all_profile_keys(user_id)
.ready_for_each(|(profile_key, _)| {
services.users.set_profile_key(user_id, &profile_key, None);
})
.await;
services
.pusher
@@ -361,49 +372,62 @@ pub async fn full_user_deactivate(
// TODO: Rescind all user invites
let mut pdu_queue: Vec<(PartialPdu, &OwnedRoomId)> = Vec::new();
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
for room_id in all_joined_rooms {
let room_power_levels = services
.rooms
.state_accessor
.get_room_power_levels(room_id)
.await;
.room_state_get_content::<RoomPowerLevelsEventContent>(
room_id,
&StateEventType::RoomPowerLevels,
"",
)
.await
.ok();
let user_can_demote_self =
room_power_levels.user_can_change_user_power_level(user_id, user_id);
room_power_levels
.as_ref()
.is_some_and(|power_levels_content| {
RoomPowerLevels::from(power_levels_content.clone())
.user_can_change_user_power_level(user_id, user_id)
}) || services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, "")
.await
.is_ok_and(|event| event.sender() == user_id);
if user_can_demote_self
&& let Ok(mut power_levels_content) =
RoomPowerLevelsEventContent::try_from(room_power_levels)
{
if user_can_demote_self {
let mut power_levels_content = room_power_levels.unwrap_or_default();
power_levels_content.users.remove(user_id);
let pl_evt = PartialPdu::state(String::new(), &power_levels_content);
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
pdu_queue.push((pl_evt, room_id));
}
// Leave the room
pdu_queue.push((
PartialPdu::state(
user_id.to_string(),
&RoomMemberEventContent::new(MembershipState::Leave),
),
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
avatar_url: None,
blurhash: None,
membership: MembershipState::Leave,
displayname: None,
join_authorized_via_users_server: None,
reason: None,
is_direct: None,
third_party_invite: None,
redact_events: None,
}),
room_id,
));
// TODO: Redact all messages sent by the user in the room
}
for (pdu, room_id) in pdu_queue {
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
let _ = services
.rooms
.timeline
.build_and_append_pdu(pdu, user_id, Some(room_id.as_ref()), &state_lock)
.await;
}
super::update_all_rooms(services, pdu_queue, user_id)
.boxed()
.await;
for room_id in all_joined_rooms {
services.rooms.state_cache.forget(room_id, user_id);
}
+19 -16
View File
@@ -20,11 +20,7 @@ use ruma::{
},
uiaa::{AuthFlow, AuthType},
},
assign,
events::{
GlobalAccountDataEventType, push_rules::PushRulesEvent,
room::message::RoomMessageEventContent,
},
events::{GlobalAccountDataEventType, room::message::RoomMessageEventContent},
push,
};
use serde_json::value::RawValue;
@@ -190,7 +186,7 @@ pub(crate) async fn register_route(
let password = if is_guest { None } else { body.password.as_deref() };
// Create user
services.users.create(&user_id, password).await?;
services.users.create(&user_id, password, None).await?;
// Set an initial display name
let mut displayname = user_id.localpart().to_owned();
@@ -213,15 +209,23 @@ pub(crate) async fn register_route(
None,
&user_id,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent::new(
push::Ruleset::server_default(&user_id).into(),
))
.expect("should be able to serialize push rules"),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
},
})?,
)
.await?;
// Generate new device id if the user didn't specify one
let (token, device) = if !body.inhibit_login {
let no_device = body.inhibit_login
|| body
.appservice_info
.as_ref()
.is_some_and(|aps| aps.registration.device_management);
let (token, device) = if !no_device {
// Don't create a device for inhibited logins
let device_id = if is_guest { None } else { body.device_id.clone() }
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
@@ -239,14 +243,12 @@ pub(crate) async fn register_route(
Some(client.to_string()),
)
.await?;
debug_info!(%user_id, %device_id, "User account was created");
(Some(new_token), Some(device_id))
} else {
// Don't create a device for inhibited logins
(None, None)
};
debug_info!(%user_id, ?device, "User account was created");
// If the user registered with an email, associate it with their account.
if let Some(identity) = identity
&& let Some(email) = identity.email
@@ -391,12 +393,13 @@ pub(crate) async fn register_route(
}
}
Ok(assign!(register::v3::Response::new(user_id), {
Ok(register::v3::Response {
access_token: token,
user_id,
device_id: device,
refresh_token: None,
expires_in: None,
}))
})
}
/// Determine which flows and parameters should be presented when
+6 -2
View File
@@ -141,7 +141,9 @@ pub(crate) async fn delete_3pid_route(
let sender_user = body.sender_user();
if body.medium != Medium::Email {
return Ok(delete_3pid::v3::Response::new(ThirdPartyIdRemovalStatus::NoSupport));
return Ok(delete_3pid::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
});
}
if !services.threepid.email_requirement().may_remove() {
@@ -157,5 +159,7 @@ pub(crate) async fn delete_3pid_route(
return Err!(Request(ThreepidNotFound("Your account has no associated email.")));
}
Ok(delete_3pid::v3::Response::new(ThirdPartyIdRemovalStatus::Success))
Ok(delete_3pid::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
})
}
+9 -6
View File
@@ -7,7 +7,10 @@ use ruma::{
get_global_account_data, get_room_account_data, set_global_account_data,
set_room_account_data,
},
events::{AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent},
events::{
AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent,
RoomAccountDataEventType,
},
serde::Raw,
};
use serde::Deserialize;
@@ -37,7 +40,7 @@ pub(crate) async fn set_global_account_data_route(
)
.await?;
Ok(set_global_account_data::v3::Response::new())
Ok(set_global_account_data::v3::Response {})
}
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
@@ -62,7 +65,7 @@ pub(crate) async fn set_room_account_data_route(
)
.await?;
Ok(set_room_account_data::v3::Response::new())
Ok(set_room_account_data::v3::Response {})
}
/// # `GET /_matrix/client/r0/user/{userId}/account_data/{type}`
@@ -84,7 +87,7 @@ pub(crate) async fn get_global_account_data_route(
.await
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
Ok(get_global_account_data::v3::Response::new(account_data.content))
Ok(get_global_account_data::v3::Response { account_data: account_data.content })
}
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
@@ -106,7 +109,7 @@ pub(crate) async fn get_room_account_data_route(
.await
.map_err(|_| err!(Request(NotFound("Data not found."))))?;
Ok(get_room_account_data::v3::Response::new(account_data.content))
Ok(get_room_account_data::v3::Response { account_data: account_data.content })
}
async fn set_account_data(
@@ -116,7 +119,7 @@ async fn set_account_data(
event_type_s: &str,
data: &RawJsonValue,
) -> Result {
if event_type_s == "m.fully_read" {
if event_type_s == RoomAccountDataEventType::FullyRead.to_cow_str() {
return Err!(Request(BadJson(
"This endpoint cannot be used for marking a room as fully read (setting \
m.fully_read)"
+1 -1
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::future::{join, join3};
use ruminuwuity::admin::{get_suspended, set_suspended};
use ruma::api::client::admin::{get_suspended, set_suspended};
use crate::Ruma;
+4 -7
View File
@@ -1,9 +1,6 @@
use axum::extract::State;
use conduwuit::{Err, Result, err};
use ruma::{
api::{appservice::ping, client::appservice::request_ping},
assign,
};
use ruma::api::{appservice::ping, client::appservice::request_ping};
use crate::Ruma;
@@ -43,12 +40,12 @@ pub(crate) async fn appservice_ping(
.sending
.send_appservice_request(
appservice_info.registration.clone(),
assign!(ping::send_ping::v1::Request::new(), {
ping::send_ping::v1::Request {
transaction_id: body.transaction_id.clone(),
}),
},
)
.await?
.expect("We already validated if an appservice URL exists above");
Ok(request_ping::v1::Response::new(timer.elapsed()))
Ok(request_ping::v1::Response { duration: timer.elapsed() })
}
+33 -29
View File
@@ -3,6 +3,7 @@ use std::cmp::Ordering;
use axum::extract::State;
use conduwuit::{Err, Result, err};
use conduwuit_service::Services;
use futures::{FutureExt, future::try_join};
use ruma::{
UInt, UserId,
api::client::backup::{
@@ -27,7 +28,7 @@ pub(crate) async fn create_backup_version_route(
.key_backups
.create_backup(body.sender_user(), &body.algorithm)?;
Ok(create_backup_version::v3::Response::new(version))
Ok(create_backup_version::v3::Response { version })
}
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
@@ -43,7 +44,7 @@ pub(crate) async fn update_backup_version_route(
.update_backup(body.sender_user(), &body.version, &body.algorithm)
.await?;
Ok(update_backup_version::v3::Response::new())
Ok(update_backup_version::v3::Response {})
}
/// # `GET /_matrix/client/r0/room_keys/version`
@@ -59,9 +60,9 @@ pub(crate) async fn get_latest_backup_info_route(
.await
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await?;
Ok(get_latest_backup_info::v3::Response::new(algorithm, count, etag, version))
Ok(get_latest_backup_info::v3::Response { algorithm, count, etag, version })
}
/// # `GET /_matrix/client/v3/room_keys/version/{version}`
@@ -79,9 +80,14 @@ pub(crate) async fn get_backup_info_route(
err!(Request(NotFound("Key backup does not exist at version {:?}", body.version)))
})?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(get_backup_info::v3::Response::new(algorithm, count, etag, body.version.clone()))
Ok(get_backup_info::v3::Response {
algorithm,
count,
etag,
version: body.version.clone(),
})
}
/// # `DELETE /_matrix/client/r0/room_keys/version/{version}`
@@ -99,7 +105,7 @@ pub(crate) async fn delete_backup_version_route(
.delete_backup(body.sender_user(), &body.version)
.await;
Ok(delete_backup_version::v3::Response::new())
Ok(delete_backup_version::v3::Response {})
}
/// # `PUT /_matrix/client/r0/room_keys/keys`
@@ -134,9 +140,9 @@ pub(crate) async fn add_backup_keys_route(
}
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(add_backup_keys::v3::Response::new(etag, count))
Ok(add_backup_keys::v3::Response { count, etag })
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
@@ -169,9 +175,9 @@ pub(crate) async fn add_backup_keys_for_room_route(
.await?;
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(add_backup_keys_for_room::v3::Response::new(etag, count))
Ok(add_backup_keys_for_room::v3::Response { count, etag })
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@@ -269,9 +275,9 @@ pub(crate) async fn add_backup_keys_for_session_route(
.await?;
}
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(add_backup_keys_for_session::v3::Response::new(etag, count))
Ok(add_backup_keys_for_session::v3::Response { count, etag })
}
/// # `GET /_matrix/client/r0/room_keys/keys`
@@ -286,7 +292,7 @@ pub(crate) async fn get_backup_keys_route(
.get_all(body.sender_user(), &body.version)
.await;
Ok(get_backup_keys::v3::Response::new(rooms))
Ok(get_backup_keys::v3::Response { rooms })
}
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
@@ -301,7 +307,7 @@ pub(crate) async fn get_backup_keys_for_room_route(
.get_room(body.sender_user(), &body.version, &body.room_id)
.await;
Ok(get_backup_keys_for_room::v3::Response::new(sessions))
Ok(get_backup_keys_for_room::v3::Response { sessions })
}
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@@ -319,7 +325,7 @@ pub(crate) async fn get_backup_keys_for_session_route(
err!(Request(NotFound(debug_error!("Backup key not found for this user's session."))))
})?;
Ok(get_backup_keys_for_session::v3::Response::new(key_data))
Ok(get_backup_keys_for_session::v3::Response { key_data })
}
/// # `DELETE /_matrix/client/r0/room_keys/keys`
@@ -334,9 +340,9 @@ pub(crate) async fn delete_backup_keys_route(
.delete_all_keys(body.sender_user(), &body.version)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(delete_backup_keys::v3::Response::new(etag, count))
Ok(delete_backup_keys::v3::Response { count, etag })
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
@@ -351,9 +357,9 @@ pub(crate) async fn delete_backup_keys_for_room_route(
.delete_room_keys(body.sender_user(), &body.version, &body.room_id)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(delete_backup_keys_for_room::v3::Response::new(etag, count))
Ok(delete_backup_keys_for_room::v3::Response { count, etag })
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@@ -368,24 +374,22 @@ pub(crate) async fn delete_backup_keys_for_session_route(
.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await?;
Ok(delete_backup_keys_for_session::v3::Response::new(etag, count))
Ok(delete_backup_keys_for_session::v3::Response { count, etag })
}
async fn get_count_etag(
services: &Services,
sender_user: &UserId,
version: &str,
) -> (UInt, String) {
let count: UInt = services
) -> Result<(UInt, String)> {
let count = services
.key_backups
.count_keys(sender_user, version)
.await
.try_into()
.expect("number of keys should fit into a UInt");
.map(TryInto::try_into);
let etag = services.key_backups.get_etag(sender_user, version).await;
let etag = services.key_backups.get_etag(sender_user, version).map(Ok);
(count, etag)
Ok(try_join(count, etag).await?)
}
+12 -13
View File
@@ -5,11 +5,8 @@ use conduwuit::{Result, Server};
use ruma::{
RoomVersionId,
api::client::discovery::get_capabilities::{
self,
v3::{
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
ThirdPartyIdChangesCapability,
},
self, Capabilities, GetLoginTokenCapability, RoomVersionStability,
RoomVersionsCapability, ThirdPartyIdChangesCapability,
},
};
use serde_json::json;
@@ -28,17 +25,19 @@ pub(crate) async fn get_capabilities_route(
Server::available_room_versions().collect();
let mut capabilities = Capabilities::default();
capabilities.room_versions = RoomVersionsCapability::new(
services.server.config.default_room_version.clone(),
capabilities.room_versions = RoomVersionsCapability {
available,
);
default: services.server.config.default_room_version.clone(),
};
// Only allow 3pid changes if SMTP is configured
capabilities.thirdparty_id_changes =
ThirdPartyIdChangesCapability::new(services.threepid.email_requirement().may_change());
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
enabled: services.threepid.email_requirement().may_change(),
};
capabilities.get_login_token =
GetLoginTokenCapability::new(services.server.config.login_via_existing_session);
capabilities.get_login_token = GetLoginTokenCapability {
enabled: services.server.config.login_via_existing_session,
};
// MSC4133 capability
capabilities.set("uk.tcpip.msc4133.profile_fields", json!({"enabled": true}))?;
@@ -57,5 +56,5 @@ pub(crate) async fn get_capabilities_route(
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?;
}
Ok(get_capabilities::v3::Response::new(capabilities))
Ok(get_capabilities::v3::Response { capabilities })
}
+3 -5
View File
@@ -12,9 +12,7 @@ use futures::{
FutureExt, StreamExt, TryFutureExt, TryStreamExt,
future::{OptionFuture, join, join3, try_join3},
};
use ruma::{
OwnedEventId, UserId, api::client::context::get_context, assign, events::StateEventType,
};
use ruma::{OwnedEventId, UserId, api::client::context::get_context, events::StateEventType};
use crate::{
Ruma,
@@ -215,7 +213,7 @@ pub(crate) async fn get_context_route(
.collect()
.await;
Ok(assign!(get_context::v3::Response::new(), {
Ok(get_context::v3::Response {
event: base_event.map(at!(1)).map(Event::into_format),
start: events_before
@@ -245,5 +243,5 @@ pub(crate) async fn get_context_route(
.collect(),
state,
}))
})
}
+13 -13
View File
@@ -2,14 +2,10 @@ use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, at};
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,
},
assign,
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 crate::Ruma;
@@ -37,7 +33,7 @@ pub(crate) async fn put_dehydrated_device_route(
.set_dehydrated_device(sender_user, body.body)
.await?;
Ok(put_dehydrated_device::Response::new(device_id))
Ok(put_dehydrated_device::Response { device_id })
}
/// # `DELETE /_matrix/client/../dehydrated_device`
@@ -55,7 +51,7 @@ pub(crate) async fn delete_dehydrated_device_route(
services.users.remove_device(sender_user, &device_id).await;
Ok(delete_dehydrated_device::Response::new(device_id))
Ok(delete_dehydrated_device::Response { device_id })
}
/// # `GET /_matrix/client/../dehydrated_device`
@@ -71,7 +67,10 @@ pub(crate) async fn get_dehydrated_device_route(
let device = services.users.get_dehydrated_device(sender_user).await?;
Ok(get_dehydrated_device::Response::new(device.device_id, device.device_data))
Ok(get_dehydrated_device::Response {
device_id: device.device_id,
device_data: device.device_data,
})
}
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
@@ -115,7 +114,8 @@ pub(crate) async fn get_dehydrated_events_route(
.collect()
.await;
Ok(assign!(get_events::Response::new(events), {
Ok(get_events::Response {
events,
next_batch: next_batch.as_ref().map(ToString::to_string),
}))
})
}
+46 -23
View File
@@ -25,7 +25,7 @@ pub(crate) async fn get_devices_route(
.collect()
.await;
Ok(get_devices::v3::Response::new(devices))
Ok(get_devices::v3::Response { devices })
}
/// # `GET /_matrix/client/r0/devices/{deviceId}`
@@ -41,7 +41,7 @@ pub(crate) async fn get_device_route(
.await
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
Ok(get_device::v3::Response::new(device))
Ok(get_device::v3::Response { device })
}
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
@@ -73,16 +73,19 @@ pub(crate) async fn update_device_route(
.update_device_metadata(sender_user, &body.device_id, &device)
.await?;
Ok(update_device::v3::Response::new())
Ok(update_device::v3::Response {})
},
| Err(_) => {
let Some(appservice) = appservice else {
return Err!(Request(NotFound("Device not found.")));
};
if !appservice.registration.device_management {
return Err!(Request(NotFound("Device not found.")));
}
debug!(
"Creating new device for {sender_user} from appservice {} as device ID does not \
exist",
"Creating new device for {sender_user} from appservice {} as MSC4190 is enabled \
and device ID does not exist",
appservice.registration.id
);
@@ -99,7 +102,7 @@ pub(crate) async fn update_device_route(
)
.await?;
return Ok(update_device::v3::Response::new());
return Ok(update_device::v3::Response {});
},
}
}
@@ -121,28 +124,39 @@ pub(crate) async fn delete_device_route(
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
// Appservices get to skip UIAA for this endpoint
if appservice.is_none() {
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
services
.users
.remove_device(sender_user, &body.device_id)
.await;
return Ok(delete_device::v3::Response {});
}
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
services
.users
.remove_device(sender_user, &body.device_id)
.await;
Ok(delete_device::v3::Response::new())
Ok(delete_device::v3::Response {})
}
/// # `POST /_matrix/client/v3/delete_devices`
///
/// Deletes the given list of devices.
///
/// - Requires UIAA to verify user password.
/// - Requires UIAA to verify user password unless from an appservice with
/// MSC4190 enabled.
///
/// For each device:
/// - Invalidates access token
@@ -157,18 +171,27 @@ pub(crate) async fn delete_devices_route(
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
// Appservices get to skip UIAA for this endpoint
if appservice.is_none() {
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
debug!(
"Skipping UIAA for {sender_user} as this is from an appservice and MSC4190 is \
enabled"
);
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id).await;
}
return Ok(delete_devices::v3::Response {});
}
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
for device_id in &body.devices {
services.users.remove_device(sender_user, device_id).await;
}
Ok(delete_devices::v3::Response::new())
Ok(delete_devices::v3::Response {})
}
+140 -50
View File
@@ -1,16 +1,23 @@
use std::iter::once;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err, info,
Err, Event, Result, RoomVersion, err, info,
utils::{
TryFutureExtExt,
math::Expected,
result::FlatOk,
stream::{ReadyExt, WidebandExt},
},
};
use conduwuit_service::Services;
use futures::StreamExt;
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{join, join4, join5},
};
use ruma::{
RoomId, ServerName, UInt, UserId,
OwnedRoomId, RoomId, ServerName, UInt, UserId,
api::{
client::{
directory::{
@@ -21,9 +28,15 @@ use ruma::{
},
federation,
},
assign,
directory::{Filter, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::StateEventType,
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork, RoomTypeFilter},
events::{
StateEventType,
room::{
create::RoomCreateEventContent,
join_rules::{JoinRule, RoomJoinRulesEventContent},
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
},
},
uint,
};
use tokio::join;
@@ -96,11 +109,12 @@ pub(crate) async fn get_public_rooms_route(
err!(Request(Unknown(warn!(?body.server, "Failed to return /publicRooms: {e}"))))
})?;
Ok(assign!(get_public_rooms::v3::Response::new(response.chunk), {
Ok(get_public_rooms::v3::Response {
chunk: response.chunk,
prev_batch: response.prev_batch,
next_batch: response.next_batch,
total_room_count_estimate: response.total_room_count_estimate,
}))
})
}
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
@@ -183,7 +197,7 @@ pub(crate) async fn set_room_visibility_route(
},
}
Ok(set_room_visibility::v3::Response::new())
Ok(set_room_visibility::v3::Response {})
}
/// # `GET /_matrix/client/r0/directory/list/room/{roomId}`
@@ -198,13 +212,13 @@ pub(crate) async fn get_room_visibility_route(
return Err!(Request(NotFound("Room not found")));
}
let visibility = if services.rooms.directory.is_public_room(&body.room_id).await {
room::Visibility::Public
} else {
room::Visibility::Private
};
Ok(get_room_visibility::v3::Response::new(visibility))
Ok(get_room_visibility::v3::Response {
visibility: if services.rooms.directory.is_public_room(&body.room_id).await {
room::Visibility::Public
} else {
room::Visibility::Private
},
})
}
pub(crate) async fn get_public_rooms_filtered_helper(
@@ -222,24 +236,24 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.sending
.send_federation_request(
other_server,
assign!(federation::directory::get_public_rooms_filtered::v1::Request::new(), {
federation::directory::get_public_rooms_filtered::v1::Request {
limit,
since: since.map(ToOwned::to_owned),
filter: assign!(Filter::new(), {
filter: Filter {
generic_search_term: filter.generic_search_term.clone(),
room_types: filter.room_types.clone(),
}),
},
room_network: RoomNetwork::Matrix,
}),
},
)
.await?;
return Ok(assign!(get_public_rooms_filtered::v3::Response::new(), {
return Ok(get_public_rooms_filtered::v3::Response {
chunk: response.chunk,
prev_batch: response.prev_batch,
next_batch: response.next_batch,
total_room_count_estimate: response.total_room_count_estimate,
}));
});
}
// Use limit or else 10, with maximum 100
@@ -270,24 +284,16 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.rooms
.directory
.public_rooms()
.wide_then(async |room_id| {
let summary = services
.rooms
.summary
.build_local_room_summary(&room_id)
.await
.expect("room in public room directory should exist");
summary.into()
})
.ready_filter_map(|chunk: PublicRoomsChunk| {
.map(ToOwned::to_owned)
.wide_then(|room_id| public_rooms_chunk(services, room_id))
.ready_filter_map(|chunk| {
if !filter.room_types.is_empty() && !filter.room_types.contains(&RoomTypeFilter::from(chunk.room_type.clone())) {
return None;
}
if let Some(query) = filter.generic_search_term.as_ref().map(|q| q.to_lowercase()) {
if let Some(name) = &chunk.name {
if name.to_lowercase().contains(&query) {
if name.as_str().to_lowercase().contains(&query) {
return Some(chunk);
}
}
@@ -314,7 +320,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.collect()
.await;
all_rooms.sort_by_key(|r| std::cmp::Reverse(r.num_joined_members));
all_rooms.sort_by(|l, r| r.num_joined_members.cmp(&l.num_joined_members));
let total_room_count_estimate = UInt::try_from(all_rooms.len())
.unwrap_or_else(|_| uint!(0))
@@ -329,12 +335,12 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.ge(&limit)
.then_some(format!("n{}", num_since.expected_add(limit)));
Ok(assign!(get_public_rooms_filtered::v3::Response::new(), {
Ok(get_public_rooms_filtered::v3::Response {
chunk,
prev_batch,
next_batch,
total_room_count_estimate,
}))
})
}
/// Checks whether the given user ID is allowed to publish the target room to
@@ -350,25 +356,109 @@ async fn user_can_publish_room(
// Server admins can always publish to their own room directory.
return Ok(true);
}
let (room_version, room_creators, power_levels) = join!(
let (create_event, room_version, power_levels_content) = join!(
services
.rooms
.state_accessor
.room_state_get(room_id, &StateEventType::RoomCreate, ""),
services.rooms.state.get_room_version(room_id),
services.rooms.state_accessor.get_room_creators(room_id),
services.rooms.state_accessor.get_room_power_levels(room_id),
services
.rooms
.state_accessor
.room_state_get_content::<RoomPowerLevelsEventContent>(
room_id,
&StateEventType::RoomPowerLevels,
""
)
);
let room_version = room_version
.as_ref()
.map_err(|_| err!(Request(NotFound("Unknown room"))))?;
let room_version_rules = room_version.rules().unwrap();
if room_version_rules
.authorization
let create_event = create_event.map_err(|_| err!(Request(NotFound("Unknown room"))))?;
if RoomVersion::new(room_version)
.expect("room version must be supported")
.explicitly_privilege_room_creators
&& room_creators.contains(user_id)
{
return Ok(true);
let create_content: RoomCreateEventContent =
serde_json::from_str(create_event.content().get())
.map_err(|_| err!(Database("Invalid event content for m.room.create")))?;
let is_creator = create_content
.additional_creators
.unwrap_or_default()
.into_iter()
.chain(once(create_event.sender().to_owned()))
.any(|sender| sender == user_id);
if is_creator {
return Ok(true);
}
}
match power_levels_content.map(RoomPowerLevels::from) {
| Ok(pl) => Ok(pl.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias)),
| Err(e) =>
if e.is_not_found() {
Ok(create_event.sender() == user_id)
} else {
Err!(Database("Invalid event content for m.room.power_levels: {e}"))
},
}
}
async fn public_rooms_chunk(services: &Services, room_id: OwnedRoomId) -> PublicRoomsChunk {
let name = services.rooms.state_accessor.get_name(&room_id).ok();
let room_type = services.rooms.state_accessor.get_room_type(&room_id).ok();
let canonical_alias = services
.rooms
.state_accessor
.get_canonical_alias(&room_id)
.ok();
let avatar_url = services.rooms.state_accessor.get_avatar(&room_id);
let topic = services.rooms.state_accessor.get_room_topic(&room_id).ok();
let world_readable = services.rooms.state_accessor.is_world_readable(&room_id);
let join_rule = services
.rooms
.state_accessor
.room_state_get_content(&room_id, &StateEventType::RoomJoinRules, "")
.map_ok(|c: RoomJoinRulesEventContent| match c.join_rule {
| JoinRule::Public => PublicRoomJoinRule::Public,
| JoinRule::Knock => "knock".into(),
| JoinRule::KnockRestricted(_) => "knock_restricted".into(),
| _ => "invite".into(),
});
let guest_can_join = services.rooms.state_accessor.guest_can_join(&room_id);
let num_joined_members = services.rooms.state_cache.room_joined_count(&room_id);
let (
(avatar_url, canonical_alias, guest_can_join, join_rule, name),
(num_joined_members, room_type, topic, world_readable),
) = join(
join5(avatar_url, canonical_alias, guest_can_join, join_rule, name),
join4(num_joined_members, room_type, topic, world_readable),
)
.boxed()
.await;
PublicRoomsChunk {
avatar_url: avatar_url.into_option().unwrap_or_default().url,
canonical_alias,
guest_can_join,
join_rule: join_rule.unwrap_or_default(),
name,
num_joined_members: num_joined_members
.map(TryInto::try_into)
.map(Result::ok)
.flat_ok()
.unwrap_or_else(|| uint!(0)),
room_id,
room_type,
topic,
world_readable,
}
Ok(power_levels.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias))
}
+118 -99
View File
@@ -5,23 +5,25 @@ use std::{
use axum::extract::State;
use conduwuit::{
Err, Result, debug, debug_warn, err,
result::FlatOk,
utils::{IterStream, TryFutureExtExt, stream::WidebandExt},
Err, Error, Result, debug, debug_warn, err,
result::NotFound,
utils::{IterStream, stream::WidebandExt},
};
use conduwuit_service::{Services, users::parse_master_key};
use futures::{StreamExt, stream::FuturesUnordered};
use ruma::{
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
api::{
client::keys::{
claim_keys, get_key_changes, get_keys, upload_keys,
upload_signatures::{self},
upload_signing_keys,
client::{
error::ErrorKind,
keys::{
claim_keys, get_key_changes, get_keys, upload_keys,
upload_signatures::{self},
upload_signing_keys,
},
},
federation,
},
assign,
encryption::CrossSigningKey,
serde::Raw,
};
@@ -64,27 +66,6 @@ pub(crate) async fn upload_keys_route(
.await?;
}
for (key_id, fallback_key) in &body.fallback_keys {
if fallback_key
.deserialize()
.inspect_err(|e| {
debug_warn!(
%key_id,
?fallback_key,
"Invalid one time key JSON submitted by client, skipping: {e}"
);
})
.is_err()
{
continue;
}
services
.users
.add_fallback_key(sender_user, sender_device, key_id, fallback_key, false)
.await?;
}
if let Some(device_keys) = &body.device_keys {
let deser_device_keys = device_keys.deserialize().map_err(|e| {
err!(Request(BadJson(debug_warn!(
@@ -134,12 +115,12 @@ pub(crate) async fn upload_keys_route(
}
}
let one_time_key_counts = services
.users
.count_one_time_keys(sender_user, sender_device)
.await;
Ok(upload_keys::v3::Response::new(one_time_key_counts))
Ok(upload_keys::v3::Response {
one_time_key_counts: services
.users
.count_one_time_keys(sender_user, sender_device)
.await,
})
}
/// # `POST /_matrix/client/r0/keys/query`
@@ -193,7 +174,7 @@ pub(crate) async fn upload_signing_keys_route(
) -> Result<upload_signing_keys::v3::Response> {
let sender_user = body.sender_user();
if uiaa_needed_to_upload_keys(
match check_for_new_keys(
services,
sender_user,
body.self_signing_key.as_ref(),
@@ -201,11 +182,25 @@ pub(crate) async fn upload_signing_keys_route(
body.master_key.as_ref(),
)
.await
.inspect_err(|e| debug!(?e))
{
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
| Ok(exists) => {
if let Some(result) = exists {
// No-op, they tried to reupload the same set of keys
// (lost connection for example)
return Ok(result);
}
debug!(
"Skipping UIA in accordance with MSC3967, the user didn't have any existing keys"
);
// Some of the keys weren't found, so we let them upload
},
| _ => {
let _ = services
.uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
},
}
services
@@ -219,56 +214,77 @@ pub(crate) async fn upload_signing_keys_route(
)
.await?;
Ok(upload_signing_keys::v3::Response::new())
Ok(upload_signing_keys::v3::Response {})
}
async fn uiaa_needed_to_upload_keys(
async fn check_for_new_keys(
services: crate::State,
user_id: &UserId,
self_signing_key: Option<&Raw<CrossSigningKey>>,
user_signing_key: Option<&Raw<CrossSigningKey>>,
master_signing_key: Option<&Raw<CrossSigningKey>>,
) -> bool {
let (self_signing_key, user_signing_key, master_signing_key) = (
self_signing_key.map(Raw::deserialize).flat_ok(),
user_signing_key.map(Raw::deserialize).flat_ok(),
master_signing_key.map(Raw::deserialize).flat_ok(),
);
let (existing_self_signing_key, existing_user_signing_key, existing_master_signing_key) = futures::join!(
services
) -> Result<Option<upload_signing_keys::v3::Response>> {
debug!("checking for existing keys");
let mut empty = false;
if let Some(master_signing_key) = master_signing_key {
let (key, value) = parse_master_key(user_id, master_signing_key)?;
let result = services
.users
.get_master_key(None, user_id, &|_| true)
.await;
if result.is_not_found() {
empty = true;
} else {
let existing_master_key = result?;
let (existing_key, existing_value) = parse_master_key(user_id, &existing_master_key)?;
if existing_key != key || existing_value != value {
return Err!(Request(Forbidden(
"Tried to change an existing master key, UIA required"
)));
}
}
}
if let Some(user_signing_key) = user_signing_key {
let key = services.users.get_user_signing_key(user_id).await;
if key.is_not_found() && !empty {
return Err!(Request(Forbidden(
"Tried to update an existing user signing key, UIA required"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != user_signing_key.deserialize()? {
return Err!(Request(Forbidden(
"Tried to change an existing user signing key, UIA required"
)));
}
}
}
if let Some(self_signing_key) = self_signing_key {
let key = services
.users
.get_self_signing_key(None, user_id, &|_| true)
.ok(),
services.users.get_user_signing_key(user_id).ok(),
services.users.get_master_key(None, user_id, &|_| true).ok(),
);
let (existing_self_signing_key, existing_user_signing_key, existing_master_signing_key) = (
existing_self_signing_key
.as_ref()
.map(Raw::deserialize)
.flat_ok(),
existing_user_signing_key
.as_ref()
.map(Raw::deserialize)
.flat_ok(),
existing_master_signing_key
.as_ref()
.map(Raw::deserialize)
.flat_ok(),
);
if let Some(existing_master_signing_key) = existing_master_signing_key {
// If a master key exists, UIAA is required if any of the keys are different.
master_signing_key != Some(existing_master_signing_key)
|| user_signing_key != existing_user_signing_key
|| self_signing_key != existing_self_signing_key
} else {
// If no master key exists, UIAA is not required.
false
.await;
if key.is_not_found() && !empty {
debug!(?key);
return Err!(Request(Forbidden(
"Tried to add a new signing key independently from the master key"
)));
}
if !key.is_not_found() {
let existing_signing_key = key?.deserialize()?;
if existing_signing_key != self_signing_key.deserialize()? {
return Err!(Request(Forbidden(
"Tried to update an existing self signing key, UIA required"
)));
}
}
}
if empty {
return Ok(None);
}
Ok(Some(upload_signing_keys::v3::Response {}))
}
/// # `POST /_matrix/client/r0/keys/signatures/upload`
@@ -327,7 +343,7 @@ pub(crate) async fn upload_signatures_route(
}
}
Ok(upload_signatures::v3::Response::new())
Ok(upload_signatures::v3::Response { failures: BTreeMap::new() })
}
/// # `POST /_matrix/client/r0/keys/changes`
@@ -347,17 +363,18 @@ pub(crate) async fn get_key_changes_route(
let from = body
.from
.parse()
.map_err(|_| err!(Request(InvalidParam("Invalid `from`."))))?;
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`."))?;
let to = body
.to
.parse()
.map_err(|_| err!(Request(InvalidParam("Invalid `to`."))))?;
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `to`."))?;
device_list_updates.extend(
services
.users
.keys_changed(sender_user, Some(from), Some(to))
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
@@ -368,18 +385,18 @@ pub(crate) async fn get_key_changes_route(
device_list_updates.extend(
services
.users
.room_keys_changed(&room_id, Some(from), Some(to))
.room_keys_changed(room_id, Some(from), Some(to))
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
}
Ok(get_key_changes::v3::Response::new(
device_list_updates.into_iter().collect(),
// TODO
vec![],
))
Ok(get_key_changes::v3::Response {
changed: device_list_updates.into_iter().collect(),
left: Vec::new(), // TODO
})
}
pub(crate) async fn get_keys_helper<F>(
@@ -416,10 +433,10 @@ where
let mut devices = services.users.all_device_ids(user_id).boxed();
while let Some(device_id) = devices.next().await {
if let Ok(mut keys) = services.users.get_device_keys(user_id, &device_id).await {
if let Ok(mut keys) = services.users.get_device_keys(user_id, device_id).await {
let metadata = services
.users
.get_device_metadata(user_id, &device_id)
.get_device_metadata(user_id, device_id)
.await
.map_err(|_| {
err!(Database("all_device_keys contained nonexistent device."))
@@ -428,7 +445,7 @@ where
add_unsigned_device_display_name(&mut keys, metadata, include_display_names)
.map_err(|_| err!(Database("invalid device keys in database")))?;
container.insert(device_id.clone(), keys);
container.insert(device_id.to_owned(), keys);
}
}
@@ -489,7 +506,8 @@ where
device_keys_input_fed.insert(user_id.to_owned(), keys.clone());
}
let request = federation::keys::get_keys::v1::Request::new(device_keys_input_fed);
let request =
federation::keys::get_keys::v1::Request { device_keys: device_keys_input_fed };
let response = tokio::time::timeout(
timeout,
services.sending.send_federation_request(server, request),
@@ -543,13 +561,13 @@ where
}
}
Ok(assign!(get_keys::v3::Response::new(), {
Ok(get_keys::v3::Response {
failures,
device_keys,
master_keys,
self_signing_keys,
user_signing_keys,
}))
})
}
fn add_unsigned_device_display_name(
@@ -558,8 +576,7 @@ fn add_unsigned_device_display_name(
include_display_names: bool,
) -> serde_json::Result<()> {
if let Some(display_name) = metadata.display_name {
let mut object =
keys.deserialize_as_unchecked::<serde_json::Map<String, serde_json::Value>>()?;
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
let unsigned = object.entry("unsigned").or_insert_with(|| json!({}));
if let serde_json::Value::Object(unsigned_object) = unsigned {
@@ -625,7 +642,9 @@ pub(crate) async fn claim_keys_helper(
timeout,
services.sending.send_federation_request(
server,
federation::keys::claim_keys::v1::Request::new(one_time_keys_input_fed),
federation::keys::claim_keys::v1::Request {
one_time_keys: one_time_keys_input_fed,
},
),
)
.await
@@ -648,5 +667,5 @@ pub(crate) async fn claim_keys_helper(
}
}
Ok(assign!(claim_keys::v3::Response::new(one_time_keys), { failures: failures }))
Ok(claim_keys::v3::Response { failures, one_time_keys })
}
+81 -36
View File
@@ -9,11 +9,11 @@ use conduwuit::{
use conduwuit_core::error;
use conduwuit_service::{
Services,
media::{Dim, FileMeta, MXC_LENGTH},
media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta, MXC_LENGTH},
};
use reqwest::Url;
use ruma::{
UserId,
Mxc, UserId,
api::client::{
authenticated_media::{
get_content, get_content_as_filename, get_content_thumbnail, get_media_config,
@@ -21,9 +21,7 @@ use ruma::{
},
media::create_content,
},
assign,
};
use service::media::mxc::Mxc;
use crate::Ruma;
@@ -32,9 +30,9 @@ pub(crate) async fn get_media_config_route(
State(services): State<crate::State>,
_body: Ruma<get_media_config::v1::Request>,
) -> Result<get_media_config::v1::Response> {
Ok(get_media_config::v1::Response::new(ruma_from_usize(
services.server.config.max_request_size,
)))
Ok(get_media_config::v1::Response {
upload_size: ruma_from_usize(services.server.config.max_request_size),
})
}
/// # `POST /_matrix/media/v3/upload`
@@ -84,9 +82,10 @@ pub(crate) async fn create_content_route(
.flatten()
});
Ok(assign!(create_content::v3::Response::new(mxc.to_string().into()), {
Ok(create_content::v3::Response {
content_uri: mxc.to_string().into(),
blurhash: blurhash.flatten(),
}))
})
}
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
@@ -115,7 +114,7 @@ pub(crate) async fn get_content_thumbnail_route(
content,
content_type,
content_disposition,
} = match fetch_thumbnail_meta(&services, &mxc, user, body.timeout_ms, &dim).await {
} = match fetch_thumbnail(&services, &mxc, user, body.timeout_ms, &dim).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound =>
@@ -129,14 +128,13 @@ pub(crate) async fn get_content_thumbnail_route(
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching thumbnail."))),
};
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content_thumbnail::v1::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
Ok(get_content_thumbnail::v1::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition,
))
})
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
@@ -163,7 +161,7 @@ pub(crate) async fn get_content_route(
content,
content_type,
content_disposition,
} = match fetch_file_meta(&services, &mxc, user, body.timeout_ms).await {
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
@@ -176,14 +174,13 @@ pub(crate) async fn get_content_route(
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
let content_disposition =
make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);
Ok(get_content::v1::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
Ok(get_content::v1::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition,
))
})
}
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
@@ -211,7 +208,7 @@ pub(crate) async fn get_content_as_filename_route(
content,
content_type,
content_disposition,
} = match fetch_file_meta(&services, &mxc, user, body.timeout_ms).await {
} = match fetch_file(&services, &mxc, user, body.timeout_ms, None).await {
| Ok(meta) => meta,
| Err(conduwuit::Error::Io(e)) => match e.kind() {
| std::io::ErrorKind::NotFound => return Err!(Request(NotFound("Media not found."))),
@@ -224,17 +221,13 @@ pub(crate) async fn get_content_as_filename_route(
| Err(_) => return Err!(Request(Unknown("Unknown error when fetching file."))),
};
let content_disposition = make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
Some(&body.filename),
);
Ok(get_content_as_filename::v1::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
Ok(get_content_as_filename::v1::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition,
))
})
}
/// # `GET /_matrix/client/v1/media/preview_url`
@@ -285,6 +278,58 @@ pub(crate) async fn get_media_preview_route(
})
}
async fn fetch_thumbnail(
services: &Services,
mxc: &Mxc<'_>,
user: &UserId,
timeout_ms: Duration,
dim: &Dim,
) -> Result<FileMeta> {
let FileMeta {
content,
content_type,
content_disposition,
} = fetch_thumbnail_meta(services, mxc, user, timeout_ms, dim).await?;
let content_disposition = Some(make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
None,
));
Ok(FileMeta {
content,
content_type,
content_disposition,
})
}
async fn fetch_file(
services: &Services,
mxc: &Mxc<'_>,
user: &UserId,
timeout_ms: Duration,
filename: Option<&str>,
) -> Result<FileMeta> {
let FileMeta {
content,
content_type,
content_disposition,
} = fetch_file_meta(services, mxc, user, timeout_ms).await?;
let content_disposition = Some(make_content_disposition(
content_disposition.as_ref(),
content_type.as_deref(),
filename,
));
Ok(FileMeta {
content,
content_type,
content_disposition,
})
}
async fn fetch_thumbnail_meta(
services: &Services,
mxc: &Mxc<'_>,
+47 -65
View File
@@ -6,16 +6,15 @@ use conduwuit::{
Err, Result, err,
utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
};
use conduwuit_service::media::{CORP_CROSS_ORIGIN, Dim, FileMeta};
use conduwuit_service::media::{CACHE_CONTROL_IMMUTABLE, CORP_CROSS_ORIGIN, Dim, FileMeta};
use reqwest::Url;
use ruma::{
Mxc,
api::client::media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail,
get_media_config, get_media_preview,
},
assign,
};
use service::media::mxc::Mxc;
use crate::{Ruma, RumaResponse, client::create_content_route};
@@ -26,9 +25,9 @@ pub(crate) async fn get_media_config_legacy_route(
State(services): State<crate::State>,
_body: Ruma<get_media_config::v3::Request>,
) -> Result<get_media_config::v3::Response> {
Ok(get_media_config::v3::Response::new(ruma_from_usize(
services.server.config.max_request_size,
)))
Ok(get_media_config::v3::Response {
upload_size: ruma_from_usize(services.server.config.max_request_size),
})
}
/// # `GET /_matrix/media/v1/config`
@@ -154,16 +153,13 @@ pub(crate) async fn get_content_legacy_route(
None,
);
Ok(assign!(
get_content::v3::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
Ok(get_content::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
@@ -181,16 +177,13 @@ pub(crate) async fn get_content_legacy_route(
None,
);
Ok(assign!(
get_content::v3::Response::new(
response.file,
response.content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
Ok(get_content::v3::Response {
file: response.file,
content_type: response.content_type,
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
},
@@ -251,15 +244,13 @@ pub(crate) async fn get_content_as_filename_legacy_route(
Some(&body.filename),
);
Ok(assign!(get_content_as_filename::v3::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
Ok(get_content_as_filename::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
content_disposition: Some(content_disposition),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
@@ -277,16 +268,13 @@ pub(crate) async fn get_content_as_filename_legacy_route(
None,
);
Ok(assign!(
get_content_as_filename::v3::Response::new(
response.file,
response.content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
}
))
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(content_disposition),
content_type: response.content_type,
file: response.file,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
})
} else {
Err!(Request(NotFound("Media not found.")))
},
@@ -347,16 +335,13 @@ pub(crate) async fn get_content_thumbnail_legacy_route(
None,
);
Ok(assign!(
get_content_thumbnail::v3::Response::new(
content.expect("entire file contents"),
content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
}
))
Ok(get_content_thumbnail::v3::Response {
file: content.expect("entire file contents"),
content_type: content_type.map(Into::into),
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
},
| _ =>
if !services.globals.server_is_ours(&body.server_name) && body.allow_remote {
@@ -374,16 +359,13 @@ pub(crate) async fn get_content_thumbnail_legacy_route(
None,
);
Ok(assign!(
get_content_thumbnail::v3::Response::new(
response.file,
response.content_type.unwrap_or_default(),
content_disposition,
),
{
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.to_owned()),
}
))
Ok(get_content_thumbnail::v3::Response {
file: response.file,
content_type: response.content_type,
cross_origin_resource_policy: Some(CORP_CROSS_ORIGIN.into()),
cache_control: Some(CACHE_CONTROL_IMMUTABLE.into()),
content_disposition: Some(content_disposition),
})
} else {
Err!(Request(NotFound("Media not found.")))
},
+20 -10
View File
@@ -1,8 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::ban_user,
assign,
events::room::member::{MembershipState, RoomMemberEventContent},
};
@@ -25,19 +24,30 @@ pub(crate) async fn ban_user_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.unwrap_or_else(|_| RoomMemberEventContent::new(MembershipState::Ban));
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
body.user_id.to_string(),
&assign!(RoomMemberEventContent::new(MembershipState::Ban), {
reason: body.reason.clone(),
redact_events: body.redact_events,
}),
),
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Ban,
reason: body.reason.clone(),
displayname: None, // display name may be offensive
avatar_url: None, // avatar may be offensive
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
redact_events: body.redact_events,
..current_member_content
}),
sender_user,
Some(&body.room_id),
&state_lock,
+46 -59
View File
@@ -2,19 +2,18 @@ use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PartialPdu},
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
warn,
};
use futures::FutureExt;
use ruma::{
RoomId, UserId,
api::{
client::membership::invite_user::{self, v3::InviteUserId},
federation::membership::create_invite,
api::{client::membership::invite_user, federation::membership::create_invite},
events::{
invite_permission_config::FilterLevel,
room::member::{MembershipState, RoomMemberEventContent},
},
events::room::member::{MembershipState, RoomMemberEventContent},
};
use ruminuwuity::invite_permission_config::FilterLevel;
use service::Services;
use super::banned_room_check;
@@ -52,11 +51,7 @@ pub(crate) async fn invite_user_route(
.await?;
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId(InviteUserId {
user_id: recipient_user,
reason,
..
}) => {
| invite_user::v3::InvitationRecipient::UserId { user_id: recipient_user } => {
let sender_filter_level = services
.users
.invite_filter_level(recipient_user, sender_user)
@@ -64,7 +59,7 @@ pub(crate) async fn invite_user_route(
if !matches!(sender_filter_level, FilterLevel::Allow) {
// drop invites if the sender has the recipient filtered
return Ok(invite_user::v3::Response::new());
return Ok(invite_user::v3::Response {});
}
if let Ok(target_user_membership) = services
@@ -100,13 +95,13 @@ pub(crate) async fn invite_user_route(
sender_user,
recipient_user,
&body.room_id,
reason.clone(),
body.reason.clone(),
false,
)
.boxed()
.await?;
Ok(invite_user::v3::Response::new())
Ok(invite_user::v3::Response {})
},
| _ => {
Err!(Request(NotFound("User not found.")))
@@ -146,28 +141,25 @@ pub(crate) async fn invite_helper(
let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
content.displayname = services.users.displayname(recipient_user).await.ok();
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
content.is_direct = Some(is_direct);
content.reason = reason;
let content = RoomMemberEventContent {
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
let (pdu, pdu_json) = services
.rooms
.timeline
.create_hash_and_sign_event(
PartialPdu::state(recipient_user.to_string(), &content),
PduBuilder::state(recipient_user.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
)
.await?;
let invite_room_state = services
.rooms
.state
.summary_stripped(&pdu, room_id, recipient_user)
.await;
let invite_room_state = services.rooms.state.summary_stripped(&pdu, room_id).await;
drop(state_lock);
@@ -176,39 +168,32 @@ pub(crate) async fn invite_helper(
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
let mut request = create_invite::v2::Request::new(
room_id.to_owned(),
(*pdu.event_id).to_owned(),
room_version_id.clone(),
services
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
);
request.via = services
.rooms
.state_cache
.servers_route_via(room_id)
.await
.ok();
let response = services
.sending
.send_federation_request(recipient_user.server_name(), request)
.send_federation_request(recipient_user.server_name(), create_invite::v2::Request {
room_id: room_id.to_owned(),
event_id: (*pdu.event_id).to_owned(),
room_version: room_version_id.clone(),
event: services
.sending
.convert_to_outgoing_federation_event(pdu_json.clone())
.await,
invite_room_state,
via: services
.rooms
.state_cache
.servers_route_via(room_id)
.await
.ok(),
})
.await?;
// We do not add the event_id field to the pdu here because of signature and
// hashes checks
let (event_id, value) = gen_event_id_canonical_json(
&response.event,
&room_version_id
.rules()
.expect("room version should have defined rules"),
)
.map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
let (event_id, value) = gen_event_id_canonical_json(&response.event, &room_version_id)
.map_err(|e| {
err!(Request(BadJson(warn!("Could not convert event to canonical JSON: {e}"))))
})?;
if pdu.event_id != event_id {
return Err!(Request(BadJson(warn!(
@@ -244,18 +229,20 @@ pub(crate) async fn invite_helper(
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let mut content = RoomMemberEventContent::new(MembershipState::Invite);
content.displayname = services.users.displayname(recipient_user).await.ok();
content.avatar_url = services.users.avatar_url(recipient_user).await.ok();
content.blurhash = services.users.blurhash(recipient_user).await.ok();
content.is_direct = Some(is_direct);
content.reason = reason;
let content = RoomMemberEventContent {
displayname: services.users.displayname(recipient_user).await.ok(),
avatar_url: services.users.avatar_url(recipient_user).await.ok(),
blurhash: services.users.blurhash(recipient_user).await.ok(),
is_direct: Some(is_direct),
reason,
..RoomMemberEventContent::new(MembershipState::Invite)
};
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(recipient_user.to_string(), &content),
PduBuilder::state(recipient_user.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
+71 -59
View File
@@ -7,7 +7,7 @@ use conduwuit::{
matrix::{
StateKey,
event::{gen_event_id, gen_event_id_canonical_json},
pdu::{PartialPdu, PduEvent},
pdu::{PduBuilder, PduEvent},
state_res,
},
result::FlatOk,
@@ -24,8 +24,10 @@ use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId,
RoomVersionId, UserId,
api::{
client::membership::{join_room_by_id, join_room_by_id_or_alias},
error::{ErrorKind, IncompatibleRoomVersionErrorData},
client::{
error::ErrorKind,
membership::{join_room_by_id, join_room_by_id_or_alias},
},
federation::{self},
},
canonical_json::to_canonical_value,
@@ -87,6 +89,7 @@ pub(crate) async fn join_room_by_id_route(
.rooms
.state_cache
.servers_invite_via(&body.room_id)
.map(ToOwned::to_owned)
.collect()
.await;
@@ -166,6 +169,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
@@ -206,7 +210,11 @@ pub(crate) async fn join_room_by_id_or_alias_route(
)
.await?;
let addl_via_servers = services.rooms.state_cache.servers_invite_via(&room_id);
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_state_servers = services
.rooms
@@ -219,7 +227,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: OwnedUserId| user.server_name().to_owned())
.map(|user: &UserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
@@ -246,7 +254,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
.boxed()
.await?;
Ok(join_room_by_id_or_alias::v3::Response::new(join_room_response.room_id))
Ok(join_room_by_id_or_alias::v3::Response { room_id: join_room_response.room_id })
}
pub async fn join_room_by_id_helper(
@@ -277,7 +285,7 @@ pub async fn join_room_by_id_helper(
.await
{
debug_warn!("{sender_user} is already joined in {room_id}");
return Ok(join_room_by_id::v3::Response::new(room_id.to_owned()));
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
}
if let Err(e) = services
@@ -377,14 +385,13 @@ async fn join_room_by_id_helper_remote(
info!("make_join finished");
let room_version = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
let room_version_rules = room_version
.rules()
.expect("room version should have defined rules");
let room_version_id = make_join_response.room_version.unwrap_or(RoomVersionId::V1);
if !services.server.supported_room_version(&room_version) {
if !services.server.supported_room_version(&room_version_id) {
// How did we get here?
return Err!(BadServerResponse("Remote room version {room_version} is not supported"));
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut join_event_stub: CanonicalJsonObject =
@@ -396,7 +403,7 @@ async fn join_room_by_id_helper_remote(
let join_authorized_via_users_server = {
use RoomVersionId::*;
if !matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
if !matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
join_event_stub
.get("content")
.map(|s| {
@@ -418,32 +425,36 @@ async fn join_room_by_id_helper_remote(
.expect("Timestamp is valid js_int value"),
),
);
let mut join_content = RoomMemberEventContent::new(MembershipState::Join);
join_content.displayname = services.users.displayname(sender_user).await.ok();
join_content.avatar_url = services.users.avatar_url(sender_user).await.ok();
join_content.blurhash = services.users.blurhash(sender_user).await.ok();
join_content.reason = reason;
join_content
.join_authorized_via_users_server
.clone_from(&join_authorized_via_users_server);
join_event_stub.insert(
"content".to_owned(),
to_canonical_value(join_content).expect("event is valid, we just created it"),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
join_authorized_via_users_server: join_authorized_via_users_server.clone(),
..RoomMemberEventContent::new(MembershipState::Join)
})
.expect("event is valid, we just created it"),
);
// Remove event id if it exists
join_event_stub.remove("event_id");
// We keep the "event_id" in the pdu only in v1 or
// v2 rooms
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
join_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut join_event_stub, &room_version_rules)?;
.hash_and_sign_event(&mut join_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&join_event_stub, &room_version_rules)?;
let event_id = gen_event_id(&join_event_stub, &room_version_id)?;
// Add event_id back
join_event_stub
@@ -453,14 +464,15 @@ async fn join_room_by_id_helper_remote(
let mut join_event = join_event_stub;
info!("Asking {remote_server} for send_join in room {room_id}");
let send_join_request = federation::membership::create_join_event::v2::Request::new(
room_id.to_owned(),
event_id.clone(),
services
let send_join_request = federation::membership::create_join_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
omit_members: false,
pdu: services
.sending
.convert_to_outgoing_federation_event(join_event.clone())
.await,
);
};
let send_join_response = match services
.sending
@@ -484,7 +496,7 @@ async fn join_room_by_id_helper_remote(
);
let (signed_event_id, signed_value) =
gen_event_id_canonical_json(signed_raw, &room_version_rules).map_err(|e| {
gen_event_id_canonical_json(signed_raw, &room_version_id).map_err(|e| {
err!(Request(BadJson(warn!(
"Could not convert event to canonical JSON: {e}"
))))
@@ -559,7 +571,7 @@ async fn join_room_by_id_helper_remote(
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_rules)
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
.inspect_err(|e| {
debug_warn!("Could not validate send_join response room_state event: {e:?}");
})
@@ -607,7 +619,7 @@ async fn join_room_by_id_helper_remote(
.then(|pdu| {
services
.server_keys
.validate_and_add_event_id_no_fetch(pdu, &room_version_rules)
.validate_and_add_event_id_no_fetch(pdu, &room_version_id)
})
.ready_filter_map(Result::ok)
.ready_for_each(|(event_id, value)| {
@@ -628,7 +640,7 @@ async fn join_room_by_id_helper_remote(
};
let auth_check = state_res::event_auth::auth_check(
&room_version.rules().unwrap(),
&state_res::RoomVersion::new(&room_version_id)?,
&parsed_join_pdu,
None, // TODO: third party invite
|k, s| state_fetch(k.clone(), s.into()),
@@ -735,7 +747,8 @@ async fn join_room_by_id_helper_local(
// This is a restricted room, check if we can complete the join requirements
// locally.
let needs_auth_user =
user_can_perform_restricted_join(services, sender_user, room_id).await;
user_can_perform_restricted_join(services, sender_user, room_id, &room_version)
.await;
if needs_auth_user.is_ok_and(is_true!()) {
// If there was an error or the value is false, we'll try joining over
// federation. Since it's Ok(true), we can authorise this locally.
@@ -748,19 +761,21 @@ async fn join_room_by_id_helper_local(
}
}
let mut content = RoomMemberEventContent::new(MembershipState::Join);
content.displayname = services.users.displayname(sender_user).await.ok();
content.avatar_url = services.users.avatar_url(sender_user).await.ok();
content.blurhash = services.users.blurhash(sender_user).await.ok();
content.reason.clone_from(&reason);
content.join_authorized_via_users_server = auth_user;
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
join_authorized_via_users_server: auth_user,
..RoomMemberEventContent::new(MembershipState::Join)
};
// Try normal join first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(sender_user.to_string(), &content),
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
@@ -809,16 +824,16 @@ async fn make_join_request(
"Asking {remote_server} for make_join (attempt {make_join_counter}/{})",
servers.len()
);
let mut request = federation::membership::prepare_join_event::v1::Request::new(
room_id.to_owned(),
sender_user.to_owned(),
);
request.ver = services.server.supported_room_versions().collect();
let make_join_response = services
.sending
.send_federation_request(remote_server, request)
.send_federation_request(
remote_server,
federation::membership::prepare_join_event::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services.server.supported_room_versions().collect(),
},
)
.await;
trace!("make_join response: {:?}", make_join_response);
@@ -851,17 +866,14 @@ async fn make_join_request(
rules, but is unable to authorise a join for us. Will continue trying."
);
},
| ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData {
room_version,
..
}) => {
| ErrorKind::IncompatibleRoomVersion { room_version } => {
warn!(
"{remote_server} reports the room we are trying to join is \
v{room_version}, which we do not support."
);
return Err(e);
},
| ErrorKind::Forbidden => {
| ErrorKind::Forbidden { .. } => {
warn!("{remote_server} refuses to let us join: {e}.");
return Err(e);
},
+27 -20
View File
@@ -1,8 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::kick_user,
assign,
events::room::member::{MembershipState, RoomMemberEventContent},
};
@@ -19,33 +18,41 @@ pub(crate) async fn kick_user_route(
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
if !services
let Ok(event) = services
.rooms
.state_cache
.user_membership(&body.user_id, &body.room_id)
.state_accessor
.get_member(&body.room_id, &body.user_id)
.await
.is_some_and(|membership| {
matches!(
membership,
MembershipState::Invite | MembershipState::Join | MembershipState::Knock
)
}) {
return Err!(Request(Forbidden("You cannot kick users who are not in the room.")));
else {
// copy synapse's behaviour of returning 200 without any change to the state
// instead of erroring on left users
return Ok(kick_user::v3::Response::new());
};
if !matches!(
event.membership,
MembershipState::Invite | MembershipState::Knock | MembershipState::Join,
) {
return Err!(Request(Forbidden(
"Cannot kick a user who is not apart of the room (current membership: {})",
event.membership
)));
}
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
body.user_id.to_string(),
&assign!(RoomMemberEventContent::new(MembershipState::Leave), {
reason: body.reason.clone(),
redact_events: body.redact_events,
}),
),
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
is_direct: None,
join_authorized_via_users_server: None,
third_party_invite: None,
..event
}),
sender_user,
Some(&body.room_id),
&state_lock,
+83 -73
View File
@@ -6,7 +6,7 @@ use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, info,
matrix::{
event::gen_event_id,
pdu::{PartialPdu, PduEvent},
pdu::{PduBuilder, PduEvent},
},
result::FlatOk,
trace,
@@ -15,8 +15,8 @@ use conduwuit::{
};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName,
OwnedUserId, RoomId, UserId,
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedServerName, RoomId,
RoomVersionId, UserId,
api::{
client::knock::knock_room,
federation::{self},
@@ -73,6 +73,7 @@ pub(crate) async fn knock_room_route(
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
@@ -112,7 +113,11 @@ pub(crate) async fn knock_room_route(
)
.await?;
let addl_via_servers = services.rooms.state_cache.servers_invite_via(&room_id);
let addl_via_servers = services
.rooms
.state_cache
.servers_invite_via(&room_id)
.map(ToOwned::to_owned);
let addl_state_servers = services
.rooms
@@ -125,7 +130,7 @@ pub(crate) async fn knock_room_route(
.iter()
.map(|event| event.get_field("sender"))
.filter_map(FlatOk::flat_ok)
.map(|user: OwnedUserId| user.server_name().to_owned())
.map(|user: &UserId| user.server_name().to_owned())
.stream()
.chain(addl_via_servers)
.collect()
@@ -183,7 +188,7 @@ async fn knock_room_by_id_helper(
.await
{
debug_warn!("{sender_user} is already knocked in {room_id}");
return Ok(knock_room::v3::Response::new(room_id.into()));
return Ok(knock_room::v3::Response { room_id: room_id.into() });
}
if let Ok(membership) = services
@@ -334,27 +339,34 @@ async fn knock_room_helper_local(
) -> Result {
debug_info!("We can knock locally");
let room_version = services.rooms.state.get_room_version(room_id).await?;
let room_version_rules = room_version
.rules()
.expect("room version should have defined rules");
let room_version_id = services.rooms.state.get_room_version(room_id).await?;
if !room_version_rules.authorization.knocking {
if matches!(
room_version_id,
RoomVersionId::V1
| RoomVersionId::V2
| RoomVersionId::V3
| RoomVersionId::V4
| RoomVersionId::V5
| RoomVersionId::V6
) {
return Err!(Request(Forbidden("This room does not support knocking.")));
}
let mut content = RoomMemberEventContent::new(MembershipState::Knock);
content.displayname = services.users.displayname(sender_user).await.ok();
content.avatar_url = services.users.avatar_url(sender_user).await.ok();
content.blurhash = services.users.blurhash(sender_user).await.ok();
content.reason.clone_from(&reason.clone());
let content = RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason: reason.clone(),
..RoomMemberEventContent::new(MembershipState::Knock)
};
// Try normal knock first
let Err(error) = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(sender_user.to_string(), &content),
PduBuilder::state(sender_user.to_string(), &content),
sender_user,
Some(room_id),
&state_lock,
@@ -369,18 +381,19 @@ async fn knock_room_helper_local(
return Err(error);
}
warn!("We couldn't do the knock locally, maybe federation can help to satisfy the knock");
let (make_knock_response, remote_server) =
make_knock_request(services, sender_user, room_id, servers).await?;
info!("make_knock finished");
let room_version = make_knock_response.room_version;
let room_version_rules = room_version
.rules()
.expect("room version should have defined rules");
let room_version_id = make_knock_response.room_version;
if !services.server.supported_room_version(&room_version) {
return Err!(BadServerResponse("Remote room version {room_version} is not supported"));
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut knock_event_stub = serde_json::from_str::<CanonicalJsonObject>(
@@ -411,17 +424,24 @@ async fn knock_room_helper_local(
);
knock_event_stub.insert(
"content".to_owned(),
to_canonical_value(content).expect("event is valid, we just created it"),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
..RoomMemberEventContent::new(MembershipState::Knock)
})
.expect("event is valid, we just created it"),
);
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut knock_event_stub, &room_version_rules)?;
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&knock_event_stub, &room_version_rules)?;
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
// Add event_id
knock_event_stub
@@ -431,14 +451,14 @@ async fn knock_room_helper_local(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let send_knock_request = federation::membership::create_knock_event::v1::Request::new(
room_id.to_owned(),
event_id.clone(),
services
let send_knock_request = federation::knock::send_knock::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.await,
);
};
services
.sending
@@ -500,13 +520,12 @@ async fn knock_room_helper_remote(
info!("make_knock finished");
let room_version = make_knock_response.room_version;
let room_version_rules = room_version
.rules()
.expect("room version should have defined rules");
let room_version_id = make_knock_response.room_version;
if !services.server.supported_room_version(&room_version) {
return Err!(BadServerResponse("Remote room version {room_version} is not supported"));
if !services.server.supported_room_version(&room_version_id) {
return Err!(BadServerResponse(
"Remote room version {room_version_id} is not supported by conduwuit"
));
}
let mut knock_event_stub: CanonicalJsonObject =
@@ -526,26 +545,26 @@ async fn knock_room_helper_remote(
.expect("Timestamp is valid js_int value"),
),
);
let mut knock_content = RoomMemberEventContent::new(MembershipState::Knock);
knock_content.displayname = services.users.displayname(sender_user).await.ok();
knock_content.avatar_url = services.users.avatar_url(sender_user).await.ok();
knock_content.blurhash = services.users.blurhash(sender_user).await.ok();
knock_content.reason = reason;
knock_event_stub.insert(
"content".to_owned(),
to_canonical_value(knock_content).expect("event is valid, we just created it"),
to_canonical_value(RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
reason,
..RoomMemberEventContent::new(MembershipState::Knock)
})
.expect("event is valid, we just created it"),
);
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut knock_event_stub, &room_version_rules)?;
.hash_and_sign_event(&mut knock_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&knock_event_stub, &room_version_rules)?;
let event_id = gen_event_id(&knock_event_stub, &room_version_id)?;
// Add event_id
knock_event_stub
@@ -555,18 +574,18 @@ async fn knock_room_helper_remote(
let knock_event = knock_event_stub;
info!("Asking {remote_server} for send_knock in room {room_id}");
let request = federation::membership::create_knock_event::v1::Request::new(
room_id.to_owned(),
event_id.clone(),
services
let send_knock_request = federation::knock::send_knock::v1::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(knock_event.clone())
.await,
);
};
let send_knock_response = services
.sending
.send_federation_request(&remote_server, request)
.send_federation_request(&remote_server, send_knock_request)
.await?;
info!("send_knock finished");
@@ -585,17 +604,7 @@ async fn knock_room_helper_remote(
let state = send_knock_response
.knock_room_state
.iter()
.map(|event| {
#[allow(deprecated)]
let raw_value = match event {
| federation::membership::RawStrippedState::Stripped(raw_state) =>
&raw_state.clone().into_json(),
| federation::membership::RawStrippedState::Pdu(raw_value) => raw_value,
| _ => panic!("unknown raw stripped state type"),
};
serde_json::from_str::<CanonicalJsonObject>(raw_value.get())
})
.map(|event| serde_json::from_str::<CanonicalJsonObject>(event.clone().into_json().get()))
.filter_map(Result::ok);
let mut state_map: HashMap<u64, OwnedEventId> = HashMap::new();
@@ -620,7 +629,7 @@ async fn knock_room_helper_remote(
continue;
};
let event_id = gen_event_id(&event, &room_version_rules)?;
let event_id = gen_event_id(&event, &room_version_id)?;
let shortstatekey = services
.rooms
.short
@@ -700,7 +709,7 @@ async fn make_knock_request(
sender_user: &UserId,
room_id: &RoomId,
servers: &[OwnedServerName],
) -> Result<(federation::membership::prepare_knock_event::v1::Response, OwnedServerName)> {
) -> Result<(federation::knock::create_knock_event_template::v1::Response, OwnedServerName)> {
let mut make_knock_response_and_server =
Err!(BadServerResponse("No server available to assist in knocking."));
@@ -713,15 +722,16 @@ async fn make_knock_request(
info!("Asking {remote_server} for make_knock ({make_knock_counter})");
let mut request = federation::membership::prepare_knock_event::v1::Request::new(
room_id.to_owned(),
sender_user.to_owned(),
);
request.ver = services.server.supported_room_versions().collect();
let make_knock_response = services
.sending
.send_federation_request(remote_server, request)
.send_federation_request(
remote_server,
federation::knock::create_knock_event_template::v1::Request {
room_id: room_id.to_owned(),
user_id: sender_user.to_owned(),
ver: services.server.supported_room_versions().collect(),
},
)
.await;
trace!("make_knock response: {make_knock_response:?}");
+34 -27
View File
@@ -3,13 +3,13 @@ use std::collections::HashSet;
use axum::extract::State;
use conduwuit::{
Err, Pdu, Result, debug_info, debug_warn, err,
matrix::{event::gen_event_id, pdu::PartialPdu},
matrix::{event::gen_event_id, pdu::PduBuilder},
utils::{self, FutureBoolExt, future::ReadyEqExt},
warn,
};
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, UserId,
CanonicalJsonObject, CanonicalJsonValue, OwnedServerName, RoomId, RoomVersionId, UserId,
api::{
client::membership::leave_room,
federation::{self},
@@ -42,7 +42,11 @@ pub(crate) async fn leave_room_route(
// Make a user leave all their joined rooms, rescinds knocks, forgets all rooms,
// and ignores errors
pub async fn leave_all_rooms(services: &Services, user_id: &UserId) {
let rooms_joined = services.rooms.state_cache.rooms_joined(user_id);
let rooms_joined = services
.rooms
.state_cache
.rooms_joined(user_id)
.map(ToOwned::to_owned);
let rooms_invited = services
.rooms
@@ -138,17 +142,18 @@ pub async fn leave_room(
.await;
match user_member_event_content {
| Ok(mut content) => {
content.membership = MembershipState::Leave;
content.reason = reason;
content.join_authorized_via_users_server = None;
content.is_direct = None;
| Ok(content) => {
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(user_id.to_string(), &content),
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason,
join_authorized_via_users_server: None,
is_direct: None,
..content
}),
user_id,
Some(room_id),
&state_lock,
@@ -221,6 +226,7 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
.rooms
.state_cache
.servers_invite_via(room_id)
.map(ToOwned::to_owned)
.collect::<HashSet<OwnedServerName>>()
.await,
);
@@ -254,7 +260,7 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
.filter_map(|event| event.get_field("sender").ok().flatten())
.filter_map(|sender: &str| UserId::parse(sender).ok())
.filter_map(|sender| {
if !services.globals.user_is_local(&sender) {
if !services.globals.user_is_local(sender) {
Some(sender.server_name().to_owned())
} else {
None
@@ -283,10 +289,10 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
.sending
.send_federation_request(
remote_server.as_ref(),
federation::membership::prepare_leave_event::v1::Request::new(
room_id.to_owned(),
user_id.to_owned(),
),
federation::membership::prepare_leave_event::v1::Request {
room_id: room_id.to_owned(),
user_id: user_id.to_owned(),
},
)
.await;
@@ -323,10 +329,6 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
)));
}
let room_version_rules = room_version_id
.rules()
.expect("room version should have defined rules");
let mut leave_event_stub = serde_json::from_str::<CanonicalJsonObject>(
make_leave_response.event.get(),
)
@@ -364,16 +366,21 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
}
// room v3 and above removed the "event_id" field from remote PDU format
leave_event_stub.remove("event_id");
match room_version_id {
| RoomVersionId::V1 | RoomVersionId::V2 => {},
| _ => {
leave_event_stub.remove("event_id");
},
}
// In order to create a compatible ref hash (EventID) the `hashes` field needs
// to be present
services
.server_keys
.hash_and_sign_event(&mut leave_event_stub, &room_version_rules)?;
.hash_and_sign_event(&mut leave_event_stub, &room_version_id)?;
// Generate event id
let event_id = gen_event_id(&leave_event_stub, &room_version_rules)?;
let event_id = gen_event_id(&leave_event_stub, &room_version_id)?;
// Add event_id back
leave_event_stub
@@ -386,14 +393,14 @@ pub async fn remote_leave_room<S: ::std::hash::BuildHasher>(
.sending
.send_federation_request(
&remote_server,
federation::membership::create_leave_event::v2::Request::new(
room_id.to_owned(),
event_id.clone(),
services
federation::membership::create_leave_event::v2::Request {
room_id: room_id.to_owned(),
event_id: event_id.clone(),
pdu: services
.sending
.convert_to_outgoing_federation_event(leave_event.clone())
.await,
),
},
)
.await?;
+72 -48
View File
@@ -9,7 +9,7 @@ use conduwuit::{
use futures::{FutureExt, StreamExt, future::join};
use ruma::{
api::client::membership::{
get_member_events::{self},
get_member_events::{self, v3::MembershipEventFilter},
joined_members::{self, v3::RoomMember},
},
events::{
@@ -43,20 +43,20 @@ pub(crate) async fn get_member_events_route(
return Err!(Request(Forbidden("You don't have permission to view this room.")));
}
let chunk = services
.rooms
.state_accessor
.room_state_full(&body.room_id)
.ready_filter_map(Result::ok)
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
.map(at!(1))
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
.map(Event::into_format)
.collect()
.boxed()
.await;
Ok(get_member_events::v3::Response::new(chunk))
Ok(get_member_events::v3::Response {
chunk: services
.rooms
.state_accessor
.room_state_full(&body.room_id)
.ready_filter_map(Result::ok)
.ready_filter(|((ty, _), _)| *ty == StateEventType::RoomMember)
.map(at!(1))
.ready_filter_map(|pdu| membership_filter(pdu, membership, not_membership))
.map(Event::into_format)
.collect()
.boxed()
.await,
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/joined_members`
@@ -78,46 +78,70 @@ pub(crate) async fn joined_members_route(
return Err!(Request(Forbidden("You don't have permission to view this room.")));
}
let joined = services
.rooms
.state_cache
.room_members(&body.room_id)
.broad_then(|user_id| async move {
let mut member = RoomMember::new();
let (display_name, avatar_url) = join(
services.users.displayname(&user_id).ok(),
services.users.avatar_url(&user_id).ok(),
)
.await;
member.display_name = display_name;
member.avatar_url = avatar_url;
Ok(joined_members::v3::Response {
joined: services
.rooms
.state_cache
.room_members(&body.room_id)
.map(ToOwned::to_owned)
.broad_then(|user_id| async move {
let (display_name, avatar_url) = join(
services.users.displayname(&user_id).ok(),
services.users.avatar_url(&user_id).ok(),
)
.await;
(user_id, member)
})
.collect()
.await;
Ok(joined_members::v3::Response::new(joined))
(user_id, RoomMember { display_name, avatar_url })
})
.collect()
.await,
})
}
fn membership_filter<Pdu: Event>(
pdu: Pdu,
membership_state_filter: Option<&MembershipState>,
not_membership_state_filter: Option<&MembershipState>,
for_membership: Option<&MembershipEventFilter>,
not_membership: Option<&MembershipEventFilter>,
) -> Option<impl Event> {
let membership_state_filter = match for_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(MembershipEventFilter::Leave) => MembershipState::Leave,
| Some(_) | None => MembershipState::Join,
};
let not_membership_state_filter = match not_membership {
| Some(MembershipEventFilter::Ban) => MembershipState::Ban,
| Some(MembershipEventFilter::Invite) => MembershipState::Invite,
| Some(MembershipEventFilter::Join) => MembershipState::Join,
| Some(MembershipEventFilter::Knock) => MembershipState::Knock,
| Some(_) | None => MembershipState::Leave,
};
let evt_membership = pdu.get_content::<RoomMemberEventContent>().ok()?.membership;
if let Some(membership_state_filter) = membership_state_filter
&& *membership_state_filter != evt_membership
{
return None;
if for_membership.is_some() && not_membership.is_some() {
if membership_state_filter != evt_membership
|| not_membership_state_filter == evt_membership
{
None
} else {
Some(pdu)
}
} else if for_membership.is_some() && not_membership.is_none() {
if membership_state_filter != evt_membership {
None
} else {
Some(pdu)
}
} else if not_membership.is_some() && for_membership.is_none() {
if not_membership_state_filter == evt_membership {
None
} else {
Some(pdu)
}
} else {
Some(pdu)
}
if let Some(not_membership_state_filter) = not_membership_state_filter
&& *not_membership_state_filter == evt_membership
{
return None;
}
Some(pdu)
}
+9 -8
View File
@@ -47,14 +47,15 @@ pub(crate) async fn joined_rooms_route(
State(services): State<crate::State>,
body: Ruma<joined_rooms::v3::Request>,
) -> Result<joined_rooms::v3::Response> {
let joined_rooms = services
.rooms
.state_cache
.rooms_joined(body.sender_user())
.collect()
.await;
Ok(joined_rooms::v3::Response::new(joined_rooms))
Ok(joined_rooms::v3::Response {
joined_rooms: services
.rooms
.state_cache
.rooms_joined(body.sender_user())
.map(ToOwned::to_owned)
.collect()
.await,
})
}
/// Checks if the room is banned in any way possible and the sender user is not
+11 -10
View File
@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::membership::unban_user,
events::room::member::{MembershipState, RoomMemberEventContent},
@@ -18,9 +18,9 @@ pub(crate) async fn unban_user_route(
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let mut current_member_content = services
let current_member_content = services
.rooms
.state_accessor
.get_member(&body.room_id, &body.user_id)
@@ -34,17 +34,18 @@ pub(crate) async fn unban_user_route(
)));
}
current_member_content.membership = MembershipState::Leave;
current_member_content.reason.clone_from(&body.reason);
current_member_content.join_authorized_via_users_server = None;
current_member_content.third_party_invite = None;
current_member_content.is_direct = None;
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(body.user_id.to_string(), &current_member_content),
PduBuilder::state(body.user_id.to_string(), &RoomMemberEventContent {
membership: MembershipState::Leave,
reason: body.reason.clone(),
join_authorized_via_users_server: None,
third_party_invite: None,
is_direct: None,
..current_member_content
}),
sender_user,
Some(&body.room_id),
&state_lock,
+10 -13
View File
@@ -26,17 +26,15 @@ use ruma::{
DeviceId, RoomId, UserId,
api::{
Direction,
client::{filter::RoomEventFilter, message::get_message_events},
error::{ErrorKind, SenderIgnoredErrorData},
client::{error::ErrorKind, filter::RoomEventFilter, message::get_message_events},
},
assign,
events::{
AnyStateEvent, StateEventType,
TimelineEventType::{self, *},
invite_permission_config::FilterLevel,
},
serde::Raw,
};
use ruminuwuity::invite_permission_config::FilterLevel;
use tracing::warn;
use crate::Ruma;
@@ -76,6 +74,7 @@ pub(crate) async fn get_message_events_route(
ClientIp(client_ip): ClientIp,
body: Ruma<get_message_events::v3::Request>,
) -> Result<get_message_events::v3::Response> {
debug_assert!(IGNORED_MESSAGE_TYPES.is_sorted(), "IGNORED_MESSAGE_TYPES is not sorted");
let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref();
let room_id = &body.room_id;
@@ -200,12 +199,12 @@ pub(crate) async fn get_message_events_route(
.map(Event::into_format)
.collect();
Ok(assign!(get_message_events::v3::Response::new(), {
Ok(get_message_events::v3::Response {
start: from.to_string(),
end: next_token.as_ref().map(PduCount::to_string),
chunk: chunk,
state: state,
}))
chunk,
state,
})
}
pub(crate) async fn lazy_loading_witness<'a, I>(
@@ -302,7 +301,7 @@ where
{
// exclude Synapse's dummy events from bloating up response bodies. clients
// don't need to see this.
if event.kind().to_string() == "org.matrix.dummy_event" {
if event.kind().to_cow_str() == "org.matrix.dummy_event" {
return Ok(true);
}
@@ -324,7 +323,7 @@ where
if server_ignored {
// the sender's server is ignored, so ignore this event
return Err(Error::BadRequest(
ErrorKind::SenderIgnored(SenderIgnoredErrorData::new()),
ErrorKind::SenderIgnored { sender: None },
"The sender's server is ignored by this server.",
));
}
@@ -333,9 +332,7 @@ where
// the recipient of this PDU has the sender ignored, and we're not
// configured to send ignored messages to clients
return Err(Error::BadRequest(
ErrorKind::SenderIgnored(SenderIgnoredErrorData::with_sender(
event.sender().to_owned(),
)),
ErrorKind::SenderIgnored { sender: Some(event.sender().to_owned()) },
"You have ignored this sender.",
));
}
+3 -2
View File
@@ -15,7 +15,6 @@ pub(super) mod media;
pub(super) mod media_legacy;
pub(super) mod membership;
pub(super) mod message;
pub(super) mod mutual_rooms;
pub(super) mod openid;
pub(super) mod presence;
pub(super) mod profile;
@@ -36,6 +35,7 @@ pub(super) mod thirdparty;
pub(super) mod threads;
pub(super) mod to_device;
pub(super) mod typing;
pub(super) mod unstable;
pub(super) mod unversioned;
pub(super) mod user_directory;
pub(super) mod voip;
@@ -60,10 +60,10 @@ pub(super) use media_legacy::*;
pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, remote_leave_room};
pub(super) use message::*;
pub(super) use mutual_rooms::*;
pub(super) use openid::*;
pub(super) use presence::*;
pub(super) use profile::*;
pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
pub use push::recreate_push_rules_and_return;
pub(super) use push::*;
pub(super) use read_marker::*;
@@ -82,6 +82,7 @@ pub(super) use thirdparty::*;
pub(super) use threads::*;
pub(super) use to_device::*;
pub(super) use typing::*;
pub(super) use unstable::*;
pub(super) use unversioned::*;
pub(super) use user_directory::*;
pub(super) use voip::*;
-36
View File
@@ -1,36 +0,0 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::api::client::membership::mutual_rooms;
use crate::Ruma;
/// # `GET /_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms`
///
/// Gets all the rooms the sender shares with the specified user.
///
/// An implementation of [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
#[tracing::instrument(skip_all, name = "mutual_rooms", level = "info")]
pub(crate) async fn get_mutual_rooms_route(
State(services): State<crate::State>,
body: Ruma<mutual_rooms::unstable::Request>,
) -> Result<mutual_rooms::unstable::Response> {
let sender_user = body.sender_user();
if sender_user == body.user_id {
return Err!(Request(Unknown("You cannot request rooms in common with yourself.")));
}
if !services.users.exists(&body.user_id).await {
return Ok(mutual_rooms::unstable::Response::new(vec![]));
}
let mutual_rooms = services
.rooms
.state_cache
.get_shared_rooms(sender_user, &body.user_id)
.collect()
.await;
Ok(mutual_rooms::unstable::Response::new(mutual_rooms))
}
+5 -5
View File
@@ -29,10 +29,10 @@ pub(crate) async fn create_openid_token_route(
.users
.create_openid_token(&body.user_id, &access_token)?;
Ok(account::request_openid_token::v3::Response::new(
Ok(account::request_openid_token::v3::Response {
access_token,
TokenType::Bearer,
services.server.name.clone(),
Duration::from_secs(expires_in),
))
token_type: TokenType::Bearer,
matrix_server_name: services.server.name.clone(),
expires_in: Duration::from_secs(expires_in),
})
}
+6 -7
View File
@@ -2,10 +2,7 @@ use std::time::Duration;
use axum::extract::State;
use conduwuit::{Err, Result};
use ruma::{
api::client::presence::{get_presence, set_presence},
assign,
};
use ruma::api::client::presence::{get_presence, set_presence};
use crate::Ruma;
@@ -29,7 +26,7 @@ pub(crate) async fn set_presence_route(
.set_presence(body.sender_user(), &body.presence, None, None, body.status_msg.clone())
.await?;
Ok(set_presence::v3::Response::new())
Ok(set_presence::v3::Response {})
}
/// # `GET /_matrix/client/r0/presence/{userId}/status`
@@ -79,11 +76,13 @@ pub(crate) async fn get_presence_route(
.map(|millis| Duration::from_millis(millis.into())),
};
Ok(assign!(get_presence::v3::Response::new(presence.content.presence), {
Ok(get_presence::v3::Response {
// TODO: Should ruma just use the presenceeventcontent type here?
status_msg,
currently_active: presence.content.currently_active,
last_active_ago,
}))
presence: presence.content.presence,
})
},
| _ => Err!(Request(NotFound("Presence state for this user was not found"))),
}
+384 -340
View File
@@ -1,26 +1,229 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu, utils::to_canonical_object};
use conduwuit::{
Err, Result,
matrix::pdu::PduBuilder,
utils::{IterStream, future::TryExtExt, stream::TryIgnore},
warn,
};
use conduwuit_service::Services;
use futures::StreamExt;
use futures::{
FutureExt, StreamExt, TryStreamExt,
future::{join, join3, join4},
};
use ruma::{
UserId,
OwnedMxcUri, OwnedRoomId, UserId,
api::{
client::profile::{
delete_profile_field, get_profile, get_profile_field, set_profile_field,
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
},
federation,
},
assign,
events::room::member::{MembershipState, RoomMemberEventContent},
presence::PresenceState,
profile::{ProfileFieldName, ProfileFieldValue},
};
use serde_json::{Value, to_value};
use crate::Ruma;
/// # `PUT /_matrix/client/r0/profile/{userId}/displayname`
///
/// Updates the displayname.
///
/// - Also makes sure other users receive the update using presence EDUs
pub(crate) async fn set_displayname_route(
State(services): State<crate::State>,
body: Ruma<set_display_name::v3::Request>,
) -> Result<set_display_name::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(ToOwned::to_owned)
.collect()
.await;
update_displayname(&services, &body.user_id, body.displayname.clone(), &all_joined_rooms)
.boxed()
.await;
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
Ok(set_display_name::v3::Response {})
}
/// # `GET /_matrix/client/v3/profile/{userId}/displayname`
///
/// Returns the displayname of the user.
///
/// - If user is on another server and we do not have a local copy already fetch
/// displayname over federation
pub(crate) async fn get_displayname_route(
State(services): State<crate::State>,
body: Ruma<get_display_name::v3::Request>,
) -> Result<get_display_name::v3::Response> {
if !services.globals.user_is_local(&body.user_id) {
// Create and update our local copy of the user
if let Ok(response) = services
.sending
.send_federation_request(
body.user_id.server_name(),
federation::query::get_profile_information::v1::Request {
user_id: body.user_id.clone(),
field: None, // we want the full user's profile to update locally too
},
)
.await
{
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None, None).await?;
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
return Ok(get_display_name::v3::Response { displayname: response.displayname });
}
}
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err!(Request(NotFound("Profile was not found.")));
}
Ok(get_display_name::v3::Response {
displayname: services.users.displayname(&body.user_id).await.ok(),
})
}
/// # `PUT /_matrix/client/v3/profile/{userId}/avatar_url`
///
/// Updates the `avatar_url` and `blurhash`.
///
/// - Also makes sure other users receive the update using presence EDUs
pub(crate) async fn set_avatar_url_route(
State(services): State<crate::State>,
body: Ruma<set_avatar_url::v3::Request>,
) -> Result<set_avatar_url::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(ToOwned::to_owned)
.collect()
.await;
update_avatar_url(
&services,
&body.user_id,
body.avatar_url.clone(),
body.blurhash.clone(),
&all_joined_rooms,
)
.boxed()
.await;
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await
.ok();
}
Ok(set_avatar_url::v3::Response {})
}
/// # `GET /_matrix/client/v3/profile/{userId}/avatar_url`
///
/// Returns the `avatar_url` and `blurhash` of the user.
///
/// - If user is on another server and we do not have a local copy already fetch
/// `avatar_url` and blurhash over federation
pub(crate) async fn get_avatar_url_route(
State(services): State<crate::State>,
body: Ruma<get_avatar_url::v3::Request>,
) -> Result<get_avatar_url::v3::Response> {
if !services.globals.user_is_local(&body.user_id) {
// Create and update our local copy of the user
if let Ok(response) = services
.sending
.send_federation_request(
body.user_id.server_name(),
federation::query::get_profile_information::v1::Request {
user_id: body.user_id.clone(),
field: None, // we want the full user's profile to update locally as well
},
)
.await
{
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None, None).await?;
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
return Ok(get_avatar_url::v3::Response {
avatar_url: response.avatar_url,
blurhash: response.blurhash,
});
}
}
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err!(Request(NotFound("Profile was not found.")));
}
let (avatar_url, blurhash) = join(
services.users.avatar_url(&body.user_id).ok(),
services.users.blurhash(&body.user_id).ok(),
)
.await;
Ok(get_avatar_url::v3::Response { avatar_url, blurhash })
}
/// # `GET /_matrix/client/v3/profile/{userId}`
///
/// Returns the displayname, avatar_url, blurhash, and custom profile fields of
@@ -32,347 +235,188 @@ pub(crate) async fn get_profile_route(
State(services): State<crate::State>,
body: Ruma<get_profile::v3::Request>,
) -> Result<get_profile::v3::Response> {
let Some(profile) = fetch_full_profile(&services, &body.user_id).await else {
return Err!(Request(NotFound("This user's profile could not be fetched.")));
};
Ok(get_profile::v3::Response::from_iter(profile))
}
pub(crate) async fn get_profile_field_route(
State(services): State<crate::State>,
body: Ruma<get_profile_field::v3::Request>,
) -> Result<get_profile_field::v3::Response> {
let value = fetch_profile_field(&services, &body.user_id, body.field.clone()).await?;
Ok(assign!(get_profile_field::v3::Response::default(), { value }))
}
pub(crate) async fn set_profile_field_route(
State(services): State<crate::State>,
body: Ruma<set_profile_field::v3::Request>,
) -> Result<set_profile_field::v3::Response> {
if body.user_id != body.sender_user()
&& !(body.appservice_info.is_some()
|| services.admin.user_is_admin(body.sender_user()).await)
{
return Err!(Request(Forbidden("You may not change other users' profile data.")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
set_profile_field(&services, &body.user_id, ProfileFieldChange::Set(body.value.clone()))
.await?;
Ok(set_profile_field::v3::Response::new())
}
pub(crate) async fn delete_profile_field_route(
State(services): State<crate::State>,
body: Ruma<delete_profile_field::v3::Request>,
) -> Result<delete_profile_field::v3::Response> {
if body.user_id != body.sender_user()
&& !(body.appservice_info.is_some()
|| services.admin.user_is_admin(body.sender_user()).await)
{
return Err!(Request(Forbidden("You may not change other users' profile data.")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
set_profile_field(&services, &body.user_id, ProfileFieldChange::Delete(body.field.clone()))
.await?;
Ok(delete_profile_field::v3::Response::new())
}
async fn fetch_full_profile(
services: &Services,
user_id: &UserId,
) -> Option<BTreeMap<String, Value>> {
// If the user exists locally, fetch their local profile
if services.users.exists(user_id).await {
return Some(get_local_profile(services, user_id).await);
}
// Otherwise ask their homeserver
let Ok(response) = services
.sending
.send_federation_request(
user_id.server_name(),
federation::query::get_profile_information::v1::Request::new(user_id.to_owned()),
)
.await
else {
return None;
};
// Update our local copies of their profile fields
services.users.clear_profile(user_id).await;
for (field, value) in response.iter() {
let Ok(value) = ProfileFieldValue::new(field, value.to_owned()) else {
// Skip malformed fields
continue;
};
let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value)).await;
}
Some(BTreeMap::from_iter(response))
}
async fn fetch_profile_field(
services: &Services,
user_id: &UserId,
field: ProfileFieldName,
) -> Result<Option<ProfileFieldValue>> {
// If the user exists locally, fetch their local profile field
if services.globals.user_is_local(user_id) {
return Ok(get_local_profile_field(services, user_id, field).await);
}
// Otherwise ask their homeserver
let Ok(response) = services
.sending
.send_federation_request(
user_id.server_name(),
assign!(federation::query::get_profile_information::v1::Request::new(user_id.to_owned()), {
field: Some(field.clone())
}),
)
.await
else {
return Err!(Request(NotFound(
"User's homeserver could not provide this profile field."
)));
};
if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value.clone()))
.await;
Ok(Some(value))
} else {
Err!(Request(Unknown(
"User's homeserver returned malformed data for this profile field."
)))
}
} else {
let _ = set_profile_field(services, user_id, ProfileFieldChange::Delete(field)).await;
Ok(None)
}
}
pub(crate) async fn get_local_profile(
services: &Services,
user_id: &UserId,
) -> BTreeMap<String, Value> {
let mut profile = BTreeMap::new();
// Get displayname and avatar_url independently because `all_profile_keys`
// doesn't include them
for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] {
let key = field.as_str().to_owned();
if let Some(value) = get_local_profile_field(services, user_id, field).await {
profile.insert(key, value.value().into_owned());
}
}
// Insert all other profile fields
let mut all_fields = services.users.all_profile_keys(user_id);
while let Some((key, value)) = all_fields.next().await {
profile.insert(key, value);
}
profile
}
pub(crate) async fn get_local_profile_field(
services: &Services,
user_id: &UserId,
field: ProfileFieldName,
) -> Option<ProfileFieldValue> {
let value = match field.clone() {
| ProfileFieldName::AvatarUrl => services
.users
.avatar_url(user_id)
// Create and update our local copy of the user
if let Ok(response) = services
.sending
.send_federation_request(
body.user_id.server_name(),
federation::query::get_profile_information::v1::Request {
user_id: body.user_id.clone(),
field: None,
},
)
.await
.ok()
.map(to_value)
.transpose()
.expect("converting avatar url to value should succeed"),
| ProfileFieldName::DisplayName => services
.users
.displayname(user_id)
.await
.ok()
.map(to_value)
.transpose()
.expect("converting displayname to value should succeed"),
| other => services
.users
.profile_key(user_id, other.as_str())
.await
.ok(),
}?;
{
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None, None).await?;
}
Some(
ProfileFieldValue::new(field.as_str(), value)
.expect("local profile field should be valid"),
)
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
enum ProfileFieldChange {
Set(ProfileFieldValue),
Delete(ProfileFieldName),
}
impl ProfileFieldChange {
fn field_name(&self) -> ProfileFieldName {
match self {
| &Self::Delete(ref name) => name.clone(),
| &Self::Set(ref value) => value.field_name(),
}
}
fn value(&self) -> Option<Value> {
if let Self::Set(value) = self {
Some(value.value().into_owned())
} else {
None
}
}
}
async fn set_profile_field(
services: &Services,
user_id: &UserId,
change: ProfileFieldChange,
) -> Result<()> {
const MAX_KEY_LENGTH_BYTES: usize = 255;
const MAX_PROFILE_LENGTH_BYTES: usize = 65536;
let field_name = change.field_name();
// TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE,
// M_KEY_TOO_LARGE) for profile field size limits, but they're not in its list
// of error codes and Ruma doesn't have them. Should we return those, or is
// M_TOO_LARGE okay?
if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES {
return Err!(Request(TooLarge(
"Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length."
)));
}
// Serialize the entire profile as canonical JSON, including the new change,
// to check if it exceeds 64 KiB
{
let mut full_profile = get_local_profile(services, user_id).await;
match &change {
| ProfileFieldChange::Set(value) => {
full_profile.insert(
value.field_name().as_str().to_owned(),
value.value().clone().into_owned(),
);
},
| ProfileFieldChange::Delete(key) => {
full_profile.remove(key.as_str());
},
}
if let Ok(canonical_profile) = to_canonical_object(full_profile) {
if serde_json::to_string(&canonical_profile)
.expect("should be able to serialize to string")
.len() > MAX_PROFILE_LENGTH_BYTES
{
return Err!(
"Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in length."
for (profile_key, profile_key_value) in &response.custom_profile_fields {
services.users.set_profile_key(
&body.user_id,
profile_key,
Some(profile_key_value.clone()),
);
}
} else {
return Err!(Request(BadJson("Failed to canonicalize profile.")));
return Ok(get_profile::v3::Response {
displayname: response.displayname,
avatar_url: response.avatar_url,
blurhash: response.blurhash,
custom_profile_fields: response.custom_profile_fields,
});
}
}
match change {
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
services
.users
.set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty()));
},
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
services
.users
.set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid()));
},
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
services.users.set_displayname(user_id, None);
},
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
services.users.set_avatar_url(user_id, None);
},
| other =>
if other.field_name().as_str() == "blurhash" {
if let Some(Value::String(blurhash)) = other.value() {
services.users.set_blurhash(user_id, Some(blurhash));
} else {
services.users.set_blurhash(user_id, None);
}
} else {
services.users.set_profile_key(
user_id,
other.field_name().as_str(),
other.value(),
);
},
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err!(Request(NotFound("Profile was not found.")));
}
// If the user is local and changed their displayname or avatar_url, update it
// in all their joined rooms
if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
&& services.users.is_active_local(user_id).await
{
let displayname = services.users.displayname(user_id).await.ok();
let avatar_url = services.users.avatar_url(user_id).await.ok();
let membership_content = assign!(
RoomMemberEventContent::new(MembershipState::Join), { displayname, avatar_url }
);
let (avatar_url, blurhash, displayname, custom_profile_fields) = join4(
services.users.avatar_url(&body.user_id).ok(),
services.users.blurhash(&body.user_id).ok(),
services.users.displayname(&body.user_id).ok(),
services.users.all_profile_keys(&body.user_id).collect(),
)
.await;
let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id);
while let Some(room_id) = all_joined_rooms.next().await {
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
let _ = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(user_id.to_string(), &membership_content),
user_id,
Some(&room_id),
&state_lock,
)
.await;
}
if services.config.allow_local_presence {
// Send a presence EDU to indicate the profile changed
let _ = services
.presence
.ping_presence(user_id, &PresenceState::Online)
.await;
}
}
Ok(())
Ok(get_profile::v3::Response {
avatar_url,
blurhash,
displayname,
custom_profile_fields,
})
}
pub async fn update_displayname(
services: &Services,
user_id: &UserId,
displayname: Option<String>,
all_joined_rooms: &[OwnedRoomId],
) {
let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id).ok(),
services.users.displayname(user_id).ok(),
)
.await;
if displayname == current_displayname {
return;
}
services.users.set_displayname(user_id, displayname.clone());
// Send a new join membership event into all joined rooms
let avatar_url = &current_avatar_url;
let blurhash = &current_blurhash;
let displayname = &displayname;
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.try_stream()
.and_then(|room_id: &OwnedRoomId| async move {
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
displayname: displayname.clone(),
membership: MembershipState::Join,
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
join_authorized_via_users_server: None,
reason: None,
is_direct: None,
third_party_invite: None,
redact_events: None,
});
Ok((pdu, room_id))
})
.ignore_err()
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_avatar_url(
services: &Services,
user_id: &UserId,
avatar_url: Option<OwnedMxcUri>,
blurhash: Option<String>,
all_joined_rooms: &[OwnedRoomId],
) {
let (current_avatar_url, current_blurhash, current_displayname) = join3(
services.users.avatar_url(user_id).ok(),
services.users.blurhash(user_id).ok(),
services.users.displayname(user_id).ok(),
)
.await;
if current_avatar_url == avatar_url && current_blurhash == blurhash {
return;
}
services.users.set_avatar_url(user_id, avatar_url.clone());
services.users.set_blurhash(user_id, blurhash.clone());
// Send a new join membership event into all joined rooms
let avatar_url = &avatar_url;
let blurhash = &blurhash;
let displayname = &current_displayname;
let all_joined_rooms: Vec<_> = all_joined_rooms
.iter()
.try_stream()
.and_then(|room_id: &OwnedRoomId| async move {
let pdu = PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
avatar_url: avatar_url.clone(),
blurhash: blurhash.clone(),
membership: MembershipState::Join,
displayname: displayname.clone(),
join_authorized_via_users_server: None,
reason: None,
is_direct: None,
third_party_invite: None,
redact_events: None,
});
Ok((pdu, room_id))
})
.ignore_err()
.collect()
.await;
update_all_rooms(services, all_joined_rooms, user_id)
.boxed()
.await;
}
pub async fn update_all_rooms(
services: &Services,
all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>,
user_id: &UserId,
) {
for (pdu_builder, room_id) in all_joined_rooms {
let state_lock = services.rooms.state.mutex.lock(room_id).await;
if let Err(e) = services
.rooms
.timeline
.build_and_append_pdu(pdu_builder, user_id, Some(room_id), &state_lock)
.await
{
warn!(%user_id, %room_id, "Failed to update/send new profile join membership update in room: {e}");
}
}
}
+42 -36
View File
@@ -3,13 +3,13 @@ use conduwuit::{Err, Error, Result, err};
use conduwuit_service::Services;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue,
api::{
client::push::{
api::client::{
error::ErrorKind,
push::{
delete_pushrule, 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,
},
error::ErrorKind,
},
events::{
GlobalAccountDataEventType,
@@ -80,7 +80,9 @@ pub(crate) async fn get_pushrules_all_route(
global_ruleset.update_with_server_default(Ruleset::server_default(sender_user));
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent::new(PushRulesEventContent::new(global_ruleset.clone()));
let event = PushRulesEvent {
content: PushRulesEventContent { global: global_ruleset.clone() },
};
services
.account_data
@@ -89,7 +91,7 @@ pub(crate) async fn get_pushrules_all_route(
}
};
Ok(get_pushrules_all::v3::Response::new(global_ruleset))
Ok(get_pushrules_all::v3::Response { global: global_ruleset })
}
/// # `GET /_matrix/client/r0/pushrules/global/`
@@ -114,20 +116,21 @@ pub(crate) async fn get_pushrules_global_route(
// user somehow has non-existent push rule event. recreate it and return server
// default silently
let global_ruleset = Ruleset::server_default(sender_user);
let event = PushRulesEvent::new(PushRulesEventContent::new(global_ruleset.clone()));
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
};
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(event)?,
)
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
.await?;
return Ok(get_pushrules_global_scope::v3::Response::new(global_ruleset));
return Ok(get_pushrules_global_scope::v3::Response {
global: Ruleset::server_default(sender_user),
});
};
let account_data_content =
@@ -170,16 +173,16 @@ pub(crate) async fn get_pushrules_global_route(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent::new(PushRulesEventContent::new(
global_ruleset.clone(),
)))
&serde_json::to_value(PushRulesEvent {
content: PushRulesEventContent { global: global_ruleset.clone() },
})
.expect("to json always works"),
)
.await?;
}
};
Ok(get_pushrules_global_scope::v3::Response::new(global_ruleset))
Ok(get_pushrules_global_scope::v3::Response { global: global_ruleset })
}
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
@@ -213,7 +216,7 @@ pub(crate) async fn get_pushrule_route(
.map(Into::into);
if let Some(rule) = rule {
Ok(get_pushrule::v3::Response::new(rule))
Ok(get_pushrule::v3::Response { rule })
} else {
Err!(Request(NotFound("Push rule not found.")))
}
@@ -272,7 +275,7 @@ pub(crate) async fn set_pushrule_route(
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.await?;
Ok(set_pushrule::v3::Response::new())
Ok(set_pushrule::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/actions`
@@ -306,7 +309,7 @@ pub(crate) async fn get_pushrule_actions_route(
.map(|rule| rule.actions().to_owned())
.ok_or_else(|| err!(Request(NotFound("Push rule not found."))))?;
Ok(get_pushrule_actions::v3::Response::new(actions))
Ok(get_pushrule_actions::v3::Response { actions })
}
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/actions`
@@ -339,7 +342,7 @@ pub(crate) async fn set_pushrule_actions_route(
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.await?;
Ok(set_pushrule_actions::v3::Response::new())
Ok(set_pushrule_actions::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/enabled`
@@ -357,7 +360,7 @@ pub(crate) async fn get_pushrule_enabled_route(
|| body.rule_id.as_str() == PredefinedOverrideRuleId::ContainsDisplayName.as_str()
|| body.rule_id.as_str() == PredefinedOverrideRuleId::RoomNotif.as_str()
{
return Ok(get_pushrule_enabled::v3::Response::new(false));
return Ok(get_pushrule_enabled::v3::Response { enabled: false });
}
let event: PushRulesEvent = services
@@ -373,7 +376,7 @@ pub(crate) async fn get_pushrule_enabled_route(
.map(ruma::push::AnyPushRuleRef::enabled)
.ok_or_else(|| err!(Request(NotFound("Push rule not found."))))?;
Ok(get_pushrule_enabled::v3::Response::new(enabled))
Ok(get_pushrule_enabled::v3::Response { enabled })
}
/// # `PUT /_matrix/client/r0/pushrules/global/{kind}/{ruleId}/enabled`
@@ -406,7 +409,7 @@ pub(crate) async fn set_pushrule_enabled_route(
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.await?;
Ok(set_pushrule_enabled::v3::Response::new())
Ok(set_pushrule_enabled::v3::Response {})
}
/// # `DELETE /_matrix/client/r0/pushrules/global/{kind}/{ruleId}`
@@ -448,7 +451,7 @@ pub(crate) async fn delete_pushrule_route(
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(account_data)?)
.await?;
Ok(delete_pushrule::v3::Response::new())
Ok(delete_pushrule::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushers`
@@ -460,7 +463,9 @@ pub(crate) async fn get_pushers_route(
) -> Result<get_pushers::v3::Response> {
let sender_user = body.sender_user();
Ok(get_pushers::v3::Response::new(services.pusher.get_pushers(sender_user).await))
Ok(get_pushers::v3::Response {
pushers: services.pusher.get_pushers(sender_user).await,
})
}
/// # `POST /_matrix/client/r0/pushers/set`
@@ -488,18 +493,19 @@ pub async fn recreate_push_rules_and_return(
services: &Services,
sender_user: &ruma::UserId,
) -> Result<get_pushrules_all::v3::Response> {
let global_ruleset = Ruleset::server_default(sender_user);
let event = PushRulesEvent::new(PushRulesEventContent::new(global_ruleset.clone()));
let ty = GlobalAccountDataEventType::PushRules;
let event = PushRulesEvent {
content: PushRulesEventContent {
global: Ruleset::server_default(sender_user),
},
};
services
.account_data
.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(event)?,
)
.update(None, sender_user, ty.to_string().into(), &serde_json::to_value(event)?)
.await?;
Ok(get_pushrules_all::v3::Response::new(global_ruleset))
Ok(get_pushrules_all::v3::Response {
global: Ruleset::server_default(sender_user),
})
}
+31 -21
View File
@@ -8,8 +8,7 @@ use ruma::{
api::client::{read_marker::set_read_marker, receipt::create_receipt},
events::{
RoomAccountDataEventType,
fully_read::{FullyReadEvent, FullyReadEventContent},
receipt::{Receipt, ReceiptEvent, ReceiptEventContent, ReceiptType},
receipt::{ReceiptThread, ReceiptType},
},
};
@@ -29,7 +28,9 @@ pub(crate) async fn set_read_marker_route(
let sender_user = body.sender_user();
if let Some(event) = &body.fully_read {
let fully_read_event = FullyReadEvent::new(FullyReadEventContent::new(event.to_owned()));
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
content: ruma::events::fully_read::FullyReadEventContent { event_id: event.clone() },
};
services
.account_data
@@ -61,16 +62,19 @@ pub(crate) async fn set_read_marker_route(
if services.config.allow_local_read_receipts
&& !services.users.is_suspended(sender_user).await?
{
let receipt_content = [(
let receipt_content = BTreeMap::from_iter([(
event.to_owned(),
BTreeMap::from_iter([(
ReceiptType::Read,
BTreeMap::from_iter([(
sender_user.to_owned(),
Receipt::new(MilliSecondsSinceUnixEpoch::now()),
ruma::events::receipt::Receipt {
ts: Some(MilliSecondsSinceUnixEpoch::now()),
thread: ReceiptThread::Unthreaded,
},
)]),
)]),
)];
)]);
services
.rooms
@@ -78,10 +82,10 @@ pub(crate) async fn set_read_marker_route(
.readreceipt_update(
sender_user,
&body.room_id,
&ReceiptEvent::new(
body.room_id.clone(),
ReceiptEventContent::from_iter(receipt_content),
),
&ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
},
)
.await;
}
@@ -107,7 +111,7 @@ pub(crate) async fn set_read_marker_route(
.private_read_set(&body.room_id, sender_user, count);
}
Ok(set_read_marker::v3::Response::new())
Ok(set_read_marker::v3::Response {})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}`
@@ -144,8 +148,11 @@ pub(crate) async fn create_receipt_route(
match body.receipt_type {
| create_receipt::v3::ReceiptType::FullyRead => {
let fully_read_event =
FullyReadEvent::new(FullyReadEventContent::new(body.event_id.clone()));
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
content: ruma::events::fully_read::FullyReadEventContent {
event_id: body.event_id.clone(),
},
};
services
.account_data
.update(
@@ -157,16 +164,19 @@ pub(crate) async fn create_receipt_route(
.await?;
},
| create_receipt::v3::ReceiptType::Read => {
let receipt_content = [(
let receipt_content = BTreeMap::from_iter([(
body.event_id.clone(),
BTreeMap::from_iter([(
ReceiptType::Read,
BTreeMap::from_iter([(
sender_user.to_owned(),
Receipt::new(MilliSecondsSinceUnixEpoch::now()),
ruma::events::receipt::Receipt {
ts: Some(MilliSecondsSinceUnixEpoch::now()),
thread: ReceiptThread::Unthreaded,
},
)]),
)]),
)];
)]);
services
.rooms
@@ -174,10 +184,10 @@ pub(crate) async fn create_receipt_route(
.readreceipt_update(
sender_user,
&body.room_id,
&ReceiptEvent::new(
body.room_id.clone(),
ReceiptEventContent::from_iter(receipt_content),
),
&ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
},
)
.await;
},
@@ -208,5 +218,5 @@ pub(crate) async fn create_receipt_route(
},
}
Ok(create_receipt::v3::Response::new())
Ok(create_receipt::v3::Response {})
}
+9 -10
View File
@@ -1,8 +1,8 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{
api::client::redact::redact_event, assign, events::room::redaction::RoomRedactionEventContent,
api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent,
};
use crate::Ruma;
@@ -28,19 +28,18 @@ pub(crate) async fn redact_event_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let event_id = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
redacts: Some(body.event_id.clone()),
..PartialPdu::timeline(
&assign!(RoomRedactionEventContent::new_v11(body.event_id.clone()), {
reason: body.reason.clone()
}),
)
..PduBuilder::timeline(&RoomRedactionEventContent {
redacts: Some(body.event_id.clone()),
reason: body.reason.clone(),
})
},
sender_user,
Some(&body.room_id),
@@ -50,5 +49,5 @@ pub(crate) async fn redact_event_route(
drop(state_lock);
Ok(redact_event::v3::Response::new(event_id))
Ok(redact_event::v3::Response { event_id })
}
+13 -15
View File
@@ -15,7 +15,6 @@ use ruma::{
get_relating_events_with_rel_type_and_event_type,
},
},
assign,
events::{TimelineEventType, relation::RelationType},
};
@@ -40,12 +39,11 @@ pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
body.dir,
)
.await
.map(|res| {
assign!(get_relating_events_with_rel_type_and_event_type::v1::Response::new(res.chunk), {
next_batch: res.next_batch,
prev_batch: res.prev_batch,
recursion_depth: res.recursion_depth,
})
.map(|res| get_relating_events_with_rel_type_and_event_type::v1::Response {
chunk: res.chunk,
next_batch: res.next_batch,
prev_batch: res.prev_batch,
recursion_depth: res.recursion_depth,
})
}
@@ -68,12 +66,11 @@ pub(crate) async fn get_relating_events_with_rel_type_route(
body.dir,
)
.await
.map(|res| {
assign!(get_relating_events_with_rel_type::v1::Response::new(res.chunk), {
next_batch: res.next_batch,
prev_batch: res.prev_batch,
recursion_depth: res.recursion_depth,
})
.map(|res| get_relating_events_with_rel_type::v1::Response {
chunk: res.chunk,
next_batch: res.next_batch,
prev_batch: res.prev_batch,
recursion_depth: res.recursion_depth,
})
}
@@ -204,11 +201,12 @@ async fn paginate_relations_with_filter(
.map(Event::into_format)
.collect();
Ok(assign!(get_relating_events::v1::Response::new(chunk), {
Ok(get_relating_events::v1::Response {
next_batch,
prev_batch: from.map(Into::into),
recursion_depth: recurse.then_some(depth.into()),
}))
chunk,
})
}
async fn visibility_filter<Pdu: Event + Send + Sync>(
+39 -33
View File
@@ -7,7 +7,7 @@ use conduwuit_service::Services;
use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::{
reporting::report_user,
report_user,
room::{report_content, report_room},
},
events::{Mentions, room::message::RoomMessageEventContent},
@@ -22,11 +22,9 @@ struct Report {
event_id: Option<OwnedEventId>,
user_id: Option<OwnedUserId>,
report_type: String,
reason: String,
reason: Option<String>,
}
const MAX_REASON_LENGTH: usize = 2000;
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
@@ -41,10 +39,10 @@ pub(crate) async fn report_room_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if body.reason.len() > MAX_REASON_LENGTH {
return Err!(Request(InvalidParam(
"Reason too long, should be {MAX_REASON_LENGTH} bytes or fewer",
)));
if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
));
}
delay_response().await;
@@ -54,7 +52,8 @@ pub(crate) async fn report_room_route(
// their discretion.
info!(
"Received room report by user {sender_user} for room {} with reason: \"{}\"",
body.room_id, body.reason
body.room_id,
body.reason.as_deref().unwrap_or("")
);
if !services
@@ -79,7 +78,7 @@ pub(crate) async fn report_room_route(
services.admin.send_message(build_report(report)).await.ok();
Ok(report_room::v3::Response::new())
Ok(report_room::v3::Response {})
}
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
@@ -99,22 +98,26 @@ pub(crate) async fn report_event_route(
delay_response().await;
let reason = body
.reason
.clone()
.unwrap_or_else(|| "<no reason provided>".to_owned());
// check if we know about the reported event ID or if it's invalid
let Ok(pdu) = services.rooms.timeline.get_pdu(&body.event_id).await else {
return Err!(Request(NotFound("Event ID is not known to us or Event ID is invalid")));
};
is_event_report_valid(&services, &pdu.event_id, &body.room_id, sender_user, &reason, &pdu)
.await?;
is_event_report_valid(
&services,
&pdu.event_id,
&body.room_id,
sender_user,
body.reason.as_ref(),
&pdu,
)
.await?;
info!(
"Received event report by user {sender_user} for room {} and event ID {}, with reason: \
\"{}\"",
body.room_id, body.event_id, reason
body.room_id,
body.event_id,
body.reason.as_deref().unwrap_or("")
);
let report = Report {
sender: sender_user.to_owned(),
@@ -122,11 +125,11 @@ pub(crate) async fn report_event_route(
event_id: Some(body.event_id.clone()),
user_id: None,
report_type: "event".to_owned(),
reason,
reason: body.reason.clone(),
};
services.admin.send_message(build_report(report)).await.ok();
Ok(report_content::v3::Response::new())
Ok(report_content::v3::Response {})
}
#[tracing::instrument(skip_all, fields(%client), name = "report_user", level = "info")]
@@ -141,17 +144,17 @@ pub(crate) async fn report_user_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if body.reason.len() > MAX_REASON_LENGTH {
return Err!(Request(InvalidParam(
"Reason too long, should be {MAX_REASON_LENGTH} bytes or fewer",
)));
if body.reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
));
}
delay_response().await;
if !services.users.is_active_local(&body.user_id).await {
// return 200 as to not reveal if the user exists. Recommended by spec.
return Ok(report_user::v3::Response::new());
return Ok(report_user::v3::Response {});
}
let report = Report {
@@ -165,12 +168,13 @@ pub(crate) async fn report_user_route(
info!(
"Received room report from {sender_user} for user {} with reason: \"{}\"",
body.user_id, body.reason
body.user_id,
body.reason.as_deref().unwrap_or("")
);
services.admin.send_message(build_report(report)).await.ok();
Ok(report_user::v3::Response::new())
Ok(report_user::v3::Response {})
}
/// in the following order:
@@ -184,7 +188,7 @@ async fn is_event_report_valid(
event_id: &EventId,
room_id: &RoomId,
sender_user: &UserId,
reason: &str,
reason: Option<&String>,
pdu: &PduEvent,
) -> Result<()> {
debug_info!(
@@ -196,10 +200,10 @@ async fn is_event_report_valid(
return Err!(Request(NotFound("Event ID does not belong to the reported room",)));
}
if reason.len() > MAX_REASON_LENGTH {
return Err!(Request(InvalidParam(
"Reason too long, should be {MAX_REASON_LENGTH} bytes or fewer",
)));
if reason.as_ref().is_some_and(|s| s.len() > 750) {
return Err!(Request(
InvalidParam("Reason too long, should be 750 characters or fewer",)
));
}
if !services
@@ -228,7 +232,9 @@ fn build_report(report: Report) -> RoomMessageEventContent {
if report.event_id.is_some() {
let _ = writeln!(text, "- Reported Event ID: `{}`", report.event_id.unwrap());
}
let _ = writeln!(text, "- Report Reason: {}", report.reason);
if let Some(reason) = report.reason {
let _ = writeln!(text, "- Report Reason: {reason}");
}
RoomMessageEventContent::text_markdown(text).add_mentions(Mentions::with_room_mention())
}
+9 -8
View File
@@ -26,12 +26,13 @@ pub(crate) async fn get_room_aliases_route(
return Err!(Request(Forbidden("You don't have permission to view this room.",)));
}
let aliases = services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.collect()
.await;
Ok(aliases::v3::Response::new(aliases))
Ok(aliases::v3::Response {
aliases: services
.rooms
.alias
.local_aliases_for_room(&body.room_id)
.map(ToOwned::to_owned)
.collect()
.await,
})
}
+126 -90
View File
@@ -2,19 +2,18 @@ use std::collections::{BTreeMap, BTreeSet};
use axum::extract::State;
use conduwuit::{
Err, Result, debug, debug_info, err, info,
matrix::{StateKey, pdu::PartialPdu},
Err, Result, RoomVersion, debug, debug_info, debug_warn, err, info,
matrix::{StateKey, pdu::PduBuilder},
trace, warn,
};
use conduwuit_service::{Services, appservice::RegistrationInfo};
use futures::FutureExt;
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, Int, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId,
OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
CanonicalJsonObject, Int, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId, RoomVersionId,
api::client::room::{self, create_room},
assign,
events::{
TimelineEventType,
invite_permission_config::FilterLevel,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
@@ -28,10 +27,8 @@ use ruma::{
},
},
int,
room_version_rules::{AuthorizationRules, RoomIdFormatVersion},
serde::{JsonObject, Raw},
};
use ruminuwuity::invite_permission_config::FilterLevel;
use serde_json::{json, value::to_raw_value};
use crate::{Ruma, client::invite_helper};
@@ -84,23 +81,15 @@ pub(crate) async fn create_room_route(
},
| None => services.server.config.default_room_version.clone(),
};
let room_version_rules = room_version.rules().unwrap();
let room_features = RoomVersion::new(&room_version)?;
let room_id: Option<OwnedRoomId> = match room_version_rules.room_id_format {
| RoomIdFormatVersion::V1 => {
// Check for custom room ID field
if let Some(CanonicalJsonValue::String(room_id)) =
body.json_body.as_ref().unwrap().get("room_id")
{
Some(
RoomId::parse(room_id)
.map_err(|_| err!(Request(BadJson("Malformed custom room ID"))))?,
)
} else {
Some(RoomId::new_v1(services.globals.server_name()))
}
},
| _ => None,
let room_id: Option<OwnedRoomId> = if !room_features.room_ids_as_hashes {
match &body.room_id {
| Some(custom_room_id) => Some(custom_room_id_check(&services, custom_room_id)?),
| None => Some(RoomId::new(services.globals.server_name())),
}
} else {
None
};
// check if room ID doesn't already exist instead of erroring on auth check
@@ -178,7 +167,7 @@ pub(crate) async fn create_room_route(
use RoomVersionId::*;
let mut content = content
.deserialize_as_unchecked::<CanonicalJsonObject>()
.deserialize_as::<CanonicalJsonObject>()
.map_err(|e| {
err!(Request(BadJson(error!(
"Failed to deserialise content as canonical JSON: {e}"
@@ -212,7 +201,8 @@ pub(crate) async fn create_room_route(
let content = match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(sender_user.to_owned()),
| _ => RoomCreateEventContent::new_v11(),
| V11 => RoomCreateEventContent::new_v11(),
| _ => RoomCreateEventContent::new_v12(),
};
let mut content =
serde_json::from_str::<CanonicalJsonObject>(to_raw_value(&content)?.get())?;
@@ -228,40 +218,27 @@ pub(crate) async fn create_room_route(
.short
.get_or_create_shortroomid(&room_id)
.await;
services.rooms.state.mutex.lock(room_id.as_str()).await
services.rooms.state.mutex.lock(&room_id).await
},
| None => {
let temp_room_id = RoomId::new_v1(services.globals.server_name());
let temp_room_id = RoomId::new(services.globals.server_name());
trace!("Locking temporary room state mutex for {temp_room_id}");
services.rooms.state.mutex.lock(temp_room_id.as_str()).await
services.rooms.state.mutex.lock(&temp_room_id).await
},
};
// 1. The room create event
debug!("Creating room create event for {sender_user} in room {room_id:?}");
let tmp_id = room_id.as_deref();
// Allow requesters to override the `origin_server_ts` to customize room ids
// from v12 onwards
let custom_origin_server_ts = body
.json_body
.as_ref()
.unwrap()
.get("origin_server_ts")
.and_then(CanonicalJsonValue::as_integer)
.map(Into::into)
.and_then(|value: i64| value.try_into().ok())
.map(MilliSecondsSinceUnixEpoch);
let create_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_content)?,
state_key: Some(StateKey::new()),
timestamp: custom_origin_server_ts,
timestamp: body.origin_server_ts,
..Default::default()
},
sender_user,
@@ -276,27 +253,34 @@ pub(crate) async fn create_room_route(
| None => {
let as_room_id = create_event_id.as_str().replace('$', "!");
trace!("Creating room with v12 room ID {as_room_id}");
RoomId::parse(&as_room_id)?.clone()
RoomId::parse(&as_room_id)?.to_owned()
},
};
drop(state_lock);
if let Some(expected_room_id) = body.room_id.as_ref() {
if expected_room_id.as_str() != room_id.as_str() {
return Err!(Request(InvalidParam(
"Custom room ID {expected_room_id} does not match the generated room ID \
{room_id}.",
)));
}
}
debug!("Room created with ID {room_id}");
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
// 2. Let the room creator join
let mut join_event = RoomMemberEventContent::new(MembershipState::Join);
join_event.displayname = services.users.displayname(sender_user).await.ok();
join_event.avatar_url = services.users.avatar_url(sender_user).await.ok();
join_event.blurhash = services.users.blurhash(sender_user).await.ok();
join_event.is_direct = Some(body.is_direct);
debug_info!("Joining {sender_user} to room {room_id}");
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(sender_user.to_string(), &join_event),
PduBuilder::state(sender_user.to_string(), &RoomMemberEventContent {
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
blurhash: services.users.blurhash(sender_user).await.ok(),
is_direct: Some(body.is_direct),
..RoomMemberEventContent::new(MembershipState::Join)
}),
sender_user,
Some(&room_id),
&state_lock,
@@ -322,10 +306,7 @@ pub(crate) async fn create_room_route(
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
// Do we care about additional_creators?
if room_version_rules
.authorization
.explicitly_privilege_room_creators
{
if room_features.explicitly_privilege_room_creators {
// Have they been specified?
if let Some(additional_creators) = create_content.get("additional_creators") {
// Are they a real array?
@@ -335,9 +316,9 @@ pub(crate) async fn create_room_route(
// Are they a string?
if let Some(creator) = creator.as_str() {
// Do they parse into a real user ID?
if let Ok(creator) = UserId::parse(creator) {
if let Ok(creator) = OwnedUserId::parse(creator) {
// Add them to the power levels and creators
creators.push(creator);
creators.push(creator.clone());
}
}
}
@@ -350,20 +331,17 @@ pub(crate) async fn create_room_route(
}
let power_levels_content = default_power_levels_content(
body.power_level_content_override
.as_ref()
.map(Raw::cast_ref),
body.power_level_content_override.as_ref(),
&body.visibility,
power_levels_to_grant,
creators,
&room_version_rules.authorization,
)?;
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content)?,
state_key: Some(StateKey::new()),
@@ -382,13 +360,10 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
String::new(),
&assign!(RoomCanonicalAliasEventContent::new(), {
alias: Some(room_alias_id.to_owned()),
alt_aliases: vec![],
}),
),
PduBuilder::state(String::new(), &RoomCanonicalAliasEventContent {
alias: Some(room_alias_id.to_owned()),
alt_aliases: vec![],
}),
sender_user,
Some(&room_id),
&state_lock,
@@ -404,7 +379,7 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
PduBuilder::state(
String::new(),
&RoomJoinRulesEventContent::new(match preset {
| RoomPreset::PublicChat => JoinRule::Public,
@@ -424,7 +399,7 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
PduBuilder::state(
String::new(),
&RoomHistoryVisibilityEventContent::new(HistoryVisibility::Shared),
),
@@ -440,7 +415,7 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
PduBuilder::state(
String::new(),
&RoomGuestAccessEventContent::new(match preset {
| RoomPreset::PublicChat => GuestAccess::Forbidden,
@@ -456,19 +431,26 @@ pub(crate) async fn create_room_route(
// 6. Events listed in initial_state
for event in &body.initial_state {
let mut partial_pdu = event
.deserialize_as_unchecked::<PartialPdu>()
.map_err(|e| {
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
})?;
let mut pdu_builder = event.deserialize_as::<PduBuilder>().map_err(|e| {
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
})?;
debug_info!("Room creation initial state event: {event:?}");
// client/appservice workaround: if a user sends an initial_state event with a
// state event in there with the content of literally `{}` (not null or empty
// string), let's just skip it over and warn.
if pdu_builder.content.get().eq("{}") {
debug_warn!("skipping empty initial state event with content of `{{}}`: {event:?}");
debug_warn!("content: {}", pdu_builder.content.get());
continue;
}
// Implicit state key defaults to ""
partial_pdu.state_key.get_or_insert_with(StateKey::new);
pdu_builder.state_key.get_or_insert_with(StateKey::new);
// Silently skip encryption events if they are not allowed
if partial_pdu.event_type == TimelineEventType::RoomEncryption
if pdu_builder.event_type == TimelineEventType::RoomEncryption
&& !services.config.allow_encryption
{
continue;
@@ -477,7 +459,7 @@ pub(crate) async fn create_room_route(
services
.rooms
.timeline
.build_and_append_pdu(partial_pdu, sender_user, Some(&room_id), &state_lock)
.build_and_append_pdu(pdu_builder, sender_user, Some(&room_id), &state_lock)
.boxed()
.await?;
}
@@ -488,7 +470,7 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(String::new(), &RoomNameEventContent::new(name.clone())),
PduBuilder::state(String::new(), &RoomNameEventContent::new(name.clone())),
sender_user,
Some(&room_id),
&state_lock,
@@ -502,7 +484,7 @@ pub(crate) async fn create_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(String::new(), &RoomTopicEventContent::new(topic.clone())),
PduBuilder::state(String::new(), &RoomTopicEventContent { topic: topic.clone() }),
sender_user,
Some(&room_id),
&state_lock,
@@ -557,13 +539,10 @@ fn default_power_levels_content(
visibility: &room::Visibility,
users: BTreeMap<OwnedUserId, Int>,
creators: Vec<OwnedUserId>,
authorization_rules: &AuthorizationRules,
) -> Result<serde_json::Value> {
let mut power_levels_content =
serde_json::to_value(assign!(RoomPowerLevelsEventContent::new(authorization_rules), {
users
}))
.unwrap();
serde_json::to_value(RoomPowerLevelsEventContent { users, ..Default::default() })
.expect("event is valid, we just created it");
// secure proper defaults of sensitive/dangerous permissions that moderators
// (power level 50) should not have easy access to
@@ -653,7 +632,7 @@ async fn room_alias_check(
}
let server_name = services.globals.server_name();
let full_room_alias = RoomAliasId::parse(format!("#{room_alias_name}:{server_name}"))
let full_room_alias = OwnedRoomAliasId::parse(format!("#{room_alias_name}:{server_name}"))
.map_err(|e| {
err!(Request(InvalidParam(debug_error!(
?e,
@@ -688,3 +667,60 @@ async fn room_alias_check(
Ok(full_room_alias)
}
/// if a room is being created with a custom room ID, run our checks against it
fn custom_room_id_check(services: &Services, custom_room_id: &str) -> Result<OwnedRoomId> {
// apply forbidden room alias checks to custom room IDs too
if services
.globals
.forbidden_alias_names()
.is_match(custom_room_id)
{
return Err!(Request(Unknown("Custom room ID is forbidden.")));
}
if custom_room_id.contains(':') {
return Err!(Request(InvalidParam(
"Custom room ID contained `:` which is not allowed. Please note that this expects a \
localpart, not the full room ID.",
)));
} else if custom_room_id.contains(char::is_whitespace) {
return Err!(Request(InvalidParam(
"Custom room ID contained spaces which is not valid."
)));
}
let server_name = services.globals.server_name();
let mut room_id = custom_room_id.to_owned();
if custom_room_id.contains(':') {
if !custom_room_id.starts_with('!') {
return Err!(Request(InvalidParam(
"Custom room ID contains an unexpected `:` which is not allowed.",
)));
}
} else if custom_room_id.starts_with('!') {
return Err!(Request(InvalidParam(
"Room ID is prefixed with !, but is not fully qualified. You likely did not want \
this.",
)));
} else {
room_id = format!("!{custom_room_id}:{server_name}");
}
OwnedRoomId::parse(room_id)
.map_err(Into::into)
.and_then(|full_room_id| {
if full_room_id
.server_name()
.expect("failed to extract server name from room ID")
!= server_name
{
Err!(Request(InvalidParam("Custom room ID must be on this server.",)))
} else {
Ok(full_room_id)
}
})
.inspect(|full_room_id| {
debug_info!(%full_room_id, "Full custom room ID");
})
.inspect_err(|e| warn!(?e, %custom_room_id, "Failed to create room with custom room ID",))
}
+1 -1
View File
@@ -44,5 +44,5 @@ pub(crate) async fn get_room_event_route(
event.set_unsigned(body.sender_user.as_deref());
Ok(get_room_event::v3::Response::new(event.into_format()))
Ok(get_room_event::v3::Response { event: event.into_format() })
}
+20 -21
View File
@@ -4,10 +4,7 @@ use conduwuit::{
utils::{BoolExt, stream::TryTools},
};
use futures::{FutureExt, TryStreamExt, future::try_join4};
use ruma::{
api::client::peeking::get_current_state::v3::{PaginationChunk, Request, Response},
assign,
};
use ruma::api::client::room::initial_sync::v3::{PaginationChunk, Request, Response};
use crate::Ruma;
@@ -72,27 +69,29 @@ pub(crate) async fn room_initial_sync_route(
.boxed()
.await?;
let end = events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string)
.unwrap_or_default();
let start = events.last().map(at!(0)).as_ref().map(ToString::to_string);
let messages = PaginationChunk {
start: events.last().map(at!(0)).as_ref().map(ToString::to_string),
let chunk = events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect();
end: events
.first()
.map(at!(0))
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
let messages = assign!(PaginationChunk::new(chunk, end), { start });
chunk: events
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
};
Ok(assign!(Response::new(room_id.to_owned()), {
account_data: vec![],
state: state,
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,
}))
})
}
+5 -2
View File
@@ -6,7 +6,10 @@ mod summary;
mod upgrade;
pub(crate) use self::{
aliases::get_room_aliases_route, create::create_room_route, event::get_room_event_route,
initial_sync::room_initial_sync_route, summary::get_room_summary,
aliases::get_room_aliases_route,
create::create_room_route,
event::get_room_event_route,
initial_sync::room_initial_sync_route,
summary::{get_room_summary, get_room_summary_legacy},
upgrade::upgrade_room_route,
};
+320 -17
View File
@@ -1,11 +1,48 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result};
use ruma::api::client::room::get_summary;
use service::rooms::summary::Accessibility;
use conduwuit::{
Err, Result, debug, debug_warn, info, trace,
utils::{IterStream, future::TryExtExt},
};
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join3},
stream::FuturesUnordered,
};
use ruma::{
OwnedServerName, RoomId, UserId,
api::{
client::room::get_summary,
federation::space::{SpaceHierarchyParentSummary, get_hierarchy},
},
events::room::member::MembershipState,
space::SpaceRoomJoinRule::{self, *},
};
use service::Services;
use crate::Ruma;
use crate::{Ruma, RumaResponse};
/// # `GET /_matrix/client/unstable/im.nheko.summary/rooms/{roomIdOrAlias}/summary`
///
/// Returns a short description of the state of a room.
///
/// This is the "wrong" endpoint that some implementations/clients may use
/// according to the MSC. Request and response bodies are the same as
/// `get_room_summary`.
///
/// An implementation of [MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)
pub(crate) async fn get_room_summary_legacy(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_summary::msc3266::Request>,
) -> Result<RumaResponse<get_summary::msc3266::Response>> {
get_room_summary(State(services), ClientIp(client), body)
.boxed()
.await
.map(RumaResponse)
}
/// # `GET /_matrix/client/unstable/im.nheko.summary/summary/{roomIdOrAlias}`
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
///
/// Returns a short description of the state of a room.
@@ -13,8 +50,8 @@ use crate::Ruma;
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_summary::v1::Request>,
) -> Result<get_summary::v1::Response> {
body: Ruma<get_summary::msc3266::Request>,
) -> Result<get_summary::msc3266::Response> {
let (room_id, servers) = services
.rooms
.alias
@@ -25,19 +62,285 @@ pub(crate) async fn get_room_summary(
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
}
let summary = services
.rooms
.summary
.get_room_summary_for_user(body.sender_user.as_deref(), &room_id, &servers)
.await?;
room_summary_response(&services, &room_id, &servers, body.sender_user.as_deref())
.boxed()
.await
}
match summary {
| Accessibility::Accessible(summary) => Ok(get_summary::v1::Response::new(summary)),
| Accessibility::Inaccessible => {
Err!(Request(Forbidden("You may not preview this room."), FORBIDDEN))
async fn room_summary_response(
services: &Services,
room_id: &RoomId,
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
if services
.rooms
.state_cache
.server_in_room(services.globals.server_name(), room_id)
.await
{
match local_room_summary_response(services, room_id, sender_user)
.boxed()
.await
{
| Ok(response) => return Ok(response),
| Err(e) => {
debug_warn!("Failed to get local room summary: {e:?}, falling back to remote");
},
}
}
let room =
remote_room_summary_hierarchy_response(services, room_id, servers, sender_user).await?;
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias: room.canonical_alias,
avatar_url: room.avatar_url,
guest_can_join: room.guest_can_join,
name: room.name,
num_joined_members: room.num_joined_members,
topic: room.topic,
world_readable: room.world_readable,
join_rule: room.join_rule,
room_type: room.room_type,
room_version: room.room_version,
encryption: room.encryption,
allowed_room_ids: room.allowed_room_ids,
membership: sender_user.is_some().then_some(MembershipState::Leave),
})
}
async fn local_room_summary_response(
services: &Services,
room_id: &RoomId,
sender_user: Option<&UserId>,
) -> Result<get_summary::msc3266::Response> {
trace!(
sender_user = sender_user.map(tracing::field::display),
"Sending local room summary response for {room_id:?}"
);
let (join_rule, world_readable, guest_can_join) = join3(
services.rooms.state_accessor.get_join_rules(room_id),
services.rooms.state_accessor.is_world_readable(room_id),
services.rooms.state_accessor.guest_can_join(room_id),
)
.await;
// Synapse allows server admins to bypass visibility checks.
// That seems neat so we'll copy that behaviour.
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
user_can_see_summary(
services,
room_id,
&join_rule.clone().into(),
guest_can_join,
world_readable,
join_rule.allowed_rooms(),
sender_user,
)
.await?;
}
let canonical_alias = services
.rooms
.state_accessor
.get_canonical_alias(room_id)
.ok();
let name = services.rooms.state_accessor.get_name(room_id).ok();
let topic = services.rooms.state_accessor.get_room_topic(room_id).ok();
let room_type = services.rooms.state_accessor.get_room_type(room_id).ok();
let avatar_url = services
.rooms
.state_accessor
.get_avatar(room_id)
.map(|res| res.into_option().unwrap_or_default().url);
let room_version = services.rooms.state.get_room_version(room_id).ok();
let encryption = services
.rooms
.state_accessor
.get_room_encryption(room_id)
.ok();
let num_joined_members = services
.rooms
.state_cache
.room_joined_count(room_id)
.unwrap_or(0);
let membership: OptionFuture<_> = sender_user
.map(|sender_user| {
services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.map_ok_or(MembershipState::Leave, |content| content.membership)
})
.into();
let (
canonical_alias,
name,
num_joined_members,
topic,
avatar_url,
room_type,
room_version,
encryption,
membership,
) = futures::join!(
canonical_alias,
name,
num_joined_members,
topic,
avatar_url,
room_type,
room_version,
encryption,
membership,
);
Ok(get_summary::msc3266::Response {
room_id: room_id.to_owned(),
canonical_alias,
avatar_url,
guest_can_join,
name,
num_joined_members: num_joined_members.try_into().unwrap_or_default(),
topic,
world_readable,
room_type,
room_version,
encryption,
membership,
allowed_room_ids: join_rule.allowed_rooms().map(Into::into).collect(),
join_rule: join_rule.into(),
})
}
/// used by MSC3266 to fetch a room's info if we do not know about it
async fn remote_room_summary_hierarchy_response(
services: &Services,
room_id: &RoomId,
servers: &[OwnedServerName],
sender_user: Option<&UserId>,
) -> Result<SpaceHierarchyParentSummary> {
trace!(sender_user = ?sender_user.map(tracing::field::display), ?servers, "Sending remote room summary response for {room_id:?}");
if !services.config.allow_federation {
return Err!(Request(Forbidden("Federation is disabled.")));
}
if services.rooms.metadata.is_disabled(room_id).await {
return Err!(Request(Forbidden(
"Federaton of room {room_id} is currently disabled on this server."
)));
}
if servers.is_empty() {
return Err!(Request(MissingParam(
"No servers were provided to fetch the room over federation"
)));
}
let request = get_hierarchy::v1::Request::new(room_id.to_owned());
let mut requests: FuturesUnordered<_> = servers
.iter()
.map(|server| {
info!("Fetching room summary for {room_id} from server {server}");
services
.sending
.send_federation_request(server, request.clone())
.inspect_ok(move |v| {
debug!("Fetched room summary for {room_id} from server {server}: {v:?}");
})
.inspect_err(move |e| {
info!("Failed to fetch room summary for {room_id} from server {server}: {e}");
})
})
.collect();
while let Some(Ok(response)) = requests.next().await {
trace!("{response:?}");
let room = response.room.clone();
if room.room_id != room_id {
debug_warn!(
"Room ID {} returned does not belong to the requested room ID {}",
room.room_id,
room_id
);
continue;
}
if sender_user.is_none() || !services.users.is_admin(sender_user.unwrap()).await {
return user_can_see_summary(
services,
room_id,
&room.join_rule,
room.guest_can_join,
room.world_readable,
room.allowed_room_ids.iter().map(AsRef::as_ref),
sender_user,
)
.await
.map(|()| room);
}
return Ok(room);
}
Err!(Request(NotFound("Room not found or is not accessible")))
}
async fn user_can_see_summary<'a, I>(
services: &Services,
room_id: &RoomId,
join_rule: &SpaceRoomJoinRule,
guest_can_join: bool,
world_readable: bool,
allowed_room_ids: I,
sender_user: Option<&UserId>,
) -> Result
where
I: Iterator<Item = &'a RoomId> + Send,
{
let is_public_room = matches!(join_rule, Public | Knock | KnockRestricted);
match sender_user {
| Some(sender_user) => {
let user_can_see_state_events = services
.rooms
.state_accessor
.user_can_see_state_events(sender_user, room_id);
let is_guest = services.users.is_deactivated(sender_user).unwrap_or(false);
let user_in_allowed_restricted_room = allowed_room_ids
.stream()
.any(|room| services.rooms.state_cache.is_joined(sender_user, room));
let (user_can_see_state_events, is_guest, user_in_allowed_restricted_room) =
join3(user_can_see_state_events, is_guest, user_in_allowed_restricted_room)
.boxed()
.await;
if user_can_see_state_events
|| (is_guest && guest_can_join)
|| is_public_room
|| user_in_allowed_restricted_room
{
return Ok(());
}
Err!(Request(Forbidden("Room is not accessible")))
},
| Accessibility::NotFound => {
Err!(Request(Forbidden("This room does not exist."), FORBIDDEN))
| None => {
if is_public_room || world_readable {
return Ok(());
}
Err!(Request(Forbidden("Room is not accessible")))
},
}
}
+91 -106
View File
@@ -2,18 +2,16 @@ use std::cmp::max;
use axum::extract::State;
use conduwuit::{
Err, Error, Event, Result, debug, err, info,
matrix::{StateKey, pdu::PartialPdu},
Err, Error, Event, Result, RoomVersion, debug, err, info,
matrix::{StateKey, pdu::PduBuilder},
};
use futures::{FutureExt, StreamExt};
use ruma::{
CanonicalJsonObject, RoomId, RoomVersionId,
api::{client::room::upgrade_room, error::ErrorKind},
assign,
api::client::{error::ErrorKind, room::upgrade_room},
events::{
StateEventType, TimelineEventType,
room::{
create::PreviousRoom,
member::{MembershipState, RoomMemberEventContent},
power_levels::RoomPowerLevelsEventContent,
tombstone::RoomTombstoneEventContent,
@@ -21,7 +19,6 @@ use ruma::{
space::child::{RedactedSpaceChildEventContent, SpaceChildEventContent},
},
int,
room_version_rules::RoomIdFormatVersion,
};
use serde_json::{json, value::to_raw_value};
@@ -79,7 +76,7 @@ pub(crate) async fn upgrade_room_route(
// First, check if the user has permission to upgrade the room (send tombstone
// event)
let old_room_state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// Check tombstone permission by attempting to create (but not send) the event
// Note that this does internally call the policy server with a fake room ID,
@@ -88,13 +85,10 @@ pub(crate) async fn upgrade_room_route(
.rooms
.timeline
.create_hash_and_sign_event(
PartialPdu::state(
StateKey::new(),
&RoomTombstoneEventContent::new(
String::new(),
RoomId::new_v1(services.globals.server_name()),
),
),
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: RoomId::new(services.globals.server_name()),
}),
sender_user,
Some(&body.room_id),
&old_room_state_lock,
@@ -108,19 +102,16 @@ pub(crate) async fn upgrade_room_route(
drop(old_room_state_lock);
// Create a replacement room
let room_version_rules = body
.new_version
.rules()
.expect("new room version should have defined rules");
let replacement_room_owned = if room_version_rules.room_id_format == RoomIdFormatVersion::V2 {
Some(RoomId::new_v1(services.globals.server_name()))
let room_features = RoomVersion::new(&body.new_version)?;
let replacement_room_owned = if !room_features.room_ids_as_hashes {
Some(RoomId::new(services.globals.server_name()))
} else {
None
};
let replacement_room: Option<&RoomId> = replacement_room_owned.as_ref().map(AsRef::as_ref);
let replacement_room_tmp = match replacement_room {
| Some(v) => v,
| None => &RoomId::new_v1(services.globals.server_name()),
| None => &RoomId::new(services.globals.server_name()),
};
let _short_id = services
@@ -130,21 +121,18 @@ pub(crate) async fn upgrade_room_route(
.await;
// For pre-v12 rooms, send tombstone before creating replacement room
let tombstone_event_id = if room_version_rules.room_id_format != RoomIdFormatVersion::V2 {
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
let tombstone_event_id = if !room_features.room_ids_as_hashes {
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// Send a m.room.tombstone event to the old room to indicate that it is not
// intended to be used any further
let tombstone_event_id = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
StateKey::new(),
&RoomTombstoneEventContent::new(
"This room has been replaced".to_owned(),
replacement_room.unwrap().to_owned(),
),
),
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.unwrap().to_owned(),
}),
sender_user,
Some(&body.room_id),
&state_lock,
@@ -157,12 +145,7 @@ pub(crate) async fn upgrade_room_route(
} else {
None
};
let state_lock = services
.rooms
.state
.mutex
.lock(replacement_room_tmp.as_str())
.await;
let state_lock = services.rooms.state.mutex.lock(replacement_room_tmp).await;
// Get the old room creation event
let mut create_event_content: CanonicalJsonObject = services
@@ -173,13 +156,10 @@ pub(crate) async fn upgrade_room_route(
.map_err(|_| err!(Database("Found room without m.room.create event.")))?;
// Use the m.room.tombstone event as the predecessor
let predecessor = {
#[allow(deprecated, reason = "Clients still use event_id even though it's deprecated")]
Some(assign!(PreviousRoom::new(body.room_id.clone()), {
event_id: tombstone_event_id,
}))
};
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
body.room_id.clone(),
tombstone_event_id,
));
// Send a m.room.create event containing a predecessor field and the applicable
// room_version
@@ -231,7 +211,7 @@ pub(crate) async fn upgrade_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_event_content)
.expect("event is valid, we just created it"),
@@ -247,35 +227,39 @@ pub(crate) async fn upgrade_room_route(
.boxed()
.await?;
let create_id = create_event_id.as_str().replace('$', "!");
let (replacement_room, state_lock) =
if room_version_rules.room_id_format == RoomIdFormatVersion::V2 {
let parsed_room_id = RoomId::parse(&create_id)?;
let lock = services
.rooms
.state
.mutex
.lock(parsed_room_id.as_str())
.await;
(Some(parsed_room_id), lock)
} else {
(replacement_room.map(ToOwned::to_owned), state_lock)
};
let (replacement_room, state_lock) = if room_features.room_ids_as_hashes {
let parsed_room_id = RoomId::parse(&create_id)?;
(Some(parsed_room_id), services.rooms.state.mutex.lock(parsed_room_id).await)
} else {
(replacement_room, state_lock)
};
// Join the new room
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
sender_user.as_str(),
&assign!(RoomMemberEventContent::new(MembershipState::Join), {
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services.users.displayname(sender_user).await.ok(),
avatar_url: services.users.avatar_url(sender_user).await.ok(),
is_direct: None,
third_party_invite: None,
blurhash: services.users.blurhash(sender_user).await.ok(),
}),
),
reason: None,
join_authorized_via_users_server: None,
redact_events: None,
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.as_str().into()),
redacts: None,
timestamp: None,
},
sender_user,
replacement_room.as_deref(),
replacement_room,
&state_lock,
)
.boxed()
@@ -322,14 +306,14 @@ pub(crate) async fn upgrade_room_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
event_type: event_type.to_string().into(),
content: event_content,
state_key: Some(StateKey::from(state_key)),
..Default::default()
},
sender_user,
replacement_room.as_deref(),
replacement_room,
&state_lock,
)
.boxed()
@@ -348,27 +332,27 @@ pub(crate) async fn upgrade_room_route(
services
.rooms
.alias
.remove_alias(&alias, sender_user)
.remove_alias(alias, sender_user)
.await?;
services.rooms.alias.set_alias(
&alias,
replacement_room.as_ref().unwrap(),
sender_user,
)?;
services
.rooms
.alias
.set_alias(alias, replacement_room.unwrap(), sender_user)?;
}
// Get the old room power levels
let mut power_levels = services
let power_levels_event_content: RoomPowerLevelsEventContent = services
.rooms
.state_accessor
.get_room_power_levels(&body.room_id)
.await;
.room_state_get_content(&body.room_id, &StateEventType::RoomPowerLevels, "")
.await
.map_err(|_| err!(Database("Found room without m.room.power_levels event.")))?;
// Setting events_default and invite to the greater of 50 and users_default + 1
let new_level = max(
int!(50),
power_levels
power_levels_event_content
.users_default
.checked_add(int!(1))
.ok_or_else(|| {
@@ -376,19 +360,17 @@ pub(crate) async fn upgrade_room_route(
})?,
);
power_levels.events_default = new_level;
power_levels.invite = new_level;
// Modify the power levels in the old room to prevent sending of events and
// inviting new users
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
StateKey::new(),
&RoomPowerLevelsEventContent::try_from(power_levels).unwrap(),
),
PduBuilder::state(StateKey::new(), &RoomPowerLevelsEventContent {
events_default: new_level,
invite: new_level,
..power_levels_event_content
}),
sender_user,
Some(&body.room_id),
&state_lock,
@@ -399,21 +381,18 @@ pub(crate) async fn upgrade_room_route(
drop(state_lock);
// For v12 rooms, send tombstone AFTER creating replacement room
if room_version_rules.room_id_format == RoomIdFormatVersion::V2 {
let old_room_state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
if room_features.room_ids_as_hashes {
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
// For v12 rooms, no event reference in predecessor due to cyclic dependency -
// could best effort one maybe?
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
StateKey::new(),
&RoomTombstoneEventContent::new(
"This room has been replaced".to_owned(),
replacement_room.as_ref().unwrap().to_owned(),
),
),
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.unwrap().to_owned(),
}),
sender_user,
Some(&body.room_id),
&old_room_state_lock,
@@ -436,7 +415,7 @@ pub(crate) async fn upgrade_room_route(
.rooms
.state_accessor
.room_state_get_content::<SpaceChildEventContent>(
&space_id,
space_id,
&StateEventType::SpaceChild,
body.room_id.as_str(),
)
@@ -448,24 +427,24 @@ pub(crate) async fn upgrade_room_route(
debug!(
"Updating space {space_id} child event for room {} to {}",
&body.room_id,
replacement_room.as_ref().unwrap()
replacement_room.unwrap()
);
// First, drop the space's child event
let state_lock = services.rooms.state.mutex.lock(space_id.as_str()).await;
let state_lock = services.rooms.state.mutex.lock(space_id).await;
debug!("Removing space child event for room {} in space {space_id}", &body.room_id);
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
event_type: StateEventType::SpaceChild.into(),
content: to_raw_value(&RedactedSpaceChildEventContent::new())
content: to_raw_value(&RedactedSpaceChildEventContent {})
.expect("event is valid, we just created it"),
state_key: Some(body.room_id.clone().as_str().into()),
..Default::default()
},
sender_user,
Some(&space_id),
Some(space_id),
&state_lock,
)
.boxed()
@@ -474,21 +453,25 @@ pub(crate) async fn upgrade_room_route(
// Now, add a new child event for the replacement room
debug!(
"Adding space child event for room {} in space {space_id}",
replacement_room.as_ref().unwrap()
replacement_room.unwrap()
);
services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(
replacement_room.as_ref().unwrap().as_str(),
&assign!(SpaceChildEventContent::new(vec![sender_user.server_name().to_owned()]), {
PduBuilder {
event_type: StateEventType::SpaceChild.into(),
content: to_raw_value(&SpaceChildEventContent {
via: vec![sender_user.server_name().to_owned()],
order: child.order,
suggested: child.suggested,
}),
),
})
.expect("event is valid, we just created it"),
state_key: Some(replacement_room.unwrap().as_str().into()),
..Default::default()
},
sender_user,
Some(&space_id),
Some(space_id),
&state_lock,
)
.boxed()
@@ -497,11 +480,13 @@ pub(crate) async fn upgrade_room_route(
debug!(
"Finished updating space {space_id} child event for room {} to {}",
&body.room_id,
replacement_room.as_ref().unwrap()
replacement_room.unwrap()
);
drop(state_lock);
}
// Return the replacement room id
Ok(upgrade_room::v3::Response::new(replacement_room.as_ref().unwrap().to_owned()))
Ok(upgrade_room::v3::Response {
replacement_room: replacement_room.unwrap().to_owned(),
})
}
+37 -26
View File
@@ -8,17 +8,13 @@ use conduwuit::{
utils::{IterStream, stream::ReadyExt},
};
use conduwuit_service::{Services, rooms::search::RoomQuery};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt, future::OptionFuture};
use ruma::{
OwnedRoomId, RoomId, UInt, UserId,
api::client::search::search_events::{
self,
v3::{
Criteria, EventContextResult, ResultCategories, ResultGroupMapsByGroupingKey,
ResultRoomEvents, SearchResult,
},
v3::{Criteria, EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
},
assign,
events::AnyStateEvent,
serde::Raw,
};
@@ -45,15 +41,20 @@ pub(crate) async fn search_events_route(
) -> Result<Response> {
let sender_user = body.sender_user();
let next_batch = body.next_batch.as_deref();
let room_events_result: OptionFuture<_> = body
.search_categories
.room_events
.as_ref()
.map(|criteria| category_room_events(&services, sender_user, next_batch, criteria))
.into();
let mut result_categories = ResultCategories::new();
if let Some(criteria) = &body.search_categories.room_events {
result_categories.room_events =
category_room_events(&services, sender_user, next_batch, criteria).await?;
}
Ok(Response::new(result_categories))
Ok(Response {
search_categories: ResultCategories {
room_events: Box::pin(room_events_result)
.await
.unwrap_or_else(|| Ok(ResultRoomEvents::default()))?,
},
})
}
#[allow(clippy::map_unwrap_or)]
@@ -84,7 +85,14 @@ async fn category_room_events(
.map(IntoIterator::into_iter)
.map(IterStream::stream)
.map(StreamExt::boxed)
.unwrap_or_else(|| services.rooms.state_cache.rooms_joined(sender_user).boxed());
.unwrap_or_else(|| {
services
.rooms
.state_cache
.rooms_joined(sender_user)
.map(ToOwned::to_owned)
.boxed()
});
let results: Vec<_> = rooms
.filter_map(|room_id| async move {
@@ -121,8 +129,7 @@ async fn category_room_events(
let total: UInt = results
.iter()
.fold(0, |a: usize, (_, count, _)| a.saturating_add(*count))
.try_into()
.expect("total results should fit into a UInt");
.try_into()?;
let state: RoomStates = results
.iter()
@@ -154,12 +161,16 @@ async fn category_room_events(
pdu
})
.map(Event::into_format)
.map(|result| {
assign!(SearchResult::new(), {
rank: None,
result: Some(result),
context: EventContextResult::default() // TODO
})
.map(|result| SearchResult {
rank: None,
result: Some(result),
context: EventContextResult {
profile_info: BTreeMap::new(), //TODO
events_after: Vec::new(), //TODO
events_before: Vec::new(), //TODO
start: None, //TODO
end: None, //TODO
},
})
.collect()
.await;
@@ -175,14 +186,14 @@ async fn category_room_events(
.as_ref()
.map(ToString::to_string);
Ok(assign!(ResultRoomEvents::new(), {
Ok(ResultRoomEvents {
count: Some(total),
next_batch,
results,
state,
highlights,
groups: ResultGroupMapsByGroupingKey::default(), // TODO
}))
groups: BTreeMap::new(), // TODO
})
}
async fn procure_room_state(services: &Services, room_id: &RoomId) -> Result<RoomState> {
+9 -9
View File
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu, utils};
use conduwuit::{Err, Result, err, matrix::pdu::PduBuilder, utils};
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
use serde_json::from_str;
@@ -40,7 +40,7 @@ pub(crate) async fn send_message_event_route(
return Err!(Request(Forbidden("Encryption has been disabled")));
}
let state_lock = services.rooms.state.mutex.lock(body.room_id.as_str()).await;
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
if body.event_type == MessageLikeEventType::CallInvite
&& services.rooms.directory.is_public_room(&body.room_id).await
@@ -62,11 +62,11 @@ pub(crate) async fn send_message_event_route(
)));
}
let event_id = utils::string_from_bytes(&response)
.map(TryInto::try_into)
.map_err(|e| err!(Database("Invalid event_id in txnid data: {e:?}")))??;
return Ok(send_message_event::v3::Response::new(event_id));
return Ok(send_message_event::v3::Response {
event_id: utils::string_from_bytes(&response)
.map(TryInto::try_into)
.map_err(|e| err!(Database("Invalid event_id in txnid data: {e:?}")))??,
});
}
let mut unsigned = BTreeMap::new();
@@ -79,7 +79,7 @@ pub(crate) async fn send_message_event_route(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
event_type: body.event_type.clone().into(),
content,
unsigned: Some(unsigned),
@@ -101,5 +101,5 @@ pub(crate) async fn send_message_event_route(
drop(state_lock);
Ok(send_message_event::v3::Response::new(event_id))
Ok(send_message_event::v3::Response { event_id })
}
+111 -23
View File
@@ -3,17 +3,18 @@ use std::time::Duration;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug, err, info,
Err, Error, Result, debug, err, info,
utils::{self, ReadyExt, hash, stream::BroadbandExt},
warn,
};
use conduwuit_core::debug_error;
use conduwuit_core::{debug_error, debug_warn};
use conduwuit_service::Services;
use futures::StreamExt;
use lettre::Address;
use ruma::{
OwnedUserId, UserId,
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
@@ -26,9 +27,8 @@ use ruma::{
},
logout, logout_all,
},
uiaa::{EmailUserIdentifier, MatrixUserIdentifier, UserIdentifier},
uiaa::UserIdentifier,
},
assign,
};
use service::uiaa::Identity;
@@ -48,9 +48,9 @@ pub(crate) async fn get_login_types_route(
Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
get_login_types::v3::LoginType::Token(assign!(TokenLoginType::new(), {
get_login_types::v3::LoginType::Token(TokenLoginType {
get_login_token: services.server.config.login_via_existing_session,
})),
}),
]))
}
@@ -64,6 +64,17 @@ pub(crate) async fn password_login(
lowercased_user_id: &UserId,
password: &str,
) -> Result<OwnedUserId> {
// Restrict login to accounts only of type 'password', including untyped
// legacy accounts which are equivalent to 'password'.
if services
.users
.origin(user_id)
.await
.is_ok_and(|origin| origin != "password")
{
return Err!(Request(Forbidden("Account does not permit password login.")));
}
let (hash, user_id) = match services.users.password_hash(user_id).await {
| Ok(hash) => (hash, user_id),
| Err(_) => services
@@ -85,6 +96,71 @@ pub(crate) async fn password_login(
Ok(user_id.to_owned())
}
/// Authenticates the given user through the configured LDAP server.
///
/// Creates the user if the user is found in the LDAP and do not already have an
/// account.
#[tracing::instrument(skip_all, fields(%user_id), name = "ldap", level = "debug")]
pub(super) async fn ldap_login(
services: &Services,
user_id: &UserId,
lowercased_user_id: &UserId,
password: &str,
) -> Result<OwnedUserId> {
let (user_dn, is_ldap_admin) = match services.config.ldap.bind_dn.as_ref() {
| Some(bind_dn) if bind_dn.contains("{username}") =>
(bind_dn.replace("{username}", lowercased_user_id.localpart()), None),
| _ => {
debug!("Searching user in LDAP");
let dns = services.users.search_ldap(user_id).await?;
if dns.len() >= 2 {
return Err!(Ldap("LDAP search returned two or more results"));
}
let Some((user_dn, is_admin)) = dns.first() else {
return password_login(services, user_id, lowercased_user_id, password).await;
};
(user_dn.clone(), *is_admin)
},
};
let user_id = services
.users
.auth_ldap(&user_dn, password)
.await
.map(|()| lowercased_user_id.to_owned())?;
// LDAP users are automatically created on first login attempt. This is a very
// common feature that can be seen on many services using a LDAP provider for
// their users (synapse, Nextcloud, Jellyfin, ...).
//
// LDAP users are crated with a dummy password but non empty because an empty
// password is reserved for deactivated accounts. The conduwuit password field
// will never be read to login a LDAP user so it's not an issue.
if !services.users.exists(lowercased_user_id).await {
services
.users
.create(lowercased_user_id, Some("*"), Some("ldap"))
.await?;
}
// Only sync admin status if LDAP can actually determine it.
// None means LDAP cannot determine admin status (manual config required).
if let Some(is_ldap_admin) = is_ldap_admin {
let is_conduwuit_admin = services.admin.user_is_admin(lowercased_user_id).await;
if is_ldap_admin && !is_conduwuit_admin {
Box::pin(services.admin.make_user_admin(lowercased_user_id)).await?;
} else if !is_ldap_admin && is_conduwuit_admin {
Box::pin(services.admin.revoke_admin(lowercased_user_id)).await?;
}
}
Ok(user_id)
}
pub(crate) async fn handle_login(
services: &Services,
identifier: Option<&UserIdentifier>,
@@ -92,11 +168,9 @@ pub(crate) async fn handle_login(
user: Option<&String>,
) -> Result<OwnedUserId> {
debug!("Got password login type");
let user_id_or_localpart = match (identifier, user) {
| (Some(UserIdentifier::Matrix(MatrixUserIdentifier { user, .. })), _)
| (None, Some(user)) => user,
| (Some(UserIdentifier::Email(EmailUserIdentifier { address, .. })), _) => {
| (Some(UserIdentifier::UserIdOrLocalpart(localpart)), _) => localpart,
| (Some(UserIdentifier::Email { address }), _) => {
let email = Address::try_from(address.to_owned())
.map_err(|_| err!(Request(InvalidParam("Email is malformed"))))?;
@@ -106,6 +180,7 @@ pub(crate) async fn handle_login(
.await
.ok_or_else(|| err!(Request(Forbidden("Invalid identifier or password"))))?
},
| (None, Some(user)) => user,
| _ => {
return Err!(Request(InvalidParam("Identifier type not recognized")));
},
@@ -124,11 +199,11 @@ pub(crate) async fn handle_login(
if !services.globals.user_is_local(&user_id)
|| !services.globals.user_is_local(&lowercased_user_id)
{
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
}
if services.users.is_locked(&user_id).await? {
return Err!(Request(UserLocked("This account has been locked.")));
return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
}
if services.users.is_login_disabled(&user_id).await {
@@ -136,7 +211,18 @@ pub(crate) async fn handle_login(
return Err!(Request(Forbidden("This account is not permitted to log in.")));
}
password_login(services, &user_id, &lowercased_user_id, password).await
if cfg!(feature = "ldap") && services.config.ldap.enable {
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
| Ok(user_id) => Ok(user_id),
| Err(err) if services.config.ldap.ldap_only => Err(err),
| Err(err) => {
debug_warn!("{err}");
password_login(services, &user_id, &lowercased_user_id, password).await
},
}
} else {
password_login(services, &user_id, &lowercased_user_id, password).await
}
}
/// # `POST /_matrix/client/v3/login`
@@ -171,7 +257,7 @@ pub(crate) async fn login_route(
user,
..
}) => handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
| login::v3::LoginInfo::Token(login::v3::Token { token, .. }) => {
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
debug!("Got token login type");
if !services.server.config.login_via_existing_session {
return Err!(Request(Unknown("Token login is not enabled.")));
@@ -182,7 +268,6 @@ pub(crate) async fn login_route(
| login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
identifier,
user,
..
}) => {
debug!("Got appservice login type");
@@ -191,8 +276,8 @@ pub(crate) async fn login_route(
};
let user_id =
if let Some(UserIdentifier::Matrix(MatrixUserIdentifier { user, .. })) = identifier {
UserId::parse_with_server_name(user, &services.config.server_name)
if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
UserId::parse_with_server_name(user_id, &services.config.server_name)
} else if let Some(user) = user {
UserId::parse_with_server_name(user, &services.config.server_name)
} else {
@@ -270,12 +355,15 @@ pub(crate) async fn login_route(
info!("{user_id} logged in");
#[allow(deprecated)]
Ok(assign!(login::v3::Response::new(user_id, token, device_id), {
Ok(login::v3::Response {
user_id,
access_token: token,
device_id,
well_known: client_discovery_info,
expires_in: None,
home_server: Some(services.config.server_name.clone()),
refresh_token: None,
}))
})
}
/// # `POST /_matrix/client/v1/login/get_token`
@@ -305,10 +393,10 @@ pub(crate) async fn login_token_route(
let login_token = utils::random_string(TOKEN_LENGTH);
let expires_in = services.users.create_login_token(sender_user, &login_token);
Ok(get_login_token::v1::Response::new(
Duration::from_millis(expires_in),
Ok(get_login_token::v1::Response {
expires_in: Duration::from_millis(expires_in),
login_token,
))
})
}
/// # `POST /_matrix/client/v3/logout`
@@ -376,7 +464,7 @@ pub(crate) async fn logout_all_route(
services
.users
.all_device_ids(sender_user)
.for_each(async |device_id| services.users.remove_device(sender_user, &device_id).await)
.for_each(|device_id| services.users.remove_device(sender_user, device_id))
.await;
services
.pusher
+176 -28
View File
@@ -1,12 +1,26 @@
use std::{
collections::{BTreeSet, VecDeque},
str::FromStr,
};
use axum::extract::State;
use conduwuit::{Err, Result};
use ruma::{UInt, api::client::space::get_hierarchy, assign};
use service::rooms::summary::Accessibility;
use conduwuit::{
Err, Result,
utils::{future::TryExtExt, stream::IterStream},
};
use conduwuit_service::{
Services,
rooms::spaces::{
PaginationToken, SummaryAccessibility, get_parent_children_via, summary_to_chunk,
},
};
use futures::{StreamExt, TryFutureExt, future::OptionFuture};
use ruma::{
OwnedRoomId, OwnedServerName, RoomId, UInt, UserId, api::client::space::get_hierarchy,
};
use crate::Ruma;
const MAX_MAX_DEPTH: u32 = 10;
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy`
///
/// Paginates over the space tree in a depth-first manner to locate child rooms
@@ -15,33 +29,167 @@ pub(crate) async fn get_hierarchy_route(
State(services): State<crate::State>,
body: Ruma<get_hierarchy::v1::Request>,
) -> Result<get_hierarchy::v1::Response> {
// We don't do pagination for this route (and therefore ignore `limit`), since
// there's no reasonable way to handle a space hierarchy changing during
// pagination.
let limit = body
.limit
.unwrap_or_else(|| UInt::from(10_u32))
.min(UInt::from(100_u32));
let max_depth = body
.max_depth
.map(|max_depth| max_depth.min(UInt::from(MAX_MAX_DEPTH)));
.unwrap_or_else(|| UInt::from(3_u32))
.min(UInt::from(10_u32));
let hierarchy = services
.rooms
.summary
.get_room_hierarchy_for_user(
body.sender_user(),
body.room_id.clone(),
max_depth,
body.suggested_only,
)
.await?;
let key = body
.from
.as_ref()
.and_then(|s| PaginationToken::from_str(s).ok());
match hierarchy {
| Accessibility::Accessible(rooms) =>
Ok(assign!(get_hierarchy::v1::Response::new(), { rooms: rooms })),
| Accessibility::Inaccessible => {
Err!(Request(Forbidden("You may not preview this room."), FORBIDDEN))
},
| Accessibility::NotFound => {
Err!(Request(Forbidden("This room does not exist."), FORBIDDEN))
},
// Should prevent unexpected behaviour in (bad) clients
if let Some(ref token) = key {
if token.suggested_only != body.suggested_only || token.max_depth != max_depth {
return Err!(Request(InvalidParam(
"suggested_only and max_depth cannot change on paginated requests"
)));
}
}
get_client_hierarchy(
&services,
body.sender_user(),
&body.room_id,
limit.try_into().unwrap_or(10),
max_depth.try_into().unwrap_or(usize::MAX),
body.suggested_only,
key.as_ref()
.into_iter()
.flat_map(|t| t.short_room_ids.iter()),
)
.await
}
async fn get_client_hierarchy<'a, ShortRoomIds>(
services: &Services,
sender_user: &UserId,
room_id: &RoomId,
limit: usize,
max_depth: usize,
suggested_only: bool,
short_room_ids: ShortRoomIds,
) -> Result<get_hierarchy::v1::Response>
where
ShortRoomIds: Iterator<Item = &'a u64> + Clone + Send + Sync + 'a,
{
type Via = Vec<OwnedServerName>;
type Entry = (OwnedRoomId, Via);
type Rooms = VecDeque<Entry>;
let mut queue: Rooms = [(
room_id.to_owned(),
room_id
.server_name()
.map(ToOwned::to_owned)
.into_iter()
.collect(),
)]
.into();
let mut rooms = Vec::with_capacity(limit);
let mut parents = BTreeSet::new();
while let Some((current_room, via)) = queue.pop_front() {
let summary = services
.rooms
.spaces
.get_summary_and_children_client(&current_room, suggested_only, sender_user, &via)
.await?;
match (summary, current_room == room_id) {
| (None | Some(SummaryAccessibility::Inaccessible), false) => {
// Just ignore other unavailable rooms
},
| (None, true) => {
return Err!(Request(Forbidden("The requested room was not found")));
},
| (Some(SummaryAccessibility::Inaccessible), true) => {
return Err!(Request(Forbidden("The requested room is inaccessible")));
},
| (Some(SummaryAccessibility::Accessible(summary)), _) => {
let populate = parents.len() >= short_room_ids.clone().count();
let mut children: Vec<Entry> = get_parent_children_via(&summary, suggested_only)
.filter(|(room, _)| !parents.contains(room))
.rev()
.map(|(key, val)| (key, val.collect()))
.collect();
if populate {
rooms.push(summary_to_chunk(summary.clone()));
} else {
children = children
.iter()
.rev()
.stream()
.skip_while(|(room, _)| {
services
.rooms
.short
.get_shortroomid(room)
.map_ok(|short| {
Some(&short) != short_room_ids.clone().nth(parents.len())
})
.unwrap_or_else(|_| false)
})
.map(Clone::clone)
.collect::<Vec<Entry>>()
.await
.into_iter()
.rev()
.collect();
}
if !populate && queue.is_empty() && children.is_empty() {
break;
}
parents.insert(current_room.clone());
if rooms.len() >= limit {
break;
}
if parents.len() > max_depth {
continue;
}
queue.extend(children);
},
}
}
let next_batch: OptionFuture<_> = queue
.pop_front()
.map(|(room, _)| async move {
parents.insert(room);
let next_short_room_ids: Vec<_> = parents
.iter()
.stream()
.filter_map(|room_id| services.rooms.short.get_shortroomid(room_id).ok())
.collect()
.await;
(next_short_room_ids.iter().ne(short_room_ids) && !next_short_room_ids.is_empty())
.then_some(PaginationToken {
short_room_ids: next_short_room_ids,
limit: limit.try_into().ok()?,
max_depth: max_depth.try_into().ok()?,
suggested_only,
})
.as_ref()
.map(PaginationToken::to_string)
})
.into();
Ok(get_hierarchy::v1::Response {
next_batch: next_batch.await.flatten(),
rooms,
})
}
+122 -129
View File
@@ -4,16 +4,14 @@ use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err,
matrix::{Event, pdu::PartialPdu},
matrix::{Event, pdu::PduBuilder},
utils::BoolExt,
};
use conduwuit_service::Services;
use futures::{FutureExt, TryStreamExt};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
api::client::state::{
get_state_event_for_key::{self, v3::StateEventFormat},
get_state_events, send_state_event,
},
api::client::state::{get_state_events, get_state_events_for_key, send_state_event},
events::{
AnyStateEventContent, StateEventType,
room::{
@@ -26,7 +24,7 @@ use ruma::{
},
serde::Raw,
};
use serde_json::{json, value::to_raw_value};
use serde_json::json;
use crate::{Ruma, RumaResponse};
@@ -48,23 +46,23 @@ pub(crate) async fn send_state_event_for_key_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let event_id = send_state_event_for_key_helper(
&services,
sender_user,
&body.room_id,
&body.event_type,
&body.body.body,
&body.state_key,
if body.appservice_info.is_some() {
body.timestamp
} else {
None
},
)
.boxed()
.await?;
Ok(send_state_event::v3::Response::new(event_id))
Ok(send_state_event::v3::Response {
event_id: send_state_event_for_key_helper(
&services,
sender_user,
&body.room_id,
&body.event_type,
&body.body.body,
&body.state_key,
if body.appservice_info.is_some() {
body.timestamp
} else {
None
},
)
.boxed()
.await?,
})
}
/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}`
@@ -102,15 +100,15 @@ pub(crate) async fn get_state_events_route(
return Err!(Request(Forbidden("You don't have permission to view the room state.")));
}
let room_state = services
.rooms
.state_accessor
.room_state_full_pdus(&body.room_id)
.map_ok(Event::into_format)
.try_collect()
.await?;
Ok(get_state_events::v3::Response::new(room_state))
Ok(get_state_events::v3::Response {
room_state: services
.rooms
.state_accessor
.room_state_full_pdus(&body.room_id)
.map_ok(Event::into_format)
.try_collect()
.await?,
})
}
/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}/{stateKey}`
@@ -121,10 +119,10 @@ pub(crate) async fn get_state_events_route(
///
/// - If not joined: Only works if current room history visibility is world
/// readable
pub(crate) async fn get_state_event_for_key_route(
pub(crate) async fn get_state_events_for_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<get_state_event_for_key::v3::Response> {
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<get_state_events_for_key::v3::Response> {
let sender_user = body.sender_user();
if !services
@@ -151,23 +149,26 @@ pub(crate) async fn get_state_event_for_key_route(
))))
})?;
let content = match body.format {
| StateEventFormat::Content => event.content,
| StateEventFormat::Event => to_raw_value(&json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id_or_hash(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
"unsigned": event.unsigned(),
}))
.unwrap(),
| _ => return Err!(Request(InvalidParam("Unknown response format"))),
};
let event_format = body
.format
.as_ref()
.is_some_and(|f| f.to_lowercase().eq("event"));
Ok(get_state_event_for_key::v3::Response::new(content))
Ok(get_state_events_for_key::v3::Response {
content: event_format.or(|| event.get_content_as_value()),
event: event_format.then(|| {
json!({
"content": event.content(),
"event_id": event.event_id(),
"origin_server_ts": event.origin_server_ts(),
"room_id": event.room_id_or_hash(),
"sender": event.sender(),
"state_key": event.state_key(),
"type": event.kind(),
"unsigned": event.unsigned(),
})
}),
})
}
/// # `GET /_matrix/client/v3/rooms/{roomid}/state/{eventType}`
@@ -180,9 +181,9 @@ pub(crate) async fn get_state_event_for_key_route(
/// readable
pub(crate) async fn get_state_events_for_empty_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_event_for_key::v3::Response>> {
get_state_event_for_key_route(State(services), body)
body: Ruma<get_state_events_for_key::v3::Request>,
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
get_state_events_for_key_route(State(services), body)
.await
.map(RumaResponse)
}
@@ -203,7 +204,7 @@ async fn send_state_event_for_key_helper(
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
PduBuilder {
event_type: event_type.to_string().into(),
content: serde_json::from_str(json.json().get())?,
state_key: Some(state_key.into()),
@@ -236,16 +237,9 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomServerAcl => {
// prevents common ACL paw-guns as ACL management is difficult and prone to
// irreversible mistakes
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
match json.deserialize_as::<RoomServerAclEventContent>() {
| Ok(acl_content) => {
let allow_has_wildcard = acl_content.allow.iter().any(|entry| entry == "*");
let deny_has_wildcard = acl_content.deny.iter().any(|entry| entry == "*");
let allow_has_server = acl_content
.allow
.iter()
.any(|entry| entry == services.globals.server_name().as_str());
if acl_content.allow.is_empty() {
if acl_content.allow_is_empty() {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with an empty allow key will permanently \
@@ -254,7 +248,7 @@ async fn allowed_to_send_state_event(
))));
}
if allow_has_wildcard && deny_has_wildcard {
if acl_content.deny_contains("*") && acl_content.allow_contains("*") {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with a deny and allow key value of \"*\" will \
@@ -263,9 +257,9 @@ async fn allowed_to_send_state_event(
))));
}
if deny_has_wildcard
if acl_content.deny_contains("*")
&& !acl_content.is_allowed(services.globals.server_name())
&& !allow_has_server
&& !acl_content.allow_contains(services.globals.server_name().as_str())
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
@@ -275,9 +269,9 @@ async fn allowed_to_send_state_event(
))));
}
if !allow_has_wildcard
if !acl_content.allow_contains("*")
&& !acl_content.is_allowed(services.globals.server_name())
&& !allow_has_server
&& !acl_content.allow_contains(services.globals.server_name().as_str())
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
@@ -303,7 +297,7 @@ async fn allowed_to_send_state_event(
// admin room is a sensitive room, it should not ever be made public
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id {
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
match json.deserialize_as::<RoomJoinRulesEventContent>() {
| Ok(join_rule) =>
if join_rule.join_rule == JoinRule::Public {
return Err!(Request(Forbidden(
@@ -322,7 +316,7 @@ async fn allowed_to_send_state_event(
| StateEventType::RoomHistoryVisibility => {
// admin room is a sensitive room, it should not ever be made world readable
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
match json.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>() {
match json.deserialize_as::<RoomHistoryVisibilityEventContent>() {
| Ok(visibility_content) => {
if admin_room_id == room_id
&& visibility_content.history_visibility
@@ -343,7 +337,7 @@ async fn allowed_to_send_state_event(
}
},
| StateEventType::RoomCanonicalAlias => {
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
match json.deserialize_as::<RoomCanonicalAliasEventContent>() {
| Ok(canonical_alias_content) => {
let mut aliases = canonical_alias_content.alt_aliases.clone();
@@ -375,66 +369,65 @@ async fn allowed_to_send_state_event(
},
}
},
| StateEventType::RoomMember =>
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
| Ok(mut membership_content) => {
let Ok(state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
// join_authorized_via_users_server must be thrown away, if user is
// already a member of the room.
if services
.rooms
.state_cache
.is_joined(&state_key, room_id)
.await
{
membership_content.join_authorized_via_users_server = None;
*json = Raw::<AnyStateEventContent>::from_json_string(
serde_json::to_string(&membership_content)?,
)?;
return Ok(());
}
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
if !services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.await
{
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room, they \
cannot authorise the join."
)));
}
}
},
| Err(e) => {
| StateEventType::RoomMember => match json.deserialize_as::<RoomMemberEventContent>() {
| Ok(mut membership_content) => {
let Ok(state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
"Membership event has invalid or non-existent state key"
)));
},
};
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
// join_authorized_via_users_server must be thrown away, if user is already a
// member of the room.
if services
.rooms
.state_cache
.is_joined(state_key, room_id)
.await
{
membership_content.join_authorized_via_users_server = None;
*json = Raw::<AnyStateEventContent>::from_json_string(
serde_json::to_string(&membership_content)?,
)?;
return Ok(());
}
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
if !services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.await
{
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room, they \
cannot authorise the join."
)));
}
}
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| _ => (),
}
+1 -1
View File
@@ -14,7 +14,7 @@ fn test_strip_room_member() -> Result<()> {
let json: &mut Raw<AnyStateEventContent> =
&mut Raw::<AnyStateEventContent>::from_json_string(body.to_owned())?;
let mut membership_content: RoomMemberEventContent =
json.deserialize_as_unchecked::<RoomMemberEventContent>()?;
json.deserialize_as::<RoomMemberEventContent>()?;
//Begin Test
membership_content.join_authorized_via_users_server = None;
+2 -1
View File
@@ -152,7 +152,8 @@ async fn share_encrypted_room(
.rooms
.state_cache
.get_shared_rooms(sender_user, user_id)
.ready_filter(|room_id| Some(room_id.as_ref()) != ignore_room)
.ready_filter(|&room_id| Some(room_id) != ignore_room)
.map(ToOwned::to_owned)
.broad_any(|other_room_id| async move {
services
.rooms
+27 -20
View File
@@ -23,21 +23,17 @@ use ruma::{
OwnedRoomId, OwnedUserId, RoomId, UserId,
api::client::sync::sync_events::{
UnreadNotificationsCount,
v3::{
Ephemeral, JoinedRoom, RoomAccountData, RoomSummary, State as RoomState, StateEvents,
Timeline,
},
v3::{Ephemeral, JoinedRoom, RoomAccountData, RoomSummary, State as RoomState, Timeline},
},
assign,
events::{
StateEventType,
AnyRawAccountDataEvent, StateEventType,
TimelineEventType::*,
room::member::{MembershipState, RoomMemberEventContent},
},
serde::Raw,
uint,
};
use service::{account_data::AnyRawAccountDataEvent, rooms::short::ShortStateHash};
use service::rooms::short::ShortStateHash;
use super::{load_timeline, share_encrypted_room};
use crate::client::{
@@ -96,15 +92,17 @@ pub(super) async fn load_joined_room(
);
}
let joined_room = assign!(JoinedRoom::new(), {
let joined_room = JoinedRoom {
account_data,
summary: summary.unwrap_or_default(),
unread_notifications: notification_counts.unwrap_or_default(),
timeline,
state: RoomState::Before(StateEvents::with_events(state_events.into_iter().map(Event::into_format).collect())),
state: RoomState {
events: state_events.into_iter().map(Event::into_format).collect(),
},
ephemeral,
unread_thread_notifications: BTreeMap::new(),
});
};
Ok((joined_room, device_list_updates))
}
@@ -128,7 +126,7 @@ async fn build_account_data(
.collect()
.await;
Ok(assign!(RoomAccountData::new(), { events: account_data_changes }))
Ok(RoomAccountData { events: account_data_changes })
}
/// Collect new ephemeral events.
@@ -235,7 +233,7 @@ async fn build_ephemeral(
edus.extend(typing_event);
edus.extend(private_read_event);
Ok(assign!(Ephemeral::new(), { events: edus }))
Ok(Ephemeral { events: edus })
}
/// A struct to hold the state events, timeline, and other data which is
@@ -320,11 +318,11 @@ async fn build_state_and_timeline(
Ok(StateAndTimeline {
state_events,
timeline: assign!(Timeline::new(), {
timeline: Timeline {
limited,
prev_batch: prev_batch.as_ref().map(ToString::to_string),
events: filtered_timeline,
}),
},
summary,
notification_counts,
device_list_updates,
@@ -582,10 +580,10 @@ async fn build_notification_counts(
trace!(%notification_count, %highlight_count, "syncing new notification counts");
Ok(Some(assign!(UnreadNotificationsCount::new(), {
Ok(Some(UnreadNotificationsCount {
notification_count: Some(notification_count),
highlight_count: Some(highlight_count),
})))
}))
} else {
Ok(None)
}
@@ -700,13 +698,13 @@ async fn build_room_summary(
"syncing updated summary"
);
Ok(Some(assign!(RoomSummary::new(), {
Ok(Some(RoomSummary {
heroes: heroes
.map(|heroes| heroes.into_iter().collect())
.unwrap_or_default(),
joined_member_count: Some(ruma_from_u64(joined_member_count)),
invited_member_count: Some(ruma_from_u64(invited_member_count)),
})))
}))
}
/// Fetch the user IDs to include in the `m.heroes` property of the room
@@ -720,10 +718,18 @@ async fn build_heroes(
const MAX_HERO_COUNT: usize = 5;
// fetch joined members from the state cache first
let joined_members_stream = services.rooms.state_cache.room_members(room_id);
let joined_members_stream = services
.rooms
.state_cache
.room_members(room_id)
.map(ToOwned::to_owned);
// then fetch invited members
let invited_members_stream = services.rooms.state_cache.room_members_invited(room_id);
let invited_members_stream = services
.rooms
.state_cache
.room_members_invited(room_id)
.map(ToOwned::to_owned);
// then as a last resort fetch every membership event
let all_members_stream = services
@@ -790,6 +796,7 @@ async fn build_device_list_updates(
.users
.room_keys_changed(room_id, last_sync_end_count, Some(current_count))
.map(at!(0))
.map(ToOwned::to_owned)
.ready_for_each(|user_id| {
device_list_updates.changed.insert(user_id);
})
+10 -11
View File
@@ -7,10 +7,7 @@ use conduwuit::{
use futures::{StreamExt, future::join};
use ruma::{
EventId, OwnedRoomId, RoomId,
api::client::sync::sync_events::v3::{
LeftRoom, RoomAccountData, State, StateEvents, Timeline,
},
assign,
api::client::sync::sync_events::v3::{LeftRoom, RoomAccountData, State, Timeline},
events::{StateEventType, TimelineEventType},
uint,
};
@@ -181,15 +178,17 @@ pub(super) async fn load_left_room(
.collect::<Vec<_>>()
.await;
Ok(Some(assign!(LeftRoom::new(), {
account_data: RoomAccountData::new(),
timeline: assign!(Timeline::new(), {
Ok(Some(LeftRoom {
account_data: RoomAccountData { events: Vec::new() },
timeline: Timeline {
limited: timeline.limited,
prev_batch: Some(current_count.to_string()),
events: raw_timeline_pdus,
}),
state: State::Before(StateEvents::with_events(state_events.into_iter().map(Event::into_format).collect())),
})))
},
state: State {
events: state_events.into_iter().map(Event::into_format).collect(),
},
}))
}
async fn build_left_state_and_timeline(
@@ -318,7 +317,7 @@ fn create_dummy_leave_event(
// clients. perhaps a database table could be created to hold these dummy
// events, or they could be stored as outliers?
PduEvent {
event_id: EventId::new_v1(services.globals.server_name()),
event_id: EventId::new(services.globals.server_name()),
sender: syncing_user.to_owned(),
origin: None,
origin_server_ts: utils::millis_since_unix_epoch()
+54 -71
View File
@@ -11,7 +11,7 @@ use std::{
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, at, extract_variant,
Result, at, extract_variant,
utils::{
ReadyExt, TryFutureExtExt,
stream::{BroadbandExt, Tools, WidebandExt},
@@ -19,7 +19,10 @@ use conduwuit::{
warn,
};
use conduwuit_service::Services;
use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture};
use futures::{
FutureExt, StreamExt, TryFutureExt,
future::{OptionFuture, join3, join4, join5},
};
use ruma::{
DeviceId, OwnedUserId, RoomId, UserId,
api::client::{
@@ -31,19 +34,19 @@ use ruma::{
Presence, Rooms, ToDevice,
},
},
uiaa::UiaaResponse,
},
events::{
AnyRawAccountDataEvent,
presence::{PresenceEvent, PresenceEventContent},
},
assign,
events::presence::{PresenceEvent, PresenceEventContent},
serde::Raw,
};
use service::{
account_data::AnyRawAccountDataEvent,
rooms::lazy_loading::{self, MemberSet, Options as _},
};
use service::rooms::lazy_loading::{self, MemberSet, Options as _};
use super::{load_timeline, share_encrypted_room};
use crate::{
Ruma,
Ruma, RumaResponse,
client::{
is_ignored_invite,
sync::v3::{joined::load_joined_room, left::load_left_room},
@@ -79,10 +82,10 @@ impl DeviceListUpdates {
impl From<DeviceListUpdates> for DeviceLists {
fn from(val: DeviceListUpdates) -> Self {
assign!(Self::new(), {
Self {
changed: val.changed.into_iter().collect(),
left: val.left.into_iter().collect(),
})
}
}
}
@@ -180,7 +183,7 @@ pub(crate) async fn sync_events_route(
State(services): State<crate::State>,
ClientIp(client_ip): ClientIp,
body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response> {
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
let (sender_user, sender_device) = body.sender();
// Presence update
@@ -224,7 +227,7 @@ pub(crate) async fn sync_events_route(
pub(crate) async fn build_sync_events(
services: &Services,
body: &Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response> {
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
let (syncing_user, syncing_device) = body.sender();
let current_count = services.globals.current_count()?;
@@ -250,8 +253,6 @@ pub(crate) async fn build_sync_events(
.get_filter(syncing_user, filter_id)
.await
.unwrap_or_default(),
// error out for unknown filter types
| _ => return Err!(Request(InvalidParam("Unknown filter type"))),
});
let context = SyncContext {
@@ -267,6 +268,7 @@ pub(crate) async fn build_sync_events(
.rooms
.state_cache
.rooms_joined(syncing_user)
.map(ToOwned::to_owned)
.broad_filter_map(|room_id| async {
let joined_room = load_joined_room(services, context, room_id.clone()).await;
@@ -330,9 +332,9 @@ pub(crate) async fn build_sync_events(
// only sync this invite if it was sent after the last /sync call
if last_sync_end_count < invite_count {
let invited_room = assign!(InvitedRoom::new(), {
invite_state: InviteState::from(invite_state),
});
let invited_room = InvitedRoom {
invite_state: InviteState { events: invite_state },
};
invited_rooms.insert(room_id, invited_room);
}
@@ -353,9 +355,9 @@ pub(crate) async fn build_sync_events(
// only sync this knock if it was sent after the last /sync call
if last_sync_end_count < knock_count {
let knocked_room = assign!(KnockedRoom::new(), {
knock_state: assign!(KnockState::new(), { events: knock_state }),
});
let knocked_room = KnockedRoom {
knock_state: KnockState { events: knock_state },
};
knocked_rooms.insert(room_id, knocked_room);
}
@@ -374,6 +376,13 @@ pub(crate) async fn build_sync_events(
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Global))
.collect();
// Look for device list updates of this account
let keys_changed = services
.users
.keys_changed(syncing_user, last_sync_end_count, Some(current_count))
.map(ToOwned::to_owned)
.collect::<HashSet<_>>();
let to_device_events = services
.users
.get_to_device_events(
@@ -385,62 +394,36 @@ pub(crate) async fn build_sync_events(
.map(at!(1))
.collect::<Vec<_>>();
// Look for device list updates of this account
let keys_changed = services
.users
.keys_changed(syncing_user, last_sync_end_count, Some(current_count))
.collect::<HashSet<_>>();
let device_one_time_keys_count = services
.users
.count_one_time_keys(syncing_user, syncing_device);
let unused_fallback_key_types = services
.users
.list_unused_fallback_key_types(syncing_user, syncing_device);
let (
(joined_rooms, mut device_list_updates),
left_rooms,
invited_rooms,
knocked_rooms,
presence_updates,
account_data,
to_device_events,
keys_changed,
device_one_time_keys_count,
unused_fallback_key_types,
) = async {
futures::join!(
joined_rooms,
left_rooms,
invited_rooms,
knocked_rooms,
presence_updates,
account_data,
to_device_events,
keys_changed,
device_one_time_keys_count,
unused_fallback_key_types,
)
}
.boxed()
.await;
// Remove all to-device events the device received *last time*
services
.users
.remove_to_device_events(syncing_user, syncing_device, last_sync_end_count)
let remove_to_device_events =
services
.users
.remove_to_device_events(syncing_user, syncing_device, last_sync_end_count);
let rooms = join4(joined_rooms, left_rooms, invited_rooms, knocked_rooms);
let ephemeral = join3(remove_to_device_events, to_device_events, presence_updates);
let top = join5(account_data, ephemeral, device_one_time_keys_count, keys_changed, rooms)
.boxed()
.await;
let (account_data, ephemeral, device_one_time_keys_count, keys_changed, rooms) = top;
let ((), to_device_events, presence_updates) = ephemeral;
let (joined_rooms, left_rooms, invited_rooms, knocked_rooms) = rooms;
let (joined_rooms, mut device_list_updates) = joined_rooms;
device_list_updates.changed.extend(keys_changed);
let response = assign!(sync_events::v3::Response::new(current_count.to_string()), {
account_data: assign!(GlobalAccountData::new(), { events: account_data }),
let response = sync_events::v3::Response {
account_data: GlobalAccountData { events: account_data },
device_lists: device_list_updates.into(),
device_one_time_keys_count,
device_unused_fallback_key_types: Some(unused_fallback_key_types),
presence: assign!(Presence::new(), {
// Fallback keys are not yet supported
device_unused_fallback_key_types: None,
next_batch: current_count.to_string(),
presence: Presence {
events: presence_updates
.into_iter()
.flat_map(IntoIterator::into_iter)
@@ -448,15 +431,15 @@ pub(crate) async fn build_sync_events(
.map(|ref event| Raw::new(event))
.filter_map(Result::ok)
.collect(),
}),
rooms: assign!(Rooms::new(), {
},
rooms: Rooms {
leave: left_rooms,
join: joined_rooms,
invite: invited_rooms,
knock: knocked_rooms,
}),
to_device: assign!(ToDevice::new(), { events: to_device_events }),
});
},
to_device: ToDevice { events: to_device_events },
};
Ok(response)
}
+111 -115
View File
@@ -27,20 +27,17 @@ use futures::{
};
use ruma::{
DeviceId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
api::client::sync::sync_events::{
self, DeviceLists, UnreadNotificationsCount, v5::request::ExtensionRoomConfig,
},
assign,
api::client::sync::sync_events::{self, DeviceLists, UnreadNotificationsCount},
directory::RoomTypeFilter,
events::{
AnySyncEphemeralRoomEvent, AnySyncStateEvent, StateEventType, TimelineEventType,
AnyRawAccountDataEvent, AnySyncEphemeralRoomEvent, AnySyncStateEvent, StateEventType,
TimelineEventType,
room::member::{MembershipState, RoomMemberEventContent},
typing::{SyncTypingEvent, TypingEventContent},
typing::TypingEventContent,
},
serde::Raw,
uint,
};
use service::account_data::AnyRawAccountDataEvent;
use super::share_encrypted_room;
use crate::{
@@ -70,8 +67,8 @@ pub(crate) async fn sync_events_v5_route(
body: Ruma<sync_events::v5::Request>,
) -> Result<sync_events::v5::Response> {
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted");
let ref sender_user = body.sender_user().to_owned();
let ref sender_device = body.sender_device().to_owned();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
services
.users
@@ -93,7 +90,7 @@ pub(crate) async fn sync_events_v5_route(
.and_then(|string| string.parse().ok())
.unwrap_or(0);
let snake_key = into_snake_key(sender_user.as_ref(), sender_device.as_str(), conn_id);
let snake_key = into_snake_key(sender_user, sender_device, conn_id);
if globalsince != 0 && !services.sync.snake_connection_cached(&snake_key) {
return Err!(Request(UnknownPos(
@@ -115,6 +112,7 @@ pub(crate) async fn sync_events_v5_route(
.rooms
.state_cache
.rooms_joined(sender_user)
.map(ToOwned::to_owned)
.collect::<Vec<OwnedRoomId>>();
let all_invited_rooms = services
@@ -166,20 +164,21 @@ pub(crate) async fn sync_events_v5_route(
let (account_data, e2ee, to_device, receipts) =
try_join4(account_data, e2ee, to_device, receipts).await?;
let extensions = assign!(sync_events::v5::response::Extensions::default(), {
let extensions = sync_events::v5::response::Extensions {
account_data,
e2ee,
to_device,
receipts,
typing: sync_events::v5::response::Typing::default(),
});
};
let mut response = assign!(sync_events::v5::Response::new(pos), {
let mut response = sync_events::v5::Response {
txn_id: body.txn_id.clone(),
pos,
lists: BTreeMap::new(),
rooms: BTreeMap::new(),
extensions,
});
};
handle_lists(
services,
@@ -380,12 +379,11 @@ where
);
}
}
response.lists.insert(
list_id.clone(),
assign!(sync_events::v5::response::List::default(), {
response
.lists
.insert(list_id.clone(), sync_events::v5::response::List {
count: ruma_from_usize(active_rooms.len()),
}),
);
});
if let Some(conn_id) = body.conn_id.clone() {
let snake_key = into_snake_key(sender_user, sender_device, conn_id);
@@ -565,19 +563,17 @@ where
.state_cache
.room_members(room_id)
.ready_filter(|member| *member != sender_user)
.filter_map(async |user_id| {
.filter_map(|user_id| {
services
.rooms
.state_accessor
.get_member(room_id, &user_id)
.map_ok(|member_event| {
assign!(sync_events::v5::response::Hero::new(user_id.clone()), {
name: member_event.displayname,
avatar: member_event.avatar_url,
})
.get_member(room_id, user_id)
.map_ok(|memberevent| sync_events::v5::response::Hero {
user_id: user_id.into(),
name: memberevent.displayname,
avatar: memberevent.avatar_url,
})
.ok()
.await
})
.take(5)
.collect()
@@ -613,76 +609,73 @@ where
None
};
rooms.insert(
room_id.clone(),
assign!(sync_events::v5::response::Room::new(), {
name: services
.rooms
.state_accessor
.get_name(room_id)
.await
.ok()
.or(name),
avatar: match heroes_avatar {
| Some(heroes_avatar) => ruma::JsOption::Some(heroes_avatar),
| _ => match services.rooms.state_accessor.get_avatar(room_id).await {
| ruma::JsOption::Some(avatar) => ruma::JsOption::from_option(avatar.url),
| ruma::JsOption::Null => ruma::JsOption::Null,
| ruma::JsOption::Undefined => ruma::JsOption::Undefined,
},
rooms.insert(room_id.clone(), sync_events::v5::response::Room {
name: services
.rooms
.state_accessor
.get_name(room_id)
.await
.ok()
.or(name),
avatar: match heroes_avatar {
| Some(heroes_avatar) => ruma::JsOption::Some(heroes_avatar),
| _ => match services.rooms.state_accessor.get_avatar(room_id).await {
| ruma::JsOption::Some(avatar) => ruma::JsOption::from_option(avatar.url),
| ruma::JsOption::Null => ruma::JsOption::Null,
| ruma::JsOption::Undefined => ruma::JsOption::Undefined,
},
initial: Some(roomsince == &0),
is_dm: None,
invite_state,
unread_notifications: assign!(UnreadNotificationsCount::new(), {
highlight_count: Some(
services
.rooms
.user
.highlight_count(sender_user, room_id)
.await
.try_into()
.expect("notification count can't go that high"),
),
notification_count: Some(
services
.rooms
.user
.notification_count(sender_user, room_id)
.await
.try_into()
.expect("notification count can't go that high"),
),
}),
timeline: room_events,
required_state,
prev_batch,
limited,
joined_count: Some(
},
initial: Some(roomsince == &0),
is_dm: None,
invite_state,
unread_notifications: UnreadNotificationsCount {
highlight_count: Some(
services
.rooms
.state_cache
.room_joined_count(room_id)
.user
.highlight_count(sender_user, room_id)
.await
.unwrap_or(0)
.try_into()
.unwrap_or_else(|_| uint!(0)),
.expect("notification count can't go that high"),
),
invited_count: Some(
notification_count: Some(
services
.rooms
.state_cache
.room_invited_count(room_id)
.user
.notification_count(sender_user, room_id)
.await
.unwrap_or(0)
.try_into()
.unwrap_or_else(|_| uint!(0)),
.expect("notification count can't go that high"),
),
num_live: None, // Count events in timeline greater than global sync counter
bump_stamp: timestamp,
heroes: Some(heroes),
}),
);
},
timeline: room_events,
required_state,
prev_batch,
limited,
joined_count: Some(
services
.rooms
.state_cache
.room_joined_count(room_id)
.await
.unwrap_or(0)
.try_into()
.unwrap_or_else(|_| uint!(0)),
),
invited_count: Some(
services
.rooms
.state_cache
.room_invited_count(room_id)
.await
.unwrap_or(0)
.try_into()
.unwrap_or_else(|_| uint!(0)),
),
num_live: None, // Count events in timeline greater than global sync counter
bump_stamp: timestamp,
heroes: Some(heroes),
});
}
Ok(rooms)
}
@@ -744,8 +737,7 @@ async fn collect_typing_events(
let rooms: Vec<_> = body.extensions.typing.rooms.clone().unwrap_or_else(|| {
body.room_subscriptions
.keys()
.cloned()
.map(ExtensionRoomConfig::Room)
.map(ToOwned::to_owned)
.collect()
});
let lists: Vec<_> = body
@@ -774,7 +766,9 @@ async fn collect_typing_events(
| Ok(typing_users) => {
typing_response.rooms.insert(
room_id.to_owned(), // Already OwnedRoomId
Raw::new(&SyncTypingEvent::new(TypingEventContent::new(typing_users)))?,
Raw::new(&sync_events::v5::response::SyncTypingEvent {
content: TypingEventContent::new(typing_users),
})?,
);
},
| Err(e) => {
@@ -790,7 +784,10 @@ async fn collect_account_data(
services: &Services,
(sender_user, _, globalsince, body): (&UserId, &DeviceId, u64, &sync_events::v5::Request),
) -> sync_events::v5::response::AccountData {
let mut account_data = sync_events::v5::response::AccountData::default();
let mut account_data = sync_events::v5::response::AccountData {
global: Vec::new(),
rooms: BTreeMap::new(),
};
if !body.extensions.account_data.enabled.unwrap_or(false) {
return sync_events::v5::response::AccountData::default();
@@ -805,17 +802,15 @@ async fn collect_account_data(
if let Some(rooms) = &body.extensions.account_data.rooms {
for room in rooms {
if let ExtensionRoomConfig::Room(room) = room {
account_data.rooms.insert(
room.clone(),
services
.account_data
.changes_since(Some(room.as_ref()), sender_user, Some(globalsince), None)
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect()
.await,
);
}
account_data.rooms.insert(
room.clone(),
services
.account_data
.changes_since(Some(room), sender_user, Some(globalsince), None)
.ready_filter_map(|e| extract_variant!(e, AnyRawAccountDataEvent::Room))
.collect()
.await,
);
}
}
@@ -846,6 +841,7 @@ where
services
.users
.keys_changed(sender_user, Some(globalsince), None)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
@@ -936,18 +932,18 @@ where
if !share_encrypted_room(
services,
sender_user,
&user_id,
user_id,
Some(room_id),
)
.await
{
device_list_changes.insert(user_id.clone());
device_list_changes.insert(user_id.to_owned());
}
},
| MembershipState::Leave => {
// Write down users that have left encrypted rooms we
// are in
left_encrypted_users.insert(user_id.clone());
left_encrypted_users.insert(user_id.to_owned());
},
| _ => {},
}
@@ -966,10 +962,9 @@ where
.ready_filter(|user_id| sender_user != *user_id)
// Only send keys if the sender doesn't share an encrypted room with the target
// already
.filter_map(async |user_id| {
share_encrypted_room(services, sender_user, &user_id, Some(room_id))
.map(|res| res.or_some(user_id.clone()))
.await
.filter_map(|user_id| {
share_encrypted_room(services, sender_user, user_id, Some(room_id))
.map(|res| res.or_some(user_id.to_owned()))
})
.collect::<Vec<_>>()
.await,
@@ -983,6 +978,7 @@ where
.users
.room_keys_changed(room_id, Some(globalsince), None)
.map(|(user_id, _)| user_id)
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.await,
);
@@ -999,7 +995,7 @@ where
}
}
Ok(assign!(sync_events::v5::response::E2EE::default(), {
Ok(sync_events::v5::response::E2EE {
device_unused_fallback_key_types: None,
device_one_time_keys_count: services
@@ -1007,11 +1003,11 @@ where
.count_one_time_keys(sender_user, sender_device)
.await,
device_lists: assign!(DeviceLists::new(), {
device_lists: DeviceLists {
changed: device_list_changes.into_iter().collect(),
left: device_list_left.into_iter().collect(),
}),
}))
},
})
}
async fn collect_to_device(
@@ -1028,7 +1024,7 @@ async fn collect_to_device(
.remove_to_device_events(sender_user, sender_device, globalsince)
.await;
Some(assign!(sync_events::v5::response::ToDevice::default(), {
Some(sync_events::v5::response::ToDevice {
next_batch: next_batch.to_string(),
events: services
.users
@@ -1036,12 +1032,12 @@ async fn collect_to_device(
.map(at!(1))
.collect()
.await,
}))
})
}
async fn collect_receipts(_services: &Services) -> sync_events::v5::response::Receipts {
sync_events::v5::response::Receipts { rooms: BTreeMap::new() }
// TODO: get explicitly requested read receipts
sync_events::v5::response::Receipts::default()
}
fn filter_rooms<'a, Rooms>(
+12 -6
View File
@@ -27,7 +27,9 @@ pub(crate) async fn update_tag_route(
.account_data
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
.await
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
tags_event
.content
@@ -44,7 +46,7 @@ pub(crate) async fn update_tag_route(
)
.await?;
Ok(create_tag::v3::Response::new())
Ok(create_tag::v3::Response {})
}
/// # `DELETE /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
@@ -62,7 +64,9 @@ pub(crate) async fn delete_tag_route(
.account_data
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
.await
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
tags_event.content.tags.remove(&body.tag.clone().into());
@@ -76,7 +80,7 @@ pub(crate) async fn delete_tag_route(
)
.await?;
Ok(delete_tag::v3::Response::new())
Ok(delete_tag::v3::Response {})
}
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags`
@@ -94,7 +98,9 @@ pub(crate) async fn get_tags_route(
.account_data
.get_room(&body.room_id, sender_user, RoomAccountDataEventType::Tag)
.await
.unwrap_or_else(|_| TagEvent::new(TagEventContent::new(BTreeMap::new())));
.unwrap_or(TagEvent {
content: TagEventContent { tags: BTreeMap::new() },
});
Ok(get_tags::v3::Response::new(tags_event.content.tags))
Ok(get_tags::v3::Response { tags: tags_event.content.tags })
}
+2 -2
View File
@@ -11,8 +11,8 @@ use crate::{Ruma, RumaResponse};
pub(crate) async fn get_protocols_route(
_body: Ruma<get_protocols::v3::Request>,
) -> Result<get_protocols::v3::Response> {
// We don't support this at all
Ok(get_protocols::v3::Response::new(BTreeMap::new()))
// TODO
Ok(get_protocols::v3::Response { protocols: BTreeMap::new() })
}
/// # `GET /_matrix/client/unstable/thirdparty/protocols`
+14 -14
View File
@@ -7,7 +7,7 @@ use conduwuit::{
},
};
use futures::StreamExt;
use ruma::{api::client::threads::get_threads, assign, uint};
use ruma::{api::client::threads::get_threads, uint};
use crate::Ruma;
@@ -59,18 +59,18 @@ pub(crate) async fn get_threads_route(
.collect()
.await;
let next_batch = threads
.last()
.filter(|_| threads.len() >= limit)
.map(at!(0))
.as_ref()
.map(ToString::to_string);
Ok(get_threads::v1::Response {
next_batch: threads
.last()
.filter(|_| threads.len() >= limit)
.map(at!(0))
.as_ref()
.map(ToString::to_string),
let chunk = threads
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect();
Ok(assign!(get_threads::v1::Response::new(chunk), { next_batch }))
chunk: threads
.into_iter()
.map(at!(1))
.map(Event::into_format)
.collect(),
})
}
+26 -31
View File
@@ -1,15 +1,14 @@
use std::collections::BTreeMap;
use axum::extract::State;
use conduwuit::{Err, Result};
use conduwuit::{Error, Result};
use conduwuit_service::sending::EduBuf;
use futures::StreamExt;
use ruma::{
api::{
client::to_device::send_event_to_device,
federation::transactions::edu::{DirectDeviceContent, Edu},
client::{error::ErrorKind, to_device::send_event_to_device},
federation::{self, transactions::edu::DirectDeviceContent},
},
assign,
to_device::DeviceIdOrAllDevices,
};
@@ -32,14 +31,14 @@ pub(crate) async fn send_event_to_device_route(
.await
.is_ok()
{
return Ok(send_event_to_device::v3::Response::new());
return Ok(send_event_to_device::v3::Response {});
}
for (target_user_id, map) in &body.messages {
for (target_device, event) in map {
for (target_device_id_maybe, event) in map {
if !services.globals.user_is_local(target_user_id) {
let mut map = BTreeMap::new();
map.insert(target_device.clone(), event.clone());
map.insert(target_device_id_maybe.clone(), event.clone());
let mut messages = BTreeMap::new();
messages.insert(target_user_id.clone(), map);
let count = services.globals.next_count()?;
@@ -47,14 +46,12 @@ pub(crate) async fn send_event_to_device_route(
let mut buf = EduBuf::new();
serde_json::to_writer(
&mut buf,
&Edu::DirectToDevice(assign!(
DirectDeviceContent::new(
sender_user.to_owned(),
body.event_type.clone(),
count.to_string().into(),
),
{ messages }
)),
&federation::transactions::edu::Edu::DirectToDevice(DirectDeviceContent {
sender: sender_user.to_owned(),
ev_type: body.event_type.clone(),
message_id: count.to_string().into(),
messages,
}),
)
.expect("DirectToDevice EDU can be serialized");
@@ -67,11 +64,11 @@ pub(crate) async fn send_event_to_device_route(
let event_type = &body.event_type.to_string();
let Ok(event) = event.deserialize_as() else {
return Err!(Request(InvalidParam("Failed to deserialize event body.")));
};
let event = event
.deserialize_as()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid"))?;
match target_device {
match target_device_id_maybe {
| DeviceIdOrAllDevices::DeviceId(target_device_id) => {
services
.users
@@ -84,22 +81,20 @@ pub(crate) async fn send_event_to_device_route(
)
.await;
},
| DeviceIdOrAllDevices::AllDevices => {
let (event_type, event) = (&event_type, &event);
services
.users
.all_device_ids(target_user_id)
.for_each(async |target_device_id| {
services
.users
.add_to_device_event(
sender_user,
target_user_id,
&target_device_id,
event_type,
event.clone(),
)
.await;
.for_each(|target_device_id| {
services.users.add_to_device_event(
sender_user,
target_user_id,
target_device_id,
event_type,
event.clone(),
)
})
.await;
},
@@ -112,5 +107,5 @@ pub(crate) async fn send_event_to_device_route(
.transactions
.add_client_txnid(sender_user, sender_device, &body.txn_id, &[]);
Ok(send_event_to_device::v3::Response::new())
Ok(send_event_to_device::v3::Response {})
}
+4 -4
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, utils, utils::math::Tried};
use ruma::api::client::typing::create_typing_event::{self, v3::TypingInfo};
use ruma::api::client::typing::create_typing_event;
use crate::Ruma;
@@ -34,9 +34,9 @@ pub(crate) async fn create_typing_event_route(
}
if services.config.allow_local_typing && !services.users.is_suspended(sender_user).await? {
match body.state {
| Typing::Yes(TypingInfo { timeout, .. }) => {
| Typing::Yes(duration) => {
let duration = utils::clamp(
timeout.as_millis().try_into().unwrap_or(u64::MAX),
duration.as_millis().try_into().unwrap_or(u64::MAX),
services
.server
.config
@@ -78,5 +78,5 @@ pub(crate) async fn create_typing_event_route(
.await?;
}
Ok(create_typing_event::v3::Response::new())
Ok(create_typing_event::v3::Response {})
}
+303
View File
@@ -0,0 +1,303 @@
use std::collections::BTreeMap;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result};
use futures::{FutureExt, StreamExt};
use ruma::{
OwnedRoomId,
api::{
client::{
membership::mutual_rooms,
profile::{delete_profile_key, get_profile_key, set_profile_key},
},
federation,
},
presence::PresenceState,
};
use super::{update_avatar_url, update_displayname};
use crate::Ruma;
/// # `GET /_matrix/client/unstable/uk.half-shot.msc2666/user/mutual_rooms`
///
/// Gets all the rooms the sender shares with the specified user.
///
/// TODO: Implement pagination, currently this just returns everything
///
/// An implementation of [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
#[tracing::instrument(skip_all, fields(%client), name = "mutual_rooms", level = "info")]
pub(crate) async fn get_mutual_rooms_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<mutual_rooms::unstable::Request>,
) -> Result<mutual_rooms::unstable::Response> {
let sender_user = body.sender_user();
if sender_user == body.user_id {
return Err!(Request(Unknown("You cannot request rooms in common with yourself.")));
}
if !services.users.exists(&body.user_id).await {
return Ok(mutual_rooms::unstable::Response { joined: vec![], next_batch_token: None });
}
let mutual_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.get_shared_rooms(sender_user, &body.user_id)
.map(ToOwned::to_owned)
.collect()
.await;
Ok(mutual_rooms::unstable::Response {
joined: mutual_rooms,
next_batch_token: None,
})
}
/// # `PUT /_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}`
///
/// Updates the profile key-value field of a user, as per MSC4133.
///
/// This also handles the avatar_url and displayname being updated.
pub(crate) async fn set_profile_key_route(
State(services): State<crate::State>,
body: Ruma<set_profile_key::unstable::Request>,
) -> Result<set_profile_key::unstable::Response> {
let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
if body.kv_pair.is_empty() {
return Err!(Request(BadJson(
"The key-value pair JSON body is empty. Use DELETE to delete a key"
)));
}
if body.kv_pair.len() > 1 {
// TODO: support PATCH or "recursively" adding keys in some sort
return Err!(Request(BadJson(
"This endpoint can only take one key-value pair at a time"
)));
}
let Some(profile_key_value) = body.kv_pair.get(&body.key_name) else {
return Err!(Request(BadJson(
"The key does not match the URL field key, or JSON body is empty (use DELETE)"
)));
};
if body.kv_pair.keys().any(|key| key.len() > 128) {
return Err!(Request(BadJson("Key names cannot be longer than 128 bytes")));
}
if body.key_name == "displayname" {
let Some(display_name) = profile_key_value.as_str() else {
return Err!(Request(BadJson("displayname must be a string")));
};
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_displayname(
&services,
&body.user_id,
Some(display_name.to_owned()),
&all_joined_rooms,
)
.boxed()
.await;
} else if body.key_name == "avatar_url" {
let Some(avatar_url) = profile_key_value.as_str() else {
return Err!(Request(BadJson("avatar_url must be a string")));
};
let mxc = ruma::OwnedMxcUri::from(avatar_url);
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_avatar_url(&services, &body.user_id, Some(mxc), None, &all_joined_rooms)
.boxed()
.await;
} else {
services.users.set_profile_key(
&body.user_id,
&body.key_name,
Some(profile_key_value.clone()),
);
}
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
Ok(set_profile_key::unstable::Response {})
}
/// # `DELETE /_matrix/client/unstable/uk.tcpip.msc4133/profile/{user_id}/{field}`
///
/// Deletes the profile key-value field of a user, as per MSC4133.
///
/// This also handles the avatar_url and displayname being updated.
pub(crate) async fn delete_profile_key_route(
State(services): State<crate::State>,
body: Ruma<delete_profile_key::unstable::Request>,
) -> Result<delete_profile_key::unstable::Response> {
let sender_user = body.sender_user();
if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user")));
}
if body.kv_pair.len() > 1 {
// TODO: support PATCH or "recursively" adding keys in some sort
return Err!(Request(BadJson(
"This endpoint can only take one key-value pair at a time"
)));
}
if body.key_name == "displayname" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_displayname(&services, &body.user_id, None, &all_joined_rooms)
.boxed()
.await;
} else if body.key_name == "avatar_url" {
let all_joined_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.rooms_joined(&body.user_id)
.map(Into::into)
.collect()
.await;
update_avatar_url(&services, &body.user_id, None, None, &all_joined_rooms)
.boxed()
.await;
} else {
services
.users
.set_profile_key(&body.user_id, &body.key_name, None);
}
if services.config.allow_local_presence {
// Presence update
services
.presence
.ping_presence(&body.user_id, &PresenceState::Online)
.await?;
}
Ok(delete_profile_key::unstable::Response {})
}
/// # `GET /_matrix/client/unstable/uk.tcpip.msc4133/profile/{userId}/{field}}`
///
/// Gets the profile key-value field of a user, as per MSC4133.
///
/// - If user is on another server and we do not have a local copy already fetch
/// the value over federation
pub(crate) async fn get_profile_key_route(
State(services): State<crate::State>,
body: Ruma<get_profile_key::unstable::Request>,
) -> Result<get_profile_key::unstable::Response> {
let mut profile_key_value: BTreeMap<String, serde_json::Value> = BTreeMap::new();
if !services.globals.user_is_local(&body.user_id) {
// Create and update our local copy of the user
if let Ok(response) = services
.sending
.send_federation_request(
body.user_id.server_name(),
federation::query::get_profile_information::v1::Request {
user_id: body.user_id.clone(),
field: None, // we want the full user's profile to update locally as well
},
)
.await
{
if !services.users.exists(&body.user_id).await {
services.users.create(&body.user_id, None, None).await?;
}
services
.users
.set_displayname(&body.user_id, response.displayname.clone());
services
.users
.set_avatar_url(&body.user_id, response.avatar_url.clone());
services
.users
.set_blurhash(&body.user_id, response.blurhash.clone());
match response.custom_profile_fields.get(&body.key_name) {
| Some(value) => {
profile_key_value.insert(body.key_name.clone(), value.clone());
services.users.set_profile_key(
&body.user_id,
&body.key_name,
Some(value.clone()),
);
},
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));
},
}
if profile_key_value.is_empty() {
return Err!(Request(NotFound("The requested profile key does not exist.")));
}
return Ok(get_profile_key::unstable::Response { value: profile_key_value });
}
}
if !services.users.exists(&body.user_id).await {
// Return 404 if this user doesn't exist and we couldn't fetch it over
// federation
return Err!(Request(NotFound("Profile was not found.")));
}
match services
.users
.profile_key(&body.user_id, &body.key_name)
.await
{
| Ok(value) => {
profile_key_value.insert(body.key_name.clone(), value);
},
| _ => {
return Err!(Request(NotFound("The requested profile key does not exist.")));
},
}
if profile_key_value.is_empty() {
return Err!(Request(NotFound("The requested profile key does not exist.")));
}
Ok(get_profile_key::unstable::Response { value: profile_key_value })
}
+42 -41
View File
@@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use axum::{Json, extract::State, response::IntoResponse};
use conduwuit::Result;
use futures::StreamExt;
use ruma::{api::client::discovery::get_supported_versions, assign};
use ruma::api::client::discovery::get_supported_versions;
use crate::Ruma;
@@ -22,47 +22,48 @@ use crate::Ruma;
pub(crate) async fn get_supported_versions_route(
_body: Ruma<get_supported_versions::Request>,
) -> Result<get_supported_versions::Response> {
let versions = vec![
"r0.0.1".to_owned(),
"r0.1.0".to_owned(),
"r0.2.0".to_owned(),
"r0.3.0".to_owned(),
"r0.4.0".to_owned(),
"r0.5.0".to_owned(),
"r0.6.0".to_owned(),
"r0.6.1".to_owned(),
"v1.1".to_owned(),
"v1.2".to_owned(),
"v1.3".to_owned(),
"v1.4".to_owned(),
"v1.5".to_owned(),
"v1.8".to_owned(),
"v1.11".to_owned(),
"v1.12".to_owned(),
"v1.13".to_owned(),
"v1.14".to_owned(),
];
let resp = get_supported_versions::Response {
versions: vec![
"r0.0.1".to_owned(),
"r0.1.0".to_owned(),
"r0.2.0".to_owned(),
"r0.3.0".to_owned(),
"r0.4.0".to_owned(),
"r0.5.0".to_owned(),
"r0.6.0".to_owned(),
"r0.6.1".to_owned(),
"v1.1".to_owned(),
"v1.2".to_owned(),
"v1.3".to_owned(),
"v1.4".to_owned(),
"v1.5".to_owned(),
"v1.8".to_owned(),
"v1.11".to_owned(),
"v1.12".to_owned(),
"v1.13".to_owned(),
"v1.14".to_owned(),
],
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) */
("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.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.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("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) */
("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.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
]),
};
let 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) */
("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.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.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
("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) */
("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.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
]);
Ok(assign!(get_supported_versions::Response::new(versions), { unstable_features }))
Ok(resp)
}
/// # `GET /_conduwuit/server_version`
+31 -29
View File
@@ -6,7 +6,6 @@ use conduwuit::{
use futures::{FutureExt, StreamExt, pin_mut};
use ruma::{
api::client::user_directory::search_users::{self},
assign,
events::room::join_rules::JoinRule,
};
@@ -32,25 +31,29 @@ pub(crate) async fn search_users_route(
.min(LIMIT_MAX);
let search_term = body.search_term.to_lowercase();
let mut users = services.users.stream().broad_filter_map(async |user_id| {
let display_name = services.users.displayname(&user_id).await.ok();
let mut users = services
.users
.stream()
.map(ToOwned::to_owned)
.broad_filter_map(async |user_id| {
let display_name = services.users.displayname(&user_id).await.ok();
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
let display_name_matches = display_name
.as_deref()
.map(str::to_lowercase)
.is_some_and(|display_name| display_name.contains(&search_term));
let display_name_matches = display_name
.as_deref()
.map(str::to_lowercase)
.is_some_and(|display_name| display_name.contains(&search_term));
if !user_id_matches && !display_name_matches {
return None;
}
if !user_id_matches && !display_name_matches {
return None;
}
let user_in_public_room =
services
let user_in_public_room = services
.rooms
.state_cache
.rooms_joined(&user_id)
.map(ToOwned::to_owned)
.broad_any(async |room_id| {
services
.rooms
@@ -60,25 +63,24 @@ pub(crate) async fn search_users_route(
.await
});
let user_sees_user = services
.rooms
.state_cache
.user_sees_user(sender_user, &user_id);
let user_sees_user = services
.rooms
.state_cache
.user_sees_user(sender_user, &user_id);
pin_mut!(user_in_public_room, user_sees_user);
if user_in_public_room.or(user_sees_user).await {
Some(assign!(search_users::v3::User::new(user_id.clone()), {
display_name,
avatar_url: services.users.avatar_url(&user_id).await.ok(),
}))
} else {
None
}
});
pin_mut!(user_in_public_room, user_sees_user);
user_in_public_room
.or(user_sees_user)
.await
.then_some(search_users::v3::User {
user_id: user_id.clone(),
display_name,
avatar_url: services.users.avatar_url(&user_id).await.ok(),
})
});
let results = users.by_ref().take(limit).collect().await;
let limited = users.next().await.is_some();
Ok(search_users::v3::Response::new(results, limited))
Ok(search_users::v3::Response { results, limited })
}
+4 -4
View File
@@ -59,10 +59,10 @@ pub(crate) async fn turn_server_route(
)
};
Ok(get_turn_server_info::v3::Response::new(
Ok(get_turn_server_info::v3::Response {
username,
password,
services.globals.turn_uris().to_vec(),
Duration::from_secs(services.globals.turn_ttl()),
))
uris: services.globals.turn_uris().to_vec(),
ttl: Duration::from_secs(services.globals.turn_ttl()),
})
}

Some files were not shown because too many files have changed in this diff Show More