#[macro_use]
extern crate serde_derive;
extern crate reqwest;
extern crate serde_json;
#[macro_use]
extern crate url;
pub mod matrix_types;
use serde_json::Value;
use std::collections::HashMap;
use std::rc::Rc;
use url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET, USERINFO_ENCODE_SET};
define_encode_set! {
#[doc(hidden)]
pub ACCESS_TOKEN_ENCODE_SET = [USERINFO_ENCODE_SET] | {
'%', '&'
}
}
#[derive(Deserialize, Debug)]
struct JoinInfo {
room_id: String,
}
#[derive(Deserialize, Debug)]
struct SinceInfo {
next_batch: String,
}
#[derive(Deserialize, Debug)]
struct AccesstokenInfo {
access_token: String,
}
#[derive(Deserialize, Debug, Clone)]
struct ServerInfo {
server_name: String,
access_token: String,
}
#[doc(hidden)]
pub struct HomeserverBuilder<Username, Password, AccessToken> {
server: String,
username: Username,
password: Password,
access_token: AccessToken,
}
pub struct Homeserver {
client: Rc<reqwest::Client>,
info: ServerInfo,
}
pub struct Room {
id: String,
latest_since: Option<String>,
client: Rc<reqwest::Client>,
info: ServerInfo,
}
pub enum Message {
Text(String),
Emote(String),
Notice(String),
Image { body: String, url: String },
File { body: String, url: String },
Location { body: String, geo_uri: String },
Video { body: String, url: String },
Audio { body: String, url: String },
}
pub enum RoomEvent {
Message(Message),
Name(String),
Topic(String),
Avatar { url: String },
}
impl HomeserverBuilder<(), (), ()> {
pub fn access_token(self, access_token: &str) -> HomeserverBuilder<(), (), String> {
HomeserverBuilder {
server: self.server,
username: self.username,
password: self.password,
access_token: access_token.to_owned(),
}
}
}
impl<T> HomeserverBuilder<(), T, ()> {
pub fn username(self, username: &str) -> HomeserverBuilder<String, T, ()> {
HomeserverBuilder {
server: self.server,
username: username.to_owned(),
password: self.password,
access_token: (),
}
}
}
impl<T> HomeserverBuilder<T, (), ()> {
pub fn password(self, password: &str) -> HomeserverBuilder<T, String, ()> {
HomeserverBuilder {
server: self.server,
username: self.username,
password: password.to_owned(),
access_token: (),
}
}
}
impl HomeserverBuilder<String, String, ()> {
pub fn login(self) -> HomeserverBuilder<String, String, String> {
let at_info: AccesstokenInfo = {
let client = reqwest::Client::new();
let mut res = client
.get(&format!("{}/_matrix/client/r0/login", self.server))
.send()
.unwrap();
let mut pwlogin: bool = false;
let v: Value = serde_json::from_str(&(res.text().unwrap())).unwrap();
match v["flows"].as_array() {
Some(flowlist) => for flow in flowlist {
match flow["type"].as_str() {
Some("m.login.password") => {
println!("Found login option `m.login.password`");
pwlogin = true;
}
_ => (),
}
},
_ => (),
}
if !pwlogin {
panic!("Server does not offer the login option `m.login.password`")
}
let mut map: HashMap<&str, &str> = HashMap::new();
map.insert("type", "m.login.password");
map.insert("user", &self.username);
map.insert("password", &self.password);
let mut res = client
.post(&format!("{}/_matrix/client/r0/login", self.server))
.json(&map)
.send()
.unwrap();
res.json().unwrap()
};
HomeserverBuilder {
server: self.server,
username: self.username,
password: self.password,
access_token: at_info.access_token,
}
}
}
impl<T, R> HomeserverBuilder<T, R, String> {
pub fn connect(self) -> Homeserver {
Homeserver {
client: Rc::new(reqwest::Client::new()),
info: ServerInfo {
server_name: self.server,
access_token: self.access_token,
},
}
}
}
impl Homeserver {
pub fn new(server_url: &str) -> HomeserverBuilder<(), (), ()> {
HomeserverBuilder {
server: server_url.to_owned(),
username: (),
password: (),
access_token: (),
}
}
pub fn connect(server_url: &str, access_token: &str) -> Self {
Self::new(server_url).access_token(access_token).connect()
}
pub fn login_and_connect(server_url: &str, username: &str, password: &str) -> Self {
Self::new(server_url)
.username(username)
.password(password)
.login()
.connect()
}
pub fn get_access_token(&self) -> String {
return self.info.access_token.clone();
}
pub fn join_room(&self, room_name: String) -> Option<Room> {
let map: HashMap<String, String> = HashMap::new();
let mut res = self
.client
.post(&format!(
"{}{}{}{}{}",
self.info.server_name,
"/_matrix/client/r0/join/",
utf8_percent_encode(&room_name, PATH_SEGMENT_ENCODE_SET).to_string(),
"?access_token=",
utf8_percent_encode(&self.info.access_token, ACCESS_TOKEN_ENCODE_SET).to_string()
)).json(&map)
.send()
.unwrap();
let info: Result<JoinInfo, _> = res.json();
match info {
Ok(info) => Some(Room {
id: info.room_id,
latest_since: None,
client: self.client.clone(),
info: self.info.clone(),
}),
_ => None,
}
}
pub fn create_room(&self, room_name: String) -> Option<Room> {
let mut map: HashMap<String, String> = HashMap::new();
map.insert("room_alias_name".to_owned(), room_name);
map.insert("preset".to_owned(), "public_chat".to_owned());
let mut res = self
.client
.post(&format!(
"{}/_matrix/client/r0/createRoom?access_token={}",
self.info.server_name,
utf8_percent_encode(&self.info.access_token, ACCESS_TOKEN_ENCODE_SET).to_string()
)).json(&map)
.send()
.unwrap();
let info: Result<JoinInfo, _> = res.json();
match info {
Ok(info) => Some(Room {
id: info.room_id,
latest_since: None,
client: self.client.clone(),
info: self.info.clone(),
}),
_ => None,
}
}
pub fn get_invites(&mut self) -> Vec<String> {
let res = self
.client
.get(&format!(
"{}/_matrix/client/r0/sync?filter={{\"room\":{{\"rooms\":[]}}}}&access_token={}",
self.info.server_name,
utf8_percent_encode(&self.info.access_token, ACCESS_TOKEN_ENCODE_SET).to_string()
)).send();
match res {
Ok(mut res) => {
let mut vec = Vec::new();
let v: Value = serde_json::from_str(&(res.text().unwrap())).unwrap();
match v["rooms"]["invite"].as_object() {
Some(invitelist) => for (room, info) in invitelist {
for event in info["invite_state"]["events"].as_array().unwrap() {
if event["membership"].as_str() == Some("invite") {
vec.push(room.to_owned());
}
}
},
_ => (),
}
vec
}
_ => Vec::new(),
}
}
}
extern crate rand;
impl Room {
pub fn get_new_messages(&mut self) -> Vec<RoomEvent> {
let res = match self.latest_since.clone() {
None => self.client.get(
&format!("{}/_matrix/client/r0/sync?filter={{\"room\":{{\"rooms\":[\"{}\"],\"timeline\":{{\"limit\":0}}}}}}&access_token={}",
self.info.server_name,
utf8_percent_encode(&self.id, PATH_SEGMENT_ENCODE_SET).to_string(),
utf8_percent_encode(&self.info.access_token, ACCESS_TOKEN_ENCODE_SET).to_string()))
.send(),
Some(since) => self.client.get(
&format!("{}/_matrix/client/r0/sync?since={}&filter={{\"room\":{{\"rooms\":[\"{}\"]}}}}&access_token={}",
self.info.server_name,
since,
utf8_percent_encode(&self.id, PATH_SEGMENT_ENCODE_SET).to_string(),
utf8_percent_encode(&self.info.access_token, ACCESS_TOKEN_ENCODE_SET).to_string()))
.send()
};
match res {
Ok(mut res) => {
let mut vec = Vec::new();
let v: Value = serde_json::from_str(&(res.text().unwrap())).unwrap();
self.latest_since = Some(v["next_batch"].as_str().unwrap().to_owned());
match v["rooms"]["join"][&self.id]["timeline"]["events"].as_array() {
Some(eventlist) => for event in eventlist {
match event["content"]["msgtype"].as_str() {
Some("m.text") => vec.push(RoomEvent::Message(Message::Text(
event["content"]["body"].as_str().unwrap().to_owned(),
))),
Some("m.emote") => vec.push(RoomEvent::Message(Message::Emote(
event["content"]["body"].as_str().unwrap().to_owned(),
))),
Some("m.notice") => vec.push(RoomEvent::Message(Message::Notice(
event["content"]["body"].as_str().unwrap().to_owned(),
))),
Some("m.image") => vec.push(RoomEvent::Message(Message::Image {
body: event["content"]["body"].as_str().unwrap().to_owned(),
url: event["content"]["url"].as_str().unwrap().to_owned(),
})),
Some("m.file") => vec.push(RoomEvent::Message(Message::File {
body: event["content"]["body"].as_str().unwrap().to_owned(),
url: event["content"]["url"].as_str().unwrap().to_owned(),
})),
Some("m.location") => vec.push(RoomEvent::Message(Message::Location {
body: event["content"]["body"].as_str().unwrap().to_owned(),
geo_uri: event["content"]["geo_uri"].as_str().unwrap().to_owned(),
})),
Some("m.video") => vec.push(RoomEvent::Message(Message::Audio {
body: event["content"]["body"].as_str().unwrap().to_owned(),
url: event["content"]["url"].as_str().unwrap().to_owned(),
})),
Some("m.audio") => vec.push(RoomEvent::Message(Message::Audio {
body: event["content"]["body"].as_str().unwrap().to_owned(),
url: event["content"]["url"].as_str().unwrap().to_owned(),
})),
_ => (),
}
},
_ => (),
}
vec
}
_ => Vec::new(),
}
}
pub fn send_message(&self, message: Message) {
let mut map: HashMap<String, String> = HashMap::new();
match message {
Message::Text(text) => {
map.insert("msgtype".to_owned(), "m.text".to_owned());
map.insert("body".to_owned(), text);
}
Message::Emote(text) => {
map.insert("msgtype".to_owned(), "m.emote".to_owned());
map.insert("body".to_owned(), text);
}
Message::Notice(text) => {
map.insert("msgtype".to_owned(), "m.notice".to_owned());
map.insert("body".to_owned(), text);
}
Message::Image { body, url } => {
map.insert("msgtype".to_owned(), "m.image".to_owned());
map.insert("body".to_owned(), body);
map.insert("url".to_owned(), url);
}
Message::File { body, url } => {
map.insert("msgtype".to_owned(), "m.image".to_owned());
map.insert("body".to_owned(), body);
map.insert("url".to_owned(), url);
}
Message::Location { body, geo_uri } => {
map.insert("msgtype".to_owned(), "m.image".to_owned());
map.insert("body".to_owned(), body);
map.insert("geo_uri".to_owned(), geo_uri);
}
Message::Audio { body, url } => {
map.insert("msgtype".to_owned(), "m.image".to_owned());
map.insert("body".to_owned(), body);
map.insert("url".to_owned(), url);
}
Message::Video { body, url } => {
map.insert("msgtype".to_owned(), "m.image".to_owned());
map.insert("body".to_owned(), body);
map.insert("url".to_owned(), url);
}
}
self.client
.put(&format!(
"{}/_matrix/client/r0/rooms/{}/send/m.room.message/{}?access_token={}",
self.info.server_name,
utf8_percent_encode(&self.id, PATH_SEGMENT_ENCODE_SET).to_string(),
rand::random::<u64>(),
utf8_percent_encode(&self.info.access_token, ACCESS_TOKEN_ENCODE_SET).to_string()
)).json(&map)
.send()
.unwrap();
}
pub fn invite(&self, user_id: &str) {
let mut map: HashMap<&str, &str> = HashMap::new();
map.insert("user_id", user_id);
self.client
.put(&format!(
"{}/_matrix/client/r0/rooms/{}/invite?access_token={}",
self.info.server_name,
utf8_percent_encode(&self.id, PATH_SEGMENT_ENCODE_SET).to_string(),
utf8_percent_encode(&self.info.access_token, ACCESS_TOKEN_ENCODE_SET).to_string()
)).json(&map)
.send()
.unwrap();
}
pub fn send_text(&self, text: String) {
self.send_message(Message::Text(text));
}
pub fn send_emote(&self, text: String) {
self.send_message(Message::Emote(text));
}
pub fn send_notice(&self, text: String) {
self.send_message(Message::Notice(text));
}
}