diff --git a/Cargo.toml b/Cargo.toml index 4c68f6c..289c3e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ async-trait = "0.1" crossbeam-channel = "0.4" dirs-next = "2.0" futures = "0.3" -iced = { version = "0.2", features = ["debug", "tokio_old"] } +iced = { version = "0.2", features = ["debug", "tokio_old", "image"] } iced_futures = "0.2" hostname = "0.3" matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", rev = "e65915e" } @@ -20,3 +20,4 @@ matrix-sdk-common-macros = { git = "https://github.com/matrix-org/matrix-rust-sd serde = { version = "1.0", features = ["derive"] } tokio = { version = "0.2", features = ["sync"] } toml = "0.5" +tracing-subscriber = { version = "0.2", features = ["parking_lot"] } diff --git a/src/main.rs b/src/main.rs index 566c985..1cfad68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,8 @@ pub mod matrix; pub mod ui; fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + let config_dir = dirs::config_dir().unwrap().join("retrix"); // Make sure config dir exists and is not accessible by other users. if !config_dir.is_dir() { diff --git a/src/matrix.rs b/src/matrix.rs index 4c7a4df..5b9505e 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -1,6 +1,13 @@ +use std::time::Duration; + use matrix_sdk::{ - events::AnyRoomEvent, events::AnySyncRoomEvent, identifiers::DeviceId, identifiers::UserId, - reqwest::Url, Client, ClientConfig, LoopCtrl, SyncSettings, + api::r0::{account::register::Request as RegistrationRequest, uiaa::AuthData}, + events::AnyRoomEvent, + events::AnySyncRoomEvent, + identifiers::DeviceId, + identifiers::UserId, + reqwest::Url, + Client, ClientConfig, LoopCtrl, SyncSettings, }; use serde::{Deserialize, Serialize}; @@ -25,6 +32,53 @@ impl From for matrix_sdk::Session { } } +pub async fn signup( + username: &str, + password: &str, + server: &str, +) -> Result<(Client, Session), Error> { + let url = Url::parse(server)?; + let client = client(url)?; + + let mut request = RegistrationRequest::new(); + request.username = Some(username); + request.password = Some(password); + request.initial_device_display_name = Some("retrix"); + request.inhibit_login = false; + + // Get UIAA session key + let uiaa = match client.register(request.clone()).await { + Err(e) => match e.uiaa_response().cloned() { + Some(uiaa) => uiaa, + None => return Err(anyhow::anyhow!("Missing UIAA response")), + }, + Ok(_) => { + return Err(anyhow::anyhow!("Missing UIAA response")); + } + }; + // Get the first step in the authentication flow (we're ignoring the rest) + let stages = uiaa.flows.get(0); + let kind = stages.and_then(|flow| flow.stages.get(0)).cloned(); + + // Set authentication data, fallback to password type + request.auth = Some(AuthData::DirectRequest { + kind: kind.as_deref().unwrap_or("m.login.password"), + session: uiaa.session.as_deref(), + auth_parameters: Default::default(), + }); + + let response = client.register(request).await?; + + let session = Session { + access_token: response.access_token.unwrap(), + user_id: response.user_id, + device_id: response.device_id.unwrap(), + homeserver: server.to_owned(), + }; + + Ok((client, session)) +} + /// Login with credentials, creating a new authentication session pub async fn login( username: &str, @@ -98,12 +152,13 @@ fn write_session(session: &Session) -> Result<(), Error> { pub struct MatrixSync { client: matrix_sdk::Client, + join: Option>, //id: String, } impl MatrixSync { pub fn subscription(client: matrix_sdk::Client) -> iced::Subscription { - iced::Subscription::from_recipe(MatrixSync { client }) + iced::Subscription::from_recipe(MatrixSync { client, join: None }) } } @@ -136,41 +191,48 @@ where } fn stream( - self: Box, + mut self: Box, _input: iced_futures::BoxStream, ) -> iced_futures::BoxStream { let (sender, receiver) = tokio::sync::mpsc::unbounded_channel(); let client = self.client.clone(); - tokio::task::spawn(async move { + let join = tokio::task::spawn(async move { client - .sync_with_callback(SyncSettings::new(), |response| async { - for (room_id, room) in response.rooms.join { - for event in room.timeline.events { - if let Ok(event) = event.deserialize() { - let room_id = room_id.clone(); - let event = match event { - AnySyncRoomEvent::Message(e) => { - AnyRoomEvent::Message(e.into_full_event(room_id)) - } - AnySyncRoomEvent::State(e) => { - AnyRoomEvent::State(e.into_full_event(room_id)) - } - AnySyncRoomEvent::RedactedMessage(e) => { - AnyRoomEvent::RedactedMessage(e.into_full_event(room_id)) - } - AnySyncRoomEvent::RedactedState(e) => { - AnyRoomEvent::RedactedState(e.into_full_event(room_id)) - } - }; - sender.send(event).ok(); + .sync_with_callback( + SyncSettings::new().timeout(Duration::from_secs(90)), + |response| async { + for (room_id, room) in response.rooms.join { + for event in room.timeline.events { + if let Ok(event) = event.deserialize() { + let room_id = room_id.clone(); + let event = match event { + AnySyncRoomEvent::Message(e) => { + AnyRoomEvent::Message(e.into_full_event(room_id)) + } + AnySyncRoomEvent::State(e) => { + AnyRoomEvent::State(e.into_full_event(room_id)) + } + AnySyncRoomEvent::RedactedMessage(e) => { + AnyRoomEvent::RedactedMessage( + e.into_full_event(room_id), + ) + } + AnySyncRoomEvent::RedactedState(e) => { + AnyRoomEvent::RedactedState(e.into_full_event(room_id)) + } + }; + sender.send(event).ok(); + } } } - } - LoopCtrl::Continue - }) + LoopCtrl::Continue + }, + ) .await; + println!("We stopped syncing!"); }); + self.join = Some(join); Box::pin(receiver) } } diff --git a/src/ui.rs b/src/ui.rs index 56cde8c..cef40bc 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -2,13 +2,13 @@ use std::collections::{BTreeMap, HashMap}; use iced::{ text_input::{self, TextInput}, - Application, Button, Column, Command, Container, Element, Length, Row, Rule, Scrollable, + Application, Button, Column, Command, Container, Element, Length, Radio, Row, Rule, Scrollable, Subscription, Text, }; use matrix_sdk::{ events::{ - room::message::MessageEventContent, AnyPossiblyRedactedSyncMessageEvent, AnyRoomEvent, - AnySyncMessageEvent, + room::message::MessageEventContent, AnyMessageEventContent, + AnyPossiblyRedactedSyncMessageEvent, AnyRoomEvent, AnySyncMessageEvent, }, identifiers::RoomId, }; @@ -26,6 +26,7 @@ pub enum Retrix { user: String, password: String, server: String, + action: PromptAction, error: Option, }, AwaitLogin(std::time::Instant), @@ -38,6 +39,10 @@ pub enum Retrix { messages: BTreeMap, selected: Option, room_scroll: iced::scrollable::State, + message_scroll: iced::scrollable::State, + message_input: iced::text_input::State, + draft: String, + send_button: iced::button::State, }, } @@ -52,18 +57,28 @@ impl Retrix { user: String::new(), password: String::new(), server: String::new(), + action: PromptAction::Login, error: None, } } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PromptAction { + Login, + Signup, +} + #[derive(Debug, Clone)] pub enum Message { // Login form messages SetUser(String), SetPassword(String), SetServer(String), + SetAction(PromptAction), Login, + Signup, + // Auth result messages LoggedIn(matrix_sdk::Client, matrix::Session), LoginFailed(String), @@ -71,6 +86,8 @@ pub enum Message { ResetRooms(BTreeMap), SelectRoom(RoomId), Sync(AnyRoomEvent), + SetMessage(String), + SendMessage, } impl Application for Retrix { @@ -114,11 +131,13 @@ impl Application for Retrix { user, password, server, + action, .. } => match message { Message::SetUser(u) => *user = u, Message::SetPassword(p) => *password = p, Message::SetServer(s) => *server = s, + Message::SetAction(a) => *action = a, Message::Login => { let user = user.clone(); let password = password.clone(); @@ -132,6 +151,19 @@ impl Application for Retrix { }, ); } + Message::Signup => { + let user = user.clone(); + let password = password.clone(); + let server = server.clone(); + *self = Retrix::AwaitLogin(std::time::Instant::now()); + return Command::perform( + async move { matrix::signup(&user, &password, &server).await }, + |result| match result { + Ok((client, response)) => Message::LoggedIn(client, response), + Err(e) => Message::LoginFailed(e.to_string()), + }, + ); + } _ => (), }, Retrix::AwaitLogin(_) => match message { @@ -148,10 +180,14 @@ impl Application for Retrix { rooms: BTreeMap::new(), selected: None, room_scroll: Default::default(), + message_scroll: Default::default(), + message_input: Default::default(), buttons: Default::default(), messages: Default::default(), + draft: String::new(), + send_button: Default::default(), }; - let client = client.clone(); + /*let client = client.clone(); return Command::perform( async move { let mut rooms = BTreeMap::new(); @@ -162,15 +198,48 @@ impl Application for Retrix { rooms }, |rooms| Message::ResetRooms(rooms), - ); + );*/ } _ => (), }, Retrix::LoggedIn { - rooms, selected, .. + rooms, + selected, + draft, + client, + .. } => match message { Message::ResetRooms(r) => *rooms = r, Message::SelectRoom(r) => *selected = Some(r), + Message::Sync(event) => match event { + AnyRoomEvent::Message(_message) => (), + AnyRoomEvent::State(_state) => (), + AnyRoomEvent::RedactedMessage(_message) => (), + AnyRoomEvent::RedactedState(_state) => (), + }, + Message::SetMessage(m) => *draft = m, + Message::SendMessage => { + let selected = selected.to_owned(); + let draft = draft.clone(); + let client = client.clone(); + return Command::perform( + async move { + client + .room_send( + &selected.unwrap(), + AnyMessageEventContent::RoomMessage( + MessageEventContent::text_plain(draft), + ), + None, + ) + .await + }, + |result| match result { + Ok(_) => Message::SetMessage(String::new()), + Err(e) => Message::SetMessage(format!("{:?}", e)), + }, + ); + } _ => (), }, }; @@ -187,11 +256,27 @@ impl Application for Retrix { user, password, server, + action, error, } => { // Login form let mut content = Column::new() .width(500.into()) + .push( + Row::new() + .push(Radio::new( + PromptAction::Login, + "Login", + Some(*action), + Message::SetAction, + )) + .push(Radio::new( + PromptAction::Signup, + "Sign up", + Some(*action), + Message::SetAction, + )), + ) .push(Text::new("Username")) .push(TextInput::new(user_input, "Username", user, Message::SetUser).padding(5)) .push(Text::new("Password")) @@ -204,8 +289,22 @@ impl Application for Retrix { .push( TextInput::new(server_input, "Server", server, Message::SetServer) .padding(5), - ) - .push(Button::new(login_button, Text::new("Login")).on_press(Message::Login)); + ); + let button = match *action { + PromptAction::Login => { + Button::new(login_button, Text::new("Login")).on_press(Message::Login) + } + PromptAction::Signup => { + content = content.push( + Text::new( + "NB: Signup is very naively implemented, and prone to breaking", + ) + .color([0.2, 0.2, 0.0]), + ); + Button::new(login_button, Text::new("Sign up")).on_press(Message::Signup) + } + }; + content = content.push(button); if let Some(ref error) = error { content = content.push(Text::new(error).color([1.0, 0.0, 0.0])); } @@ -234,8 +333,12 @@ impl Application for Retrix { Retrix::LoggedIn { client, room_scroll, + message_scroll, + message_input, + send_button, buttons, selected, + draft, .. } => { let mut root_row = Row::new().width(Length::Fill).height(Length::Fill); @@ -290,32 +393,66 @@ impl Application for Retrix { .padding(5) .push(Text::new(room.display_name()).size(25)) .push(Rule::horizontal(2)); + let mut scroll = Scrollable::new(message_scroll) + .scrollbar_width(2) + .height(Length::Fill); for message in room.messages.iter() { if let AnyPossiblyRedactedSyncMessageEvent::Regular(event) = message { - match event { - AnySyncMessageEvent::RoomMessage(room_message) => { - match &room_message.content { - MessageEventContent::Text(text) => { - let row = Row::new() - .spacing(5) - .push( - Text::new(room_message.sender.localpart()) - .color([0.2, 0.2, 1.0]), + if let AnySyncMessageEvent::RoomMessage(room_message) = event { + match &room_message.content { + MessageEventContent::Text(text) => { + let row = Row::new() + .spacing(5) + .push( + // Render senders disambiguated name or fallback to + // mxid + Text::new( + room.joined_members + .get(&room_message.sender) + .map(|sender| sender.disambiguated_name()) + .unwrap_or(room_message.sender.to_string()), ) - .push(Text::new(&text.body).width(Length::Fill)) - .push(Text::new(format_systime( - room_message.origin_server_ts, - ))); - col = col.push(row); - } - _ => (), + .color([0.2, 0.2, 1.0]), + ) + .push(Text::new(&text.body).width(Length::Fill)) + .push(Text::new(format_systime( + room_message.origin_server_ts, + ))); + scroll = scroll.push(row); } + _ => (), } - _ => (), } } } + col = col.push(scroll).push( + Row::new() + .push( + TextInput::new( + message_input, + "Write a message...", + draft, + Message::SetMessage, + ) + .width(Length::Fill) + .padding(5) + .on_submit(Message::SendMessage), + ) + .push( + Button::new(send_button, Text::new("Send")) + .on_press(Message::SendMessage), + ), + ); + root_row = root_row.push(col); + } else { + root_row = root_row.push( + Container::new(Text::new("Select a room to start chatting")) + .center_x() + .center_y() + .width(Length::Fill) + .height(Length::Fill), + ); } root_row.into()