diff --git a/STATUS.md b/STATUS.md index 97025c1c..56d6cb60 100644 --- a/STATUS.md +++ b/STATUS.md @@ -120,12 +120,12 @@ Legend: GET /rooms/:room_id/members - :no_entry_sign: + :white_check_mark: #11 GET /rooms/:room_id/state/:event_type/:state_key - :no_entry_sign: + :white_check_mark: #12 GET /rooms/:room_id/state/:event_type diff --git a/src/api/r0/mod.rs b/src/api/r0/mod.rs index df2a0682..2ef9484f 100644 --- a/src/api/r0/mod.rs +++ b/src/api/r0/mod.rs @@ -6,22 +6,22 @@ pub use self::account::{ PutAccountData, PutRoomAccountData, }; -pub use self::directory::{GetRoomAlias, DeleteRoomAlias, PutRoomAlias}; +pub use self::directory::{DeleteRoomAlias, GetRoomAlias, PutRoomAlias}; pub use self::event_creation::{SendMessageEvent, StateMessageEvent}; +pub use self::filter::{GetFilter, PostFilter}; pub use self::join::{InviteToRoom, JoinRoom, JoinRoomWithIdOrAlias, KickFromRoom, LeaveRoom}; pub use self::login::Login; pub use self::logout::Logout; pub use self::members::Members; pub use self::presence::{GetPresenceList, GetPresenceStatus, PostPresenceList, PutPresenceStatus}; -pub use self::profile::{Profile, GetAvatarUrl, PutAvatarUrl, GetDisplayName, PutDisplayName}; pub use self::pushers::{GetPushers, SetPushers}; +pub use self::profile::{GetAvatarUrl, GetDisplayName, Profile, PutAvatarUrl, PutDisplayName}; pub use self::registration::Register; pub use self::room_creation::CreateRoom; -pub use self::room_info::RoomState; -pub use self::tags::{DeleteTag, GetTags, PutTag}; +pub use self::room_info::{GetStateEvent, RoomState}; pub use self::sync::Sync; +pub use self::tags::{DeleteTag, GetTags, PutTag}; pub use self::versions::Versions; -pub use self::filter::{GetFilter, PostFilter}; mod account; mod directory; @@ -37,6 +37,6 @@ mod pushers; mod registration; mod room_creation; mod room_info; -mod tags; mod sync; +mod tags; mod versions; diff --git a/src/api/r0/room_info.rs b/src/api/r0/room_info.rs index 8af44a57..eb083526 100644 --- a/src/api/r0/room_info.rs +++ b/src/api/r0/room_info.rs @@ -2,19 +2,44 @@ use std::convert::TryInto; -use iron::{Chain, Handler, IronResult, Request, Response}; use iron::status::Status; +use iron::{Chain, Handler, IronResult, Request, Response}; +use router::Router; +use ruma_events::EventType; use ruma_events::collections::all::StateEvent; +use ruma_events::room::aliases::AliasesEventContent; +use ruma_events::room::avatar::AvatarEventContent; +use ruma_events::room::canonical_alias::CanonicalAliasEventContent; +use ruma_events::room::create::CreateEventContent; +use ruma_events::room::guest_access::GuestAccessEventContent; +use ruma_events::room::history_visibility::HistoryVisibilityEventContent; +use ruma_events::room::join_rules::JoinRulesEventContent; +use ruma_events::room::member::MemberEventContent; +use ruma_events::room::name::NameEventContent; +use ruma_events::room::power_levels::PowerLevelsEventContent; +use ruma_events::room::third_party_invite::ThirdPartyInviteEventContent; +use ruma_events::room::topic::TopicEventContent; +use serde_json::from_str; use db::DB; use error::ApiError; -use middleware::{AccessTokenAuth, MiddlewareChain, RoomIdParam}; +use middleware::{AccessTokenAuth, EventTypeParam, MiddlewareChain, RoomIdParam}; use models::event::Event; use models::room::Room; use models::room_membership::RoomMembership; use models::user::User; use modifier::SerializableResponse; +/// Deserialize event's content with the given `EventType` and send it as the response. +macro_rules! send_content { + ($ty:ty, $content:ident) => { + { + let content = from_str::<$ty>($content).map_err(ApiError::from)?; + Ok(Response::with((Status::Ok, SerializableResponse(content)))) + } + } +} + /// The `/rooms/:room_id/state` endpoint. pub struct RoomState; @@ -37,39 +62,115 @@ impl Handler for RoomState { } }; - let membership = RoomMembership::find(&connection, &room.id, &user.id)?; + let membership = match RoomMembership::find(&connection, &room.id, &user.id)? { + Some(membership) => membership, + None => Err(ApiError::unauthorized("The user is not a member of the room".to_string()))? + }; - if membership.is_none() { - Err(ApiError::unauthorized("The user is not a member of the room".to_string()))? - } + let state_events: Vec = match membership.membership.as_ref() { + "join" => { + Event::get_room_full_state(&connection, &room_id)?.iter() + .cloned() + .map(|e| e.try_into()) + .collect::, ApiError>>()? + }, + "ban" | "leave" => { + let last_event = Event::find(&connection, &membership.event_id)? + .expect("A room membership should be associated with an event"); - let membership_state = membership.clone().unwrap().membership; - let mut events = Vec::::new(); + Event::get_room_state_events_until(&connection, &room_id, &last_event)?.iter() + .cloned() + .map(|e| e.try_into()) + .collect::, ApiError>>()? + }, + _ => Err(ApiError::unauthorized("The user is not a member of the room".to_string()))? + }; - match membership_state.as_ref() { + Ok(Response::with((Status::Ok, SerializableResponse(state_events)))) + } +} + +/// The `/rooms/:room_id/state/:event_type` and `/rooms/:room_id/state/:event_type/:state_key` endpoints. +pub struct GetStateEvent; + +middleware_chain!(GetStateEvent, [RoomIdParam, EventTypeParam, AccessTokenAuth]); + +impl Handler for GetStateEvent { + fn handle(&self, request: &mut Request) -> IronResult { + let params = request.extensions.get::() + .expect("Params object is missing").clone(); + + let room_id = request.extensions.get::() + .expect("RoomIdParam should ensure a RoomId").clone(); + + let event_type = request.extensions.get::() + .expect("EventTypeParam should ensure an EventType").clone(); + + let state_key = params.find("state_key").unwrap_or(""); + + let user = request.extensions.get::() + .expect("AccessTokenAuth should ensure a user").clone(); + + let connection = DB::from_request(request)?; + + let room = match Room::find(&connection, &room_id)? { + Some(room) => room, + None => { + Err(ApiError::unauthorized("The room was not found on this server".to_string()))? + } + }; + + let membership = match RoomMembership::find(&connection, &room.id, &user.id)? { + Some(membership) => membership, + None => Err(ApiError::unauthorized("The user is not a member of the room".to_string()))? + }; + + let state_event = match membership.membership.as_ref() { "join" => { - events.append( - &mut Event::get_room_full_state(&connection, &room_id)? - ); + Event::get_room_full_state(&connection, &room.id)?.iter() + .filter(|e| { + e.event_type == event_type.to_string() && + e.state_key.clone().unwrap_or("".to_string()) == state_key + }) + .next() + .cloned() }, - "leave" => { - let last_event = Event::find(&connection, &membership.unwrap().event_id)? + "ban" | "leave" => { + let last_event = Event::find(&connection, &membership.event_id)? .expect("A room membership should be associated with an event"); - events.append( - &mut Event::get_room_state_events_until(&connection, &room_id, &last_event)? - ); + Event::get_room_state_events_until(&connection, &room_id, &last_event)?.iter() + .filter(|e| { + e.event_type == event_type.to_string() && + e.state_key.clone().unwrap_or("".to_string()) == state_key + }) + .next() + .cloned() }, - _ => {} - } - - let mut state_events: Vec = Vec::new(); + _ => Err(ApiError::unauthorized("The user is not a member of the room".to_string()))? + }; - for event in events { - state_events.push(event.try_into()?); + if state_event.is_none() { + Err(ApiError::not_found("The requested state event was not found".to_string()))? } - Ok(Response::with((Status::Ok, SerializableResponse(state_events)))) + let content = &state_event.unwrap().content.clone(); + + match event_type { + EventType::RoomAliases => send_content!(AliasesEventContent, content), + EventType::RoomAvatar => send_content!(AvatarEventContent, content), + EventType::RoomCanonicalAlias => send_content!(CanonicalAliasEventContent, content), + EventType::RoomCreate => send_content!(CreateEventContent, content), + EventType::RoomGuestAccess => send_content!(GuestAccessEventContent, content), + EventType::RoomHistoryVisibility => send_content!(HistoryVisibilityEventContent, content), + EventType::RoomJoinRules => send_content!(JoinRulesEventContent, content), + EventType::RoomMember => send_content!(MemberEventContent, content), + EventType::RoomName => send_content!(NameEventContent, content), + EventType::RoomPowerLevels => send_content!(PowerLevelsEventContent, content), + EventType::RoomThirdPartyInvite => send_content!(ThirdPartyInviteEventContent, content), + EventType::RoomTopic => send_content!(TopicEventContent, content), + _ => Err(ApiError::bad_event("Unsupported state event type".to_string()))?, + } } } @@ -306,7 +407,7 @@ mod tests { // Alice updates the topic. let event_content = r#"{"topic": "Topic for Alice"}"#; - let response = test.send_state_event(&alice.token, &room_id, "m.room.topic", &event_content); + let response = test.send_state_event(&alice.token, &room_id, "m.room.topic", &event_content, None); assert_eq!(response.status, Status::Ok); // Bob can't see the changes. @@ -353,4 +454,127 @@ mod tests { } } } + + #[test] + fn retrieve_state_event_by_type() { + let test = Test::new(); + let (alice, room_id) = test.initial_fixtures("{}"); + + let response = test.get_state_event(&alice.token, &room_id, "m.room.create", None); + assert_eq!(response.status, Status::Ok); + assert_eq!(response.json().get("creator").unwrap().as_str().unwrap(), &alice.id); + + let topic_content = r#"{"topic": "Initial Topic"}"#; + let response = test.send_state_event(&alice.token, &room_id, "m.room.topic", topic_content, None); + assert_eq!(response.status, Status::Ok); + + let response = test.get_state_event(&alice.token, &room_id, "m.room.topic", None); + assert_eq!(response.status, Status::Ok); + assert_eq!(response.json().get("topic").unwrap().as_str().unwrap(), "Initial Topic"); + + // Change the topic again to ensure we only get the latest version of the event. + let topic_content = r#"{"topic": "Updated Topic"}"#; + let response = test.send_state_event(&alice.token, &room_id, "m.room.topic", topic_content, None); + assert_eq!(response.status, Status::Ok); + + let response = test.get_state_event(&alice.token, &room_id, "m.room.topic", None); + assert_eq!(response.status, Status::Ok); + assert_eq!(response.json().get("topic").unwrap().as_str().unwrap(), "Updated Topic"); + } + + #[test] + fn retrieve_state_event_by_type_and_key() { + let test = Test::new(); + let (alice, room_id) = test.initial_fixtures("{}"); + + let third_party_invite_content = r#"{ + "display_name": "Alice", + "key_validity_url": "https://magic.forest/verifykey", + "public_key": "abc123" + }"#; + let response = test.send_state_event( + &alice.token, + &room_id, + "m.room.third_party_invite", + third_party_invite_content, + Some("pc89") + ); + assert_eq!(response.status, Status::Ok); + + let response = test.get_state_event(&alice.token, &room_id, "m.room.third_party_invite", Some("pc89")); + assert_eq!(response.status, Status::Ok); + assert_eq!(response.json().get("public_key").unwrap().as_str().unwrap(), "abc123"); + + let response = test.get_state_event(&alice.token, &room_id, "m.room.third_party_invite", Some("pc100")); + assert_eq!(response.status, Status::NotFound); + } + + #[test] + fn retrieve_state_event_from_left_room() { + let test = Test::new(); + let bob = test.create_user(); + let room_options = format!(r#"{{ + "invite": ["{}"], + "initial_state": [{{ + "state_key": "", + "type": "m.room.name", + "content": {{ "name": "Initial Name" }} + }}] + }}"#, bob.id); + let (alice, room_id) = test.initial_fixtures(&room_options); + + assert_eq!(test.join_room(&bob.token, &room_id).status, Status::Ok); + assert_eq!(test.leave_room(&bob.token, &room_id).status, Status::Ok); + + let name_content = r#"{"name": "Updated Name"}"#; + let response = test.send_state_event(&alice.token, &room_id, "m.room.name", name_content, None); + assert_eq!(response.status, Status::Ok); + + let topic_content = r#"{"topic": "Initial Topic"}"#; + let response = test.send_state_event(&alice.token, &room_id, "m.room.topic", topic_content, None); + assert_eq!(response.status, Status::Ok); + + let response = test.get_state_event(&bob.token, &room_id, "m.room.name", None); + assert_eq!(response.status, Status::Ok); + assert_eq!(response.json().get("name").unwrap().as_str().unwrap(), "Initial Name"); + + let response = test.get_state_event(&bob.token, &room_id, "m.room.topic", None); + assert_eq!(response.status, Status::NotFound); + } + + #[test] + fn non_existent_state_event_type() { + let test = Test::new(); + let (alice, room_id) = test.initial_fixtures("{}"); + + let response = test.get_state_event(&alice.token, &room_id, "m.room.create", Some("unknown")); + assert_eq!(response.status, Status::NotFound); + + let response = test.get_state_event(&alice.token, &room_id, "m.room.unknown", None); + assert_eq!(response.status, Status::NotFound); + } + + #[test] + fn retrieve_state_event_non_member() { + let test = Test::new(); + let bob = test.create_user(); + let carl = test.create_user(); + let room_options = format!(r#"{{"invite": ["{}"]}}"#, carl.id); + let (_, room_id) = test.initial_fixtures(&room_options); + + // Neither Bob nor Carl can retrieve state events. + let response = test.get_state_event(&bob.token, &room_id, "m.room.create", None); + assert_eq!(response.status, Status::Forbidden); + assert_eq!( + response.json().get("error").unwrap().as_str().unwrap(), + "The user is not a member of the room" + ); + + let response = test.get_state_event(&carl.token, &room_id, "m.room.create", None); + assert_eq!(response.status, Status::Forbidden); + assert_eq!( + response.json().get("error").unwrap().as_str().unwrap(), + "The user is not a member of the room" + ); + } } diff --git a/src/api/r0/sync.rs b/src/api/r0/sync.rs index 43ef6310..04e87667 100644 --- a/src/api/r0/sync.rs +++ b/src/api/r0/sync.rs @@ -387,7 +387,8 @@ mod tests { &alice.token, &room_id, "m.room.topic", - r#"{ "topic": "Updated Topic" }"# + r#"{ "topic": "Updated Topic" }"#, + None ); assert_eq!(response.status, Status::Ok); @@ -526,7 +527,8 @@ mod tests { &alice.token, &room_id, "m.room.name", - r#"{ "name": "Updated Name" }"# + r#"{ "name": "Updated Name" }"#, + None ); assert_eq!(room_name_event_response.status, Status::Ok); @@ -599,7 +601,8 @@ mod tests { &alice.token, &room_id, "m.room.topic", - r#"{ "topic": "New Topic" }"# + r#"{ "topic": "New Topic" }"#, + None ); assert_eq!(response.status, Status::Ok); @@ -643,7 +646,8 @@ mod tests { &alice.token, &room_id, "m.room.topic", - r#"{ "topic": "Another Topic" }"# + r#"{ "topic": "Another Topic" }"#, + None ); assert_eq!(response.status, Status::Ok); diff --git a/src/server.rs b/src/server.rs index f0650924..ff0b5dd4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -22,6 +22,7 @@ use api::r0::{ GetPresenceStatus, GetPushers, GetRoomAlias, + GetStateEvent, GetTags, InviteToRoom, JoinRoom, @@ -124,6 +125,12 @@ impl<'a> Server<'a> { r0_router.post("rooms/:room_id/leave", LeaveRoom::chain(), "leave_room"); r0_router.get("/rooms/:room_id/members", Members::chain(), "members"); r0_router.get("/rooms/:room_id/state", RoomState::chain(), "get_room_state"); + r0_router.get("/rooms/:room_id/state/:event_type", GetStateEvent::chain(), "get_state_event"); + r0_router.get( + "/rooms/:room_id/state/:event_type/:state_key", + GetStateEvent::chain(), + "get_state_event_with_key" + ); r0_router.get("/profile/:user_id", Profile::chain(), "profile"); r0_router.get("/profile/:user_id/avatar_url", GetAvatarUrl::chain(), "get_avatar_url"); r0_router.get("/profile/:user_id/displayname", GetDisplayName::chain(), "get_display_name"); diff --git a/src/test.rs b/src/test.rs index 99b2f4b7..a954cd58 100644 --- a/src/test.rs +++ b/src/test.rs @@ -340,17 +340,62 @@ impl Test { room_id: &str, event_type: &str, event_content: &str, + state_key: Option<&str>, ) -> Response { - let state_event_path = format!( - "/_matrix/client/r0/rooms/{}/state/{}?access_token={}", - room_id, - event_type, - access_token - ); + let state_event_path = match state_key { + Some(state_key) => { + format!( + "/_matrix/client/r0/rooms/{}/state/{}/{}?access_token={}", + room_id, + event_type, + state_key, + access_token + ) + }, + None => { + format!( + "/_matrix/client/r0/rooms/{}/state/{}?access_token={}", + room_id, + event_type, + access_token + ) + } + }; self.put(&state_event_path, &event_content) } + /// Get a state event content from a room given its type. + pub fn get_state_event( + &self, + access_token: &str, + room_id: &str, + event_type: &str, + state_key: Option<&str>, + ) -> Response { + let state_event_path = match state_key { + Some(state_key) => { + format!( + "/_matrix/client/r0/rooms/{}/state/{}/{}?access_token={}", + room_id, + event_type, + state_key, + access_token, + ) + }, + None => { + format!( + "/_matrix/client/r0/rooms/{}/state/{}?access_token={}", + room_id, + event_type, + access_token, + ) + } + }; + + self.get(&state_event_path) + } + /// Create a User and Room. pub fn initial_fixtures(&self, body: &str) -> (TestUser, String) { let user = self.create_user();