[fractal] API: Implement types for room management, no events yet



commit 0db947a00a3b26550f564d6bb923a01afd85cd54
Author: Alejandro Domínguez <adomu net-c com>
Date:   Fri Dec 13 05:25:27 2019 +0100

    API: Implement types for room management, no events yet

 fractal-matrix-api/src/backend/room.rs             | 424 +++++++++++----------
 fractal-matrix-api/src/backend/user.rs             |  22 +-
 fractal-matrix-api/src/error.rs                    |   1 +
 fractal-matrix-api/src/meson.build                 |  10 +
 fractal-matrix-api/src/model/member.rs             |  17 +-
 fractal-matrix-api/src/r0.rs                       |   3 +
 fractal-matrix-api/src/r0/media/create.rs          |  18 +-
 fractal-matrix-api/src/r0/membership.rs            |   3 +
 .../src/r0/membership/invite_user.rs               |  30 ++
 .../src/r0/membership/join_room_by_id_or_alias.rs  |  31 ++
 fractal-matrix-api/src/r0/membership/leave_room.rs |  20 +
 fractal-matrix-api/src/r0/sync.rs                  |   1 +
 .../src/r0/sync/get_joined_members.rs              |  38 ++
 fractal-matrix-api/src/r0/tag.rs                   |   2 +
 fractal-matrix-api/src/r0/tag/create_tag.rs        |  37 ++
 fractal-matrix-api/src/r0/tag/delete_tag.rs        |  29 ++
 fractal-matrix-api/src/r0/typing.rs                |  59 +++
 fractal-matrix-api/src/serde.rs                    |  15 +-
 fractal-matrix-api/src/util.rs                     |   2 +-
 19 files changed, 529 insertions(+), 233 deletions(-)
---
diff --git a/fractal-matrix-api/src/backend/room.rs b/fractal-matrix-api/src/backend/room.rs
index d4c5681a..dea519a6 100644
--- a/fractal-matrix-api/src/backend/room.rs
+++ b/fractal-matrix-api/src/backend/room.rs
@@ -1,27 +1,29 @@
 use log::error;
 use serde_json::json;
 
-use reqwest::header::CONTENT_TYPE;
-use std::fs::File;
-use std::io::prelude::*;
+use ruma_identifiers::RoomId;
+use ruma_identifiers::RoomIdOrAliasId;
+use std::convert::TryFrom;
+use std::fs;
 use std::sync::mpsc::Sender;
 use url::Url;
 
 use std::collections::HashMap;
 use std::sync::{Arc, Mutex};
+use std::time::Duration;
 
 use crate::error::Error;
 use crate::globals;
 use std::thread;
 
 use crate::util::cache_dir_path;
+use crate::util::client_url;
 use crate::util::dw_media;
 use crate::util::get_prev_batch_from;
 use crate::util::json_q;
 use crate::util::ContentType;
 use crate::util::ResultExpectLog;
 use crate::util::HTTP_CLIENT;
-use crate::util::{client_url, media_url};
 
 use crate::backend::types::BKCommand;
 use crate::backend::types::BKResponse;
@@ -30,7 +32,28 @@ use crate::backend::types::BackendData;
 use crate::backend::types::RoomType;
 
 use crate::r0::filter::RoomEventFilter;
+use crate::r0::media::create::request as create_content;
+use crate::r0::media::create::Parameters as CreateContentParameters;
+use crate::r0::media::create::Response as CreateContentResponse;
+use crate::r0::membership::invite_user::request as invite_user;
+use crate::r0::membership::invite_user::Body as InviteUserBody;
+use crate::r0::membership::invite_user::Parameters as InviteUserParameters;
+use crate::r0::membership::join_room_by_id_or_alias::request as join_room_req;
+use crate::r0::membership::join_room_by_id_or_alias::Parameters as JoinRoomParameters;
+use crate::r0::membership::leave_room::request as leave_room_req;
+use crate::r0::membership::leave_room::Parameters as LeaveRoomParameters;
+use crate::r0::sync::get_joined_members::request as get_joined_members;
+use crate::r0::sync::get_joined_members::Parameters as JoinedMembersParameters;
+use crate::r0::sync::get_joined_members::Response as JoinedMembersResponse;
 use crate::r0::sync::sync_events::Language;
+use crate::r0::tag::create_tag::request as create_tag;
+use crate::r0::tag::create_tag::Body as CreateTagBody;
+use crate::r0::tag::create_tag::Parameters as CreateTagParameters;
+use crate::r0::tag::delete_tag::request as delete_tag;
+use crate::r0::tag::delete_tag::Parameters as DeleteTagParameters;
+use crate::r0::typing::request as send_typing_notification;
+use crate::r0::typing::Body as TypingNotificationBody;
+use crate::r0::typing::Parameters as TypingNotificationParameters;
 use crate::r0::AccessToken;
 use crate::types::ExtraContent;
 use crate::types::Member;
