Merge remote-tracking branch 'origin/master' into federation

merge-requests/22/head
Timo Kösters 4 years ago
commit e6b1f54857
No known key found for this signature in database
GPG Key ID: 24DA7517711A2BA4

28
Cargo.lock generated

@ -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",

@ -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

@ -25,7 +25,14 @@ Clone the repo, build it with `cargo build --release` and call the binary
##### 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?

@ -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

@ -28,10 +28,10 @@ ARG GIT_REF=HEAD
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 @@ which also will tag the resulting image as `conduit_homeserver:latest`.
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 @@ 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` 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

@ -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

@ -3,13 +3,15 @@ use crate::{ConduitResult, Database, Error, Ruma};
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())
}

@ -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())
}

@ -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())
}

@ -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(

@ -3,15 +3,15 @@ use crate::{pdu::PduBuilder, ConduitResult, Database, Error, Ruma};
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())
}

@ -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"),
)

@ -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())
}

@ -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())

@ -3,6 +3,7 @@ pub mod globals;
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 @@ impl Database {
},
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 @@ impl Database {
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 @@ impl Database {
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 @@ impl Database {
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),
);

@ -37,6 +37,28 @@ impl KeyBackups {
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 @@ impl KeyBackups {
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 @@ impl KeyBackups {
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(())
}
}

@ -12,7 +12,6 @@ use ruma::{
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 @@ impl Rooms {
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 @@ impl Rooms {
.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 @@ impl Rooms {
}
_ => {}
}
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 @@ impl Rooms {
}
/// 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 @@ impl Rooms {
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 @@ impl Rooms {
})
}
/// 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 @@ impl Rooms {
})
}
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);

@ -14,17 +14,18 @@ use std::{
};
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 @@ impl RoomEdus {
// Remove old entry
if let Some(old) = self
.roomlatestid_roomlatest
.readreceiptid_readreceipt
.scan_prefix(&prefix)
.keys()
.rev()
@ -50,7 +51,7 @@ impl RoomEdus {
})
{
// 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 @@ impl RoomEdus {
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 @@ impl RoomEdus {
}
/// 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 @@ impl RoomEdus {
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 @@ impl RoomEdus {
}))
}
/// 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 @@ impl RoomEdus {
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 @@ impl RoomEdus {
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 @@ impl RoomEdus {
}
/// 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 @@ impl RoomEdus {
// 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 @@ impl RoomEdus {
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 @@ impl RoomEdus {
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 @@ impl RoomEdus {
.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 @@ impl RoomEdus {
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 @@ impl RoomEdus {
})
}
/// 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

@ -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)?)
}
}

@ -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,

@ -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 @@ impl PduEvent {
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();

@ -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

Loading…
Cancel
Save