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>
next
gnieto 2020-07-26 22:33:20 +02:00 committed by Timo Kösters
parent c3d142ad28
commit 5a8705bd25
4 changed files with 190 additions and 92 deletions

View File

@ -51,6 +51,7 @@ use ruma::{
get_state_events_for_empty_key, get_state_events_for_key, get_state_events_for_empty_key, get_state_events_for_key,
}, },
sync::sync_events, sync::sync_events,
tag::{create_tag, delete_tag, get_tags},
thirdparty::get_protocols, thirdparty::get_protocols,
to_device::{self, send_event_to_device}, to_device::{self, send_event_to_device},
typing::create_typing_event, typing::create_typing_event,
@ -64,11 +65,10 @@ use ruma::{
canonical_alias, guest_access, history_visibility, join_rules, member, name, redaction, canonical_alias, guest_access, history_visibility, join_rules, member, name, redaction,
topic, topic,
}, },
AnyBasicEvent, AnyEphemeralRoomEvent, AnyEvent, AnySyncEphemeralRoomEvent, EventType, AnyEphemeralRoomEvent, AnyEvent, AnySyncEphemeralRoomEvent, EventType,
}, },
Raw, RoomAliasId, RoomId, RoomVersionId, UserId, Raw, RoomAliasId, RoomId, RoomVersionId, UserId,
}; };
use serde_json::json;
const GUEST_NAME_LENGTH: usize = 10; const GUEST_NAME_LENGTH: usize = 10;
const DEVICE_ID_LENGTH: usize = 10; const DEVICE_ID_LENGTH: usize = 10;
@ -205,15 +205,12 @@ pub fn register_route(
db.account_data.update( db.account_data.update(
None, None,
&user_id, &user_id,
&EventType::PushRules, EventType::PushRules,
serde_json::to_value(ruma::events::push_rules::PushRulesEvent { &ruma::events::push_rules::PushRulesEvent {
content: ruma::events::push_rules::PushRulesEventContent { content: ruma::events::push_rules::PushRulesEventContent {
global: crate::push_rules::default_pushrules(&user_id), 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, &db.globals,
)?; )?;
@ -474,23 +471,18 @@ pub fn get_pushrules_all_route(
) -> ConduitResult<get_pushrules_all::Response> { ) -> ConduitResult<get_pushrules_all::Response> {
let user_id = body.user_id.as_ref().expect("user is authenticated"); 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 .account_data
.get(None, &user_id, &EventType::PushRules)? .get::<ruma::events::push_rules::PushRulesEvent>(None, &user_id, EventType::PushRules)?
.ok_or(Error::BadRequest( .ok_or(Error::BadRequest(
ErrorKind::NotFound, ErrorKind::NotFound,
"PushRules event not found.", "PushRules event not found.",
))? ))?;
.deserialize()
.map_err(|_| Error::BadRequest(ErrorKind::NotFound, "PushRules event in db is invalid."))? Ok(get_pushrules_all::Response {
{ global: event.content.global,
Ok(get_pushrules_all::Response {
global: pushrules.content.global,
}
.into())
} else {
Err(Error::bad_database("Pushrules event has wrong content."))
} }
.into())
} }
#[put( #[put(
@ -559,17 +551,16 @@ pub fn set_global_account_data_route(
) -> ConduitResult<set_global_account_data::Response> { ) -> ConduitResult<set_global_account_data::Response> {
let user_id = body.user_id.as_ref().expect("user is authenticated"); 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( db.account_data.update(
None, None,
user_id, user_id,
&EventType::try_from(&body.event_type).expect("EventType::try_from can never fail"), EventType::Custom(event_type),
json!( &content,
{"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"),
&db.globals, &db.globals,
)?; )?;
@ -588,19 +579,19 @@ pub fn get_global_account_data_route(
) -> ConduitResult<get_global_account_data::Response> { ) -> ConduitResult<get_global_account_data::Response> {
let user_id = body.user_id.as_ref().expect("user is authenticated"); let user_id = body.user_id.as_ref().expect("user is authenticated");
let event = db let data = db
.account_data .account_data
.get( .get::<ruma::events::AnyBasicEvent>(
None, None,
user_id, 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."))?; .ok_or(Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
let data = serde_json::from_str(event.json().get()) Ok(get_global_account_data::Response {
.map_err(|_| Error::bad_database("Invalid account data event in db."))?; account_data: Raw::from(data),
}
Ok(get_global_account_data::Response { account_data: data }.into()) .into())
} }
#[put("/_matrix/client/r0/profile/<_user_id>/displayname", data = "<body>")] #[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> { ) -> ConduitResult<set_read_marker::Response> {
let user_id = body.user_id.as_ref().expect("user is authenticated"); 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( db.account_data.update(
Some(&body.room_id), Some(&body.room_id),
&user_id, &user_id,
&EventType::FullyRead, EventType::FullyRead,
serde_json::to_value(ruma::events::fully_read::FullyReadEvent { &fully_read_event,
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"),
&db.globals, &db.globals,
)?; )?;
@ -3318,6 +3307,104 @@ pub fn set_pushers_route() -> ConduitResult<get_pushers::Response> {
.into()) .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..>")] #[options("/<_segments..>")]
pub fn options_route( pub fn options_route(
_segments: rocket::http::uri::Segments<'_>, _segments: rocket::http::uri::Segments<'_>,

View File

@ -1,9 +1,11 @@
use crate::{utils, Error, Result}; use crate::{utils, Error, Result};
use ruma::{ use ruma::{
api::client::error::ErrorKind,
events::{AnyEvent as EduEvent, EventType}, events::{AnyEvent as EduEvent, EventType},
Raw, RoomId, UserId, Raw, RoomId, UserId,
}; };
use serde::de::DeserializeOwned;
use serde::Serialize;
use sled::IVec;
use std::{collections::HashMap, convert::TryFrom}; use std::{collections::HashMap, convert::TryFrom};
pub struct AccountData { pub struct AccountData {
@ -12,77 +14,55 @@ pub struct AccountData {
impl AccountData { impl AccountData {
/// Places one event in the account data of the user and removes the previous entry. /// Places one event in the account data of the user and removes the previous entry.
pub fn update( pub fn update<T: Serialize>(
&self, &self,
room_id: Option<&RoomId>, room_id: Option<&RoomId>,
user_id: &UserId, user_id: &UserId,
kind: &EventType, event_type: EventType,
json: &mut serde_json::Map<String, serde_json::Value>, event: &T,
globals: &super::globals::Globals, globals: &super::globals::Globals,
) -> Result<()> { ) -> 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 let mut prefix = room_id
.map(|r| r.to_string()) .map(|r| r.to_string())
.unwrap_or_default() .unwrap_or_default()
.as_bytes() .as_bytes()
.to_vec(); .to_vec();
prefix.push(0xff); 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); prefix.push(0xff);
// Remove old entry // Remove old entry
if let Some(old) = self if let Some(previous) = self.find_event(room_id, user_id, &event_type) {
.roomuserdataid_accountdata let (old_key, _) = previous?;
.scan_prefix(&prefix) self.roomuserdataid_accountdata.remove(old_key)?;
.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)?;
} }
let mut key = prefix; let mut key = prefix;
key.extend_from_slice(&globals.next_count()?.to_be_bytes()); key.extend_from_slice(&globals.next_count()?.to_be_bytes());
key.push(0xff); 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( self.roomuserdataid_accountdata.insert(
key, key,
&*serde_json::to_string(&json).expect("Map::to_string always works"), &*serde_json::to_string(&event).expect("Map::to_string always works"),
)?; )?;
Ok(()) Ok(())
} }
// TODO: Optimize
/// Searches the account data for a specific kind. /// Searches the account data for a specific kind.
pub fn get( pub fn get<T: DeserializeOwned>(
&self, &self,
room_id: Option<&RoomId>, room_id: Option<&RoomId>,
user_id: &UserId, user_id: &UserId,
kind: &EventType, kind: EventType,
) -> Result<Option<Raw<EduEvent>>> { ) -> Result<Option<T>> {
Ok(self.all(room_id, user_id)?.remove(kind)) 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`. /// Returns all changes to the account data that happened after `since`.
@ -134,12 +114,37 @@ impl AccountData {
Ok(userdata) Ok(userdata)
} }
/// Returns all account data. fn find_event(
pub fn all(
&self, &self,
room_id: Option<&RoomId>, room_id: Option<&RoomId>,
user_id: &UserId, user_id: &UserId,
) -> Result<HashMap<EventType, Raw<EduEvent>>> { kind: &EventType,
self.changes_since(room_id, user_id, 0) ) -> 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?))
} }
} }

View File

@ -99,6 +99,9 @@ fn setup_rocket() -> rocket::Rocket {
client_server::update_device_route, client_server::update_device_route,
client_server::delete_device_route, client_server::delete_device_route,
client_server::delete_devices_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::options_route,
client_server::upload_signing_keys_route, client_server::upload_signing_keys_route,
client_server::upload_signatures_route, client_server::upload_signatures_route,

View File

@ -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 revoked keys are rejected
3pid invite join valid signature but unreachable ID server are rejected 3pid invite join valid signature but unreachable ID server are rejected
3pid invite join with wrong but valid signature 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 Alternative server names do not cause a routing loop
Both GET and PUT work Both GET and PUT work
Can add account data Can add account data
Can add tag
Can create filter Can create filter
Can list tags for a room
Can logout all devices Can logout all devices
Can read configuration endpoint Can read configuration endpoint
Can remove tag
Can send a message directly to a device using PUT /sendToDevice Can send a message directly to a device using PUT /sendToDevice
Can upload with ASCII file name Can upload with ASCII file name
Can upload with Unicode file name Can upload with Unicode file name
@ -22,6 +26,7 @@ GET /devices
GET /events with negative 'limit' GET /events with negative 'limit'
GET /events with non-numeric 'limit' GET /events with non-numeric 'limit'
GET /events with non-numeric 'timeout' GET /events with non-numeric 'timeout'
GET /joined_rooms lists newly-created room
GET /login yields a set of flows GET /login yields a set of flows
GET /media/r0/download can fetch the value again GET /media/r0/download can fetch the value again
GET /profile/:user_id/displayname publicly accessible 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 /register yields a set of flows
GET /rooms/:room_id/state fetches entire room state GET /rooms/:room_id/state fetches entire room state
GET /rooms/:room_id/state/m.room.member/:user_id fetches my membership 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 Getting push rules doesn't corrupt the cache SYN-390
POST /createRoom makes a private room POST /createRoom makes a private room
POST /createRoom makes a private room with invites POST /createRoom makes a private room with invites