From 5a8705bd257c399a54bed9e1109a16901cf70519 Mon Sep 17 00:00:00 2001 From: gnieto Date: Sun, 26 Jul 2020 22:33:20 +0200 Subject: [PATCH] Add room tags (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge branch 'master' into task/add-tags Add room tagging support Co-authored-by: Timo Kösters Co-authored-by: Guillem Nieto Reviewed-on: https://git.koesters.xyz/timo/conduit/pulls/140 Reviewed-by: Timo Kösters --- src/client_server.rs | 177 ++++++++++++++++++++++++++--------- src/database/account_data.rs | 95 ++++++++++--------- src/main.rs | 3 + sytest/sytest-whitelist | 7 +- 4 files changed, 190 insertions(+), 92 deletions(-) diff --git a/src/client_server.rs b/src/client_server.rs index ab7e515..c8f264c 100644 --- a/src/client_server.rs +++ b/src/client_server.rs @@ -51,6 +51,7 @@ use ruma::{ get_state_events_for_empty_key, get_state_events_for_key, }, sync::sync_events, + tag::{create_tag, delete_tag, get_tags}, thirdparty::get_protocols, to_device::{self, send_event_to_device}, typing::create_typing_event, @@ -64,11 +65,10 @@ use ruma::{ canonical_alias, guest_access, history_visibility, join_rules, member, name, redaction, topic, }, - AnyBasicEvent, AnyEphemeralRoomEvent, AnyEvent, AnySyncEphemeralRoomEvent, EventType, + AnyEphemeralRoomEvent, AnyEvent, AnySyncEphemeralRoomEvent, EventType, }, Raw, RoomAliasId, RoomId, RoomVersionId, UserId, }; -use serde_json::json; const GUEST_NAME_LENGTH: usize = 10; const DEVICE_ID_LENGTH: usize = 10; @@ -205,15 +205,12 @@ pub fn register_route( db.account_data.update( None, &user_id, - &EventType::PushRules, - serde_json::to_value(ruma::events::push_rules::PushRulesEvent { + EventType::PushRules, + &ruma::events::push_rules::PushRulesEvent { content: ruma::events::push_rules::PushRulesEventContent { global: crate::push_rules::default_pushrules(&user_id), }, - }) - .expect("data is valid, we just created it") - .as_object_mut() - .expect("data is valid, we just created it"), + }, &db.globals, )?; @@ -474,23 +471,18 @@ pub fn get_pushrules_all_route( ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); - if let AnyEvent::Basic(AnyBasicEvent::PushRules(pushrules)) = db + let event = db .account_data - .get(None, &user_id, &EventType::PushRules)? + .get::(None, &user_id, EventType::PushRules)? .ok_or(Error::BadRequest( ErrorKind::NotFound, "PushRules event not found.", - ))? - .deserialize() - .map_err(|_| Error::BadRequest(ErrorKind::NotFound, "PushRules event in db is invalid."))? - { - Ok(get_pushrules_all::Response { - global: pushrules.content.global, - } - .into()) - } else { - Err(Error::bad_database("Pushrules event has wrong content.")) + ))?; + + Ok(get_pushrules_all::Response { + global: event.content.global, } + .into()) } #[put( @@ -559,17 +551,16 @@ pub fn set_global_account_data_route( ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); + let content = serde_json::from_str::(body.data.get()) + .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?; + + let event_type = body.event_type.to_string(); + db.account_data.update( None, user_id, - &EventType::try_from(&body.event_type).expect("EventType::try_from can never fail"), - json!( - {"content": serde_json::from_str::(body.data.get()) - .map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))? - } - ) - .as_object_mut() - .expect("we just created a valid object"), + EventType::Custom(event_type), + &content, &db.globals, )?; @@ -588,19 +579,19 @@ pub fn get_global_account_data_route( ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); - let event = db + let data = db .account_data - .get( + .get::( None, user_id, - &EventType::try_from(&body.event_type).expect("EventType::try_from can never fail"), + EventType::try_from(&body.event_type).expect("EventType::try_from can never fail"), )? .ok_or(Error::BadRequest(ErrorKind::NotFound, "Data not found."))?; - let data = serde_json::from_str(event.json().get()) - .map_err(|_| Error::bad_database("Invalid account data event in db."))?; - - Ok(get_global_account_data::Response { account_data: data }.into()) + Ok(get_global_account_data::Response { + account_data: Raw::from(data), + } + .into()) } #[put("/_matrix/client/r0/profile/<_user_id>/displayname", data = "")] @@ -1088,19 +1079,17 @@ pub fn set_read_marker_route( ) -> ConduitResult { let user_id = body.user_id.as_ref().expect("user is authenticated"); + let fully_read_event = ruma::events::fully_read::FullyReadEvent { + content: ruma::events::fully_read::FullyReadEventContent { + event_id: body.fully_read.clone(), + }, + room_id: body.room_id.clone(), + }; db.account_data.update( Some(&body.room_id), &user_id, - &EventType::FullyRead, - serde_json::to_value(ruma::events::fully_read::FullyReadEvent { - content: ruma::events::fully_read::FullyReadEventContent { - event_id: body.fully_read.clone(), - }, - room_id: body.room_id.clone(), - }) - .expect("we just created a valid event") - .as_object_mut() - .expect("we just created a valid event"), + EventType::FullyRead, + &fully_read_event, &db.globals, )?; @@ -3318,6 +3307,104 @@ pub fn set_pushers_route() -> ConduitResult { .into()) } +#[put( + "/_matrix/client/r0/user/<_user_id>/rooms/<_room_id>/tags/<_tag>", + data = "" +)] +pub fn update_tag_route( + db: State<'_, Database>, + _user_id: String, + _room_id: String, + _tag: String, + body: Ruma, +) -> ConduitResult { + let user_id = body.user_id.as_ref().expect("user is authenticated"); + + let mut tags_event = db + .account_data + .get::(Some(&body.room_id), user_id, EventType::Tag)? + .unwrap_or_else(|| ruma::events::tag::TagEvent { + content: ruma::events::tag::TagEventContent { + tags: BTreeMap::new(), + }, + }); + tags_event + .content + .tags + .insert(body.tag.to_string(), body.tag_info.clone()); + + db.account_data.update( + Some(&body.room_id), + user_id, + EventType::Tag, + &tags_event, + &db.globals, + )?; + + Ok(create_tag::Response.into()) +} + +#[delete( + "/_matrix/client/r0/user/<_user_id>/rooms/<_room_id>/tags/<_tag>", + data = "" +)] +pub fn delete_tag_route( + db: State<'_, Database>, + _user_id: String, + _room_id: String, + _tag: String, + body: Ruma, +) -> ConduitResult { + let user_id = body.user_id.as_ref().expect("user is authenticated"); + + let mut tags_event = db + .account_data + .get::(Some(&body.room_id), user_id, EventType::Tag)? + .unwrap_or_else(|| ruma::events::tag::TagEvent { + content: ruma::events::tag::TagEventContent { + tags: BTreeMap::new(), + }, + }); + tags_event.content.tags.remove(&body.tag); + + db.account_data.update( + Some(&body.room_id), + user_id, + EventType::Tag, + &tags_event, + &db.globals, + )?; + + Ok(delete_tag::Response.into()) +} + +#[get( + "/_matrix/client/r0/user/<_user_id>/rooms/<_room_id>/tags", + data = "" +)] +pub fn get_tags_route( + db: State<'_, Database>, + _user_id: String, + _room_id: String, + body: Ruma, +) -> ConduitResult { + let user_id = body.user_id.as_ref().expect("user is authenticated"); + + Ok(get_tags::Response { + tags: db + .account_data + .get::(Some(&body.room_id), user_id, EventType::Tag)? + .unwrap_or_else(|| ruma::events::tag::TagEvent { + content: ruma::events::tag::TagEventContent { + tags: BTreeMap::new(), + }, + }) + .content + .tags, + } + .into()) +} + #[options("/<_segments..>")] pub fn options_route( _segments: rocket::http::uri::Segments<'_>, diff --git a/src/database/account_data.rs b/src/database/account_data.rs index 8397c12..1afbcd6 100644 --- a/src/database/account_data.rs +++ b/src/database/account_data.rs @@ -1,9 +1,11 @@ use crate::{utils, Error, Result}; use ruma::{ - api::client::error::ErrorKind, events::{AnyEvent as EduEvent, EventType}, Raw, RoomId, UserId, }; +use serde::de::DeserializeOwned; +use serde::Serialize; +use sled::IVec; use std::{collections::HashMap, convert::TryFrom}; pub struct AccountData { @@ -12,77 +14,55 @@ pub struct AccountData { impl AccountData { /// Places one event in the account data of the user and removes the previous entry. - pub fn update( + pub fn update( &self, room_id: Option<&RoomId>, user_id: &UserId, - kind: &EventType, - json: &mut serde_json::Map, + event_type: EventType, + event: &T, globals: &super::globals::Globals, ) -> Result<()> { - if json.get("content").is_none() { - return Err(Error::BadRequest( - ErrorKind::BadJson, - "Json needs to have a content field.", - )); - } - json.insert("type".to_owned(), kind.to_string().into()); - - let user_id_string = user_id.to_string(); - let kind_string = kind.to_string(); - let mut prefix = room_id .map(|r| r.to_string()) .unwrap_or_default() .as_bytes() .to_vec(); prefix.push(0xff); - prefix.extend_from_slice(&user_id_string.as_bytes()); + prefix.extend_from_slice(&user_id.to_string().as_bytes()); prefix.push(0xff); // Remove old entry - if let Some(old) = self - .roomuserdataid_accountdata - .scan_prefix(&prefix) - .keys() - .rev() - .filter_map(|r| r.ok()) - .take_while(|key| key.starts_with(&prefix)) - .find(|key| { - let user = key.split(|&b| b == 0xff).nth(1); - let k = key.rsplit(|&b| b == 0xff).next(); - - user.filter(|&user| user == user_id_string.as_bytes()) - .is_some() - && k.filter(|&k| k == kind_string.as_bytes()).is_some() - }) - { - // This is the old room_latest - self.roomuserdataid_accountdata.remove(old)?; + if let Some(previous) = self.find_event(room_id, user_id, &event_type) { + let (old_key, _) = previous?; + self.roomuserdataid_accountdata.remove(old_key)?; } let mut key = prefix; key.extend_from_slice(&globals.next_count()?.to_be_bytes()); key.push(0xff); - key.extend_from_slice(kind.to_string().as_bytes()); + key.extend_from_slice(event_type.to_string().as_bytes()); self.roomuserdataid_accountdata.insert( key, - &*serde_json::to_string(&json).expect("Map::to_string always works"), + &*serde_json::to_string(&event).expect("Map::to_string always works"), )?; Ok(()) } - // TODO: Optimize /// Searches the account data for a specific kind. - pub fn get( + pub fn get( &self, room_id: Option<&RoomId>, user_id: &UserId, - kind: &EventType, - ) -> Result>> { - Ok(self.all(room_id, user_id)?.remove(kind)) + kind: EventType, + ) -> Result> { + self.find_event(room_id, user_id, &kind) + .map(|r| { + let (_, v) = r?; + serde_json::from_slice(&v).map_err(|_| Error::BadDatabase("could not deserialize")) + }) + .transpose() } /// Returns all changes to the account data that happened after `since`. @@ -134,12 +114,37 @@ impl AccountData { Ok(userdata) } - /// Returns all account data. - pub fn all( + fn find_event( &self, room_id: Option<&RoomId>, user_id: &UserId, - ) -> Result>> { - self.changes_since(room_id, user_id, 0) + kind: &EventType, + ) -> Option> { + let mut prefix = room_id + .map(|r| r.to_string()) + .unwrap_or_default() + .as_bytes() + .to_vec(); + prefix.push(0xff); + prefix.extend_from_slice(&user_id.to_string().as_bytes()); + prefix.push(0xff); + let kind = kind.clone(); + + self.roomuserdataid_accountdata + .scan_prefix(prefix) + .rev() + .find(move |r| { + r.as_ref() + .map(|(k, _)| { + k.rsplit(|&b| b == 0xff) + .next() + .map(|current_event_type| { + current_event_type == kind.to_string().as_bytes() + }) + .unwrap_or(false) + }) + .unwrap_or(false) + }) + .map(|r| Ok(r?)) } } diff --git a/src/main.rs b/src/main.rs index ef2b7cc..a530a20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,6 +99,9 @@ fn setup_rocket() -> rocket::Rocket { client_server::update_device_route, client_server::delete_device_route, client_server::delete_devices_route, + client_server::get_tags_route, + client_server::update_tag_route, + client_server::delete_tag_route, client_server::options_route, client_server::upload_signing_keys_route, client_server::upload_signatures_route, diff --git a/sytest/sytest-whitelist b/sytest/sytest-whitelist index 0d5ff7b..a2766de 100644 --- a/sytest/sytest-whitelist +++ b/sytest/sytest-whitelist @@ -1,3 +1,4 @@ +/joined_rooms returns only joined rooms 3pid invite join valid signature but revoked keys are rejected 3pid invite join valid signature but unreachable ID server are rejected 3pid invite join with wrong but valid signature are rejected @@ -6,9 +7,12 @@ After deactivating account, can't log in with an email Alternative server names do not cause a routing loop Both GET and PUT work Can add account data +Can add tag Can create filter +Can list tags for a room Can logout all devices Can read configuration endpoint +Can remove tag Can send a message directly to a device using PUT /sendToDevice Can upload with ASCII file name Can upload with Unicode file name @@ -22,6 +26,7 @@ GET /devices GET /events with negative 'limit' GET /events with non-numeric 'limit' GET /events with non-numeric 'timeout' +GET /joined_rooms lists newly-created room GET /login yields a set of flows GET /media/r0/download can fetch the value again GET /profile/:user_id/displayname publicly accessible @@ -29,8 +34,6 @@ GET /publicRooms lists newly-created room GET /register yields a set of flows GET /rooms/:room_id/state fetches entire room state GET /rooms/:room_id/state/m.room.member/:user_id fetches my membership -GET /joined_rooms lists newly-created room -/joined_rooms returns only joined rooms Getting push rules doesn't corrupt the cache SYN-390 POST /createRoom makes a private room POST /createRoom makes a private room with invites