daydream

A small matrix web client written in rust
git clone git://archive.git.mtrnord.blog/daydream-mx/daydream.git
Log | Files | Refs | README | LICENSE

commit f9049009d7ad736ac234ec5ea6425b12fccb911c
parent 20c615e0afcb9aff2630d330e6034d7fdc668183
Author: Marcel <mtrnord1@gmail.com>
Date:   Sat,  6 Jun 2020 22:19:59 +0200

Display error on missing field, Handle timeouts and do 10 tries before failing (with 5 seconds timeout in between each try)

Took 1 hour 3 minutes

Diffstat:
Msrc/app/matrix/mod.rs | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/app/views/login.rs | 300++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/app/views/main_view.rs | 5+++--
Msrc/errors.rs | 37+++++++++++++++++++++++++++++++++----
Mstatic/custom-css.scss | 4++++
5 files changed, 342 insertions(+), 129 deletions(-)

diff --git a/src/app/matrix/mod.rs b/src/app/matrix/mod.rs @@ -3,7 +3,10 @@ use matrix_sdk::{ api::r0::{filter::RoomEventFilter, message::get_message_events::Direction}, events::{ collections::all::RoomEvent, - room::message::{MessageEvent, MessageEventContent, TextMessageEventContent, FormattedBody, MessageFormat}, + room::message::{ + FormattedBody, MessageEvent, MessageEventContent, MessageFormat, + TextMessageEventContent, + }, EventJson, }, identifiers::RoomId, @@ -23,7 +26,7 @@ use yew::worker::*; use crate::app::matrix::types::{get_media_download_url, get_video_media_download_url}; use crate::constants::AUTH_KEY; -use crate::errors::MatrixError; +use crate::errors::{Field, MatrixError}; use pulldown_cmark::{html, Options, Parser}; mod sync; @@ -60,7 +63,7 @@ pub enum Request { SetHomeserver(String), SetUsername(String), SetPassword(String), - Login(), + Login, GetLoggedIn, GetUserdata, GetOldMessages((RoomId, Option<String>)), @@ -140,7 +143,8 @@ impl Agent for MatrixAgent { Request::SetPassword(password) => { self.matrix_state.password = Some(password); } - Request::Login() => { + Request::Login => { + info!("Starting Login"); let login_client = self.login(); if login_client.is_none() { for sub in self.subscribers.iter() { @@ -169,23 +173,62 @@ impl Agent for MatrixAgent { client.restore_login(session).await; } else { // FIXME gracefully handle login errors - let login_response: matrix_sdk::api::r0::session::login::Response = client + let login_response = client .login(username, password, None, Some("Daydream".to_string())) - .await - .unwrap(); - let session_store = SessionStore { - access_token: login_response.access_token, - user_id: login_response.user_id.to_string(), - device_id: login_response.device_id, - homeserver_url: client.homeserver().to_string(), - }; - let mut storage = agent.storage.lock().unwrap(); - storage.store(AUTH_KEY, Json(&session_store)); - } - - for sub in subscribers.iter() { - let resp = Response::LoggedIn(true); - agent.link.respond(*sub, resp); + .await; + match login_response { + Ok(login_response) => { + let session_store = SessionStore { + access_token: login_response.access_token, + user_id: login_response.user_id.to_string(), + device_id: login_response.device_id, + homeserver_url: client.homeserver().to_string(), + }; + let mut storage = agent.storage.lock().unwrap(); + storage.store(AUTH_KEY, Json(&session_store)); + + for sub in subscribers.iter() { + let resp = Response::LoggedIn(true); + agent.link.respond(*sub, resp); + } + } + Err(e) => { + if let matrix_sdk::Error::Reqwest(e) = e { + match e.status() { + None => { + for sub in subscribers.iter() { + let resp = Response::Error(MatrixError::SDKError( + e.to_string(), + )); + agent.link.respond(*sub, resp); + } + } + Some(v) => { + if v.is_server_error() { + for sub in subscribers.iter() { + let resp = + Response::Error(MatrixError::LoginTimeout); + agent.link.respond(*sub, resp); + } + } else { + for sub in subscribers.iter() { + let resp = Response::Error( + MatrixError::SDKError(e.to_string()), + ); + agent.link.respond(*sub, resp); + } + } + } + } + } else { + for sub in subscribers.iter() { + let resp = + Response::Error(MatrixError::SDKError(e.to_string())); + agent.link.respond(*sub, resp); + } + } + } + } } }); } @@ -292,7 +335,9 @@ impl Agent for MatrixAgent { for event in deserialized_events.into_iter().rev() { if let RoomEvent::RoomMessage(mut event) = event { - if let MessageEventContent::Image(mut image_event) = event.clone().content { + if let MessageEventContent::Image(mut image_event) = + event.clone().content + { if image_event.url.is_some() { let new_url = Some(get_media_download_url( agent.matrix_client.clone().unwrap(), @@ -381,10 +426,10 @@ impl Agent for MatrixAgent { MessageEventContent::Text(TextMessageEventContent { body: message, relates_to: None, - formatted: Some(FormattedBody{ + formatted: Some(FormattedBody { body: formatted_message, format: MessageFormat::Html, - }) + }), }) }; client.room_send(&room_id, content, None).await; @@ -419,18 +464,9 @@ impl MatrixAgent { } fn login(&mut self) -> Option<Client> { - if (self.matrix_state.homeserver.is_none() - || self.matrix_state.username.is_none() - || self.matrix_state.password.is_none() - || self.matrix_client.is_some()) - && self.session.is_none() - { - for sub in self.subscribers.iter() { - let resp = Response::Error(MatrixError::MissingFields); - self.link.respond(*sub, resp); - } - None - } else if self.session.is_some() { + info!("preparing client"); + if self.session.is_some() { + info!("restoring login"); let homeserver = self.session.clone().unwrap().homeserver_url; let client_config = ClientConfig::new(); @@ -452,7 +488,26 @@ impl MatrixAgent { }); Some(client) + } else if self.matrix_state.homeserver.is_none() { + for sub in self.subscribers.iter() { + let resp = Response::Error(MatrixError::MissingFields(Field::Homeserver)); + self.link.respond(*sub, resp); + } + None + } else if self.matrix_state.username.is_none() { + for sub in self.subscribers.iter() { + let resp = Response::Error(MatrixError::MissingFields(Field::MXID)); + self.link.respond(*sub, resp); + } + None + } else if self.matrix_state.password.is_none() { + for sub in self.subscribers.iter() { + let resp = Response::Error(MatrixError::MissingFields(Field::Password)); + self.link.respond(*sub, resp); + } + None } else { + info!("new login"); let homeserver = self.matrix_state.homeserver.clone().unwrap(); let client_config = ClientConfig::new(); diff --git a/src/app/views/login.rs b/src/app/views/login.rs @@ -1,37 +1,58 @@ -use yew::agent::{Dispatched, Dispatcher}; use yew::prelude::*; +use serde::{Deserialize, Serialize}; +use log::*; use tr::tr; -use crate::app::matrix::{MatrixAgent, Request}; +use crate::app::matrix::{MatrixAgent, Request, Response}; +use crate::errors::{MatrixError, Field}; +use wasm_bindgen::__rt::std::thread::sleep; +use wasm_bindgen::__rt::core::time::Duration; pub struct Login { link: ComponentLink<Self>, - homeserver: String, - username: String, - password: String, - matrix_agent: Dispatcher<MatrixAgent>, + state: State, + matrix_agent: Box<dyn Bridge<MatrixAgent>>, } pub enum Msg { + NewMessage(Response), SetHomeserver(String), SetUsername(String), SetPassword(String), Login, } +#[derive(Serialize, Deserialize, Default)] +pub struct State { + loading: bool, + homeserver: String, + username: String, + password: String, + error: Option<String>, + error_field: Option<Field>, + retries: u64 +} + impl Component for Login { type Message = Msg; type Properties = (); fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self { - let matrix_agent = MatrixAgent::dispatcher(); - Login { - link, - //TODO use state + let matrix_callback = link.callback(Msg::NewMessage); + let matrix_agent = MatrixAgent::bridge(matrix_callback); + let state = State { + loading: false, homeserver: "".to_string(), username: "".to_string(), password: "".to_string(), + error: None, + error_field: None, + retries: 0 + }; + Login { + link, + state, matrix_agent, } } @@ -39,23 +60,66 @@ impl Component for Login { fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { Msg::SetHomeserver(homeserver) => { - self.homeserver = homeserver.clone(); + self.state.homeserver = homeserver.clone(); self.matrix_agent.send(Request::SetHomeserver(homeserver)); true } Msg::SetUsername(username) => { - self.username = username.clone(); + self.state.username = username.clone(); self.matrix_agent.send(Request::SetUsername(username)); true } Msg::SetPassword(password) => { - self.password = password.clone(); + self.state.password = password.clone(); self.matrix_agent.send(Request::SetPassword(password)); true } Msg::Login => { - self.matrix_agent.send(Request::Login()); - false + // Reset Errors + self.state.error = None; + self.state.error_field = None; + + // Start loading + self.matrix_agent.send(Request::Login); + self.state.loading = true; + true + } + Msg::NewMessage(response) => { + match response { + Response::Error(error) => { + match error.clone() { + MatrixError::MissingFields(field) => { + self.state.loading = false; + self.state.error = Some(error.to_string()); + self.state.error_field = Some(field); + true + }, + MatrixError::LoginTimeout => { + // If we had less than 10 tries try again + if self.state.retries < 10 { + self.state.retries += 1; + info!("Trying login again in 5 seconds"); + sleep(Duration::from_secs(5)); + self.link.send_message(Msg::Login); + false + } else { + self.state.loading = false; + self.state.error = Some("Login failed after 10 tries.".to_string()); + true + } + }, + MatrixError::SDKError(e) => { + // TODO handle login error != timeout better + error!("SDK Error: {}", e); + false + } + _ => { + false + } + } + } + _ => false + } } } } @@ -64,105 +128,165 @@ impl Component for Login { false } + //noinspection RsTypeCheck fn view(&self) -> Html { - html! { - <div class="container"> - <div class="uk-position-center uk-padding"> - <h1 class="title">{"Login"}</h1> - <form class="uk-form-stacked uk-margin" onsubmit=self.link.callback(|e: FocusEvent| {e.prevent_default(); Msg::Login})> - <div class="uk-margin"> - <label class="uk-form-label"> - { - tr!( - // The URL Field of the Login page - "Homeserver URL" - ) + let mut homeserver_classes = "uk-input"; + let mut mxid_classes = "uk-input"; + let mut password_classes = "uk-input"; + match self.state.error_field.as_ref() { + Some(v) => { + match v { + Field::Homeserver => { + homeserver_classes = "uk-input uk-form-danger" + }, + Field::MXID => { + mxid_classes = "uk-input uk-form-danger" + }, + Field::Password => { + password_classes = "uk-input uk-form-danger" + }, + } + }, + _ => {} + } + + if self.state.loading { + html! { + <div class="container"> + <div class="uk-position-center uk-padding"> + <span uk-spinner="ratio: 4.5"></span> + </div> + </div> + } + } else { + html! { + <div class="container"> + <div class="uk-position-center uk-padding"> + <h1 class="title"> + { + tr!( + // The Login Button of the Login page + "Login" + ) + } + </h1> + { + match &self.state.error { + Some(v) => { + html! { + <h3 class="error"> + { + tr!( + // {0} is the Error that happened on login + // The error message of the Login page + "Error: {0}", + v + ) + } + </h3> + } + } + None => { + html!{} } - </label> - <div class="uk-form-controls"> - <input - class="uk-input" - type="url" - id="homeserver" - placeholder= + } + } + + <form class="uk-form-stacked uk-margin" onsubmit=self.link.callback(|e: FocusEvent| {e.prevent_default(); Msg::Login})> + <div class="uk-margin"> + <label class="uk-form-label"> { tr!( // The URL Field of the Login page "Homeserver URL" ) } - value=&self.homeserver - oninput=self.link.callback(|e: InputData| Msg::SetHomeserver(e.value)) - /> + </label> + <div class="uk-form-controls"> + <input + class=homeserver_classes + type="url" + id="homeserver" + placeholder= + { + tr!( + // The URL Field of the Login page + "Homeserver URL" + ) + } + value=&self.state.homeserver + oninput=self.link.callback(|e: InputData| Msg::SetHomeserver(e.value)) + /> + </div> </div> - </div> - <div class="uk-margin"> - <label class="uk-form-label"> - { - tr!( - // The Matrix ID Field of the Login page - "MXID" - ) - } - </label> - <div class="uk-form-controls"> - <input - class="uk-input" - id="username" - placeholder= + <div class="uk-margin"> + <label class="uk-form-label"> { tr!( // The Matrix ID Field of the Login page "MXID" ) } - value=&self.username - oninput=self.link.callback(|e: InputData| Msg::SetUsername(e.value)) - /> + </label> + <div class="uk-form-controls"> + <input + class=mxid_classes + id="username" + placeholder= + { + tr!( + // The Matrix ID Field of the Login page + "MXID" + ) + } + value=&self.state.username + oninput=self.link.callback(|e: InputData| Msg::SetUsername(e.value)) + /> + </div> </div> - </div> - <div class="uk-margin"> - <label class="uk-form-label"> - { - tr!( - // The Password Field of the Login page - "Password" - ) - } - </label> - <div class="uk-form-controls"> - <input - class="uk-input" - type="password" - id="password" - placeholder= + <div class="uk-margin"> + <label class="uk-form-label"> { tr!( // The Password Field of the Login page "Password" ) } - value=&self.password - oninput=self.link.callback(|e: InputData| Msg::SetPassword(e.value)) - /> + </label> + <div class="uk-form-controls"> + <input + class=password_classes + type="password" + id="password" + placeholder= + { + tr!( + // The Password Field of the Login page + "Password" + ) + } + value=&self.state.password + oninput=self.link.callback(|e: InputData| Msg::SetPassword(e.value)) + /> + </div> </div> - </div> - <div class="uk-margin"> - <div class="uk-form-controls"> - <button class="uk-button uk-button-primary"> - { - tr!( - // The Login Button of the Login page - "Login" - ) - } - </button> + <div class="uk-margin"> + <div class="uk-form-controls"> + <button class="uk-button uk-button-primary"> + { + tr!( + // The Login Button of the Login page + "Login" + ) + } + </button> + </div> </div> - </div> - </form> + </form> + </div> </div> - </div> + } } } } diff --git a/src/app/views/main_view.rs b/src/app/views/main_view.rs @@ -1,7 +1,8 @@ +use log::*; use matrix_sdk::Room; use serde::{Deserialize, Serialize}; -use yew::prelude::*; use yew::ComponentLink; +use yew::prelude::*; use crate::app::components::{event_list::EventList, room_list::RoomList}; use crate::app::matrix::{MatrixAgent, Request}; @@ -39,6 +40,7 @@ impl Component for MainView { fn update(&mut self, msg: Self::Message) -> bool { match msg { Msg::ChangeRoom(room) => { + info!("Changing room to: {}", room.room_id); self.state.current_room = Some(room); } } @@ -82,7 +84,6 @@ impl Component for MainView { </div> } } - } } } diff --git a/src/errors.rs b/src/errors.rs @@ -1,10 +1,39 @@ -use serde::{Deserialize, Serialize}; use thiserror::Error; +use std::fmt; +use serde::{Serialize, Deserialize}; -#[derive(Error, Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Field { + Homeserver, + MXID, + Password, +} + +impl fmt::Display for Field { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Field::Homeserver => write!(f, "Homeserver Field"), + Field::MXID => write!(f, "MXID Field"), + Field::Password => write!(f, "Password Field"), + } + } +} + +// TODO figure out a way to translate this +#[derive(Error, Debug, Clone)] pub enum MatrixError { #[error("No Matrix Client is available yet")] MissingClient, - #[error("Missing required Data")] - MissingFields, + #[error("Missing required Data in {0}")] + MissingFields(Field), + + /// An error occurred in the Matrix client library. + /// This can't use transparent as we need Clone + #[error("A Timeout happened on Login")] + LoginTimeout, + + /// An error occurred in the Matrix client library. + /// This can't use transparent as we need Clone + #[error("An error occurred in the Matrix client library: `{0}`")] + SDKError(String), } diff --git a/static/custom-css.scss b/static/custom-css.scss @@ -85,3 +85,7 @@ em { .uk-nav-default > li > a { color: $active-menu !important; } + +.error { + color: red; +}