@@ -59,24 +82,23 @@ pub fn get_room_detail(
     base: Url,
     access_token: AccessToken,
     roomid: String,
-    key: String,
+    keys: String,
 ) -> Result<(), Error> {
     let url = bk.url(
         base,
         &access_token,
-        &format!("rooms/{}/state/{}", roomid, key),
+        &format!("rooms/{}/state/{}", roomid, keys),
         vec![],
     )?;
 
     let tx = bk.tx.clone();
-    let keys = key.clone();
     get!(
         url,
         |r: JsonValue| {
             let k = keys.split('.').last().unwrap();
 
-            let value = String::from(r[&k].as_str().unwrap_or_default());
-            tx.send(BKResponse::RoomDetail(Ok((roomid, key, value))))
+            let value = r[&k].as_str().map(Into::into).unwrap_or_default();
+            tx.send(BKResponse::RoomDetail(Ok((roomid, keys, value))))
                 .expect_log("Connection closed");
         },
         |err| {
@@ -140,34 +162,30 @@ pub fn get_room_members(
     access_token: AccessToken,
     roomid: String,
 ) -> Result<(), Error> {
-    let url = bk.url(
-        base,
-        &access_token,
-        &format!("rooms/{}/joined_members", roomid),
-        vec![],
-    )?;
-
     let tx = bk.tx.clone();
-    get!(
-        url,
-        |r: JsonValue| {
-            let joined = r["joined"].as_object().unwrap();
-            let ms: Vec<Member> = joined
-                .iter()
-                .map(|(mxid, member_data)| {
-                    let mut member: Member = serde_json::from_value(member_data.clone()).unwrap();
-                    member.uid = mxid.to_string();
-                    member
-                })
-                .collect();
-            tx.send(BKResponse::RoomMembers(Ok((roomid, ms))))
-                .expect_log("Connection closed");
-        },
-        |err| {
-            tx.send(BKResponse::RoomMembers(Err(err)))
-                .expect_log("Connection closed");
-        }
-    );
+
+    let room_id = RoomId::try_from(roomid.as_str())?;
+    let params = JoinedMembersParameters { access_token };
+
+    thread::spawn(move || {
+        let query = get_joined_members(base, &room_id, &params)
+            .map_err(Into::into)
+            .and_then(|request| {
+                HTTP_CLIENT
+                    .get_client()?
+                    .execute(request)?
+                    .json::<JoinedMembersResponse>()
+                    .map_err(Into::into)
+            })
+            .map(|response| {
+                let ms = response.joined.into_iter().map(Member::from).collect();
+
+                (room_id.to_string(), ms)
+            });
+
+        tx.send(BKResponse::RoomMembers(query))
+            .expect_log("Connection closed");
+    });
 
     Ok(())
 }
@@ -378,22 +396,29 @@ pub fn send_typing(
     userid: String,
     roomid: String,
 ) -> Result<(), Error> {
-    let url = bk.url(
-        base,
-        &access_token,
-        &format!("rooms/{}/typing/{}", roomid, userid),
-        vec![],
-    )?;
+    let tx = bk.tx.clone();
 
-    let attrs = json!({
-        "timeout": 4000,
-        "typing": true
-    });
+    let room_id = RoomId::try_from(roomid.as_str())?;
+    let params = TypingNotificationParameters { access_token };
+    let body = TypingNotificationBody::Typing(Duration::from_secs(4));
 
-    let tx = bk.tx.clone();
-    query!("put", url, &attrs, move |_| {}, |err| {
-        tx.send(BKResponse::SendTypingError(err))
-            .expect_log("Connection closed");
+    thread::spawn(move || {
+        let query = send_typing_notification(base, &room_id, &userid, &params, &body)
+            .map_err(Into::into)
+            .and_then(|request| {
+                HTTP_CLIENT
+                    .get_client()?
+                    .execute(request)
+                    .map_err(Into::into)
+            });
+
+        match query {
+            Err(err) => {
+                tx.send(BKResponse::SendTypingError(err))
+                    .expect_log("Connection closed");
+            }
+            _ => (),
+        }
     });
 
     Ok(())
@@ -447,27 +472,31 @@ pub fn join_room(
     access_token: AccessToken,
     roomid: String,
 ) -> Result<(), Error> {
-    let url = bk.url(
-        base,
-        &access_token,
-        &format!("join/{}", urlencoding::encode(&roomid)),
-        vec![],
-    )?;
-
     let tx = bk.tx.clone();
     let data = bk.data.clone();
-    post!(
-        url,
-        move |_: JsonValue| {
-            data.lock().unwrap().join_to_room = roomid.clone();
-            tx.send(BKResponse::JoinRoom(Ok(())))
-                .expect_log("Connection closed");
-        },
-        |err| {
-            tx.send(BKResponse::JoinRoom(Err(err)))
-                .expect_log("Connection closed");
-        }
-    );
+
+    let room_id_or_alias_id = RoomIdOrAliasId::try_from(roomid.as_str())?;
+    let params = JoinRoomParameters {
+        access_token,
+        server_name: Default::default(),
+    };
+
+    thread::spawn(move || {
+        let query = join_room_req(base, &room_id_or_alias_id, &params)
+            .map_err(Into::into)
+            .and_then(|request| {
+                HTTP_CLIENT
+                    .get_client()?
+                    .execute(request)
+                    .map_err(Into::into)
+            })
+            .map(|_| {
+                data.lock().unwrap().join_to_room = room_id_or_alias_id.to_string();
+            });
+
+        tx.send(BKResponse::JoinRoom(query))
+            .expect_log("Connection closed");
+    });
 
     Ok(())
 }
@@ -478,25 +507,25 @@ pub fn leave_room(
     access_token: AccessToken,
     roomid: String,
 ) -> Result<(), Error> {
-    let url = bk.url(
-        base,
-        &access_token,
-        &format!("rooms/{}/leave", roomid),
-        vec![],
-    )?;
-
     let tx = bk.tx.clone();
-    post!(
-        url,
-        move |_: JsonValue| {
-            tx.send(BKResponse::LeaveRoom(Ok(())))
-                .expect_log("Connection closed");
-        },
-        |err| {
-            tx.send(BKResponse::LeaveRoom(Err(err)))
-                .expect_log("Connection closed");
-        }
-    );
+
+    let room_id = RoomId::try_from(roomid.as_str())?;
+    let params = LeaveRoomParameters { access_token };
+
+    thread::spawn(move || {
+        let query = leave_room_req(base, &room_id, &params)
+            .map_err(Into::into)
+            .and_then(|request| {
+                HTTP_CLIENT
+                    .get_client()?
+                    .execute(request)
+                    .map_err(Into::into)
+            })
+            .and(Ok(()));
+
+        tx.send(BKResponse::LeaveRoom(query))
+            .expect_log("Connection closed");
+    });
 
     Ok(())
 }
@@ -628,43 +657,29 @@ pub fn set_room_avatar(
     roomid: String,
     avatar: String,
 ) -> Result<(), Error> {
-    let params = &[("access_token", tk.to_string())];
-    let mediaurl = media_url(&baseu, "upload", params)?;
     let roomurl = bk.url(
-        baseu,
+        baseu.clone(),
         &tk,
         &format!("rooms/{}/state/m.room.avatar", roomid),
         vec![],
     )?;
 
-    let mut file = File::open(&avatar)?;
-    let mut contents: Vec<u8> = vec![];
-    file.read_to_end(&mut contents)?;
-
     let tx = bk.tx.clone();
     thread::spawn(move || {
-        match put_media(mediaurl.as_str(), contents) {
-            Err(err) => {
-                tx.send(BKResponse::SetRoomAvatar(Err(err)))
-                    .expect_log("Connection closed");
-            }
-            Ok(js) => {
-                let uri = js["content_uri"].as_str().unwrap_or_default();
-                let attrs = json!({ "url": uri });
-                put!(
-                    roomurl,
-                    &attrs,
-                    |_| {
-                        tx.send(BKResponse::SetRoomAvatar(Ok(())))
-                            .expect_log("Connection closed");
-                    },
-                    |err| {
-                        tx.send(BKResponse::SetRoomAvatar(Err(err)))
-                            .expect_log("Connection closed");
-                    }
-                );
-            }
-        };
+        let query = upload_file(baseu, tk, &avatar).and_then(|response| {
+            let js = json!({ "url": response.content_uri.as_str() });
+
+            HTTP_CLIENT
+                .get_client()?
+                .put(roomurl)
+                .json(&js)
+                .send()
+                .map_err(Into::into)
+                .and(Ok(()))
+        });
+
+        tx.send(BKResponse::SetRoomAvatar(query))
+            .expect_log("Connection closed");
     });
 
     Ok(())
@@ -677,15 +692,15 @@ pub fn attach_file(
     mut msg: Message,
 ) -> Result<(), Error> {
     let fname = msg.url.clone().unwrap_or_default();
-    let extra_content: Option<ExtraContent> = {
-        msg.clone()
-            .extra_content
-            .map_or(None, |c| Some(serde_json::from_value(c).unwrap()))
-    };
+    let mut extra_content: Option<ExtraContent> = msg
+        .clone()
+        .extra_content
+        .and_then(|c| serde_json::from_value(c).ok());
 
     let thumb = extra_content
         .clone()
-        .map_or(String::new(), |c| c.info.thumbnail_url.unwrap_or_default());
+        .and_then(|c| c.info.thumbnail_url)
+        .unwrap_or_default();
 
     let tx = bk.tx.clone();
     let itx = bk.internal_tx.clone();
@@ -695,57 +710,65 @@ pub fn attach_file(
     }
 
     thread::spawn(move || {
-        if thumb != "" {
-            match upload_file(&baseu, &tk, &thumb) {
+        if !thumb.is_empty() {
+            match upload_file(baseu.clone(), tk.clone(), &thumb) {
                 Err(err) => {
                     tx.send(BKResponse::AttachedFile(Err(err)))
                         .expect_log("Connection closed");
                 }
-                Ok(thumb_uri) => {
-                    msg.thumb = Some(thumb_uri.to_string());
-                    if let Some(mut xctx) = extra_content {
+                Ok(response) => {
+                    let thumb_uri = response.content_uri.to_string();
+                    msg.thumb = Some(thumb_uri.clone());
+                    if let Some(ref mut xctx) = extra_content {
                         xctx.info.thumbnail_url = Some(thumb_uri);
-                        msg.extra_content = Some(serde_json::to_value(&xctx).unwrap());
                     }
+                    msg.extra_content = serde_json::to_value(&extra_content).ok();
                 }
             }
+
             if let Err(_e) = std::fs::remove_file(&thumb) {
                 error!("Can't remove thumbnail: {}", thumb);
             }
         }
 
-        match upload_file(&baseu, &tk, &fname) {
-            Err(err) => {
-                tx.send(BKResponse::AttachedFile(Err(err)))
-                    .expect_log("Connection closed");
-            }
-            Ok(uri) => {
-                msg.url = Some(uri.to_string());
-                if let Some(t) = itx {
-                    t.send(BKCommand::SendMsg(baseu, tk, msg.clone()))
-                        .expect_log("Connection closed");
-                }
-                tx.send(BKResponse::AttachedFile(Ok(msg)))
+        let query = upload_file(baseu.clone(), tk.clone(), &fname).map(|response| {
+            msg.url = Some(response.content_uri.to_string());
+            if let Some(t) = itx {
+                t.send(BKCommand::SendMsg(baseu, tk, msg.clone()))
                     .expect_log("Connection closed");
             }
-        };
+
+            msg
+        });
+
+        tx.send(BKResponse::AttachedFile(query))
+            .expect_log("Connection closed");
     });
 
     Ok(())
 }
 
-fn upload_file(baseu: &Url, tk: &AccessToken, fname: &str) -> Result<String, Error> {
-    let mut file = File::open(fname)?;
-    let mut contents: Vec<u8> = vec![];
-    file.read_to_end(&mut contents)?;
-
-    let params = &[("access_token", tk.to_string())];
-    let mediaurl = media_url(&baseu, "upload", params)?;
+fn upload_file(
+    base: Url,
+    access_token: AccessToken,
+    fname: &str,
+) -> Result<CreateContentResponse, Error> {
+    let params_upload = CreateContentParameters {
+        access_token,
+        filename: None,
+    };
 
-    match put_media(mediaurl.as_str(), contents) {
-        Err(err) => Err(err),
-        Ok(js) => Ok(js["content_uri"].as_str().unwrap_or_default().to_string()),
-    }
+    let contents = fs::read(fname)?;
+
+    create_content(base, &params_upload, contents)
+        .map_err::<Error, _>(Into::into)
+        .and_then(|request| {
+            HTTP_CLIENT
+                .get_client()?
+                .execute(request)?
+                .json::<CreateContentResponse>()
+                .map_err(Into::into)
+        })
 }
 
 pub fn new_room(
@@ -888,32 +911,33 @@ pub fn add_to_fav(
     roomid: String,
     tofav: bool,
 ) -> Result<(), Error> {
-    let url = bk.url(
-        base,
-        &access_token,
-        &format!("user/{}/rooms/{}/tags/m.favourite", userid, roomid),
-        vec![],
-    )?;
+    let tx = bk.tx.clone();
 
-    let attrs = json!({
-        "order": 0.5,
-    });
+    let room_id = RoomId::try_from(roomid.as_str())?;
 
-    let tx = bk.tx.clone();
-    let method = if tofav { "put" } else { "delete" };
-    query!(
-        method,
-        url,
-        &attrs,
-        |_| {
-            tx.send(BKResponse::AddedToFav(Ok((roomid.clone(), tofav))))
-                .expect_log("Connection closed");
-        },
-        |err| {
-            tx.send(BKResponse::AddedToFav(Err(err)))
-                .expect_log("Connection closed");
-        }
-    );
+    thread::spawn(move || {
+        let request_res = if tofav {
+            let params = CreateTagParameters { access_token };
+            let body = CreateTagBody { order: Some(0.5) };
+            create_tag(base, &userid, &room_id, "m.favourite", &params, &body)
+        } else {
+            let params = DeleteTagParameters { access_token };
+            delete_tag(base, &userid, &room_id, "m.favourite", &params)
+        };
+
+        let query = request_res
+            .map_err(Into::into)
+            .and_then(|request| {
+                HTTP_CLIENT
+                    .get_client()?
+                    .execute(request)
+                    .map_err(Into::into)
+            })
+            .and(Ok((room_id.to_string(), tofav)));
+
+        tx.send(BKResponse::AddedToFav(query))
+            .expect_log("Connection closed");
+    });
 
     Ok(())
 }
@@ -923,41 +947,35 @@ pub fn invite(
     base: Url,
     access_token: AccessToken,
     roomid: String,
-    userid: String,
+    user_id: String,
 ) -> Result<(), Error> {
-    let url = bk.url(
-        base,
-        &access_token,
-        &format!("rooms/{}/invite", roomid),
-        vec![],
-    )?;
+    let tx = bk.tx.clone();
 
-    let attrs = json!({
-        "user_id": userid,
-    });
+    let room_id = RoomId::try_from(roomid.as_str())?;
+    let params = InviteUserParameters { access_token };
+    let body = InviteUserBody { user_id };
 
-    let tx = bk.tx.clone();
-    post!(url, &attrs, |_| {}, |err| {
-        tx.send(BKResponse::InviteError(err))
-            .expect_log("Connection closed");
+    thread::spawn(move || {
+        let query = invite_user(base, &room_id, &params, &body)
+            .map_err(Into::into)
+            .and_then(|request| {
+                HTTP_CLIENT
+                    .get_client()?
+                    .execute(request)
+                    .map_err(Into::into)
+            });
+
+        match query {
+            Err(err) => {
+                let _ = tx.send(BKResponse::InviteError(err));
+            }
+            _ => (),
+        }
     });
 
     Ok(())
 }
 
-fn put_media(url: &str, file: Vec<u8>) -> Result<JsonValue, Error> {
-    let (mime, _) = gio::content_type_guess(None, &file);
-
-    HTTP_CLIENT
-        .get_client()?
-        .post(url)
-        .body(file)
-        .header(CONTENT_TYPE, mime.to_string())
-        .send()?
-        .json()
-        .or(Err(Error::BackendError))
-}
-
 pub fn set_language(
     bk: &Backend,
     access_token: AccessToken,
diff --git a/fractal-matrix-api/src/backend/user.rs b/fractal-matrix-api/src/backend/user.rs
index 542b97ea..a76953da 100644
--- a/fractal-matrix-api/src/backend/user.rs
+++ b/fractal-matrix-api/src/backend/user.rs
@@ -11,7 +11,6 @@ use crate::util::semaphore;
 use crate::util::ContentType;
 use crate::util::ResultExpectLog;
 use crate::util::HTTP_CLIENT;
-use reqwest::header::HeaderValue;
 use std::convert::TryInto;
 use std::sync::mpsc::Sender;
 use std::sync::{Arc, Mutex};
@@ -372,18 +371,15 @@ pub fn set_user_avatar(
     fs::read(&avatar)
         .map_err(Into::into)
         .and_then(|contents| {
-            let (mime, _) = gio::content_type_guess(None, &contents);
-            let mime_value = HeaderValue::from_str(&mime).or(Err(Error::BackendError))?;
-            let upload_response =
-                create_content(base.clone(), &params_upload, contents, Some(mime_value))
-                    .map_err::<Error, _>(Into::into)
-                    .and_then(|request| {
-                        HTTP_CLIENT
-                            .get_client()?
-                            .execute(request)?
-                            .json::<CreateContentResponse>()
-                            .map_err(Into::into)
-                    })?;
+            let upload_response = create_content(base.clone(), &params_upload, contents)
+                .map_err::<Error, _>(Into::into)
+                .and_then(|request| {
+                    HTTP_CLIENT
+                        .get_client()?
+                        .execute(request)?
+                        .json::<CreateContentResponse>()
+                        .map_err(Into::into)
+                })?;
 
             let params_avatar = SetAvatarUrlParameters { access_token };
             let body = SetAvatarUrlBody {
diff --git a/fractal-matrix-api/src/error.rs b/fractal-matrix-api/src/error.rs
index 13f82f65..08d02ad9 100644
--- a/fractal-matrix-api/src/error.rs
+++ b/fractal-matrix-api/src/error.rs
@@ -27,6 +27,7 @@ derror!(url::ParseError, Error::BackendError);
 derror!(io::Error, Error::BackendError);
 derror!(gio::Error, Error::BackendError);
 derror!(regex::Error, Error::BackendError);
+derror!(ruma_identifiers::Error, Error::BackendError);
 derror!(SystemTimeError, Error::BackendError);
 
 derror!(serde_json::Error, Error::CacheError);
diff --git a/fractal-matrix-api/src/meson.build b/fractal-matrix-api/src/meson.build
index 9df5e735..c4b1892b 100644
--- a/fractal-matrix-api/src/meson.build
+++ b/fractal-matrix-api/src/meson.build
@@ -29,30 +29,40 @@ api_sources = files(
   'r0/contact/request_verification_token_msisdn.rs',
   'r0/directory/post_public_rooms.rs',
   'r0/media/create.rs',
+  'r0/membership/invite_user.rs',
+  'r0/membership/join_room_by_id_or_alias.rs',
+  'r0/membership/leave_room.rs',
   'r0/profile/get_display_name.rs',
   'r0/profile/get_profile.rs',
   'r0/profile/set_avatar_url.rs',
   'r0/profile/set_display_name.rs',
   'r0/search/user.rs',
   'r0/server/domain_info.rs',
+  'r0/sync/get_joined_members.rs',
   'r0/sync/sync_events.rs',
+  'r0/tag/create_tag.rs',
+  'r0/tag/delete_tag.rs',
   'r0/thirdparty/get_supported_protocols.rs',
   'r0/account.rs',
   'r0/contact.rs',
   'r0/directory.rs',
   'r0/filter.rs',
   'r0/media.rs',
+  'r0/membership.rs',
   'r0/profile.rs',
   'r0/search.rs',
   'r0/server.rs',
   'r0/sync.rs',
+  'r0/tag.rs',
   'r0/thirdparty.rs',
+  'r0/typing.rs',
   'cache.rs',
   'client.rs',
   'error.rs',
   'globals.rs',
   'identity.rs',
   'lib.rs',
+  'meson.build',
   'r0.rs',
   'serde.rs',
   'types.rs',
diff --git a/fractal-matrix-api/src/model/member.rs b/fractal-matrix-api/src/model/member.rs
index 3f9ae5cd..160beae8 100644
--- a/fractal-matrix-api/src/model/member.rs
+++ b/fractal-matrix-api/src/model/member.rs
@@ -1,9 +1,10 @@
 use crate::r0::search::user::User;
+use crate::r0::sync::get_joined_members::RoomMember;
 use serde::{Deserialize, Serialize};
 use std::collections::HashMap;
 use url::Url;
 
-// TODO: Remove this and use only crate::r0::search::user::User
+// TODO: Make this non-(de)serializable
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Member {
     // The mxid is either inside the json object, or outside of it.
@@ -43,5 +44,19 @@ impl From<User> for Member {
     }
 }
 
+impl From<(String, RoomMember)> for Member {
+    fn from(uid_roommember: (String, RoomMember)) -> Self {
+        Member {
+            uid: uid_roommember.0,
+            alias: uid_roommember.1.display_name,
+            avatar: uid_roommember
+                .1
+                .avatar_url
+                .as_ref()
+                .map(ToString::to_string),
+        }
+    }
+}
+
 // hashmap userid -> Member
 pub type MemberList = HashMap<String, Member>;
diff --git a/fractal-matrix-api/src/r0.rs b/fractal-matrix-api/src/r0.rs
index 46c5ae18..9b7e03e8 100644
--- a/fractal-matrix-api/src/r0.rs
+++ b/fractal-matrix-api/src/r0.rs
@@ -3,11 +3,14 @@ pub mod contact;
 pub mod directory;
 pub mod filter;
 pub mod media;
+pub mod membership;
 pub mod profile;
 pub mod search;
 pub mod server;
 pub mod sync;
+pub mod tag;
 pub mod thirdparty;
+pub mod typing;
 
 use serde::{Deserialize, Serialize, Serializer};
 use std::convert::TryFrom;
diff --git a/fractal-matrix-api/src/r0/media/create.rs b/fractal-matrix-api/src/r0/media/create.rs
index 4080d973..c1f95dac 100644
--- a/fractal-matrix-api/src/r0/media/create.rs
+++ b/fractal-matrix-api/src/r0/media/create.rs
@@ -1,6 +1,6 @@
 use crate::r0::AccessToken;
 use crate::serde::url as serde_url;
-use reqwest::header::{HeaderValue, CONTENT_TYPE};
+use reqwest::header::CONTENT_TYPE;
 use reqwest::Client;
 use reqwest::Error;
 use reqwest::Request;
@@ -19,16 +19,8 @@ pub struct Response {
     pub content_uri: Url,
 }
 
-pub fn request(
-    base: Url,
-    params: &Parameters,
-    file: Vec<u8>,
-    content_type: Option<HeaderValue>,
-) -> Result<Request, Error> {
-    let header = content_type
-        .map(|mime| (CONTENT_TYPE, mime))
-        .into_iter()
-        .collect();
+pub fn request(base: Url, params: &Parameters, contents: Vec<u8>) -> Result<Request, Error> {
+    let (mime, _) = gio::content_type_guess(None, &contents);
 
     let url = base
         .join("/_matrix/media/r0/upload")
@@ -37,7 +29,7 @@ pub fn request(
     Client::new()
         .post(url)
         .query(params)
-        .body(file)
-        .headers(header)
+        .body(contents)
+        .header(CONTENT_TYPE, mime.to_string())
         .build()
 }
diff --git a/fractal-matrix-api/src/r0/membership.rs b/fractal-matrix-api/src/r0/membership.rs
new file mode 100644
index 00000000..b20640bb
--- /dev/null
+++ b/fractal-matrix-api/src/r0/membership.rs
@@ -0,0 +1,3 @@
+pub mod invite_user;
+pub mod join_room_by_id_or_alias;
+pub mod leave_room;
diff --git a/fractal-matrix-api/src/r0/membership/invite_user.rs 
b/fractal-matrix-api/src/r0/membership/invite_user.rs
new file mode 100644
index 00000000..7bb8c07e
--- /dev/null
+++ b/fractal-matrix-api/src/r0/membership/invite_user.rs
@@ -0,0 +1,30 @@
+use crate::r0::AccessToken;
+use reqwest::Client;
+use reqwest::Error;
+use reqwest::Request;
+use ruma_identifiers::RoomId;
+use serde::Serialize;
+use url::Url;
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Parameters {
+    pub access_token: AccessToken,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Body {
+    pub user_id: String,
+}
+
+pub fn request(
+    base: Url,
+    room_id: &RoomId,
+    params: &Parameters,
+    body: &Body,
+) -> Result<Request, Error> {
+    let url = base
+        .join(&format!("/_matrix/client/r0/rooms/{}/invite", room_id))
+        .expect("Malformed URL in leave_room");
+
+    Client::new().post(url).query(params).json(body).build()
+}
diff --git a/fractal-matrix-api/src/r0/membership/join_room_by_id_or_alias.rs 
b/fractal-matrix-api/src/r0/membership/join_room_by_id_or_alias.rs
new file mode 100644
index 00000000..e6141c4a
--- /dev/null
+++ b/fractal-matrix-api/src/r0/membership/join_room_by_id_or_alias.rs
@@ -0,0 +1,31 @@
+use crate::r0::AccessToken;
+use crate::serde::host_list;
+use reqwest::Client;
+use reqwest::Error;
+use reqwest::Request;
+use ruma_identifiers::RoomIdOrAliasId;
+use serde::Serialize;
+use url::Host;
+use url::Url;
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Parameters {
+    pub access_token: AccessToken,
+    #[serde(with = "host_list")]
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub server_name: Vec<Host>,
+}
+
+// TODO: Implement Body
+
+pub fn request(
+    base: Url,
+    room_id_or_alias: &RoomIdOrAliasId,
+    params: &Parameters,
+) -> Result<Request, Error> {
+    let url = base
+        .join(&format!("/_matrix/client/r0/join/{}", room_id_or_alias))
+        .expect("Malformed URL in join_room_by_id_or_alias");
+
+    Client::new().post(url).query(params).build()
+}
diff --git a/fractal-matrix-api/src/r0/membership/leave_room.rs 
b/fractal-matrix-api/src/r0/membership/leave_room.rs
new file mode 100644
index 00000000..9a4a5297
--- /dev/null
+++ b/fractal-matrix-api/src/r0/membership/leave_room.rs
@@ -0,0 +1,20 @@
+use crate::r0::AccessToken;
+use reqwest::Client;
+use reqwest::Error;
+use reqwest::Request;
+use ruma_identifiers::RoomId;
+use serde::Serialize;
+use url::Url;
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Parameters {
+    pub access_token: AccessToken,
+}
+
+pub fn request(base: Url, room_id: &RoomId, params: &Parameters) -> Result<Request, Error> {
+    let url = base
+        .join(&format!("/_matrix/client/r0/rooms/{}/leave", room_id))
+        .expect("Malformed URL in leave_room");
+
+    Client::new().post(url).query(params).build()
+}
diff --git a/fractal-matrix-api/src/r0/sync.rs b/fractal-matrix-api/src/r0/sync.rs
index 78aeaf20..54e0463f 100644
--- a/fractal-matrix-api/src/r0/sync.rs
+++ b/fractal-matrix-api/src/r0/sync.rs
@@ -1 +1,2 @@
+pub mod get_joined_members;
 pub mod sync_events;
diff --git a/fractal-matrix-api/src/r0/sync/get_joined_members.rs 
b/fractal-matrix-api/src/r0/sync/get_joined_members.rs
new file mode 100644
index 00000000..e8776c39
--- /dev/null
+++ b/fractal-matrix-api/src/r0/sync/get_joined_members.rs
@@ -0,0 +1,38 @@
+use crate::r0::AccessToken;
+use crate::serde::option_url;
+use reqwest::Client;
+use reqwest::Error;
+use reqwest::Request;
+use ruma_identifiers::RoomId;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use url::Url;
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Parameters {
+    pub access_token: AccessToken,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct Response {
+    #[serde(default)]
+    pub joined: HashMap<String, RoomMember>,
+}
+
+#[derive(Clone, Debug, Deserialize)]
+pub struct RoomMember {
+    pub display_name: Option<String>,
+    #[serde(with = "option_url")]
+    pub avatar_url: Option<Url>,
+}
+
+pub fn request(base: Url, room_id: &RoomId, params: &Parameters) -> Result<Request, Error> {
+    let url = base
+        .join(&format!(
+            "/_matrix/client/r0/rooms/{}/joined_members",
+            room_id
+        ))
+        .expect("Malformed URL in get_joined_members");
+
+    Client::new().get(url).query(params).build()
+}
diff --git a/fractal-matrix-api/src/r0/tag.rs b/fractal-matrix-api/src/r0/tag.rs
new file mode 100644
index 00000000..0a3f29ca
--- /dev/null
+++ b/fractal-matrix-api/src/r0/tag.rs
@@ -0,0 +1,2 @@
+pub mod create_tag;
+pub mod delete_tag;
diff --git a/fractal-matrix-api/src/r0/tag/create_tag.rs b/fractal-matrix-api/src/r0/tag/create_tag.rs
new file mode 100644
index 00000000..e143d604
--- /dev/null
+++ b/fractal-matrix-api/src/r0/tag/create_tag.rs
@@ -0,0 +1,37 @@
+use crate::r0::AccessToken;
+use reqwest::Client;
+use reqwest::Error;
+use reqwest::Request;
+use ruma_identifiers::RoomId;
+use serde::Serialize;
+use url::Url;
+
+#[derive(Debug, Clone, Serialize)]
+pub struct Parameters {
+    pub access_token: AccessToken,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Body {
+    // TODO: Restrict values to the range [0.0, 1.0]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub order: Option<f64>,
+}
+
+pub fn request(
+    base: Url,
+    user_id: &str,
+    room_id: &RoomId,
+    tag: &str,
+    params: &Parameters,
+    body: &Body,
+) -> Result<Request, Error> {
+    let url = base
+        .join(&format!(
+            "/_matrix/client/r0/user/{}/rooms/{}/tags/{}",
+            user_id, room_id, tag
+        ))
+        .expect("Malformed URL in create_tag");
+
+    Client::new().put(url).query(params).json(body).build()
+}
diff --git a/fractal-matrix-api/src/r0/tag/delete_tag.rs b/fractal-matrix-api/src/r0/tag/delete_tag.rs
new file mode 100644
index 00000000..6c116725
--- /dev/null
+++ b/fractal-matrix-api/src/r0/tag/delete_tag.rs
@@ -0,0 +1,29 @@
+use crate::r0::AccessToken;
+use reqwest::Client;
+use reqwest::Error;
+use reqwest::Request;
+use ruma_identifiers::RoomId;
+use serde::Serialize;
+use url::Url;
+
+#[derive(Debug, Clone, Serialize)]
+pub struct Parameters {
+    pub access_token: AccessToken,
+}
+
+pub fn request(
+    base: Url,
+    user_id: &str,
+    room_id: &RoomId,
+    tag: &str,
+    params: &Parameters,
+) -> Result<Request, Error> {
+    let url = base
+        .join(&format!(
+            "/_matrix/client/r0/user/{}/rooms/{}/tags/{}",
+            user_id, room_id, tag
+        ))
+        .expect("Malformed URL in delete_tag");
+
+    Client::new().delete(url).query(params).build()
+}
diff --git a/fractal-matrix-api/src/r0/typing.rs b/fractal-matrix-api/src/r0/typing.rs
new file mode 100644
index 00000000..473a023d
--- /dev/null
+++ b/fractal-matrix-api/src/r0/typing.rs
@@ -0,0 +1,59 @@
+use crate::r0::AccessToken;
+use reqwest::Client;
+use reqwest::Error;
+use reqwest::Request;
+use ruma_identifiers::RoomId;
+use serde::ser::SerializeMap;
+use serde::Serialize;
+use serde::Serializer;
+use std::time::Duration;
+use url::Url;
+
+#[derive(Debug, Clone, Serialize)]
+pub struct Parameters {
+    pub access_token: AccessToken,
+}
+
+#[derive(Clone, Debug)]
+pub enum Body {
+    StopTyping,
+    Typing(Duration),
+}
+
+impl Serialize for Body {
+    fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        match self {
+            Body::StopTyping => {
+                let mut serialized_map = ser.serialize_map(Some(1))?;
+                serialized_map.serialize_entry("typing", &false)?;
+                serialized_map.end()
+            }
+            Body::Typing(dur) => {
+                let mut serialized_map = ser.serialize_map(Some(2))?;
+                serialized_map.serialize_entry("typing", &true)?;
+                serialized_map.serialize_entry("timeout", &dur.as_millis())?;
+                serialized_map.end()
+            }
+        }
+    }
+}
+
+pub fn request(
+    base: Url,
+    room_id: &RoomId,
+    user_id: &str,
+    params: &Parameters,
+    body: &Body,
+) -> Result<Request, Error> {
+    let url = base
+        .join(&format!(
+            "/_matrix/client/r0/rooms/{}/typing/{}",
+            room_id, user_id,
+        ))
+        .expect("Malformed URL in typing");
+
+    Client::new().put(url).query(params).json(body).build()
+}
diff --git a/fractal-matrix-api/src/serde.rs b/fractal-matrix-api/src/serde.rs
index 3076ae8e..47a02df0 100644
--- a/fractal-matrix-api/src/serde.rs
+++ b/fractal-matrix-api/src/serde.rs
@@ -109,15 +109,26 @@ pub mod option_host {
     }
 }
 
+pub mod host_list {
+    use serde::ser::Serializer;
+    use url::Host;
+
+    pub fn serialize<S>(host_list: &[Host], ser: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        ser.collect_seq(host_list.iter().map(ToString::to_string))
+    }
+}
+
 pub mod duration_as_millis {
     use serde::Serializer;
     use std::time::Duration;
 
-    // TODO: use as_millis when duration_as_u128 is stable
     pub fn serialize<S>(duration: &Duration, ser: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
     {
-        ser.serialize_u64(duration.as_secs() * 1000 + (duration.subsec_millis() as u64))
+        ser.serialize_u64(duration.as_millis() as u64)
     }
 }
diff --git a/fractal-matrix-api/src/util.rs b/fractal-matrix-api/src/util.rs
index c3aa28bb..47db4fdb 100644
--- a/fractal-matrix-api/src/util.rs
+++ b/fractal-matrix-api/src/util.rs
@@ -364,7 +364,7 @@ pub fn scalar_url(base: &Url, path: &str, params: &[(&str, String)]) -> Result<U
     build_url(base, &format!("api/{}", path), params)
 }
 
-pub fn media_url(base: &Url, path: &str, params: &[(&str, String)]) -> Result<Url, Error> {
+fn media_url(base: &Url, path: &str, params: &[(&str, String)]) -> Result<Url, Error> {
     build_url(base, &format!("/_matrix/media/r0/{}", path), params)
 }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]