aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-02-24 15:06:58 +0000
committerLibravatar cel 🌸 <cel@bunny.garden>2025-02-24 15:06:58 +0000
commit5a2fae397cb0269cdb2b9ce5ded742a78d58cdef (patch)
tree8e49ebebef9cb12899a3d45c4d12491677184a39
parent0ae95d1e7da3e7aede41cccb840d26fe2b9763e0 (diff)
downloadmacaw-5a2fae397cb0269cdb2b9ce5ded742a78d58cdef.tar.gz
macaw-5a2fae397cb0269cdb2b9ce5ded742a78d58cdef.tar.bz2
macaw-5a2fae397cb0269cdb2b9ce5ded742a78d58cdef.zip
implement log-in modal
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock115
-rw-r--r--Cargo.toml2
-rw-r--r--src/main.rs478
4 files changed, 502 insertions, 94 deletions
diff --git a/.gitignore b/.gitignore
index d2c28ba..8c308d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/target
+.vscode
macaw.db
diff --git a/Cargo.lock b/Cargo.lock
index 994388f..a58f241 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -34,6 +34,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
name = "ahash"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -372,6 +383,15 @@ dependencies = [
]
[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
name = "block2"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -464,6 +484,15 @@ dependencies = [
]
[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
name = "cc"
version = "1.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -499,6 +528,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
name = "circular"
version = "0.3.0"
dependencies = [
@@ -1981,6 +2020,16 @@ dependencies = [
]
[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
name = "instant"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2254,6 +2303,7 @@ dependencies = [
"iced",
"jid",
"luz",
+ "secret-service",
"tokio",
"tokio-stream",
"tracing",
@@ -2476,6 +2526,30 @@ dependencies = [
]
[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
name = "num-bigint-dig"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2493,6 +2567,15 @@ dependencies = [
]
[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2513,6 +2596,17 @@ dependencies = [
]
[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3492,6 +3586,25 @@ dependencies = [
]
[[package]]
+name = "secret-service"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4"
+dependencies = [
+ "aes",
+ "cbc",
+ "futures-util",
+ "generic-array",
+ "hkdf",
+ "num",
+ "once_cell",
+ "rand",
+ "serde",
+ "sha2",
+ "zbus",
+]
+
+[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4246,6 +4359,7 @@ dependencies = [
"signal-hook-registry",
"socket2",
"tokio-macros",
+ "tracing",
"windows-sys 0.52.0",
]
@@ -5454,6 +5568,7 @@ dependencies = [
"serde_repr",
"sha1",
"static_assertions",
+ "tokio",
"tracing",
"uds_windows",
"windows-sys 0.52.0",
diff --git a/Cargo.toml b/Cargo.toml
index 13f2e6f..5c1322b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,4 +11,4 @@ tokio = "1.43.0"
tokio-stream = "0.1.17"
tracing-subscriber = "0.3.19"
tracing = "0.1.41"
-
+secret-service = { version = "4.0.0", features = ["rt-tokio-crypto-rust"] }
diff --git a/src/main.rs b/src/main.rs
index b2308fb..1a940fd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,38 +1,127 @@
+use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
+use std::fmt::Debug;
+use std::ops::{Deref, DerefMut};
use iced::futures::{SinkExt, Stream, StreamExt};
-use iced::widget::{button, column, row, text, text_input};
-use iced::{stream, Element, Subscription, Task, Theme};
+use iced::widget::text::{Fragment, IntoFragment};
+use iced::widget::{
+ button, center, column, container, mouse_area, opaque, row, stack, text, text_input, Text,
+};
+use iced::{stream, Color, Element, Subscription, Task, Theme};
use jid::JID;
use luz::chat::{Chat, Message as ChatMessage};
use luz::presence::{Offline, Presence};
use luz::CommandMessage;
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
-use tokio::sync::oneshot;
+use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
+use tracing::info;
-#[derive(Default)]
pub struct Macaw {
- client: Option<LuzHandle>,
+ client: Account,
roster: HashMap<JID, Contact>,
users: HashMap<JID, User>,
presences: HashMap<JID, Presence>,
chats: HashMap<JID, (Chat, Vec<ChatMessage>)>,
subscription_requests: HashSet<JID>,
- connection_status: Option<Presence>,
+}
+
+pub struct Creds {
+ jid: String,
+ password: String,
+}
+
+impl Macaw {
+ pub fn new(client: Option<Client>) -> Self {
+ let account;
+ if let Some(client) = client {
+ account = Account::LoggedIn(client);
+ } else {
+ account = Account::LoggedOut {
+ jid: "".to_string(),
+ password: "".to_string(),
+ error: None,
+ };
+ }
+
+ Self {
+ client: account,
+ roster: HashMap::new(),
+ users: HashMap::new(),
+ presences: HashMap::new(),
+ chats: HashMap::new(),
+ subscription_requests: HashSet::new(),
+ }
+ }
+}
+
+pub enum Account {
+ LoggedIn(Client),
+ LoggedOut {
+ jid: String,
+ password: String,
+ error: Option<Error>,
+ },
+}
+
+#[derive(Debug, Clone)]
+pub enum Error {
+ InvalidJID(String),
+ DatabaseConnection,
+}
+
+#[derive(Clone, Debug)]
+pub struct Client {
+ client: LuzHandle,
+ jid: JID,
+ connection_status: Presence,
+}
+
+impl DerefMut for Client {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.client
+ }
+}
+
+impl Deref for Client {
+ type Target = LuzHandle;
+
+ fn deref(&self) -> &Self::Target {
+ &self.client
+ }
}
fn main() -> iced::Result {
tracing_subscriber::fmt::init();
- iced::application("Macaw", Macaw::update, Macaw::view)
- .subscription(Macaw::subscription)
- .run()
+ let client: Option<(JID, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None;
+
+ if let Some((jid, luz_handle, update_recv)) = client {
+ let stream = ReceiverStream::new(update_recv);
+ let stream = stream.map(|message| Message::Luz(message));
+ iced::application("Macaw", Macaw::update, Macaw::view).run_with(|| {
+ (
+ Macaw::new(Some(Client {
+ client: luz_handle,
+ // TODO:
+ jid,
+ connection_status: Presence::Offline(Offline::default()),
+ })),
+ // TODO: autoconnect config
+ Task::stream(stream),
+ )
+ })
+ } else {
+ iced::application("Macaw", Macaw::update, Macaw::view)
+ .run_with(|| (Macaw::new(None), Task::none()))
+ }
}
#[derive(Debug, Clone)]
enum Message {
- ClientCreated(LuzHandle),
+ LoginModal(LoginModalMessage),
+ ClientCreated(Client),
Luz(UpdateMessage),
Roster(HashMap<JID, Contact>),
Connect,
@@ -40,27 +129,15 @@ enum Message {
OpenChat(JID),
}
-impl Macaw {
- fn stream() -> impl Stream<Item = Message> {
- stream::channel(100, |mut output| async {
- let (luz, recv) = LuzHandle::new(
- "test@blos.sm".try_into().unwrap(),
- "slayed".to_string(),
- "./macaw.db",
- )
- .await
- .unwrap();
- output.send(Message::ClientCreated(luz)).await;
- let stream = ReceiverStream::new(recv);
- let stream = stream.map(|message| Message::Luz(message)).map(Ok);
- stream.forward(output).await;
- })
- }
-
- fn subscription(&self) -> Subscription<Message> {
- Subscription::run(Macaw::stream)
- }
+#[derive(Debug, Clone)]
+enum LoginModalMessage {
+ JID(String),
+ Password(String),
+ Submit,
+ Error(Error),
+}
+impl Macaw {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Luz(update_message) => match update_message {
@@ -68,18 +145,35 @@ impl Macaw {
tracing::error!("Luz error: {:?}", error);
Task::none()
}
- UpdateMessage::Online(online, vec) => {
- self.connection_status = Some(Presence::Online(online));
- let mut roster = HashMap::new();
- for contact in vec {
- roster.insert(contact.user_jid.clone(), contact);
+ UpdateMessage::Online(online, vec) => match &mut self.client {
+ Account::LoggedIn(client) => {
+ client.connection_status = Presence::Online(online);
+ let mut roster = HashMap::new();
+ for contact in vec {
+ roster.insert(contact.user_jid.clone(), contact);
+ }
+ self.roster = roster;
+ Task::none()
}
- self.roster = roster;
- Task::none()
- }
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => Task::none(),
+ },
UpdateMessage::Offline(offline) => {
- self.connection_status = Some(Presence::Offline(offline));
- Task::none()
+ // TODO: update all contacts' presences to unknown (offline)
+ match &mut self.client {
+ Account::LoggedIn(client) => {
+ client.connection_status = Presence::Offline(offline);
+ Task::none()
+ }
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => Task::none(),
+ }
}
UpdateMessage::FullRoster(vec) => {
let mut macaw_roster = HashMap::new();
@@ -118,13 +212,13 @@ impl Macaw {
Task::none()
}
},
- Message::ClientCreated(luz_handle) => {
- let cloned: LuzHandle = luz_handle.clone();
- self.client = Some(cloned);
+ // TODO: NEXT
+ Message::ClientCreated(client) => {
+ self.client = Account::LoggedIn(client.clone());
let (send, recv) = oneshot::channel();
Task::perform(
async move {
- luz_handle.send(CommandMessage::GetRoster(send)).await;
+ client.client.send(CommandMessage::GetRoster(send)).await;
recv.await
},
|result| {
@@ -141,64 +235,262 @@ impl Macaw {
self.roster = hash_map;
Task::none()
}
- Message::Connect => {
- let client = self.client.clone();
- Task::future(async move {
- client.clone().unwrap().send(CommandMessage::Connect).await;
- })
- .discard()
- }
- Message::Disconnect => {
- let client = self.client.clone();
- Task::future(async move {
- client
- .clone()
- .unwrap()
- .send(CommandMessage::Disconnect(Offline::default()))
- .await;
- })
- .discard()
- }
+ Message::Connect => match &self.client {
+ Account::LoggedIn(client) => {
+ let client = client.client.clone();
+ Task::future(async move {
+ client.send(CommandMessage::Connect).await;
+ })
+ .discard()
+ }
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => Task::none(),
+ },
+ Message::Disconnect => match &self.client {
+ Account::LoggedIn(client) => {
+ let client = client.client.clone();
+ Task::future(async move {
+ client
+ .send(CommandMessage::Disconnect(Offline::default()))
+ .await;
+ })
+ .discard()
+ }
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => Task::none(),
+ },
Message::OpenChat(jid) => todo!(),
+ Message::LoginModal(login_modal_message) => match login_modal_message {
+ LoginModalMessage::JID(j) => match &mut self.client {
+ Account::LoggedIn(_client) => Task::none(),
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => {
+ *jid = j;
+ Task::none()
+ }
+ },
+ LoginModalMessage::Password(p) => match &mut self.client {
+ Account::LoggedIn(_client) => Task::none(),
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => {
+ *password = p;
+ Task::none()
+ }
+ },
+ LoginModalMessage::Submit => match &self.client {
+ Account::LoggedIn(_client) => Task::none(),
+ Account::LoggedOut {
+ jid: jid_str,
+ password,
+ error,
+ } => {
+ info!("submitting login");
+ let jid_str = jid_str.clone();
+ let password = password.clone();
+ Task::future(async move {
+ let jid: Result<JID, _> = jid_str.parse();
+ match jid {
+ Ok(j) => {
+ let result =
+ LuzHandle::new(j.clone(), password.to_string(), "macaw.db")
+ .await;
+ match result {
+ Ok((luz_handle, receiver)) => {
+ let stream = ReceiverStream::new(receiver);
+ let stream =
+ stream.map(|message| Message::Luz(message));
+ vec![
+ Task::done(Message::ClientCreated(Client {
+ client: luz_handle,
+ jid: j,
+ connection_status: Presence::Offline(
+ Offline::default(),
+ ),
+ })),
+ Task::stream(stream),
+ ]
+ }
+ Err(e) => {
+ tracing::error!("error (database probably)");
+ return vec![Task::done(Message::LoginModal(
+ LoginModalMessage::Error(Error::DatabaseConnection),
+ ))];
+ }
+ }
+ }
+ Err(_) => {
+ tracing::error!("parsing jid");
+ return vec![Task::done(Message::LoginModal(
+ LoginModalMessage::Error(Error::InvalidJID(
+ jid_str.to_string(),
+ )),
+ ))];
+ }
+ }
+ })
+ .then(|tasks| Task::batch(tasks))
+ }
+ },
+ LoginModalMessage::Error(e) => match &mut self.client {
+ Account::LoggedIn(_client) => Task::none(),
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => {
+ tracing::error!("luz::new: {:?}", e);
+ *error = Some(e);
+ Task::none()
+ }
+ },
+ },
}
}
fn view(&self) -> Element<Message> {
- let mut contacts: Vec<Element<Message>> = Vec::new();
- for (_, contact) in &self.roster {
- contacts.push(
- button(match &contact.user_jid.localpart {
- Some(u) => u,
- None => "no username",
- })
- .on_press(Message::OpenChat(contact.user_jid.clone()))
- .into(),
- );
- }
- let column = column(contacts);
- let connection_status = match &self.connection_status {
- Some(s) => match s {
- Presence::Online(online) => "connected",
- Presence::Offline(offline) => "disconnected",
- },
- None => "no account",
+ let ui = {
+ let mut contacts: Vec<Element<Message>> = Vec::new();
+ for (_, contact) in &self.roster {
+ let jid: Cow<'_, str> = (&contact.user_jid).into();
+ contacts.push(
+ button(text(jid))
+ .on_press(Message::OpenChat(contact.user_jid.clone()))
+ .into(),
+ );
+ }
+ let column = column(contacts);
+ let connection_status = match &self.client {
+ Account::LoggedIn(client) => match &client.connection_status {
+ Presence::Online(_online) => "online",
+ Presence::Offline(_offline) => "disconnected",
+ },
+ Account::LoggedOut {
+ jid: _,
+ password: _,
+ error,
+ } => "disconnected",
+ };
+ // match &self.client.as_ref().map(|client| &client.connection_status) {
+ // Some(s) => match s {
+ // Presence::Online(online) => "connected",
+ // Presence::Offline(offline) => "disconnected",
+ // },
+ // None => "no account",
+ // };
+ let client_jid: Cow<'_, str> = match &self.client {
+ Account::LoggedIn(client) => (&client.jid).into(),
+ Account::LoggedOut {
+ jid: _,
+ password: _,
+ error,
+ } => Cow::from("no account"),
+ // map(|client| (&client.jid).into());
+ };
+ column![
+ row![
+ text(client_jid),
+ text(connection_status),
+ button("connect").on_press(Message::Connect),
+ button("disconnect").on_press(Message::Disconnect)
+ ],
+ text("Buddy List:"),
+ //
+ //
+ column,
+ ]
};
- column![
- row![
- text("test@blos.sm"),
- text(connection_status),
- button("connect").on_press(Message::Connect),
- button("disconnect").on_press(Message::Disconnect)
- ],
- text("Buddy List:"),
- //
- //
- column,
- ]
- .into()
+
+ // temporarily center to fill space
+ let ui = center(ui).into();
+
+ match &self.client {
+ Account::LoggedIn(_client) => ui,
+ Account::LoggedOut {
+ jid,
+ password,
+ error,
+ } => {
+ let signup = container(
+ column![
+ text("Log In").size(24),
+ column![
+ column![
+ text("JID").size(12),
+ text_input("berry@macaw.chat", &jid)
+ .on_input(|j| Message::LoginModal(LoginModalMessage::JID(j)))
+ .on_submit(Message::LoginModal(LoginModalMessage::Submit))
+ .padding(5),
+ ]
+ .spacing(5),
+ column![
+ text("Password").size(12),
+ text_input("", &password)
+ .on_input(|p| Message::LoginModal(LoginModalMessage::Password(
+ p
+ )))
+ .on_submit(Message::LoginModal(LoginModalMessage::Submit))
+ .secure(true)
+ .padding(5),
+ ]
+ .spacing(5),
+ button(text("Submit"))
+ .on_press(Message::LoginModal(LoginModalMessage::Submit)),
+ ]
+ .spacing(10)
+ ]
+ .spacing(20),
+ )
+ .width(300)
+ .padding(10)
+ .style(container::rounded_box);
+
+ // signup.into()
+ modal(ui, signup)
+ }
+ }
}
fn theme(&self) -> Theme {
Theme::Dark
}
}
+
+fn modal<'a, Message>(
+ base: impl Into<Element<'a, Message>>,
+ content: impl Into<Element<'a, Message>>,
+ // on_blur: Message,
+) -> Element<'a, Message>
+where
+ Message: Clone + 'a,
+{
+ stack![
+ base.into(),
+ opaque(
+ mouse_area(center(opaque(content)).style(|_theme| {
+ container::Style {
+ background: Some(
+ Color {
+ a: 0.8,
+ ..Color::BLACK
+ }
+ .into(),
+ ),
+ ..container::Style::default()
+ }
+ })) // .on_press(on_blur)
+ )
+ ]
+ .into()
+}