diff --git a/Cargo.lock b/Cargo.lock index dc215c3969f2f1d4a8de1874dab0915e218f19da..646cdcc66e5901e3f649aeeb5ff8366286631529 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1634,7 +1634,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.0.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "ruma-api", "ruma-appservice-api", @@ -1650,7 +1650,7 @@ dependencies = [ [[package]] name = "ruma-api" version = "0.17.0-alpha.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "http", "percent-encoding", @@ -1665,7 +1665,7 @@ dependencies = [ [[package]] name = "ruma-api-macros" version = "0.17.0-alpha.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" version = "0.2.0-alpha.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "ruma-api", "ruma-common", @@ -1689,7 +1689,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.10.0-alpha.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "assign", "http", @@ -1708,7 +1708,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.2.0" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "js_int", "ruma-api", @@ -1722,7 +1722,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.22.0-alpha.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "js_int", "ruma-common", @@ -1737,7 +1737,7 @@ dependencies = [ [[package]] name = "ruma-events-macros" version = "0.22.0-alpha.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1748,7 +1748,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.0.3" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "js_int", "ruma-api", @@ -1763,7 +1763,7 @@ dependencies = [ [[package]] name = "ruma-identifiers" version = "0.17.4" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "rand", "ruma-identifiers-macros", @@ -1775,7 +1775,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-macros" version = "0.17.4" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "proc-macro2", "quote", @@ -1786,7 +1786,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.1.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "serde", "strum", @@ -1795,7 +1795,7 @@ dependencies = [ [[package]] name = "ruma-serde" version = "0.2.3" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "form_urlencoded", "itoa", @@ -1807,7 +1807,7 @@ dependencies = [ [[package]] name = "ruma-signatures" version = "0.6.0-dev.1" -source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#ca07bb61d88fd665464dab9707de6d47048fc225" +source = "git+https://github.com/timokoesters/ruma?branch=timo-fed-fixes#63341000fbabce9b230b6665ce65c617944408fa" dependencies = [ "base64", "ring", diff --git a/Dockerfile b/Dockerfile index fa4b16d6179d12d7f97d716fd236f5ab28df1d4d..ff84ac6236d717e548c7b616291820a9ed5dff75 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,10 +53,10 @@ LABEL org.opencontainers.image.created=${CREATED} \ org.opencontainers.image.url="https://conduit.rs/" \ org.opencontainers.image.revision=${GIT_REF} \ org.opencontainers.image.source="https://git.koesters.xyz/timo/conduit.git" \ - org.opencontainers.image.documentation.="" \ org.opencontainers.image.licenses="AGPL-3.0-only" \ + org.opencontainers.image.documentation="" \ org.opencontainers.image.ref.name="" \ - org.label-schema.docker.build="docker build . -t conduit_homeserver:latest --build-arg CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml)" \ + org.label-schema.docker.build="docker build . -t matrixconduit/matrix-conduit:latest --build-arg CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml)" \ maintainer="Weasy666" # Standard port on which Rocket launches @@ -81,11 +81,15 @@ RUN chown -cR www-data:www-data /srv/conduit # Install packages needed to run Conduit RUN apk add --no-cache \ ca-certificates \ + curl \ libgcc # Create a volume for the database, to persist its contents VOLUME ["/srv/conduit/.local/share/conduit"] +# Test if Conduit is still alive, uses the same endpoint as Element +HEALTHCHECK --start-period=2s CMD curl --fail -s http://localhost:8000/_matrix/client/versions || curl -k --fail -s https://localhost:8000/_matrix/client/versions || exit 1 + # Set user to www-data USER www-data # Set container home directory diff --git a/README.md b/README.md index ad1308928f1892b37562804c5e0447c20a946d6a..44ab0d6930cf274adff31fa81d18fb89d6a62c00 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,14 @@ ##### From source ##### Using Docker -Build the docker image and run it with docker or docker-compose. [Read more](docker/README.md) +Pull and run the docker image with + +``` bash +docker pull matrixconduit/matrix-conduit:latest +docker run -d matrixconduit/matrix-conduit:latest -p 8448:8000 -v db:/srv/conduit/.local/share/conduit +``` + +Or build and run it with docker or docker-compose. [Read more](docker/README.md) #### What is it build on? diff --git a/docker-compose.yml b/docker-compose.yml index afd36990c5a24d83c86623b9dbe07046268f1be3..f06eaca9729a9a707a3c6e487cdca44f10dd408c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,18 +3,19 @@ version: '3' services: homeserver: - ### If you already built the Conduit image with 'docker build', then you can uncomment the - ### 'image' line and comment out the 'build' option. - # image: conduit_homeserver:latest - ### If you want meaningful labels in you built Conduit image, you should run docker-compose like this: + ### 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: - VERSION: - LOCAL: "false" - GIT_REF: HEAD + # build: + # context: . + # args: + # CREATED: + # VERSION: + # LOCAL: 'false' + # GIT_REF: HEAD restart: unless-stopped ports: - 8448:8000 diff --git a/docker/README.md b/docker/README.md index 5a6ecdec8c0f06a6d3d0707818afb127d58effbb..c569c5f6b8d5aa1837ae451d86af53c6d4da8608 100644 --- a/docker/README.md +++ b/docker/README.md @@ -28,10 +28,10 @@ ### Build & Dockerfile To build the image you can use the following command ``` bash -docker build . -t conduit_homeserver:latest --build-arg CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml) +docker build . -t matrixconduit/matrix-conduit:latest --build-arg CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') --build-arg VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml) ``` -which also will tag the resulting image as `conduit_homeserver:latest`. +which also will tag the resulting image as `matrixconduit/matrix-conduit:latest`. **Note:** it ommits the two optional `build-arg`s. @@ -40,7 +40,7 @@ ### Run After building the image you can simply run it with ``` bash -docker run conduit_homeserver:latest -p 8448:8000 -v db:/srv/conduit/.local/share/conduit -e ROCKET_SERVER_NAME="localhost:8000" +docker run -d matrixconduit/matrix-conduit:latest -p 8448:8000 -v db:/srv/conduit/.local/share/conduit -e ROCKET_SERVER_NAME="localhost:8000" ``` For detached mode, you also need to use the `-d` flag. You can pass in more env vars as are shown here, for an overview of possible values, you can take a look at the `docker-compose.yml` file. @@ -49,7 +49,7 @@ ### Run ## 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` including `docker-compose.override.traefik.yml` or the normal `docker-compose.yml` for every other reverse proxy. +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) including [`docker-compose.override.traefik.yml`](docker-compose.override.traefik.yml) or the normal [`docker-compose.yml`](../docker-compose.yml) for every other reverse proxy. ### Build diff --git a/docker/docker-compose.traefik.yml b/docker/docker-compose.traefik.yml index ad1dad826611240ac8d19ec3d0f94765fd93a2f8..111eaa5af3c1ed11a8966e6da42c1a3623cdf5a8 100644 --- a/docker/docker-compose.traefik.yml +++ b/docker/docker-compose.traefik.yml @@ -3,18 +3,19 @@ version: '3' services: homeserver: - ### If you already built the Conduit image with 'docker build', then you can uncomment the - ### 'image' line and comment out the 'build' option. - # image: conduit_homeserver:latest - ### If you want meaningful labels in you built Conduit image, you should run docker-compose like this: + ### 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: - VERSION: - LOCAL: false - GIT_REF: HEAD + # build: + # context: . + # args: + # CREATED: + # VERSION: + # LOCAL: 'false' + # GIT_REF: HEAD restart: unless-stopped volumes: - db:/srv/conduit/.local/share/conduit diff --git a/src/client_server/backup.rs b/src/client_server/backup.rs index 8966c017c04463fa1d08a578dd89791eb325e401..5d9a9250323229f8d6c497406e56bc860dd7ba69 100644 --- a/src/client_server/backup.rs +++ b/src/client_server/backup.rs @@ -3,13 +3,15 @@ use ruma::api::client::{ error::ErrorKind, r0::backup::{ - add_backup_keys, create_backup, get_backup, get_backup_keys, get_latest_backup, - update_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, }, }; #[cfg(feature = "conduit_bin")] -use rocket::{get, post, put}; +use rocket::{delete, get, post, put}; #[cfg_attr( feature = "conduit_bin", @@ -95,7 +97,22 @@ pub fn get_backup_route( .into()) } -/// Add the received backup_keys to the database. +#[cfg_attr( + feature = "conduit_bin", + delete("/_matrix/client/unstable/room_keys/version/<_>", data = "<body>") +)] +pub fn delete_backup_route( + db: State<'_, Database>, + body: Ruma<delete_backup::Request>, +) -> ConduitResult<delete_backup::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + db.key_backups.delete_backup(&sender_id, &body.version)?; + + Ok(delete_backup::Response.into()) +} + +/// Add the received backup keys to the database. #[cfg_attr( feature = "conduit_bin", put("/_matrix/client/unstable/room_keys/keys", data = "<body>") @@ -126,6 +143,62 @@ pub fn add_backup_keys_route( .into()) } +/// Add the received backup keys to the database. +#[cfg_attr( + feature = "conduit_bin", + put("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>") +)] +pub fn add_backup_key_sessions_route( + db: State<'_, Database>, + body: Ruma<add_backup_key_sessions::Request>, +) -> ConduitResult<add_backup_key_sessions::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + for (session_id, key_data) in &body.sessions { + db.key_backups.add_key( + &sender_id, + &body.version, + &body.room_id, + &session_id, + &key_data, + &db.globals, + )? + } + + Ok(add_backup_key_sessions::Response { + count: (db.key_backups.count_keys(sender_id, &body.version)? as u32).into(), + etag: db.key_backups.get_etag(sender_id, &body.version)?, + } + .into()) +} + +/// Add the received backup key to the database. +#[cfg_attr( + feature = "conduit_bin", + put("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>") +)] +pub fn add_backup_key_session_route( + db: State<'_, Database>, + body: Ruma<add_backup_key_session::Request>, +) -> ConduitResult<add_backup_key_session::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + db.key_backups.add_key( + &sender_id, + &body.version, + &body.room_id, + &body.session_id, + &body.session_data, + &db.globals, + )?; + + Ok(add_backup_key_session::Response { + count: (db.key_backups.count_keys(sender_id, &body.version)? as u32).into(), + etag: db.key_backups.get_etag(sender_id, &body.version)?, + } + .into()) +} + #[cfg_attr( feature = "conduit_bin", get("/_matrix/client/unstable/room_keys/keys", data = "<body>") @@ -140,3 +213,96 @@ pub fn get_backup_keys_route( Ok(get_backup_keys::Response { rooms }.into()) } + +#[cfg_attr( + feature = "conduit_bin", + get("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>") +)] +pub fn get_backup_key_sessions_route( + db: State<'_, Database>, + body: Ruma<get_backup_key_sessions::Request>, +) -> ConduitResult<get_backup_key_sessions::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + let sessions = db + .key_backups + .get_room(&sender_id, &body.version, &body.room_id); + + Ok(get_backup_key_sessions::Response { sessions }.into()) +} + +#[cfg_attr( + feature = "conduit_bin", + get("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>") +)] +pub fn get_backup_key_session_route( + db: State<'_, Database>, + body: Ruma<get_backup_key_session::Request>, +) -> ConduitResult<get_backup_key_session::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + let key_data = + db.key_backups + .get_session(&sender_id, &body.version, &body.room_id, &body.session_id)?; + + Ok(get_backup_key_session::Response { key_data }.into()) +} + +#[cfg_attr( + feature = "conduit_bin", + delete("/_matrix/client/unstable/room_keys/keys", data = "<body>") +)] +pub fn delete_backup_keys_route( + db: State<'_, Database>, + body: Ruma<delete_backup_keys::Request>, +) -> ConduitResult<delete_backup_keys::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + db.key_backups.delete_all_keys(&sender_id, &body.version)?; + + Ok(delete_backup_keys::Response { + count: (db.key_backups.count_keys(sender_id, &body.version)? as u32).into(), + etag: db.key_backups.get_etag(sender_id, &body.version)?, + } + .into()) +} + +#[cfg_attr( + feature = "conduit_bin", + delete("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>") +)] +pub fn delete_backup_key_sessions_route( + db: State<'_, Database>, + body: Ruma<delete_backup_key_sessions::Request>, +) -> ConduitResult<delete_backup_key_sessions::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + db.key_backups + .delete_room_keys(&sender_id, &body.version, &body.room_id)?; + + Ok(delete_backup_key_sessions::Response { + count: (db.key_backups.count_keys(sender_id, &body.version)? as u32).into(), + etag: db.key_backups.get_etag(sender_id, &body.version)?, + } + .into()) +} + +#[cfg_attr( + feature = "conduit_bin", + delete("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>") +)] +pub fn delete_backup_key_session_route( + db: State<'_, Database>, + body: Ruma<delete_backup_key_session::Request>, +) -> ConduitResult<delete_backup_key_session::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + db.key_backups + .delete_room_key(&sender_id, &body.version, &body.room_id, &body.session_id)?; + + Ok(delete_backup_key_session::Response { + count: (db.key_backups.count_keys(sender_id, &body.version)? as u32).into(), + etag: db.key_backups.get_etag(sender_id, &body.version)?, + } + .into()) +} diff --git a/src/client_server/message.rs b/src/client_server/message.rs index 025331e6998da167b050b95859b2282af5c9d621..8a09aba57ab09724909f57bef756a4afc98f9ba9 100644 --- a/src/client_server/message.rs +++ b/src/client_server/message.rs @@ -1,13 +1,14 @@ use super::State; -use crate::{pdu::PduBuilder, ConduitResult, Database, Error, Ruma}; +use crate::{pdu::PduBuilder, utils, ConduitResult, Database, Error, Ruma}; use ruma::{ api::client::{ error::ErrorKind, r0::message::{get_message_events, send_message_event}, }, events::EventContent, + EventId, }; -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; #[cfg(feature = "conduit_bin")] use rocket::{get, put}; @@ -21,6 +22,29 @@ pub fn send_message_event_route( body: Ruma<send_message_event::Request<'_>>, ) -> ConduitResult<send_message_event::Response> { let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + let device_id = body.device_id.as_ref().expect("user is authenticated"); + + // Check if this is a new transaction id + if let Some(response) = db + .transaction_ids + .existing_txnid(sender_id, device_id, &body.txn_id)? + { + // The client might have sent a txnid of the /sendToDevice endpoint + // This txnid has no response associated with it + if response.is_empty() { + return Err(Error::BadRequest( + ErrorKind::InvalidParam, + "Tried to use txn id already used for an incompatible endpoint.", + )); + } + + let event_id = EventId::try_from( + utils::string_from_bytes(&response) + .map_err(|_| Error::bad_database("Invalid txnid bytes in database."))?, + ) + .map_err(|_| Error::bad_database("Invalid event id in txnid data."))?; + return Ok(send_message_event::Response { event_id }.into()); + } let mut unsigned = serde_json::Map::new(); unsigned.insert("transaction_id".to_owned(), body.txn_id.clone().into()); @@ -45,6 +69,9 @@ pub fn send_message_event_route( &db.account_data, )?; + db.transaction_ids + .add_txnid(sender_id, device_id, &body.txn_id, event_id.as_bytes())?; + Ok(send_message_event::Response::new(event_id).into()) } diff --git a/src/client_server/profile.rs b/src/client_server/profile.rs index 2a2e05ef3b801231486ffdebd512a3fb2e9038d4..c1c0253edbe7518b7e960942ac7ffba82f997db4 100644 --- a/src/client_server/profile.rs +++ b/src/client_server/profile.rs @@ -217,11 +217,8 @@ pub fn get_profile_route( db: State<'_, Database>, body: Ruma<get_profile::Request<'_>>, ) -> ConduitResult<get_profile::Response> { - let avatar_url = db.users.avatar_url(&body.user_id)?; - let displayname = db.users.displayname(&body.user_id)?; - - if avatar_url.is_none() && displayname.is_none() { - // Return 404 if we don't have a profile for this id + if !db.users.exists(&body.user_id)? { + // Return 404 if this user doesn't exist return Err(Error::BadRequest( ErrorKind::NotFound, "Profile was not found.", @@ -229,8 +226,8 @@ pub fn get_profile_route( } Ok(get_profile::Response { - avatar_url, - displayname, + avatar_url: db.users.avatar_url(&body.user_id)?, + displayname: db.users.displayname(&body.user_id)?, } .into()) } diff --git a/src/client_server/read_marker.rs b/src/client_server/read_marker.rs index 023eece2b2050ff06a4edb782923d967c8d0945d..34d1ccc0a3fce32b889fd77a6c9a0c4d08874677 100644 --- a/src/client_server/read_marker.rs +++ b/src/client_server/read_marker.rs @@ -34,13 +34,14 @@ pub fn set_read_marker_route( )?; if let Some(event) = &body.read_receipt { - db.rooms.edus.room_read_set( + db.rooms.edus.private_read_set( &body.room_id, &sender_id, db.rooms.get_pdu_count(event)?.ok_or(Error::BadRequest( ErrorKind::InvalidParam, "Event does not exist.", ))?, + &db.globals, )?; let mut user_receipts = BTreeMap::new(); @@ -58,7 +59,7 @@ pub fn set_read_marker_route( }, ); - db.rooms.edus.roomlatest_update( + db.rooms.edus.readreceipt_update( &sender_id, &body.room_id, AnyEvent::Ephemeral(AnyEphemeralRoomEvent::Receipt( diff --git a/src/client_server/room.rs b/src/client_server/room.rs index 9a83f81b0fd2b57a5a2e24106886c2515ac28d99..a5280cf543a91d289727d51e3e64f0336261e432 100644 --- a/src/client_server/room.rs +++ b/src/client_server/room.rs @@ -3,15 +3,15 @@ use ruma::{ api::client::{ error::ErrorKind, - r0::room::{self, create_room, get_room_event}, + r0::room::{self, create_room, get_room_event, upgrade_room}, }, events::{ room::{guest_access, history_visibility, join_rules, member, name, topic}, EventType, }, - RoomAliasId, RoomId, RoomVersionId, + Raw, RoomAliasId, RoomId, RoomVersionId, }; -use std::{collections::BTreeMap, convert::TryFrom}; +use std::{cmp::max, collections::BTreeMap, convert::TryFrom}; #[cfg(feature = "conduit_bin")] use rocket::{get, post}; @@ -332,3 +332,196 @@ pub fn get_room_event_route( } .into()) } + +#[cfg_attr( + feature = "conduit_bin", + post("/_matrix/client/r0/rooms/<_room_id>/upgrade", data = "<body>") +)] +pub fn upgrade_room_route( + db: State<'_, Database>, + body: Ruma<upgrade_room::Request<'_>>, + _room_id: String, +) -> ConduitResult<upgrade_room::Response> { + let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + + // Validate the room version requested + let new_version = + RoomVersionId::try_from(body.new_version.clone()).expect("invalid room version id"); + + if !matches!( + new_version, + RoomVersionId::Version5 | RoomVersionId::Version6 + ) { + return Err(Error::BadRequest( + ErrorKind::UnsupportedRoomVersion, + "This server does not support that room version.", + )); + } + + // Create a replacement room + let replacement_room = RoomId::new(db.globals.server_name()); + + // Send a m.room.tombstone event to the old room to indicate that it is not intended to be used any further + // 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, + content: serde_json::to_value(ruma::events::room::tombstone::TombstoneEventContent { + body: "This room has been replaced".to_string(), + replacement_room: replacement_room.clone(), + }) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + sender_id, + &body.room_id, + &db.globals, + &db.account_data, + )?; + + // Get the old room federations status + let federate = serde_json::from_value::<Raw<ruma::events::room::create::CreateEventContent>>( + db.rooms + .room_state_get(&body.room_id, &EventType::RoomCreate, "")? + .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))? + .content, + ) + .expect("Raw::from_value always works") + .deserialize() + .map_err(|_| Error::bad_database("Invalid room event in database."))? + .federate; + + // Use the m.room.tombstone event as the predecessor + let predecessor = Some(ruma::events::room::create::PreviousRoom::new( + body.room_id.clone(), + tombstone_event_id, + )); + + // Send a m.room.create event containing a predecessor field and the applicable room_version + let mut create_event_content = + ruma::events::room::create::CreateEventContent::new(sender_id.clone()); + create_event_content.federate = federate; + create_event_content.room_version = new_version; + create_event_content.predecessor = predecessor; + + db.rooms.build_and_append_pdu( + PduBuilder { + event_type: EventType::RoomCreate, + content: serde_json::to_value(create_event_content) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + sender_id, + &replacement_room, + &db.globals, + &db.account_data, + )?; + + // Join the new room + db.rooms.build_and_append_pdu( + PduBuilder { + event_type: EventType::RoomMember, + content: serde_json::to_value(member::MemberEventContent { + membership: member::MembershipState::Join, + displayname: db.users.displayname(&sender_id)?, + avatar_url: db.users.avatar_url(&sender_id)?, + is_direct: None, + third_party_invite: None, + }) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some(sender_id.to_string()), + redacts: None, + }, + sender_id, + &replacement_room, + &db.globals, + &db.account_data, + )?; + + // 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, + ]; + + // Replicate transferable state events to the new room + for event_type in transferable_state_events { + let event_content = match db.rooms.room_state_get(&body.room_id, &event_type, "")? { + Some(v) => v.content.clone(), + None => continue, // Skipping missing events. + }; + + db.rooms.build_and_append_pdu( + PduBuilder { + event_type, + content: event_content, + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + sender_id, + &replacement_room, + &db.globals, + &db.account_data, + )?; + } + + // Moves any local aliases to the new room + for alias in db.rooms.room_aliases(&body.room_id).filter_map(|r| r.ok()) { + db.rooms + .set_alias(&alias, Some(&replacement_room), &db.globals)?; + } + + // Get the old room power levels + let mut power_levels_event_content = + serde_json::from_value::<Raw<ruma::events::room::power_levels::PowerLevelsEventContent>>( + db.rooms + .room_state_get(&body.room_id, &EventType::RoomPowerLevels, "")? + .ok_or_else(|| Error::bad_database("Found room without m.room.create event."))? + .content, + ) + .expect("database contains invalid PDU") + .deserialize() + .map_err(|_| Error::bad_database("Invalid room event in database."))?; + + // Setting events_default and invite to the greater of 50 and users_default + 1 + let new_level = max( + 50.into(), + power_levels_event_content.users_default + 1.into(), + ); + power_levels_event_content.events_default = new_level; + power_levels_event_content.invite = new_level; + + // Modify the power levels in the old room to prevent sending of events and inviting new users + db.rooms + .build_and_append_pdu( + PduBuilder { + event_type: EventType::RoomPowerLevels, + content: serde_json::to_value(power_levels_event_content) + .expect("event is valid, we just created it"), + unsigned: None, + state_key: Some("".to_owned()), + redacts: None, + }, + sender_id, + &body.room_id, + &db.globals, + &db.account_data, + ) + .ok(); + + // Return the replacement room id + Ok(upgrade_room::Response { replacement_room }.into()) +} diff --git a/src/client_server/sync.rs b/src/client_server/sync.rs index 167ee75ef22329ca2a10914521d85ace6a950f3c..eeeec005ed1ec1990ca8f9cfc39ebf2e1b387d08 100644 --- a/src/client_server/sync.rs +++ b/src/client_server/sync.rs @@ -81,7 +81,12 @@ pub async fn sync_events_route( .rev() .collect::<Vec<_>>(); - let send_notification_counts = !timeline_pdus.is_empty(); + let send_notification_counts = !timeline_pdus.is_empty() + || db + .rooms + .edus + .last_privateread_update(&sender_id, &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 @@ -234,7 +239,7 @@ pub async fn sync_events_route( }; let notification_count = if send_notification_counts { - if let Some(last_read) = db.rooms.edus.room_read_get(&room_id, &sender_id)? { + if let Some(last_read) = db.rooms.edus.private_read_get(&room_id, &sender_id)? { Some( (db.rooms .pdus_since(&sender_id, &room_id, last_read)? @@ -272,20 +277,15 @@ pub async fn sync_events_route( let mut edus = db .rooms .edus - .roomlatests_since(&room_id, since)? + .readreceipts_since(&room_id, since)? .filter_map(|r| r.ok()) // Filter out buggy events .collect::<Vec<_>>(); - if db - .rooms - .edus - .last_roomactive_update(&room_id, &db.globals)? - > since - { + 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.roomactives_all(&room_id)?, + db.rooms.edus.typings_all(&room_id)?, )) .expect("event is valid, we just created it"), ) diff --git a/src/client_server/to_device.rs b/src/client_server/to_device.rs index fe741011146358af80c345c39dbc13061cf7acd6..e736388d8088d1c688c29ea33f379743cd53b615 100644 --- a/src/client_server/to_device.rs +++ b/src/client_server/to_device.rs @@ -17,6 +17,16 @@ pub fn send_event_to_device_route( body: Ruma<send_event_to_device::Request<'_>>, ) -> ConduitResult<send_event_to_device::Response> { let sender_id = body.sender_id.as_ref().expect("user is authenticated"); + let device_id = body.device_id.as_ref().expect("user is authenticated"); + + // Check if this is a new transaction id + if db + .transaction_ids + .existing_txnid(sender_id, device_id, &body.txn_id)? + .is_some() + { + return Ok(send_event_to_device::Response.into()); + } for (target_user_id, map) in &body.messages { for (target_device_id_maybe, event) in map { @@ -52,5 +62,9 @@ pub fn send_event_to_device_route( } } + // Save transaction id with empty data + db.transaction_ids + .add_txnid(sender_id, device_id, &body.txn_id, &[])?; + Ok(send_event_to_device::Response.into()) } diff --git a/src/client_server/typing.rs b/src/client_server/typing.rs index b15121c02876b3d5c5a4a46f02247e8f11320e19..b019769483cf5856137ca2e4934596d2844db5fc 100644 --- a/src/client_server/typing.rs +++ b/src/client_server/typing.rs @@ -17,7 +17,7 @@ pub fn create_typing_event_route( let sender_id = body.sender_id.as_ref().expect("user is authenticated"); if let Typing::Yes(duration) = body.state { - db.rooms.edus.roomactive_add( + db.rooms.edus.typing_add( &sender_id, &body.room_id, duration.as_millis() as u64 + utils::millis_since_unix_epoch(), @@ -26,7 +26,7 @@ pub fn create_typing_event_route( } else { db.rooms .edus - .roomactive_remove(&sender_id, &body.room_id, &db.globals)?; + .typing_remove(&sender_id, &body.room_id, &db.globals)?; } Ok(create_typing_event::Response.into()) diff --git a/src/database.rs b/src/database.rs index 0d18020bc816aa0ad701cc800aca3ef51344fc73..83f30c9676cd610384ab1830b54307beb1bd8f07 100644 --- a/src/database.rs +++ b/src/database.rs @@ -3,6 +3,7 @@ pub mod key_backups; pub mod media; pub mod rooms; +pub mod transaction_ids; pub mod uiaa; pub mod users; @@ -23,6 +24,7 @@ pub struct Database { pub account_data: account_data::AccountData, pub media: media::Media, pub key_backups: key_backups::KeyBackups, + pub transaction_ids: transaction_ids::TransactionIds, pub _db: sled::Db, } @@ -88,10 +90,12 @@ pub fn load_or_create(config: &Config) -> Result<Self> { }, rooms: rooms::Rooms { edus: rooms::RoomEdus { - roomuserid_lastread: db.open_tree("roomuserid_lastread")?, // "Private" read receipt - roomlatestid_roomlatest: db.open_tree("roomlatestid_roomlatest")?, // Read receipts - roomactiveid_userid: db.open_tree("roomactiveid_userid")?, // Typing notifs - roomid_lastroomactiveupdate: db.open_tree("roomid_lastroomactiveupdate")?, + readreceiptid_readreceipt: db.open_tree("readreceiptid_readreceipt")?, + roomuserid_privateread: db.open_tree("roomuserid_privateread")?, // "Private" read receipt + roomuserid_lastprivatereadupdate: db + .open_tree("roomid_lastprivatereadupdate")?, + typingid_userid: db.open_tree("typingid_userid")?, + roomid_lasttypingupdate: db.open_tree("roomid_lasttypingupdate")?, presenceid_presence: db.open_tree("presenceid_presence")?, userid_lastpresenceupdate: db.open_tree("userid_lastpresenceupdate")?, }, @@ -107,6 +111,7 @@ pub fn load_or_create(config: &Config) -> Result<Self> { userroomid_joined: db.open_tree("userroomid_joined")?, roomuserid_joined: db.open_tree("roomuserid_joined")?, + roomuseroncejoinedids: db.open_tree("roomuseroncejoinedids")?, userroomid_invited: db.open_tree("userroomid_invited")?, roomuserid_invited: db.open_tree("roomuserid_invited")?, userroomid_left: db.open_tree("userroomid_left")?, @@ -126,6 +131,9 @@ pub fn load_or_create(config: &Config) -> Result<Self> { backupid_etag: db.open_tree("backupid_etag")?, backupkeyid_backup: db.open_tree("backupkeyid_backupmetadata")?, }, + transaction_ids: transaction_ids::TransactionIds { + userdevicetxnid_response: db.open_tree("userdevicetxnid_response")?, + }, _db: db, }) } @@ -166,14 +174,14 @@ pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) { futures.push( self.rooms .edus - .roomid_lastroomactiveupdate + .roomid_lasttypingupdate .watch_prefix(&roomid_bytes), ); futures.push( self.rooms .edus - .roomlatestid_roomlatest + .readreceiptid_readreceipt .watch_prefix(&roomid_prefix), ); diff --git a/src/database/key_backups.rs b/src/database/key_backups.rs index 5b37f1b0ea7cb42a27edf089d4244c6885f1e142..1ce75955da47688945ecff1f1da46b3c4fc39e8a 100644 --- a/src/database/key_backups.rs +++ b/src/database/key_backups.rs @@ -37,6 +37,28 @@ pub fn create_backup( Ok(version) } + pub fn delete_backup(&self, user_id: &UserId, version: &str) -> Result<()> { + let mut key = user_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&version.as_bytes()); + + self.backupid_algorithm.remove(&key)?; + self.backupid_etag.remove(&key)?; + + key.push(0xff); + + for outdated_key in self + .backupkeyid_backup + .scan_prefix(&key) + .keys() + .filter_map(|r| r.ok()) + { + self.backupkeyid_backup.remove(outdated_key)?; + } + + Ok(()) + } + pub fn update_backup( &self, user_id: &UserId, @@ -163,6 +185,7 @@ pub fn get_all(&self, user_id: &UserId, version: &str) -> Result<BTreeMap<RoomId let mut prefix = user_id.to_string().as_bytes().to_vec(); prefix.push(0xff); prefix.extend_from_slice(version.as_bytes()); + prefix.push(0xff); let mut rooms = BTreeMap::<RoomId, Sessions>::new(); @@ -204,4 +227,135 @@ pub fn get_all(&self, user_id: &UserId, version: &str) -> Result<BTreeMap<RoomId Ok(rooms) } + + pub fn get_room( + &self, + user_id: &UserId, + version: &str, + room_id: &RoomId, + ) -> BTreeMap<String, KeyData> { + let mut prefix = user_id.to_string().as_bytes().to_vec(); + prefix.push(0xff); + prefix.extend_from_slice(version.as_bytes()); + prefix.push(0xff); + prefix.extend_from_slice(room_id.as_bytes()); + prefix.push(0xff); + + self.backupkeyid_backup + .scan_prefix(&prefix) + .map(|r| { + let (key, value) = r?; + let mut parts = key.rsplit(|&b| b == 0xff); + + let session_id = + utils::string_from_bytes(&parts.next().ok_or_else(|| { + Error::bad_database("backupkeyid_backup key is invalid.") + })?) + .map_err(|_| { + Error::bad_database("backupkeyid_backup session_id is invalid.") + })?; + + let key_data = serde_json::from_slice(&value).map_err(|_| { + Error::bad_database("KeyData in backupkeyid_backup is invalid.") + })?; + + Ok::<_, Error>((session_id, key_data)) + }) + .filter_map(|r| r.ok()) + .collect() + } + + pub fn get_session( + &self, + user_id: &UserId, + version: &str, + room_id: &RoomId, + session_id: &str, + ) -> Result<Option<KeyData>> { + let mut key = user_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(version.as_bytes()); + key.push(0xff); + key.extend_from_slice(room_id.as_bytes()); + key.push(0xff); + key.extend_from_slice(session_id.as_bytes()); + + self.backupkeyid_backup + .get(&key)? + .map(|value| { + serde_json::from_slice(&value) + .map_err(|_| Error::bad_database("KeyData in backupkeyid_backup is invalid.")) + }) + .transpose() + } + + pub fn delete_all_keys(&self, user_id: &UserId, version: &str) -> Result<()> { + let mut key = user_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&version.as_bytes()); + key.push(0xff); + + for outdated_key in self + .backupkeyid_backup + .scan_prefix(&key) + .keys() + .filter_map(|r| r.ok()) + { + self.backupkeyid_backup.remove(outdated_key)?; + } + + Ok(()) + } + + pub fn delete_room_keys( + &self, + user_id: &UserId, + version: &str, + room_id: &RoomId, + ) -> Result<()> { + let mut key = user_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&version.as_bytes()); + key.push(0xff); + key.extend_from_slice(&room_id.as_bytes()); + key.push(0xff); + + for outdated_key in self + .backupkeyid_backup + .scan_prefix(&key) + .keys() + .filter_map(|r| r.ok()) + { + self.backupkeyid_backup.remove(outdated_key)?; + } + + Ok(()) + } + + pub fn delete_room_key( + &self, + user_id: &UserId, + version: &str, + room_id: &RoomId, + session_id: &str, + ) -> Result<()> { + let mut key = user_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&version.as_bytes()); + key.push(0xff); + key.extend_from_slice(&room_id.as_bytes()); + key.push(0xff); + key.extend_from_slice(&session_id.as_bytes()); + + for outdated_key in self + .backupkeyid_backup + .scan_prefix(&key) + .keys() + .filter_map(|r| r.ok()) + { + self.backupkeyid_backup.remove(outdated_key)?; + } + + Ok(()) + } } diff --git a/src/database/rooms.rs b/src/database/rooms.rs index 60333782dc47a022135c4d4d94b880a90e8deab8..87f4dcd47fdee2f04830c640aca6583ca4f4c480 100644 --- a/src/database/rooms.rs +++ b/src/database/rooms.rs @@ -12,7 +12,6 @@ room::{ member, power_levels::{self, PowerLevelsEventContent}, - redaction, }, EventType, }, @@ -47,6 +46,7 @@ pub struct Rooms { pub(super) userroomid_joined: sled::Tree, pub(super) roomuserid_joined: sled::Tree, + pub(super) roomuseroncejoinedids: sled::Tree, pub(super) userroomid_invited: sled::Tree, pub(super) roomuserid_invited: sled::Tree, pub(super) userroomid_left: sled::Tree, @@ -477,30 +477,16 @@ pub fn append_pdu( self.append_to_state(&pdu_id, &pdu)?; } - match &pdu.kind { + match pdu.kind { EventType::RoomRedaction => { if let Some(redact_id) = &pdu.redacts { - // TODO: Reason - let _reason = serde_json::from_value::<Raw<redaction::RedactionEventContent>>( - pdu.content, - ) - .expect("Raw::from_value always works.") - .deserialize() - .map_err(|_| { - Error::BadRequest( - ErrorKind::InvalidParam, - "Invalid redaction event content.", - ) - })? - .reason; - - self.redact_pdu(&redact_id)?; + self.redact_pdu(&redact_id, &pdu)?; } } EventType::RoomMember => { - if let Some(state_key) = pdu.state_key.as_ref() { + if let Some(state_key) = pdu.state_key { // if the state_key fails - let target_user_id = UserId::try_from(state_key.as_str()) + let target_user_id = UserId::try_from(state_key) .expect("This state_key was previously validated"); // Update our membership info, we do this here incase a user is invited // and immediately leaves we need the DB to record the invite event for auth @@ -527,7 +513,7 @@ pub fn append_pdu( .split_terminator(|c: char| !c.is_alphanumeric()) .map(str::to_lowercase) { - let mut key = pdu.room_id.as_bytes().to_vec(); + let mut key = pdu.room_id.to_string().as_bytes().to_vec(); key.push(0xff); key.extend_from_slice(word.as_bytes()); key.push(0xff); @@ -538,7 +524,9 @@ pub fn append_pdu( } _ => {} } - self.edus.room_read_set(&pdu.room_id, &pdu.sender, index)?; + + self.edus + .private_read_set(&pdu.room_id, &pdu.sender, index, &globals)?; Ok(pdu.event_id) } @@ -922,12 +910,12 @@ pub fn pdus_after( } /// Replace a PDU with the redacted form. - pub fn redact_pdu(&self, event_id: &EventId) -> Result<()> { + pub fn redact_pdu(&self, event_id: &EventId, reason: &PduEvent) -> Result<()> { if let Some(pdu_id) = self.get_pdu_id(event_id)? { let mut pdu = self .get_pdu_from_id(&pdu_id)? .ok_or_else(|| Error::bad_database("PDU ID points to invalid PDU."))?; - pdu.redact()?; + pdu.redact(&reason)?; self.replace_pdu(&pdu_id, &pdu)?; Ok(()) } else { @@ -959,6 +947,91 @@ fn update_membership( match &membership { member::MembershipState::Join => { + // Check if the user never joined this room + if !self.once_joined(&user_id, &room_id)? { + // Add the user ID to the join list then + self.roomuseroncejoinedids.insert(&userroom_id, &[])?; + + // Check if the room has a predecessor + if let Some(predecessor) = serde_json::from_value::< + Raw<ruma::events::room::create::CreateEventContent>, + >( + self.room_state_get(&room_id, &EventType::RoomCreate, "")? + .ok_or_else(|| { + Error::bad_database("Found room without m.room.create event.") + })? + .content, + ) + .expect("Raw::from_value always works") + .deserialize() + .map_err(|_| Error::bad_database("Invalid room event in database."))? + .predecessor + { + // Copy user settings from predecessor to the current room: + // - Push rules + // + // TODO: finish this once push rules are implemented. + // + // let mut push_rules_event_content = account_data + // .get::<ruma::events::push_rules::PushRulesEvent>( + // None, + // user_id, + // EventType::PushRules, + // )?; + // + // NOTE: find where `predecessor.room_id` match + // and update to `room_id`. + // + // account_data + // .update( + // None, + // user_id, + // EventType::PushRules, + // &push_rules_event_content, + // globals, + // ) + // .ok(); + + // Copy old tags to new room + if let Some(tag_event) = account_data.get::<ruma::events::tag::TagEvent>( + Some(&predecessor.room_id), + user_id, + EventType::Tag, + )? { + account_data + .update(Some(room_id), user_id, EventType::Tag, &tag_event, globals) + .ok(); + }; + + // Copy direct chat flag + if let Some(mut direct_event) = account_data + .get::<ruma::events::direct::DirectEvent>( + None, + user_id, + EventType::Direct, + )? { + let mut room_ids_updated = false; + + for room_ids in direct_event.content.0.values_mut() { + if room_ids.iter().any(|r| r == &predecessor.room_id) { + room_ids.push(room_id.clone()); + room_ids_updated = true; + } + } + + if room_ids_updated { + account_data.update( + None, + user_id, + EventType::Direct, + &direct_event, + globals, + )?; + } + }; + } + } + self.userroomid_joined.insert(&userroom_id, &[])?; self.roomuserid_joined.insert(&roomuser_id, &[])?; self.userroomid_invited.remove(&userroom_id)?; @@ -1218,6 +1291,27 @@ pub fn room_members(&self, room_id: &RoomId) -> impl Iterator<Item = Result<User }) } + /// Returns an iterator over all User IDs who ever joined a room. + pub fn room_useroncejoined(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> { + self.roomuseroncejoinedids + .scan_prefix(room_id.to_string()) + .keys() + .map(|key| { + Ok(UserId::try_from( + utils::string_from_bytes( + &key? + .rsplit(|&b| b == 0xff) + .next() + .expect("rsplit always returns an element"), + ) + .map_err(|_| { + Error::bad_database("User ID in room_useroncejoined is invalid unicode.") + })?, + ) + .map_err(|_| Error::bad_database("User ID in room_useroncejoined is invalid."))?) + }) + } + /// Returns an iterator over all invited members of a room. pub fn room_members_invited(&self, room_id: &RoomId) -> impl Iterator<Item = Result<UserId>> { self.roomuserid_invited @@ -1302,6 +1396,14 @@ pub fn rooms_left(&self, user_id: &UserId) -> impl Iterator<Item = Result<RoomId }) } + pub fn once_joined(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> { + let mut userroom_id = user_id.to_string().as_bytes().to_vec(); + userroom_id.push(0xff); + userroom_id.extend_from_slice(room_id.to_string().as_bytes()); + + Ok(self.roomuseroncejoinedids.get(userroom_id)?.is_some()) + } + pub fn is_joined(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> { let mut userroom_id = user_id.as_bytes().to_vec(); userroom_id.push(0xff); diff --git a/src/database/rooms/edus.rs b/src/database/rooms/edus.rs index fff30c22af094ea06adb2b078fc146911913da3e..d60e1f16899efa62bc70b70d4bd34bf4735598ef 100644 --- a/src/database/rooms/edus.rs +++ b/src/database/rooms/edus.rs @@ -14,17 +14,18 @@ }; pub struct RoomEdus { - pub(in super::super) roomuserid_lastread: sled::Tree, // RoomUserId = Room + User - pub(in super::super) roomlatestid_roomlatest: sled::Tree, // Read Receipts, RoomLatestId = RoomId + Count + UserId - pub(in super::super) roomactiveid_userid: sled::Tree, // Typing, RoomActiveId = RoomId + TimeoutTime + Count - pub(in super::super) roomid_lastroomactiveupdate: sled::Tree, // LastRoomActiveUpdate = Count + pub(in super::super) readreceiptid_readreceipt: sled::Tree, // ReadReceiptId = RoomId + Count + UserId + pub(in super::super) roomuserid_privateread: sled::Tree, // RoomUserId = Room + User, PrivateRead = Count + pub(in super::super) roomuserid_lastprivatereadupdate: sled::Tree, // LastPrivateReadUpdate = Count + pub(in super::super) typingid_userid: sled::Tree, // TypingId = RoomId + TimeoutTime + Count + pub(in super::super) roomid_lasttypingupdate: sled::Tree, // LastRoomTypingUpdate = Count pub(in super::super) presenceid_presence: sled::Tree, // PresenceId = RoomId + Count + UserId pub(in super::super) userid_lastpresenceupdate: sled::Tree, // LastPresenceUpdate = Count } impl RoomEdus { /// Adds an event which will be saved until a new event replaces it (e.g. read receipt). - pub fn roomlatest_update( + pub fn readreceipt_update( &self, user_id: &UserId, room_id: &RoomId, @@ -36,7 +37,7 @@ pub fn roomlatest_update( // Remove old entry if let Some(old) = self - .roomlatestid_roomlatest + .readreceiptid_readreceipt .scan_prefix(&prefix) .keys() .rev() @@ -50,7 +51,7 @@ pub fn roomlatest_update( }) { // This is the old room_latest - self.roomlatestid_roomlatest.remove(old)?; + self.readreceiptid_readreceipt.remove(old)?; } let mut room_latest_id = prefix; @@ -58,7 +59,7 @@ pub fn roomlatest_update( room_latest_id.push(0xff); room_latest_id.extend_from_slice(&user_id.to_string().as_bytes()); - self.roomlatestid_roomlatest.insert( + self.readreceiptid_readreceipt.insert( room_latest_id, &*serde_json::to_string(&event).expect("EduEvent::to_string always works"), )?; @@ -67,7 +68,7 @@ pub fn roomlatest_update( } /// Returns an iterator over the most recent read_receipts in a room that happened after the event with id `since`. - pub fn roomlatests_since( + pub fn readreceipts_since( &self, room_id: &RoomId, since: u64, @@ -79,7 +80,7 @@ pub fn roomlatests_since( first_possible_edu.extend_from_slice(&(since + 1).to_be_bytes()); // +1 so we don't send the event at since Ok(self - .roomlatestid_roomlatest + .readreceiptid_readreceipt .range(&*first_possible_edu..) .filter_map(|r| r.ok()) .take_while(move |(k, _)| k.starts_with(&prefix)) @@ -90,9 +91,60 @@ pub fn roomlatests_since( })) } - /// Sets a user as typing until the timeout timestamp is reached or roomactive_remove is + /// Sets a private read marker at `count`. + pub fn private_read_set( + &self, + room_id: &RoomId, + user_id: &UserId, + count: u64, + globals: &super::super::globals::Globals, + ) -> Result<()> { + let mut key = room_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&user_id.to_string().as_bytes()); + + self.roomuserid_privateread + .insert(&key, &count.to_be_bytes())?; + + self.roomuserid_lastprivatereadupdate + .insert(&key, &globals.next_count()?.to_be_bytes())?; + + Ok(()) + } + + /// Returns the private read marker. + pub fn private_read_get(&self, room_id: &RoomId, user_id: &UserId) -> Result<Option<u64>> { + let mut key = room_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&user_id.to_string().as_bytes()); + + self.roomuserid_privateread.get(key)?.map_or(Ok(None), |v| { + Ok(Some(utils::u64_from_bytes(&v).map_err(|_| { + Error::bad_database("Invalid private read marker bytes") + })?)) + }) + } + + /// Returns the count of the last typing update in this room. + pub fn last_privateread_update(&self, user_id: &UserId, room_id: &RoomId) -> Result<u64> { + let mut key = room_id.to_string().as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(&user_id.to_string().as_bytes()); + + Ok(self + .roomuserid_lastprivatereadupdate + .get(&key)? + .map_or(Ok::<_, Error>(None), |bytes| { + Ok(Some(utils::u64_from_bytes(&bytes).map_err(|_| { + Error::bad_database("Count in roomuserid_lastprivatereadupdate is invalid.") + })?)) + })? + .unwrap_or(0)) + } + + /// Sets a user as typing until the timeout timestamp is reached or roomtyping_remove is /// called. - pub fn roomactive_add( + pub fn typing_add( &self, user_id: &UserId, room_id: &RoomId, @@ -104,22 +156,22 @@ pub fn roomactive_add( let count = globals.next_count()?.to_be_bytes(); - let mut room_active_id = prefix; - room_active_id.extend_from_slice(&timeout.to_be_bytes()); - room_active_id.push(0xff); - room_active_id.extend_from_slice(&count); + let mut room_typing_id = prefix; + room_typing_id.extend_from_slice(&timeout.to_be_bytes()); + room_typing_id.push(0xff); + room_typing_id.extend_from_slice(&count); - self.roomactiveid_userid - .insert(&room_active_id, &*user_id.to_string().as_bytes())?; + self.typingid_userid + .insert(&room_typing_id, &*user_id.to_string().as_bytes())?; - self.roomid_lastroomactiveupdate + self.roomid_lasttypingupdate .insert(&room_id.to_string().as_bytes(), &count)?; Ok(()) } /// Removes a user from typing before the timeout is reached. - pub fn roomactive_remove( + pub fn typing_remove( &self, user_id: &UserId, room_id: &RoomId, @@ -132,19 +184,19 @@ pub fn roomactive_remove( let mut found_outdated = false; - // Maybe there are multiple ones from calling roomactive_add multiple times + // Maybe there are multiple ones from calling roomtyping_add multiple times for outdated_edu in self - .roomactiveid_userid + .typingid_userid .scan_prefix(&prefix) .filter_map(|r| r.ok()) .filter(|(_, v)| v == user_id.as_bytes()) { - self.roomactiveid_userid.remove(outdated_edu.0)?; + self.typingid_userid.remove(outdated_edu.0)?; found_outdated = true; } if found_outdated { - self.roomid_lastroomactiveupdate.insert( + self.roomid_lasttypingupdate.insert( &room_id.to_string().as_bytes(), &globals.next_count()?.to_be_bytes(), )?; @@ -154,7 +206,7 @@ pub fn roomactive_remove( } /// Makes sure that typing events with old timestamps get removed. - fn roomactives_maintain( + fn typings_maintain( &self, room_id: &RoomId, globals: &super::super::globals::Globals, @@ -168,7 +220,7 @@ fn roomactives_maintain( // Find all outdated edus before inserting a new one for outdated_edu in self - .roomactiveid_userid + .typingid_userid .scan_prefix(&prefix) .keys() .map(|key| { @@ -176,21 +228,21 @@ fn roomactives_maintain( Ok::<_, Error>(( key.clone(), utils::u64_from_bytes(key.split(|&b| b == 0xff).nth(1).ok_or_else(|| { - Error::bad_database("RoomActive has invalid timestamp or delimiters.") + Error::bad_database("RoomTyping has invalid timestamp or delimiters.") })?) - .map_err(|_| Error::bad_database("RoomActive has invalid timestamp bytes."))?, + .map_err(|_| Error::bad_database("RoomTyping has invalid timestamp bytes."))?, )) }) .filter_map(|r| r.ok()) .take_while(|&(_, timestamp)| timestamp < current_timestamp) { // This is an outdated edu (time > timestamp) - self.roomactiveid_userid.remove(outdated_edu.0)?; + self.typingid_userid.remove(outdated_edu.0)?; found_outdated = true; } if found_outdated { - self.roomid_lastroomactiveupdate.insert( + self.roomid_lasttypingupdate.insert( &room_id.to_string().as_bytes(), &globals.next_count()?.to_be_bytes(), )?; @@ -199,16 +251,16 @@ fn roomactives_maintain( Ok(()) } - /// Returns an iterator over all active events (e.g. typing notifications). - pub fn last_roomactive_update( + /// Returns the count of the last typing update in this room. + pub fn last_typing_update( &self, room_id: &RoomId, globals: &super::super::globals::Globals, ) -> Result<u64> { - self.roomactives_maintain(room_id, globals)?; + self.typings_maintain(room_id, globals)?; Ok(self - .roomid_lastroomactiveupdate + .roomid_lasttypingupdate .get(&room_id.to_string().as_bytes())? .map_or(Ok::<_, Error>(None), |bytes| { Ok(Some(utils::u64_from_bytes(&bytes).map_err(|_| { @@ -218,7 +270,7 @@ pub fn last_roomactive_update( .unwrap_or(0)) } - pub fn roomactives_all( + pub fn typings_all( &self, room_id: &RoomId, ) -> Result<SyncEphemeralRoomEvent<ruma::events::typing::TypingEventContent>> { @@ -228,17 +280,15 @@ pub fn roomactives_all( let mut user_ids = Vec::new(); for user_id in self - .roomactiveid_userid + .typingid_userid .scan_prefix(prefix) .values() .map(|user_id| { Ok::<_, Error>( UserId::try_from(utils::string_from_bytes(&user_id?).map_err(|_| { - Error::bad_database("User ID in roomactiveid_userid is invalid unicode.") + Error::bad_database("User ID in typingid_userid is invalid unicode.") })?) - .map_err(|_| { - Error::bad_database("User ID in roomactiveid_userid is invalid.") - })?, + .map_err(|_| Error::bad_database("User ID in typingid_userid is invalid."))?, ) }) { @@ -250,30 +300,6 @@ pub fn roomactives_all( }) } - /// Sets a private read marker at `count`. - pub fn room_read_set(&self, room_id: &RoomId, user_id: &UserId, count: u64) -> Result<()> { - let mut key = room_id.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&user_id.to_string().as_bytes()); - - self.roomuserid_lastread.insert(key, &count.to_be_bytes())?; - - Ok(()) - } - - /// Returns the private read marker. - pub fn room_read_get(&self, room_id: &RoomId, user_id: &UserId) -> Result<Option<u64>> { - let mut key = room_id.to_string().as_bytes().to_vec(); - key.push(0xff); - key.extend_from_slice(&user_id.to_string().as_bytes()); - - self.roomuserid_lastread.get(key)?.map_or(Ok(None), |v| { - Ok(Some(utils::u64_from_bytes(&v).map_err(|_| { - Error::bad_database("Invalid private read marker bytes") - })?)) - }) - } - /// Adds a presence event which will be saved until a new event replaces it. /// /// Note: This method takes a RoomId because presence updates are always bound to rooms to diff --git a/src/database/transaction_ids.rs b/src/database/transaction_ids.rs new file mode 100644 index 0000000000000000000000000000000000000000..9485b361fcb4511573721190f482093e17e9936b --- /dev/null +++ b/src/database/transaction_ids.rs @@ -0,0 +1,43 @@ +use crate::Result; +use ruma::{DeviceId, UserId}; +use sled::IVec; + +pub struct TransactionIds { + pub(super) userdevicetxnid_response: sled::Tree, // Response can be empty (/sendToDevice) or the event id (/send) +} + +impl TransactionIds { + pub fn add_txnid( + &self, + user_id: &UserId, + device_id: &DeviceId, + txn_id: &str, + data: &[u8], + ) -> Result<()> { + let mut key = user_id.as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(device_id.as_bytes()); + key.push(0xff); + key.extend_from_slice(txn_id.as_bytes()); + + self.userdevicetxnid_response.insert(key, data)?; + + Ok(()) + } + + pub fn existing_txnid( + &self, + user_id: &UserId, + device_id: &DeviceId, + txn_id: &str, + ) -> Result<Option<IVec>> { + let mut key = user_id.as_bytes().to_vec(); + key.push(0xff); + key.extend_from_slice(device_id.as_bytes()); + key.push(0xff); + key.extend_from_slice(txn_id.as_bytes()); + + // If there's no entry, this is a new transaction + Ok(self.userdevicetxnid_response.get(key)?) + } +} diff --git a/src/main.rs b/src/main.rs index bbe7c9622fda1d6bcfb00fdca46b1e5ce53f57ef..eb060e3eca8f09b961a0bfae3baa11375e15da57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,9 +53,16 @@ fn setup_rocket() -> rocket::Rocket { 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_typing_event_route, @@ -111,6 +118,7 @@ fn setup_rocket() -> rocket::Rocket { client_server::get_key_changes_route, client_server::get_pushers_route, client_server::set_pushers_route, + client_server::upgrade_room_route, server_server::well_known_server, server_server::get_server_version, server_server::get_server_keys, diff --git a/src/pdu.rs b/src/pdu.rs index f23e688c10fc955960839e5089e58f43ca3362de..7f842e2d72dd2c8cd934f462854c3374ccb3a0f7 100644 --- a/src/pdu.rs +++ b/src/pdu.rs @@ -34,7 +34,7 @@ pub struct PduEvent { } impl PduEvent { - pub fn redact(&mut self) -> Result<()> { + pub fn redact(&mut self, reason: &PduEvent) -> Result<()> { self.unsigned.clear(); let allowed: &[&str] = match self.kind { @@ -70,7 +70,9 @@ pub fn redact(&mut self) -> Result<()> { self.unsigned.insert( "redacted_because".to_owned(), - json!({"content": {}, "type": "m.room.redaction"}), + serde_json::to_string(reason) + .expect("PduEvent::to_string always works") + .into(), ); self.content = new_content.into(); diff --git a/sytest/sytest-whitelist b/sytest/sytest-whitelist index 3c1095b3a3e7c4e08730c63766ebdc7cc6b2f9f8..e1f4e5cd86494653e7608e64d441e07d25baa177 100644 --- a/sytest/sytest-whitelist +++ b/sytest/sytest-whitelist @@ -17,6 +17,7 @@ Can invite users to invite-only rooms Can list tags for a room Can logout all devices Can logout current device +Can re-join room if re-invited Can read configuration endpoint Can recv a device message using /sync Can recv device messages until they are acknowledged @@ -37,6 +38,7 @@ Current state appears in timeline in private history with many messages before Deleted tags appear in an incremental v2 /sync Deleting a non-existent alias should return a 404 Device messages wake up /sync +Device messages with the same txn_id are deduplicated Events come down the correct room GET /device/{deviceId} GET /device/{deviceId} gives a 404 for unknown devices @@ -87,6 +89,7 @@ POST /rooms/:room_id/join can join a room POST /rooms/:room_id/leave can leave a room POST /rooms/:room_id/state/m.room.name sets name POST /rooms/:room_id/state/m.room.topic sets topic +POST /rooms/:room_id/upgrade can upgrade a room version POSTed media can be thumbnailed PUT /device/{deviceId} gives a 404 for unknown devices PUT /device/{deviceId} updates device fields @@ -113,7 +116,6 @@ Typing events appear in incremental sync Typing events appear in initial sync Uninvited users cannot join the room User appears in user directory -User directory correctly update on display name change User in dir while user still shares private rooms User in shared private room does appear in user directory User is offline if they set_presence=offline in their sync