Add room tags (#140)
Merge branch 'master' into task/add-tags Add room tagging support Co-authored-by: Timo Kösters <timo@koesters.xyz> Co-authored-by: Guillem Nieto <gnieto.talo@gmail.com> Reviewed-on: https://git.koesters.xyz/timo/conduit/pulls/140 Reviewed-by: Timo Kösters <timo@koesters.xyz>
This commit is contained in:
		
							parent
							
								
									c3d142ad28
								
							
						
					
					
						commit
						5a8705bd25
					
				
					 4 changed files with 190 additions and 92 deletions
				
			
		|  | @ -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<get_pushrules_all::Response> { | ||||
|     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::<ruma::events::push_rules::PushRulesEvent>(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<set_global_account_data::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     let content = serde_json::from_str::<serde_json::Value>(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::<serde_json::Value>(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<get_global_account_data::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     let event = db | ||||
|     let data = db | ||||
|         .account_data | ||||
|         .get( | ||||
|         .get::<ruma::events::AnyBasicEvent>( | ||||
|             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 = "<body>")] | ||||
|  | @ -1088,19 +1079,17 @@ pub fn set_read_marker_route( | |||
| ) -> ConduitResult<set_read_marker::Response> { | ||||
|     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<get_pushers::Response> { | |||
|     .into()) | ||||
| } | ||||
| 
 | ||||
| #[put(
 | ||||
|     "/_matrix/client/r0/user/<_user_id>/rooms/<_room_id>/tags/<_tag>", | ||||
|     data = "<body>" | ||||
| )] | ||||
| pub fn update_tag_route( | ||||
|     db: State<'_, Database>, | ||||
|     _user_id: String, | ||||
|     _room_id: String, | ||||
|     _tag: String, | ||||
|     body: Ruma<create_tag::Request>, | ||||
| ) -> ConduitResult<create_tag::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     let mut tags_event = db | ||||
|         .account_data | ||||
|         .get::<ruma::events::tag::TagEvent>(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 = "<body>" | ||||
| )] | ||||
| pub fn delete_tag_route( | ||||
|     db: State<'_, Database>, | ||||
|     _user_id: String, | ||||
|     _room_id: String, | ||||
|     _tag: String, | ||||
|     body: Ruma<delete_tag::Request>, | ||||
| ) -> ConduitResult<delete_tag::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     let mut tags_event = db | ||||
|         .account_data | ||||
|         .get::<ruma::events::tag::TagEvent>(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 = "<body>" | ||||
| )] | ||||
| pub fn get_tags_route( | ||||
|     db: State<'_, Database>, | ||||
|     _user_id: String, | ||||
|     _room_id: String, | ||||
|     body: Ruma<get_tags::Request>, | ||||
| ) -> ConduitResult<get_tags::Response> { | ||||
|     let user_id = body.user_id.as_ref().expect("user is authenticated"); | ||||
| 
 | ||||
|     Ok(get_tags::Response { | ||||
|         tags: db | ||||
|             .account_data | ||||
|             .get::<ruma::events::tag::TagEvent>(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<'_>, | ||||
|  |  | |||
|  | @ -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<T: Serialize>( | ||||
|         &self, | ||||
|         room_id: Option<&RoomId>, | ||||
|         user_id: &UserId, | ||||
|         kind: &EventType, | ||||
|         json: &mut serde_json::Map<String, serde_json::Value>, | ||||
|         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<T: DeserializeOwned>( | ||||
|         &self, | ||||
|         room_id: Option<&RoomId>, | ||||
|         user_id: &UserId, | ||||
|         kind: &EventType, | ||||
|     ) -> Result<Option<Raw<EduEvent>>> { | ||||
|         Ok(self.all(room_id, user_id)?.remove(kind)) | ||||
|         kind: EventType, | ||||
|     ) -> Result<Option<T>> { | ||||
|         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<HashMap<EventType, Raw<EduEvent>>> { | ||||
|         self.changes_since(room_id, user_id, 0) | ||||
|         kind: &EventType, | ||||
|     ) -> Option<Result<(IVec, IVec)>> { | ||||
|         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?)) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue