Merge branch 'next' into 'master'

Release v0.4.0

See merge request famedly/conduit!369
Nyaaori/destructive-rocksdb-recovery v0.4.0
Timo Kösters 2 years ago
commit df16012661

1
.gitignore vendored

@ -57,7 +57,6 @@ $RECYCLE.BIN/
*.lnk
# Conduit
Rocket.toml
conduit.toml
conduit.db

@ -26,7 +26,7 @@ variables:
- if: "$CI_COMMIT_TAG"
- if: '($CI_MERGE_REQUEST_APPROVED == "true") || $BUILD_EVERYTHING' # Once MR is approved, test all builds. Or if BUILD_EVERYTHING is set.
interruptible: true
image: "registry.gitlab.com/jfowl/conduit-containers/rust-with-tools:latest"
image: "registry.gitlab.com/jfowl/conduit-containers/rust-with-tools@sha256:69ab327974aef4cc0daf4273579253bf7ae5e379a6c52729b83137e4caa9d093"
tags: ["docker"]
services: ["docker:dind"]
variables:
@ -220,11 +220,25 @@ docker:master:dockerhub:
variables:
TAG: "$DOCKER_HUB_IMAGE/matrixconduit/matrix-conduit:latest"
docker:tags:gitlab:
extends: .docker-shared-settings
rules:
- if: "$CI_COMMIT_TAG"
variables:
TAG: "$CI_REGISTRY_IMAGE/matrix-conduit:$CI_COMMIT_TAG"
docker:tags:dockerhub:
extends: .docker-shared-settings
rules:
- if: "$CI_COMMIT_TAG && $DOCKER_HUB"
variables:
TAG: "$DOCKER_HUB_IMAGE/matrixconduit/matrix-conduit:$CI_COMMIT_TAG"
# --------------------------------------------------------------------- #
# Run tests #
# --------------------------------------------------------------------- #
test:cargo:
.test-shared-settings:
stage: "test"
needs: []
image: "registry.gitlab.com/jfowl/conduit-containers/rust-with-tools:latest"
@ -232,21 +246,54 @@ test:cargo:
variables:
CARGO_INCREMENTAL: "false" # https://matklad.github.io/2021/09/04/fast-rust-builds.html#ci-workflow
interruptible: true
test:cargo:
extends: .test-shared-settings
before_script:
- rustup component add clippy rustfmt
# If provided, bring in caching through sccache, which uses an external S3 endpoint to store compilation results:
- if [ -n "${SCCACHE_ENDPOINT}" ]; then export RUSTC_WRAPPER=/usr/local/cargo/bin/sccache; fi
script:
- rustc --version && cargo --version # Print version info for debugging
- cargo fmt --all -- --check
- "cargo test --color always --workspace --verbose --locked --no-fail-fast -- -Z unstable-options --format json | gitlab-report -p test > $CI_PROJECT_DIR/report.xml"
- "cargo clippy --color always --verbose --message-format=json | gitlab-report -p clippy > $CI_PROJECT_DIR/gl-code-quality-report.json"
artifacts:
when: always
reports:
junit: report.xml
test:clippy:
extends: .test-shared-settings
allow_failure: true
before_script:
- rustup component add clippy
# If provided, bring in caching through sccache, which uses an external S3 endpoint to store compilation results:
- if [ -n "${SCCACHE_ENDPOINT}" ]; then export RUSTC_WRAPPER=/usr/local/cargo/bin/sccache; fi
script:
- rustc --version && cargo --version # Print version info for debugging
- "cargo clippy --color always --verbose --message-format=json | gitlab-report -p clippy > $CI_PROJECT_DIR/gl-code-quality-report.json"
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
test:format:
extends: .test-shared-settings
before_script:
- rustup component add rustfmt
script:
- cargo fmt --all -- --check
test:audit:
extends: .test-shared-settings
allow_failure: true
script:
- cargo audit --color always || true
- cargo audit --stale --json | gitlab-report -p audit > gl-sast-report.json
artifacts:
when: always
reports:
sast: gl-sast-report.json
test:sytest:
stage: "test"
allow_failure: true
@ -258,6 +305,7 @@ test:sytest:
tags: ["docker"]
variables:
PLUGINS: "https://github.com/valkum/sytest_conduit/archive/master.tar.gz"
interruptible: true
before_script:
- "mkdir -p /app"
- "cp ./conduit-debug-x86_64-unknown-linux-musl /app/conduit"
@ -278,6 +326,41 @@ test:sytest:
reports:
junit: "$CI_PROJECT_DIR/sytest.xml"
test:dockerlint:
stage: "test"
needs: []
image: "ghcr.io/hadolint/hadolint@sha256:6c4b7c23f96339489dd35f21a711996d7ce63047467a9a562287748a03ad5242" # 2.8.0-alpine
interruptible: true
script:
- hadolint --version
# First pass: Print for CI log:
- >
hadolint
--no-fail --verbose
./Dockerfile
./docker/ci-binaries-packaging.Dockerfile
# Then output the results into a json for GitLab to pretty-print this in the MR:
- >
hadolint
--format gitlab_codeclimate
--failure-threshold error
./Dockerfile
./docker/ci-binaries-packaging.Dockerfile > dockerlint.json
artifacts:
when: always
reports:
codequality: dockerlint.json
paths:
- dockerlint.json
rules:
- if: '$CI_COMMIT_REF_NAME != "master"'
changes:
- docker/*Dockerfile
- Dockerfile
- .gitlab-ci.yml
- if: '$CI_COMMIT_REF_NAME == "master"'
- if: '$CI_COMMIT_REF_NAME == "next"'
# --------------------------------------------------------------------- #
# Store binaries as package so they have download urls #
# --------------------------------------------------------------------- #
@ -313,3 +396,4 @@ workflow:
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
when: never
- if: "$CI_COMMIT_BRANCH"
- if: "$CI_COMMIT_TAG"

@ -0,0 +1,11 @@
{
"recommendations": [
"rust-lang.rust-analyzer",
"bungcip.better-toml",
"ms-azuretools.vscode-docker",
"eamodio.gitlens",
"serayuzgur.crates",
"vadimcn.vscode-lldb",
"timonwong.shellcheck"
]
}

@ -0,0 +1,35 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug conduit",
"sourceLanguages": ["rust"],
"cargo": {
"args": [
"build",
"--bin=conduit",
"--package=conduit"
],
"filter": {
"name": "conduit",
"kind": "bin"
}
},
"args": [],
"env": {
"RUST_BACKTRACE": "1",
"CONDUIT_CONFIG": "",
"CONDUIT_SERVER_NAME": "localhost",
"CONDUIT_DATABASE_PATH": "/tmp",
"CONDUIT_ADDRESS": "0.0.0.0",
"CONDUIT_PORT": "6167"
},
"cwd": "${workspaceFolder}"
}
]
}

@ -1,3 +1,3 @@
{
"rust-analyzer.procMacro.enable": true
"rust-analyzer.procMacro.enable": true,
}

1714
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -6,41 +6,41 @@ authors = ["timokoesters <timo@koesters.xyz>"]
homepage = "https://conduit.rs"
repository = "https://gitlab.com/famedly/conduit"
readme = "README.md"
version = "0.3.0"
version = "0.4.0"
rust-version = "1.56"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Used to handle requests
# TODO: This can become optional as soon as proper configs are supported
# rocket = { git = "https://github.com/SergioBenitez/Rocket.git", rev = "801e04bd5369eb39e126c75f6d11e1e9597304d8", features = ["tls"] } # Used to handle requests
rocket = { version = "0.5.0-rc.1", features = ["tls"] } # Used to handle requests
# Web framework
axum = { version = "0.5.8", default-features = false, features = ["form", "headers", "http1", "http2", "json", "matched-path"], optional = true }
axum-server = { version = "0.4.0", features = ["tls-rustls"] }
tower = { version = "0.4.8", features = ["util"] }
tower-http = { version = "0.3.4", features = ["add-extension", "cors", "compression-full", "sensitive-headers", "trace", "util"] }
# Used for matrix spec type definitions and helpers
#ruma = { version = "0.4.0", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "f7a10a7e471b59d3096be2695c2a05d407d80df1", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "d614ad1422d6c4b3437ebc318ca8514ae338fd6d", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-pre-spec", "unstable-exhaustive-types"] }
#ruma = { git = "https://github.com/timokoesters/ruma", rev = "50c1db7e0a3a21fc794b0cce3b64285a4c750c71", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
#ruma = { path = "../ruma/crates/ruma", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
# Used for long polling and federation sender, should be the same as rocket::tokio
tokio = "1.11.0"
# Async runtime and utilities
tokio = { version = "1.11.0", features = ["fs", "macros", "signal", "sync"] }
# Used for storing data permanently
sled = { version = "0.34.6", features = ["compression", "no_metrics"], optional = true }
sled = { version = "0.34.7", features = ["compression", "no_metrics"], optional = true }
#sled = { git = "https://github.com/spacejam/sled.git", rev = "e4640e0773595229f398438886f19bca6f7326a2", features = ["compression"] }
persy = { version = "1.2" , optional = true, features=["background_ops"] }
persy = { version = "1.0.0", optional = true, features = ["background_ops"] }
# Used for the http request / response body type for Ruma endpoints used with reqwest
bytes = "1.1.0"
# Used for rocket<->ruma conversions
http = "0.2.4"
# Used to find data directory for default db path
directories = "3.0.2"
directories = "4.0.0"
# Used for ruma wrapper
serde_json = { version = "1.0.70", features = ["raw_value"] }
serde_json = { version = "1.0.68", features = ["raw_value"] }
# Used for appservice registration files
serde_yaml = "0.8.20"
serde_yaml = "0.8.21"
# Used for pdu definition
serde = { version = "1.0.130", features = ["rc"] }
# Used for secure identifiers
@ -48,9 +48,9 @@ rand = "0.8.4"
# Used to hash passwords
rust-argon2 = "0.8.3"
# Used to send requests
reqwest = { default-features = false, features = ["rustls-tls", "socks"], git = "https://github.com/timokoesters/reqwest", rev = "57b7cf4feb921573dfafad7d34b9ac6e44ead0bd" }
reqwest = { default-features = false, features = ["rustls-tls-native-roots", "socks"], git = "https://github.com/timokoesters/reqwest", rev = "57b7cf4feb921573dfafad7d34b9ac6e44ead0bd" }
# Used for conduit::Error type
thiserror = "1.0.28"
thiserror = "1.0.29"
# Used to generate thumbnails for images
image = { version = "0.23.14", default-features = false, features = ["jpeg", "png", "gif"] }
# Used to encode server public key
@ -64,8 +64,8 @@ regex = "1.5.4"
# jwt jsonwebtokens
jsonwebtoken = "7.2.0"
# Performance measurements
tracing = { version = "0.1.26", features = ["release_max_level_warn"] }
tracing-subscriber = "0.2.20"
tracing = { version = "0.1.27", features = [] }
tracing-subscriber = "0.2.22"
tracing-flame = "0.1.0"
opentelemetry = { version = "0.16.0", features = ["rt-tokio"] }
opentelemetry-jaeger = { version = "0.15.0", features = ["rt-tokio"] }
@ -76,15 +76,17 @@ crossbeam = { version = "0.8.1", optional = true }
num_cpus = "1.13.0"
threadpool = "1.8.1"
heed = { git = "https://github.com/timokoesters/heed.git", rev = "f6f825da7fb2c758867e05ad973ef800a6fe1d5d", optional = true }
rocksdb = { version = "0.17.0", default-features = false, features = ["multi-threaded-cf", "zstd"], optional = true }
rocksdb = { version = "0.17.0", default-features = true, features = ["multi-threaded-cf", "zstd"], optional = true }
thread_local = "1.1.3"
# used for TURN server authentication
hmac = "0.11.0"
sha-1 = "0.9.8"
# used for conduit's CLI and admin room command parsing
clap = { version = "3.0.10", default-features = false, features = ["std", "derive"] }
maplit = "1.0.2"
clap = { version = "3.2.5", default-features = false, features = ["std", "derive"] }
futures-util = { version = "0.3.17", default-features = false }
# Used for reading the configuration from conduit.toml & environment variables
figment = { version = "0.10.6", features = ["env", "toml"] }
tikv-jemalloc-ctl = { version = "0.4.2", features = ["use_std"], optional = true }
tikv-jemallocator = { version = "0.4.1", features = ["unprefixed_malloc_on_supported_platforms"], optional = true }
@ -98,7 +100,7 @@ backend_heed = ["heed", "crossbeam"]
backend_rocksdb = ["rocksdb"]
jemalloc = ["tikv-jemalloc-ctl", "tikv-jemallocator"]
sqlite = ["rusqlite", "parking_lot", "tokio/signal"]
conduit_bin = [] # TODO: add rocket to this when it is optional
conduit_bin = ["axum"]
[[bin]]
name = "conduit"

@ -20,4 +20,4 @@ image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-arm-unknown-lin
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-armv7-unknown-linux-musleabihf:latest"
[target.x86_64-unknown-linux-musl]
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-x86_64-unknown-linux-musl:latest"
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-x86_64-unknown-linux-musl@sha256:b6d689e42f0236c8a38b961bca2a12086018b85ed20e0826310421daf182e2bb"

@ -43,7 +43,6 @@ $ sudo apt install libclang-dev build-essential
$ cargo build --release
```
Note that this currently requires Rust 1.50.
If you want to cross compile Conduit to another architecture, read the [Cross-Compile Guide](cross/README.md).
@ -58,6 +57,12 @@ In Debian you can use this command to create a Conduit user:
sudo adduser --system conduit --no-create-home
```
## Forwarding ports in the firewall or the router
Conduit uses the ports 443 and 8448 both of which need to be open in the firewall.
If Conduit runs behind a router or in a container and has a different public IP address than the host system these public ports need to be forwarded directly or indirectly to the port mentioned in the config.
## Setting up a systemd service
Now we'll set up a systemd service for Conduit, so it's easy to start/stop Conduit and set it to autostart when your
@ -89,28 +94,35 @@ $ sudo systemctl daemon-reload
## Creating the Conduit configuration file
Now we need to create the Conduit's config file in `/etc/matrix-conduit/conduit.toml`. Paste this in **and take a moment
to read it. You need to change at least the server name.**
to read it. You need to change at least the server name.**
You can also choose to use a different database backend, but right now only `rocksdb` and `sqlite` are recommended.
```toml
[global]
# The server_name is the name of this server. It is used as a suffix for user
# The server_name is the pretty name of this server. It is used as a suffix for user
# and room ids. Examples: matrix.org, conduit.rs
# The Conduit server needs to be reachable at https://your.server.name/ on port
# 443 (client-server) and 8448 (federation) OR you can create /.well-known
# files to redirect requests. See
# The Conduit server needs all /_matrix/ requests to be reachable at
# https://your.server.name/ on port 443 (client-server) and 8448 (federation).
# If that's not possible for you, you can create /.well-known files to redirect
# requests. See
# https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
# and https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
# and
# https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
# for more information
# YOU NEED TO EDIT THIS
#server_name = "your.server.name"
# This is the only directory where Conduit will save its data
database_path = "/var/lib/matrix-conduit/conduit_db"
database_path = "/var/lib/matrix-conduit/"
database_backend = "rocksdb"
# The port Conduit will be running on. You need to set up a reverse proxy in
# your web server (e.g. apache or nginx), so all requests to /_matrix on port
# 443 and 8448 will be forwarded to the Conduit instance running on this port
# Docker users: Don't change this, you'll need to map an external port to this.
port = 6167
# Max size for uploads
@ -119,20 +131,15 @@ max_request_size = 20_000_000 # in bytes
# Enables registration. If set to false, no users can register on this server.
allow_registration = true
# Disable encryption, so no new encrypted rooms can be created
# Note: existing rooms will continue to work
allow_encryption = true
allow_federation = true
trusted_servers = ["matrix.org"]
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
#workers = 4 # default: cpu core count * 2
#log = "info,state_res=warn,rocket=off,_=off,sled=off"
address = "127.0.0.1" # This makes sure Conduit can only be reached using the reverse proxy
# The total amount of memory that the database will use.
#db_cache_capacity_mb = 200
#address = "0.0.0.0" # If Conduit is running in a container, make sure the reverse proxy (ie. Traefik) can reach it.
```
## Setting the correct file permissions
@ -141,19 +148,21 @@ As we are using a Conduit specific user we need to allow it to read the config.
Debian:
```bash
sudo chown -R conduit:nogroup /etc/matrix-conduit
sudo chown -R root:root /etc/matrix-conduit
sudo chmod 755 /etc/matrix-conduit
```
If you use the default database path you also need to run this:
```bash
sudo mkdir -p /var/lib/matrix-conduit/conduit_db
sudo chown -R conduit:nogroup /var/lib/matrix-conduit/conduit_db
sudo mkdir -p /var/lib/matrix-conduit/
sudo chown -R conduit:nogroup /var/lib/matrix-conduit/
sudo chmod 700 /var/lib/matrix-conduit/
```
## Setting up the Reverse Proxy
This depends on whether you use Apache, Nginx or another web server.
This depends on whether you use Apache, Caddy, Nginx or another web server.
### Apache
@ -179,6 +188,19 @@ ProxyPassReverse /_matrix/ http://127.0.0.1:6167/_matrix/
$ sudo systemctl reload apache2
```
### Caddy
Create `/etc/caddy/conf.d/conduit_caddyfile` and enter this (substitute for your server name).
```caddy
your.server.name, your.server.name:8448 {
reverse_proxy /_matrix/* 127.0.0.1:6167
}
```
That's it! Just start or enable the service and you're set.
```bash
$ sudo systemctl enable caddy
```
### Nginx
If you use Nginx and not Apache, add the following server section inside the http section of `/etc/nginx/nginx.conf`
@ -213,6 +235,8 @@ $ sudo systemctl reload nginx
## SSL Certificate
If you chose Caddy as your web proxy SSL certificates are handled automatically and you can skip this step.
The easiest way to get an SSL certificate, if you don't have one already, is to install `certbot` and run this:
```bash
@ -244,5 +268,15 @@ $ curl https://your.server.name/_matrix/client/versions
$ curl https://your.server.name:8448/_matrix/client/versions
```
- To check if your server can talk with other homeservers, you can use the [Matrix Federation Tester](https://federationtester.matrix.org/)
- If you want to set up an appservice, take a look at the [Appservice Guide](APPSERVICES.md).
- To check if your server can talk with other homeservers, you can use the [Matrix Federation Tester](https://federationtester.matrix.org/).
If you can register but cannot join federated rooms check your config again and also check if the port 8448 is open and forwarded correctly.
# What's next?
## Audio/Video calls
For Audio/Video call functionality see the [TURN Guide](TURN.md).
## Appservices
If you want to set up an appservice, take a look at the [Appservice Guide](APPSERVICES.md).

@ -3,7 +3,8 @@ FROM docker.io/rust:1.58-bullseye AS builder
WORKDIR /usr/src/conduit
# Install required packages to build Conduit and it's dependencies
RUN apt update && apt -y install libclang-dev
RUN apt-get update && \
apt-get -y --no-install-recommends install libclang-dev=1:11.0-51+nmu5
# == Build dependencies without our own code separately for caching ==
#
@ -35,14 +36,16 @@ FROM docker.io/debian:bullseye-slim AS runner
# You still need to map the port when using the docker command or docker-compose.
EXPOSE 6167
# Note from @jfowl: I would like to remove the config file in the future and just have the Docker version be configured with envs.
ENV CONDUIT_CONFIG="/srv/conduit/conduit.toml" \
CONDUIT_PORT=6167
ENV CONDUIT_PORT=6167 \
CONDUIT_ADDRESS="0.0.0.0" \
CONDUIT_DATABASE_PATH=/var/lib/matrix-conduit \
CONDUIT_CONFIG=''
# └─> Set no config file to do all configuration with env vars
# Conduit needs:
# ca-certificates: for https
# iproute2 & wget: for the healthcheck script
RUN apt update && apt -y install \
RUN apt-get update && apt-get -y --no-install-recommends install \
ca-certificates \
iproute2 \
wget \
@ -59,12 +62,12 @@ HEALTHCHECK --start-period=5s --interval=5s CMD ./healthcheck.sh
COPY --from=builder /usr/src/conduit/target/release/conduit /srv/conduit/conduit
# Improve security: Don't run stuff as root, that does not need to run as root
# Add 'conduit' user and group (100:82). The UID:GID choice is to be compatible
# with previous, Alpine-based containers, where the user and group were both
# named 'www-data'.
# Most distros also use 1000:1000 for the first real user, so this should resolve volume mounting problems.
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN set -x ; \
groupadd -r -g 82 conduit ; \
useradd -r -M -d /srv/conduit -o -u 100 -g conduit conduit && exit 0 ; exit 1
groupadd -r -g ${GROUP_ID} conduit ; \
useradd -l -r -M -d /srv/conduit -o -u ${USER_ID} -g conduit conduit && exit 0 ; exit 1
# Change ownership of Conduit files to conduit user and group and make the healthcheck executable:
RUN chown -cR conduit:conduit /srv/conduit && \

@ -19,7 +19,7 @@ HQ.
#### What is the current status?
As of 2021-09-01, Conduit is Beta, meaning you can join and participate in most
Conduit is Beta, meaning you can join and participate in most
Matrix rooms, but not all features are supported and you might run into bugs
from time to time.
@ -54,7 +54,7 @@ Thanks to Famedly, Prototype Fund (DLR and German BMBF) and all other individual
Thanks to the contributors to Conduit and all libraries we use, for example:
- Ruma: A clean library for the Matrix Spec in Rust
- Rocket: A flexible web framework
- axum: A modular web framework
#### Donate

@ -0,0 +1,25 @@
# Setting up TURN/STURN
## General instructions
* It is assumed you have a [Coturn server](https://github.com/coturn/coturn) up and running. See [Synapse reference implementation](https://github.com/matrix-org/synapse/blob/develop/docs/turn-howto.md).
## Edit/Add a few settings to your existing conduit.toml
```
# Refer to your Coturn settings.
# `your.turn.url` has to match the REALM setting of your Coturn as well as `transport`.
turn_uris = ["turn:your.turn.url?transport=udp", "turn:your.turn.url?transport=tcp"]
# static-auth-secret of your turnserver
turn_secret = "ADD SECRET HERE"
# If you have your TURN server configured to use a username and password
# you can provide these information too. In this case comment out `turn_secret above`!
#turn_username = ""
#turn_password = ""
```
## Apply settings
Restart Conduit.

@ -1,3 +1,10 @@
# =============================================================================
# This is the official example config for Conduit.
# If you use it for your server, you will need to adjust it to your own needs.
# At the very least, change the server_name field!
# =============================================================================
[global]
# The server_name is the pretty name of this server. It is used as a suffix for user
# and room ids. Examples: matrix.org, conduit.rs
@ -16,7 +23,7 @@
#server_name = "your.server.name"
# This is the only directory where Conduit will save its data
database_path = "/var/lib/conduit/"
database_path = "/var/lib/matrix-conduit/"
database_backend = "rocksdb"
# The port Conduit will be running on. You need to set up a reverse proxy in
@ -31,24 +38,15 @@ max_request_size = 20_000_000 # in bytes
# Enables registration. If set to false, no users can register on this server.
allow_registration = true
# Disable encryption, so no new encrypted rooms can be created
# Note: existing rooms will continue to work
#allow_encryption = false
#allow_federation = false
allow_federation = true
# Enable jaeger to support monitoring and troubleshooting through jaeger
#allow_jaeger = false
# Enable the display name lightning bolt on registration.
enable_lightning_bolt = true
trusted_servers = ["matrix.org"]
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
#log = "info,state_res=warn,rocket=off,_=off,sled=off"
#workers = 4 # default: cpu core count * 2
address = "127.0.0.1" # This makes sure Conduit can only be reached using the reverse proxy
#address = "0.0.0.0" # If Conduit is running in a container, make sure the reverse proxy (ie. Traefik) can reach it.
proxy = "none" # more examples can be found at src/database/proxy.rs:6
# The total amount of memory that the database will use.
#db_cache_capacity_mb = 200

@ -1,37 +0,0 @@
## Cross compilation
The `cross` folder contains a set of convenience scripts (`build.sh` and `test.sh`) for cross-compiling Conduit.
Currently supported targets are
- aarch64-unknown-linux-musl
- arm-unknown-linux-musleabihf
- armv7-unknown-linux-musleabihf
- x86\_64-unknown-linux-musl
### Install prerequisites
#### Docker
[Installation guide](https://docs.docker.com/get-docker/).
```sh
$ sudo apt install docker
$ sudo systemctl start docker
$ sudo usermod -aG docker $USER
$ newgrp docker
```
#### Cross
[Installation guide](https://github.com/rust-embedded/cross/#installation).
```sh
$ cargo install cross
```
### Buiding Conduit
```sh
$ TARGET=armv7-unknown-linux-musleabihf ./cross/build.sh --release
```
The cross-compiled binary is at `target/armv7-unknown-linux-musleabihf/release/conduit`
### Testing Conduit
```sh
$ TARGET=armv7-unknown-linux-musleabihf ./cross/test.sh --release
```

@ -3,6 +3,7 @@ Description=Conduit Matrix homeserver
After=network.target
[Service]
DynamicUser=yes
User=_matrix-conduit
Group=_matrix-conduit
Type=simple

37
debian/postinst vendored

@ -5,7 +5,7 @@ set -e
CONDUIT_CONFIG_PATH=/etc/matrix-conduit
CONDUIT_CONFIG_FILE="${CONDUIT_CONFIG_PATH}/conduit.toml"
CONDUIT_DATABASE_PATH=/var/lib/matrix-conduit/conduit_db
CONDUIT_DATABASE_PATH=/var/lib/matrix-conduit/
case "$1" in
configure)
@ -36,18 +36,24 @@ case "$1" in
mkdir -p "$CONDUIT_CONFIG_PATH"
cat > "$CONDUIT_CONFIG_FILE" << EOF
[global]
# The server_name is the name of this server. It is used as a suffix for user
# and room ids. Examples: matrix.org, conduit.rs
# The Conduit server needs to be reachable at https://your.server.name/ on port
# 443 (client-server) and 8448 (federation) OR you can create /.well-known
# files to redirect requests. See
# The server_name is the pretty name of this server. It is used as a suffix for
# user and room ids. Examples: matrix.org, conduit.rs
# The Conduit server needs all /_matrix/ requests to be reachable at
# https://your.server.name/ on port 443 (client-server) and 8448 (federation).
# If that's not possible for you, you can create /.well-known files to redirect
# requests. See
# https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
# and https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
# for more information.
# and
# https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
# for more information
server_name = "${CONDUIT_SERVER_NAME}"
# This is the only directory where Conduit will save its data.
database_path = "${CONDUIT_DATABASE_PATH}"
database_backend = "rocksdb"
# The address Conduit will be listening on.
# By default the server listens on address 0.0.0.0. Change this to 127.0.0.1 to
@ -56,7 +62,8 @@ address = "${CONDUIT_ADDRESS}"
# The port Conduit will be running on. You need to set up a reverse proxy in
# your web server (e.g. apache or nginx), so all requests to /_matrix on port
# 443 and 8448 will be forwarded to the Conduit instance running on this port.
# 443 and 8448 will be forwarded to the Conduit instance running on this port
# Docker users: Don't change this, you'll need to map an external port to this.
port = ${CONDUIT_PORT}
# Max size for uploads
@ -65,20 +72,12 @@ max_request_size = 20_000_000 # in bytes
# Enables registration. If set to false, no users can register on this server.
allow_registration = true
# Disable encryption, so no new encrypted rooms can be created.
# Note: Existing rooms will continue to work.
#allow_encryption = false
#allow_federation = false
allow_federation = true
# Enable jaeger to support monitoring and troubleshooting through jaeger.
#allow_jaeger = false
trusted_servers = ["matrix.org"]
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
#log = "info,state_res=warn,rocket=off,_=off,sled=off"
#workers = 4 # default: cpu core count * 2
# The total amount of memory that the database will use.
#db_cache_capacity_mb = 200
EOF
fi
;;

@ -20,27 +20,21 @@ services:
ports:
- 8448:6167
volumes:
- db:/srv/conduit/.local/share/conduit
### Uncomment if you want to use conduit.toml to configure Conduit
### Note: Set env vars will override conduit.toml values
# - ./conduit.toml:/srv/conduit/conduit.toml
- db:/var/lib/matrix-conduit/
environment:
CONDUIT_SERVER_NAME: localhost:6167 # replace with your own name
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
CONDUIT_SERVER_NAME: your.server.name # EDIT THIS
CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit/
CONDUIT_DATABASE_BACKEND: rocksdb
CONDUIT_PORT: 6167
CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUIT_ALLOW_REGISTRATION: 'true'
### Uncomment and change values as desired
# CONDUIT_ADDRESS: 0.0.0.0
# CONDUIT_PORT: 6167
# CONDUIT_CONFIG: '/srv/conduit/conduit.toml' # if you want to configure purely by env vars, set this to an empty string ''
# Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
# CONDUIT_LOG: info # default is: "info,rocket=off,_=off,sled=off"
# CONDUIT_ALLOW_JAEGER: 'false'
# CONDUIT_ALLOW_ENCRYPTION: 'false'
# CONDUIT_ALLOW_FEDERATION: 'false'
# CONDUIT_DATABASE_PATH: /srv/conduit/.local/share/conduit
# CONDUIT_WORKERS: 10
# CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUIT_ALLOW_FEDERATION: 'true'
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
#CONDUIT_MAX_CONCURRENT_REQUESTS: 100
#CONDUIT_LOG: info,rocket=off,_=off,sled=off
CONDUIT_ADDRESS: 0.0.0.0
CONDUIT_CONFIG: '' # Ignore this
#
### Uncomment if you want to use your own Element-Web App.
### Note: You need to provide a config.json for Element and you also need a second
### Domain or Subdomain for the communication between Element and Conduit
@ -56,4 +50,4 @@ services:
# - homeserver
volumes:
db:
db:

@ -24,7 +24,17 @@ which also will tag the resulting image as `matrixconduit/matrix-conduit:latest`
After building the image you can simply run it with
```bash
docker run -d -p 8448:6167 -v ~/conduit.toml:/srv/conduit/conduit.toml -v db:/srv/conduit/.local/share/conduit matrixconduit/matrix-conduit:latest
docker run -d -p 8448:6167 \
-v db:/var/lib/matrix-conduit/ \
-e CONDUIT_SERVER_NAME="your.server.name" \
-e CONDUIT_DATABASE_BACKEND="rocksdb" \
-e CONDUIT_ALLOW_REGISTRATION=true \
-e CONDUIT_ALLOW_FEDERATION=true \
-e CONDUIT_MAX_REQUEST_SIZE="20_000_000" \
-e CONDUIT_TRUSTED_SERVERS="[\"matrix.org\"]" \
-e CONDUIT_MAX_CONCURRENT_REQUESTS="100" \
-e CONDUIT_LOG="info,rocket=off,_=off,sled=off" \
--name conduit matrixconduit/matrix-conduit:latest
```
or you can skip the build step and pull the image from one of the following registries:
@ -46,8 +56,18 @@ If you just want to test Conduit for a short time, you can use the `--rm` flag,
## Docker-compose
If the docker command is not for you or your setup, you can also use one of the provided `docker-compose` files. Depending on your proxy setup, use the [`docker-compose.traefik.yml`](docker-compose.traefik.yml) and [`docker-compose.override.traefik.yml`](docker-compose.override.traefik.yml) for Traefik (don't forget to remove `.traefik` from the filenames) or the normal [`docker-compose.yml`](../docker-compose.yml) for every other reverse proxy. Additional info about deploying
Conduit can be found [here](../DEPLOY.md).
If the `docker run` command is not for you or your setup, you can also use one of the provided `docker-compose` files.
Depending on your proxy setup, you can use one of the following files;
- If you already have a `traefik` instance set up, use [`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml)
- If you don't have a `traefik` instance set up (or any other reverse proxy), use [`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)
- For any other reverse proxy, use [`docker-compose.yml`](docker-compose.yml)
When picking the traefik-related compose file, rename it so it matches `docker-compose.yml`, and
rename the override file to `docker-compose.override.yml`. Edit the latter with the values you want
for your server.
Additional info about deploying Conduit can be found [here](../DEPLOY.md).
### Build
@ -71,11 +91,16 @@ docker-compose up -d
### Use Traefik as Proxy
As a container user, you probably know about Traefik. It is a easy to use reverse proxy for making containerized app and services available through the web. With the
two provided files, [`docker-compose.traefik.yml`](docker-compose.traefik.yml) and [`docker-compose.override.traefik.yml`](docker-compose.override.traefik.yml), it is
equally easy to deploy and use Conduit, with a little caveat. If you already took a look at the files, then you should have seen the `well-known` service, and that is
the little caveat. Traefik is simply a proxy and loadbalancer and is not able to serve any kind of content, but for Conduit to federate, we need to either expose ports
`443` and `8448` or serve two endpoints `.well-known/matrix/client` and `.well-known/matrix/server`.
As a container user, you probably know about Traefik. It is a easy to use reverse proxy for making
containerized app and services available through the web. With the two provided files,
[`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml) (or
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)) and
[`docker-compose.override.yml`](docker-compose.override.traefik.yml), it is equally easy to deploy
and use Conduit, with a little caveat. If you already took a look at the files, then you should have
seen the `well-known` service, and that is the little caveat. Traefik is simply a proxy and
loadbalancer and is not able to serve any kind of content, but for Conduit to federate, we need to
either expose ports `443` and `8448` or serve two endpoints `.well-known/matrix/client` and
`.well-known/matrix/server`.
With the service `well-known` we use a single `nginx` container that will serve those two files.
@ -112,4 +137,4 @@ So...step by step:
```
6. Run `docker-compose up -d`
7. Connect to your homeserver with your preferred client and create a user. You should do this immediatly after starting Conduit, because the first created user is the admin.
7. Connect to your homeserver with your preferred client and create a user. You should do this immediately after starting Conduit, because the first created user is the admin.

@ -7,16 +7,21 @@
# Credit's for the original Dockerfile: Weasy666.
# ---------------------------------------------------------------------------------------------------------
FROM docker.io/alpine:3.15.0 AS runner
FROM docker.io/alpine:3.16.0@sha256:4ff3ca91275773af45cb4b0834e12b7eb47d1c18f770a0b151381cd227f4c253 AS runner
# Standard port on which Conduit launches.
# You still need to map the port when using the docker command or docker-compose.
EXPOSE 6167
# Note from @jfowl: I would like to remove the config file in the future and just have the Docker version be configured with envs.
ENV CONDUIT_CONFIG="/srv/conduit/conduit.toml" \
CONDUIT_PORT=6167
# Users are expected to mount a volume to this directory:
ARG DEFAULT_DB_PATH=/var/lib/matrix-conduit
ENV CONDUIT_PORT=6167 \
CONDUIT_ADDRESS="0.0.0.0" \
CONDUIT_DATABASE_PATH=${DEFAULT_DB_PATH} \
CONDUIT_CONFIG=''
# └─> Set no config file to do all configuration with env vars
# Conduit needs:
# ca-certificates: for https
@ -25,7 +30,6 @@ RUN apk add --no-cache \
ca-certificates \
iproute2
ARG CREATED
ARG VERSION
ARG GIT_REF
@ -44,37 +48,37 @@ LABEL org.opencontainers.image.created=${CREATED} \
org.opencontainers.image.documentation="https://gitlab.com/famedly/conduit" \
org.opencontainers.image.ref.name=""
# Created directory for the database and media files
RUN mkdir -p /srv/conduit/.local/share/conduit
# Test if Conduit is still alive, uses the same endpoint as Element
COPY ./docker/healthcheck.sh /srv/conduit/healthcheck.sh
HEALTHCHECK --start-period=5s --interval=5s CMD ./healthcheck.sh
# Depending on the target platform (e.g. "linux/arm/v7", "linux/arm64/v8", or "linux/amd64")
# copy the matching binary into this docker image
ARG TARGETPLATFORM
COPY ./$TARGETPLATFORM /srv/conduit/conduit
# Improve security: Don't run stuff as root, that does not need to run as root:
# Add www-data user and group with UID 82, as used by alpine
# https://git.alpinelinux.org/aports/tree/main/nginx/nginx.pre-install
# Most distros also use 1000:1000 for the first real user, so this should resolve volume mounting problems.
ARG USER_ID=1000
ARG GROUP_ID=1000
RUN set -x ; \
addgroup -Sg 82 www-data 2>/dev/null ; \
adduser -S -D -H -h /srv/conduit -G www-data -g www-data www-data 2>/dev/null ; \
addgroup www-data www-data 2>/dev/null && exit 0 ; exit 1
# Change ownership of Conduit files to www-data user and group
RUN chown -cR www-data:www-data /srv/conduit
RUN chmod +x /srv/conduit/healthcheck.sh
# Change user to www-data
USER www-data
deluser --remove-home www-data ; \
addgroup -S -g ${GROUP_ID} conduit 2>/dev/null ; \
adduser -S -u ${USER_ID} -D -H -h /srv/conduit -G conduit -g conduit conduit 2>/dev/null ; \
addgroup conduit conduit 2>/dev/null && exit 0 ; exit 1
# Change ownership of Conduit files to conduit user and group
RUN chown -cR conduit:conduit /srv/conduit && \
chmod +x /srv/conduit/healthcheck.sh && \
mkdir -p ${DEFAULT_DB_PATH} && \
chown -cR conduit:conduit ${DEFAULT_DB_PATH}
# Change user to conduit
USER conduit
# Set container home directory
WORKDIR /srv/conduit
# Run Conduit and print backtraces on panics
ENV RUST_BACKTRACE=1
ENTRYPOINT [ "/srv/conduit/conduit" ]
# Depending on the target platform (e.g. "linux/arm/v7", "linux/arm64/v8", or "linux/amd64")
# copy the matching binary into this docker image
ARG TARGETPLATFORM
COPY --chown=conduit:conduit ./$TARGETPLATFORM /srv/conduit/conduit

@ -0,0 +1,68 @@
# Conduit - Behind Traefik Reverse Proxy
version: '3'
services:
homeserver:
### If you already built the Conduit image with 'docker build' or want to use the Docker Hub image,
### then you are ready to go.
image: matrixconduit/matrix-conduit:latest
### If you want to build a fresh image from the sources, then comment the image line and uncomment the
### build lines. If you want meaningful labels in your built Conduit image, you should run docker-compose like this:
### CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml) docker-compose up -d
# build:
# context: .
# args:
# CREATED: '2021-03-16T08:18:27Z'
# VERSION: '0.1.0'
# LOCAL: 'false'
# GIT_REF: origin/master
restart: unless-stopped
volumes:
- db:/var/lib/matrix-conduit/
networks:
- proxy
environment:
CONDUIT_SERVER_NAME: your.server.name # EDIT THIS
CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit/
CONDUIT_DATABASE_BACKEND: rocksdb
CONDUIT_PORT: 6167
CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUIT_ALLOW_REGISTRATION: 'true'
CONDUIT_ALLOW_FEDERATION: 'true'
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
#CONDUIT_MAX_CONCURRENT_REQUESTS: 100
#CONDUIT_LOG: info,rocket=off,_=off,sled=off
CONDUIT_ADDRESS: 0.0.0.0
CONDUIT_CONFIG: '' # Ignore this
# We need some way to server the client and server .well-known json. The simplest way is to use a nginx container
# to serve those two as static files. If you want to use a different way, delete or comment the below service, here
# and in the docker-compose override file.
well-known:
image: nginx:latest
restart: unless-stopped
volumes:
- ./nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf # the config to serve the .well-known/matrix files
- ./nginx/www:/var/www/ # location of the client and server .well-known-files
### Uncomment if you want to use your own Element-Web App.
### Note: You need to provide a config.json for Element and you also need a second
### Domain or Subdomain for the communication between Element and Conduit
### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md
# element-web:
# image: vectorim/element-web:latest
# restart: unless-stopped
# volumes:
# - ./element_config.json:/app/config.json
# networks:
# - proxy
# depends_on:
# - homeserver
volumes:
db:
networks:
# This is the network Traefik listens to, if your network has a different
# name, don't forget to change it here and in the docker-compose.override.yml
proxy:
external: true

@ -33,7 +33,7 @@ services:
# CONDUIT_PORT: 6167
# CONDUIT_CONFIG: '/srv/conduit/conduit.toml' # if you want to configure purely by env vars, set this to an empty string ''
# Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
# CONDUIT_LOG: info # default is: "info,rocket=off,_=off,sled=off"
# CONDUIT_LOG: info # default is: "info,_=off,sled=off"
# CONDUIT_ALLOW_JAEGER: 'false'
# CONDUIT_ALLOW_ENCRYPTION: 'false'
# CONDUIT_ALLOW_FEDERATION: 'false'
@ -65,11 +65,33 @@ services:
# depends_on:
# - homeserver
traefik:
image: "traefik:latest"
container_name: "traefik"
restart: "unless-stopped"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
# - "./traefik_config:/etc/traefik"
- "acme:/etc/traefik/acme"
labels:
- "traefik.enable=true"
# middleware redirect
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
# global redirect to https
- "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
- "traefik.http.routers.redirs.entrypoints=http"
- "traefik.http.routers.redirs.middlewares=redirect-to-https"
networks:
- proxy
volumes:
db:
acme:
networks:
# This is the network Traefik listens to, if your network has a different
# name, don't forget to change it here and in the docker-compose.override.yml
proxy:
external: true
proxy:

@ -1,9 +1,10 @@
use crate::{utils, Error, Result};
use bytes::BytesMut;
use ruma::api::{IncomingResponse, OutgoingRequest, SendAccessToken};
use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
use std::{fmt::Debug, mem, time::Duration};
use tracing::warn;
#[tracing::instrument(skip(globals, request))]
pub(crate) async fn send_request<T: OutgoingRequest>(
globals: &crate::database::globals::Globals,
registration: serde_yaml::Value,
@ -16,7 +17,11 @@ where
let hs_token = registration.get("hs_token").unwrap().as_str().unwrap();
let mut http_request = request
.try_into_http_request::<BytesMut>(destination, SendAccessToken::IfRequired(""))
.try_into_http_request::<BytesMut>(
destination,
SendAccessToken::IfRequired(""),
&[MatrixVersion::V1_0],
)
.unwrap()
.map(|body| body.freeze());

@ -1,36 +1,25 @@
use std::sync::Arc;
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
use crate::{
database::{admin::make_user_admin, DatabaseGuard},
pdu::PduBuilder,
utils, ConduitResult, Error, Ruma,
utils, Error, Result, Ruma,
};
use ruma::{
api::client::{
error::ErrorKind,
r0::{
account::{
change_password, deactivate, get_3pids, get_username_availability, register,
whoami, ThirdPartyIdRemovalStatus,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
account::{
change_password, deactivate, get_3pids, get_username_availability, register, whoami,
ThirdPartyIdRemovalStatus,
},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{
room::member::{MembershipState, RoomMemberEventContent},
EventType,
},
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
push, UserId,
};
use serde_json::value::to_raw_value;
use tracing::{info, warn};
use register::RegistrationKind;
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
const GUEST_NAME_LENGTH: usize = 10;
const RANDOM_USER_ID_LENGTH: usize = 10;
/// # `GET /_matrix/client/r0/register/available`
///
@ -42,15 +31,10 @@ const GUEST_NAME_LENGTH: usize = 10;
/// - No user or appservice on this server already claimed this username
///
/// Note: This will not reserve the username, so the username might become invalid when trying to register
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/register/available", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_register_available_route(
db: DatabaseGuard,
body: Ruma<get_username_availability::Request<'_>>,
) -> ConduitResult<get_username_availability::Response> {
body: Ruma<get_username_availability::v3::IncomingRequest>,
) -> Result<get_username_availability::v3::Response> {
// Validate user id
let user_id =
UserId::parse_with_server_name(body.username.to_lowercase(), db.globals.server_name())
@ -74,7 +58,7 @@ pub async fn get_register_available_route(
// TODO add check for appservice namespaces
// If no if check is true we have an username that's available to be used.
Ok(get_username_availability::Response { available: true }.into())
Ok(get_username_availability::v3::Response { available: true })
}
/// # `POST /_matrix/client/r0/register`
@ -90,15 +74,10 @@ pub async fn get_register_available_route(
/// - If type is not guest and no username is given: Always fails after UIAA check
/// - Creates a new account and populates it with default account data
/// - If `inhibit_login` is false: Creates a device and returns device id and access_token
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/register", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn register_route(
db: DatabaseGuard,
body: Ruma<register::Request<'_>>,
) -> ConduitResult<register::Response> {
body: Ruma<register::v3::IncomingRequest>,
) -> Result<register::v3::Response> {
if !db.globals.allow_registration() && !body.from_appservice {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
@ -108,38 +87,38 @@ pub async fn register_route(
let is_guest = body.kind == RegistrationKind::Guest;
let mut missing_username = false;
// Validate user id
let user_id = UserId::parse_with_server_name(
if is_guest {
utils::random_string(GUEST_NAME_LENGTH)
} else {
body.username.clone().unwrap_or_else(|| {
// If the user didn't send a username field, that means the client is just trying
// the get an UIAA error to see available flows
missing_username = true;
// Just give the user a random name. He won't be able to register with it anyway.
utils::random_string(GUEST_NAME_LENGTH)
})
let user_id = match (&body.username, is_guest) {
(Some(username), false) => {
let proposed_user_id =
UserId::parse_with_server_name(username.to_lowercase(), db.globals.server_name())
.ok()
.filter(|user_id| {
!user_id.is_historical()
&& user_id.server_name() == db.globals.server_name()
})
.ok_or(Error::BadRequest(
ErrorKind::InvalidUsername,
"Username is invalid.",
))?;
if db.users.exists(&proposed_user_id)? {
return Err(Error::BadRequest(
ErrorKind::UserInUse,
"Desired user ID is already taken.",
));
}
proposed_user_id
}
.to_lowercase(),
db.globals.server_name(),
)
.ok()
.filter(|user_id| !user_id.is_historical() && user_id.server_name() == db.globals.server_name())
.ok_or(Error::BadRequest(
ErrorKind::InvalidUsername,
"Username is invalid.",
))?;
// Check if username is creative enough
if db.users.exists(&user_id)? {
return Err(Error::BadRequest(
ErrorKind::UserInUse,
"Desired user ID is already taken.",
));
}
_ => loop {
let proposed_user_id = UserId::parse_with_server_name(
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
db.globals.server_name(),
)
.unwrap();
if !db.users.exists(&proposed_user_id)? {
break proposed_user_id;
}
},
};
// UIAA
let mut uiaainfo = UiaaInfo {
@ -182,13 +161,6 @@ pub async fn register_route(
}
}
if missing_username {
return Err(Error::BadRequest(
ErrorKind::MissingParam,
"Missing username field.",
));
}
let password = if is_guest {
None
} else {
@ -199,7 +171,13 @@ pub async fn register_route(
db.users.create(&user_id, password)?;
// Default to pretty displayname
let displayname = format!("{} ⚡️", user_id.localpart());
let mut displayname = user_id.localpart().to_owned();
// If enabled append lightning bolt to display name (default true)
if db.globals.enable_lightning_bolt() {
displayname.push_str(" ⚡️");
}
db.users
.set_displayname(&user_id, Some(displayname.clone()))?;
@ -207,7 +185,7 @@ pub async fn register_route(
db.account_data.update(
None,
&user_id,
EventType::PushRules,
GlobalAccountDataEventType::PushRules.to_string().into(),
&ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: push::Ruleset::server_default(&user_id),
@ -218,12 +196,11 @@ pub async fn register_route(
// Inhibit login does not work for guests
if !is_guest && body.inhibit_login {
return Ok(register::Response {
return Ok(register::v3::Response {
access_token: None,
user_id,
device_id: None,
}
.into());
});
}
// Generate new device id if the user didn't specify one
@ -245,7 +222,12 @@ pub async fn register_route(
body.initial_device_display_name.clone(),
)?;
info!("{} registered on this server", user_id);
info!("New user {} registered on this server.", user_id);
db.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user {} registered on this server.",
user_id
)));
// If this is the first real user, grant them admin privileges
// Note: the server user, @conduit:servername, is generated first
@ -257,12 +239,11 @@ pub async fn register_route(
db.flush()?;
Ok(register::Response {
Ok(register::v3::Response {
access_token: Some(token),
user_id,
device_id: Some(device_id),
}
.into())
})
}
/// # `POST /_matrix/client/r0/account/password`
@ -279,15 +260,10 @@ pub async fn register_route(
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/account/password", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn change_password_route(
db: DatabaseGuard,
body: Ruma<change_password::Request<'_>>,
) -> ConduitResult<change_password::Response> {
body: Ruma<change_password::v3::IncomingRequest>,
) -> Result<change_password::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -340,7 +316,14 @@ pub async fn change_password_route(
db.flush()?;
Ok(change_password::Response {}.into())
info!("User {} changed their password.", sender_user);
db.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"User {} changed their password.",
sender_user
)));
Ok(change_password::v3::Response {})
}
/// # `GET _matrix/client/r0/account/whoami`
@ -348,17 +331,18 @@ pub async fn change_password_route(
/// Get user_id of the sender user.
///
/// Note: Also works for Application Services
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/account/whoami", data = "<body>")
)]
#[tracing::instrument(skip(body))]
pub async fn whoami_route(body: Ruma<whoami::Request>) -> ConduitResult<whoami::Response> {
pub async fn whoami_route(
db: DatabaseGuard,
body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(whoami::Response {
let device_id = body.sender_device.as_ref().cloned();
Ok(whoami::v3::Response {
user_id: sender_user.clone(),
}
.into())
device_id,
is_guest: db.users.is_deactivated(&sender_user)?,
})
}
/// # `POST /_matrix/client/r0/account/deactivate`
@ -371,15 +355,10 @@ pub async fn whoami_route(body: Ruma<whoami::Request>) -> ConduitResult<whoami::
/// - Forgets all to-device events
/// - Triggers device list updates
/// - Removes ability to log in again
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/account/deactivate", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn deactivate_route(
db: DatabaseGuard,
body: Ruma<deactivate::Request<'_>>,
) -> ConduitResult<deactivate::Response> {
body: Ruma<deactivate::v3::IncomingRequest>,
) -> Result<deactivate::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -415,67 +394,24 @@ pub async fn deactivate_route(
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
}
// Leave all joined rooms and reject all invitations
// TODO: work over federation invites
let all_rooms = db
.rooms
.rooms_joined(sender_user)
.chain(
db.rooms
.rooms_invited(sender_user)
.map(|t| t.map(|(r, _)| r)),
)
.collect::<Vec<_>>();
for room_id in all_rooms {
let room_id = room_id?;
let event = RoomMemberEventContent {
membership: MembershipState::Leave,
displayname: None,
avatar_url: None,
is_direct: None,
third_party_invite: None,
blurhash: None,
reason: None,
join_authorized_via_users_server: None,
};
let mutex_state = Arc::clone(
db.globals
.roomid_mutex_state
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
redacts: None,
},
sender_user,
&room_id,
&db,
&state_lock,
)?;
}
// Make the user leave all rooms before deactivation
db.rooms.leave_all_rooms(&sender_user, &db).await?;
// Remove devices and mark account as deactivated
db.users.deactivate_account(sender_user)?;
info!("{} deactivated their account", sender_user);
info!("User {} deactivated their account.", sender_user);
db.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"User {} deactivated their account.",
sender_user
)));
db.flush()?;
Ok(deactivate::Response {
Ok(deactivate::v3::Response {
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
}
.into())
})
}
/// # `GET _matrix/client/r0/account/3pid`
@ -483,14 +419,10 @@ pub async fn deactivate_route(
/// Get a list of third party identifiers associated with this account.
///
/// - Currently always returns empty list
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/account/3pid", data = "<body>")
)]
pub async fn third_party_route(
body: Ruma<get_3pids::Request>,
) -> ConduitResult<get_3pids::Response> {
body: Ruma<get_3pids::v3::Request>,
) -> Result<get_3pids::v3::Response> {
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(get_3pids::Response::new(Vec::new()).into())
Ok(get_3pids::v3::Response::new(Vec::new()))
}

@ -1,32 +1,24 @@
use crate::{database::DatabaseGuard, ConduitResult, Database, Error, Ruma};
use crate::{database::DatabaseGuard, Database, Error, Result, Ruma};
use regex::Regex;
use ruma::{
api::{
appservice,
client::{
alias::{create_alias, delete_alias, get_alias},
error::ErrorKind,
r0::alias::{create_alias, delete_alias, get_alias},
},
federation,
},
RoomAliasId,
};
#[cfg(feature = "conduit_bin")]
use rocket::{delete, get, put};
/// # `PUT /_matrix/client/r0/directory/room/{roomAlias}`
///
/// Creates a new room alias on this server.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/directory/room/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn create_alias_route(
db: DatabaseGuard,
body: Ruma<create_alias::Request<'_>>,
) -> ConduitResult<create_alias::Response> {
body: Ruma<create_alias::v3::IncomingRequest>,
) -> Result<create_alias::v3::Response> {
if body.room_alias.server_name() != db.globals.server_name() {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
@ -43,7 +35,7 @@ pub async fn create_alias_route(
db.flush()?;
Ok(create_alias::Response::new().into())
Ok(create_alias::v3::Response::new())
}
/// # `DELETE /_matrix/client/r0/directory/room/{roomAlias}`
@ -52,15 +44,10 @@ pub async fn create_alias_route(
///
/// - TODO: additional access control checks
/// - TODO: Update canonical alias event
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/r0/directory/room/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_alias_route(
db: DatabaseGuard,
body: Ruma<delete_alias::Request<'_>>,
) -> ConduitResult<delete_alias::Response> {
body: Ruma<delete_alias::v3::IncomingRequest>,
) -> Result<delete_alias::v3::Response> {
if body.room_alias.server_name() != db.globals.server_name() {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
@ -74,7 +61,7 @@ pub async fn delete_alias_route(
db.flush()?;
Ok(delete_alias::Response::new().into())
Ok(delete_alias::v3::Response::new())
}
/// # `GET /_matrix/client/r0/directory/room/{roomAlias}`
@ -82,22 +69,17 @@ pub async fn delete_alias_route(
/// Resolve an alias locally or over federation.
///
/// - TODO: Suggest more servers to join via
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/directory/room/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_alias_route(
db: DatabaseGuard,
body: Ruma<get_alias::Request<'_>>,
) -> ConduitResult<get_alias::Response> {
body: Ruma<get_alias::v3::IncomingRequest>,
) -> Result<get_alias::v3::Response> {
get_alias_helper(&db, &body.room_alias).await
}
pub(crate) async fn get_alias_helper(
db: &Database,
room_alias: &RoomAliasId,
) -> ConduitResult<get_alias::Response> {
) -> Result<get_alias::v3::Response> {
if room_alias.server_name() != db.globals.server_name() {
let response = db
.sending
@ -108,7 +90,10 @@ pub(crate) async fn get_alias_helper(
)
.await?;
return Ok(get_alias::Response::new(response.room_id, response.servers).into());
return Ok(get_alias::v3::Response::new(
response.room_id,
response.servers,
));
}
let mut room_id = None;
@ -159,5 +144,8 @@ pub(crate) async fn get_alias_helper(
}
};
Ok(get_alias::Response::new(room_id, vec![db.globals.server_name().to_owned()]).into())
Ok(get_alias::v3::Response::new(
room_id,
vec![db.globals.server_name().to_owned()],
))
}

@ -1,29 +1,22 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::api::client::{
error::ErrorKind,
r0::backup::{
add_backup_key_session, add_backup_key_sessions, add_backup_keys, create_backup,
delete_backup, delete_backup_key_session, delete_backup_key_sessions, delete_backup_keys,
get_backup, get_backup_key_session, get_backup_key_sessions, get_backup_keys,
get_latest_backup, update_backup,
backup::{
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session,
create_backup_version, delete_backup_keys, delete_backup_keys_for_room,
delete_backup_keys_for_session, delete_backup_version, get_backup_info, get_backup_keys,
get_backup_keys_for_room, get_backup_keys_for_session, get_latest_backup_info,
update_backup_version,
},
error::ErrorKind,
};
#[cfg(feature = "conduit_bin")]
use rocket::{delete, get, post, put};
/// # `POST /_matrix/client/r0/room_keys/version`
///
/// Creates a new backup.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/unstable/room_keys/version", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn create_backup_route(
pub async fn create_backup_version_route(
db: DatabaseGuard,
body: Ruma<create_backup::Request>,
) -> ConduitResult<create_backup::Response> {
body: Ruma<create_backup_version::v3::Request>,
) -> Result<create_backup_version::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let version = db
.key_backups
@ -31,42 +24,32 @@ pub async fn create_backup_route(
db.flush()?;
Ok(create_backup::Response { version }.into())
Ok(create_backup_version::v3::Response { version })
}
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
///
/// Update information about an existing backup. Only `auth_data` can be modified.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/unstable/room_keys/version/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn update_backup_route(
pub async fn update_backup_version_route(
db: DatabaseGuard,
body: Ruma<update_backup::Request<'_>>,
) -> ConduitResult<update_backup::Response> {
body: Ruma<update_backup_version::v3::IncomingRequest>,
) -> Result<update_backup_version::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.key_backups
.update_backup(sender_user, &body.version, &body.algorithm, &db.globals)?;
db.flush()?;
Ok(update_backup::Response {}.into())
Ok(update_backup_version::v3::Response {})
}
/// # `GET /_matrix/client/r0/room_keys/version`
///
/// Get information about the latest backup version.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/unstable/room_keys/version", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_latest_backup_route(
pub async fn get_latest_backup_info_route(
db: DatabaseGuard,
body: Ruma<get_latest_backup::Request>,
) -> ConduitResult<get_latest_backup::Response> {
body: Ruma<get_latest_backup_info::v3::Request>,
) -> Result<get_latest_backup_info::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let (version, algorithm) =
@ -77,27 +60,21 @@ pub async fn get_latest_backup_route(
"Key backup does not exist.",
))?;
Ok(get_latest_backup::Response {
Ok(get_latest_backup_info::v3::Response {
algorithm,
count: (db.key_backups.count_keys(sender_user, &version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &version)?,
version,
}
.into())
})
}
/// # `GET /_matrix/client/r0/room_keys/version`
///
/// Get information about an existing backup.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/unstable/room_keys/version/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_backup_route(
pub async fn get_backup_info_route(
db: DatabaseGuard,
body: Ruma<get_backup::Request<'_>>,
) -> ConduitResult<get_backup::Response> {
body: Ruma<get_backup_info::v3::IncomingRequest>,
) -> Result<get_backup_info::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let algorithm = db
.key_backups
@ -107,13 +84,12 @@ pub async fn get_backup_route(
"Key backup does not exist.",
))?;
Ok(get_backup::Response {
Ok(get_backup_info::v3::Response {
algorithm,
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &body.version)?,
version: body.version.to_owned(),
}
.into())
})
}
/// # `DELETE /_matrix/client/r0/room_keys/version/{version}`
@ -121,22 +97,17 @@ pub async fn get_backup_route(
/// Delete an existing key backup.
///
/// - Deletes both information about the backup, as well as all key data related to the backup
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/unstable/room_keys/version/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_backup_route(
pub async fn delete_backup_version_route(
db: DatabaseGuard,
body: Ruma<delete_backup::Request<'_>>,
) -> ConduitResult<delete_backup::Response> {
body: Ruma<delete_backup_version::v3::IncomingRequest>,
) -> Result<delete_backup_version::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.key_backups.delete_backup(sender_user, &body.version)?;
db.flush()?;
Ok(delete_backup::Response {}.into())
Ok(delete_backup_version::v3::Response {})
}
/// # `PUT /_matrix/client/r0/room_keys/keys`
@ -146,15 +117,10 @@ pub async fn delete_backup_route(
/// - Only manipulating the most recently created version of the backup is allowed
/// - Adds the keys to the backup
/// - Returns the new number of keys in this backup and the etag
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/unstable/room_keys/keys", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn add_backup_keys_route(
db: DatabaseGuard,
body: Ruma<add_backup_keys::Request<'_>>,
) -> ConduitResult<add_backup_keys::Response> {
body: Ruma<add_backup_keys::v3::IncomingRequest>,
) -> Result<add_backup_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if Some(&body.version)
@ -184,11 +150,10 @@ pub async fn add_backup_keys_route(
db.flush()?;
Ok(add_backup_keys::Response {
Ok(add_backup_keys::v3::Response {
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &body.version)?,
}
.into())
})
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
@ -198,15 +163,10 @@ pub async fn add_backup_keys_route(
/// - Only manipulating the most recently created version of the backup is allowed
/// - Adds the keys to the backup
/// - Returns the new number of keys in this backup and the etag
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn add_backup_key_sessions_route(
pub async fn add_backup_keys_for_room_route(
db: DatabaseGuard,
body: Ruma<add_backup_key_sessions::Request<'_>>,
) -> ConduitResult<add_backup_key_sessions::Response> {
body: Ruma<add_backup_keys_for_room::v3::IncomingRequest>,
) -> Result<add_backup_keys_for_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if Some(&body.version)
@ -234,11 +194,10 @@ pub async fn add_backup_key_sessions_route(
db.flush()?;
Ok(add_backup_key_sessions::Response {
Ok(add_backup_keys_for_room::v3::Response {
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &body.version)?,
}
.into())
})
}
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
@ -248,15 +207,10 @@ pub async fn add_backup_key_sessions_route(
/// - Only manipulating the most recently created version of the backup is allowed
/// - Adds the keys to the backup
/// - Returns the new number of keys in this backup and the etag
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn add_backup_key_session_route(
pub async fn add_backup_keys_for_session_route(
db: DatabaseGuard,
body: Ruma<add_backup_key_session::Request<'_>>,
) -> ConduitResult<add_backup_key_session::Response> {
body: Ruma<add_backup_keys_for_session::v3::IncomingRequest>,
) -> Result<add_backup_keys_for_session::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if Some(&body.version)
@ -282,65 +236,49 @@ pub async fn add_backup_key_session_route(
db.flush()?;
Ok(add_backup_key_session::Response {
Ok(add_backup_keys_for_session::v3::Response {
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &body.version)?,
}
.into())
})
}
/// # `GET /_matrix/client/r0/room_keys/keys`
///
/// Retrieves all keys from the backup.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/unstable/room_keys/keys", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_backup_keys_route(
db: DatabaseGuard,
body: Ruma<get_backup_keys::Request<'_>>,
) -> ConduitResult<get_backup_keys::Response> {
body: Ruma<get_backup_keys::v3::IncomingRequest>,
) -> Result<get_backup_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let rooms = db.key_backups.get_all(sender_user, &body.version)?;
Ok(get_backup_keys::Response { rooms }.into())
Ok(get_backup_keys::v3::Response { rooms })
}
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
///
/// Retrieves all keys from the backup for a given room.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_backup_key_sessions_route(
pub async fn get_backup_keys_for_room_route(
db: DatabaseGuard,
body: Ruma<get_backup_key_sessions::Request<'_>>,
) -> ConduitResult<get_backup_key_sessions::Response> {
body: Ruma<get_backup_keys_for_room::v3::IncomingRequest>,
) -> Result<get_backup_keys_for_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sessions = db
.key_backups
.get_room(sender_user, &body.version, &body.room_id)?;
Ok(get_backup_key_sessions::Response { sessions }.into())
Ok(get_backup_keys_for_room::v3::Response { sessions })
}
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
///
/// Retrieves a key from the backup.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_backup_key_session_route(
pub async fn get_backup_keys_for_session_route(
db: DatabaseGuard,
body: Ruma<get_backup_key_session::Request<'_>>,
) -> ConduitResult<get_backup_key_session::Response> {
body: Ruma<get_backup_keys_for_session::v3::IncomingRequest>,
) -> Result<get_backup_keys_for_session::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let key_data = db
@ -351,46 +289,35 @@ pub async fn get_backup_key_session_route(
"Backup key not found for this user's session.",
))?;
Ok(get_backup_key_session::Response { key_data }.into())
Ok(get_backup_keys_for_session::v3::Response { key_data })
}
/// # `DELETE /_matrix/client/r0/room_keys/keys`
///
/// Delete the keys from the backup.
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/unstable/room_keys/keys", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_backup_keys_route(
db: DatabaseGuard,
body: Ruma<delete_backup_keys::Request<'_>>,
) -> ConduitResult<delete_backup_keys::Response> {
body: Ruma<delete_backup_keys::v3::IncomingRequest>,
) -> Result<delete_backup_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.key_backups.delete_all_keys(sender_user, &body.version)?;
db.flush()?;
Ok(delete_backup_keys::Response {
Ok(delete_backup_keys::v3::Response {
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &body.version)?,
}
.into())
})
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
///
/// Delete the keys from the backup for a given room.
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_backup_key_sessions_route(
pub async fn delete_backup_keys_for_room_route(
db: DatabaseGuard,
body: Ruma<delete_backup_key_sessions::Request<'_>>,
) -> ConduitResult<delete_backup_key_sessions::Response> {
body: Ruma<delete_backup_keys_for_room::v3::IncomingRequest>,
) -> Result<delete_backup_keys_for_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.key_backups
@ -398,25 +325,19 @@ pub async fn delete_backup_key_sessions_route(
db.flush()?;
Ok(delete_backup_key_sessions::Response {
Ok(delete_backup_keys_for_room::v3::Response {
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &body.version)?,
}
.into())
})
}
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
///
/// Delete a key from the backup.
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_backup_key_session_route(
pub async fn delete_backup_keys_for_session_route(
db: DatabaseGuard,
body: Ruma<delete_backup_key_session::Request<'_>>,
) -> ConduitResult<delete_backup_key_session::Response> {
body: Ruma<delete_backup_keys_for_session::v3::IncomingRequest>,
) -> Result<delete_backup_keys_for_session::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.key_backups
@ -424,9 +345,8 @@ pub async fn delete_backup_key_session_route(
db.flush()?;
Ok(delete_backup_key_session::Response {
Ok(delete_backup_keys_for_session::v3::Response {
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
etag: db.key_backups.get_etag(sender_user, &body.version)?,
}
.into())
})
}

@ -1,35 +1,35 @@
use crate::{ConduitResult, Ruma};
use ruma::{
api::client::r0::capabilities::{
get_capabilities, Capabilities, RoomVersionStability, RoomVersionsCapability,
},
RoomVersionId,
use crate::{database::DatabaseGuard, Result, Ruma};
use ruma::api::client::discovery::get_capabilities::{
self, Capabilities, RoomVersionStability, RoomVersionsCapability,
};
use std::collections::BTreeMap;
#[cfg(feature = "conduit_bin")]
use rocket::get;
/// # `GET /_matrix/client/r0/capabilities`
///
/// Get information on the supported feature set and other relevent capabilities of this server.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/capabilities", data = "<_body>")
)]
#[tracing::instrument(skip(_body))]
pub async fn get_capabilities_route(
_body: Ruma<get_capabilities::Request>,
) -> ConduitResult<get_capabilities::Response> {
db: DatabaseGuard,
_body: Ruma<get_capabilities::v3::IncomingRequest>,
) -> Result<get_capabilities::v3::Response> {
let mut available = BTreeMap::new();
available.insert(RoomVersionId::V5, RoomVersionStability::Stable);
available.insert(RoomVersionId::V6, RoomVersionStability::Stable);
if db.globals.allow_unstable_room_versions() {
for room_version in &db.globals.unstable_room_versions {
available.insert(room_version.clone(), RoomVersionStability::Stable);
}
} else {
for room_version in &db.globals.unstable_room_versions {
available.insert(room_version.clone(), RoomVersionStability::Unstable);
}
}
for room_version in &db.globals.stable_room_versions {
available.insert(room_version.clone(), RoomVersionStability::Stable);
}
let mut capabilities = Capabilities::new();
capabilities.room_versions = RoomVersionsCapability {
default: RoomVersionId::V6,
default: db.globals.default_room_version(),
available,
};
Ok(get_capabilities::Response { capabilities }.into())
Ok(get_capabilities::v3::Response { capabilities })
}

@ -1,11 +1,11 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::{
api::client::{
error::ErrorKind,
r0::config::{
config::{
get_global_account_data, get_room_account_data, set_global_account_data,
set_room_account_data,
},
error::ErrorKind,
},
events::{AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent},
serde::Raw,
@ -13,24 +13,16 @@ use ruma::{
use serde::Deserialize;
use serde_json::{json, value::RawValue as RawJsonValue};
#[cfg(feature = "conduit_bin")]
use rocket::{get, put};
/// # `PUT /_matrix/client/r0/user/{userId}/account_data/{type}`
///
/// Sets some account data for the sender user.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/user/<_>/account_data/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_global_account_data_route(
db: DatabaseGuard,
body: Ruma<set_global_account_data::Request<'_>>,
) -> ConduitResult<set_global_account_data::Response> {
body: Ruma<set_global_account_data::v3::IncomingRequest>,
) -> Result<set_global_account_data::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let data: serde_json::Value = serde_json::from_str(body.data.get())
let data: serde_json::Value = serde_json::from_str(body.data.json().get())
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?;
let event_type = body.event_type.to_string();
@ -48,27 +40,19 @@ pub async fn set_global_account_data_route(
db.flush()?;
Ok(set_global_account_data::Response {}.into())
Ok(set_global_account_data::v3::Response {})
}
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
///
/// Sets some room account data for the sender user.
#[cfg_attr(
feature = "conduit_bin",
put(
"/_matrix/client/r0/user/<_>/rooms/<_>/account_data/<_>",
data = "<body>"
)
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_room_account_data_route(
db: DatabaseGuard,
body: Ruma<set_room_account_data::Request<'_>>,
) -> ConduitResult<set_room_account_data::Response> {
body: Ruma<set_room_account_data::v3::IncomingRequest>,
) -> Result<set_room_account_data::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let data: serde_json::Value = serde_json::from_str(body.data.get())
let data: serde_json::Value = serde_json::from_str(body.data.json().get())
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?;
let event_type = body.event_type.to_string();
@ -86,21 +70,16 @@ pub async fn set_room_account_data_route(
db.flush()?;
Ok(set_room_account_data::Response {}.into())
Ok(set_room_account_data::v3::Response {})
}
/// # `GET /_matrix/client/r0/user/{userId}/account_data/{type}`
///
/// Gets some account data for the sender user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/user/<_>/account_data/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_global_account_data_route(
db: DatabaseGuard,
body: Ruma<get_global_account_data::Request<'_>>,
) -> ConduitResult<get_global_account_data::Response> {
body: Ruma<get_global_account_data::v3::IncomingRequest>,
) -> Result<get_global_account_data::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event: Box<RawJsonValue> = db
@ -112,24 +91,16 @@ pub async fn get_global_account_data_route(
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
.content;
Ok(get_global_account_data::Response { account_data }.into())
Ok(get_global_account_data::v3::Response { account_data })
}
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
///
/// Gets some room account data for the sender user.
#[cfg_attr(
feature = "conduit_bin",
get(
"/_matrix/client/r0/user/<_>/rooms/<_>/account_data/<_>",
data = "<body>"
)
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_room_account_data_route(
db: DatabaseGuard,
body: Ruma<get_room_account_data::Request<'_>>,
) -> ConduitResult<get_room_account_data::Response> {
body: Ruma<get_room_account_data::v3::IncomingRequest>,
) -> Result<get_room_account_data::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event: Box<RawJsonValue> = db
@ -145,7 +116,7 @@ pub async fn get_room_account_data_route(
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
.content;
Ok(get_room_account_data::Response { account_data }.into())
Ok(get_room_account_data::v3::Response { account_data })
}
#[derive(Deserialize)]

@ -1,42 +1,28 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::{
api::client::{
error::ErrorKind,
r0::{context::get_context, filter::LazyLoadOptions},
},
events::EventType,
api::client::{context::get_context, error::ErrorKind, filter::LazyLoadOptions},
events::StateEventType,
};
use std::{collections::HashSet, convert::TryFrom};
use tracing::error;
#[cfg(feature = "conduit_bin")]
use rocket::get;
/// # `GET /_matrix/client/r0/rooms/{roomId}/context`
///
/// Allows loading room history around an event.
///
/// - Only works if the user is joined (TODO: always allow, but only show events if the user was
/// joined, depending on history_visibility)
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/context/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_context_route(
db: DatabaseGuard,
body: Ruma<get_context::Request<'_>>,
) -> ConduitResult<get_context::Response> {
body: Ruma<get_context::v3::IncomingRequest>,
) -> Result<get_context::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
// Load filter
let filter = body.filter.clone().unwrap_or_default();
let (lazy_load_enabled, lazy_load_send_redundant) = match filter.lazy_load_options {
let (lazy_load_enabled, lazy_load_send_redundant) = match &body.filter.lazy_load_options {
LazyLoadOptions::Enabled {
include_redundant_members: redundant,
} => (true, redundant),
include_redundant_members,
} => (true, *include_redundant_members),
_ => (false, false),
};
@ -151,7 +137,7 @@ pub async fn get_context_route(
.expect("All rooms have state"),
};
let state_ids = db.rooms.state_full_ids(shortstatehash)?;
let state_ids = db.rooms.state_full_ids(shortstatehash).await?;
let end_token = events_after
.last()
@ -168,7 +154,7 @@ pub async fn get_context_route(
for (shortstatekey, id) in state_ids {
let (event_type, state_key) = db.rooms.get_statekey_from_short(shortstatekey)?;
if event_type != EventType::RoomMember {
if event_type != StateEventType::RoomMember {
let pdu = match db.rooms.get_pdu(&id)? {
Some(pdu) => pdu,
None => {
@ -189,7 +175,7 @@ pub async fn get_context_route(
}
}
let resp = get_context::Response {
let resp = get_context::v3::Response {
start: start_token,
end: end_token,
events_before,
@ -198,5 +184,5 @@ pub async fn get_context_route(
state,
};
Ok(resp.into())
Ok(resp)
}

@ -1,28 +1,19 @@
use crate::{database::DatabaseGuard, utils, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, utils, Error, Result, Ruma};
use ruma::api::client::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
error::ErrorKind,
r0::{
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
};
use super::SESSION_ID_LENGTH;
#[cfg(feature = "conduit_bin")]
use rocket::{delete, get, post, put};
/// # `GET /_matrix/client/r0/devices`
///
/// Get metadata on all devices of the sender user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/devices", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_devices_route(
db: DatabaseGuard,
body: Ruma<get_devices::Request>,
) -> ConduitResult<get_devices::Response> {
body: Ruma<get_devices::v3::Request>,
) -> Result<get_devices::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let devices: Vec<device::Device> = db
@ -31,21 +22,16 @@ pub async fn get_devices_route(
.filter_map(|r| r.ok()) // Filter out buggy devices
.collect();
Ok(get_devices::Response { devices }.into())
Ok(get_devices::v3::Response { devices })
}
/// # `GET /_matrix/client/r0/devices/{deviceId}`
///
/// Get metadata on a single device of the sender user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/devices/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_device_route(
db: DatabaseGuard,
body: Ruma<get_device::Request<'_>>,
) -> ConduitResult<get_device::Response> {
body: Ruma<get_device::v3::IncomingRequest>,
) -> Result<get_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let device = db
@ -53,21 +39,16 @@ pub async fn get_device_route(
.get_device_metadata(sender_user, &body.body.device_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
Ok(get_device::Response { device }.into())
Ok(get_device::v3::Response { device })
}
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
///
/// Updates the metadata on a given device of the sender user.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/devices/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn update_device_route(
db: DatabaseGuard,
body: Ruma<update_device::Request<'_>>,
) -> ConduitResult<update_device::Response> {
body: Ruma<update_device::v3::IncomingRequest>,
) -> Result<update_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut device = db
@ -82,7 +63,7 @@ pub async fn update_device_route(
db.flush()?;
Ok(update_device::Response {}.into())
Ok(update_device::v3::Response {})
}
/// # `DELETE /_matrix/client/r0/devices/{deviceId}`
@ -94,15 +75,10 @@ pub async fn update_device_route(
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/r0/devices/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_device_route(
db: DatabaseGuard,
body: Ruma<delete_device::Request<'_>>,
) -> ConduitResult<delete_device::Response> {
body: Ruma<delete_device::v3::IncomingRequest>,
) -> Result<delete_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -143,7 +119,7 @@ pub async fn delete_device_route(
db.flush()?;
Ok(delete_device::Response {}.into())
Ok(delete_device::v3::Response {})
}
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
@ -157,15 +133,10 @@ pub async fn delete_device_route(
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/delete_devices", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_devices_route(
db: DatabaseGuard,
body: Ruma<delete_devices::Request<'_>>,
) -> ConduitResult<delete_devices::Response> {
body: Ruma<delete_devices::v3::IncomingRequest>,
) -> Result<delete_devices::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -208,5 +179,5 @@ pub async fn delete_devices_route(
db.flush()?;
Ok(delete_devices::Response {}.into())
Ok(delete_devices::v3::Response {})
}

@ -1,51 +1,45 @@
use crate::{database::DatabaseGuard, ConduitResult, Database, Error, Result, Ruma};
use crate::{database::DatabaseGuard, Database, Error, Result, Ruma};
use ruma::{
api::{
client::{
error::ErrorKind,
r0::{
directory::{
get_public_rooms, get_public_rooms_filtered, get_room_visibility,
set_room_visibility,
},
room,
directory::{
get_public_rooms, get_public_rooms_filtered, get_room_visibility,
set_room_visibility,
},
error::ErrorKind,
room,
},
federation,
},
directory::{Filter, IncomingFilter, IncomingRoomNetwork, PublicRoomsChunk, RoomNetwork},
directory::{
Filter, IncomingFilter, IncomingRoomNetwork, PublicRoomJoinRule, PublicRoomsChunk,
RoomNetwork,
},
events::{
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
name::RoomNameEventContent,
topic::RoomTopicEventContent,
},
EventType,
StateEventType,
},
ServerName, UInt,
};
use tracing::{info, warn};
#[cfg(feature = "conduit_bin")]
use rocket::{get, post, put};
/// # `POST /_matrix/client/r0/publicRooms`
///
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/publicRooms", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_public_rooms_filtered_route(
db: DatabaseGuard,
body: Ruma<get_public_rooms_filtered::Request<'_>>,
) -> ConduitResult<get_public_rooms_filtered::Response> {
body: Ruma<get_public_rooms_filtered::v3::IncomingRequest>,
) -> Result<get_public_rooms_filtered::v3::Response> {
get_public_rooms_filtered_helper(
&db,
body.server.as_deref(),
@ -62,15 +56,10 @@ pub async fn get_public_rooms_filtered_route(
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/publicRooms", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_public_rooms_route(
db: DatabaseGuard,
body: Ruma<get_public_rooms::Request<'_>>,
) -> ConduitResult<get_public_rooms::Response> {
body: Ruma<get_public_rooms::v3::IncomingRequest>,
) -> Result<get_public_rooms::v3::Response> {
let response = get_public_rooms_filtered_helper(
&db,
body.server.as_deref(),
@ -79,16 +68,14 @@ pub async fn get_public_rooms_route(
&IncomingFilter::default(),
&IncomingRoomNetwork::Matrix,
)
.await?
.0;
.await?;
Ok(get_public_rooms::Response {
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,
}
.into())
})
}
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
@ -96,15 +83,10 @@ pub async fn get_public_rooms_route(
/// Sets the visibility of a given room in the room directory.
///
/// - TODO: Access control checks
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/directory/list/room/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_room_visibility_route(
db: DatabaseGuard,
body: Ruma<set_room_visibility::Request<'_>>,
) -> ConduitResult<set_room_visibility::Response> {
body: Ruma<set_room_visibility::v3::IncomingRequest>,
) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
match &body.visibility {
@ -123,29 +105,23 @@ pub async fn set_room_visibility_route(
db.flush()?;
Ok(set_room_visibility::Response {}.into())
Ok(set_room_visibility::v3::Response {})
}
/// # `GET /_matrix/client/r0/directory/list/room/{roomId}`
///
/// Gets the visibility of a given room in the room directory.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/directory/list/room/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_room_visibility_route(
db: DatabaseGuard,
body: Ruma<get_room_visibility::Request<'_>>,
) -> ConduitResult<get_room_visibility::Response> {
Ok(get_room_visibility::Response {
body: Ruma<get_room_visibility::v3::IncomingRequest>,
) -> Result<get_room_visibility::v3::Response> {
Ok(get_room_visibility::v3::Response {
visibility: if db.rooms.is_public_room(&body.room_id)? {
room::Visibility::Public
} else {
room::Visibility::Private
},
}
.into())
})
}
pub(crate) async fn get_public_rooms_filtered_helper(
@ -155,7 +131,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
since: Option<&str>,
filter: &IncomingFilter,
_network: &IncomingRoomNetwork,
) -> ConduitResult<get_public_rooms_filtered::Response> {
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(other_server) = server.filter(|server| *server != db.globals.server_name().as_str())
{
let response = db
@ -174,25 +150,12 @@ pub(crate) async fn get_public_rooms_filtered_helper(
)
.await?;
return Ok(get_public_rooms_filtered::Response {
chunk: response
.chunk
.into_iter()
.map(|c| {
// Convert ruma::api::federation::directory::get_public_rooms::v1::PublicRoomsChunk
// to ruma::api::client::r0::directory::PublicRoomsChunk
serde_json::from_str(
&serde_json::to_string(&c)
.expect("PublicRoomsChunk::to_string always works"),
)
.expect("federation and client-server PublicRoomsChunk are the same type")
})
.collect(),
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,
}
.into());
});
}
let limit = limit.map_or(10, u64::from);
@ -228,10 +191,9 @@ pub(crate) async fn get_public_rooms_filtered_helper(
let room_id = room_id?;
let chunk = PublicRoomsChunk {
aliases: Vec::new(),
canonical_alias: db
.rooms
.room_state_get(&room_id, &EventType::RoomCanonicalAlias, "")?
.room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomCanonicalAliasEventContent| c.alias)
@ -241,7 +203,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
})?,
name: db
.rooms
.room_state_get(&room_id, &EventType::RoomName, "")?
.room_state_get(&room_id, &StateEventType::RoomName, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomNameEventContent| c.name)
@ -260,7 +222,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.expect("user count should not be that big"),
topic: db
.rooms
.room_state_get(&room_id, &EventType::RoomTopic, "")?
.room_state_get(&room_id, &StateEventType::RoomTopic, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomTopicEventContent| Some(c.topic))
@ -270,7 +232,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
})?,
world_readable: db
.rooms
.room_state_get(&room_id, &EventType::RoomHistoryVisibility, "")?
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| {
@ -284,7 +246,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
})?,
guest_can_join: db
.rooms
.room_state_get(&room_id, &EventType::RoomGuestAccess, "")?
.room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomGuestAccessEventContent| {
@ -296,7 +258,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
})?,
avatar_url: db
.rooms
.room_state_get(&room_id, &EventType::RoomAvatar, "")?
.room_state_get(&room_id, &StateEventType::RoomAvatar, "")?
.map(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomAvatarEventContent| c.url)
@ -307,6 +269,25 @@ pub(crate) async fn get_public_rooms_filtered_helper(
.transpose()?
// url is now an Option<String> so we must flatten
.flatten(),
join_rule: db
.rooms
.room_state_get(&room_id, &StateEventType::RoomJoinRules, "")?
.map(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomJoinRulesEventContent| match c.join_rule {
JoinRule::Public => Some(PublicRoomJoinRule::Public),
JoinRule::Knock => Some(PublicRoomJoinRule::Knock),
_ => None,
})
.map_err(|_| {
Error::bad_database("Invalid room join rule event in database.")
})
})
.transpose()?
.flatten()
.ok_or(Error::bad_database(
"Invalid room join rule event in database.",
))?,
room_id,
};
Ok(chunk)
@ -367,11 +348,10 @@ pub(crate) async fn get_public_rooms_filtered_helper(
Some(format!("n{}", num_since + limit))
};
Ok(get_public_rooms_filtered::Response {
Ok(get_public_rooms_filtered::v3::Response {
chunk,
prev_batch,
next_batch,
total_room_count_estimate: Some(total_room_count_estimate),
}
.into())
})
}

@ -1,47 +1,36 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::api::client::{
error::ErrorKind,
r0::filter::{create_filter, get_filter},
filter::{create_filter, get_filter},
};
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
/// # `GET /_matrix/client/r0/user/{userId}/filter/{filterId}`
///
/// Loads a filter that was previously created.
///
/// - A user can only access their own filters
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/user/<_>/filter/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_filter_route(
db: DatabaseGuard,
body: Ruma<get_filter::Request<'_>>,
) -> ConduitResult<get_filter::Response> {
body: Ruma<get_filter::v3::IncomingRequest>,
) -> Result<get_filter::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let filter = match db.users.get_filter(sender_user, &body.filter_id)? {
Some(filter) => filter,
None => return Err(Error::BadRequest(ErrorKind::NotFound, "Filter not found.")),
};
Ok(get_filter::Response::new(filter).into())
Ok(get_filter::v3::Response::new(filter))
}
/// # `PUT /_matrix/client/r0/user/{userId}/filter`
///
/// Creates a new filter to be used by other endpoints.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/user/<_>/filter", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn create_filter_route(
db: DatabaseGuard,
body: Ruma<create_filter::Request<'_>>,
) -> ConduitResult<create_filter::Response> {
body: Ruma<create_filter::v3::IncomingRequest>,
) -> Result<create_filter::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(create_filter::Response::new(db.users.create_filter(sender_user, &body.filter)?).into())
Ok(create_filter::v3::Response::new(
db.users.create_filter(sender_user, &body.filter)?,
))
}

@ -1,17 +1,15 @@
use super::SESSION_ID_LENGTH;
use crate::{database::DatabaseGuard, utils, ConduitResult, Database, Error, Result, Ruma};
use rocket::futures::{prelude::*, stream::FuturesUnordered};
use crate::{database::DatabaseGuard, utils, Database, Error, Result, Ruma};
use futures_util::{stream::FuturesUnordered, StreamExt};
use ruma::{
api::{
client::{
error::ErrorKind,
r0::{
keys::{
claim_keys, get_key_changes, get_keys, upload_keys, upload_signatures,
upload_signing_keys,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
keys::{
claim_keys, get_key_changes, get_keys, upload_keys, upload_signatures,
upload_signing_keys,
},
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
federation,
},
@ -21,24 +19,16 @@ use ruma::{
use serde_json::json;
use std::collections::{BTreeMap, HashMap, HashSet};
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
/// # `POST /_matrix/client/r0/keys/upload`
///
/// Publish end-to-end encryption keys for the sender device.
///
/// - Adds one time keys
/// - If there are no device keys yet: Adds device keys (TODO: merge with existing keys?)
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/keys/upload", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn upload_keys_route(
db: DatabaseGuard,
body: Ruma<upload_keys::Request>,
) -> ConduitResult<upload_keys::Response> {
body: Ruma<upload_keys::v3::Request>,
) -> Result<upload_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -67,10 +57,9 @@ pub async fn upload_keys_route(
db.flush()?;
Ok(upload_keys::Response {
Ok(upload_keys::v3::Response {
one_time_key_counts: db.users.count_one_time_keys(sender_user, sender_device)?,
}
.into())
})
}
/// # `POST /_matrix/client/r0/keys/query`
@ -80,15 +69,10 @@ pub async fn upload_keys_route(
/// - Always fetches users from other servers over federation
/// - Gets master keys, self-signing keys, user signing keys and device keys.
/// - The master and self-signing keys contain signatures that the user is allowed to see
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/keys/query", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_keys_route(
db: DatabaseGuard,
body: Ruma<get_keys::Request<'_>>,
) -> ConduitResult<get_keys::Response> {
body: Ruma<get_keys::v3::IncomingRequest>,
) -> Result<get_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let response = get_keys_helper(
@ -99,26 +83,21 @@ pub async fn get_keys_route(
)
.await?;
Ok(response.into())
Ok(response)
}
/// # `POST /_matrix/client/r0/keys/claim`
///
/// Claims one-time keys
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/keys/claim", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn claim_keys_route(
db: DatabaseGuard,
body: Ruma<claim_keys::Request>,
) -> ConduitResult<claim_keys::Response> {
body: Ruma<claim_keys::v3::Request>,
) -> Result<claim_keys::v3::Response> {
let response = claim_keys_helper(&body.one_time_keys, &db).await?;
db.flush()?;
Ok(response.into())
Ok(response)
}
/// # `POST /_matrix/client/r0/keys/device_signing/upload`
@ -126,15 +105,10 @@ pub async fn claim_keys_route(
/// Uploads end-to-end key information for the sender user.
///
/// - Requires UIAA to verify password
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/unstable/keys/device_signing/upload", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn upload_signing_keys_route(
db: DatabaseGuard,
body: Ruma<upload_signing_keys::Request<'_>>,
) -> ConduitResult<upload_signing_keys::Response> {
body: Ruma<upload_signing_keys::v3::IncomingRequest>,
) -> Result<upload_signing_keys::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -184,25 +158,22 @@ pub async fn upload_signing_keys_route(
db.flush()?;
Ok(upload_signing_keys::Response {}.into())
Ok(upload_signing_keys::v3::Response {})
}
/// # `POST /_matrix/client/r0/keys/signatures/upload`
///
/// Uploads end-to-end key signatures from the sender user.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/unstable/keys/signatures/upload", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn upload_signatures_route(
db: DatabaseGuard,
body: Ruma<upload_signatures::Request>,
) -> ConduitResult<upload_signatures::Response> {
body: Ruma<upload_signatures::v3::Request>,
) -> Result<upload_signatures::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
for (user_id, signed_keys) in &body.signed_keys {
for (key_id, signed_key) in signed_keys {
let signed_key = serde_json::to_value(signed_key).unwrap();
for signature in signed_key
.get("signatures")
.ok_or(Error::BadRequest(
@ -248,7 +219,9 @@ pub async fn upload_signatures_route(
db.flush()?;
Ok(upload_signatures::Response {}.into())
Ok(upload_signatures::v3::Response {
failures: BTreeMap::new(), // TODO: integrate
})
}
/// # `POST /_matrix/client/r0/keys/changes`
@ -256,15 +229,10 @@ pub async fn upload_signatures_route(
/// Gets a list of users who have updated their device identity keys since the previous sync token.
///
/// - TODO: left users
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/keys/changes", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_key_changes_route(
db: DatabaseGuard,
body: Ruma<get_key_changes::Request<'_>>,
) -> ConduitResult<get_key_changes::Response> {
body: Ruma<get_key_changes::v3::IncomingRequest>,
) -> Result<get_key_changes::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut device_list_updates = HashSet::new();
@ -300,11 +268,10 @@ pub async fn get_key_changes_route(
.filter_map(|r| r.ok()),
);
}
Ok(get_key_changes::Response {
Ok(get_key_changes::v3::Response {
changed: device_list_updates.into_iter().collect(),
left: Vec::new(), // TODO
}
.into())
})
}
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
@ -312,7 +279,7 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
device_keys_input: &BTreeMap<Box<UserId>, Vec<Box<DeviceId>>>,
allowed_signatures: F,
db: &Database,
) -> Result<get_keys::Response> {
) -> Result<get_keys::v3::Response> {
let mut master_keys = BTreeMap::new();
let mut self_signing_keys = BTreeMap::new();
let mut user_signing_keys = BTreeMap::new();
@ -421,7 +388,7 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
}
}
Ok(get_keys::Response {
Ok(get_keys::v3::Response {
master_keys,
self_signing_keys,
user_signing_keys,
@ -432,7 +399,7 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
fn add_unsigned_device_display_name(
keys: &mut Raw<ruma::encryption::DeviceKeys>,
metadata: ruma::api::client::r0::device::Device,
metadata: ruma::api::client::device::Device,
) -> serde_json::Result<()> {
if let Some(display_name) = metadata.display_name {
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
@ -451,7 +418,7 @@ fn add_unsigned_device_display_name(
pub(crate) async fn claim_keys_helper(
one_time_keys_input: &BTreeMap<Box<UserId>, BTreeMap<Box<DeviceId>, DeviceKeyAlgorithm>>,
db: &Database,
) -> Result<claim_keys::Response> {
) -> Result<claim_keys::v3::Response> {
let mut one_time_keys = BTreeMap::new();
let mut get_over_federation = BTreeMap::new();
@ -503,7 +470,7 @@ pub(crate) async fn claim_keys_helper(
}
}
Ok(claim_keys::Response {
Ok(claim_keys::v3::Response {
failures,
one_time_keys,
})

@ -1,32 +1,27 @@
use crate::{
database::{media::FileMeta, DatabaseGuard},
utils, ConduitResult, Error, Ruma,
utils, Error, Result, Ruma,
};
use ruma::api::client::{
error::ErrorKind,
r0::media::{
media::{
create_content, get_content, get_content_as_filename, get_content_thumbnail,
get_media_config,
},
};
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
const MXC_LENGTH: usize = 32;
/// # `GET /_matrix/media/r0/config`
///
/// Returns max upload size.
#[cfg_attr(feature = "conduit_bin", get("/_matrix/media/r0/config"))]
#[tracing::instrument(skip(db))]
pub async fn get_media_config_route(
db: DatabaseGuard,
) -> ConduitResult<get_media_config::Response> {
Ok(get_media_config::Response {
_body: Ruma<get_media_config::v3::Request>,
) -> Result<get_media_config::v3::Response> {
Ok(get_media_config::v3::Response {
upload_size: db.globals.max_request_size().into(),
}
.into())
})
}
/// # `POST /_matrix/media/r0/upload`
@ -35,15 +30,10 @@ pub async fn get_media_config_route(
///
/// - Some metadata will be saved in the database
/// - Media will be saved in the media/ directory
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/media/r0/upload", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn create_content_route(
db: DatabaseGuard,
body: Ruma<create_content::Request<'_>>,
) -> ConduitResult<create_content::Response> {
body: Ruma<create_content::v3::IncomingRequest>,
) -> Result<create_content::v3::Response> {
let mxc = format!(
"mxc://{}/{}",
db.globals.server_name(),
@ -66,11 +56,10 @@ pub async fn create_content_route(
db.flush()?;
Ok(create_content::Response {
Ok(create_content::v3::Response {
content_uri: mxc.try_into().expect("Invalid mxc:// URI"),
blurhash: None,
}
.into())
})
}
pub async fn get_remote_content(
@ -78,13 +67,13 @@ pub async fn get_remote_content(
mxc: &str,
server_name: &ruma::ServerName,
media_id: &str,
) -> Result<get_content::Response, Error> {
) -> Result<get_content::v3::Response, Error> {
let content_response = db
.sending
.send_federation_request(
&db.globals,
server_name,
get_content::Request {
get_content::v3::Request {
allow_remote: false,
server_name,
media_id,
@ -110,15 +99,10 @@ pub async fn get_remote_content(
/// Load media from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/media/r0/download/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_content_route(
db: DatabaseGuard,
body: Ruma<get_content::Request<'_>>,
) -> ConduitResult<get_content::Response> {
body: Ruma<get_content::v3::IncomingRequest>,
) -> Result<get_content::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
if let Some(FileMeta {
@ -127,16 +111,15 @@ pub async fn get_content_route(
file,
}) = db.media.get(&db.globals, &mxc).await?
{
Ok(get_content::Response {
Ok(get_content::v3::Response {
file,
content_type,
content_disposition,
}
.into())
})
} else if &*body.server_name != db.globals.server_name() && body.allow_remote {
let remote_content_response =
get_remote_content(&db, &mxc, &body.server_name, &body.media_id).await?;
Ok(remote_content_response.into())
Ok(remote_content_response)
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
}
@ -147,15 +130,10 @@ pub async fn get_content_route(
/// Load media from our server or over federation, permitting desired filename.
///
/// - Only allows federation if `allow_remote` is true
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/media/r0/download/<_>/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_content_as_filename_route(
db: DatabaseGuard,
body: Ruma<get_content_as_filename::Request<'_>>,
) -> ConduitResult<get_content_as_filename::Response> {
body: Ruma<get_content_as_filename::v3::IncomingRequest>,
) -> Result<get_content_as_filename::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
if let Some(FileMeta {
@ -164,22 +142,20 @@ pub async fn get_content_as_filename_route(
file,
}) = db.media.get(&db.globals, &mxc).await?
{
Ok(get_content_as_filename::Response {
Ok(get_content_as_filename::v3::Response {
file,
content_type,
content_disposition: Some(format!("inline; filename={}", body.filename)),
}
.into())
})
} else if &*body.server_name != db.globals.server_name() && body.allow_remote {
let remote_content_response =
get_remote_content(&db, &mxc, &body.server_name, &body.media_id).await?;
Ok(get_content_as_filename::Response {
Ok(get_content_as_filename::v3::Response {
content_disposition: Some(format!("inline: filename={}", body.filename)),
content_type: remote_content_response.content_type,
file: remote_content_response.file,
}
.into())
})
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
}
@ -190,15 +166,10 @@ pub async fn get_content_as_filename_route(
/// Load media thumbnail from our server or over federation.
///
/// - Only allows federation if `allow_remote` is true
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/media/r0/thumbnail/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_content_thumbnail_route(
db: DatabaseGuard,
body: Ruma<get_content_thumbnail::Request<'_>>,
) -> ConduitResult<get_content_thumbnail::Response> {
body: Ruma<get_content_thumbnail::v3::IncomingRequest>,
) -> Result<get_content_thumbnail::v3::Response> {
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
if let Some(FileMeta {
@ -217,14 +188,14 @@ pub async fn get_content_thumbnail_route(
)
.await?
{
Ok(get_content_thumbnail::Response { file, content_type }.into())
Ok(get_content_thumbnail::v3::Response { file, content_type })
} else if &*body.server_name != db.globals.server_name() && body.allow_remote {
let get_thumbnail_response = db
.sending
.send_federation_request(
&db.globals,
&body.server_name,
get_content_thumbnail::Request {
get_content_thumbnail::v3::Request {
allow_remote: false,
height: body.height,
width: body.width,
@ -247,7 +218,7 @@ pub async fn get_content_thumbnail_route(
)
.await?;
Ok(get_thumbnail_response.into())
Ok(get_thumbnail_response)
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
}

@ -2,13 +2,13 @@ use crate::{
client_server,
database::DatabaseGuard,
pdu::{EventHash, PduBuilder, PduEvent},
server_server, utils, ConduitResult, Database, Error, Result, Ruma,
server_server, utils, Database, Error, Result, Ruma,
};
use ruma::{
api::{
client::{
error::ErrorKind,
r0::membership::{
membership::{
ban_user, forget_room, get_member_events, invite_user, join_room_by_id,
join_room_by_id_or_alias, joined_members, joined_rooms, kick_user, leave_room,
unban_user, IncomingThirdPartySigned,
@ -21,7 +21,7 @@ use ruma::{
create::RoomCreateEventContent,
member::{MembershipState, RoomMemberEventContent},
},
EventType,
RoomEventType, StateEventType,
},
serde::{to_canonical_value, Base64, CanonicalJsonObject, CanonicalJsonValue},
state_res::{self, RoomVersion},
@ -29,46 +29,39 @@ use ruma::{
};
use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
use std::{
collections::{hash_map::Entry, BTreeMap, HashMap, HashSet},
collections::{hash_map::Entry, BTreeMap, HashMap},
iter,
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use tracing::{debug, error, warn};
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
/// # `POST /_matrix/client/r0/rooms/{roomId}/join`
///
/// Tries to join the sender user into a room.
///
/// - If the server knowns about this room: creates the join event and does auth rules locally
/// - If the server does not know about the room: asks other servers over federation
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/join", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn join_room_by_id_route(
db: DatabaseGuard,
body: Ruma<join_room_by_id::Request<'_>>,
) -> ConduitResult<join_room_by_id::Response> {
body: Ruma<join_room_by_id::v3::IncomingRequest>,
) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut servers: HashSet<_> = db
.rooms
.invite_state(sender_user, &body.room_id)?
.unwrap_or_default()
.iter()
.filter_map(|event| serde_json::from_str(event.json().get()).ok())
.filter_map(|event: serde_json::Value| event.get("sender").cloned())
.filter_map(|sender| sender.as_str().map(|s| s.to_owned()))
.filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned())
.collect();
servers.insert(body.room_id.server_name().to_owned());
let mut servers = Vec::new(); // There is no body.server_name for /roomId/join
servers.extend(
db.rooms
.invite_state(sender_user, &body.room_id)?
.unwrap_or_default()
.iter()
.filter_map(|event| serde_json::from_str(event.json().get()).ok())
.filter_map(|event: serde_json::Value| event.get("sender").cloned())
.filter_map(|sender| sender.as_str().map(|s| s.to_owned()))
.filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
servers.push(body.room_id.server_name().to_owned());
let ret = join_room_by_id_helper(
&db,
@ -90,39 +83,35 @@ pub async fn join_room_by_id_route(
///
/// - If the server knowns about this room: creates the join event and does auth rules locally
/// - If the server does not know about the room: asks other servers over federation
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/join/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn join_room_by_id_or_alias_route(
db: DatabaseGuard,
body: Ruma<join_room_by_id_or_alias::Request<'_>>,
) -> ConduitResult<join_room_by_id_or_alias::Response> {
body: Ruma<join_room_by_id_or_alias::v3::IncomingRequest>,
) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.sender_user.as_deref().expect("user is authenticated");
let body = body.body;
let (servers, room_id) = match Box::<RoomId>::try_from(body.room_id_or_alias) {
Ok(room_id) => {
let mut servers: HashSet<_> = db
.rooms
.invite_state(sender_user, &room_id)?
.unwrap_or_default()
.iter()
.filter_map(|event| serde_json::from_str(event.json().get()).ok())
.filter_map(|event: serde_json::Value| event.get("sender").cloned())
.filter_map(|sender| sender.as_str().map(|s| s.to_owned()))
.filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned())
.collect();
let mut servers = body.server_name.clone();
servers.extend(
db.rooms
.invite_state(sender_user, &room_id)?
.unwrap_or_default()
.iter()
.filter_map(|event| serde_json::from_str(event.json().get()).ok())
.filter_map(|event: serde_json::Value| event.get("sender").cloned())
.filter_map(|sender| sender.as_str().map(|s| s.to_owned()))
.filter_map(|sender| UserId::parse(sender).ok())
.map(|user| user.server_name().to_owned()),
);
servers.insert(room_id.server_name().to_owned());
servers.push(room_id.server_name().to_owned());
(servers, room_id)
}
Err(room_alias) => {
let response = client_server::get_alias_helper(&db, &room_alias).await?;
(response.0.servers.into_iter().collect(), response.0.room_id)
(response.servers.into_iter().collect(), response.room_id)
}
};
@ -137,10 +126,9 @@ pub async fn join_room_by_id_or_alias_route(
db.flush()?;
Ok(join_room_by_id_or_alias::Response {
room_id: join_room_response.0.room_id,
}
.into())
Ok(join_room_by_id_or_alias::v3::Response {
room_id: join_room_response.room_id,
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/leave`
@ -148,42 +136,32 @@ pub async fn join_room_by_id_or_alias_route(
/// Tries to leave the sender user from a room.
///
/// - This should always work if the user is currently joined.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/leave", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn leave_room_route(
db: DatabaseGuard,
body: Ruma<leave_room::Request<'_>>,
) -> ConduitResult<leave_room::Response> {
body: Ruma<leave_room::v3::IncomingRequest>,
) -> Result<leave_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.rooms.leave_room(sender_user, &body.room_id, &db).await?;
db.flush()?;
Ok(leave_room::Response::new().into())
Ok(leave_room::v3::Response::new())
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
///
/// Tries to send an invite event into the room.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/invite", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn invite_user_route(
db: DatabaseGuard,
body: Ruma<invite_user::Request<'_>>,
) -> ConduitResult<invite_user::Response> {
body: Ruma<invite_user::v3::IncomingRequest>,
) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if let invite_user::IncomingInvitationRecipient::UserId { user_id } = &body.recipient {
if let invite_user::v3::IncomingInvitationRecipient::UserId { user_id } = &body.recipient {
invite_helper(sender_user, user_id, &body.room_id, &db, false).await?;
db.flush()?;
Ok(invite_user::Response {}.into())
Ok(invite_user::v3::Response {})
} else {
Err(Error::BadRequest(ErrorKind::NotFound, "User not found."))
}
@ -192,22 +170,17 @@ pub async fn invite_user_route(
/// # `POST /_matrix/client/r0/rooms/{roomId}/kick`
///
/// Tries to send a kick event into the room.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/kick", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn kick_user_route(
db: DatabaseGuard,
body: Ruma<kick_user::Request<'_>>,
) -> ConduitResult<kick_user::Response> {
body: Ruma<kick_user::v3::IncomingRequest>,
) -> Result<kick_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut event: RoomMemberEventContent = serde_json::from_str(
db.rooms
.room_state_get(
&body.room_id,
&EventType::RoomMember,
&StateEventType::RoomMember,
&body.user_id.to_string(),
)?
.ok_or(Error::BadRequest(
@ -234,7 +207,7 @@ pub async fn kick_user_route(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(body.user_id.to_string()),
@ -250,21 +223,16 @@ pub async fn kick_user_route(
db.flush()?;
Ok(kick_user::Response::new().into())
Ok(kick_user::v3::Response::new())
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/ban`
///
/// Tries to send a ban event into the room.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/ban", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn ban_user_route(
db: DatabaseGuard,
body: Ruma<ban_user::Request<'_>>,
) -> ConduitResult<ban_user::Response> {
body: Ruma<ban_user::v3::IncomingRequest>,
) -> Result<ban_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// TODO: reason
@ -273,7 +241,7 @@ pub async fn ban_user_route(
.rooms
.room_state_get(
&body.room_id,
&EventType::RoomMember,
&StateEventType::RoomMember,
&body.user_id.to_string(),
)?
.map_or(
@ -309,7 +277,7 @@ pub async fn ban_user_route(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(body.user_id.to_string()),
@ -325,28 +293,23 @@ pub async fn ban_user_route(
db.flush()?;
Ok(ban_user::Response::new().into())
Ok(ban_user::v3::Response::new())
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/unban`
///
/// Tries to send an unban event into the room.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/unban", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn unban_user_route(
db: DatabaseGuard,
body: Ruma<unban_user::Request<'_>>,
) -> ConduitResult<unban_user::Response> {
body: Ruma<unban_user::v3::IncomingRequest>,
) -> Result<unban_user::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut event: RoomMemberEventContent = serde_json::from_str(
db.rooms
.room_state_get(
&body.room_id,
&EventType::RoomMember,
&StateEventType::RoomMember,
&body.user_id.to_string(),
)?
.ok_or(Error::BadRequest(
@ -372,7 +335,7 @@ pub async fn unban_user_route(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(body.user_id.to_string()),
@ -388,7 +351,7 @@ pub async fn unban_user_route(
db.flush()?;
Ok(unban_user::Response::new().into())
Ok(unban_user::v3::Response::new())
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/forget`
@ -399,46 +362,35 @@ pub async fn unban_user_route(
///
/// Note: Other devices of the user have no way of knowing the room was forgotten, so this has to
/// be called from every device
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/forget", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn forget_room_route(
db: DatabaseGuard,
body: Ruma<forget_room::Request<'_>>,
) -> ConduitResult<forget_room::Response> {
body: Ruma<forget_room::v3::IncomingRequest>,
) -> Result<forget_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.rooms.forget(&body.room_id, sender_user)?;
db.flush()?;
Ok(forget_room::Response::new().into())
Ok(forget_room::v3::Response::new())
}
/// # `POST /_matrix/client/r0/joined_rooms`
///
/// Lists all rooms the user has joined.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/joined_rooms", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn joined_rooms_route(
db: DatabaseGuard,
body: Ruma<joined_rooms::Request>,
) -> ConduitResult<joined_rooms::Response> {
body: Ruma<joined_rooms::v3::Request>,
) -> Result<joined_rooms::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(joined_rooms::Response {
Ok(joined_rooms::v3::Response {
joined_rooms: db
.rooms
.rooms_joined(sender_user)
.filter_map(|r| r.ok())
.collect(),
}
.into())
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/members`
@ -446,15 +398,10 @@ pub async fn joined_rooms_route(
/// Lists all joined users in a room (TODO: at a specific point in time, with a specific membership).
///
/// - Only works if the user is currently joined
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/members", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_member_events_route(
db: DatabaseGuard,
body: Ruma<get_member_events::Request<'_>>,
) -> ConduitResult<get_member_events::Response> {
body: Ruma<get_member_events::v3::IncomingRequest>,
) -> Result<get_member_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// TODO: check history visibility?
@ -465,16 +412,16 @@ pub async fn get_member_events_route(
));
}
Ok(get_member_events::Response {
Ok(get_member_events::v3::Response {
chunk: db
.rooms
.room_state_full(&body.room_id)?
.room_state_full(&body.room_id)
.await?
.iter()
.filter(|(key, _)| key.0 == EventType::RoomMember)
.map(|(_, pdu)| pdu.to_member_event())
.filter(|(key, _)| key.0 == StateEventType::RoomMember)
.map(|(_, pdu)| pdu.to_member_event().into())
.collect(),
}
.into())
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/joined_members`
@ -483,15 +430,10 @@ pub async fn get_member_events_route(
///
/// - The sender user must be in the room
/// - TODO: An appservice just needs a puppet joined
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/joined_members", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn joined_members_route(
db: DatabaseGuard,
body: Ruma<joined_members::Request<'_>>,
) -> ConduitResult<joined_members::Response> {
body: Ruma<joined_members::v3::IncomingRequest>,
) -> Result<joined_members::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !db.rooms.is_joined(sender_user, &body.room_id)? {
@ -508,14 +450,14 @@ pub async fn joined_members_route(
joined.insert(
user_id,
joined_members::RoomMember {
joined_members::v3::RoomMember {
display_name,
avatar_url,
},
);
}
Ok(joined_members::Response { joined }.into())
Ok(joined_members::v3::Response { joined })
}
#[tracing::instrument(skip(db))]
@ -523,9 +465,9 @@ async fn join_room_by_id_helper(
db: &Database,
sender_user: Option<&UserId>,
room_id: &RoomId,
servers: &HashSet<Box<ServerName>>,
servers: &[Box<ServerName>],
_third_party_signed: Option<&IncomingThirdPartySigned>,
) -> ConduitResult<join_room_by_id::Response> {
) -> Result<join_room_by_id::v3::Response> {
let sender_user = sender_user.expect("user is authenticated");
let mutex_state = Arc::clone(
@ -539,7 +481,7 @@ async fn join_room_by_id_helper(
let state_lock = mutex_state.lock().await;
// Ask a remote server if we don't have this room
if !db.rooms.exists(room_id)? && room_id.server_name() != db.globals.server_name() {
if !db.rooms.exists(room_id)? {
let mut make_join_response_and_server = Err(Error::BadServerResponse(
"No server available to assist in joining.",
));
@ -550,10 +492,10 @@ async fn join_room_by_id_helper(
.send_federation_request(
&db.globals,
remote_server,
federation::membership::create_join_event_template::v1::Request {
federation::membership::prepare_join_event::v1::Request {
room_id,
user_id: sender_user,
ver: &[RoomVersionId::V5, RoomVersionId::V6],
ver: &db.globals.supported_room_versions(),
},
)
.await;
@ -568,11 +510,7 @@ async fn join_room_by_id_helper(
let (make_join_response, remote_server) = make_join_response_and_server?;
let room_version = match make_join_response.room_version {
Some(room_version)
if room_version == RoomVersionId::V5 || room_version == RoomVersionId::V6 =>
{
room_version
}
Some(room_version) if db.rooms.is_supported_version(&db, &room_version) => room_version,
_ => return Err(Error::BadServerResponse("Room version is not supported")),
};
@ -686,15 +624,17 @@ async fn join_room_by_id_helper(
db.rooms.add_pdu_outlier(&event_id, &value)?;
if let Some(state_key) = &pdu.state_key {
let shortstatekey =
db.rooms
.get_or_create_shortstatekey(&pdu.kind, state_key, &db.globals)?;
let shortstatekey = db.rooms.get_or_create_shortstatekey(
&pdu.kind.to_string().into(),
state_key,
&db.globals,
)?;
state.insert(shortstatekey, pdu.event_id.clone());
}
}
let incoming_shortstatekey = db.rooms.get_or_create_shortstatekey(
&parsed_pdu.kind,
&parsed_pdu.kind.to_string().into(),
parsed_pdu
.state_key
.as_ref()
@ -706,7 +646,7 @@ async fn join_room_by_id_helper(
let create_shortstatekey = db
.rooms
.get_shortstatekey(&EventType::RoomCreate, "")?
.get_shortstatekey(&StateEventType::RoomCreate, "")?
.expect("Room exists");
if state.get(&create_shortstatekey).is_none() {
@ -764,7 +704,7 @@ async fn join_room_by_id_helper(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
@ -781,7 +721,7 @@ async fn join_room_by_id_helper(
db.flush()?;
Ok(join_room_by_id::Response::new(room_id.to_owned()).into())
Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
}
fn validate_and_add_event_id(
@ -875,7 +815,7 @@ pub(crate) async fn invite_helper<'a>(
let create_event = db
.rooms
.room_state_get(room_id, &EventType::RoomCreate, "")?;
.room_state_get(room_id, &StateEventType::RoomCreate, "")?;
let create_event_content: Option<RoomCreateEventContent> = create_event
.as_ref()
@ -887,17 +827,12 @@ pub(crate) async fn invite_helper<'a>(
})
.transpose()?;
let create_prev_event = if prev_events.len() == 1
&& Some(&prev_events[0]) == create_event.as_ref().map(|c| &c.event_id)
{
create_event
} else {
None
};
// If there was no create event yet, assume we are creating a version 6 room right now
// If there was no create event yet, assume we are creating a room with the default
// version right now
let room_version_id = create_event_content
.map_or(RoomVersionId::V6, |create_event| create_event.room_version);
.map_or(db.globals.default_room_version(), |create_event| {
create_event.room_version
});
let room_version =
RoomVersion::new(&room_version_id).expect("room version is supported");
@ -914,11 +849,11 @@ pub(crate) async fn invite_helper<'a>(
.expect("member event is valid value");
let state_key = user_id.to_string();
let kind = EventType::RoomMember;
let kind = StateEventType::RoomMember;
let auth_events = db.rooms.get_auth_events(
room_id,
&kind,
&kind.to_string().into(),
sender_user,
Some(&state_key),
&content,
@ -949,7 +884,7 @@ pub(crate) async fn invite_helper<'a>(
origin_server_ts: utils::millis_since_unix_epoch()
.try_into()
.expect("time is valid"),
kind,
kind: kind.to_string().into(),
content,
state_key: Some(state_key),
prev_events,
@ -973,7 +908,6 @@ pub(crate) async fn invite_helper<'a>(
let auth_check = state_res::auth_check(
&room_version,
&pdu,
create_prev_event,
None::<PduEvent>, // TODO: third_party_invite
|k, s| auth_events.get(&(k.clone(), s.to_owned())),
)
@ -1044,7 +978,8 @@ pub(crate) async fn invite_helper<'a>(
let pub_key_map = RwLock::new(BTreeMap::new());
// We do not add the event_id field to the pdu here because of signature and hashes checks
let (event_id, value) = match crate::pdu::gen_event_id_canonical_json(&response.event) {
let (event_id, value) = match crate::pdu::gen_event_id_canonical_json(&response.event, &db)
{
Ok(t) => t,
Err(_) => {
// Event could not be converted to canonical json
@ -1100,6 +1035,13 @@ pub(crate) async fn invite_helper<'a>(
return Ok(());
}
if !db.rooms.is_joined(sender_user, &room_id)? {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"You don't have permission to view this room.",
));
}
let mutex_state = Arc::clone(
db.globals
.roomid_mutex_state
@ -1112,7 +1054,7 @@ pub(crate) async fn invite_helper<'a>(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Invite,
displayname: db.users.displayname(user_id)?,

@ -1,19 +1,16 @@
use crate::{database::DatabaseGuard, pdu::PduBuilder, utils, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, pdu::PduBuilder, utils, Error, Result, Ruma};
use ruma::{
api::client::{
error::ErrorKind,
r0::message::{get_message_events, send_message_event},
message::{get_message_events, send_message_event},
},
events::EventType,
events::{RoomEventType, StateEventType},
};
use std::{
collections::{BTreeMap, HashSet},
sync::Arc,
};
#[cfg(feature = "conduit_bin")]
use rocket::{get, put};
/// # `PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}`
///
/// Send a message event into the room.
@ -21,15 +18,10 @@ use rocket::{get, put};
/// - Is a NOOP if the txn id was already used before and returns the same event id again
/// - The only requirement for the content is that it has to be valid json
/// - Tries to send the event into the room, auth rules will determine if it is allowed
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/rooms/<_>/send/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn send_message_event_route(
db: DatabaseGuard,
body: Ruma<send_message_event::Request<'_>>,
) -> ConduitResult<send_message_event::Response> {
body: Ruma<send_message_event::v3::IncomingRequest>,
) -> Result<send_message_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_deref();
@ -44,7 +36,9 @@ pub async fn send_message_event_route(
let state_lock = mutex_state.lock().await;
// Forbid m.room.encrypted if encryption is disabled
if &body.event_type == "m.room.encrypted" && !db.globals.allow_encryption() {
if RoomEventType::RoomEncrypted == body.event_type.to_string().into()
&& !db.globals.allow_encryption()
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Encryption has been disabled",
@ -69,7 +63,7 @@ pub async fn send_message_event_route(
.map_err(|_| Error::bad_database("Invalid txnid bytes in database."))?
.try_into()
.map_err(|_| Error::bad_database("Invalid event id in txnid data."))?;
return Ok(send_message_event::Response { event_id }.into());
return Ok(send_message_event::v3::Response { event_id });
}
let mut unsigned = BTreeMap::new();
@ -77,7 +71,7 @@ pub async fn send_message_event_route(
let event_id = db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::from(&*body.event_type),
event_type: body.event_type.to_string().into(),
content: serde_json::from_str(body.body.body.json().get())
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
unsigned: Some(unsigned),
@ -101,7 +95,9 @@ pub async fn send_message_event_route(
db.flush()?;
Ok(send_message_event::Response::new((*event_id).to_owned()).into())
Ok(send_message_event::v3::Response::new(
(*event_id).to_owned(),
))
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/messages`
@ -110,15 +106,10 @@ pub async fn send_message_event_route(
///
/// - Only works if the user is joined (TODO: always allow, but only show events where the user was
/// joined, depending on history_visibility)
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/messages", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_message_events_route(
db: DatabaseGuard,
body: Ruma<get_message_events::Request<'_>>,
) -> ConduitResult<get_message_events::Response> {
body: Ruma<get_message_events::v3::IncomingRequest>,
) -> Result<get_message_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -129,11 +120,16 @@ pub async fn get_message_events_route(
));
}
let from = body
.from
.clone()
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from` value."))?;
let from = match body.from.clone() {
Some(from) => from
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from` value."))?,
None => match body.dir {
get_message_events::v3::Direction::Forward => 0,
get_message_events::v3::Direction::Backward => u64::MAX,
},
};
let to = body.to.as_ref().map(|t| t.parse());
@ -145,12 +141,12 @@ pub async fn get_message_events_route(
let next_token;
let mut resp = get_message_events::Response::new();
let mut resp = get_message_events::v3::Response::new();
let mut lazy_loaded = HashSet::new();
match body.dir {
get_message_events::Direction::Forward => {
get_message_events::v3::Direction::Forward => {
let events_after: Vec<_> = db
.rooms
.pdus_after(sender_user, &body.room_id, from)?
@ -183,11 +179,11 @@ pub async fn get_message_events_route(
.map(|(_, pdu)| pdu.to_room_event())
.collect();
resp.start = body.from.to_owned();
resp.start = from.to_string();
resp.end = next_token.map(|count| count.to_string());
resp.chunk = events_after;
}
get_message_events::Direction::Backward => {
get_message_events::v3::Direction::Backward => {
let events_before: Vec<_> = db
.rooms
.pdus_until(sender_user, &body.room_id, from)?
@ -220,7 +216,7 @@ pub async fn get_message_events_route(
.map(|(_, pdu)| pdu.to_room_event())
.collect();
resp.start = body.from.to_owned();
resp.start = from.to_string();
resp.end = next_token.map(|count| count.to_string());
resp.chunk = events_before;
}
@ -230,7 +226,7 @@ pub async fn get_message_events_route(
for ll_id in &lazy_loaded {
if let Some(member_event) =
db.rooms
.room_state_get(&body.room_id, &EventType::RoomMember, ll_id.as_str())?
.room_state_get(&body.room_id, &StateEventType::RoomMember, ll_id.as_str())?
{
resp.state.push(member_event.to_state_event());
}
@ -246,5 +242,5 @@ pub async fn get_message_events_route(
);
}
Ok(resp.into())
Ok(resp)
}

@ -62,23 +62,7 @@ pub use unversioned::*;
pub use user_directory::*;
pub use voip::*;
#[cfg(not(feature = "conduit_bin"))]
use super::State;
#[cfg(feature = "conduit_bin")]
use {
crate::ConduitResult, rocket::options, ruma::api::client::r0::to_device::send_event_to_device,
};
pub const DEVICE_ID_LENGTH: usize = 10;
pub const TOKEN_LENGTH: usize = 256;
pub const SESSION_ID_LENGTH: usize = 256;
/// # `OPTIONS`
///
/// Web clients use this to get CORS headers.
#[cfg(feature = "conduit_bin")]
#[options("/<_..>")]
#[tracing::instrument]
pub async fn options_route() -> ConduitResult<send_event_to_device::Response> {
Ok(send_event_to_device::Response {}.into())
}
pub const AUTO_GEN_PASSWORD_LENGTH: usize = 15;

@ -1,22 +1,14 @@
use crate::{database::DatabaseGuard, utils, ConduitResult, Ruma};
use ruma::api::client::r0::presence::{get_presence, set_presence};
use crate::{database::DatabaseGuard, utils, Result, Ruma};
use ruma::api::client::presence::{get_presence, set_presence};
use std::time::Duration;
#[cfg(feature = "conduit_bin")]
use rocket::{get, put};
/// # `PUT /_matrix/client/r0/presence/{userId}/status`
///
/// Sets the presence state of the sender user.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/presence/<_>/status", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_presence_route(
db: DatabaseGuard,
body: Ruma<set_presence::Request<'_>>,
) -> ConduitResult<set_presence::Response> {
body: Ruma<set_presence::v3::IncomingRequest>,
) -> Result<set_presence::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
for room_id in db.rooms.rooms_joined(sender_user) {
@ -46,7 +38,7 @@ pub async fn set_presence_route(
db.flush()?;
Ok(set_presence::Response {}.into())
Ok(set_presence::v3::Response {})
}
/// # `GET /_matrix/client/r0/presence/{userId}/status`
@ -54,15 +46,10 @@ pub async fn set_presence_route(
/// Gets the presence state of the given user.
///
/// - Only works if you share a room with the user
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/presence/<_>/status", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_presence_route(
db: DatabaseGuard,
body: Ruma<get_presence::Request<'_>>,
) -> ConduitResult<get_presence::Response> {
body: Ruma<get_presence::v3::IncomingRequest>,
) -> Result<get_presence::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut presence_event = None;
@ -84,7 +71,7 @@ pub async fn get_presence_route(
}
if let Some(presence) = presence_event {
Ok(get_presence::Response {
Ok(get_presence::v3::Response {
// TODO: Should ruma just use the presenceeventcontent type here?
status_msg: presence.content.status_msg,
currently_active: presence.content.currently_active,
@ -93,8 +80,7 @@ pub async fn get_presence_route(
.last_active_ago
.map(|millis| Duration::from_millis(millis.into())),
presence: presence.content.presence,
}
.into())
})
} else {
todo!();
}

@ -1,36 +1,28 @@
use crate::{database::DatabaseGuard, pdu::PduBuilder, utils, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, pdu::PduBuilder, utils, Error, Result, Ruma};
use ruma::{
api::{
client::{
error::ErrorKind,
r0::profile::{
profile::{
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
},
},
federation::{self, query::get_profile_information::v1::ProfileField},
},
events::{room::member::RoomMemberEventContent, EventType},
events::{room::member::RoomMemberEventContent, RoomEventType, StateEventType},
};
use serde_json::value::to_raw_value;
use std::sync::Arc;
#[cfg(feature = "conduit_bin")]
use rocket::{get, put};
/// # `PUT /_matrix/client/r0/profile/{userId}/displayname`
///
/// Updates the displayname.
///
/// - Also makes sure other users receive the update using presence EDUs
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/profile/<_>/displayname", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_displayname_route(
db: DatabaseGuard,
body: Ruma<set_display_name::Request<'_>>,
) -> ConduitResult<set_display_name::Response> {
body: Ruma<set_display_name::v3::IncomingRequest>,
) -> Result<set_display_name::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.users
@ -44,14 +36,14 @@ pub async fn set_displayname_route(
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
displayname: body.displayname.clone(),
..serde_json::from_str(
db.rooms
.room_state_get(
&room_id,
&EventType::RoomMember,
&StateEventType::RoomMember,
sender_user.as_str(),
)?
.ok_or_else(|| {
@ -116,7 +108,7 @@ pub async fn set_displayname_route(
db.flush()?;
Ok(set_display_name::Response {}.into())
Ok(set_display_name::v3::Response {})
}
/// # `GET /_matrix/client/r0/profile/{userId}/displayname`
@ -124,15 +116,10 @@ pub async fn set_displayname_route(
/// Returns the displayname of the user.
///
/// - If user is on another server: Fetches displayname over federation
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/profile/<_>/displayname", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_displayname_route(
db: DatabaseGuard,
body: Ruma<get_display_name::Request<'_>>,
) -> ConduitResult<get_display_name::Response> {
body: Ruma<get_display_name::v3::IncomingRequest>,
) -> Result<get_display_name::v3::Response> {
if body.user_id.server_name() != db.globals.server_name() {
let response = db
.sending
@ -146,16 +133,14 @@ pub async fn get_displayname_route(
)
.await?;
return Ok(get_display_name::Response {
return Ok(get_display_name::v3::Response {
displayname: response.displayname,
}
.into());
});
}
Ok(get_display_name::Response {
Ok(get_display_name::v3::Response {
displayname: db.users.displayname(&body.user_id)?,
}
.into())
})
}
/// # `PUT /_matrix/client/r0/profile/{userId}/avatar_url`
@ -163,15 +148,10 @@ pub async fn get_displayname_route(
/// Updates the avatar_url and blurhash.
///
/// - Also makes sure other users receive the update using presence EDUs
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/profile/<_>/avatar_url", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_avatar_url_route(
db: DatabaseGuard,
body: Ruma<set_avatar_url::Request<'_>>,
) -> ConduitResult<set_avatar_url::Response> {
body: Ruma<set_avatar_url::v3::IncomingRequest>,
) -> Result<set_avatar_url::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.users
@ -187,14 +167,14 @@ pub async fn set_avatar_url_route(
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
avatar_url: body.avatar_url.clone(),
..serde_json::from_str(
db.rooms
.room_state_get(
&room_id,
&EventType::RoomMember,
&StateEventType::RoomMember,
sender_user.as_str(),
)?
.ok_or_else(|| {
@ -259,7 +239,7 @@ pub async fn set_avatar_url_route(
db.flush()?;
Ok(set_avatar_url::Response {}.into())
Ok(set_avatar_url::v3::Response {})
}
/// # `GET /_matrix/client/r0/profile/{userId}/avatar_url`
@ -267,15 +247,10 @@ pub async fn set_avatar_url_route(
/// Returns the avatar_url and blurhash of the user.
///
/// - If user is on another server: Fetches avatar_url and blurhash over federation
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/profile/<_>/avatar_url", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_avatar_url_route(
db: DatabaseGuard,
body: Ruma<get_avatar_url::Request<'_>>,
) -> ConduitResult<get_avatar_url::Response> {
body: Ruma<get_avatar_url::v3::IncomingRequest>,
) -> Result<get_avatar_url::v3::Response> {
if body.user_id.server_name() != db.globals.server_name() {
let response = db
.sending
@ -289,18 +264,16 @@ pub async fn get_avatar_url_route(
)
.await?;
return Ok(get_avatar_url::Response {
return Ok(get_avatar_url::v3::Response {
avatar_url: response.avatar_url,
blurhash: response.blurhash,
}
.into());
});
}
Ok(get_avatar_url::Response {
Ok(get_avatar_url::v3::Response {
avatar_url: db.users.avatar_url(&body.user_id)?,
blurhash: db.users.blurhash(&body.user_id)?,
}
.into())
})
}
/// # `GET /_matrix/client/r0/profile/{userId}`
@ -308,15 +281,10 @@ pub async fn get_avatar_url_route(
/// Returns the displayname, avatar_url and blurhash of the user.
///
/// - If user is on another server: Fetches profile over federation
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/profile/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_profile_route(
db: DatabaseGuard,
body: Ruma<get_profile::Request<'_>>,
) -> ConduitResult<get_profile::Response> {
body: Ruma<get_profile::v3::IncomingRequest>,
) -> Result<get_profile::v3::Response> {
if body.user_id.server_name() != db.globals.server_name() {
let response = db
.sending
@ -330,12 +298,11 @@ pub async fn get_profile_route(
)
.await?;
return Ok(get_profile::Response {
return Ok(get_profile::v3::Response {
displayname: response.displayname,
avatar_url: response.avatar_url,
blurhash: response.blurhash,
}
.into());
});
}
if !db.users.exists(&body.user_id)? {
@ -346,10 +313,9 @@ pub async fn get_profile_route(
));
}
Ok(get_profile::Response {
Ok(get_profile::v3::Response {
avatar_url: db.users.avatar_url(&body.user_id)?,
blurhash: db.users.blurhash(&body.user_id)?,
displayname: db.users.displayname(&body.user_id)?,
}
.into())
})
}

@ -1,65 +1,59 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::{
api::client::{
error::ErrorKind,
r0::push::{
push::{
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions, get_pushrule_enabled,
get_pushrules_all, set_pusher, set_pushrule, set_pushrule_actions,
set_pushrule_enabled, RuleKind,
},
},
events::{push_rules::PushRulesEvent, EventType},
events::{push_rules::PushRulesEvent, GlobalAccountDataEventType},
push::{ConditionalPushRuleInit, PatternedPushRuleInit, SimplePushRuleInit},
};
#[cfg(feature = "conduit_bin")]
use rocket::{delete, get, post, put};
/// # `GET /_matrix/client/r0/pushrules`
///
/// Retrieves the push rules event for this user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/pushrules", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_pushrules_all_route(
db: DatabaseGuard,
body: Ruma<get_pushrules_all::Request>,
) -> ConduitResult<get_pushrules_all::Response> {
body: Ruma<get_pushrules_all::v3::Request>,
) -> Result<get_pushrules_all::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
))?;
Ok(get_pushrules_all::Response {
Ok(get_pushrules_all::v3::Response {
global: event.content.global,
}
.into())
})
}
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
///
/// Retrieves a single specified push rule for this user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/pushrules/<_>/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_pushrule_route(
db: DatabaseGuard,
body: Ruma<get_pushrule::Request<'_>>,
) -> ConduitResult<get_pushrule::Response> {
body: Ruma<get_pushrule::v3::IncomingRequest>,
) -> Result<get_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
@ -91,7 +85,7 @@ pub async fn get_pushrule_route(
};
if let Some(rule) = rule {
Ok(get_pushrule::Response { rule }.into())
Ok(get_pushrule::v3::Response { rule })
} else {
Err(Error::BadRequest(
ErrorKind::NotFound,
@ -103,15 +97,10 @@ pub async fn get_pushrule_route(
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
///
/// Creates a single specified push rule for this user.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/pushrules/<_>/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_pushrule_route(
db: DatabaseGuard,
body: Ruma<set_pushrule::Request<'_>>,
) -> ConduitResult<set_pushrule::Response> {
body: Ruma<set_pushrule::v3::IncomingRequest>,
) -> Result<set_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let body = body.body;
@ -124,7 +113,11 @@ pub async fn set_pushrule_route(
let mut event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
@ -193,26 +186,26 @@ pub async fn set_pushrule_route(
_ => {}
}
db.account_data
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
db.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&event,
&db.globals,
)?;
db.flush()?;
Ok(set_pushrule::Response {}.into())
Ok(set_pushrule::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
///
/// Gets the actions of a single specified push rule for this user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/pushrules/<_>/<_>/<_>/actions", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_pushrule_actions_route(
db: DatabaseGuard,
body: Ruma<get_pushrule_actions::Request<'_>>,
) -> ConduitResult<get_pushrule_actions::Response> {
body: Ruma<get_pushrule_actions::v3::IncomingRequest>,
) -> Result<get_pushrule_actions::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != "global" {
@ -224,7 +217,11 @@ pub async fn get_pushrule_actions_route(
let mut event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
@ -257,24 +254,18 @@ pub async fn get_pushrule_actions_route(
db.flush()?;
Ok(get_pushrule_actions::Response {
Ok(get_pushrule_actions::v3::Response {
actions: actions.unwrap_or_default(),
}
.into())
})
}
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
///
/// Sets the actions of a single specified push rule for this user.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/pushrules/<_>/<_>/<_>/actions", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_pushrule_actions_route(
db: DatabaseGuard,
body: Ruma<set_pushrule_actions::Request<'_>>,
) -> ConduitResult<set_pushrule_actions::Response> {
body: Ruma<set_pushrule_actions::v3::IncomingRequest>,
) -> Result<set_pushrule_actions::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != "global" {
@ -286,7 +277,11 @@ pub async fn set_pushrule_actions_route(
let mut event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
@ -327,26 +322,26 @@ pub async fn set_pushrule_actions_route(
_ => {}
};
db.account_data
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
db.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&event,
&db.globals,
)?;
db.flush()?;
Ok(set_pushrule_actions::Response {}.into())
Ok(set_pushrule_actions::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
///
/// Gets the enabled status of a single specified push rule for this user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/pushrules/<_>/<_>/<_>/enabled", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_pushrule_enabled_route(
db: DatabaseGuard,
body: Ruma<get_pushrule_enabled::Request<'_>>,
) -> ConduitResult<get_pushrule_enabled::Response> {
body: Ruma<get_pushrule_enabled::v3::IncomingRequest>,
) -> Result<get_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != "global" {
@ -358,7 +353,11 @@ pub async fn get_pushrule_enabled_route(
let mut event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
@ -396,21 +395,16 @@ pub async fn get_pushrule_enabled_route(
db.flush()?;
Ok(get_pushrule_enabled::Response { enabled }.into())
Ok(get_pushrule_enabled::v3::Response { enabled })
}
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
///
/// Sets the enabled status of a single specified push rule for this user.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/pushrules/<_>/<_>/<_>/enabled", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_pushrule_enabled_route(
db: DatabaseGuard,
body: Ruma<set_pushrule_enabled::Request<'_>>,
) -> ConduitResult<set_pushrule_enabled::Response> {
body: Ruma<set_pushrule_enabled::v3::IncomingRequest>,
) -> Result<set_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != "global" {
@ -422,7 +416,11 @@ pub async fn set_pushrule_enabled_route(
let mut event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
@ -468,26 +466,26 @@ pub async fn set_pushrule_enabled_route(
_ => {}
}
db.account_data
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
db.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&event,
&db.globals,
)?;
db.flush()?;
Ok(set_pushrule_enabled::Response {}.into())
Ok(set_pushrule_enabled::v3::Response {})
}
/// # `DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
///
/// Deletes a single specified push rule for this user.
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/r0/pushrules/<_>/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_pushrule_route(
db: DatabaseGuard,
body: Ruma<delete_pushrule::Request<'_>>,
) -> ConduitResult<delete_pushrule::Response> {
body: Ruma<delete_pushrule::v3::IncomingRequest>,
) -> Result<delete_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if body.scope != "global" {
@ -499,7 +497,11 @@ pub async fn delete_pushrule_route(
let mut event: PushRulesEvent = db
.account_data
.get(None, sender_user, EventType::PushRules)?
.get(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"PushRules event not found.",
@ -535,32 +537,31 @@ pub async fn delete_pushrule_route(
_ => {}
}
db.account_data
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
db.account_data.update(
None,
sender_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&event,
&db.globals,
)?;
db.flush()?;
Ok(delete_pushrule::Response {}.into())
Ok(delete_pushrule::v3::Response {})
}
/// # `GET /_matrix/client/r0/pushers`
///
/// Gets all currently active pushers for the sender user.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/pushers", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_pushers_route(
db: DatabaseGuard,
body: Ruma<get_pushers::Request>,
) -> ConduitResult<get_pushers::Response> {
body: Ruma<get_pushers::v3::Request>,
) -> Result<get_pushers::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(get_pushers::Response {
Ok(get_pushers::v3::Response {
pushers: db.pusher.get_pushers(sender_user)?,
}
.into())
})
}
/// # `POST /_matrix/client/r0/pushers/set`
@ -568,15 +569,10 @@ pub async fn get_pushers_route(
/// Adds a pusher for the sender user.
///
/// - TODO: Handle `append`
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/pushers/set", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_pushers_route(
db: DatabaseGuard,
body: Ruma<set_pusher::Request>,
) -> ConduitResult<set_pusher::Response> {
body: Ruma<set_pusher::v3::Request>,
) -> Result<set_pusher::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let pusher = body.pusher.clone();
@ -584,5 +580,5 @@ pub async fn set_pushers_route(
db.flush()?;
Ok(set_pusher::Response::default().into())
Ok(set_pusher::v3::Response::default())
}

@ -1,33 +1,22 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::{
api::client::{
error::ErrorKind,
r0::{read_marker::set_read_marker, receipt::create_receipt},
},
events::{AnyEphemeralRoomEvent, EventType},
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
events::RoomAccountDataEventType,
receipt::ReceiptType,
MilliSecondsSinceUnixEpoch,
};
use std::collections::BTreeMap;
#[cfg(feature = "conduit_bin")]
use rocket::post;
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
///
/// Sets different types of read markers.
///
/// - Updates fully-read account data event to `fully_read`
/// - If `read_receipt` is set: Update private marker and public read receipt EDU
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/read_markers", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn set_read_marker_route(
db: DatabaseGuard,
body: Ruma<set_read_marker::Request<'_>>,
) -> ConduitResult<set_read_marker::Response> {
body: Ruma<set_read_marker::v3::IncomingRequest>,
) -> Result<set_read_marker::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
@ -38,7 +27,7 @@ pub async fn set_read_marker_route(
db.account_data.update(
Some(&body.room_id),
sender_user,
EventType::FullyRead,
RoomAccountDataEventType::FullyRead,
&fully_read_event,
&db.globals,
)?;
@ -73,31 +62,26 @@ pub async fn set_read_marker_route(
db.rooms.edus.readreceipt_update(
sender_user,
&body.room_id,
AnyEphemeralRoomEvent::Receipt(ruma::events::receipt::ReceiptEvent {
ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
}),
},
&db.globals,
)?;
}
db.flush()?;
Ok(set_read_marker::Response {}.into())
Ok(set_read_marker::v3::Response {})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}`
///
/// Sets private read marker and public read receipt EDU.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/receipt/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn create_receipt_route(
db: DatabaseGuard,
body: Ruma<create_receipt::Request<'_>>,
) -> ConduitResult<create_receipt::Response> {
body: Ruma<create_receipt::v3::IncomingRequest>,
) -> Result<create_receipt::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
db.rooms.edus.private_read_set(
@ -130,14 +114,14 @@ pub async fn create_receipt_route(
db.rooms.edus.readreceipt_update(
sender_user,
&body.room_id,
AnyEphemeralRoomEvent::Receipt(ruma::events::receipt::ReceiptEvent {
ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(),
}),
},
&db.globals,
)?;
db.flush()?;
Ok(create_receipt::Response {}.into())
Ok(create_receipt::v3::Response {})
}

@ -1,13 +1,11 @@
use std::sync::Arc;
use crate::{database::DatabaseGuard, pdu::PduBuilder, ConduitResult, Ruma};
use crate::{database::DatabaseGuard, pdu::PduBuilder, Result, Ruma};
use ruma::{
api::client::r0::redact::redact_event,
events::{room::redaction::RoomRedactionEventContent, EventType},
api::client::redact::redact_event,
events::{room::redaction::RoomRedactionEventContent, RoomEventType},
};
#[cfg(feature = "conduit_bin")]
use rocket::put;
use serde_json::value::to_raw_value;
/// # `PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}`
@ -15,15 +13,10 @@ use serde_json::value::to_raw_value;
/// Tries to send a redaction event into the room.
///
/// - TODO: Handle txn id
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/rooms/<_>/redact/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn redact_event_route(
db: DatabaseGuard,
body: Ruma<redact_event::Request<'_>>,
) -> ConduitResult<redact_event::Response> {
body: Ruma<redact_event::v3::IncomingRequest>,
) -> Result<redact_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let body = body.body;
@ -39,7 +32,7 @@ pub async fn redact_event_route(
let event_id = db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomRedaction,
event_type: RoomEventType::RoomRedaction,
content: to_raw_value(&RoomRedactionEventContent {
reason: body.reason.clone(),
})
@ -59,5 +52,5 @@ pub async fn redact_event_route(
db.flush()?;
let event_id = (*event_id).to_owned();
Ok(redact_event::Response { event_id }.into())
Ok(redact_event::v3::Response { event_id })
}

@ -1,26 +1,18 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, utils::HtmlEscape, Error, Result, Ruma};
use ruma::{
api::client::{error::ErrorKind, r0::room::report_content},
api::client::{error::ErrorKind, room::report_content},
events::room::message,
int,
};
#[cfg(feature = "conduit_bin")]
use rocket::{http::RawStr, post};
/// # `POST /_matrix/client/r0/rooms/{roomId}/report/{eventId}`
///
/// Reports an inappropriate event to homeserver admins
///
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/report/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn report_event_route(
db: DatabaseGuard,
body: Ruma<report_content::Request<'_>>,
) -> ConduitResult<report_content::Response> {
body: Ruma<report_content::v3::IncomingRequest>,
) -> Result<report_content::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let pdu = match db.rooms.get_pdu(&body.event_id)? {
@ -33,14 +25,14 @@ pub async fn report_event_route(
}
};
if body.score > int!(0) || body.score < int!(-100) {
if let Some(true) = body.score.map(|s| s > int!(0) || s < int!(-100)) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid score, must be within 0 to -100",
));
};
if body.reason.chars().count() > 250 {
if let Some(true) = body.reason.clone().map(|s| s.chars().count() > 250) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Reason too long, should be 250 characters or fewer",
@ -51,30 +43,30 @@ pub async fn report_event_route(
.send_message(message::RoomMessageEventContent::text_html(
format!(
"Report received from: {}\n\n\
Event ID: {}\n\
Room ID: {}\n\
Sent By: {}\n\n\
Report Score: {}\n\
Report Reason: {}",
Event ID: {:?}\n\
Room ID: {:?}\n\
Sent By: {:?}\n\n\
Report Score: {:?}\n\
Report Reason: {:?}",
sender_user, pdu.event_id, pdu.room_id, pdu.sender, body.score, body.reason
),
format!(
"<details><summary>Report received from: <a href=\"https://matrix.to/#/{0}\">{0}\
</a></summary><ul><li>Event Info<ul><li>Event ID: <code>{1}</code>\
<a href=\"https://matrix.to/#/{2}/{1}\">🔗</a></li><li>Room ID: <code>{2}</code>\
</li><li>Sent By: <a href=\"https://matrix.to/#/{3}\">{3}</a></li></ul></li><li>\
Report Info<ul><li>Report Score: {4}</li><li>Report Reason: {5}</li></ul></li>\
"<details><summary>Report received from: <a href=\"https://matrix.to/#/{0:?}\">{0:?}\
</a></summary><ul><li>Event Info<ul><li>Event ID: <code>{1:?}</code>\
<a href=\"https://matrix.to/#/{2:?}/{1:?}\">🔗</a></li><li>Room ID: <code>{2:?}</code>\
</li><li>Sent By: <a href=\"https://matrix.to/#/{3:?}\">{3:?}</a></li></ul></li><li>\
Report Info<ul><li>Report Score: {4:?}</li><li>Report Reason: {5}</li></ul></li>\
</ul></details>",
sender_user,
pdu.event_id,
pdu.room_id,
pdu.sender,
body.score,
RawStr::new(&body.reason).html_escape()
HtmlEscape(body.reason.as_deref().unwrap_or(""))
),
));
db.flush()?;
Ok(report_content::Response {}.into())
Ok(report_content::v3::Response {})
}

@ -1,11 +1,10 @@
use crate::{
client_server::invite_helper, database::DatabaseGuard, pdu::PduBuilder, ConduitResult, Error,
Ruma,
client_server::invite_helper, database::DatabaseGuard, pdu::PduBuilder, Error, Result, Ruma,
};
use ruma::{
api::client::{
error::ErrorKind,
r0::room::{self, aliases, create_room, get_room_event, upgrade_room},
room::{self, aliases, create_room, get_room_event, upgrade_room},
},
events::{
room::{
@ -20,19 +19,16 @@ use ruma::{
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
EventType,
RoomEventType, StateEventType,
},
int,
serde::{CanonicalJsonObject, JsonObject},
RoomAliasId, RoomId, RoomVersionId,
RoomAliasId, RoomId,
};
use serde_json::{json, value::to_raw_value};
use std::{cmp::max, collections::BTreeMap, sync::Arc};
use tracing::{info, warn};
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
/// # `POST /_matrix/client/r0/createRoom`
///
/// Creates a new room.
@ -49,15 +45,12 @@ use rocket::{get, post};
/// - Send events listed in initial state
/// - Send events implied by `name` and `topic`
/// - Send invite events
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/createRoom", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn create_room_route(
db: DatabaseGuard,
body: Ruma<create_room::Request<'_>>,
) -> ConduitResult<create_room::Response> {
body: Ruma<create_room::v3::IncomingRequest>,
) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset;
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let room_id = RoomId::new(db.globals.server_name());
@ -107,7 +100,7 @@ pub async fn create_room_route(
let room_version = match body.room_version.clone() {
Some(room_version) => {
if room_version == RoomVersionId::V5 || room_version == RoomVersionId::V6 {
if db.rooms.is_supported_version(&db, &room_version) {
room_version
} else {
return Err(Error::BadRequest(
@ -116,7 +109,7 @@ pub async fn create_room_route(
));
}
}
None => RoomVersionId::V6,
None => db.globals.default_room_version(),
};
let content = match &body.creation_content {
@ -172,7 +165,7 @@ pub async fn create_room_route(
// 1. The room create event
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomCreate,
event_type: RoomEventType::RoomCreate,
content: to_raw_value(&content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
@ -187,7 +180,7 @@ pub async fn create_room_route(
// 2. Let the room creator join
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: db.users.displayname(sender_user)?,
@ -216,15 +209,15 @@ pub async fn create_room_route(
.preset
.clone()
.unwrap_or_else(|| match &body.visibility {
room::Visibility::Private => create_room::RoomPreset::PrivateChat,
room::Visibility::Public => create_room::RoomPreset::PublicChat,
_ => create_room::RoomPreset::PrivateChat, // Room visibility should not be custom
room::Visibility::Private => RoomPreset::PrivateChat,
room::Visibility::Public => RoomPreset::PublicChat,
_ => RoomPreset::PrivateChat, // Room visibility should not be custom
});
let mut users = BTreeMap::new();
users.insert(sender_user.clone(), int!(100));
if preset == create_room::RoomPreset::TrustedPrivateChat {
if preset == RoomPreset::TrustedPrivateChat {
for invite_ in &body.invite {
users.insert(invite_.clone(), int!(100));
}
@ -249,7 +242,7 @@ pub async fn create_room_route(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomPowerLevels,
event_type: RoomEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content)
.expect("to_raw_value always works on serde_json::Value"),
unsigned: None,
@ -266,7 +259,7 @@ pub async fn create_room_route(
if let Some(room_alias_id) = &alias {
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomCanonicalAlias,
event_type: RoomEventType::RoomCanonicalAlias,
content: to_raw_value(&RoomCanonicalAliasEventContent {
alias: Some(room_alias_id.to_owned()),
alt_aliases: vec![],
@ -288,9 +281,9 @@ pub async fn create_room_route(
// 5.1 Join Rules
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomJoinRules,
event_type: RoomEventType::RoomJoinRules,
content: to_raw_value(&RoomJoinRulesEventContent::new(match preset {
create_room::RoomPreset::PublicChat => JoinRule::Public,
RoomPreset::PublicChat => JoinRule::Public,
// according to spec "invite" is the default
_ => JoinRule::Invite,
}))
@ -308,7 +301,7 @@ pub async fn create_room_route(
// 5.2 History Visibility
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomHistoryVisibility,
event_type: RoomEventType::RoomHistoryVisibility,
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(
HistoryVisibility::Shared,
))
@ -326,9 +319,9 @@ pub async fn create_room_route(
// 5.3 Guest Access
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomGuestAccess,
event_type: RoomEventType::RoomGuestAccess,
content: to_raw_value(&RoomGuestAccessEventContent::new(match preset {
create_room::RoomPreset::PublicChat => GuestAccess::Forbidden,
RoomPreset::PublicChat => GuestAccess::Forbidden,
_ => GuestAccess::CanJoin,
}))
.expect("event is valid, we just created it"),
@ -353,7 +346,8 @@ pub async fn create_room_route(
pdu_builder.state_key.get_or_insert_with(|| "".to_owned());
// Silently skip encryption events if they are not allowed
if pdu_builder.event_type == EventType::RoomEncryption && !db.globals.allow_encryption() {
if pdu_builder.event_type == RoomEventType::RoomEncryption && !db.globals.allow_encryption()
{
continue;
}
@ -365,7 +359,7 @@ pub async fn create_room_route(
if let Some(name) = &body.name {
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomName,
event_type: RoomEventType::RoomName,
content: to_raw_value(&RoomNameEventContent::new(Some(name.clone())))
.expect("event is valid, we just created it"),
unsigned: None,
@ -382,7 +376,7 @@ pub async fn create_room_route(
if let Some(topic) = &body.topic {
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomTopic,
event_type: RoomEventType::RoomTopic,
content: to_raw_value(&RoomTopicEventContent {
topic: topic.clone(),
})
@ -417,7 +411,7 @@ pub async fn create_room_route(
db.flush()?;
Ok(create_room::Response::new(room_id).into())
Ok(create_room::v3::Response::new(room_id))
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
@ -425,15 +419,10 @@ pub async fn create_room_route(
/// Gets a single event.
///
/// - You have to currently be joined to the room (TODO: Respect history visibility)
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/event/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_room_event_route(
db: DatabaseGuard,
body: Ruma<get_room_event::Request<'_>>,
) -> ConduitResult<get_room_event::Response> {
body: Ruma<get_room_event::v3::IncomingRequest>,
) -> Result<get_room_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !db.rooms.is_joined(sender_user, &body.room_id)? {
@ -443,14 +432,13 @@ pub async fn get_room_event_route(
));
}
Ok(get_room_event::Response {
Ok(get_room_event::v3::Response {
event: db
.rooms
.get_pdu(&body.event_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Event not found."))?
.to_room_event(),
}
.into())
})
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
@ -458,15 +446,10 @@ pub async fn get_room_event_route(
/// Lists all aliases of the room.
///
/// - Only users joined to the room are allowed to call this TODO: Allow any user to call it if history_visibility is world readable
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/aliases", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_room_aliases_route(
db: DatabaseGuard,
body: Ruma<aliases::Request<'_>>,
) -> ConduitResult<aliases::Response> {
body: Ruma<aliases::v3::IncomingRequest>,
) -> Result<aliases::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !db.rooms.is_joined(sender_user, &body.room_id)? {
@ -476,14 +459,13 @@ pub async fn get_room_aliases_route(
));
}
Ok(aliases::Response {
Ok(aliases::v3::Response {
aliases: db
.rooms
.room_aliases(&body.room_id)
.filter_map(|a| a.ok())
.collect(),
}
.into())
})
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
@ -496,18 +478,13 @@ pub async fn get_room_aliases_route(
/// - Transfers some state events
/// - Moves local aliases
/// - Modifies old room power levels to prevent users from speaking
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/rooms/<_>/upgrade", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn upgrade_room_route(
db: DatabaseGuard,
body: Ruma<upgrade_room::Request<'_>>,
) -> ConduitResult<upgrade_room::Response> {
body: Ruma<upgrade_room::v3::IncomingRequest>,
) -> Result<upgrade_room::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !matches!(body.new_version, RoomVersionId::V5 | RoomVersionId::V6) {
if !db.rooms.is_supported_version(&db, &body.new_version) {
return Err(Error::BadRequest(
ErrorKind::UnsupportedRoomVersion,
"This server does not support that room version.",
@ -533,7 +510,7 @@ pub async fn upgrade_room_route(
// Fail if the sender does not have the required permissions
let tombstone_event_id = db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomTombstone,
event_type: RoomEventType::RoomTombstone,
content: to_raw_value(&RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.clone(),
@ -564,7 +541,7 @@ pub async fn upgrade_room_route(
// Get the old room creation event
let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>(
db.rooms
.room_state_get(&body.room_id, &EventType::RoomCreate, "")?
.room_state_get(&body.room_id, &StateEventType::RoomCreate, "")?
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
.content
.get(),
@ -613,7 +590,7 @@ pub async fn upgrade_room_route(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomCreate,
event_type: RoomEventType::RoomCreate,
content: to_raw_value(&create_event_content)
.expect("event is valid, we just created it"),
unsigned: None,
@ -629,7 +606,7 @@ pub async fn upgrade_room_route(
// Join the new room
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: db.users.displayname(sender_user)?,
@ -653,15 +630,15 @@ pub async fn upgrade_room_route(
// Recommended transferable state events list from the specs
let transferable_state_events = vec![
EventType::RoomServerAcl,
EventType::RoomEncryption,
EventType::RoomName,
EventType::RoomAvatar,
EventType::RoomTopic,
EventType::RoomGuestAccess,
EventType::RoomHistoryVisibility,
EventType::RoomJoinRules,
EventType::RoomPowerLevels,
StateEventType::RoomServerAcl,
StateEventType::RoomEncryption,
StateEventType::RoomName,
StateEventType::RoomAvatar,
StateEventType::RoomTopic,
StateEventType::RoomGuestAccess,
StateEventType::RoomHistoryVisibility,
StateEventType::RoomJoinRules,
StateEventType::RoomPowerLevels,
];
// Replicate transferable state events to the new room
@ -673,7 +650,7 @@ pub async fn upgrade_room_route(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type,
event_type: event_type.to_string().into(),
content: event_content,
unsigned: None,
state_key: Some("".to_owned()),
@ -695,7 +672,7 @@ pub async fn upgrade_room_route(
// Get the old room power levels
let mut power_levels_event_content: RoomPowerLevelsEventContent = serde_json::from_str(
db.rooms
.room_state_get(&body.room_id, &EventType::RoomPowerLevels, "")?
.room_state_get(&body.room_id, &StateEventType::RoomPowerLevels, "")?
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
.content
.get(),
@ -710,7 +687,7 @@ pub async fn upgrade_room_route(
// Modify the power levels in the old room to prevent sending of events and inviting new users
let _ = db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomPowerLevels,
event_type: RoomEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_event_content)
.expect("event is valid, we just created it"),
unsigned: None,
@ -728,5 +705,5 @@ pub async fn upgrade_room_route(
db.flush()?;
// Return the replacement room id
Ok(upgrade_room::Response { replacement_room }.into())
Ok(upgrade_room::v3::Response { replacement_room })
}

@ -1,9 +1,12 @@
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use ruma::api::client::{error::ErrorKind, r0::search::search_events};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::api::client::{
error::ErrorKind,
search::search_events::{
self,
v3::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
},
};
#[cfg(feature = "conduit_bin")]
use rocket::post;
use search_events::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult};
use std::collections::BTreeMap;
/// # `POST /_matrix/client/r0/search`
@ -11,19 +14,14 @@ use std::collections::BTreeMap;
/// Searches rooms for messages.
///
/// - Only works if the user is currently joined to the room (TODO: Respect history visibility)
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/search", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn search_events_route(
db: DatabaseGuard,
body: Ruma<search_events::Request<'_>>,
) -> ConduitResult<search_events::Response> {
body: Ruma<search_events::v3::IncomingRequest>,
) -> Result<search_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let search_criteria = body.search_categories.room_events.as_ref().unwrap();
let filter = search_criteria.filter.clone().unwrap_or_default();
let filter = &search_criteria.filter;
let room_ids = filter.rooms.clone().unwrap_or_else(|| {
db.rooms
@ -104,7 +102,7 @@ pub async fn search_events_route(
Some((skip + limit).to_string())
};
Ok(search_events::Response::new(ResultCategories {
Ok(search_events::v3::Response::new(ResultCategories {
room_events: ResultRoomEvents {
count: Some((results.len() as u32).into()), // TODO: set this to none. Element shouldn't depend on it
groups: BTreeMap::new(), // TODO
@ -117,6 +115,5 @@ pub async fn search_events_route(
.map(str::to_lowercase)
.collect(),
},
})
.into())
}))
}

@ -1,12 +1,10 @@
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{database::DatabaseGuard, utils, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, utils, Error, Result, Ruma};
use ruma::{
api::client::{
error::ErrorKind,
r0::{
session::{get_login_types, login, logout, logout_all},
uiaa::IncomingUserIdentifier,
},
session::{get_login_types, login, logout, logout_all},
uiaa::IncomingUserIdentifier,
},
UserId,
};
@ -16,25 +14,19 @@ use tracing::info;
#[derive(Debug, Deserialize)]
struct Claims {
sub: String,
exp: usize,
//exp: usize,
}
#[cfg(feature = "conduit_bin")]
use rocket::{get, post};
/// # `GET /_matrix/client/r0/login`
///
/// Get the supported login types of this server. One of these should be used as the `type` field
/// when logging in.
#[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/login"))]
#[tracing::instrument]
pub async fn get_login_types_route() -> ConduitResult<get_login_types::Response> {
Ok(
get_login_types::Response::new(vec![get_login_types::LoginType::Password(
Default::default(),
)])
.into(),
)
pub async fn get_login_types_route(
_body: Ruma<get_login_types::v3::IncomingRequest>,
) -> Result<get_login_types::v3::Response> {
Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(Default::default()),
]))
}
/// # `POST /_matrix/client/r0/login`
@ -48,24 +40,19 @@ pub async fn get_login_types_route() -> ConduitResult<get_login_types::Response>
///
/// Note: You can use [`GET /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
/// supported login types.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/login", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn login_route(
db: DatabaseGuard,
body: Ruma<login::Request<'_>>,
) -> ConduitResult<login::Response> {
body: Ruma<login::v3::IncomingRequest>,
) -> Result<login::v3::Response> {
// Validate login method
// TODO: Other login methods
let user_id = match &body.login_info {
login::IncomingLoginInfo::Password(login::IncomingPassword {
login::v3::IncomingLoginInfo::Password(login::v3::IncomingPassword {
identifier,
password,
}) => {
let username = if let IncomingUserIdentifier::MatrixId(matrix_id) = identifier {
matrix_id
let username = if let IncomingUserIdentifier::UserIdOrLocalpart(user_id) = identifier {
user_id.to_lowercase()
} else {
return Err(Error::BadRequest(ErrorKind::Forbidden, "Bad login type."));
};
@ -97,7 +84,7 @@ pub async fn login_route(
user_id
}
login::IncomingLoginInfo::Token(login::IncomingToken { token }) => {
login::v3::IncomingLoginInfo::Token(login::v3::IncomingToken { token }) => {
if let Some(jwt_decoding_key) = db.globals.jwt_decoding_key() {
let token = jsonwebtoken::decode::<Claims>(
token,
@ -155,14 +142,13 @@ pub async fn login_route(
db.flush()?;
Ok(login::Response {
Ok(login::v3::Response {
user_id,
access_token: token,
home_server: Some(db.globals.server_name().to_owned()),
device_id,
well_known: None,
}
.into())
})
}
/// # `POST /_matrix/client/r0/logout`
@ -173,15 +159,10 @@ pub async fn login_route(
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/logout", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn logout_route(
db: DatabaseGuard,
body: Ruma<logout::Request>,
) -> ConduitResult<logout::Response> {
body: Ruma<logout::v3::Request>,
) -> Result<logout::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
@ -189,7 +170,7 @@ pub async fn logout_route(
db.flush()?;
Ok(logout::Response::new().into())
Ok(logout::v3::Response::new())
}
/// # `POST /_matrix/client/r0/logout/all`
@ -203,15 +184,10 @@ pub async fn logout_route(
///
/// Note: This is equivalent to calling [`GET /_matrix/client/r0/logout`](fn.logout_route.html)
/// from each device of this user.
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/logout/all", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn logout_all_route(
db: DatabaseGuard,
body: Ruma<logout_all::Request>,
) -> ConduitResult<logout_all::Response> {
body: Ruma<logout_all::v3::Request>,
) -> Result<logout_all::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
for device_id in db.users.all_device_ids(sender_user).flatten() {
@ -220,5 +196,5 @@ pub async fn logout_all_route(
db.flush()?;
Ok(logout_all::Response::new().into())
Ok(logout_all::v3::Response::new())
}

@ -1,27 +1,24 @@
use std::sync::Arc;
use crate::{
database::DatabaseGuard, pdu::PduBuilder, ConduitResult, Database, Error, Result, Ruma,
database::DatabaseGuard, pdu::PduBuilder, Database, Error, Result, Ruma, RumaResponse,
};
use ruma::{
api::client::{
error::ErrorKind,
r0::state::{get_state_events, get_state_events_for_key, send_state_event},
state::{get_state_events, get_state_events_for_key, send_state_event},
},
events::{
room::{
canonical_alias::RoomCanonicalAliasEventContent,
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
},
AnyStateEventContent, EventType,
AnyStateEventContent, StateEventType,
},
serde::Raw,
EventId, RoomId, UserId,
};
#[cfg(feature = "conduit_bin")]
use rocket::{get, put};
/// # `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}`
///
/// Sends a state event into the room.
@ -29,22 +26,17 @@ use rocket::{get, put};
/// - The only requirement for the content is that it has to be valid json
/// - Tries to send the event into the room, auth rules will determine if it is allowed
/// - If event is new canonical_alias: Rejects if alias is incorrect
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/rooms/<_>/state/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn send_state_event_for_key_route(
db: DatabaseGuard,
body: Ruma<send_state_event::Request<'_>>,
) -> ConduitResult<send_state_event::Response> {
body: Ruma<send_state_event::v3::IncomingRequest>,
) -> Result<send_state_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let event_id = send_state_event_for_key_helper(
&db,
sender_user,
&body.room_id,
EventType::from(&*body.event_type),
&body.event_type,
&body.body.body, // Yes, I hate it too
body.state_key.to_owned(),
)
@ -53,7 +45,7 @@ pub async fn send_state_event_for_key_route(
db.flush()?;
let event_id = (*event_id).to_owned();
Ok(send_state_event::Response { event_id }.into())
Ok(send_state_event::v3::Response { event_id })
}
/// # `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}`
@ -63,19 +55,14 @@ pub async fn send_state_event_for_key_route(
/// - The only requirement for the content is that it has to be valid json
/// - Tries to send the event into the room, auth rules will determine if it is allowed
/// - If event is new canonical_alias: Rejects if alias is incorrect
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/rooms/<_>/state/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn send_state_event_for_empty_key_route(
db: DatabaseGuard,
body: Ruma<send_state_event::Request<'_>>,
) -> ConduitResult<send_state_event::Response> {
body: Ruma<send_state_event::v3::IncomingRequest>,
) -> Result<RumaResponse<send_state_event::v3::Response>> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// Forbid m.room.encryption if encryption is disabled
if &body.event_type == "m.room.encryption" && !db.globals.allow_encryption() {
if body.event_type == StateEventType::RoomEncryption && !db.globals.allow_encryption() {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Encryption has been disabled",
@ -86,7 +73,7 @@ pub async fn send_state_event_for_empty_key_route(
&db,
sender_user,
&body.room_id,
EventType::from(&*body.event_type),
&body.event_type.to_string().into(),
&body.body.body,
body.state_key.to_owned(),
)
@ -95,7 +82,7 @@ pub async fn send_state_event_for_empty_key_route(
db.flush()?;
let event_id = (*event_id).to_owned();
Ok(send_state_event::Response { event_id }.into())
Ok(send_state_event::v3::Response { event_id }.into())
}
/// # `GET /_matrix/client/r0/rooms/{roomid}/state`
@ -103,15 +90,10 @@ pub async fn send_state_event_for_empty_key_route(
/// Get all state events for a room.
///
/// - If not joined: Only works if current room history visibility is world readable
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/state", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_state_events_route(
db: DatabaseGuard,
body: Ruma<get_state_events::Request<'_>>,
) -> ConduitResult<get_state_events::Response> {
body: Ruma<get_state_events::v3::IncomingRequest>,
) -> Result<get_state_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
#[allow(clippy::blocks_in_if_conditions)]
@ -120,7 +102,7 @@ pub async fn get_state_events_route(
if !db.rooms.is_joined(sender_user, &body.room_id)?
&& !matches!(
db.rooms
.room_state_get(&body.room_id, &EventType::RoomHistoryVisibility, "")?
.room_state_get(&body.room_id, &StateEventType::RoomHistoryVisibility, "")?
.map(|event| {
serde_json::from_str(event.content.get())
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
@ -139,15 +121,15 @@ pub async fn get_state_events_route(
));
}
Ok(get_state_events::Response {
Ok(get_state_events::v3::Response {
room_state: db
.rooms
.room_state_full(&body.room_id)?
.room_state_full(&body.room_id)
.await?
.values()
.map(|pdu| pdu.to_state_event())
.collect(),
}
.into())
})
}
/// # `GET /_matrix/client/r0/rooms/{roomid}/state/{eventType}/{stateKey}`
@ -155,15 +137,10 @@ pub async fn get_state_events_route(
/// Get single state event of a room.
///
/// - If not joined: Only works if current room history visibility is world readable
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/state/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_state_events_for_key_route(
db: DatabaseGuard,
body: Ruma<get_state_events_for_key::Request<'_>>,
) -> ConduitResult<get_state_events_for_key::Response> {
body: Ruma<get_state_events_for_key::v3::IncomingRequest>,
) -> Result<get_state_events_for_key::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
#[allow(clippy::blocks_in_if_conditions)]
@ -172,7 +149,7 @@ pub async fn get_state_events_for_key_route(
if !db.rooms.is_joined(sender_user, &body.room_id)?
&& !matches!(
db.rooms
.room_state_get(&body.room_id, &EventType::RoomHistoryVisibility, "")?
.room_state_get(&body.room_id, &StateEventType::RoomHistoryVisibility, "")?
.map(|event| {
serde_json::from_str(event.content.get())
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
@ -199,11 +176,10 @@ pub async fn get_state_events_for_key_route(
"State event not found.",
))?;
Ok(get_state_events_for_key::Response {
Ok(get_state_events_for_key::v3::Response {
content: serde_json::from_str(event.content.get())
.map_err(|_| Error::bad_database("Invalid event content in database"))?,
}
.into())
})
}
/// # `GET /_matrix/client/r0/rooms/{roomid}/state/{eventType}`
@ -211,15 +187,10 @@ pub async fn get_state_events_for_key_route(
/// Get single state event of a room.
///
/// - If not joined: Only works if current room history visibility is world readable
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/rooms/<_>/state/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_state_events_for_empty_key_route(
db: DatabaseGuard,
body: Ruma<get_state_events_for_key::Request<'_>>,
) -> ConduitResult<get_state_events_for_key::Response> {
body: Ruma<get_state_events_for_key::v3::IncomingRequest>,
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
#[allow(clippy::blocks_in_if_conditions)]
@ -228,7 +199,7 @@ pub async fn get_state_events_for_empty_key_route(
if !db.rooms.is_joined(sender_user, &body.room_id)?
&& !matches!(
db.rooms
.room_state_get(&body.room_id, &EventType::RoomHistoryVisibility, "")?
.room_state_get(&body.room_id, &StateEventType::RoomHistoryVisibility, "")?
.map(|event| {
serde_json::from_str(event.content.get())
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
@ -255,7 +226,7 @@ pub async fn get_state_events_for_empty_key_route(
"State event not found.",
))?;
Ok(get_state_events_for_key::Response {
Ok(get_state_events_for_key::v3::Response {
content: serde_json::from_str(event.content.get())
.map_err(|_| Error::bad_database("Invalid event content in database"))?,
}
@ -266,7 +237,7 @@ async fn send_state_event_for_key_helper(
db: &Database,
sender: &UserId,
room_id: &RoomId,
event_type: EventType,
event_type: &StateEventType,
json: &Raw<AnyStateEventContent>,
state_key: String,
) -> Result<Arc<EventId>> {
@ -312,7 +283,7 @@ async fn send_state_event_for_key_helper(
let event_id = db.rooms.build_and_append_pdu(
PduBuilder {
event_type,
event_type: event_type.to_string().into(),
content: serde_json::from_str(json.json().get()).expect("content is valid json"),
unsigned: None,
state_key: Some(state_key),

@ -1,13 +1,13 @@
use crate::{database::DatabaseGuard, ConduitResult, Database, Error, Result, Ruma, RumaResponse};
use crate::{database::DatabaseGuard, Database, Error, Result, Ruma, RumaResponse};
use ruma::{
api::client::r0::{
api::client::{
filter::{IncomingFilterDefinition, LazyLoadOptions},
sync::sync_events,
uiaa::UiaaResponse,
},
events::{
room::member::{MembershipState, RoomMemberEventContent},
AnySyncEphemeralRoomEvent, EventType,
RoomEventType, StateEventType,
},
serde::Raw,
DeviceId, RoomId, UserId,
@ -20,9 +20,6 @@ use std::{
use tokio::sync::watch::Sender;
use tracing::error;
#[cfg(feature = "conduit_bin")]
use rocket::{get, tokio};
/// # `GET /_matrix/client/r0/sync`
///
/// Synchronize the client's state with the latest state on the server.
@ -57,15 +54,10 @@ use rocket::{get, tokio};
///
/// - Sync is handled in an async task, multiple requests from the same device with the same
/// `since` will be cached
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/sync", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn sync_events_route(
db: DatabaseGuard,
body: Ruma<sync_events::Request<'_>>,
) -> Result<RumaResponse<sync_events::Response>, RumaResponse<UiaaResponse>> {
body: Ruma<sync_events::v3::IncomingRequest>,
) -> Result<sync_events::v3::Response, RumaResponse<UiaaResponse>> {
let sender_user = body.sender_user.expect("user is authenticated");
let sender_device = body.sender_device.expect("user is authenticated");
let body = body.body;
@ -82,7 +74,7 @@ pub async fn sync_events_route(
Entry::Vacant(v) => {
let (tx, rx) = tokio::sync::watch::channel(None);
v.insert((body.since.clone(), rx.clone()));
v.insert((body.since.to_owned(), rx.clone()));
tokio::spawn(sync_helper_wrapper(
Arc::clone(&arc_db),
@ -138,8 +130,8 @@ async fn sync_helper_wrapper(
db: Arc<DatabaseGuard>,
sender_user: Box<UserId>,
sender_device: Box<DeviceId>,
body: sync_events::IncomingRequest,
tx: Sender<Option<ConduitResult<sync_events::Response>>>,
body: sync_events::v3::IncomingRequest,
tx: Sender<Option<Result<sync_events::v3::Response>>>,
) {
let since = body.since.clone();
@ -173,16 +165,22 @@ async fn sync_helper_wrapper(
drop(db);
let _ = tx.send(Some(r.map(|(r, _)| r.into())));
let _ = tx.send(Some(r.map(|(r, _)| r)));
}
async fn sync_helper(
db: Arc<DatabaseGuard>,
sender_user: Box<UserId>,
sender_device: Box<DeviceId>,
body: sync_events::IncomingRequest,
body: sync_events::v3::IncomingRequest,
// bool = caching allowed
) -> Result<(sync_events::Response, bool), Error> {
) -> Result<(sync_events::v3::Response, bool), Error> {
use sync_events::v3::{
DeviceLists, Ephemeral, GlobalAccountData, IncomingFilter, InviteState, InvitedRoom,
JoinedRoom, LeftRoom, Presence, RoomAccountData, RoomSummary, Rooms, State, Timeline,
ToDevice, UnreadNotificationsCount,
};
// TODO: match body.set_presence {
db.rooms.edus.ping_presence(&sender_user)?;
@ -195,8 +193,8 @@ async fn sync_helper(
// Load filter
let filter = match body.filter {
None => IncomingFilterDefinition::default(),
Some(sync_events::IncomingFilter::FilterDefinition(filter)) => filter,
Some(sync_events::IncomingFilter::FilterId(filter_id)) => db
Some(IncomingFilter::FilterDefinition(filter)) => filter,
Some(IncomingFilter::FilterId(filter_id)) => db
.users
.get_filter(&sender_user, &filter_id)?
.unwrap_or_default(),
@ -232,43 +230,56 @@ async fn sync_helper(
for room_id in all_joined_rooms {
let room_id = room_id?;
// Get and drop the lock to wait for remaining operations to finish
// This will make sure the we have all events until next_batch
let mutex_insert = Arc::clone(
db.globals
.roomid_mutex_insert
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().unwrap();
drop(insert_lock);
{
// Get and drop the lock to wait for remaining operations to finish
// This will make sure the we have all events until next_batch
let mutex_insert = Arc::clone(
db.globals
.roomid_mutex_insert
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().unwrap();
drop(insert_lock);
}
let mut non_timeline_pdus = db
.rooms
.pdus_until(&sender_user, &room_id, u64::MAX)?
.filter_map(|r| {
// Filter out buggy events
if r.is_err() {
error!("Bad pdu in pdus_since: {:?}", r);
}
r.ok()
})
.take_while(|(pduid, _)| {
db.rooms
.pdu_count(pduid)
.map_or(false, |count| count > since)
});
// Take the last 10 events for the timeline
let timeline_pdus: Vec<_> = non_timeline_pdus
.by_ref()
.take(10)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
let timeline_pdus;
let limited;
if db.rooms.last_timeline_count(&sender_user, &room_id)? > since {
let mut non_timeline_pdus = db
.rooms
.pdus_until(&sender_user, &room_id, u64::MAX)?
.filter_map(|r| {
// Filter out buggy events
if r.is_err() {
error!("Bad pdu in pdus_since: {:?}", r);
}
r.ok()
})
.take_while(|(pduid, _)| {
db.rooms
.pdu_count(pduid)
.map_or(false, |count| count > since)
});
// Take the last 10 events for the timeline
timeline_pdus = non_timeline_pdus
.by_ref()
.take(10)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>();
// They /sync response doesn't always return all messages, so we say the output is
// limited unless there are events in non_timeline_pdus
limited = non_timeline_pdus.next().is_some();
} else {
timeline_pdus = Vec::new();
limited = false;
}
let send_notification_counts = !timeline_pdus.is_empty()
|| db
@ -277,10 +288,6 @@ async fn sync_helper(
.last_privateread_update(&sender_user, &room_id)?
> since;
// They /sync response doesn't always return all messages, so we say the output is
// limited unless there are events in non_timeline_pdus
let limited = non_timeline_pdus.next().is_some();
let mut timeline_users = HashSet::new();
for (_, event) in &timeline_pdus {
timeline_users.insert(event.sender.as_str().to_owned());
@ -291,10 +298,12 @@ async fn sync_helper(
// Database queries:
let current_shortstatehash = db
.rooms
.current_shortstatehash(&room_id)?
.expect("All rooms have state");
let current_shortstatehash = if let Some(s) = db.rooms.current_shortstatehash(&room_id)? {
s
} else {
error!("Room {} has no state", room_id);
continue;
};
let since_shortstatehash = db.rooms.get_token_shortstatehash(&room_id, since)?;
@ -314,7 +323,7 @@ async fn sync_helper(
.rooms
.all_pdus(&sender_user, &room_id)?
.filter_map(|pdu| pdu.ok()) // Ignore all broken pdus
.filter(|(_, pdu)| pdu.kind == EventType::RoomMember)
.filter(|(_, pdu)| pdu.kind == RoomEventType::RoomMember)
.map(|(_, pdu)| {
let content: RoomMemberEventContent =
serde_json::from_str(pdu.content.get()).map_err(|_| {
@ -372,15 +381,16 @@ async fn sync_helper(
let (joined_member_count, invited_member_count, heroes) = calculate_counts()?;
let current_state_ids = db.rooms.state_full_ids(current_shortstatehash)?;
let current_state_ids = db.rooms.state_full_ids(current_shortstatehash).await?;
let mut state_events = Vec::new();
let mut lazy_loaded = HashSet::new();
let mut i = 0;
for (shortstatekey, id) in current_state_ids {
let (event_type, state_key) = db.rooms.get_statekey_from_short(shortstatekey)?;
if event_type != EventType::RoomMember {
if event_type != StateEventType::RoomMember {
let pdu = match db.rooms.get_pdu(&id)? {
Some(pdu) => pdu,
None => {
@ -389,6 +399,11 @@ async fn sync_helper(
}
};
state_events.push(pdu);
i += 1;
if i % 100 == 0 {
tokio::task::yield_now().await;
}
} else if !lazy_load_enabled
|| body.full_state
|| timeline_users.contains(&state_key)
@ -400,11 +415,17 @@ async fn sync_helper(
continue;
}
};
lazy_loaded.insert(
UserId::parse(state_key.as_ref())
.expect("they are in timeline_users, so they should be correct"),
);
// This check is in case a bad user ID made it into the database
if let Ok(uid) = UserId::parse(state_key.as_ref()) {
lazy_loaded.insert(uid);
}
state_events.push(pdu);
i += 1;
if i % 100 == 0 {
tokio::task::yield_now().await;
}
}
}
@ -440,7 +461,7 @@ async fn sync_helper(
.rooms
.state_get(
since_shortstatehash,
&EventType::RoomMember,
&StateEventType::RoomMember,
sender_user.as_str(),
)?
.and_then(|pdu| {
@ -456,8 +477,8 @@ async fn sync_helper(
let mut lazy_loaded = HashSet::new();
if since_shortstatehash != current_shortstatehash {
let current_state_ids = db.rooms.state_full_ids(current_shortstatehash)?;
let since_state_ids = db.rooms.state_full_ids(since_shortstatehash)?;
let current_state_ids = db.rooms.state_full_ids(current_shortstatehash).await?;
let since_state_ids = db.rooms.state_full_ids(since_shortstatehash).await?;
for (key, id) in current_state_ids {
if body.full_state || since_state_ids.get(&key) != Some(&id) {
@ -469,7 +490,7 @@ async fn sync_helper(
}
};
if pdu.kind == EventType::RoomMember {
if pdu.kind == RoomEventType::RoomMember {
match UserId::parse(
pdu.state_key
.as_ref()
@ -484,6 +505,7 @@ async fn sync_helper(
}
state_events.push(pdu);
tokio::task::yield_now().await;
}
}
}
@ -502,7 +524,7 @@ async fn sync_helper(
{
if let Some(member_event) = db.rooms.room_state_get(
&room_id,
&EventType::RoomMember,
&StateEventType::RoomMember,
event.sender.as_str(),
)? {
lazy_loaded.insert(event.sender.clone());
@ -521,23 +543,23 @@ async fn sync_helper(
let encrypted_room = db
.rooms
.state_get(current_shortstatehash, &EventType::RoomEncryption, "")?
.state_get(current_shortstatehash, &StateEventType::RoomEncryption, "")?
.is_some();
let since_encryption =
db.rooms
.state_get(since_shortstatehash, &EventType::RoomEncryption, "")?;
.state_get(since_shortstatehash, &StateEventType::RoomEncryption, "")?;
// Calculations:
let new_encrypted_room = encrypted_room && since_encryption.is_none();
let send_member_count = state_events
.iter()
.any(|event| event.kind == EventType::RoomMember);
.any(|event| event.kind == RoomEventType::RoomMember);
if encrypted_room {
for state_event in &state_events {
if state_event.kind != EventType::RoomMember {
if state_event.kind != RoomEventType::RoomMember {
continue;
}
@ -656,10 +678,8 @@ async fn sync_helper(
if db.rooms.edus.last_typing_update(&room_id, &db.globals)? > since {
edus.push(
serde_json::from_str(
&serde_json::to_string(&AnySyncEphemeralRoomEvent::Typing(
db.rooms.edus.typings_all(&room_id)?,
))
.expect("event is valid, we just created it"),
&serde_json::to_string(&db.rooms.edus.typings_all(&room_id)?)
.expect("event is valid, we just created it"),
)
.expect("event is valid, we just created it"),
);
@ -669,8 +689,8 @@ async fn sync_helper(
db.rooms
.associate_token_shortstatehash(&room_id, next_batch, current_shortstatehash)?;
let joined_room = sync_events::JoinedRoom {
account_data: sync_events::RoomAccountData {
let joined_room = JoinedRoom {
account_data: RoomAccountData {
events: db
.account_data
.changes_since(Some(&room_id), &sender_user, since)?
@ -682,27 +702,27 @@ async fn sync_helper(
})
.collect(),
},
summary: sync_events::RoomSummary {
summary: RoomSummary {
heroes,
joined_member_count: joined_member_count.map(|n| (n as u32).into()),
invited_member_count: invited_member_count.map(|n| (n as u32).into()),
},
unread_notifications: sync_events::UnreadNotificationsCount {
unread_notifications: UnreadNotificationsCount {
highlight_count,
notification_count,
},
timeline: sync_events::Timeline {
timeline: Timeline {
limited: limited || joined_since_last_sync,
prev_batch,
events: room_events,
},
state: sync_events::State {
state: State {
events: state_events
.iter()
.map(|pdu| pdu.to_sync_state_event())
.collect(),
},
ephemeral: sync_events::Ephemeral { events: edus },
ephemeral: Ephemeral { events: edus },
};
if !joined_room.is_empty() {
@ -749,17 +769,19 @@ async fn sync_helper(
for result in all_left_rooms {
let (room_id, left_state_events) = result?;
// Get and drop the lock to wait for remaining operations to finish
let mutex_insert = Arc::clone(
db.globals
.roomid_mutex_insert
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().unwrap();
drop(insert_lock);
{
// Get and drop the lock to wait for remaining operations to finish
let mutex_insert = Arc::clone(
db.globals
.roomid_mutex_insert
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().unwrap();
drop(insert_lock);
}
let left_count = db.rooms.get_left_count(&room_id, &sender_user)?;
@ -770,14 +792,14 @@ async fn sync_helper(
left_rooms.insert(
room_id.clone(),
sync_events::LeftRoom {
account_data: sync_events::RoomAccountData { events: Vec::new() },
timeline: sync_events::Timeline {
LeftRoom {
account_data: RoomAccountData { events: Vec::new() },
timeline: Timeline {
limited: false,
prev_batch: Some(next_batch_string.clone()),
events: Vec::new(),
},
state: sync_events::State {
state: State {
events: left_state_events,
},
},
@ -789,17 +811,19 @@ async fn sync_helper(
for result in all_invited_rooms {
let (room_id, invite_state_events) = result?;
// Get and drop the lock to wait for remaining operations to finish
let mutex_insert = Arc::clone(
db.globals
.roomid_mutex_insert
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().unwrap();
drop(insert_lock);
{
// Get and drop the lock to wait for remaining operations to finish
let mutex_insert = Arc::clone(
db.globals
.roomid_mutex_insert
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().unwrap();
drop(insert_lock);
}
let invite_count = db.rooms.get_invite_count(&room_id, &sender_user)?;
@ -810,8 +834,8 @@ async fn sync_helper(
invited_rooms.insert(
room_id.clone(),
sync_events::InvitedRoom {
invite_state: sync_events::InviteState {
InvitedRoom {
invite_state: InviteState {
events: invite_state_events,
},
},
@ -826,7 +850,7 @@ async fn sync_helper(
.filter_map(|other_room_id| {
Some(
db.rooms
.room_state_get(&other_room_id, &EventType::RoomEncryption, "")
.room_state_get(&other_room_id, &StateEventType::RoomEncryption, "")
.ok()?
.is_some(),
)
@ -843,21 +867,21 @@ async fn sync_helper(
db.users
.remove_to_device_events(&sender_user, &sender_device, since)?;
let response = sync_events::Response {
let response = sync_events::v3::Response {
next_batch: next_batch_string,
rooms: sync_events::Rooms {
rooms: Rooms {
leave: left_rooms,
join: joined_rooms,
invite: invited_rooms,
knock: BTreeMap::new(), // TODO
},
presence: sync_events::Presence {
presence: Presence {
events: presence_updates
.into_iter()
.map(|(_, v)| Raw::new(&v).expect("PresenceEvent always serializes successfully"))
.collect(),
},
account_data: sync_events::GlobalAccountData {
account_data: GlobalAccountData {
events: db
.account_data
.changes_since(None, &sender_user, since)?
@ -869,12 +893,12 @@ async fn sync_helper(
})
.collect(),
},
device_lists: sync_events::DeviceLists {
device_lists: DeviceLists {
changed: device_list_updates.into_iter().collect(),
left: device_list_left.into_iter().collect(),
},
device_one_time_keys_count: db.users.count_one_time_keys(&sender_user, &sender_device)?,
to_device: sync_events::ToDevice {
to_device: ToDevice {
events: db
.users
.get_to_device_events(&sender_user, &sender_device)?,
@ -919,7 +943,7 @@ fn share_encrypted_room(
.filter_map(|other_room_id| {
Some(
db.rooms
.room_state_get(&other_room_id, &EventType::RoomEncryption, "")
.room_state_get(&other_room_id, &StateEventType::RoomEncryption, "")
.ok()?
.is_some(),
)

@ -1,35 +1,31 @@
use crate::{database::DatabaseGuard, ConduitResult, Ruma};
use crate::{database::DatabaseGuard, Result, Ruma};
use ruma::{
api::client::r0::tag::{create_tag, delete_tag, get_tags},
api::client::tag::{create_tag, delete_tag, get_tags},
events::{
tag::{TagEvent, TagEventContent},
EventType,
RoomAccountDataEventType,
},
};
use std::collections::BTreeMap;
#[cfg(feature = "conduit_bin")]
use rocket::{delete, get, put};
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
///
/// Adds a tag to the room.
///
/// - Inserts the tag into the tag event of the room account data.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/user/<_>/rooms/<_>/tags/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn update_tag_route(
db: DatabaseGuard,
body: Ruma<create_tag::Request<'_>>,
) -> ConduitResult<create_tag::Response> {
body: Ruma<create_tag::v3::IncomingRequest>,
) -> Result<create_tag::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut tags_event = db
.account_data
.get(Some(&body.room_id), sender_user, EventType::Tag)?
.get(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
)?
.unwrap_or_else(|| TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
@ -43,14 +39,14 @@ pub async fn update_tag_route(
db.account_data.update(
Some(&body.room_id),
sender_user,
EventType::Tag,
RoomAccountDataEventType::Tag,
&tags_event,
&db.globals,
)?;
db.flush()?;
Ok(create_tag::Response {}.into())
Ok(create_tag::v3::Response {})
}
/// # `DELETE /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
@ -58,20 +54,19 @@ pub async fn update_tag_route(
/// Deletes a tag from the room.
///
/// - Removes the tag from the tag event of the room account data.
#[cfg_attr(
feature = "conduit_bin",
delete("/_matrix/client/r0/user/<_>/rooms/<_>/tags/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn delete_tag_route(
db: DatabaseGuard,
body: Ruma<delete_tag::Request<'_>>,
) -> ConduitResult<delete_tag::Response> {
body: Ruma<delete_tag::v3::IncomingRequest>,
) -> Result<delete_tag::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let mut tags_event = db
.account_data
.get(Some(&body.room_id), sender_user, EventType::Tag)?
.get(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
)?
.unwrap_or_else(|| TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
@ -82,14 +77,14 @@ pub async fn delete_tag_route(
db.account_data.update(
Some(&body.room_id),
sender_user,
EventType::Tag,
RoomAccountDataEventType::Tag,
&tags_event,
&db.globals,
)?;
db.flush()?;
Ok(delete_tag::Response {}.into())
Ok(delete_tag::v3::Response {})
}
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags`
@ -97,21 +92,20 @@ pub async fn delete_tag_route(
/// Returns tags on the room.
///
/// - Gets the tag event of the room account data.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/user/<_>/rooms/<_>/tags", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn get_tags_route(
db: DatabaseGuard,
body: Ruma<get_tags::Request<'_>>,
) -> ConduitResult<get_tags::Response> {
body: Ruma<get_tags::v3::IncomingRequest>,
) -> Result<get_tags::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
Ok(get_tags::Response {
Ok(get_tags::v3::Response {
tags: db
.account_data
.get(Some(&body.room_id), sender_user, EventType::Tag)?
.get(
Some(&body.room_id),
sender_user,
RoomAccountDataEventType::Tag,
)?
.unwrap_or_else(|| TagEvent {
content: TagEventContent {
tags: BTreeMap::new(),
@ -119,6 +113,5 @@ pub async fn get_tags_route(
})
.content
.tags,
}
.into())
})
}

@ -1,22 +1,16 @@
use crate::ConduitResult;
use ruma::api::client::r0::thirdparty::get_protocols;
use crate::{Result, Ruma};
use ruma::api::client::thirdparty::get_protocols;
#[cfg(feature = "conduit_bin")]
use rocket::get;
use std::collections::BTreeMap;
/// # `GET /_matrix/client/r0/thirdparty/protocols`
///
/// TODO: Fetches all metadata about protocols supported by the homeserver.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/thirdparty/protocols")
)]
#[tracing::instrument]
pub async fn get_protocols_route() -> ConduitResult<get_protocols::Response> {
pub async fn get_protocols_route(
_body: Ruma<get_protocols::v3::IncomingRequest>,
) -> Result<get_protocols::v3::Response> {
// TODO
Ok(get_protocols::Response {
Ok(get_protocols::v3::Response {
protocols: BTreeMap::new(),
}
.into())
})
}

@ -1,44 +1,33 @@
use ruma::events::ToDeviceEventType;
use std::collections::BTreeMap;
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
use crate::{database::DatabaseGuard, Error, Result, Ruma};
use ruma::{
api::{
client::{error::ErrorKind, r0::to_device::send_event_to_device},
client::{error::ErrorKind, to_device::send_event_to_device},
federation::{self, transactions::edu::DirectDeviceContent},
},
events::EventType,
to_device::DeviceIdOrAllDevices,
};
#[cfg(feature = "conduit_bin")]
use rocket::put;
/// # `PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId}`
///
/// Send a to-device event to a set of client devices.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/sendToDevice/<_>/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub async fn send_event_to_device_route(
db: DatabaseGuard,
body: Ruma<send_event_to_device::Request<'_>>,
) -> ConduitResult<send_event_to_device::Response> {
body: Ruma<send_event_to_device::v3::IncomingRequest>,
) -> Result<send_event_to_device::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_deref();
// TODO: uncomment when https://github.com/vector-im/element-android/issues/3589 is solved
// Check if this is a new transaction id
/*
if db
.transaction_ids
.existing_txnid(sender_user, sender_device, &body.txn_id)?
.is_some()
{
return Ok(send_event_to_device::Response.into());
return Ok(send_event_to_device::v3::Response {});
}
*/
for (target_user_id, map) in &body.messages {
for (target_device_id_maybe, event) in map {
@ -53,8 +42,8 @@ pub async fn send_event_to_device_route(
serde_json::to_vec(&federation::transactions::edu::Edu::DirectToDevice(
DirectDeviceContent {
sender: sender_user.clone(),
ev_type: EventType::from(&*body.event_type),
message_id: body.txn_id.clone(),
ev_type: ToDeviceEventType::from(&*body.event_type),
message_id: body.txn_id.to_owned(),
messages,
},
))
@ -69,7 +58,7 @@ pub async fn send_event_to_device_route(
DeviceIdOrAllDevices::DeviceId(target_device_id) => db.users.add_to_device_event(
sender_user,
target_user_id,
target_device_id,
&target_device_id,
&body.event_type,
event.deserialize_as().map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid")
@ -101,5 +90,5 @@ pub async fn send_event_to_device_route(
db.flush()?;
Ok(send_event_to_device::Response {}.into())
Ok(send_event_to_device::v3::Response {})
}

@ -1,24 +1,24 @@
use crate::{database::DatabaseGuard, utils, ConduitResult, Ruma};
use create_typing_event::Typing;
use ruma::api::client::r0::typing::create_typing_event;
#[cfg(feature = "conduit_bin")]
use rocket::put;
use crate::{database::DatabaseGuard, utils, Error, Result, Ruma};
use ruma::api::client::{error::ErrorKind, typing::create_typing_event};
/// # `PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}`
///
/// Sets the typing state of the sender user.
#[cfg_attr(
feature = "conduit_bin",
put("/_matrix/client/r0/rooms/<_>/typing/<_>", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
pub fn create_typing_event_route(
pub async fn create_typing_event_route(
db: DatabaseGuard,
body: Ruma<create_typing_event::Request<'_>>,
) -> ConduitResult<create_typing_event::Response> {
body: Ruma<create_typing_event::v3::IncomingRequest>,
) -> Result<create_typing_event::v3::Response> {
use create_typing_event::v3::Typing;
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !db.rooms.is_joined(sender_user, &body.room_id)? {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"You are not in this room.",
));
}
if let Typing::Yes(duration) = body.state {
db.rooms.edus.typing_add(
sender_user,
@ -32,5 +32,5 @@ pub fn create_typing_event_route(
.typing_remove(sender_user, &body.room_id, &db.globals)?;
}
Ok(create_typing_event::Response {}.into())
Ok(create_typing_event::v3::Response {})
}

@ -1,10 +1,8 @@
use std::{collections::BTreeMap, iter::FromIterator};
use crate::ConduitResult;
use ruma::api::client::unversioned::get_supported_versions;
use ruma::api::client::discovery::get_supported_versions;
#[cfg(feature = "conduit_bin")]
use rocket::get;
use crate::{Result, Ruma};
/// # `GET /_matrix/client/versions`
///
@ -16,13 +14,18 @@ use rocket::get;
///
/// Note: Unstable features are used while developing new features. Clients should avoid using
/// unstable features in their stable releases
#[cfg_attr(feature = "conduit_bin", get("/_matrix/client/versions"))]
#[tracing::instrument]
pub async fn get_supported_versions_route() -> ConduitResult<get_supported_versions::Response> {
pub async fn get_supported_versions_route(
_body: Ruma<get_supported_versions::IncomingRequest>,
) -> Result<get_supported_versions::Response> {
let resp = get_supported_versions::Response {
versions: vec!["r0.5.0".to_owned(), "r0.6.0".to_owned()],
versions: vec![
"r0.5.0".to_owned(),
"r0.6.0".to_owned(),
"v1.1".to_owned(),
"v1.2".to_owned(),
],
unstable_features: BTreeMap::from_iter([("org.matrix.e2e_cross_signing".to_owned(), true)]),
};
Ok(resp.into())
Ok(resp)
}

@ -1,30 +1,30 @@
use crate::{database::DatabaseGuard, ConduitResult, Ruma};
use ruma::api::client::r0::user_directory::search_users;
#[cfg(feature = "conduit_bin")]
use rocket::post;
use crate::{database::DatabaseGuard, Result, Ruma};
use ruma::{
api::client::user_directory::search_users,
events::{
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
StateEventType,
},
};
/// # `POST /_matrix/client/r0/user_directory/search`
///
/// Searches all known users for a match.
///
/// - TODO: Hide users that are not in any public rooms?
#[cfg_attr(
feature = "conduit_bin",
post("/_matrix/client/r0/user_directory/search", data = "<body>")
)]
#[tracing::instrument(skip(db, body))]
/// - Hides any local users that aren't in any public rooms (i.e. those that have the join rule set to public)
/// and don't share a room with the sender
pub async fn search_users_route(
db: DatabaseGuard,
body: Ruma<search_users::Request<'_>>,
) -> ConduitResult<search_users::Response> {
body: Ruma<search_users::v3::IncomingRequest>,
) -> Result<search_users::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let limit = u64::from(body.limit) as usize;
let mut users = db.users.iter().filter_map(|user_id| {
// Filter out buggy users (they should not exist, but you never know...)
let user_id = user_id.ok()?;
let user = search_users::User {
let user = search_users::v3::User {
user_id: user_id.clone(),
display_name: db.users.displayname(&user_id).ok()?,
avatar_url: db.users.avatar_url(&user_id).ok()?,
@ -49,11 +49,43 @@ pub async fn search_users_route(
return None;
}
Some(user)
let user_is_in_public_rooms =
db.rooms
.rooms_joined(&user_id)
.filter_map(|r| r.ok())
.any(|room| {
db.rooms
.room_state_get(&room, &StateEventType::RoomJoinRules, "")
.map_or(false, |event| {
event.map_or(false, |event| {
serde_json::from_str(event.content.get())
.map_or(false, |r: RoomJoinRulesEventContent| {
r.join_rule == JoinRule::Public
})
})
})
});
if user_is_in_public_rooms {
return Some(user);
}
let user_is_in_shared_rooms = db
.rooms
.get_shared_rooms(vec![sender_user.clone(), user_id.clone()])
.ok()?
.next()
.is_some();
if user_is_in_shared_rooms {
return Some(user);
}
None
});
let results = users.by_ref().take(limit).collect();
let limited = users.next().is_some();
Ok(search_users::Response { results, limited }.into())
Ok(search_users::v3::Response { results, limited })
}

@ -1,27 +1,18 @@
use crate::{database::DatabaseGuard, ConduitResult, Ruma};
use crate::{database::DatabaseGuard, Result, Ruma};
use hmac::{Hmac, Mac, NewMac};
use ruma::api::client::r0::voip::get_turn_server_info;
use ruma::SecondsSinceUnixEpoch;
use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch};
use sha1::Sha1;
use std::time::{Duration, SystemTime};
type HmacSha1 = Hmac<Sha1>;
#[cfg(feature = "conduit_bin")]
use rocket::get;
/// # `GET /_matrix/client/r0/voip/turnServer`
///
/// TODO: Returns information about the recommended turn server.
#[cfg_attr(
feature = "conduit_bin",
get("/_matrix/client/r0/voip/turnServer", data = "<body>")
)]
#[tracing::instrument(skip(body, db))]
pub async fn turn_server_route(
body: Ruma<get_turn_server_info::Request>,
db: DatabaseGuard,
) -> ConduitResult<get_turn_server_info::Response> {
body: Ruma<get_turn_server_info::v3::IncomingRequest>,
) -> Result<get_turn_server_info::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let turn_secret = db.globals.turn_secret();
@ -48,11 +39,10 @@ pub async fn turn_server_route(
)
};
Ok(get_turn_server_info::Response {
Ok(get_turn_server_info::v3::Response {
username,
password,
uris: db.globals.turn_uris().to_vec(),
ttl: Duration::from_secs(db.globals.turn_ttl()),
}
.into())
})
}

@ -1,6 +1,10 @@
use std::collections::BTreeMap;
use std::{
collections::BTreeMap,
fmt,
net::{IpAddr, Ipv4Addr},
};
use ruma::ServerName;
use ruma::{RoomVersionId, ServerName};
use serde::{de::IgnoredAny, Deserialize};
use tracing::warn;
@ -10,12 +14,20 @@ use self::proxy::ProxyConfig;
#[derive(Clone, Debug, Deserialize)]
pub struct Config {
#[serde(default = "default_address")]
pub address: IpAddr,
#[serde(default = "default_port")]
pub port: u16,
pub tls: Option<TlsConfig>,
pub server_name: Box<ServerName>,
#[serde(default = "default_database_backend")]
pub database_backend: String,
pub database_path: String,
#[serde(default = "default_db_cache_capacity_mb")]
pub db_cache_capacity_mb: f64,
#[serde(default = "true_fn")]
pub enable_lightning_bolt: bool,
#[serde(default = "default_conduit_cache_capacity_modifier")]
pub conduit_cache_capacity_modifier: f64,
#[serde(default = "default_rocksdb_max_open_files")]
@ -36,6 +48,10 @@ pub struct Config {
pub allow_federation: bool,
#[serde(default = "true_fn")]
pub allow_room_creation: bool,
#[serde(default = "true_fn")]
pub allow_unstable_room_versions: bool,
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
#[serde(default = "false_fn")]
pub allow_jaeger: bool,
#[serde(default = "false_fn")]
@ -58,10 +74,18 @@ pub struct Config {
#[serde(default = "default_turn_ttl")]
pub turn_ttl: u64,
pub emergency_password: Option<String>,
#[serde(flatten)]
pub catchall: BTreeMap<String, IgnoredAny>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct TlsConfig {
pub certs: String,
pub key: String,
}
const DEPRECATED_KEYS: &[&str] = &["cache_capacity"];
impl Config {
@ -82,6 +106,101 @@ impl Config {
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Prepare a list of config values to show
let lines = [
("Server name", self.server_name.host()),
("Database backend", &self.database_backend),
("Database path", &self.database_path),
(
"Database cache capacity (MB)",
&self.db_cache_capacity_mb.to_string(),
),
(
"Cache capacity modifier",
&self.conduit_cache_capacity_modifier.to_string(),
),
#[cfg(feature = "rocksdb")]
(
"Maximum open files for RocksDB",
&self.rocksdb_max_open_files.to_string(),
),
("PDU cache capacity", &self.pdu_cache_capacity.to_string()),
(
"Cleanup interval in seconds",
&self.cleanup_second_interval.to_string(),
),
("Maximum request size", &self.max_request_size.to_string()),
(
"Maximum concurrent requests",
&self.max_concurrent_requests.to_string(),
),
("Allow registration", &self.allow_registration.to_string()),
(
"Enabled lightning bolt",
&self.enable_lightning_bolt.to_string(),
),
("Allow encryption", &self.allow_encryption.to_string()),
("Allow federation", &self.allow_federation.to_string()),
("Allow room creation", &self.allow_room_creation.to_string()),
(
"JWT secret",
match self.jwt_secret {
Some(_) => "set",
None => "not set",
},
),
("Trusted servers", {
let mut lst = vec![];
for server in &self.trusted_servers {
lst.push(server.host());
}
&lst.join(", ")
}),
(
"TURN username",
if self.turn_username.is_empty() {
"not set"
} else {
&self.turn_username
},
),
("TURN password", {
if self.turn_password.is_empty() {
"not set"
} else {
"set"
}
}),
("TURN secret", {
if self.turn_secret.is_empty() {
"not set"
} else {
"set"
}
}),
("Turn TTL", &self.turn_ttl.to_string()),
("Turn URIs", {
let mut lst = vec![];
for item in self.turn_uris.to_vec().into_iter().enumerate() {
let (_, uri): (usize, String) = item;
lst.push(uri);
}
&lst.join(", ")
}),
];
let mut msg: String = "Active config values:\n\n".to_string();
for line in lines.into_iter().enumerate() {
msg += &format!("{}: {}\n", line.1 .0, line.1 .1);
}
write!(f, "{}", msg)
}
}
fn false_fn() -> bool {
false
}
@ -90,6 +209,14 @@ fn true_fn() -> bool {
true
}
fn default_address() -> IpAddr {
Ipv4Addr::LOCALHOST.into()
}
fn default_port() -> u16 {
8000
}
fn default_database_backend() -> String {
"sqlite".to_owned()
}
@ -123,9 +250,14 @@ fn default_max_concurrent_requests() -> u16 {
}
fn default_log() -> String {
"info,state_res=warn,rocket=off,_=off,sled=off".to_owned()
"info,state_res=warn,_=off,sled=off".to_owned()
}
fn default_turn_ttl() -> u64 {
60 * 60 * 24
}
// I know, it's a great name
fn default_default_room_version() -> RoomVersionId {
RoomVersionId::V6
}

@ -10,13 +10,13 @@ use crate::Result;
/// ```
/// - Global proxy
/// ```toml
/// [proxy]
/// [global.proxy]
/// global = { url = "socks5h://localhost:9050" }
/// ```
/// - Proxy some domains
/// ```toml
/// [proxy]
/// [[proxy.by_domain]]
/// [global.proxy]
/// [[global.proxy.by_domain]]
/// url = "socks5h://localhost:9050"
/// include = ["*.onion", "matrix.myspecial.onion"]
/// exclude = ["*.myspecial.onion"]

@ -13,17 +13,20 @@ pub mod transaction_ids;
pub mod uiaa;
pub mod users;
use self::admin::create_admin_room;
use crate::{utils, Config, Error, Result};
use abstraction::DatabaseEngine;
use directories::ProjectDirs;
use futures_util::{stream::FuturesUnordered, StreamExt};
use lru_cache::LruCache;
use rocket::{
futures::{channel::mpsc, stream::FuturesUnordered, StreamExt},
outcome::{try_outcome, IntoOutcome},
request::{FromRequest, Request},
Shutdown, State,
use ruma::{
events::{
push_rules::PushRulesEventContent, room::message::RoomMessageEventContent,
GlobalAccountDataEvent, GlobalAccountDataEventType,
},
push::Ruleset,
DeviceId, EventId, RoomId, UserId,
};
use ruma::{DeviceId, EventId, RoomId, UserId};
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs::{self, remove_dir_all},
@ -33,11 +36,9 @@ use std::{
path::Path,
sync::{Arc, Mutex, RwLock},
};
use tokio::sync::{OwnedRwLockReadGuard, RwLock as TokioRwLock, Semaphore};
use tokio::sync::{mpsc, OwnedRwLockReadGuard, RwLock as TokioRwLock, Semaphore};
use tracing::{debug, error, info, warn};
use self::admin::create_admin_room;
pub struct Database {
_db: Arc<dyn DatabaseEngine>,
pub globals: globals::Globals,
@ -151,8 +152,8 @@ impl Database {
eprintln!("ERROR: Max request size is less than 1KB. Please increase it.");
}
let (admin_sender, admin_receiver) = mpsc::unbounded();
let (sending_sender, sending_receiver) = mpsc::unbounded();
let (admin_sender, admin_receiver) = mpsc::unbounded_channel();
let (sending_sender, sending_receiver) = mpsc::unbounded_channel();
let db = Arc::new(TokioRwLock::from(Self {
_db: builder.clone(),
@ -212,6 +213,8 @@ impl Database {
userroomid_leftstate: builder.open_tree("userroomid_leftstate")?,
roomuserid_leftcount: builder.open_tree("roomuserid_leftcount")?,
disabledroomids: builder.open_tree("disabledroomids")?,
lazyloadedids: builder.open_tree("lazyloadedids")?,
userroomid_notificationcount: builder.open_tree("userroomid_notificationcount")?,
@ -263,6 +266,7 @@ impl Database {
stateinfo_cache: Mutex::new(LruCache::new(
(100.0 * config.conduit_cache_capacity_modifier) as usize,
)),
lasttimelinecount_cache: Mutex::new(HashMap::new()),
},
account_data: account_data::AccountData {
roomuserdataid_accountdata: builder.open_tree("roomuserdataid_accountdata")?,
@ -752,6 +756,23 @@ impl Database {
guard.rooms.edus.presenceid_presence.clear()?;
guard.admin.start_handler(Arc::clone(&db), admin_receiver);
// Set emergency access for the conduit user
match set_emergency_access(&guard) {
Ok(pwd_set) => {
if pwd_set {
warn!("The Conduit account emergency password is set! Please unset it as soon as you finish admin account recovery!");
guard.admin.send_message(RoomMessageEventContent::text_plain("The Conduit account emergency password is set! Please unset it as soon as you finish admin account recovery!"));
}
}
Err(e) => {
error!(
"Could not set the configured emergency password for the conduit user: {}",
e
)
}
};
guard
.sending
.start_handler(Arc::clone(&db), sending_receiver);
@ -764,14 +785,9 @@ impl Database {
}
#[cfg(feature = "conduit_bin")]
pub async fn start_on_shutdown_tasks(db: Arc<TokioRwLock<Self>>, shutdown: Shutdown) {
tokio::spawn(async move {
shutdown.await;
info!(target: "shutdown-sync", "Received shutdown notification, notifying sync helpers...");
db.read().await.globals.rotate.fire();
});
pub async fn on_shutdown(db: Arc<TokioRwLock<Self>>) {
info!(target: "shutdown-sync", "Received shutdown notification, notifying sync helpers...");
db.read().await.globals.rotate.fire();
}
pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) {
@ -938,6 +954,32 @@ impl Database {
}
}
/// Sets the emergency password and push rules for the @conduit account in case emergency password is set
fn set_emergency_access(db: &Database) -> Result<bool> {
let conduit_user = UserId::parse_with_server_name("conduit", db.globals.server_name())
.expect("@conduit:server_name is a valid UserId");
db.users
.set_password(&conduit_user, db.globals.emergency_password().as_deref())?;
let (ruleset, res) = match db.globals.emergency_password() {
Some(_) => (Ruleset::server_default(&conduit_user), Ok(true)),
None => (Ruleset::new(), Ok(false)),
};
db.account_data.update(
None,
&conduit_user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&GlobalAccountDataEvent {
content: PushRulesEventContent { global: ruleset },
},
&db.globals,
)?;
res
}
pub struct DatabaseGuard(OwnedRwLockReadGuard<Database>);
impl Deref for DatabaseGuard {
@ -948,14 +990,23 @@ impl Deref for DatabaseGuard {
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for DatabaseGuard {
type Error = ();
#[cfg(feature = "conduit_bin")]
#[axum::async_trait]
impl<B> axum::extract::FromRequest<B> for DatabaseGuard
where
B: Send,
{
type Rejection = axum::extract::rejection::ExtensionRejection;
async fn from_request(
req: &mut axum::extract::RequestParts<B>,
) -> Result<Self, Self::Rejection> {
use axum::extract::Extension;
async fn from_request(req: &'r Request<'_>) -> rocket::request::Outcome<Self, ()> {
let db = try_outcome!(req.guard::<&State<Arc<TokioRwLock<Database>>>>().await);
let Extension(db): Extension<Arc<TokioRwLock<Database>>> =
Extension::from_request(req).await?;
Ok(DatabaseGuard(Arc::clone(db).read_owned().await)).or_forward(())
Ok(DatabaseGuard(db.read_owned().await))
}
}

@ -69,7 +69,6 @@ impl DatabaseEngine for Engine {
}
impl EngineTree {
#[tracing::instrument(skip(self, tree, from, backwards))]
fn iter_from_thread(
&self,
tree: Arc<heed::UntypedDatabase>,
@ -94,7 +93,6 @@ impl EngineTree {
}
}
#[tracing::instrument(skip(tree, txn, from, backwards))]
fn iter_from_thread_work(
tree: Arc<heed::UntypedDatabase>,
txn: &heed::RoTxn<'_>,
@ -126,7 +124,6 @@ fn iter_from_thread_work(
}
impl Tree for EngineTree {
#[tracing::instrument(skip(self, key))]
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
let txn = self.engine.env.read_txn().map_err(convert_error)?;
Ok(self
@ -136,7 +133,6 @@ impl Tree for EngineTree {
.map(|s| s.to_vec()))
}
#[tracing::instrument(skip(self, key, value))]
fn insert(&self, key: &[u8], value: &[u8]) -> Result<()> {
let mut txn = self.engine.env.write_txn().map_err(convert_error)?;
self.tree
@ -147,7 +143,6 @@ impl Tree for EngineTree {
Ok(())
}
#[tracing::instrument(skip(self, key))]
fn remove(&self, key: &[u8]) -> Result<()> {
let mut txn = self.engine.env.write_txn().map_err(convert_error)?;
self.tree.delete(&mut txn, &key).map_err(convert_error)?;
@ -155,12 +150,10 @@ impl Tree for EngineTree {
Ok(())
}
#[tracing::instrument(skip(self))]
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + Send + 'a> {
self.iter_from(&[], false)
}
#[tracing::instrument(skip(self, from, backwards))]
fn iter_from(
&self,
from: &[u8],
@ -169,7 +162,6 @@ impl Tree for EngineTree {
self.iter_from_thread(Arc::clone(&self.tree), from.to_vec(), backwards)
}
#[tracing::instrument(skip(self, key))]
fn increment(&self, key: &[u8]) -> Result<Vec<u8>> {
let mut txn = self.engine.env.write_txn().map_err(convert_error)?;
@ -186,7 +178,6 @@ impl Tree for EngineTree {
Ok(new)
}
#[tracing::instrument(skip(self, prefix))]
fn scan_prefix<'a>(
&'a self,
prefix: Vec<u8>,
@ -197,7 +188,6 @@ impl Tree for EngineTree {
)
}
#[tracing::instrument(skip(self, prefix))]
fn watch_prefix<'a>(&'a self, prefix: &[u8]) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
self.watchers.watch(prefix)
}

@ -62,7 +62,6 @@ impl PersyTree {
}
impl Tree for PersyTree {
#[tracing::instrument(skip(self, key))]
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
let result = self
.persy
@ -72,14 +71,12 @@ impl Tree for PersyTree {
Ok(result)
}
#[tracing::instrument(skip(self, key, value))]
fn insert(&self, key: &[u8], value: &[u8]) -> Result<()> {
self.insert_batch(&mut Some((key.to_owned(), value.to_owned())).into_iter())?;
self.watchers.wake(key);
Ok(())
}
#[tracing::instrument(skip(self, iter))]
fn insert_batch<'a>(&self, iter: &mut dyn Iterator<Item = (Vec<u8>, Vec<u8>)>) -> Result<()> {
let mut tx = self.begin()?;
for (key, value) in iter {
@ -93,7 +90,6 @@ impl Tree for PersyTree {
Ok(())
}
#[tracing::instrument(skip(self, iter))]
fn increment_batch<'a>(&self, iter: &mut dyn Iterator<Item = Vec<u8>>) -> Result<()> {
let mut tx = self.begin()?;
for key in iter {
@ -108,7 +104,6 @@ impl Tree for PersyTree {
Ok(())
}
#[tracing::instrument(skip(self, key))]
fn remove(&self, key: &[u8]) -> Result<()> {
let mut tx = self.begin()?;
tx.remove::<ByteVec, ByteVec>(&self.name, ByteVec::from(key), None)?;
@ -116,7 +111,6 @@ impl Tree for PersyTree {
Ok(())
}
#[tracing::instrument(skip(self))]
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a> {
let iter = self.persy.range::<ByteVec, ByteVec, _>(&self.name, ..);
match iter {
@ -132,7 +126,6 @@ impl Tree for PersyTree {
}
}
#[tracing::instrument(skip(self, from, backwards))]
fn iter_from<'a>(
&'a self,
from: &[u8],
@ -165,13 +158,11 @@ impl Tree for PersyTree {
}
}
#[tracing::instrument(skip(self, key))]
fn increment(&self, key: &[u8]) -> Result<Vec<u8>> {
self.increment_batch(&mut Some(key.to_owned()).into_iter())?;
Ok(self.get(key)?.unwrap())
}
#[tracing::instrument(skip(self, prefix))]
fn scan_prefix<'a>(
&'a self,
prefix: Vec<u8>,
@ -200,7 +191,6 @@ impl Tree for PersyTree {
}
}
#[tracing::instrument(skip(self, prefix))]
fn watch_prefix<'a>(&'a self, prefix: &[u8]) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
self.watchers.watch(prefix)
}

@ -1,6 +1,10 @@
use super::{super::Config, watchers::Watchers, DatabaseEngine, Tree};
use crate::{utils, Result};
use std::{future::Future, pin::Pin, sync::Arc, sync::RwLock};
use std::{
future::Future,
pin::Pin,
sync::{Arc, RwLock},
};
pub struct Engine {
rocks: rocksdb::DBWithThreadMode<rocksdb::MultiThreaded>,

@ -39,7 +39,6 @@ impl Tree for SledEngineTree {
Ok(())
}
#[tracing::instrument(skip(self, iter))]
fn insert_batch<'a>(&self, iter: &mut dyn Iterator<Item = (Vec<u8>, Vec<u8>)>) -> Result<()> {
for (key, value) in iter {
self.0.insert(key, value)?;

@ -19,7 +19,7 @@ thread_local! {
struct PreparedStatementIterator<'a> {
pub iterator: Box<dyn Iterator<Item = TupleOfBytes> + 'a>,
pub statement_ref: NonAliasingBox<rusqlite::Statement<'a>>,
pub _statement_ref: NonAliasingBox<rusqlite::Statement<'a>>,
}
impl Iterator for PreparedStatementIterator<'_> {
@ -134,7 +134,6 @@ pub struct SqliteTable {
type TupleOfBytes = (Vec<u8>, Vec<u8>);
impl SqliteTable {
#[tracing::instrument(skip(self, guard, key))]
fn get_with_guard(&self, guard: &Connection, key: &[u8]) -> Result<Option<Vec<u8>>> {
//dbg!(&self.name);
Ok(guard
@ -143,7 +142,6 @@ impl SqliteTable {
.optional()?)
}
#[tracing::instrument(skip(self, guard, key, value))]
fn insert_with_guard(&self, guard: &Connection, key: &[u8], value: &[u8]) -> Result<()> {
//dbg!(&self.name);
guard.execute(
@ -186,18 +184,16 @@ impl SqliteTable {
Box::new(PreparedStatementIterator {
iterator,
statement_ref,
_statement_ref: statement_ref,
})
}
}
impl Tree for SqliteTable {
#[tracing::instrument(skip(self, key))]
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
self.get_with_guard(self.engine.read_lock(), key)
}
#[tracing::instrument(skip(self, key, value))]
fn insert(&self, key: &[u8], value: &[u8]) -> Result<()> {
let guard = self.engine.write_lock();
self.insert_with_guard(&guard, key, value)?;
@ -206,7 +202,6 @@ impl Tree for SqliteTable {
Ok(())
}
#[tracing::instrument(skip(self, iter))]
fn insert_batch<'a>(&self, iter: &mut dyn Iterator<Item = (Vec<u8>, Vec<u8>)>) -> Result<()> {
let guard = self.engine.write_lock();
@ -221,7 +216,6 @@ impl Tree for SqliteTable {
Ok(())
}
#[tracing::instrument(skip(self, iter))]
fn increment_batch<'a>(&self, iter: &mut dyn Iterator<Item = Vec<u8>>) -> Result<()> {
let guard = self.engine.write_lock();
@ -239,7 +233,6 @@ impl Tree for SqliteTable {
Ok(())
}
#[tracing::instrument(skip(self, key))]
fn remove(&self, key: &[u8]) -> Result<()> {
let guard = self.engine.write_lock();
@ -251,14 +244,12 @@ impl Tree for SqliteTable {
Ok(())
}
#[tracing::instrument(skip(self))]
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = TupleOfBytes> + 'a> {
let guard = self.engine.read_lock_iterator();
self.iter_with_guard(guard)
}
#[tracing::instrument(skip(self, from, backwards))]
fn iter_from<'a>(
&'a self,
from: &[u8],
@ -292,7 +283,7 @@ impl Tree for SqliteTable {
);
Box::new(PreparedStatementIterator {
iterator,
statement_ref,
_statement_ref: statement_ref,
})
} else {
let statement = Box::leak(Box::new(
@ -318,12 +309,11 @@ impl Tree for SqliteTable {
Box::new(PreparedStatementIterator {
iterator,
statement_ref,
_statement_ref: statement_ref,
})
}
}
#[tracing::instrument(skip(self, key))]
fn increment(&self, key: &[u8]) -> Result<Vec<u8>> {
let guard = self.engine.write_lock();
@ -337,7 +327,6 @@ impl Tree for SqliteTable {
Ok(new)
}
#[tracing::instrument(skip(self, prefix))]
fn scan_prefix<'a>(&'a self, prefix: Vec<u8>) -> Box<dyn Iterator<Item = TupleOfBytes> + 'a> {
Box::new(
self.iter_from(&prefix, false)
@ -345,12 +334,10 @@ impl Tree for SqliteTable {
)
}
#[tracing::instrument(skip(self, prefix))]
fn watch_prefix<'a>(&'a self, prefix: &[u8]) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
self.watchers.watch(prefix)
}
#[tracing::instrument(skip(self))]
fn clear(&self) -> Result<()> {
debug!("clear: running");
self.engine

@ -1,7 +1,7 @@
use crate::{utils, Error, Result};
use ruma::{
api::client::error::ErrorKind,
events::{AnyEphemeralRoomEvent, EventType},
events::{AnyEphemeralRoomEvent, RoomAccountDataEventType},
serde::Raw,
RoomId, UserId,
};
@ -22,7 +22,7 @@ impl AccountData {
&self,
room_id: Option<&RoomId>,
user_id: &UserId,
event_type: EventType,
event_type: RoomAccountDataEventType,
data: &T,
globals: &super::globals::Globals,
) -> Result<()> {
@ -38,10 +38,10 @@ impl AccountData {
let mut roomuserdataid = prefix.clone();
roomuserdataid.extend_from_slice(&globals.next_count()?.to_be_bytes());
roomuserdataid.push(0xff);
roomuserdataid.extend_from_slice(event_type.as_bytes());
roomuserdataid.extend_from_slice(event_type.to_string().as_bytes());
let mut key = prefix;
key.extend_from_slice(event_type.as_bytes());
key.extend_from_slice(event_type.to_string().as_bytes());
let json = serde_json::to_value(data).expect("all types here can be serialized"); // TODO: maybe add error handling
if json.get("type").is_none() || json.get("content").is_none() {
@ -75,7 +75,7 @@ impl AccountData {
&self,
room_id: Option<&RoomId>,
user_id: &UserId,
kind: EventType,
kind: RoomAccountDataEventType,
) -> Result<Option<T>> {
let mut key = room_id
.map(|r| r.to_string())
@ -85,7 +85,7 @@ impl AccountData {
key.push(0xff);
key.extend_from_slice(user_id.as_bytes());
key.push(0xff);
key.extend_from_slice(kind.as_ref().as_bytes());
key.extend_from_slice(kind.to_string().as_bytes());
self.roomusertype_roomuserdataid
.get(&key)?
@ -109,7 +109,7 @@ impl AccountData {
room_id: Option<&RoomId>,
user_id: &UserId,
since: u64,
) -> Result<HashMap<EventType, Raw<AnyEphemeralRoomEvent>>> {
) -> Result<HashMap<RoomAccountDataEventType, Raw<AnyEphemeralRoomEvent>>> {
let mut userdata = HashMap::new();
let mut prefix = room_id
@ -131,7 +131,7 @@ impl AccountData {
.take_while(move |(k, _)| k.starts_with(&prefix))
.map(|(k, v)| {
Ok::<_, Error>((
EventType::try_from(
RoomAccountDataEventType::try_from(
utils::string_from_bytes(k.rsplit(|&b| b == 0xff).next().ok_or_else(
|| Error::bad_database("RoomUserData ID in db is invalid."),
)?)

@ -1,34 +1,42 @@
use std::{collections::BTreeMap, convert::TryFrom, convert::TryInto, sync::Arc, time::Instant};
use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
sync::Arc,
time::Instant,
};
use crate::{
client_server::AUTO_GEN_PASSWORD_LENGTH,
error::{Error, Result},
pdu::PduBuilder,
server_server, Database, PduEvent,
server_server, utils,
utils::HtmlEscape,
Database, PduEvent,
};
use clap::Parser;
use regex::Regex;
use rocket::{
futures::{channel::mpsc, stream::StreamExt},
http::RawStr,
};
use ruma::{
events::room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
events::{
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
name::RoomNameEventContent,
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
},
RoomEventType,
},
events::{room::message::RoomMessageEventContent, EventType},
identifiers::{EventId, RoomAliasId, RoomId, RoomName, RoomVersionId, ServerName, UserId},
EventId, RoomAliasId, RoomId, RoomName, RoomVersionId, ServerName, UserId,
};
use serde_json::value::to_raw_value;
use tokio::sync::{MutexGuard, RwLock, RwLockReadGuard};
use tokio::sync::{mpsc, MutexGuard, RwLock, RwLockReadGuard};
#[derive(Debug)]
pub enum AdminRoomEvent {
ProcessMessage(String),
SendMessage(RoomMessageEventContent),
@ -74,7 +82,7 @@ impl Admin {
.rooms
.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMessage,
event_type: RoomEventType::RoomMessage,
content: to_raw_value(&message)
.expect("event is valid, we just created it"),
unsigned: None,
@ -91,8 +99,14 @@ impl Admin {
loop {
tokio::select! {
Some(event) = receiver.next() => {
Some(event) = receiver.recv() => {
let guard = db.read().await;
let message_content = match event {
AdminRoomEvent::SendMessage(content) => content,
AdminRoomEvent::ProcessMessage(room_message) => process_admin_message(&*guard, room_message).await
};
let mutex_state = Arc::clone(
guard.globals
.roomid_mutex_state
@ -101,18 +115,10 @@ impl Admin {
.entry(conduit_room.clone())
.or_default(),
);
let state_lock = mutex_state.lock().await;
match event {
AdminRoomEvent::SendMessage(content) => {
send_message(content, guard, &state_lock);
}
AdminRoomEvent::ProcessMessage(room_message) => {
let reply_message = process_admin_message(&*guard, room_message);
let state_lock = mutex_state.lock().await;
send_message(reply_message, guard, &state_lock);
}
}
send_message(message_content, guard, &state_lock);
drop(state_lock);
}
@ -123,19 +129,19 @@ impl Admin {
pub fn process_message(&self, room_message: String) {
self.sender
.unbounded_send(AdminRoomEvent::ProcessMessage(room_message))
.send(AdminRoomEvent::ProcessMessage(room_message))
.unwrap();
}
pub fn send_message(&self, message_content: RoomMessageEventContent) {
self.sender
.unbounded_send(AdminRoomEvent::SendMessage(message_content))
.send(AdminRoomEvent::SendMessage(message_content))
.unwrap();
}
}
// Parse and process a message from the admin room
fn process_admin_message(db: &Database, room_message: String) -> RoomMessageEventContent {
async fn process_admin_message(db: &Database, room_message: String) -> RoomMessageEventContent {
let mut lines = room_message.lines();
let command_line = lines.next().expect("each string has at least one line");
let body: Vec<_> = lines.collect();
@ -153,7 +159,7 @@ fn process_admin_message(db: &Database, room_message: String) -> RoomMessageEven
}
};
match process_admin_command(db, admin_command, body) {
match process_admin_command(db, admin_command, body).await {
Ok(reply_message) => reply_message,
Err(error) => {
let markdown_message = format!(
@ -223,9 +229,48 @@ enum AdminCommand {
/// List all the currently registered appservices
ListAppservices,
/// List all rooms the server knows about
ListRooms,
/// List users in the database
ListLocalUsers,
/// List all rooms we are currently handling an incoming pdu from
IncomingFederation,
/// Deactivate a user
///
/// User will not be removed from all rooms by default.
/// Use --leave-rooms to force the user to leave all rooms
DeactivateUser {
#[clap(short, long)]
leave_rooms: bool,
user_id: Box<UserId>,
},
#[clap(verbatim_doc_comment)]
/// Deactivate a list of users
///
/// Recommended to use in conjunction with list-local-users.
///
/// Users will not be removed from joined rooms by default.
/// Can be overridden with --leave-rooms flag.
/// Removing a mass amount of users from a room may cause a significant amount of leave events.
/// The time to leave rooms may depend significantly on joined rooms and servers.
///
/// [commandbody]
/// # ```
/// # User list here
/// # ```
DeactivateAll {
#[clap(short, long)]
/// Remove users from their joined rooms
leave_rooms: bool,
#[clap(short, long)]
/// Also deactivate admin accounts
force: bool,
},
/// Get the auth_chain of a PDU
GetAuthChain {
/// An event ID (the $ character followed by the base64 reference hash)
@ -252,9 +297,31 @@ enum AdminCommand {
/// Print database memory usage statistics
DatabaseMemoryUsage,
/// Show configuration values
ShowConfig,
/// Reset user password
ResetPassword {
/// Username of the user for whom the password should be reset
username: String,
},
/// Create a new user
CreateUser {
/// Username of the new user
username: String,
/// Password of the new user, if unspecified one is generated
password: Option<String>,
},
/// Disables incoming federation handling for a room.
DisableRoom { room_id: Box<RoomId> },
/// Enables incoming federation handling for a room again.
EnableRoom { room_id: Box<RoomId> },
}
fn process_admin_command(
async fn process_admin_command(
db: &Database,
command: AdminCommand,
body: Vec<&str>,
@ -312,6 +379,26 @@ fn process_admin_command(
RoomMessageEventContent::text_plain("Failed to get appservices.")
}
}
AdminCommand::ListRooms => {
let room_ids = db.rooms.iter_ids();
let output = format!(
"Rooms:\n{}",
room_ids
.filter_map(|r| r.ok())
.map(|id| id.to_string()
+ "\tMembers: "
+ &db
.rooms
.room_joined_count(&id)
.ok()
.flatten()
.unwrap_or(0)
.to_string())
.collect::<Vec<_>>()
.join("\n")
);
RoomMessageEventContent::text_plain(output)
}
AdminCommand::ListLocalUsers => match db.users.list_local_users() {
Ok(users) => {
let mut msg: String = format!("Found {} local user account(s):\n", users.len());
@ -320,6 +407,22 @@ fn process_admin_command(
}
Err(e) => RoomMessageEventContent::text_plain(e.to_string()),
},
AdminCommand::IncomingFederation => {
let map = db.globals.roomid_federationhandletime.read().unwrap();
let mut msg: String = format!("Handling {} incoming pdus:\n", map.len());
for (r, (e, i)) in map.iter() {
let elapsed = i.elapsed();
msg += &format!(
"{} {}: {}m{}s\n",
r,
e,
elapsed.as_secs() / 60,
elapsed.as_secs() % 60
);
}
RoomMessageEventContent::text_plain(&msg)
}
AdminCommand::GetAuthChain { event_id } => {
let event_id = Arc::<EventId>::from(event_id);
if let Some(event) = db.rooms.get_pdu_json(&event_id)? {
@ -332,7 +435,9 @@ fn process_admin_command(
Error::bad_database("Invalid room id field in event in database")
})?;
let start = Instant::now();
let count = server_server::get_auth_chain(room_id, vec![event_id], db)?.count();
let count = server_server::get_auth_chain(room_id, vec![event_id], db)
.await?
.count();
let elapsed = start.elapsed();
RoomMessageEventContent::text_plain(format!(
"Loaded auth chain with length {} in {:?}",
@ -347,24 +452,26 @@ fn process_admin_command(
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
let event_id = EventId::parse(format!(
"${}",
// Anything higher than version3 behaves the same
ruma::signatures::reference_hash(&value, &RoomVersionId::V6)
.expect("ruma can calculate reference hashes")
))
.expect("ruma's reference hashes are valid event ids");
match serde_json::from_value::<PduEvent>(
serde_json::to_value(value).expect("value is json"),
) {
Ok(pdu) => RoomMessageEventContent::text_plain(format!(
"EventId: {:?}\n{:#?}",
event_id, pdu
)),
match ruma::signatures::reference_hash(&value, &RoomVersionId::V6) {
Ok(hash) => {
let event_id = EventId::parse(format!("${}", hash));
match serde_json::from_value::<PduEvent>(
serde_json::to_value(value).expect("value is json"),
) {
Ok(pdu) => RoomMessageEventContent::text_plain(format!(
"EventId: {:?}\n{:#?}",
event_id, pdu
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"EventId: {:?}\nCould not parse event: {}",
event_id, e
)),
}
}
Err(e) => RoomMessageEventContent::text_plain(format!(
"EventId: {:?}\nCould not parse event: {}",
event_id, e
"Could not parse PDU JSON: {:?}",
e
)),
}
}
@ -405,7 +512,7 @@ fn process_admin_command(
} else {
"PDU was accepted"
},
RawStr::new(&json_text).html_escape()
HtmlEscape(&json_text)
),
)
}
@ -419,6 +526,211 @@ fn process_admin_command(
e
)),
},
AdminCommand::ShowConfig => {
// Construct and send the response
RoomMessageEventContent::text_plain(format!("{}", db.globals.config))
}
AdminCommand::ResetPassword { username } => {
let user_id = match UserId::parse_with_server_name(
username.as_str().to_lowercase(),
db.globals.server_name(),
) {
Ok(id) => id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {}",
e
)))
}
};
// Check if the specified user is valid
if !db.users.exists(&user_id)?
|| db.users.is_deactivated(&user_id)?
|| user_id
== UserId::parse_with_server_name("conduit", db.globals.server_name())
.expect("conduit user exists")
{
return Ok(RoomMessageEventContent::text_plain(
"The specified user does not exist or is deactivated!",
));
}
let new_password = utils::random_string(AUTO_GEN_PASSWORD_LENGTH);
match db.users.set_password(&user_id, Some(new_password.as_str())) {
Ok(()) => RoomMessageEventContent::text_plain(format!(
"Successfully reset the password for user {}: {}",
user_id, new_password
)),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Couldn't reset the password for user {}: {}",
user_id, e
)),
}
}
AdminCommand::CreateUser { username, password } => {
let password = password.unwrap_or(utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
// Validate user id
let user_id = match UserId::parse_with_server_name(
username.as_str().to_lowercase(),
db.globals.server_name(),
) {
Ok(id) => id,
Err(e) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"The supplied username is not a valid username: {}",
e
)))
}
};
if user_id.is_historical() {
return Ok(RoomMessageEventContent::text_plain(format!(
"userid {user_id} is not allowed due to historical"
)));
}
if db.users.exists(&user_id)? {
return Ok(RoomMessageEventContent::text_plain(format!(
"userid {user_id} already exists"
)));
}
// Create user
db.users.create(&user_id, Some(password.as_str()))?;
// Default to pretty displayname
let mut displayname = user_id.localpart().to_owned();
// If enabled append lightning bolt to display name (default true)
if db.globals.enable_lightning_bolt() {
displayname.push_str(" ⚡️");
}
db.users
.set_displayname(&user_id, Some(displayname.clone()))?;
// Initial account data
db.account_data.update(
None,
&user_id,
ruma::events::GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent {
global: ruma::push::Ruleset::server_default(&user_id),
},
},
&db.globals,
)?;
// we dont add a device since we're not the user, just the creator
db.flush()?;
// Inhibit login does not work for guests
RoomMessageEventContent::text_plain(format!(
"Created user with user_id: {user_id} and password: {password}"
))
}
AdminCommand::DisableRoom { room_id } => {
db.rooms.disabledroomids.insert(room_id.as_bytes(), &[])?;
RoomMessageEventContent::text_plain("Room disabled.")
}
AdminCommand::EnableRoom { room_id } => {
db.rooms.disabledroomids.remove(room_id.as_bytes())?;
RoomMessageEventContent::text_plain("Room enabled.")
}
AdminCommand::DeactivateUser {
leave_rooms,
user_id,
} => {
let user_id = Arc::<UserId>::from(user_id);
if db.users.exists(&user_id)? {
RoomMessageEventContent::text_plain(format!(
"Making {} leave all rooms before deactivation...",
user_id
));
db.users.deactivate_account(&user_id)?;
if leave_rooms {
db.rooms.leave_all_rooms(&user_id, &db).await?;
}
RoomMessageEventContent::text_plain(format!(
"User {} has been deactivated",
user_id
))
} else {
RoomMessageEventContent::text_plain(format!(
"User {} doesn't exist on this server",
user_id
))
}
}
AdminCommand::DeactivateAll { leave_rooms, force } => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```" {
let usernames = body.clone().drain(1..body.len() - 1).collect::<Vec<_>>();
let mut user_ids: Vec<&UserId> = Vec::new();
for &username in &usernames {
match <&UserId>::try_from(username) {
Ok(user_id) => user_ids.push(user_id),
Err(_) => {
return Ok(RoomMessageEventContent::text_plain(format!(
"{} is not a valid username",
username
)))
}
}
}
let mut deactivation_count = 0;
let mut admins = Vec::new();
if !force {
user_ids.retain(|&user_id| {
match db.users.is_admin(user_id, &db.rooms, &db.globals) {
Ok(is_admin) => match is_admin {
true => {
admins.push(user_id.localpart());
false
}
false => true,
},
Err(_) => false,
}
})
}
for &user_id in &user_ids {
match db.users.deactivate_account(user_id) {
Ok(_) => deactivation_count += 1,
Err(_) => {}
}
}
if leave_rooms {
for &user_id in &user_ids {
let _ = db.rooms.leave_all_rooms(user_id, &db).await;
}
}
if admins.is_empty() {
RoomMessageEventContent::text_plain(format!(
"Deactivated {} accounts.",
deactivation_count
))
} else {
RoomMessageEventContent::text_plain(format!("Deactivated {} accounts.\nSkipped admin accounts: {:?}. Use --force to deactivate admin accounts", deactivation_count, admins.join(", ")))
}
} else {
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
};
Ok(reply_message_content)
@ -537,7 +849,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
// 1. The room create event
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomCreate,
event_type: RoomEventType::RoomCreate,
content: to_raw_value(&content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
@ -552,7 +864,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
// 2. Make conduit bot join
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: None,
@ -580,7 +892,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomPowerLevels,
event_type: RoomEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
@ -599,7 +911,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
// 4.1 Join Rules
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomJoinRules,
event_type: RoomEventType::RoomJoinRules,
content: to_raw_value(&RoomJoinRulesEventContent::new(JoinRule::Invite))
.expect("event is valid, we just created it"),
unsigned: None,
@ -615,7 +927,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
// 4.2 History Visibility
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomHistoryVisibility,
event_type: RoomEventType::RoomHistoryVisibility,
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(
HistoryVisibility::Shared,
))
@ -633,7 +945,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
// 4.3 Guest Access
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomGuestAccess,
event_type: RoomEventType::RoomGuestAccess,
content: to_raw_value(&RoomGuestAccessEventContent::new(GuestAccess::Forbidden))
.expect("event is valid, we just created it"),
unsigned: None,
@ -651,7 +963,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
.expect("Room name is valid");
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomName,
event_type: RoomEventType::RoomName,
content: to_raw_value(&RoomNameEventContent::new(Some(room_name)))
.expect("event is valid, we just created it"),
unsigned: None,
@ -666,7 +978,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomTopic,
event_type: RoomEventType::RoomTopic,
content: to_raw_value(&RoomTopicEventContent {
topic: format!("Manage {}", db.globals.server_name()),
})
@ -688,7 +1000,7 @@ pub(crate) async fn create_admin_room(db: &Database) -> Result<()> {
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomCanonicalAlias,
event_type: RoomEventType::RoomCanonicalAlias,
content: to_raw_value(&RoomCanonicalAliasEventContent {
alias: Some(alias.clone()),
alt_aliases: Vec::new(),
@ -742,7 +1054,7 @@ pub(crate) async fn make_user_admin(
// Invite and join the real user
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Invite,
displayname: None,
@ -765,7 +1077,7 @@ pub(crate) async fn make_user_admin(
)?;
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: Some(displayname),
@ -794,7 +1106,7 @@ pub(crate) async fn make_user_admin(
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomPowerLevels,
event_type: RoomEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
@ -813,10 +1125,10 @@ pub(crate) async fn make_user_admin(
// Send welcome message
db.rooms.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMessage,
event_type: RoomEventType::RoomMessage,
content: to_raw_value(&RoomMessageEventContent::text_html(
"## Thank you for trying out Conduit!\n\nConduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.\n\nHelpful links:\n> Website: https://conduit.rs\n> Git and Documentation: https://gitlab.com/famedly/conduit\n> Report issues: https://gitlab.com/famedly/conduit/-/issues\n\nHere are some rooms you can join (by typing the command):\n\nConduit room (Ask questions and get notified on updates):\n`/join #conduit:fachschaften.org`\n\nConduit lounge (Off-topic, only Conduit users are allowed to join)\n`/join #conduit-lounge:conduit.rs`".to_owned(),
"<h2>Thank you for trying out Conduit!</h2>\n<p>Conduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.</p>\n<p>Helpful links:</p>\n<blockquote>\n<p>Website: https://conduit.rs<br>Git and Documentation: https://gitlab.com/famedly/conduit<br>Report issues: https://gitlab.com/famedly/conduit/-/issues</p>\n</blockquote>\n<p>Here are some rooms you can join (by typing the command):</p>\n<p>Conduit room (Ask questions and get notified on updates):<br><code>/join #conduit:fachschaften.org</code></p>\n<p>Conduit lounge (Off-topic, only Conduit users are allowed to join)<br><code>/join #conduit-lounge:conduit.rs</code></p>\n".to_owned(),
format!("## Thank you for trying out Conduit!\n\nConduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.\n\nHelpful links:\n> Website: https://conduit.rs\n> Git and Documentation: https://gitlab.com/famedly/conduit\n> Report issues: https://gitlab.com/famedly/conduit/-/issues\n\nFor a list of available commands, send the following message in this room: `@conduit:{}: --help`\n\nHere are some rooms you can join (by typing the command):\n\nConduit room (Ask questions and get notified on updates):\n`/join #conduit:fachschaften.org`\n\nConduit lounge (Off-topic, only Conduit users are allowed to join)\n`/join #conduit-lounge:conduit.rs`", db.globals.server_name()).to_owned(),
format!("<h2>Thank you for trying out Conduit!</h2>\n<p>Conduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.</p>\n<p>Helpful links:</p>\n<blockquote>\n<p>Website: https://conduit.rs<br>Git and Documentation: https://gitlab.com/famedly/conduit<br>Report issues: https://gitlab.com/famedly/conduit/-/issues</p>\n</blockquote>\n<p>For a list of available commands, send the following message in this room: <code>@conduit:{}: --help</code></p>\n<p>Here are some rooms you can join (by typing the command):</p>\n<p>Conduit room (Ask questions and get notified on updates):<br><code>/join #conduit:fachschaften.org</code></p>\n<p>Conduit lounge (Off-topic, only Conduit users are allowed to join)<br><code>/join #conduit-lounge:conduit.rs</code></p>\n", db.globals.server_name()).to_owned(),
))
.expect("event is valid, we just created it"),
unsigned: None,

@ -1,10 +1,11 @@
use crate::{database::Config, server_server::FedDest, utils, ConduitResult, Error, Result};
use crate::{database::Config, server_server::FedDest, utils, Error, Result};
use ruma::{
api::{
client::r0::sync::sync_events,
client::sync::sync_events,
federation::discovery::{ServerSigningKeys, VerifyKey},
},
DeviceId, EventId, MilliSecondsSinceUnixEpoch, RoomId, ServerName, ServerSigningKeyId, UserId,
DeviceId, EventId, MilliSecondsSinceUnixEpoch, RoomId, RoomVersionId, ServerName,
ServerSigningKeyId, UserId,
};
use std::{
collections::{BTreeMap, HashMap},
@ -27,20 +28,22 @@ type WellKnownMap = HashMap<Box<ServerName>, (FedDest, String)>;
type TlsNameMap = HashMap<String, (Vec<IpAddr>, u16)>;
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
type SyncHandle = (
Option<String>, // since
Receiver<Option<ConduitResult<sync_events::Response>>>, // rx
Option<String>, // since
Receiver<Option<Result<sync_events::v3::Response>>>, // rx
);
pub struct Globals {
pub actual_destination_cache: Arc<RwLock<WellKnownMap>>, // actual_destination, host
pub tls_name_override: Arc<RwLock<TlsNameMap>>,
pub(super) globals: Arc<dyn Tree>,
config: Config,
pub config: Config,
keypair: Arc<ruma::signatures::Ed25519KeyPair>,
dns_resolver: TokioAsyncResolver,
jwt_decoding_key: Option<jsonwebtoken::DecodingKey<'static>>,
federation_client: reqwest::Client,
default_client: reqwest::Client,
pub stable_room_versions: Vec<RoomVersionId>,
pub unstable_room_versions: Vec<RoomVersionId>,
pub(super) server_signingkeys: Arc<dyn Tree>,
pub bad_event_ratelimiter: Arc<RwLock<HashMap<Box<EventId>, RateLimitState>>>,
pub bad_signature_ratelimiter: Arc<RwLock<HashMap<Vec<String>, RateLimitState>>>,
@ -49,6 +52,8 @@ pub struct Globals {
pub roomid_mutex_insert: RwLock<HashMap<Box<RoomId>, Arc<Mutex<()>>>>,
pub roomid_mutex_state: RwLock<HashMap<Box<RoomId>, Arc<TokioMutex<()>>>>,
pub roomid_mutex_federation: RwLock<HashMap<Box<RoomId>, Arc<TokioMutex<()>>>>, // this lock will be held longer
pub roomid_federationhandletime: RwLock<HashMap<Box<RoomId>, (Box<EventId>, Instant)>>,
pub stateres_mutex: Arc<Mutex<()>>,
pub rotate: RotationHandler,
}
@ -145,11 +150,25 @@ impl Globals {
})
.build()?;
let s = Self {
// Supported and stable room versions
let stable_room_versions = vec![
RoomVersionId::V6,
RoomVersionId::V7,
RoomVersionId::V8,
RoomVersionId::V9,
];
// Experimental, partially supported room versions
let unstable_room_versions = vec![RoomVersionId::V3, RoomVersionId::V4, RoomVersionId::V5];
let mut s = Self {
globals,
config,
keypair: Arc::new(keypair),
dns_resolver: TokioAsyncResolver::tokio_from_system_conf().map_err(|_| {
dns_resolver: TokioAsyncResolver::tokio_from_system_conf().map_err(|e| {
error!(
"Failed to set up trust dns resolver with system config: {}",
e
);
Error::bad_config("Failed to set up trust dns resolver with system config.")
})?,
actual_destination_cache: Arc::new(RwLock::new(WellKnownMap::new())),
@ -158,18 +177,30 @@ impl Globals {
default_client,
server_signingkeys,
jwt_decoding_key,
stable_room_versions,
unstable_room_versions,
bad_event_ratelimiter: Arc::new(RwLock::new(HashMap::new())),
bad_signature_ratelimiter: Arc::new(RwLock::new(HashMap::new())),
servername_ratelimiter: Arc::new(RwLock::new(HashMap::new())),
roomid_mutex_state: RwLock::new(HashMap::new()),
roomid_mutex_insert: RwLock::new(HashMap::new()),
roomid_mutex_federation: RwLock::new(HashMap::new()),
roomid_federationhandletime: RwLock::new(HashMap::new()),
stateres_mutex: Arc::new(Mutex::new(())),
sync_receivers: RwLock::new(HashMap::new()),
rotate: RotationHandler::new(),
};
fs::create_dir_all(s.get_media_folder())?;
if !s
.supported_room_versions()
.contains(&s.config.default_room_version)
{
error!("Room version in config isn't supported, falling back to Version 6");
s.config.default_room_version = RoomVersionId::V6;
};
Ok(s)
}
@ -228,6 +259,18 @@ impl Globals {
self.config.allow_room_creation
}
pub fn allow_unstable_room_versions(&self) -> bool {
self.config.allow_unstable_room_versions
}
pub fn default_room_version(&self) -> RoomVersionId {
self.config.default_room_version.clone()
}
pub fn enable_lightning_bolt(&self) -> bool {
self.config.enable_lightning_bolt
}
pub fn trusted_servers(&self) -> &[Box<ServerName>] {
&self.config.trusted_servers
}
@ -260,6 +303,19 @@ impl Globals {
&self.config.turn_secret
}
pub fn emergency_password(&self) -> &Option<String> {
&self.config.emergency_password
}
pub fn supported_room_versions(&self) -> Vec<RoomVersionId> {
let mut room_versions: Vec<RoomVersionId> = vec![];
room_versions.extend(self.stable_room_versions.clone());
if self.allow_unstable_room_versions() {
room_versions.extend(self.unstable_room_versions.clone());
};
room_versions
}
/// TODO: the key valid until timestamp is only honored in room version > 4
/// Remove the outdated keys and insert the new ones.
///

@ -1,8 +1,8 @@
use crate::{utils, Error, Result};
use ruma::{
api::client::{
backup::{BackupAlgorithm, KeyBackupData, RoomKeyBackup},
error::ErrorKind,
r0::backup::{BackupAlgorithm, KeyBackupData, RoomKeyBackup},
},
serde::Raw,
RoomId, UserId,

@ -2,16 +2,16 @@ use crate::{Database, Error, PduEvent, Result};
use bytes::BytesMut;
use ruma::{
api::{
client::r0::push::{get_pushers, set_pusher, PusherKind},
client::push::{get_pushers, set_pusher, PusherKind},
push_gateway::send_event_notification::{
self,
v1::{Device, Notification, NotificationCounts, NotificationPriority},
},
IncomingResponse, OutgoingRequest, SendAccessToken,
IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken,
},
events::{
room::{name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent},
AnySyncRoomEvent, EventType,
AnySyncRoomEvent, RoomEventType, StateEventType,
},
push::{Action, PushConditionRoomCtx, PushFormat, Ruleset, Tweak},
serde::Raw,
@ -30,7 +30,7 @@ pub struct PushData {
impl PushData {
#[tracing::instrument(skip(self, sender, pusher))]
pub fn set_pusher(&self, sender: &UserId, pusher: set_pusher::Pusher) -> Result<()> {
pub fn set_pusher(&self, sender: &UserId, pusher: set_pusher::v3::Pusher) -> Result<()> {
let mut key = sender.as_bytes().to_vec();
key.push(0xff);
key.extend_from_slice(pusher.pushkey.as_bytes());
@ -53,7 +53,7 @@ impl PushData {
}
#[tracing::instrument(skip(self, senderkey))]
pub fn get_pusher(&self, senderkey: &[u8]) -> Result<Option<get_pushers::Pusher>> {
pub fn get_pusher(&self, senderkey: &[u8]) -> Result<Option<get_pushers::v3::Pusher>> {
self.senderkey_pusher
.get(senderkey)?
.map(|push| {
@ -64,7 +64,7 @@ impl PushData {
}
#[tracing::instrument(skip(self, sender))]
pub fn get_pushers(&self, sender: &UserId) -> Result<Vec<get_pushers::Pusher>> {
pub fn get_pushers(&self, sender: &UserId) -> Result<Vec<get_pushers::v3::Pusher>> {
let mut prefix = sender.as_bytes().to_vec();
prefix.push(0xff);
@ -101,7 +101,11 @@ where
let destination = destination.replace("/_matrix/push/v1/notify", "");
let http_request = request
.try_into_http_request::<BytesMut>(&destination, SendAccessToken::IfRequired(""))
.try_into_http_request::<BytesMut>(
&destination,
SendAccessToken::IfRequired(""),
&[MatrixVersion::V1_0],
)
.map_err(|e| {
warn!("Failed to find destination {}: {}", destination, e);
Error::BadServerResponse("Invalid destination")
@ -167,7 +171,7 @@ where
pub async fn send_push_notice(
user: &UserId,
unread: UInt,
pusher: &get_pushers::Pusher,
pusher: &get_pushers::v3::Pusher,
ruleset: Ruleset,
pdu: &PduEvent,
db: &Database,
@ -177,7 +181,7 @@ pub async fn send_push_notice(
let power_levels: RoomPowerLevelsEventContent = db
.rooms
.room_state_get(&pdu.room_id, &EventType::RoomPowerLevels, "")?
.room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")?
.map(|ev| {
serde_json::from_str(ev.content.get())
.map_err(|_| Error::bad_database("invalid m.room.power_levels event"))
@ -247,7 +251,7 @@ pub fn get_actions<'a>(
#[tracing::instrument(skip(unread, pusher, tweaks, event, db))]
async fn send_notice(
unread: UInt,
pusher: &get_pushers::Pusher,
pusher: &get_pushers::v3::Pusher,
tweaks: Vec<Tweak>,
event: &PduEvent,
db: &Database,
@ -289,7 +293,7 @@ async fn send_notice(
// TODO: missed calls
notifi.counts = NotificationCounts::new(unread, uint!(0));
if event.kind == EventType::RoomEncrypted
if event.kind == RoomEventType::RoomEncrypted
|| tweaks
.iter()
.any(|t| matches!(t, Tweak::Highlight(true) | Tweak::Sound(_)))
@ -310,7 +314,7 @@ async fn send_notice(
let content = serde_json::value::to_raw_value(&event.content).ok();
notifi.content = content.as_deref();
if event.kind == EventType::RoomMember {
if event.kind == RoomEventType::RoomMember {
notifi.user_is_target = event.state_key.as_deref() == Some(event.sender.as_str());
}
@ -319,7 +323,7 @@ async fn send_notice(
let room_name = if let Some(room_name_pdu) =
db.rooms
.room_state_get(&event.room_id, &EventType::RoomName, "")?
.room_state_get(&event.room_id, &StateEventType::RoomName, "")?
{
serde_json::from_str::<RoomNameEventContent>(room_name_pdu.content.get())
.map_err(|_| Error::bad_database("Invalid room name event in database."))?

@ -21,7 +21,8 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent,
},
tag::TagEvent,
AnyStrippedStateEvent, AnySyncStateEvent, EventType,
AnyStrippedStateEvent, AnySyncStateEvent, GlobalAccountDataEventType,
RoomAccountDataEventType, RoomEventType, StateEventType,
},
push::{Action, Ruleset, Tweak},
serde::{CanonicalJsonObject, CanonicalJsonValue, Raw},
@ -32,7 +33,7 @@ use serde::Deserialize;
use serde_json::value::to_raw_value;
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap, HashSet},
collections::{hash_map, BTreeMap, HashMap, HashSet},
fmt::Debug,
iter,
mem::size_of,
@ -75,6 +76,8 @@ pub struct Rooms {
pub(super) userroomid_leftstate: Arc<dyn Tree>,
pub(super) roomuserid_leftcount: Arc<dyn Tree>,
pub(super) disabledroomids: Arc<dyn Tree>, // Rooms where incoming federation handling is disabled
pub(super) lazyloadedids: Arc<dyn Tree>, // LazyLoadedIds = UserId + DeviceId + RoomId + LazyLoadedUserId
pub(super) userroomid_notificationcount: Arc<dyn Tree>, // NotifyCount = u64
@ -111,8 +114,8 @@ pub struct Rooms {
pub(super) shorteventid_cache: Mutex<LruCache<u64, Arc<EventId>>>,
pub(super) auth_chain_cache: Mutex<LruCache<Vec<u64>, Arc<HashSet<u64>>>>,
pub(super) eventidshort_cache: Mutex<LruCache<Box<EventId>, u64>>,
pub(super) statekeyshort_cache: Mutex<LruCache<(EventType, String), u64>>,
pub(super) shortstatekey_cache: Mutex<LruCache<u64, (EventType, String)>>,
pub(super) statekeyshort_cache: Mutex<LruCache<(StateEventType, String), u64>>,
pub(super) shortstatekey_cache: Mutex<LruCache<u64, (StateEventType, String)>>,
pub(super) our_real_users_cache: RwLock<HashMap<Box<RoomId>, Arc<HashSet<Box<UserId>>>>>,
pub(super) appservice_in_room_cache: RwLock<HashMap<Box<RoomId>, HashMap<String, bool>>>,
pub(super) lazy_load_waiting:
@ -128,54 +131,74 @@ pub struct Rooms {
)>,
>,
>,
pub(super) lasttimelinecount_cache: Mutex<HashMap<Box<RoomId>, u64>>,
}
impl Rooms {
/// Returns true if a given room version is supported
#[tracing::instrument(skip(self, db))]
pub fn is_supported_version(&self, db: &Database, room_version: &RoomVersionId) -> bool {
db.globals.supported_room_versions().contains(room_version)
}
/// Builds a StateMap by iterating over all keys that start
/// with state_hash, this gives the full state for the given state_hash.
#[tracing::instrument(skip(self))]
pub fn state_full_ids(&self, shortstatehash: u64) -> Result<BTreeMap<u64, Arc<EventId>>> {
pub async fn state_full_ids(&self, shortstatehash: u64) -> Result<BTreeMap<u64, Arc<EventId>>> {
let full_state = self
.load_shortstatehash_info(shortstatehash)?
.pop()
.expect("there is always one layer")
.1;
full_state
.into_iter()
.map(|compressed| self.parse_compressed_state_event(compressed))
.collect()
let mut result = BTreeMap::new();
let mut i = 0;
for compressed in full_state.into_iter() {
let parsed = self.parse_compressed_state_event(compressed)?;
result.insert(parsed.0, parsed.1);
i += 1;
if i % 100 == 0 {
tokio::task::yield_now().await;
}
}
Ok(result)
}
#[tracing::instrument(skip(self))]
pub fn state_full(
pub async fn state_full(
&self,
shortstatehash: u64,
) -> Result<HashMap<(EventType, String), Arc<PduEvent>>> {
) -> Result<HashMap<(StateEventType, String), Arc<PduEvent>>> {
let full_state = self
.load_shortstatehash_info(shortstatehash)?
.pop()
.expect("there is always one layer")
.1;
Ok(full_state
.into_iter()
.map(|compressed| self.parse_compressed_state_event(compressed))
.filter_map(|r| r.ok())
.map(|(_, eventid)| self.get_pdu(&eventid))
.filter_map(|r| r.ok().flatten())
.map(|pdu| {
Ok::<_, Error>((
let mut result = HashMap::new();
let mut i = 0;
for compressed in full_state {
let (_, eventid) = self.parse_compressed_state_event(compressed)?;
if let Some(pdu) = self.get_pdu(&eventid)? {
result.insert(
(
pdu.kind.clone(),
pdu.kind.to_string().into(),
pdu.state_key
.as_ref()
.ok_or_else(|| Error::bad_database("State event has no state key."))?
.clone(),
),
pdu,
))
})
.filter_map(|r| r.ok())
.collect())
);
}
i += 1;
if i % 100 == 0 {
tokio::task::yield_now().await;
}
}
Ok(result)
}
/// Returns a single PDU from `room_id` with key (`event_type`, `state_key`).
@ -183,7 +206,7 @@ impl Rooms {
pub fn state_get_id(
&self,
shortstatehash: u64,
event_type: &EventType,
event_type: &StateEventType,
state_key: &str,
) -> Result<Option<Arc<EventId>>> {
let shortstatekey = match self.get_shortstatekey(event_type, state_key)? {
@ -210,7 +233,7 @@ impl Rooms {
pub fn state_get(
&self,
shortstatehash: u64,
event_type: &EventType,
event_type: &StateEventType,
state_key: &str,
) -> Result<Option<Arc<PduEvent>>> {
self.state_get_id(shortstatehash, event_type, state_key)?
@ -218,7 +241,6 @@ impl Rooms {
}
/// Returns the state hash for this pdu.
#[tracing::instrument(skip(self))]
pub fn pdu_shortstatehash(&self, event_id: &EventId) -> Result<Option<u64>> {
self.eventid_shorteventid
.get(event_id.as_bytes())?
@ -253,7 +275,7 @@ impl Rooms {
pub fn get_auth_events(
&self,
room_id: &RoomId,
kind: &EventType,
kind: &RoomEventType,
sender: &UserId,
state_key: Option<&str>,
content: &serde_json::value::RawValue,
@ -271,7 +293,7 @@ impl Rooms {
let mut sauthevents = auth_events
.into_iter()
.filter_map(|(event_type, state_key)| {
self.get_shortstatekey(&event_type, &state_key)
self.get_shortstatekey(&event_type.to_string().into(), &state_key)
.ok()
.flatten()
.map(|s| (s, (event_type, state_key)))
@ -521,7 +543,6 @@ impl Rooms {
}
}
#[tracing::instrument(skip(self, globals))]
pub fn compress_state_event(
&self,
shortstatekey: u64,
@ -538,7 +559,6 @@ impl Rooms {
}
/// Returns shortstatekey, event id
#[tracing::instrument(skip(self, compressed_event))]
pub fn parse_compressed_state_event(
&self,
compressed_event: CompressedStateEvent,
@ -697,7 +717,6 @@ impl Rooms {
}
/// Returns (shortstatehash, already_existed)
#[tracing::instrument(skip(self, globals))]
fn get_or_create_shortstatehash(
&self,
state_hash: &StateHashId,
@ -718,7 +737,6 @@ impl Rooms {
})
}
#[tracing::instrument(skip(self, globals))]
pub fn get_or_create_shorteventid(
&self,
event_id: &EventId,
@ -749,7 +767,6 @@ impl Rooms {
Ok(short)
}
#[tracing::instrument(skip(self))]
pub fn get_shortroomid(&self, room_id: &RoomId) -> Result<Option<u64>> {
self.roomid_shortroomid
.get(room_id.as_bytes())?
@ -760,10 +777,9 @@ impl Rooms {
.transpose()
}
#[tracing::instrument(skip(self))]
pub fn get_shortstatekey(
&self,
event_type: &EventType,
event_type: &StateEventType,
state_key: &str,
) -> Result<Option<u64>> {
if let Some(short) = self
@ -775,7 +791,7 @@ impl Rooms {
return Ok(Some(*short));
}
let mut statekey = event_type.as_ref().as_bytes().to_vec();
let mut statekey = event_type.to_string().as_bytes().to_vec();
statekey.push(0xff);
statekey.extend_from_slice(state_key.as_bytes());
@ -798,7 +814,6 @@ impl Rooms {
Ok(short)
}
#[tracing::instrument(skip(self, globals))]
pub fn get_or_create_shortroomid(
&self,
room_id: &RoomId,
@ -816,10 +831,9 @@ impl Rooms {
})
}
#[tracing::instrument(skip(self, globals))]
pub fn get_or_create_shortstatekey(
&self,
event_type: &EventType,
event_type: &StateEventType,
state_key: &str,
globals: &super::globals::Globals,
) -> Result<u64> {
@ -832,7 +846,7 @@ impl Rooms {
return Ok(*short);
}
let mut statekey = event_type.as_ref().as_bytes().to_vec();
let mut statekey = event_type.to_string().as_bytes().to_vec();
statekey.push(0xff);
statekey.extend_from_slice(state_key.as_bytes());
@ -857,7 +871,6 @@ impl Rooms {
Ok(short)
}
#[tracing::instrument(skip(self))]
pub fn get_eventid_from_short(&self, shorteventid: u64) -> Result<Arc<EventId>> {
if let Some(id) = self
.shorteventid_cache
@ -886,8 +899,7 @@ impl Rooms {
Ok(event_id)
}
#[tracing::instrument(skip(self))]
pub fn get_statekey_from_short(&self, shortstatekey: u64) -> Result<(EventType, String)> {
pub fn get_statekey_from_short(&self, shortstatekey: u64) -> Result<(StateEventType, String)> {
if let Some(id) = self
.shortstatekey_cache
.lock()
@ -909,7 +921,7 @@ impl Rooms {
.ok_or_else(|| Error::bad_database("Invalid statekey in shortstatekey_statekey."))?;
let event_type =
EventType::try_from(utils::string_from_bytes(eventtype_bytes).map_err(|_| {
StateEventType::try_from(utils::string_from_bytes(eventtype_bytes).map_err(|_| {
Error::bad_database("Event type in shortstatekey_statekey is invalid unicode.")
})?)
.map_err(|_| Error::bad_database("Event type in shortstatekey_statekey is invalid."))?;
@ -930,12 +942,12 @@ impl Rooms {
/// Returns the full room state.
#[tracing::instrument(skip(self))]
pub fn room_state_full(
pub async fn room_state_full(
&self,
room_id: &RoomId,
) -> Result<HashMap<(EventType, String), Arc<PduEvent>>> {
) -> Result<HashMap<(StateEventType, String), Arc<PduEvent>>> {
if let Some(current_shortstatehash) = self.current_shortstatehash(room_id)? {
self.state_full(current_shortstatehash)
self.state_full(current_shortstatehash).await
} else {
Ok(HashMap::new())
}
@ -946,7 +958,7 @@ impl Rooms {
pub fn room_state_get_id(
&self,
room_id: &RoomId,
event_type: &EventType,
event_type: &StateEventType,
state_key: &str,
) -> Result<Option<Arc<EventId>>> {
if let Some(current_shortstatehash) = self.current_shortstatehash(room_id)? {
@ -961,7 +973,7 @@ impl Rooms {
pub fn room_state_get(
&self,
room_id: &RoomId,
event_type: &EventType,
event_type: &StateEventType,
state_key: &str,
) -> Result<Option<Arc<PduEvent>>> {
if let Some(current_shortstatehash) = self.current_shortstatehash(room_id)? {
@ -972,14 +984,12 @@ impl Rooms {
}
/// Returns the `count` of this pdu's id.
#[tracing::instrument(skip(self))]
pub fn pdu_count(&self, pdu_id: &[u8]) -> Result<u64> {
utils::u64_from_bytes(&pdu_id[pdu_id.len() - size_of::<u64>()..])
.map_err(|_| Error::bad_database("PDU has invalid count bytes."))
}
/// Returns the `count` of this pdu's id.
#[tracing::instrument(skip(self))]
pub fn get_pdu_count(&self, event_id: &EventId) -> Result<Option<u64>> {
self.eventid_pduid
.get(event_id.as_bytes())?
@ -1008,7 +1018,6 @@ impl Rooms {
}
/// Returns the json of a pdu.
#[tracing::instrument(skip(self))]
pub fn get_pdu_json(&self, event_id: &EventId) -> Result<Option<CanonicalJsonObject>> {
self.eventid_pduid
.get(event_id.as_bytes())?
@ -1027,7 +1036,6 @@ impl Rooms {
}
/// Returns the json of a pdu.
#[tracing::instrument(skip(self))]
pub fn get_outlier_pdu_json(&self, event_id: &EventId) -> Result<Option<CanonicalJsonObject>> {
self.eventid_outlierpdu
.get(event_id.as_bytes())?
@ -1038,7 +1046,6 @@ impl Rooms {
}
/// Returns the json of a pdu.
#[tracing::instrument(skip(self))]
pub fn get_non_outlier_pdu_json(
&self,
event_id: &EventId,
@ -1058,7 +1065,6 @@ impl Rooms {
}
/// Returns the pdu's id.
#[tracing::instrument(skip(self))]
pub fn get_pdu_id(&self, event_id: &EventId) -> Result<Option<Vec<u8>>> {
self.eventid_pduid.get(event_id.as_bytes())
}
@ -1066,7 +1072,6 @@ impl Rooms {
/// Returns the pdu.
///
/// Checks the `eventid_outlierpdu` Tree if not found in the timeline.
#[tracing::instrument(skip(self))]
pub fn get_non_outlier_pdu(&self, event_id: &EventId) -> Result<Option<PduEvent>> {
self.eventid_pduid
.get(event_id.as_bytes())?
@ -1085,7 +1090,6 @@ impl Rooms {
/// Returns the pdu.
///
/// Checks the `eventid_outlierpdu` Tree if not found in the timeline.
#[tracing::instrument(skip(self))]
pub fn get_pdu(&self, event_id: &EventId) -> Result<Option<Arc<PduEvent>>> {
if let Some(p) = self.pdu_cache.lock().unwrap().get_mut(event_id) {
return Ok(Some(Arc::clone(p)));
@ -1122,7 +1126,6 @@ impl Rooms {
/// Returns the pdu.
///
/// This does __NOT__ check the outliers `Tree`.
#[tracing::instrument(skip(self))]
pub fn get_pdu_from_id(&self, pdu_id: &[u8]) -> Result<Option<PduEvent>> {
self.pduid_pdu.get(pdu_id)?.map_or(Ok(None), |pdu| {
Ok(Some(
@ -1133,7 +1136,6 @@ impl Rooms {
}
/// Returns the pdu as a `BTreeMap<String, CanonicalJsonValue>`.
#[tracing::instrument(skip(self))]
pub fn get_pdu_json_from_id(&self, pdu_id: &[u8]) -> Result<Option<CanonicalJsonObject>> {
self.pduid_pdu.get(pdu_id)?.map_or(Ok(None), |pdu| {
Ok(Some(
@ -1222,7 +1224,6 @@ impl Rooms {
}
/// Returns the pdu from the outlier tree.
#[tracing::instrument(skip(self))]
pub fn get_pdu_outlier(&self, event_id: &EventId) -> Result<Option<PduEvent>> {
self.eventid_outlierpdu
.get(event_id.as_bytes())?
@ -1280,7 +1281,7 @@ impl Rooms {
{
if let Some(shortstatehash) = self.pdu_shortstatehash(&pdu.event_id).unwrap() {
if let Some(prev_state) = self
.state_get(shortstatehash, &pdu.kind, state_key)
.state_get(shortstatehash, &pdu.kind.to_string().into(), state_key)
.unwrap()
{
unsigned.insert(
@ -1331,6 +1332,10 @@ impl Rooms {
&pdu_id,
&serde_json::to_vec(&pdu_json).expect("CanonicalJsonObject is always a valid"),
)?;
self.lasttimelinecount_cache
.lock()
.unwrap()
.insert(pdu.room_id.clone(), count2);
self.eventid_pduid
.insert(pdu.event_id.as_bytes(), &pdu_id)?;
@ -1341,7 +1346,7 @@ impl Rooms {
// See if the event matches any known pushers
let power_levels: RoomPowerLevelsEventContent = db
.rooms
.room_state_get(&pdu.room_id, &EventType::RoomPowerLevels, "")?
.room_state_get(&pdu.room_id, &StateEventType::RoomPowerLevels, "")?
.map(|ev| {
serde_json::from_str(ev.content.get())
.map_err(|_| Error::bad_database("invalid m.room.power_levels event"))
@ -1362,7 +1367,11 @@ impl Rooms {
let rules_for_user = db
.account_data
.get(None, user, EventType::PushRules)?
.get(
None,
user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)?
.map(|ev: PushRulesEvent| ev.content.global)
.unwrap_or_else(|| Ruleset::server_default(user));
@ -1411,12 +1420,12 @@ impl Rooms {
.increment_batch(&mut highlights.into_iter())?;
match pdu.kind {
EventType::RoomRedaction => {
RoomEventType::RoomRedaction => {
if let Some(redact_id) = &pdu.redacts {
self.redact_pdu(redact_id, pdu)?;
}
}
EventType::RoomMember => {
RoomEventType::RoomMember => {
if let Some(state_key) = &pdu.state_key {
#[derive(Deserialize)]
struct ExtractMembership {
@ -1451,7 +1460,7 @@ impl Rooms {
)?;
}
}
EventType::RoomMessage => {
RoomEventType::RoomMessage => {
#[derive(Deserialize)]
struct ExtractBody<'a> {
#[serde(borrow)]
@ -1477,17 +1486,22 @@ impl Rooms {
self.tokenids.insert_batch(&mut batch)?;
if body.starts_with(&format!("@conduit:{}: ", db.globals.server_name()))
&& self
.id_from_alias(
<&RoomAliasId>::try_from(
format!("#admins:{}", db.globals.server_name()).as_str(),
)
.expect("#admins:server_name is a valid room alias"),
)?
.as_ref()
== Some(&pdu.room_id)
{
let admin_room = self.id_from_alias(
<&RoomAliasId>::try_from(
format!("#admins:{}", db.globals.server_name()).as_str(),
)
.expect("#admins:server_name is a valid room alias"),
)?;
let server_user = format!("@conduit:{}", db.globals.server_name());
let to_conduit = body.starts_with(&format!("{}: ", server_user));
// This will evaluate to false if the emergency password is set up so that
// the administrator can execute commands as conduit
let from_conduit =
pdu.sender == server_user && db.globals.emergency_password().is_none();
if to_conduit && !from_conduit && admin_room.as_ref() == Some(&pdu.room_id) {
db.admin.process_message(body.to_string());
}
}
@ -1498,6 +1512,36 @@ impl Rooms {
Ok(pdu_id)
}
#[tracing::instrument(skip(self))]
pub fn last_timeline_count(&self, sender_user: &UserId, room_id: &RoomId) -> Result<u64> {
match self
.lasttimelinecount_cache
.lock()
.unwrap()
.entry(room_id.to_owned())
{
hash_map::Entry::Vacant(v) => {
if let Some(last_count) = self
.pdus_until(&sender_user, &room_id, u64::MAX)?
.filter_map(|r| {
// Filter out buggy events
if r.is_err() {
error!("Bad pdu in pdus_since: {:?}", r);
}
r.ok()
})
.map(|(pduid, _)| self.pdu_count(&pduid))
.next()
{
Ok(*v.insert(last_count?))
} else {
Ok(0)
}
}
hash_map::Entry::Occupied(o) => Ok(*o.get()),
}
}
#[tracing::instrument(skip(self))]
pub fn reset_notification_counts(&self, user_id: &UserId, room_id: &RoomId) -> Result<()> {
let mut userroom_id = user_id.as_bytes().to_vec();
@ -1627,8 +1671,11 @@ impl Rooms {
let states_parents = previous_shortstatehash
.map_or_else(|| Ok(Vec::new()), |p| self.load_shortstatehash_info(p))?;
let shortstatekey =
self.get_or_create_shortstatekey(&new_pdu.kind, state_key, globals)?;
let shortstatekey = self.get_or_create_shortstatekey(
&new_pdu.kind.to_string().into(),
state_key,
globals,
)?;
let new = self.compress_state_event(shortstatekey, &new_pdu.event_id, globals)?;
@ -1677,28 +1724,36 @@ impl Rooms {
) -> Result<Vec<Raw<AnyStrippedStateEvent>>> {
let mut state = Vec::new();
// Add recommended events
if let Some(e) = self.room_state_get(&invite_event.room_id, &EventType::RoomCreate, "")? {
state.push(e.to_stripped_state_event());
}
if let Some(e) =
self.room_state_get(&invite_event.room_id, &EventType::RoomJoinRules, "")?
self.room_state_get(&invite_event.room_id, &StateEventType::RoomCreate, "")?
{
state.push(e.to_stripped_state_event());
}
if let Some(e) =
self.room_state_get(&invite_event.room_id, &EventType::RoomCanonicalAlias, "")?
self.room_state_get(&invite_event.room_id, &StateEventType::RoomJoinRules, "")?
{
state.push(e.to_stripped_state_event());
}
if let Some(e) = self.room_state_get(&invite_event.room_id, &EventType::RoomAvatar, "")? {
if let Some(e) = self.room_state_get(
&invite_event.room_id,
&StateEventType::RoomCanonicalAlias,
"",
)? {
state.push(e.to_stripped_state_event());
}
if let Some(e) =
self.room_state_get(&invite_event.room_id, &StateEventType::RoomAvatar, "")?
{
state.push(e.to_stripped_state_event());
}
if let Some(e) = self.room_state_get(&invite_event.room_id, &EventType::RoomName, "")? {
if let Some(e) =
self.room_state_get(&invite_event.room_id, &StateEventType::RoomName, "")?
{
state.push(e.to_stripped_state_event());
}
if let Some(e) = self.room_state_get(
&invite_event.room_id,
&EventType::RoomMember,
&StateEventType::RoomMember,
invite_event.sender.as_str(),
)? {
state.push(e.to_stripped_state_event());
@ -1771,7 +1826,7 @@ impl Rooms {
.take(20)
.collect::<Vec<_>>();
let create_event = self.room_state_get(room_id, &EventType::RoomCreate, "")?;
let create_event = self.room_state_get(room_id, &StateEventType::RoomCreate, "")?;
let create_event_content: Option<RoomCreateEventContent> = create_event
.as_ref()
@ -1783,17 +1838,12 @@ impl Rooms {
})
.transpose()?;
let create_prev_event = if prev_events.len() == 1
&& Some(&prev_events[0]) == create_event.as_ref().map(|c| &c.event_id)
{
create_event
} else {
None
};
// If there was no create event yet, assume we are creating a version 6 room right now
// If there was no create event yet, assume we are creating a room with the default
// version right now
let room_version_id = create_event_content
.map_or(RoomVersionId::V6, |create_event| create_event.room_version);
.map_or(db.globals.default_room_version(), |create_event| {
create_event.room_version
});
let room_version = RoomVersion::new(&room_version_id).expect("room version is supported");
let auth_events =
@ -1809,7 +1859,9 @@ impl Rooms {
let mut unsigned = unsigned.unwrap_or_default();
if let Some(state_key) = &state_key {
if let Some(prev_pdu) = self.room_state_get(room_id, &event_type, state_key)? {
if let Some(prev_pdu) =
self.room_state_get(room_id, &event_type.to_string().into(), state_key)?
{
unsigned.insert(
"prev_content".to_owned(),
serde_json::from_str(prev_pdu.content.get()).expect("string is valid json"),
@ -1852,7 +1904,6 @@ impl Rooms {
let auth_check = state_res::auth_check(
&room_version,
&pdu,
create_prev_event,
None::<PduEvent>, // TODO: third_party_invite
|k, s| auth_events.get(&(k.clone(), s.to_owned())),
)
@ -1880,13 +1931,26 @@ impl Rooms {
CanonicalJsonValue::String(db.globals.server_name().as_ref().to_owned()),
);
ruma::signatures::hash_and_sign_event(
match ruma::signatures::hash_and_sign_event(
db.globals.server_name().as_str(),
db.globals.keypair(),
&mut pdu_json,
&room_version_id,
)
.expect("event is valid, we just created it");
) {
Ok(_) => {}
Err(e) => {
return match e {
ruma::signatures::Error::PduSize => Err(Error::BadRequest(
ErrorKind::TooLarge,
"Message is too long",
)),
_ => Err(Error::BadRequest(
ErrorKind::Unknown,
"Signing event failed",
)),
}
}
}
// Generate event id
pdu.event_id = EventId::parse_arc(format!(
@ -1921,12 +1985,24 @@ impl Rooms {
// where events in the current room state do not exist
self.set_room_state(room_id, statehashid)?;
let servers = self
.room_servers(room_id)
.filter_map(|r| r.ok())
.filter(|server| &**server != db.globals.server_name());
let mut servers: HashSet<Box<ServerName>> =
self.room_servers(room_id).filter_map(|r| r.ok()).collect();
// In case we are kicking or banning a user, we need to inform their server of the change
if pdu.kind == RoomEventType::RoomMember {
if let Some(state_key_uid) = &pdu
.state_key
.as_ref()
.and_then(|state_key| UserId::parse(state_key.as_str()).ok())
{
servers.insert(Box::from(state_key_uid.server_name()));
}
}
// Remove our server from the server list since it will be added to it by room_servers() and/or the if statement above
servers.remove(db.globals.server_name());
db.sending.send_pdu(servers, &pdu_id)?;
db.sending.send_pdu(servers.into_iter(), &pdu_id)?;
for appservice in db.appservice.all()? {
if self.appservice_in_room(room_id, &appservice, db)? {
@ -1934,6 +2010,30 @@ impl Rooms {
continue;
}
// If the RoomMember event has a non-empty state_key, it is targeted at someone.
// If it is our appservice user, we send this PDU to it.
if pdu.kind == RoomEventType::RoomMember {
if let Some(state_key_uid) = &pdu
.state_key
.as_ref()
.and_then(|state_key| UserId::parse(state_key.as_str()).ok())
{
if let Some(appservice_uid) = appservice
.1
.get("sender_localpart")
.and_then(|string| string.as_str())
.and_then(|string| {
UserId::parse_with_server_name(string, db.globals.server_name()).ok()
})
{
if state_key_uid == &appservice_uid {
db.sending.send_pdu_appservice(&appservice.0, &pdu_id)?;
continue;
}
}
}
}
if let Some(namespaces) = appservice.1.get("namespaces") {
let users = namespaces
.get("users")
@ -1959,7 +2059,7 @@ impl Rooms {
let matching_users = |users: &Regex| {
users.is_match(pdu.sender.as_str())
|| pdu.kind == EventType::RoomMember
|| pdu.kind == RoomEventType::RoomMember
&& pdu
.state_key
.as_ref()
@ -2111,13 +2211,9 @@ impl Rooms {
.ok_or_else(|| Error::bad_database("PDU ID points to invalid PDU."))?;
pdu.redact(reason)?;
self.replace_pdu(&pdu_id, &pdu)?;
Ok(())
} else {
Err(Error::BadRequest(
ErrorKind::NotFound,
"Event ID does not exist.",
))
}
// If event does not exist, just noop
Ok(())
}
/// Update current membership data.
@ -2163,7 +2259,7 @@ impl Rooms {
// Check if the room has a predecessor
if let Some(predecessor) = self
.room_state_get(room_id, &EventType::RoomCreate, "")?
.room_state_get(room_id, &StateEventType::RoomCreate, "")?
.and_then(|create| serde_json::from_str(create.content.get()).ok())
.and_then(|content: RoomCreateEventContent| content.predecessor)
{
@ -2196,13 +2292,13 @@ impl Rooms {
if let Some(tag_event) = db.account_data.get::<TagEvent>(
Some(&predecessor.room_id),
user_id,
EventType::Tag,
RoomAccountDataEventType::Tag,
)? {
db.account_data
.update(
Some(room_id),
user_id,
EventType::Tag,
RoomAccountDataEventType::Tag,
&tag_event,
&db.globals,
)
@ -2210,10 +2306,11 @@ impl Rooms {
};
// Copy direct chat flag
if let Some(mut direct_event) =
db.account_data
.get::<DirectEvent>(None, user_id, EventType::Direct)?
{
if let Some(mut direct_event) = db.account_data.get::<DirectEvent>(
None,
user_id,
GlobalAccountDataEventType::Direct.to_string().into(),
)? {
let mut room_ids_updated = false;
for room_ids in direct_event.content.0.values_mut() {
@ -2227,7 +2324,7 @@ impl Rooms {
db.account_data.update(
None,
user_id,
EventType::Direct,
GlobalAccountDataEventType::Direct.to_string().into(),
&direct_event,
&db.globals,
)?;
@ -2254,7 +2351,9 @@ impl Rooms {
.get::<IgnoredUserListEvent>(
None, // Ignored users are in global account data
user_id, // Receiver
EventType::IgnoredUserList,
GlobalAccountDataEventType::IgnoredUserList
.to_string()
.into(),
)?
.map_or(false, |ignored| {
ignored
@ -2470,6 +2569,27 @@ impl Rooms {
}
}
// Make a user leave all their joined rooms
#[tracing::instrument(skip(self, db))]
pub async fn leave_all_rooms(&self, user_id: &UserId, db: &Database) -> Result<()> {
let all_rooms = db
.rooms
.rooms_joined(user_id)
.chain(db.rooms.rooms_invited(user_id).map(|t| t.map(|(r, _)| r)))
.collect::<Vec<_>>();
for room_id in all_rooms {
let room_id = match room_id {
Ok(room_id) => room_id,
Err(_) => continue,
};
let _ = self.leave_room(user_id, &room_id, db).await;
}
Ok(())
}
#[tracing::instrument(skip(self, db))]
pub async fn leave_room(
&self,
@ -2510,7 +2630,7 @@ impl Rooms {
let state_lock = mutex_state.lock().await;
let mut event: RoomMemberEventContent = serde_json::from_str(
self.room_state_get(room_id, &EventType::RoomMember, user_id.as_str())?
self.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
.ok_or(Error::BadRequest(
ErrorKind::BadState,
"Cannot leave a room you are not a member of.",
@ -2524,7 +2644,7 @@ impl Rooms {
self.build_and_append_pdu(
PduBuilder {
event_type: EventType::RoomMember,
event_type: RoomEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),
@ -2574,7 +2694,7 @@ impl Rooms {
.send_federation_request(
&db.globals,
&remote_server,
federation::membership::get_leave_event::v1::Request { room_id, user_id },
federation::membership::prepare_leave_event::v1::Request { room_id, user_id },
)
.await;
@ -2588,9 +2708,7 @@ impl Rooms {
let (make_leave_response, remote_server) = make_leave_response_and_server?;
let room_version_id = match make_leave_response.room_version {
Some(version) if version == RoomVersionId::V5 || version == RoomVersionId::V6 => {
version
}
Some(version) if self.is_supported_version(&db, &version) => version,
_ => return Err(Error::BadServerResponse("Room version is not supported")),
};
@ -2754,6 +2872,18 @@ impl Rooms {
Ok(self.publicroomids.get(room_id.as_bytes())?.is_some())
}
#[tracing::instrument(skip(self))]
pub fn iter_ids(&self) -> impl Iterator<Item = Result<Box<RoomId>>> + '_ {
self.roomid_shortroomid.iter().map(|(bytes, _)| {
RoomId::parse(
utils::string_from_bytes(&bytes).map_err(|_| {
Error::bad_database("Room ID in publicroomids is invalid unicode.")
})?,
)
.map_err(|_| Error::bad_database("Room ID in roomid_shortroomid is invalid."))
})
}
#[tracing::instrument(skip(self))]
pub fn public_rooms(&self) -> impl Iterator<Item = Result<Box<RoomId>>> + '_ {
self.publicroomids.iter().map(|(bytes, _)| {
@ -3036,6 +3166,10 @@ impl Rooms {
.transpose()
}
pub fn is_disabled(&self, room_id: &RoomId) -> Result<bool> {
Ok(self.disabledroomids.get(room_id.as_bytes())?.is_some())
}
/// Returns an iterator over all rooms this user joined.
#[tracing::instrument(skip(self))]
pub fn rooms_joined<'a>(
@ -3346,4 +3480,24 @@ impl Rooms {
Ok(())
}
/// Returns the room's version.
#[tracing::instrument(skip(self))]
pub fn get_room_version(&self, room_id: &RoomId) -> Result<RoomVersionId> {
let create_event = self.room_state_get(room_id, &StateEventType::RoomCreate, "")?;
let create_event_content: Option<RoomCreateEventContent> = create_event
.as_ref()
.map(|create_event| {
serde_json::from_str(create_event.content.get()).map_err(|e| {
warn!("Invalid create event: {}", e);
Error::bad_database("Invalid create event in db.")
})
})
.transpose()?;
let room_version = create_event_content
.map(|create_event| create_event.room_version)
.ok_or_else(|| Error::BadDatabase("Invalid room version"))?;
Ok(room_version)
}
}

@ -2,7 +2,8 @@ use crate::{database::abstraction::Tree, utils, Error, Result};
use ruma::{
events::{
presence::{PresenceEvent, PresenceEventContent},
AnyEphemeralRoomEvent, SyncEphemeralRoomEvent,
receipt::ReceiptEvent,
SyncEphemeralRoomEvent,
},
presence::PresenceState,
serde::Raw,
@ -31,7 +32,7 @@ impl RoomEdus {
&self,
user_id: &UserId,
room_id: &RoomId,
event: AnyEphemeralRoomEvent,
event: ReceiptEvent,
globals: &super::super::globals::Globals,
) -> Result<()> {
let mut prefix = room_id.as_bytes().to_vec();

@ -9,11 +9,8 @@ use crate::{
appservice_server, database::pusher, server_server, utils, Database, Error, PduEvent, Result,
};
use federation::transactions::send_transaction_message;
use futures_util::{stream::FuturesUnordered, StreamExt};
use ring::digest;
use rocket::futures::{
channel::mpsc,
stream::{FuturesUnordered, StreamExt},
};
use ruma::{
api::{
appservice,
@ -26,14 +23,14 @@ use ruma::{
OutgoingRequest,
},
device_id,
events::{push_rules::PushRulesEvent, AnySyncEphemeralRoomEvent, EventType},
events::{push_rules::PushRulesEvent, AnySyncEphemeralRoomEvent, GlobalAccountDataEventType},
push,
receipt::ReceiptType,
uint, MilliSecondsSinceUnixEpoch, ServerName, UInt, UserId,
};
use tokio::{
select,
sync::{RwLock, Semaphore},
sync::{mpsc, RwLock, Semaphore},
};
use tracing::{error, warn};
@ -41,7 +38,7 @@ use super::abstraction::Tree;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum OutgoingKind {
Appservice(Box<ServerName>),
Appservice(String),
Push(Vec<u8>, Vec<u8>), // user and pushkey
Normal(Box<ServerName>),
}
@ -170,7 +167,7 @@ impl Sending {
Self::parse_servercurrentevent(&k, v).ok().map(|ev| (ev, k))
})
.take(30)
.collect::<>();
.collect();
// TODO: find edus
@ -207,7 +204,7 @@ impl Sending {
}
};
},
Some((key, value)) = receiver.next() => {
Some((key, value)) = receiver.recv() => {
if let Ok((outgoing_kind, event)) = Self::parse_servercurrentevent(&key, value) {
let guard = db.read().await;
@ -417,7 +414,7 @@ impl Sending {
key.push(0xff);
key.extend_from_slice(pdu_id);
self.servernameevent_data.insert(&key, &[])?;
self.sender.unbounded_send((key, vec![])).unwrap();
self.sender.send((key, vec![])).unwrap();
Ok(())
}
@ -433,7 +430,7 @@ impl Sending {
key.push(0xff);
key.extend_from_slice(pdu_id);
self.sender.unbounded_send((key.clone(), vec![])).unwrap();
self.sender.send((key.clone(), vec![])).unwrap();
(key, Vec::new())
});
@ -454,7 +451,7 @@ impl Sending {
key.push(0xff);
key.extend_from_slice(&id.to_be_bytes());
self.servernameevent_data.insert(&key, &serialized)?;
self.sender.unbounded_send((key, serialized)).unwrap();
self.sender.send((key, serialized)).unwrap();
Ok(())
}
@ -466,7 +463,7 @@ impl Sending {
key.push(0xff);
key.extend_from_slice(pdu_id);
self.servernameevent_data.insert(&key, &[])?;
self.sender.unbounded_send((key, vec![])).unwrap();
self.sender.send((key, vec![])).unwrap();
Ok(())
}
@ -508,7 +505,7 @@ impl Sending {
let db = db.read().await;
match &kind {
OutgoingKind::Appservice(server) => {
OutgoingKind::Appservice(id) => {
let mut pdu_jsons = Vec::new();
for event in &events {
@ -538,7 +535,7 @@ impl Sending {
let response = appservice_server::send_request(
&db.globals,
db.appservice
.get_registration(server.as_str())
.get_registration(&id)
.map_err(|e| (kind.clone(), e))?
.ok_or_else(|| {
(
@ -638,7 +635,11 @@ impl Sending {
let rules_for_user = db
.account_data
.get(None, &userid, EventType::PushRules)
.get(
None,
&userid,
GlobalAccountDataEventType::PushRules.to_string().into(),
)
.unwrap_or_default()
.map(|ev: PushRulesEvent| ev.content.global)
.unwrap_or_else(|| push::Ruleset::server_default(&userid));
@ -759,9 +760,7 @@ impl Sending {
})?;
(
OutgoingKind::Appservice(ServerName::parse(server).map_err(|_| {
Error::bad_database("Invalid server string in server_currenttransaction")
})?),
OutgoingKind::Appservice(server),
if value.is_empty() {
SendingEventType::Pdu(event.to_vec())
} else {

@ -1,7 +1,7 @@
use std::sync::Arc;
use crate::Result;
use ruma::{identifiers::TransactionId, DeviceId, UserId};
use ruma::{DeviceId, TransactionId, UserId};
use super::abstraction::Tree;

@ -1,14 +1,15 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::RwLock;
use std::{
collections::BTreeMap,
sync::{Arc, RwLock},
};
use crate::{client_server::SESSION_ID_LENGTH, utils, Error, Result};
use ruma::{
api::client::{
error::ErrorKind,
r0::uiaa::{
AuthType, IncomingAuthData, IncomingPassword, IncomingUserIdentifier::MatrixId,
UiaaInfo,
uiaa::{
AuthType, IncomingAuthData, IncomingPassword,
IncomingUserIdentifier::UserIdOrLocalpart, UiaaInfo,
},
},
signatures::CanonicalJsonValue,
@ -73,7 +74,7 @@ impl Uiaa {
..
}) => {
let username = match identifier {
MatrixId(username) => username,
UserIdOrLocalpart(username) => username,
_ => {
return Err(Error::BadRequest(
ErrorKind::Unrecognized,

@ -1,15 +1,11 @@
use crate::{utils, Error, Result};
use ruma::{
api::client::{
error::ErrorKind,
r0::{device::Device, filter::IncomingFilterDefinition},
},
api::client::{device::Device, error::ErrorKind, filter::IncomingFilterDefinition},
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
events::{AnyToDeviceEvent, EventType},
identifiers::MxcUri,
events::{AnyToDeviceEvent, StateEventType},
serde::Raw,
DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, RoomAliasId, UInt,
UserId,
DeviceId, DeviceKeyAlgorithm, DeviceKeyId, MilliSecondsSinceUnixEpoch, MxcUri, RoomAliasId,
UInt, UserId,
};
use std::{collections::BTreeMap, mem, sync::Arc};
use tracing::warn;
@ -757,7 +753,7 @@ impl Users {
for room_id in rooms.rooms_joined(user_id).filter_map(|r| r.ok()) {
// Don't send key updates to unencrypted rooms
if rooms
.room_state_get(&room_id, &EventType::RoomEncryption, "")?
.room_state_get(&room_id, &StateEventType::RoomEncryption, "")?
.is_none()
{
continue;

@ -1,27 +1,20 @@
use std::convert::Infallible;
use http::StatusCode;
use ruma::{
api::client::{
error::{Error as RumaError, ErrorKind},
r0::uiaa::UiaaInfo,
uiaa::{UiaaInfo, UiaaResponse},
},
ServerName,
};
use thiserror::Error;
use tracing::warn;
use tracing::{error, warn};
#[cfg(feature = "persy")]
use persy::PersyError;
#[cfg(feature = "conduit_bin")]
use {
crate::RumaResponse,
http::StatusCode,
rocket::{
response::{self, Responder},
Request,
},
ruma::api::client::r0::uiaa::UiaaResponse,
tracing::error,
};
use crate::RumaResponse;
pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -81,6 +74,12 @@ pub enum Error {
BadRequest(ErrorKind, &'static str),
#[error("{0}")]
Conflict(&'static str), // This is only needed for when a room alias already exists
#[cfg(feature = "conduit_bin")]
#[error("{0}")]
ExtensionError(#[from] axum::extract::rejection::ExtensionRejection),
#[cfg(feature = "conduit_bin")]
#[error("{0}")]
PathError(#[from] axum::extract::rejection::PathRejection),
}
impl Error {
@ -139,16 +138,6 @@ impl Error {
}
}
#[cfg(feature = "conduit_bin")]
impl<'r, 'o> Responder<'r, 'o> for Error
where
'o: 'r,
{
fn respond_to(self, r: &'r Request<'_>) -> response::Result<'o> {
self.to_response().respond_to(r)
}
}
#[cfg(feature = "persy")]
impl<T: Into<PersyError>> From<persy::PE<T>> for Error {
fn from(err: persy::PE<T>) -> Self {
@ -157,3 +146,16 @@ impl<T: Into<PersyError>> From<persy::PE<T>> for Error {
}
}
}
impl From<Infallible> for Error {
fn from(i: Infallible) -> Self {
match i {}
}
}
#[cfg(feature = "conduit_bin")]
impl axum::response::IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
self.to_response().into_response()
}
}

@ -7,8 +7,6 @@
#![allow(clippy::suspicious_else_formatting)]
#![deny(clippy::dbg_macro)]
use std::ops::Deref;
mod config;
mod database;
mod error;
@ -24,16 +22,4 @@ pub use config::Config;
pub use database::Database;
pub use error::{Error, Result};
pub use pdu::PduEvent;
pub use rocket::Config as RocketConfig;
pub use ruma_wrapper::{ConduitResult, Ruma, RumaResponse};
pub struct State<'r, T: Send + Sync + 'static>(pub &'r T);
impl<'r, T: Send + Sync + 'static> Deref for State<'r, T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &T {
self.0
}
}
pub use ruma_wrapper::{Ruma, RumaResponse};

@ -7,24 +7,37 @@
#![allow(clippy::suspicious_else_formatting)]
#![deny(clippy::dbg_macro)]
use std::sync::Arc;
use std::{future::Future, io, net::SocketAddr, sync::Arc, time::Duration};
use maplit::hashset;
use axum::{
extract::{FromRequest, MatchedPath},
handler::Handler,
response::IntoResponse,
routing::{get, on, MethodFilter},
Router,
};
use axum_server::{bind, bind_rustls, tls_rustls::RustlsConfig, Handle as ServerHandle};
use figment::{
providers::{Env, Format, Toml},
Figment,
};
use http::{
header::{self, HeaderName},
Method, Uri,
};
use opentelemetry::trace::{FutureExt, Tracer};
use rocket::{
catch, catchers,
figment::{
providers::{Env, Format, Toml},
Figment,
},
routes, Request,
use ruma::api::{client::error::ErrorKind, IncomingRequest};
use tokio::{signal, sync::RwLock};
use tower::ServiceBuilder;
use tower_http::{
cors::{self, CorsLayer},
trace::TraceLayer,
ServiceBuilderExt as _,
};
use ruma::api::client::error::ErrorKind;
use tokio::sync::RwLock;
use tracing::warn;
use tracing_subscriber::{prelude::*, EnvFilter};
pub use conduit::*; // Re-export everything from the library crate
pub use rocket::State;
#[cfg(all(not(target_env = "msvc"), feature = "jemalloc"))]
use tikv_jemallocator::Jemalloc;
@ -33,160 +46,10 @@ use tikv_jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
fn setup_rocket(config: Figment, data: Arc<RwLock<Database>>) -> rocket::Rocket<rocket::Build> {
rocket::custom(config)
.manage(data)
.mount(
"/",
routes![
client_server::get_supported_versions_route,
client_server::get_register_available_route,
client_server::register_route,
client_server::get_login_types_route,
client_server::login_route,
client_server::whoami_route,
client_server::logout_route,
client_server::logout_all_route,
client_server::change_password_route,
client_server::deactivate_route,
client_server::third_party_route,
client_server::get_capabilities_route,
client_server::get_pushrules_all_route,
client_server::set_pushrule_route,
client_server::get_pushrule_route,
client_server::set_pushrule_enabled_route,
client_server::get_pushrule_enabled_route,
client_server::get_pushrule_actions_route,
client_server::set_pushrule_actions_route,
client_server::delete_pushrule_route,
client_server::get_room_event_route,
client_server::get_room_aliases_route,
client_server::get_filter_route,
client_server::create_filter_route,
client_server::set_global_account_data_route,
client_server::set_room_account_data_route,
client_server::get_global_account_data_route,
client_server::get_room_account_data_route,
client_server::set_displayname_route,
client_server::get_displayname_route,
client_server::set_avatar_url_route,
client_server::get_avatar_url_route,
client_server::get_profile_route,
client_server::set_presence_route,
client_server::get_presence_route,
client_server::upload_keys_route,
client_server::get_keys_route,
client_server::claim_keys_route,
client_server::create_backup_route,
client_server::update_backup_route,
client_server::delete_backup_route,
client_server::get_latest_backup_route,
client_server::get_backup_route,
client_server::add_backup_key_sessions_route,
client_server::add_backup_keys_route,
client_server::delete_backup_key_session_route,
client_server::delete_backup_key_sessions_route,
client_server::delete_backup_keys_route,
client_server::get_backup_key_session_route,
client_server::get_backup_key_sessions_route,
client_server::get_backup_keys_route,
client_server::set_read_marker_route,
client_server::create_receipt_route,
client_server::create_typing_event_route,
client_server::create_room_route,
client_server::redact_event_route,
client_server::report_event_route,
client_server::create_alias_route,
client_server::delete_alias_route,
client_server::get_alias_route,
client_server::join_room_by_id_route,
client_server::join_room_by_id_or_alias_route,
client_server::joined_members_route,
client_server::leave_room_route,
client_server::forget_room_route,
client_server::joined_rooms_route,
client_server::kick_user_route,
client_server::ban_user_route,
client_server::unban_user_route,
client_server::invite_user_route,
client_server::set_room_visibility_route,
client_server::get_room_visibility_route,
client_server::get_public_rooms_route,
client_server::get_public_rooms_filtered_route,
client_server::search_users_route,
client_server::get_member_events_route,
client_server::get_protocols_route,
client_server::send_message_event_route,
client_server::send_state_event_for_key_route,
client_server::send_state_event_for_empty_key_route,
client_server::get_state_events_route,
client_server::get_state_events_for_key_route,
client_server::get_state_events_for_empty_key_route,
client_server::sync_events_route,
client_server::get_context_route,
client_server::get_message_events_route,
client_server::search_events_route,
client_server::turn_server_route,
client_server::send_event_to_device_route,
client_server::get_media_config_route,
client_server::create_content_route,
client_server::get_content_as_filename_route,
client_server::get_content_route,
client_server::get_content_thumbnail_route,
client_server::get_devices_route,
client_server::get_device_route,
client_server::update_device_route,
client_server::delete_device_route,
client_server::delete_devices_route,
client_server::get_tags_route,
client_server::update_tag_route,
client_server::delete_tag_route,
client_server::options_route,
client_server::upload_signing_keys_route,
client_server::upload_signatures_route,
client_server::get_key_changes_route,
client_server::get_pushers_route,
client_server::set_pushers_route,
// client_server::third_party_route,
client_server::upgrade_room_route,
server_server::get_server_version_route,
server_server::get_server_keys_route,
server_server::get_server_keys_deprecated_route,
server_server::get_public_rooms_route,
server_server::get_public_rooms_filtered_route,
server_server::send_transaction_message_route,
server_server::get_event_route,
server_server::get_missing_events_route,
server_server::get_event_authorization_route,
server_server::get_room_state_route,
server_server::get_room_state_ids_route,
server_server::create_join_event_template_route,
server_server::create_join_event_v1_route,
server_server::create_join_event_v2_route,
server_server::create_invite_route,
server_server::get_devices_route,
server_server::get_room_information_route,
server_server::get_profile_information_route,
server_server::get_keys_route,
server_server::claim_keys_route,
],
)
.register(
"/",
catchers![
not_found_catcher,
forbidden_catcher,
unknown_token_catcher,
missing_token_catcher,
bad_json_catcher
],
)
}
#[rocket::main]
#[tokio::main]
async fn main() {
let raw_config =
Figment::from(default_config())
Figment::new()
.merge(
Toml::file(Env::var("CONDUIT_CONFIG").expect(
"The CONDUIT_CONFIG env var needs to be set. Example: /etc/conduit.toml",
@ -217,14 +80,7 @@ async fn main() {
}
};
let rocket = setup_rocket(raw_config, Arc::clone(&db))
.ignite()
.await
.unwrap();
Database::start_on_shutdown_tasks(db, rocket.shutdown()).await;
rocket.launch().await.unwrap();
run_server(&config, db).await.unwrap();
};
if config.allow_jaeger {
@ -264,55 +120,338 @@ async fn main() {
}
}
#[catch(404)]
fn not_found_catcher(_: &Request<'_>) -> String {
"404 Not Found".to_owned()
async fn run_server(config: &Config, db: Arc<RwLock<Database>>) -> io::Result<()> {
let addr = SocketAddr::from((config.address, config.port));
let x_requested_with = HeaderName::from_static("x-requested-with");
let middlewares = ServiceBuilder::new()
.sensitive_headers([header::AUTHORIZATION])
.layer(
TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| {
let path = if let Some(path) = request.extensions().get::<MatchedPath>() {
path.as_str()
} else {
request.uri().path()
};
tracing::info_span!("http_request", %path)
}),
)
.compression()
.layer(
CorsLayer::new()
.allow_origin(cors::Any)
.allow_methods([
Method::GET,
Method::POST,
Method::PUT,
Method::DELETE,
Method::OPTIONS,
])
.allow_headers([
header::ORIGIN,
x_requested_with,
header::CONTENT_TYPE,
header::ACCEPT,
header::AUTHORIZATION,
])
.max_age(Duration::from_secs(86400)),
)
.add_extension(db.clone());
let app = routes().layer(middlewares).into_make_service();
let handle = ServerHandle::new();
tokio::spawn(shutdown_signal(handle.clone()));
match &config.tls {
Some(tls) => {
let conf = RustlsConfig::from_pem_file(&tls.certs, &tls.key).await?;
bind_rustls(addr, conf).handle(handle).serve(app).await?;
}
None => {
bind(addr).handle(handle).serve(app).await?;
}
}
// After serve exits and before exiting, shutdown the DB
Database::on_shutdown(db).await;
Ok(())
}
#[catch(580)]
fn forbidden_catcher() -> Result<()> {
Err(Error::BadRequest(ErrorKind::Forbidden, "Forbidden."))
fn routes() -> Router {
Router::new()
.ruma_route(client_server::get_supported_versions_route)
.ruma_route(client_server::get_register_available_route)
.ruma_route(client_server::register_route)
.ruma_route(client_server::get_login_types_route)
.ruma_route(client_server::login_route)
.ruma_route(client_server::whoami_route)
.ruma_route(client_server::logout_route)
.ruma_route(client_server::logout_all_route)
.ruma_route(client_server::change_password_route)
.ruma_route(client_server::deactivate_route)
.ruma_route(client_server::third_party_route)
.ruma_route(client_server::get_capabilities_route)
.ruma_route(client_server::get_pushrules_all_route)
.ruma_route(client_server::set_pushrule_route)
.ruma_route(client_server::get_pushrule_route)
.ruma_route(client_server::set_pushrule_enabled_route)
.ruma_route(client_server::get_pushrule_enabled_route)
.ruma_route(client_server::get_pushrule_actions_route)
.ruma_route(client_server::set_pushrule_actions_route)
.ruma_route(client_server::delete_pushrule_route)
.ruma_route(client_server::get_room_event_route)
.ruma_route(client_server::get_room_aliases_route)
.ruma_route(client_server::get_filter_route)
.ruma_route(client_server::create_filter_route)
.ruma_route(client_server::set_global_account_data_route)
.ruma_route(client_server::set_room_account_data_route)
.ruma_route(client_server::get_global_account_data_route)
.ruma_route(client_server::get_room_account_data_route)
.ruma_route(client_server::set_displayname_route)
.ruma_route(client_server::get_displayname_route)
.ruma_route(client_server::set_avatar_url_route)
.ruma_route(client_server::get_avatar_url_route)
.ruma_route(client_server::get_profile_route)
.ruma_route(client_server::set_presence_route)
.ruma_route(client_server::get_presence_route)
.ruma_route(client_server::upload_keys_route)
.ruma_route(client_server::get_keys_route)
.ruma_route(client_server::claim_keys_route)
.ruma_route(client_server::create_backup_version_route)
.ruma_route(client_server::update_backup_version_route)
.ruma_route(client_server::delete_backup_version_route)
.ruma_route(client_server::get_latest_backup_info_route)
.ruma_route(client_server::get_backup_info_route)
.ruma_route(client_server::add_backup_keys_route)
.ruma_route(client_server::add_backup_keys_for_room_route)
.ruma_route(client_server::add_backup_keys_for_session_route)
.ruma_route(client_server::delete_backup_keys_for_room_route)
.ruma_route(client_server::delete_backup_keys_for_session_route)
.ruma_route(client_server::delete_backup_keys_route)
.ruma_route(client_server::get_backup_keys_for_room_route)
.ruma_route(client_server::get_backup_keys_for_session_route)
.ruma_route(client_server::get_backup_keys_route)
.ruma_route(client_server::set_read_marker_route)
.ruma_route(client_server::create_receipt_route)
.ruma_route(client_server::create_typing_event_route)
.ruma_route(client_server::create_room_route)
.ruma_route(client_server::redact_event_route)
.ruma_route(client_server::report_event_route)
.ruma_route(client_server::create_alias_route)
.ruma_route(client_server::delete_alias_route)
.ruma_route(client_server::get_alias_route)
.ruma_route(client_server::join_room_by_id_route)
.ruma_route(client_server::join_room_by_id_or_alias_route)
.ruma_route(client_server::joined_members_route)
.ruma_route(client_server::leave_room_route)
.ruma_route(client_server::forget_room_route)
.ruma_route(client_server::joined_rooms_route)
.ruma_route(client_server::kick_user_route)
.ruma_route(client_server::ban_user_route)
.ruma_route(client_server::unban_user_route)
.ruma_route(client_server::invite_user_route)
.ruma_route(client_server::set_room_visibility_route)
.ruma_route(client_server::get_room_visibility_route)
.ruma_route(client_server::get_public_rooms_route)
.ruma_route(client_server::get_public_rooms_filtered_route)
.ruma_route(client_server::search_users_route)
.ruma_route(client_server::get_member_events_route)
.ruma_route(client_server::get_protocols_route)
.ruma_route(client_server::send_message_event_route)
.ruma_route(client_server::send_state_event_for_key_route)
.ruma_route(client_server::get_state_events_route)
.ruma_route(client_server::get_state_events_for_key_route)
// Ruma doesn't have support for multiple paths for a single endpoint yet, and these routes
// share one Ruma request / response type pair with {get,send}_state_event_for_key_route
.route(
"/_matrix/client/r0/rooms/:room_id/state/:event_type",
get(client_server::get_state_events_for_empty_key_route)
.put(client_server::send_state_event_for_empty_key_route),
)
.route(
"/_matrix/client/v3/rooms/:room_id/state/:event_type",
get(client_server::get_state_events_for_empty_key_route)
.put(client_server::send_state_event_for_empty_key_route),
)
// These two endpoints allow trailing slashes
.route(
"/_matrix/client/r0/rooms/:room_id/state/:event_type/",
get(client_server::get_state_events_for_empty_key_route)
.put(client_server::send_state_event_for_empty_key_route),
)
.route(
"/_matrix/client/v3/rooms/:room_id/state/:event_type/",
get(client_server::get_state_events_for_empty_key_route)
.put(client_server::send_state_event_for_empty_key_route),
)
.ruma_route(client_server::sync_events_route)
.ruma_route(client_server::get_context_route)
.ruma_route(client_server::get_message_events_route)
.ruma_route(client_server::search_events_route)
.ruma_route(client_server::turn_server_route)
.ruma_route(client_server::send_event_to_device_route)
.ruma_route(client_server::get_media_config_route)
.ruma_route(client_server::create_content_route)
.ruma_route(client_server::get_content_route)
.ruma_route(client_server::get_content_as_filename_route)
.ruma_route(client_server::get_content_thumbnail_route)
.ruma_route(client_server::get_devices_route)
.ruma_route(client_server::get_device_route)
.ruma_route(client_server::update_device_route)
.ruma_route(client_server::delete_device_route)
.ruma_route(client_server::delete_devices_route)
.ruma_route(client_server::get_tags_route)
.ruma_route(client_server::update_tag_route)
.ruma_route(client_server::delete_tag_route)
.ruma_route(client_server::upload_signing_keys_route)
.ruma_route(client_server::upload_signatures_route)
.ruma_route(client_server::get_key_changes_route)
.ruma_route(client_server::get_pushers_route)
.ruma_route(client_server::set_pushers_route)
// .ruma_route(client_server::third_party_route)
.ruma_route(client_server::upgrade_room_route)
.ruma_route(server_server::get_server_version_route)
.route(
"/_matrix/key/v2/server",
get(server_server::get_server_keys_route),
)
.route(
"/_matrix/key/v2/server/:key_id",
get(server_server::get_server_keys_deprecated_route),
)
.ruma_route(server_server::get_public_rooms_route)
.ruma_route(server_server::get_public_rooms_filtered_route)
.ruma_route(server_server::send_transaction_message_route)
.ruma_route(server_server::get_event_route)
.ruma_route(server_server::get_missing_events_route)
.ruma_route(server_server::get_event_authorization_route)
.ruma_route(server_server::get_room_state_route)
.ruma_route(server_server::get_room_state_ids_route)
.ruma_route(server_server::create_join_event_template_route)
.ruma_route(server_server::create_join_event_v1_route)
.ruma_route(server_server::create_join_event_v2_route)
.ruma_route(server_server::create_invite_route)
.ruma_route(server_server::get_devices_route)
.ruma_route(server_server::get_room_information_route)
.ruma_route(server_server::get_profile_information_route)
.ruma_route(server_server::get_keys_route)
.ruma_route(server_server::claim_keys_route)
.fallback(not_found.into_service())
}
#[catch(581)]
fn unknown_token_catcher() -> Result<()> {
Err(Error::BadRequest(
ErrorKind::UnknownToken { soft_logout: false },
"Unknown token.",
))
async fn shutdown_signal(handle: ServerHandle) {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
let sig: &str;
tokio::select! {
_ = ctrl_c => { sig = "Ctrl+C"; },
_ = terminate => { sig = "SIGTERM"; },
}
warn!("Received {}, shutting down...", sig);
handle.graceful_shutdown(Some(Duration::from_secs(30)));
}
#[catch(582)]
fn missing_token_catcher() -> Result<()> {
Err(Error::BadRequest(ErrorKind::MissingToken, "Missing token."))
async fn not_found(_uri: Uri) -> impl IntoResponse {
Error::BadRequest(ErrorKind::NotFound, "Unknown or unimplemented route")
}
#[catch(583)]
fn bad_json_catcher() -> Result<()> {
Err(Error::BadRequest(ErrorKind::BadJson, "Bad json."))
trait RouterExt {
fn ruma_route<H, T>(self, handler: H) -> Self
where
H: RumaHandler<T>,
T: 'static;
}
fn default_config() -> rocket::Config {
use rocket::config::{LogLevel, Shutdown, Sig};
impl RouterExt for Router {
fn ruma_route<H, T>(self, handler: H) -> Self
where
H: RumaHandler<T>,
T: 'static,
{
handler.add_to_router(self)
}
}
pub trait RumaHandler<T> {
// Can't transform to a handler without boxing or relying on the nightly-only
// impl-trait-in-traits feature. Moving a small amount of extra logic into the trait
// allows bypassing both.
fn add_to_router(self, router: Router) -> Router;
}
rocket::Config {
// Disable rocket's logging to get only tracing-subscriber's log output
log_level: LogLevel::Off,
shutdown: Shutdown {
// Once shutdown is triggered, this is the amount of seconds before rocket
// will forcefully start shutting down connections, this gives enough time to /sync
// requests and the like (which havent gotten the memo, somehow) to still complete gracefully.
grace: 35,
macro_rules! impl_ruma_handler {
( $($ty:ident),* $(,)? ) => {
#[axum::async_trait]
#[allow(non_snake_case)]
impl<Req, E, F, Fut, $($ty,)*> RumaHandler<($($ty,)* Ruma<Req>,)> for F
where
Req: IncomingRequest + Send + 'static,
F: FnOnce($($ty,)* Ruma<Req>) -> Fut + Clone + Send + 'static,
Fut: Future<Output = Result<Req::OutgoingResponse, E>>
+ Send,
E: IntoResponse,
$( $ty: FromRequest<axum::body::Body> + Send + 'static, )*
{
fn add_to_router(self, mut router: Router) -> Router {
let meta = Req::METADATA;
let method_filter = method_to_filter(meta.method);
// After the grace period, rocket starts shutting down connections, and waits at least this
// many seconds before forcefully shutting all of them down.
mercy: 10,
for path in IntoIterator::into_iter([meta.unstable_path, meta.r0_path, meta.stable_path]).flatten() {
let handler = self.clone();
#[cfg(unix)]
signals: hashset![Sig::Term, Sig::Int],
router = router.route(path, on(method_filter, |$( $ty: $ty, )* req| async move {
handler($($ty,)* req).await.map(RumaResponse)
}))
}
..Shutdown::default()
},
..rocket::Config::release_default()
}
router
}
}
};
}
impl_ruma_handler!();
impl_ruma_handler!(T1);
impl_ruma_handler!(T1, T2);
impl_ruma_handler!(T1, T2, T3);
impl_ruma_handler!(T1, T2, T3, T4);
impl_ruma_handler!(T1, T2, T3, T4, T5);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6, T7);
impl_ruma_handler!(T1, T2, T3, T4, T5, T6, T7, T8);
fn method_to_filter(method: Method) -> MethodFilter {
let method_filter = match method {
Method::DELETE => MethodFilter::DELETE,
Method::GET => MethodFilter::GET,
Method::HEAD => MethodFilter::HEAD,
Method::OPTIONS => MethodFilter::OPTIONS,
Method::PATCH => MethodFilter::PATCH,
Method::POST => MethodFilter::POST,
Method::PUT => MethodFilter::PUT,
Method::TRACE => MethodFilter::TRACE,
m => panic!("Unsupported HTTP method: {:?}", m),
};
method_filter
}

@ -1,11 +1,11 @@
use crate::Error;
use crate::{Database, Error};
use ruma::{
events::{
room::member::RoomMemberEventContent, AnyEphemeralRoomEvent, AnyRoomEvent, AnyStateEvent,
AnyStrippedStateEvent, AnySyncRoomEvent, AnySyncStateEvent, EventType, StateEvent,
AnyStrippedStateEvent, AnySyncRoomEvent, AnySyncStateEvent, RoomEventType, StateEvent,
},
serde::{CanonicalJsonObject, CanonicalJsonValue, Raw},
state_res, EventId, MilliSecondsSinceUnixEpoch, RoomId, RoomVersionId, UInt, UserId,
state_res, EventId, MilliSecondsSinceUnixEpoch, RoomId, UInt, UserId,
};
use serde::{Deserialize, Serialize};
use serde_json::{
@ -29,7 +29,7 @@ pub struct PduEvent {
pub sender: Box<UserId>,
pub origin_server_ts: UInt,
#[serde(rename = "type")]
pub kind: EventType,
pub kind: RoomEventType,
pub content: Box<RawJsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state_key: Option<String>,
@ -51,10 +51,10 @@ impl PduEvent {
self.unsigned = None;
let allowed: &[&str] = match self.kind {
EventType::RoomMember => &["membership"],
EventType::RoomCreate => &["creator"],
EventType::RoomJoinRules => &["join_rule"],
EventType::RoomPowerLevels => &[
RoomEventType::RoomMember => &["join_authorised_via_users_server", "membership"],
RoomEventType::RoomCreate => &["creator"],
RoomEventType::RoomJoinRules => &["join_rule"],
RoomEventType::RoomPowerLevels => &[
"ban",
"events",
"events_default",
@ -64,7 +64,7 @@ impl PduEvent {
"users",
"users_default",
],
EventType::RoomHistoryVisibility => &["history_visibility"],
RoomEventType::RoomHistoryVisibility => &["history_visibility"],
_ => &[],
};
@ -279,7 +279,7 @@ impl state_res::Event for PduEvent {
&self.sender
}
fn event_type(&self) -> &EventType {
fn event_type(&self) -> &RoomEventType {
&self.kind
}
@ -332,16 +332,24 @@ impl Ord for PduEvent {
/// Returns a tuple of the new `EventId` and the PDU as a `BTreeMap<String, CanonicalJsonValue>`.
pub(crate) fn gen_event_id_canonical_json(
pdu: &RawJsonValue,
db: &Database,
) -> crate::Result<(Box<EventId>, CanonicalJsonObject)> {
let value = serde_json::from_str(pdu.get()).map_err(|e| {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
warn!("Error parsing incoming event {:?}: {:?}", pdu, e);
Error::BadServerResponse("Invalid PDU in server response")
})?;
let room_id = value
.get("room_id")
.and_then(|id| RoomId::parse(id.as_str()?).ok())
.ok_or_else(|| Error::bad_database("PDU in db has invalid room_id."))?;
let room_version_id = db.rooms.get_room_version(&room_id);
let event_id = format!(
"${}",
// Anything higher than version3 behaves the same
ruma::signatures::reference_hash(&value, &RoomVersionId::V6)
ruma::signatures::reference_hash(&value, &room_version_id?)
.expect("ruma can calculate reference hashes")
)
.try_into()
@ -354,7 +362,7 @@ pub(crate) fn gen_event_id_canonical_json(
#[derive(Debug, Deserialize)]
pub struct PduBuilder {
#[serde(rename = "type")]
pub event_type: EventType,
pub event_type: RoomEventType,
pub content: Box<RawJsonValue>,
pub unsigned: Option<BTreeMap<String, serde_json::Value>>,
pub state_key: Option<String>,

@ -1,33 +1,15 @@
use crate::{database::DatabaseGuard, Error};
use crate::Error;
use ruma::{
api::{client::r0::uiaa::UiaaResponse, OutgoingResponse},
identifiers::{DeviceId, UserId},
signatures::CanonicalJsonValue,
Outgoing, ServerName,
api::client::uiaa::UiaaResponse, signatures::CanonicalJsonValue, DeviceId, ServerName, UserId,
};
use std::ops::Deref;
#[cfg(feature = "conduit_bin")]
use {
crate::server_server,
rocket::{
data::{self, ByteUnit, Data, FromData},
http::Status,
outcome::Outcome::*,
response::{self, Responder},
tokio::io::AsyncReadExt,
Request,
},
ruma::api::{AuthScheme, IncomingRequest},
std::collections::BTreeMap,
std::io::Cursor,
tracing::{debug, warn},
};
mod axum;
/// This struct converts rocket requests into ruma structs by converting them into http requests
/// first.
pub struct Ruma<T: Outgoing> {
pub body: T::Incoming,
/// Extractor for Ruma request structs
pub struct Ruma<T> {
pub body: T,
pub sender_user: Option<Box<UserId>>,
pub sender_device: Option<Box<DeviceId>>,
pub sender_servername: Option<Box<ServerName>>,
@ -36,343 +18,14 @@ pub struct Ruma<T: Outgoing> {
pub from_appservice: bool,
}
#[cfg(feature = "conduit_bin")]
#[rocket::async_trait]
impl<'a, T: Outgoing> FromData<'a> for Ruma<T>
where
T::Incoming: IncomingRequest,
{
type Error = ();
#[tracing::instrument(skip(request, data))]
async fn from_data(
request: &'a Request<'_>,
data: Data<'a>,
) -> data::Outcome<'a, Self, Self::Error> {
let metadata = T::Incoming::METADATA;
let db = request
.guard::<DatabaseGuard>()
.await
.expect("database was loaded");
// Get token from header or query value
let token = request
.headers()
.get_one("Authorization")
.and_then(|s| s.get(7..)) // Split off "Bearer "
.or_else(|| request.query_value("access_token").and_then(|r| r.ok()));
let limit = db.globals.max_request_size();
let mut handle = data.open(ByteUnit::Byte(limit.into()));
let mut body = Vec::new();
if handle.read_to_end(&mut body).await.is_err() {
// Client disconnected
// Missing Token
return Failure((Status::new(582), ()));
}
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
let (sender_user, sender_device, sender_servername, from_appservice) = if let Some((
_id,
registration,
)) = db
.appservice
.all()
.unwrap()
.iter()
.find(|(_id, registration)| {
registration
.get("as_token")
.and_then(|as_token| as_token.as_str())
.map_or(false, |as_token| token == Some(as_token))
}) {
match metadata.authentication {
AuthScheme::AccessToken | AuthScheme::QueryOnlyAccessToken => {
let user_id = request.query_value::<String>("user_id").map_or_else(
|| {
UserId::parse_with_server_name(
registration
.get("sender_localpart")
.unwrap()
.as_str()
.unwrap(),
db.globals.server_name(),
)
.unwrap()
},
|string| {
UserId::parse(string.expect("parsing to string always works")).unwrap()
},
);
if !db.users.exists(&user_id).unwrap() {
// Forbidden
return Failure((Status::new(580), ()));
}
// TODO: Check if appservice is allowed to be that user
(Some(user_id), None, None, true)
}
AuthScheme::ServerSignatures => (None, None, None, true),
AuthScheme::None => (None, None, None, true),
}
} else {
match metadata.authentication {
AuthScheme::AccessToken | AuthScheme::QueryOnlyAccessToken => {
if let Some(token) = token {
match db.users.find_from_token(token).unwrap() {
// Unknown Token
None => return Failure((Status::new(581), ())),
Some((user_id, device_id)) => (
Some(user_id),
Some(Box::<DeviceId>::from(device_id)),
None,
false,
),
}
} else {
// Missing Token
return Failure((Status::new(582), ()));
}
}
AuthScheme::ServerSignatures => {
// Get origin from header
let x_matrix = match request
.headers()
.get_one("Authorization")
.and_then(|s| s.get(9..)) // Split off "X-Matrix " and parse the rest
.map(|s| {
s.split_terminator(',')
.map(|field| {
let mut splits = field.splitn(2, '=');
(splits.next(), splits.next().map(|s| s.trim_matches('"')))
})
.collect::<BTreeMap<_, _>>()
}) {
Some(t) => t,
None => {
warn!("No Authorization header");
// Forbidden
return Failure((Status::new(580), ()));
}
};
let origin_str = match x_matrix.get(&Some("origin")) {
Some(Some(o)) => *o,
_ => {
warn!("Invalid X-Matrix header origin field: {:?}", x_matrix);
// Forbidden
return Failure((Status::new(580), ()));
}
};
let origin = match ServerName::parse(origin_str) {
Ok(s) => s,
_ => {
warn!(
"Invalid server name in X-Matrix header origin field: {:?}",
x_matrix
);
// Forbidden
return Failure((Status::new(580), ()));
}
};
let key = match x_matrix.get(&Some("key")) {
Some(Some(k)) => *k,
_ => {
warn!("Invalid X-Matrix header key field: {:?}", x_matrix);
// Forbidden
return Failure((Status::new(580), ()));
}
};
let sig = match x_matrix.get(&Some("sig")) {
Some(Some(s)) => *s,
_ => {
warn!("Invalid X-Matrix header sig field: {:?}", x_matrix);
// Forbidden
return Failure((Status::new(580), ()));
}
};
let mut request_map = BTreeMap::<String, CanonicalJsonValue>::new();
if let Some(json_body) = &json_body {
request_map.insert("content".to_owned(), json_body.clone());
};
request_map.insert(
"method".to_owned(),
CanonicalJsonValue::String(request.method().to_string()),
);
request_map.insert(
"uri".to_owned(),
CanonicalJsonValue::String(request.uri().to_string()),
);
request_map.insert(
"origin".to_owned(),
CanonicalJsonValue::String(origin.as_str().to_owned()),
);
request_map.insert(
"destination".to_owned(),
CanonicalJsonValue::String(db.globals.server_name().as_str().to_owned()),
);
let mut origin_signatures = BTreeMap::new();
origin_signatures
.insert(key.to_owned(), CanonicalJsonValue::String(sig.to_owned()));
let mut signatures = BTreeMap::new();
signatures.insert(
origin.as_str().to_owned(),
CanonicalJsonValue::Object(origin_signatures),
);
request_map.insert(
"signatures".to_owned(),
CanonicalJsonValue::Object(signatures),
);
let keys =
match server_server::fetch_signing_keys(&db, &origin, vec![key.to_owned()])
.await
{
Ok(b) => b,
Err(e) => {
warn!("Failed to fetch signing keys: {}", e);
// Forbidden
return Failure((Status::new(580), ()));
}
};
let mut pub_key_map = BTreeMap::new();
pub_key_map.insert(origin.as_str().to_owned(), keys);
match ruma::signatures::verify_json(&pub_key_map, &request_map) {
Ok(()) => (None, None, Some(origin), false),
Err(e) => {
warn!(
"Failed to verify json request from {}: {}\n{:?}",
origin, e, request_map
);
if request.uri().to_string().contains('@') {
warn!("Request uri contained '@' character. Make sure your reverse proxy gives Conduit the raw uri (apache: use nocanon)");
}
// Forbidden
return Failure((Status::new(580), ()));
}
}
}
AuthScheme::None => (None, None, None, false),
}
};
let mut http_request = http::Request::builder()
.uri(request.uri().to_string())
.method(&*request.method().to_string());
for header in request.headers().iter() {
http_request = http_request.header(header.name.as_str(), &*header.value);
}
if let Some(json_body) = json_body.as_mut().and_then(|val| val.as_object_mut()) {
let user_id = sender_user.clone().unwrap_or_else(|| {
UserId::parse_with_server_name("", db.globals.server_name())
.expect("we know this is valid")
});
if let Some(CanonicalJsonValue::Object(initial_request)) = json_body
.get("auth")
.and_then(|auth| auth.as_object())
.and_then(|auth| auth.get("session"))
.and_then(|session| session.as_str())
.and_then(|session| {
db.uiaa.get_uiaa_request(
&user_id,
&sender_device.clone().unwrap_or_else(|| "".into()),
session,
)
})
{
for (key, value) in initial_request {
json_body.entry(key).or_insert(value);
}
}
body = serde_json::to_vec(json_body).expect("value to bytes can't fail");
}
let http_request = http_request.body(&*body).unwrap();
debug!("{:?}", http_request);
match <T::Incoming as IncomingRequest>::try_from_http_request(http_request) {
Ok(t) => Success(Ruma {
body: t,
sender_user,
sender_device,
sender_servername,
from_appservice,
json_body,
}),
Err(e) => {
warn!("{:?}", e);
// Bad Json
Failure((Status::new(583), ()))
}
}
}
}
impl<T: Outgoing> Deref for Ruma<T> {
type Target = T::Incoming;
impl<T> Deref for Ruma<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.body
}
}
/// This struct converts ruma responses into rocket http responses.
pub type ConduitResult<T> = Result<RumaResponse<T>, Error>;
pub fn response<T: OutgoingResponse>(response: RumaResponse<T>) -> response::Result<'static> {
let http_response = response
.0
.try_into_http_response::<Vec<u8>>()
.map_err(|_| Status::InternalServerError)?;
let mut response = rocket::response::Response::build();
let status = http_response.status();
response.status(Status::new(status.as_u16()));
for header in http_response.headers() {
response.raw_header(header.0.to_string(), header.1.to_str().unwrap().to_owned());
}
let http_body = http_response.into_body();
response.sized_body(http_body.len(), Cursor::new(http_body));
response.raw_header("Access-Control-Allow-Origin", "*");
response.raw_header(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS",
);
response.raw_header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization",
);
response.raw_header("Access-Control-Max-Age", "86400");
response.ok()
}
#[derive(Clone)]
pub struct RumaResponse<T>(pub T);
@ -387,14 +40,3 @@ impl From<Error> for RumaResponse<UiaaResponse> {
t.to_response()
}
}
#[cfg(feature = "conduit_bin")]
impl<'r, 'o, T> Responder<'r, 'o> for RumaResponse<T>
where
'o: 'r,
T: OutgoingResponse,
{
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> {
response(self)
}
}

@ -0,0 +1,367 @@
use std::{collections::BTreeMap, iter::FromIterator, str};
use axum::{
async_trait,
body::{Full, HttpBody},
extract::{
rejection::TypedHeaderRejectionReason, FromRequest, Path, RequestParts, TypedHeader,
},
headers::{
authorization::{Bearer, Credentials},
Authorization,
},
response::{IntoResponse, Response},
BoxError,
};
use bytes::{BufMut, Bytes, BytesMut};
use http::StatusCode;
use ruma::{
api::{client::error::ErrorKind, AuthScheme, IncomingRequest, OutgoingResponse},
signatures::CanonicalJsonValue,
DeviceId, ServerName, UserId,
};
use serde::Deserialize;
use tracing::{debug, error, warn};
use super::{Ruma, RumaResponse};
use crate::{database::DatabaseGuard, server_server, Error, Result};
#[async_trait]
impl<T, B> FromRequest<B> for Ruma<T>
where
T: IncomingRequest,
B: HttpBody + Send,
B::Data: Send,
B::Error: Into<BoxError>,
{
type Rejection = Error;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
#[derive(Deserialize)]
struct QueryParams {
access_token: Option<String>,
user_id: Option<String>,
}
let metadata = T::METADATA;
let db = DatabaseGuard::from_request(req).await?;
let auth_header = Option::<TypedHeader<Authorization<Bearer>>>::from_request(req).await?;
let path_params = Path::<Vec<String>>::from_request(req).await?;
let query = req.uri().query().unwrap_or_default();
let query_params: QueryParams = match ruma::serde::urlencoded::from_str(query) {
Ok(params) => params,
Err(e) => {
error!(%query, "Failed to deserialize query parameters: {}", e);
return Err(Error::BadRequest(
ErrorKind::Unknown,
"Failed to read query parameters",
));
}
};
let token = match &auth_header {
Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
None => query_params.access_token.as_deref(),
};
let mut body = Bytes::from_request(req)
.await
.map_err(|_| Error::BadRequest(ErrorKind::MissingToken, "Missing token."))?;
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
let appservices = db.appservice.all().unwrap();
let appservice_registration = appservices.iter().find(|(_id, registration)| {
registration
.get("as_token")
.and_then(|as_token| as_token.as_str())
.map_or(false, |as_token| token == Some(as_token))
});
let (sender_user, sender_device, sender_servername, from_appservice) =
if let Some((_id, registration)) = appservice_registration {
match metadata.authentication {
AuthScheme::AccessToken | AuthScheme::QueryOnlyAccessToken => {
let user_id = query_params.user_id.map_or_else(
|| {
UserId::parse_with_server_name(
registration
.get("sender_localpart")
.unwrap()
.as_str()
.unwrap(),
db.globals.server_name(),
)
.unwrap()
},
|s| UserId::parse(s).unwrap(),
);
if !db.users.exists(&user_id).unwrap() {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"User does not exist.",
));
}
// TODO: Check if appservice is allowed to be that user
(Some(user_id), None, None, true)
}
AuthScheme::ServerSignatures => (None, None, None, true),
AuthScheme::None => (None, None, None, true),
}
} else {
match metadata.authentication {
AuthScheme::AccessToken | AuthScheme::QueryOnlyAccessToken => {
let token = match token {
Some(token) => token,
_ => {
return Err(Error::BadRequest(
ErrorKind::MissingToken,
"Missing access token.",
))
}
};
match db.users.find_from_token(token).unwrap() {
None => {
return Err(Error::BadRequest(
ErrorKind::UnknownToken { soft_logout: false },
"Unknown access token.",
))
}
Some((user_id, device_id)) => (
Some(user_id),
Some(Box::<DeviceId>::from(device_id)),
None,
false,
),
}
}
AuthScheme::ServerSignatures => {
let TypedHeader(Authorization(x_matrix)) =
TypedHeader::<Authorization<XMatrix>>::from_request(req)
.await
.map_err(|e| {
warn!("Missing or invalid Authorization header: {}", e);
let msg = match e.reason() {
TypedHeaderRejectionReason::Missing => {
"Missing Authorization header."
}
TypedHeaderRejectionReason::Error(_) => {
"Invalid X-Matrix signatures."
}
_ => "Unknown header-related error",
};
Error::BadRequest(ErrorKind::Forbidden, msg)
})?;
let origin_signatures = BTreeMap::from_iter([(
x_matrix.key.clone(),
CanonicalJsonValue::String(x_matrix.sig),
)]);
let signatures = BTreeMap::from_iter([(
x_matrix.origin.as_str().to_owned(),
CanonicalJsonValue::Object(origin_signatures),
)]);
let mut request_map = BTreeMap::from_iter([
(
"method".to_owned(),
CanonicalJsonValue::String(req.method().to_string()),
),
(
"uri".to_owned(),
CanonicalJsonValue::String(req.uri().to_string()),
),
(
"origin".to_owned(),
CanonicalJsonValue::String(x_matrix.origin.as_str().to_owned()),
),
(
"destination".to_owned(),
CanonicalJsonValue::String(
db.globals.server_name().as_str().to_owned(),
),
),
(
"signatures".to_owned(),
CanonicalJsonValue::Object(signatures),
),
]);
if let Some(json_body) = &json_body {
request_map.insert("content".to_owned(), json_body.clone());
};
let keys_result = server_server::fetch_signing_keys(
&db,
&x_matrix.origin,
vec![x_matrix.key.to_owned()],
)
.await;
let keys = match keys_result {
Ok(b) => b,
Err(e) => {
warn!("Failed to fetch signing keys: {}", e);
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Failed to fetch signing keys.",
));
}
};
let pub_key_map =
BTreeMap::from_iter([(x_matrix.origin.as_str().to_owned(), keys)]);
match ruma::signatures::verify_json(&pub_key_map, &request_map) {
Ok(()) => (None, None, Some(x_matrix.origin), false),
Err(e) => {
warn!(
"Failed to verify json request from {}: {}\n{:?}",
x_matrix.origin, e, request_map
);
if req.uri().to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your \
reverse proxy gives Conduit the raw uri (apache: use \
nocanon)"
);
}
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Failed to verify X-Matrix signatures.",
));
}
}
}
AuthScheme::None => (None, None, None, false),
}
};
let mut http_request = http::Request::builder().uri(req.uri()).method(req.method());
*http_request.headers_mut().unwrap() = req.headers().clone();
if let Some(CanonicalJsonValue::Object(json_body)) = &mut json_body {
let user_id = sender_user.clone().unwrap_or_else(|| {
UserId::parse_with_server_name("", db.globals.server_name())
.expect("we know this is valid")
});
let uiaa_request = json_body
.get("auth")
.and_then(|auth| auth.as_object())
.and_then(|auth| auth.get("session"))
.and_then(|session| session.as_str())
.and_then(|session| {
db.uiaa.get_uiaa_request(
&user_id,
&sender_device.clone().unwrap_or_else(|| "".into()),
session,
)
});
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
for (key, value) in initial_request {
json_body.entry(key).or_insert(value);
}
}
let mut buf = BytesMut::new().writer();
serde_json::to_writer(&mut buf, json_body).expect("value serialization can't fail");
body = buf.into_inner().freeze();
}
let http_request = http_request.body(&*body).unwrap();
debug!("{:?}", http_request);
let body = T::try_from_http_request(http_request, &path_params).map_err(|e| {
warn!("{:?}", e);
Error::BadRequest(ErrorKind::BadJson, "Failed to deserialize request.")
})?;
Ok(Ruma {
body,
sender_user,
sender_device,
sender_servername,
from_appservice,
json_body,
})
}
}
struct XMatrix {
origin: Box<ServerName>,
key: String, // KeyName?
sig: String,
}
impl Credentials for XMatrix {
const SCHEME: &'static str = "X-Matrix";
fn decode(value: &http::HeaderValue) -> Option<Self> {
debug_assert!(
value.as_bytes().starts_with(b"X-Matrix "),
"HeaderValue to decode should start with \"X-Matrix ..\", received = {:?}",
value,
);
let parameters = str::from_utf8(&value.as_bytes()["X-Matrix ".len()..])
.ok()?
.trim_start();
let mut origin = None;
let mut key = None;
let mut sig = None;
for entry in parameters.split_terminator(',') {
let (name, value) = entry.split_once('=')?;
// It's not at all clear why some fields are quoted and others not in the spec,
// let's simply accept either form for every field.
let value = value
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.unwrap_or(value);
// FIXME: Catch multiple fields of the same name
match name {
"origin" => origin = Some(value.try_into().ok()?),
"key" => key = Some(value.to_owned()),
"sig" => sig = Some(value.to_owned()),
_ => debug!(
"Unexpected field `{}` in X-Matrix Authorization header",
name
),
}
}
Some(Self {
origin: origin?,
key: key?,
sig: sig?,
})
}
fn encode(&self) -> http::HeaderValue {
todo!()
}
}
impl<T: OutgoingResponse> IntoResponse for RumaResponse<T> {
fn into_response(self) -> Response {
match self.0.try_into_http_response::<BytesMut>() {
Ok(res) => res.map(BytesMut::freeze).map(Full::new).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
}

File diff suppressed because it is too large Load Diff

@ -3,12 +3,11 @@ use cmp::Ordering;
use rand::prelude::*;
use ruma::serde::{try_from_json_map, CanonicalJsonError, CanonicalJsonObject};
use std::{
cmp,
cmp, fmt,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
#[tracing::instrument]
pub fn millis_since_unix_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
@ -39,19 +38,16 @@ pub fn generate_keypair() -> Vec<u8> {
}
/// Parses the bytes into an u64.
#[tracing::instrument(skip(bytes))]
pub fn u64_from_bytes(bytes: &[u8]) -> Result<u64, std::array::TryFromSliceError> {
let array: [u8; 8] = bytes.try_into()?;
Ok(u64::from_be_bytes(array))
}
/// Parses the bytes into a string.
#[tracing::instrument(skip(bytes))]
pub fn string_from_bytes(bytes: &[u8]) -> Result<String, std::string::FromUtf8Error> {
String::from_utf8(bytes.to_vec())
}
#[tracing::instrument(skip(length))]
pub fn random_string(length: usize) -> String {
thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
@ -61,7 +57,6 @@ pub fn random_string(length: usize) -> String {
}
/// Calculate a new hash for the given password
#[tracing::instrument(skip(password))]
pub fn calculate_hash(password: &str) -> Result<String, argon2::Error> {
let hashing_config = Config {
variant: Variant::Argon2id,
@ -72,7 +67,6 @@ pub fn calculate_hash(password: &str) -> Result<String, argon2::Error> {
argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &hashing_config)
}
#[tracing::instrument(skip(iterators, check_order))]
pub fn common_elements(
mut iterators: impl Iterator<Item = impl Iterator<Item = Vec<u8>>>,
check_order: impl Fn(&[u8], &[u8]) -> Ordering,
@ -100,7 +94,6 @@ pub fn common_elements(
/// Fallible conversion from any value that implements `Serialize` to a `CanonicalJsonObject`.
///
/// `value` must serialize to an `serde_json::Value::Object`.
#[tracing::instrument(skip(value))]
pub fn to_canonical_object<T: serde::Serialize>(
value: T,
) -> Result<CanonicalJsonObject, CanonicalJsonError> {
@ -114,7 +107,6 @@ pub fn to_canonical_object<T: serde::Serialize>(
}
}
#[tracing::instrument(skip(deserializer))]
pub fn deserialize_from_str<
'de,
D: serde::de::Deserializer<'de>,
@ -140,3 +132,40 @@ pub fn deserialize_from_str<
}
deserializer.deserialize_str(Visitor(std::marker::PhantomData))
}
// Copied from librustdoc:
// https://github.com/rust-lang/rust/blob/cbaeec14f90b59a91a6b0f17fc046c66fa811892/src/librustdoc/html/escape.rs
/// Wrapper struct which will emit the HTML-escaped version of the contained
/// string when passed to a format string.
pub struct HtmlEscape<'a>(pub &'a str);
impl<'a> fmt::Display for HtmlEscape<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
// Because the internet is always right, turns out there's not that many
// characters to escape: http://stackoverflow.com/questions/7381974
let HtmlEscape(s) = *self;
let pile_o_bits = s;
let mut last = 0;
for (i, ch) in s.char_indices() {
let s = match ch {
'>' => "&gt;",
'<' => "&lt;",
'&' => "&amp;",
'\'' => "&#39;",
'"' => "&quot;",
_ => continue,
};
fmt.write_str(&pile_o_bits[last..i])?;
fmt.write_str(s)?;
// NOTE: we only expect single byte characters here - which is fine as long as we
// only match single byte characters
last = i + 1;
}
if last < s.len() {
fmt.write_str(&pile_o_bits[last..])?;
}
Ok(())
}
}

@ -27,19 +27,18 @@ RUN chmod +x /workdir/caddy
COPY conduit-example.toml conduit.toml
ENV SERVER_NAME=localhost
ENV ROCKET_LOG=normal
ENV CONDUIT_CONFIG=/workdir/conduit.toml
RUN sed -i "s/port = 6167/port = 8008/g" conduit.toml
RUN echo "allow_federation = true" >> conduit.toml
RUN echo "allow_encryption = true" >> conduit.toml
RUN echo "allow_registration = true" >> conduit.toml
RUN echo "log = \"info,rocket=info,_=off,sled=off\"" >> conduit.toml
RUN echo "log = \"info,_=off,sled=off\"" >> conduit.toml
RUN sed -i "s/address = \"127.0.0.1\"/address = \"0.0.0.0\"/g" conduit.toml
# Enabled Caddy auto cert generation for complement provided CA.
RUN echo '{"logging":{"logs":{"default":{"level":"WARN"}}}, "apps":{"http":{"https_port":8448,"servers":{"srv0":{"listen":[":8448"],"routes":[{"match":[{"host":["your.server.name"]}],"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"127.0.0.1:8008"}]}]}]}],"terminal":true}],"tls_connection_policies": [{"match": {"sni": ["your.server.name"]}}]}}},"pki": {"certificate_authorities": {"local": {"name": "Complement CA","root": {"certificate": "/ca/ca.crt","private_key": "/ca/ca.key"},"intermediate": {"certificate": "/ca/ca.crt","private_key": "/ca/ca.key"}}}},"tls":{"automation":{"policies":[{"subjects":["your.server.name"],"issuer":{"module":"internal"},"on_demand":true},{"issuer":{"module":"internal", "ca": "local"}}]}}}}' > caddy.json
RUN echo '{"logging":{"logs":{"default":{"level":"WARN"}}}, "apps":{"http":{"https_port":8448,"servers":{"srv0":{"listen":[":8448"],"routes":[{"match":[{"host":["your.server.name"]}],"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"127.0.0.1:8008"}]}]}]}],"terminal":true}],"tls_connection_policies": [{"match": {"sni": ["your.server.name"]}}]}}},"pki": {"certificate_authorities": {"local": {"name": "Complement CA","root": {"certificate": "/ca/ca.crt","private_key": "/ca/ca.key"},"intermediate": {"certificate": "/ca/ca.crt","private_key": "/ca/ca.key"}}}},"tls":{"automation":{"policies":[{"subjects":["your.server.name"],"issuer":{"module":"internal"},"on_demand":true},{"issuer":{"module":"internal", "ca": "local"}}]}}}}' > caddy.json
EXPOSE 8008 8448
CMD ([ -z "${COMPLEMENT_CA}" ] && echo "Error: Need Complement PKI support" && true) || \

@ -445,6 +445,9 @@ Typing notifications don't leak
Uninvited users cannot join the room
Unprivileged users can set m.room.topic if it only needs level 0
User appears in user directory
User in private room doesn't appear in user directory
User joining then leaving public room appears and dissappears from directory
User in shared private room does appear in user directory until leave
User can create and send/receive messages in a room with version 1
User can create and send/receive messages in a room with version 2
User can create and send/receive messages in a room with version 3

Loading…
Cancel
Save