Message sending

main
Amanda Graven 2020-12-03 12:12:07 +01:00
parent 061276524a
commit 41b8756ac2
Signed by: amanda
GPG Key ID: 45C461CDC9286390
4 changed files with 256 additions and 54 deletions

View File

@ -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"] }

View File

@ -10,6 +10,8 @@ pub mod matrix;
pub mod ui;
fn main() -> Result<(), Box<dyn std::error::Error>> {
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() {

View File

@ -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<Session> 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<tokio::task::JoinHandle<()>>,
//id: String,
}
impl MatrixSync {
pub fn subscription(client: matrix_sdk::Client) -> iced::Subscription<AnyRoomEvent> {
iced::Subscription::from_recipe(MatrixSync { client })
iced::Subscription::from_recipe(MatrixSync { client, join: None })
}
}
@ -136,41 +191,48 @@ where
}
fn stream(
self: Box<Self>,
mut self: Box<Self>,
_input: iced_futures::BoxStream<I>,
) -> iced_futures::BoxStream<Self::Output> {
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)
}
}

187
src/ui.rs
View File

@ -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<String>,
},
AwaitLogin(std::time::Instant),
@ -38,6 +39,10 @@ pub enum Retrix {
messages: BTreeMap<RoomId, MessageEventContent>,
selected: Option<RoomId>,
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<RoomId, String>),
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()