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