From 9ac27d4c905c310fcd21ef381815f3c29262ca16 Mon Sep 17 00:00:00 2001 From: farodin91 Date: Sat, 15 Oct 2016 14:18:15 +0200 Subject: [PATCH] Add basic implementation for the tags endpoint. --- migrations/001_prerelease/down.sql | 1 + migrations/001_prerelease/up.sql | 9 ++ src/api/r0/mod.rs | 2 + src/api/r0/tags.rs | 232 +++++++++++++++++++++++++++++ src/main.rs | 1 + src/schema.rs | 10 ++ src/server.rs | 6 + src/tags.rs | 149 ++++++++++++++++++ src/test.rs | 14 ++ 9 files changed, 424 insertions(+) create mode 100644 src/api/r0/tags.rs create mode 100644 src/tags.rs diff --git a/migrations/001_prerelease/down.sql b/migrations/001_prerelease/down.sql index 1f619347..f8e5830b 100644 --- a/migrations/001_prerelease/down.sql +++ b/migrations/001_prerelease/down.sql @@ -7,3 +7,4 @@ DROP TABLE room_aliases; DROP TABLE room_memberships; DROP TABLE rooms; DROP TABLE users; +DROP TABLE room_tags; diff --git a/migrations/001_prerelease/up.sql b/migrations/001_prerelease/up.sql index 17a2615b..f7e3e0cf 100644 --- a/migrations/001_prerelease/up.sql +++ b/migrations/001_prerelease/up.sql @@ -76,3 +76,12 @@ CREATE TABLE users ( created_at TIMESTAMP NOT NULL DEFAULT now(), updated_at TIMESTAMP NOT NULL DEFAULT now() ); + +CREATE TABLE room_tags ( + id BIGSERIAL PRIMARY KEY, + user_id TEXT NOT NULL, + room_id TEXT NOT NULL, + tag TEXT NOT NULL, + content TEXT NOT NULL, + UNIQUE (user_id, room_id, tag) +); diff --git a/src/api/r0/mod.rs b/src/api/r0/mod.rs index be2547b1..b3356759 100644 --- a/src/api/r0/mod.rs +++ b/src/api/r0/mod.rs @@ -15,6 +15,7 @@ pub use self::members::Members; pub use self::profile::{Profile, GetAvatarUrl, PutAvatarUrl, GetDisplayname, PutDisplayname}; pub use self::registration::Register; pub use self::room_creation::CreateRoom; +pub use self::tags::{DeleteTag, GetTags, PutTag}; pub use self::versions::Versions; mod account; @@ -27,4 +28,5 @@ mod members; mod profile; mod registration; mod room_creation; +mod tags; mod versions; diff --git a/src/api/r0/tags.rs b/src/api/r0/tags.rs new file mode 100644 index 00000000..ffb52327 --- /dev/null +++ b/src/api/r0/tags.rs @@ -0,0 +1,232 @@ +//! Endpoints for tags. +use std::collections::BTreeMap; +use std::io::Read; + +use iron::{Chain, Handler, IronResult, Request, Response}; +use iron::status::Status; +use router::Router; +use serde_json::Value; +use serde_json::de::from_str; + +use db::DB; +use error::ApiError; +use middleware::{AccessTokenAuth, RoomIdParam, UserIdParam}; +use modifier::SerializableResponse; +use tags::RoomTag; + +pub type MapTags = BTreeMap; + +/// The `/user/:user_id/rooms/:room_id/tags` endpoint. +pub struct GetTags; + +#[derive(Debug, Serialize)] +struct GetTagsResponse { + tags: MapTags, +} + +impl GetTags { + /// Create a `GetTags` with all necessary middleware. + pub fn chain() -> Chain { + let mut chain = Chain::new(GetTags); + + chain.link_before(UserIdParam); + chain.link_before(RoomIdParam); + chain.link_before(AccessTokenAuth); + + chain + } +} + +impl Handler for GetTags { + fn handle(&self, request: &mut Request) -> IronResult { + let user_id = request.extensions.get::() + .expect("UserIdParam should ensure a UserId").clone(); + let room_id = request.extensions.get::() + .expect("RoomIdParam should ensure a RoomId").clone(); + + let connection = DB::from_request(request)?; + + let tags = RoomTag::find(&connection, user_id, room_id)?; + let mut map = MapTags::new(); + for tag in tags { + let content = from_str(&tag.content).map_err(ApiError::from)?; + map.insert(tag.tag, content); + } + + let response = GetTagsResponse { tags: map }; + + Ok(Response::with((Status::Ok, SerializableResponse(response)))) + } +} + +/// The `/user/:user_id/rooms/:room_id/tags/:tag` endpoint. +pub struct PutTag; + +impl PutTag { + /// Create a `GetTags` with all necessary middleware. + pub fn chain() -> Chain { + let mut chain = Chain::new(PutTag); + + chain.link_before(UserIdParam); + chain.link_before(RoomIdParam); + chain.link_before(AccessTokenAuth); + + chain + } +} + +impl Handler for PutTag { + fn handle(&self, request: &mut Request) -> IronResult { + let user_id = request.extensions.get::() + .expect("UserIdParam should ensure a UserId").clone(); + let room_id = request.extensions.get::() + .expect("RoomIdParam should ensure a RoomId").clone(); + let params = request.extensions.get::().expect("Params object is missing").clone(); + let tag = match params.find("tag") { + Some(tag) => Ok(String::from(tag)), + None => { + Err(ApiError::missing_param("tag")) + } + }?; + + let mut content = String::new(); + if let Err(_) = request.body.read_to_string(&mut content) { + Err(ApiError::not_found(None))?; + } + let content = match content.as_ref() { + "" => Value::Null, + _ => from_str(&content).map_err(ApiError::from)? + }; + + let connection = DB::from_request(request)?; + + RoomTag::upsert(&connection, user_id, room_id, tag, content)?; + + Ok(Response::with(Status::Ok)) + } +} + +/// The `/user/:user_id/rooms/:room_id/tags/:tag` endpoint. +pub struct DeleteTag; + +impl DeleteTag { + /// Create a `GetTags` with all necessary middleware. + pub fn chain() -> Chain { + let mut chain = Chain::new(DeleteTag); + + chain.link_before(UserIdParam); + chain.link_before(RoomIdParam); + chain.link_before(AccessTokenAuth); + + chain + } +} + +impl Handler for DeleteTag { + fn handle(&self, request: &mut Request) -> IronResult { + let user_id = request.extensions.get::() + .expect("UserIdParam should ensure a UserId").clone(); + let room_id = request.extensions.get::() + .expect("RoomIdParam should ensure a RoomId").clone(); + let params = request.extensions.get::().expect("Params object is missing").clone(); + let tag = match params.find("tag") { + Some(tag) => Ok(String::from(tag)), + None => { + Err(ApiError::missing_param("tag")) + } + }?; + + let connection = DB::from_request(request)?; + + RoomTag::delete(&connection, user_id, room_id, tag)?; + + Ok(Response::with(Status::Ok)) + } +} + + +#[cfg(test)] +mod tests { + use test::Test; + use iron::status::Status; + + #[test] + fn basic_create_tag() { + let test = Test::new(); + let access_token = test.create_access_token(); // @carl:ruma.test + + let room_id = test.create_public_room(&access_token); + + test.create_tag(&access_token, &room_id, "@carl:ruma.test", "work", r#"{"test":"test"}"#); + + let get_tags_path = format!( + "/_matrix/client/r0/user/@carl:ruma.test/rooms/{}/tags?access_token={}", + room_id, + access_token + ); + + let response = test.get(&get_tags_path); + assert_eq!(response.status, Status::Ok); + let chunk = response.json().find("tags").unwrap(); + assert!(chunk.is_object()); + let chunk = chunk.as_object().unwrap(); + assert_eq!(chunk.len(), 1); + let content = chunk.get("work").unwrap(); + assert_eq!(content.to_string(), r#"{"test":"test"}"#); + } + + #[test] + fn update_tag() { + let test = Test::new(); + let access_token = test.create_access_token(); // @carl:ruma.test + + let room_id = test.create_public_room(&access_token); + + test.create_tag(&access_token, &room_id, "@carl:ruma.test", "test", r#"{"test":"test"}"#); + + test.create_tag(&access_token, &room_id, "@carl:ruma.test", "test", r#"{"test":"test2"}"#); + + let get_tags_path = format!( + "/_matrix/client/r0/user/@carl:ruma.test/rooms/{}/tags?access_token={}", + room_id, + access_token + ); + + let response = test.get(&get_tags_path); + let chunk = response.json().find("tags").unwrap(); + let chunk = chunk.as_object().unwrap(); + let content = chunk.get("test").unwrap(); + assert_eq!(content.to_string(), r#"{"test":"test2"}"#); + } + + #[test] + fn delete_tag() { + let test = Test::new(); + let access_token = test.create_access_token(); // @carl:ruma.test + + let room_id = test.create_public_room(&access_token); + + test.create_tag(&access_token, &room_id, "@carl:ruma.test", "delete", r#"{"test":"test"}"#); + + let delete_tag_path = format!( + "/_matrix/client/r0/user/@carl:ruma.test/rooms/{}/tags/delete?access_token={}", + room_id, + access_token + ); + + let response = test.delete(&delete_tag_path); + assert_eq!(response.status, Status::Ok); + + let get_tags_path = format!( + "/_matrix/client/r0/user/@carl:ruma.test/rooms/{}/tags?access_token={}", + room_id, + access_token + ); + + let response = test.get(&get_tags_path); + assert_eq!(response.status, Status::Ok); + let chunk = response.json().find("tags").unwrap(); + let chunk = chunk.as_object().unwrap(); + assert_eq!(chunk.len(), 0); + } +} diff --git a/src/main.rs b/src/main.rs index 844bd714..8c9a72bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ pub mod schema; pub mod server; pub mod swagger; pub mod room_membership; +pub mod tags; #[cfg(test)] pub mod test; pub mod user; diff --git a/src/schema.rs b/src/schema.rs index dacc7c7d..b3e87c60 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -96,3 +96,13 @@ table! { content -> Text, } } + +table! { + room_tags { + id -> BigSerial, + user_id -> Text, + room_id -> Text, + tag -> Text, + content -> Text, + } +} diff --git a/src/server.rs b/src/server.rs index 0b483511..5fd97ac4 100644 --- a/src/server.rs +++ b/src/server.rs @@ -14,9 +14,11 @@ use api::r0::{ CreateRoom, DeactivateAccount, DeleteRoomAlias, + DeleteTag, GetAvatarUrl, GetDisplayname, GetRoomAlias, + GetTags, JoinRoom, Login, Logout, @@ -27,6 +29,7 @@ use api::r0::{ PutDisplayname, PutRoomAccountData, PutRoomAlias, + PutTag, Register, SendMessageEvent, StateMessageEvent, @@ -92,6 +95,9 @@ impl<'a> Server<'a> { r0_router.get("/profile/:user_id/displayname", GetDisplayname::chain()); r0_router.put("/profile/:user_id/avatar_url", PutAvatarUrl::chain()); r0_router.put("/profile/:user_id/displayname", PutDisplayname::chain()); + r0_router.get("/user/:user_id/rooms/:room_id/tags", GetTags::chain()); + r0_router.put("/user/:user_id/rooms/:room_id/tags/:tag", PutTag::chain()); + r0_router.delete("/user/:user_id/rooms/:room_id/tags/:tag", DeleteTag::chain()); let mut r0 = Chain::new(r0_router); diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 00000000..ae7a90e5 --- /dev/null +++ b/src/tags.rs @@ -0,0 +1,149 @@ +use diesel::{ + ExpressionMethods, + ExecuteDsl, + LoadDsl, + FilterDsl, + SaveChangesDsl, + insert, + delete, +}; +use diesel::pg::PgConnection; +use diesel::result::Error as DieselError; +use ruma_identifiers::{RoomId, UserId}; +use serde_json::Value; + +use error::ApiError; +use schema::room_tags; + +#[derive(Debug, Clone)] +#[insertable_into(room_tags)] +pub struct NewRoomTag { + /// The user's ID. + pub user_id: UserId, + /// The room's ID. + pub room_id: RoomId, + /// Tag + pub tag: String, + /// Json content + pub content: String, +} + +/// A Matrix room membership. +#[derive(Debug, Clone, Identifiable, Queryable)] +#[changeset_for(room_tags)] +pub struct RoomTag { + /// Entry ID + pub id: i64, + /// The user's ID. + pub user_id: UserId, + /// The room's ID. + pub room_id: RoomId, + /// Tag + pub tag: String, + /// Json content + pub content: String, +} + + +impl RoomTag { + /// Return `RoomTag`'s for given `UserId` and `RoomId`. + pub fn find( + connection: &PgConnection, + user_id: UserId, + room_id: RoomId) + -> Result, ApiError> { + let tags: Vec = room_tags::table + .filter(room_tags::room_id.eq(room_id)) + .filter(room_tags::user_id.eq(user_id)) + .get_results(connection) + .map_err(|err| match err { + DieselError::NotFound => ApiError::not_found(None), + _ => ApiError::from(err), + })?; + + Ok(tags) + } + + /// Return `RoomTag` for given `UserId`, `RoomId` and `tag`. + pub fn first( + connection: &PgConnection, + user_id: UserId, + room_id: RoomId, + tag: String) + -> Result, ApiError> { + let tag = room_tags::table + .filter(room_tags::room_id.eq(room_id)) + .filter(room_tags::user_id.eq(user_id)) + .filter(room_tags::tag.eq(tag)) + .first(connection); + + + match tag { + Ok(tag) => Ok(Some(tag)), + Err(DieselError::NotFound) => Ok(None), + Err(err) => Err(ApiError::from(err)), + } + } + + pub fn upsert( + connection: &PgConnection, + user_id: UserId, + room_id: RoomId, + tag: String, + content: Value) + -> Result<(), ApiError> { + let entry = RoomTag::first(connection, user_id.clone(), room_id.clone(), tag.clone())?; + let content = content.to_string(); + + match entry { + Some(mut entry) => entry.update(connection, content), + None => RoomTag::insert(connection, user_id, room_id, tag, content) + } + } + + pub fn insert( + connection: &PgConnection, + user_id: UserId, + room_id: RoomId, + tag: String, + content: String) + -> Result<(), ApiError> { + let new_room_tag = NewRoomTag { + user_id: user_id, + room_id: room_id, + tag: tag, + content: content, + }; + insert(&new_room_tag) + .into(room_tags::table) + .execute(connection) + .map_err(ApiError::from)?; + Ok(()) + } + + pub fn update(&mut self, connection: &PgConnection, content: String) -> Result<(), ApiError> { + self.content = content; + self.save_changes::(connection) + .map_err(ApiError::from)?; + Ok(()) + } + + pub fn delete( + connection: &PgConnection, + user_id: UserId, + room_id: RoomId, + tag: String) + -> Result<(), ApiError> { + let tag = room_tags::table + .filter(room_tags::room_id.eq(room_id)) + .filter(room_tags::user_id.eq(user_id)) + .filter(room_tags::tag.eq(tag)); + delete(tag) + .execute(connection) + .map_err(|err| match err { + DieselError::NotFound => ApiError::not_found(None), + _ => ApiError::from(err), + })?; + Ok(()) + } +} diff --git a/src/test.rs b/src/test.rs index a635b431..c1ec3d89 100644 --- a/src/test.rs +++ b/src/test.rs @@ -207,6 +207,20 @@ impl Test { self.post(&join_path, r"{}") } + + /// Create tag + pub fn create_tag(&self, access_token: &str, room_id: &str, user_id: &str, tag: &str, content: &str) { + let put_tag_path = format!( + "/_matrix/client/r0/user/{}/rooms/{}/tags/{}?access_token={}", + user_id, + room_id, + tag, + access_token + ); + + let response = self.put(&put_tag_path, content); + assert_eq!(response.status, Status::Ok); + } } impl Response {