diff --git a/Cargo.lock b/Cargo.lock index 95c729a9a..fe1d005b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1073,6 +1073,7 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" name = "data" version = "0.1.0" dependencies = [ + "anyhow", "base64 0.21.7", "bytes", "chrono", @@ -1831,6 +1832,7 @@ dependencies = [ name = "halloy" version = "0.1.0" dependencies = [ + "anyhow", "bytesize", "chrono", "dark-light", diff --git a/Cargo.toml b/Cargo.toml index 36729948e..5f9d73e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ tokio-stream = { version = "0.1.16", features = ["fs"] } # change to 1.2.0 when it is released https://github.com/frewsxcv/rust-dark-light/issues/38 dark-light = { git = "https://github.com/frewsxcv/rust-dark-light", rev = "3eb3e93dd0fa30733c3e93082dd9517fb580ae95" } +anyhow = "1.0.91" [dependencies.uuid] version = "1.0" diff --git a/data/Cargo.toml b/data/Cargo.toml index b0220d91d..e47028cd4 100644 --- a/data/Cargo.toml +++ b/data/Cargo.toml @@ -39,6 +39,7 @@ nom = "7.1" const_format = "0.2.32" strum = { version = "0.26.3", features = ["derive"] } derive_more = { version = "1.0.0", features = ["full"] } +anyhow = "1.0.91" [dependencies.irc] path = "../irc" diff --git a/data/src/client.rs b/data/src/client.rs index 979a7a2ff..ab0cc2e71 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -6,6 +6,8 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt; use std::time::{Duration, Instant}; +use anyhow::{anyhow, bail, Result}; + use crate::history::ReadMarker; use crate::message::server_time; use crate::time::Posix; @@ -122,25 +124,8 @@ impl Client { pub fn new( server: Server, config: config::Server, - mut sender: mpsc::Sender, + sender: mpsc::Sender, ) -> Self { - // Begin registration - let _ = sender.try_send(command!("CAP", "LS", "302")); - let registration_step = RegistrationStep::List; - - // Identify - { - let nick = &config.nickname; - let user = config.username.as_ref().unwrap_or(nick); - let real = config.realname.as_ref().unwrap_or(nick); - - if let Some(pass) = config.password.as_ref() { - let _ = sender.try_send(command!("PASS", pass)); - } - let _ = sender.try_send(command!("NICK", nick)); - let _ = sender.try_send(command!("USER", user, real)); - } - Self { server, config, @@ -153,7 +138,7 @@ impl Client { labels: HashMap::new(), batches: HashMap::new(), reroute_responses_to: None, - registration_step, + registration_step: RegistrationStep::Start, listed_caps: vec![], supports_labels: false, supports_away_notify: false, @@ -166,6 +151,24 @@ impl Client { } } + pub fn connect(&mut self) -> Result<()> { + // Begin registration + self.handle.try_send(command!("CAP", "LS", "302"))?; + + // Identify + let nick = &self.config.nickname; + let user = self.config.username.as_ref().unwrap_or(nick); + let real = self.config.realname.as_ref().unwrap_or(nick); + + if let Some(pass) = self.config.password.as_ref() { + self.handle.try_send(command!("PASS", pass))?; + } + self.handle.try_send(command!("NICK", nick))?; + self.handle.try_send(command!("USER", user, real))?; + self.registration_step = RegistrationStep::List; + Ok(()) + } + fn quit(&mut self, reason: Option) { if let Err(e) = if let Some(reason) = reason { self.handle.try_send(command!("QUIT", reason)) @@ -211,25 +214,25 @@ impl Client { } } - fn receive(&mut self, message: message::Encoded) -> Vec { + fn receive(&mut self, message: message::Encoded) -> Result> { log::trace!("Message received => {:?}", *message); let stop_reroute = stop_reroute(&message.command); - let events = self.handle(message, None).unwrap_or_default(); + let events = self.handle(message, None)?; if stop_reroute { self.reroute_responses_to = None; } - events + Ok(events) } fn handle( &mut self, mut message: message::Encoded, parent_context: Option, - ) -> Option> { + ) -> Result> { use irc::proto::command::Numeric::*; let label_tag = remove_tag("label", message.tags.as_mut()); @@ -249,10 +252,16 @@ impl Client { }) }); + macro_rules! ok { + ($option:expr) => { + $option.ok_or_else(|| anyhow!("Malformed command: {:?}", message.command))? + }; + } + match &message.command { Command::BATCH(batch, ..) => { let mut chars = batch.chars(); - let symbol = chars.next()?; + let symbol = ok!(chars.next()); let reference = chars.collect::(); match symbol { @@ -269,23 +278,23 @@ impl Client { { parent.events.extend(finished.events); } else { - return Some(finished.events); + return Ok(finished.events); } } } _ => {} } - return None; + return Ok(vec![]); } _ if batch_tag.is_some() => { let events = self.handle(message, context)?; if let Some(batch) = self.batches.get_mut(&batch_tag.unwrap()) { batch.events.extend(events); - return None; + return Ok(vec![]); } else { - return Some(events); + return Ok(events); } } // Label context whois @@ -294,7 +303,7 @@ impl Client { .map(Context::buffer) .map(|buffer| buffer.server_message_target(None)) { - return Some(vec![Event::WithTarget( + return Ok(vec![Event::WithTarget( message, self.nickname().to_owned(), source, @@ -308,7 +317,7 @@ impl Client { .clone() .map(|buffer| buffer.server_message_target(None)) { - return Some(vec![Event::WithTarget( + return Ok(vec![Event::WithTarget( message, self.nickname().to_owned(), source, @@ -320,7 +329,7 @@ impl Client { (Some(caps), None) => (caps, None), (Some(asterisk), Some(caps)) => (caps, Some(asterisk)), // Unreachable - (None, None) | (None, Some(_)) => return None, + (None, None) | (None, Some(_)) => return Ok(vec![]), }; self.listed_caps.extend(caps.split(' ').map(String::from)); @@ -385,17 +394,19 @@ impl Client { self.registration_step = RegistrationStep::Req; for message in group_capability_requests(&requested) { - let _ = self.handle.try_send(message); + self.handle.try_send(message)?; } } else { // If none requested, end negotiation self.registration_step = RegistrationStep::End; - let _ = self.handle.try_send(command!("CAP", "END")); + self.handle.try_send(command!("CAP", "END"))?; } } } Command::CAP(_, sub, a, b) if sub == "ACK" => { - let caps = if b.is_none() { a.as_ref() } else { b.as_ref() }?; + // TODO this code is duplicated several times. Fix in `Command`. + let caps = ok!(b.as_ref().or(a.as_ref())); + log::info!("[{}] capabilities acknowledged: {caps}", self.server); let caps = caps.split(' ').collect::>(); @@ -420,26 +431,26 @@ impl Client { if let Some(sasl) = self.config.sasl.as_ref().filter(|_| supports_sasl) { self.registration_step = RegistrationStep::Sasl; - let _ = self - .handle - .try_send(command!("AUTHENTICATE", sasl.command())); + self.handle + .try_send(command!("AUTHENTICATE", sasl.command()))?; } else { self.registration_step = RegistrationStep::End; - let _ = self.handle.try_send(command!("CAP", "END")); + self.handle.try_send(command!("CAP", "END"))?; } } Command::CAP(_, sub, a, b) if sub == "NAK" => { - let caps = if b.is_none() { a.as_ref() } else { b.as_ref() }?; + let caps = ok!(b.as_ref().or(a.as_ref())); + log::warn!("[{}] capabilities not acknowledged: {caps}", self.server); // End we didn't move to sasl or already ended if self.registration_step < RegistrationStep::Sasl { self.registration_step = RegistrationStep::End; - let _ = self.handle.try_send(command!("CAP", "END")); + self.handle.try_send(command!("CAP", "END"))?; } } Command::CAP(_, sub, a, b) if sub == "NEW" => { - let caps = if b.is_none() { a.as_ref() } else { b.as_ref() }?; + let caps = ok!(b.as_ref().or(a.as_ref())); let new_caps = caps.split(' ').map(String::from).collect::>(); @@ -501,14 +512,14 @@ impl Client { if !requested.is_empty() { for message in group_capability_requests(&requested) { - let _ = self.handle.try_send(message); + self.handle.try_send(message)?; } } self.listed_caps.extend(new_caps); } Command::CAP(_, sub, a, b) if sub == "DEL" => { - let caps = if b.is_none() { a.as_ref() } else { b.as_ref() }?; + let caps = ok!(b.as_ref().or(a.as_ref())); let del_caps = caps.split(' ').collect::>(); @@ -535,9 +546,9 @@ impl Client { if let Some(sasl) = self.config.sasl.as_ref() { log::info!("[{}] sasl auth: {}", self.server, sasl.command()); - let _ = self.handle.try_send(command!("AUTHENTICATE", sasl.param())); + self.handle.try_send(command!("AUTHENTICATE", sasl.param()))?; self.registration_step = RegistrationStep::End; - let _ = self.handle.try_send(command!("CAP", "END")); + self.handle.try_send(command!("CAP", "END"))?; } } Command::Numeric(RPL_LOGGEDIN, args) => { @@ -548,14 +559,14 @@ impl Client { &self.registration_required_channels, &self.config.channel_keys, ) { - let _ = self.handle.try_send(message); + self.handle.try_send(message)?; } self.registration_required_channels.clear(); } if !self.supports_account_notify { - let accountname = args.first()?; + let accountname = ok!(args.first()); let old_user = User::from(self.nickname().to_owned()); @@ -585,7 +596,7 @@ impl Client { match command { dcc::Command::Send(request) => { log::trace!("DCC Send => {request:?}"); - return Some(vec![Event::FileTransferRequest( + return Ok(vec![Event::FileTransferRequest( file_transfer::ReceiveRequest { from: user.nickname().to_owned(), dcc_send: request, @@ -595,8 +606,7 @@ impl Client { )]); } dcc::Command::Unsupported(command) => { - log::debug!("Unsupported DCC command: {command}",); - return None; + bail!("Unsupported DCC command: {command}",); } } } else { @@ -610,36 +620,36 @@ impl Client { match query.command { ctcp::Command::Action => (), ctcp::Command::ClientInfo => { - let _ = self.handle.try_send(ctcp::response_message( + self.handle.try_send(ctcp::response_message( &query.command, user.nickname().to_string(), Some("ACTION CLIENTINFO DCC PING SOURCE VERSION"), - )); + ))?; } ctcp::Command::DCC => (), ctcp::Command::Ping => { - let _ = self.handle.try_send(ctcp::response_message( + self.handle.try_send(ctcp::response_message( &query.command, user.nickname().to_string(), query.params, - )); + ))?; } ctcp::Command::Source => { - let _ = self.handle.try_send(ctcp::response_message( + self.handle.try_send(ctcp::response_message( &query.command, user.nickname().to_string(), Some(crate::environment::SOURCE_WEBSITE), - )); + ))?; } ctcp::Command::Version => { - let _ = self.handle.try_send(ctcp::response_message( + self.handle.try_send(ctcp::response_message( &query.command, user.nickname().to_string(), Some(format!( "Halloy {}", crate::environment::VERSION )), - )); + ))?; } ctcp::Command::Unknown(command) => { log::debug!( @@ -649,13 +659,13 @@ impl Client { } } - return None; + return Ok(vec![]); } } // Highlight notification if message::references_user_text(user.nickname(), self.nickname(), text) { - return Some(vec![Event::Notification( + return Ok(vec![Event::Notification( message.clone(), self.nickname().to_owned(), Notification::Highlight { @@ -666,12 +676,12 @@ impl Client { )]); } else if user.nickname() == self.nickname() && context.is_some() { // If we sent (echo) & context exists (we sent from this client), ignore - return None; + return Ok(vec![]); } // use `channel` to confirm the direct message, then send notification if channel == &self.nickname().to_string() { - return Some(vec![Event::Notification( + return Ok(vec![Event::Notification( message.clone(), self.nickname().to_owned(), Notification::DirectMessage(user), @@ -682,10 +692,10 @@ impl Client { } Command::INVITE(user, channel) => { let user = User::from(Nick::from(user.as_str())); - let inviter = message.user()?; + let inviter = ok!(message.user()); let user_channels = self.user_channels(user.nickname()); - return Some(vec![Event::Broadcast(Broadcast::Invite { + return Ok(vec![Event::Broadcast(Broadcast::Invite { inviter, channel: channel.clone(), user_channels, @@ -693,7 +703,7 @@ impl Client { })]); } Command::NICK(nick) => { - let old_user = message.user()?; + let old_user = ok!(message.user()); let ourself = self.nickname() == old_user.nickname(); if ourself { @@ -710,7 +720,7 @@ impl Client { let channels = self.user_channels(old_user.nickname()); - return Some(vec![Event::Broadcast(Broadcast::Nickname { + return Ok(vec![Event::Broadcast(Broadcast::Nickname { old_user, new_nick, ourself, @@ -735,12 +745,12 @@ impl Client { } if let Some(nick) = self.alt_nick.and_then(|i| self.config.alt_nicks.get(i)) { - let _ = self.handle.try_send(command!("NICK", nick)); + self.handle.try_send(command!("NICK", nick))?; } } Command::Numeric(RPL_WELCOME, args) => { // Updated actual nick - let nick = args.first()?; + let nick = ok!(args.first()); self.resolved_nick = Some(nick.to_string()); // Send nick password & ghost @@ -748,29 +758,29 @@ impl Client { // Try ghost recovery if we couldn't claim our nick if self.config.should_ghost && nick != &self.config.nickname { for sequence in &self.config.ghost_sequence { - let _ = self.handle.try_send(command!( + self.handle.try_send(command!( "PRIVMSG", "NickServ", format!("{sequence} {} {nick_pass}", &self.config.nickname) - )); + ))?; } } - let _ = if let Some(identify_syntax) = &self.config.nick_identify_syntax { + if let Some(identify_syntax) = &self.config.nick_identify_syntax { match identify_syntax { config::server::IdentifySyntax::PasswordNick => { self.handle.try_send(command!( "PRIVMSG", "NickServ", format!("IDENTIFY {nick_pass} {}", &self.config.nickname) - )) + ))? } config::server::IdentifySyntax::NickPassword => { self.handle.try_send(command!( "PRIVMSG", "NickServ", format!("IDENTIFY {} {nick_pass}", &self.config.nickname) - )) + ))? } } } else if self.resolved_nick == Some(self.config.nickname.clone()) { @@ -780,39 +790,39 @@ impl Client { "PRIVMSG", "NickServ", format!("IDENTIFY {nick_pass}") - )) + ))? } else { // Default to most common syntax if unknown self.handle.try_send(command!( "PRIVMSG", "NickServ", format!("IDENTIFY {} {nick_pass}", &self.config.nickname) - )) - }; + ))? + } } // Send user modestring if let Some(modestring) = self.config.umodes.as_ref() { - let _ = self.handle.try_send(command!("MODE", nick, modestring)); + self.handle.try_send(command!("MODE", nick, modestring))?; } // Loop on connect commands for command in self.config.on_connect.iter() { if let Ok(cmd) = crate::command::parse(command, None) { if let Ok(command) = proto::Command::try_from(cmd) { - let _ = self.handle.try_send(command.into()); + self.handle.try_send(command.into())?; }; }; } // Send JOIN for message in group_joins(&self.config.channels, &self.config.channel_keys) { - let _ = self.handle.try_send(message); + self.handle.try_send(message)?; } } // QUIT Command::QUIT(comment) => { - let user = message.user()?; + let user = ok!(message.user()); self.chanmap.values_mut().for_each(|channel| { channel.users.remove(&user); @@ -820,7 +830,7 @@ impl Client { let channels = self.user_channels(user.nickname()); - return Some(vec![Event::Broadcast(Broadcast::Quit { + return Ok(vec![Event::Broadcast(Broadcast::Quit { user, comment: comment.clone(), channels, @@ -828,7 +838,7 @@ impl Client { })]); } Command::PART(channel, _) => { - let user = message.user()?; + let user = ok!(message.user()); if user.nickname() == self.nickname() { self.chanmap.remove(channel); @@ -837,7 +847,7 @@ impl Client { } } Command::JOIN(channel, accountname) => { - let user = message.user()?; + let user = ok!(message.user()); if user.nickname() == self.nickname() { self.chanmap.insert(channel.clone(), Channel::default()); @@ -852,26 +862,26 @@ impl Client { "tcnf" }; - let _ = self.handle.try_send(command!( + self.handle.try_send(command!( "WHO", channel, fields, isupport::WHO_POLL_TOKEN.to_owned() - )); + ))?; state.last_who = Some(WhoStatus::Requested( Instant::now(), Some(isupport::WHO_POLL_TOKEN), )); } else { - let _ = self.handle.try_send(command!("WHO", channel)); + self.handle.try_send(command!("WHO", channel))?; state.last_who = Some(WhoStatus::Requested(Instant::now(), None)); } log::debug!("[{}] {channel} - WHO requested", self.server); } } - return Some(vec![Event::JoinedChannel(channel.clone())]); + return Ok(vec![Event::JoinedChannel(channel.clone())]); } else if let Some(channel) = self.chanmap.get_mut(channel) { let user = if self.supports_extended_join { accountname.as_ref().map_or(user.clone(), |accountname| { @@ -894,11 +904,11 @@ impl Client { } } Command::Numeric(RPL_WHOREPLY, args) => { - let target = args.get(1)?; + let target = ok!(args.get(1)); if proto::is_channel(target) { if let Some(channel) = self.chanmap.get_mut(target) { - channel.update_user_away(args.get(5)?, args.get(6)?); + channel.update_user_away(ok!(args.get(5)), ok!(args.get(6))); if matches!(channel.last_who, Some(WhoStatus::Requested(_, None)) | None) { channel.last_who = Some(WhoStatus::Receiving(None)); @@ -907,17 +917,17 @@ impl Client { if matches!(channel.last_who, Some(WhoStatus::Receiving(_))) { // We requested, don't save to history - return None; + return Ok(vec![]); } } } } Command::Numeric(RPL_WHOSPCRPL, args) => { - let target = args.get(2)?; + let target = ok!(args.get(2)); if proto::is_channel(target) { if let Some(channel) = self.chanmap.get_mut(target) { - channel.update_user_away(args.get(3)?, args.get(4)?); + channel.update_user_away(ok!(args.get(3)), ok!(args.get(4))); if self.supports_account_notify { if let (Some(user), Some(accountname)) = (args.get(3), args.get(5)) { @@ -925,7 +935,7 @@ impl Client { } } - if let Ok(token) = args.get(1)?.parse::() { + if let Ok(token) = ok!(args.get(1)).parse::() { if let Some(WhoStatus::Requested(_, Some(request_token))) = channel.last_who { @@ -939,27 +949,27 @@ impl Client { if matches!(channel.last_who, Some(WhoStatus::Receiving(_))) { // We requested, don't save to history - return None; + return Ok(vec![]); } } } } Command::Numeric(RPL_ENDOFWHO, args) => { - let target = args.get(1)?; + let target = ok!(args.get(1)); if proto::is_channel(target) { if let Some(channel) = self.chanmap.get_mut(target) { if matches!(channel.last_who, Some(WhoStatus::Receiving(_))) { channel.last_who = Some(WhoStatus::Done(Instant::now())); log::debug!("[{}] {target} - WHO done", self.server); - return None; + return Ok(vec![]); } } } } Command::AWAY(args) => { let away = args.is_some(); - let user = message.user()?; + let user = ok!(message.user()); for channel in self.chanmap.values_mut() { if let Some(mut user) = channel.users.take(&user) { @@ -969,8 +979,8 @@ impl Client { } } Command::Numeric(RPL_UNAWAY, args) => { - let nick = args.first()?.as_str(); - let user = User::try_from(nick).ok()?; + let nick = ok!(args.first()).as_str(); + let user = User::try_from(nick)?; if user.nickname() == self.nickname() { for channel in self.chanmap.values_mut() { @@ -982,8 +992,8 @@ impl Client { } } Command::Numeric(RPL_NOWAWAY, args) => { - let nick = args.first()?.as_str(); - let user = User::try_from(nick).ok()?; + let nick = ok!(args.first()).as_str(); + let user = User::try_from(nick)?; if user.nickname() == self.nickname() { for channel in self.chanmap.values_mut() { @@ -1028,7 +1038,7 @@ impl Client { &self.registration_required_channels, &self.config.channel_keys, ) { - let _ = self.handle.try_send(message); + self.handle.try_send(message)?; } self.registration_required_channels.clear(); @@ -1046,19 +1056,19 @@ impl Client { // Don't save to history if names list was triggered by JOIN if !channel.names_init { - return None; + return Ok(vec![]); } } } Command::Numeric(RPL_ENDOFNAMES, args) => { - let target = args.get(1)?; + let target = ok!(args.get(1)); if proto::is_channel(target) { if let Some(channel) = self.chanmap.get_mut(target) { if !channel.names_init { channel.names_init = true; - return None; + return Ok(vec![]); } } } @@ -1076,29 +1086,27 @@ impl Client { Command::Numeric(RPL_TOPIC, args) => { if let Some(channel) = self.chanmap.get_mut(&args[1]) { channel.topic.content = - Some(message::parse_fragments(args.get(2)?.to_owned(), &[])); + Some(message::parse_fragments(ok!(args.get(2)).to_owned(), &[])); } // Exclude topic message from history to prevent spam during dev #[cfg(feature = "dev")] - return None; + return Ok(vec![]); } Command::Numeric(RPL_TOPICWHOTIME, args) => { if let Some(channel) = self.chanmap.get_mut(&args[1]) { - channel.topic.who = Some(args.get(2)?.to_string()); - channel.topic.time = Some( - args.get(3)? - .parse::() - .ok() - .map(Posix::from_seconds)? - .datetime()?, - ); + channel.topic.who = Some(ok!(args.get(2)).to_string()); + let timestamp = Posix::from_seconds(ok!(args.get(3)).parse::()?); + channel.topic.time = + Some(timestamp.datetime().ok_or_else(|| { + anyhow!("Unable to parse timestamp: {:?}", timestamp) + })?); } // Exclude topic message from history to prevent spam during dev #[cfg(feature = "dev")] - return None; + return Ok(vec![]); } Command::Numeric(ERR_NOCHANMODES, args) => { - let channel = args.get(1)?; + let channel = ok!(args.get(1)); // If the channel has not been joined but is in the configured channels, // then interpret this numeric as ERR_NEEDREGGEDNICK (which has the @@ -1115,7 +1123,7 @@ impl Client { } Command::Numeric(RPL_ISUPPORT, args) => { let args_len = args.len(); - args.iter().enumerate().skip(1).for_each(|(index, arg)| { + for (index, arg) in args.iter().enumerate().skip(1) { let operation = arg.parse::(); match operation { @@ -1138,7 +1146,7 @@ impl Client { group_monitors(&self.config.monitor, target_limit); for message in messages { - let _ = self.handle.try_send(message); + self.handle.try_send(message)?; } } } else { @@ -1172,15 +1180,15 @@ impl Client { } } } - }); + } - return None; + return Ok(vec![]); } Command::TAGMSG(_) => { - return None; + return Ok(vec![]); } Command::ACCOUNT(accountname) => { - let old_user = message.user()?; + let old_user = ok!(message.user()); self.chanmap.values_mut().for_each(|channel| { if let Some(user) = channel.users.take(&old_user) { @@ -1196,14 +1204,14 @@ impl Client { &self.registration_required_channels, &self.config.channel_keys, ) { - let _ = self.handle.try_send(message); + self.handle.try_send(message)?; } self.registration_required_channels.clear(); } } Command::CHGHOST(new_username, new_hostname) => { - let old_user = message.user()?; + let old_user = ok!(message.user()); let ourself = old_user.nickname() == self.nickname(); @@ -1218,7 +1226,7 @@ impl Client { let channels = self.user_channels(old_user.nickname()); - return Some(vec![Event::Broadcast(Broadcast::ChangeHost { + return Ok(vec![Event::Broadcast(Broadcast::ChangeHost { old_user, new_username: new_username.clone(), new_hostname: new_hostname.clone(), @@ -1228,52 +1236,55 @@ impl Client { })]); } Command::Numeric(RPL_MONONLINE, args) => { - let targets = args - .get(1)? + let targets = ok!(args.get(1)) .split(',') .filter_map(|target| User::try_from(target).ok()) .collect::>(); - return Some(vec![Event::Notification( + return Ok(vec![Event::Notification( message.clone(), self.nickname().to_owned(), Notification::MonitoredOnline(targets), )]); } Command::Numeric(RPL_MONOFFLINE, args) => { - let targets = args.get(1)?.split(',').map(Nick::from).collect::>(); + let targets = ok!(args.get(1)) + .split(',') + .map(Nick::from) + .collect::>(); - return Some(vec![Event::Notification( + return Ok(vec![Event::Notification( message.clone(), self.nickname().to_owned(), Notification::MonitoredOffline(targets), )]); } Command::Numeric(RPL_ENDOFMONLIST, _) => { - return None; + return Ok(vec![]); } Command::MARKREAD(target, Some(timestamp)) => { if let Some(read_marker) = timestamp .strip_prefix("timestamp=") .and_then(|timestamp| timestamp.parse::().ok()) { - return Some(vec![Event::UpdateReadMarker(target.clone(), read_marker)]); + return Ok(vec![Event::UpdateReadMarker(target.clone(), read_marker)]); } } _ => {} } - Some(vec![Event::Single(message, self.nickname().to_owned())]) + Ok(vec![Event::Single(message, self.nickname().to_owned())]) } - pub fn send_markread(&mut self, target: &str, read_marker: ReadMarker) { + pub fn send_markread(&mut self, target: &str, read_marker: ReadMarker) -> Result<()> { if self.supports_read_marker { - let _ = self.handle.try_send(command!( + self.handle.try_send(command!( "MARKREAD", target.to_string(), format!("timestamp={read_marker}"), - )); + ))?; } + Ok(()) } fn sync(&mut self) { @@ -1332,7 +1343,7 @@ impl Client { ) } - pub fn tick(&mut self, now: Instant) { + pub fn tick(&mut self, now: Instant) -> Result<()> { match self.highlight_blackout { HighlightBlackout::Blackout(instant) => { if now.duration_since(instant) >= HIGHLIGHT_BLACKOUT_INTERVAL { @@ -1369,19 +1380,19 @@ impl Client { "tcnf" }; - let _ = self.handle.try_send(command!( + self.handle.try_send(command!( "WHO", channel, fields, isupport::WHO_POLL_TOKEN.to_owned() - )); + ))?; state.last_who = Some(WhoStatus::Requested( Instant::now(), Some(isupport::WHO_POLL_TOKEN), )); } else { - let _ = self.handle.try_send(command!("WHO", channel)); + self.handle.try_send(command!("WHO", channel))?; state.last_who = Some(WhoStatus::Requested(Instant::now(), None)); } log::debug!( @@ -1394,6 +1405,7 @@ impl Client { ); } } + Ok(()) } } @@ -1459,10 +1471,12 @@ impl Map { self.client(server).map(Client::nickname) } - pub fn receive(&mut self, server: &Server, message: message::Encoded) -> Vec { - self.client_mut(server) - .map(|client| client.receive(message)) - .unwrap_or_default() + pub fn receive(&mut self, server: &Server, message: message::Encoded) -> Result> { + if let Some(client) = self.client_mut(server) { + client.receive(message) + } else { + Ok(Default::default()) + } } pub fn sync(&mut self, server: &Server) { @@ -1477,10 +1491,11 @@ impl Map { } } - pub fn send_markread(&mut self, server: &Server, target: &str, read_marker: ReadMarker) { + pub fn send_markread(&mut self, server: &Server, target: &str, read_marker: ReadMarker) -> Result<()> { if let Some(client) = self.client_mut(server) { - client.send_markread(target, read_marker); + client.send_markread(target, read_marker)?; } + Ok(()) } pub fn join(&mut self, server: &Server, channels: &[String]) { @@ -1577,12 +1592,13 @@ impl Map { .unwrap_or(Status::Unavailable) } - pub fn tick(&mut self, now: Instant) { - self.0.values_mut().for_each(|client| { + pub fn tick(&mut self, now: Instant) -> Result<()> { + for client in self.0.values_mut() { if let State::Ready(client) = client { - client.tick(now); + client.tick(now)?; } - }) + } + Ok(()) } } @@ -1671,6 +1687,7 @@ fn stop_reroute(command: &Command) -> bool { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] enum RegistrationStep { + Start, List, Req, Sasl, diff --git a/data/src/stream.rs b/data/src/stream.rs index 1b47b9589..b30c6a4a1 100644 --- a/data/src/stream.rs +++ b/data/src/stream.rs @@ -280,12 +280,17 @@ async fn connect( let (sender, receiver) = mpsc::channel(100); + let mut client = Client::new(server, config, sender); + if let Err(e) = client.connect() { + log::error!("Error when connecting client: {:?}", e); + } + Ok(( Stream { connection, receiver, }, - Client::new(server, config, sender), + client, )) } diff --git a/data/src/user.rs b/data/src/user.rs index f419caf49..4c00bf709 100644 --- a/data/src/user.rs +++ b/data/src/user.rs @@ -2,6 +2,8 @@ use std::collections::HashSet; use std::fmt; use std::hash::Hash; +use thiserror::Error; + use irc::proto; use itertools::sorted; use serde::{Deserialize, Serialize}; @@ -49,8 +51,16 @@ impl PartialOrd for User { } } +#[derive(Error, Debug)] +pub enum TryFromUserError { + #[error("nickname can't be empty")] + NicknameEmpty, + #[error("nickname must start with alphabetic or [ \\ ] ^ _ ` {{ | }} *")] + NicknameInvalidCharacter, +} + impl TryFrom for User { - type Error = &'static str; + type Error = TryFromUserError; fn try_from(value: String) -> Result { Self::try_from(value.as_str()) @@ -58,17 +68,17 @@ impl TryFrom for User { } impl<'a> TryFrom<&'a str> for User { - type Error = &'static str; + type Error = TryFromUserError; fn try_from(value: &'a str) -> Result { if value.is_empty() { - return Err("nickname can't be empty"); + return Err(Self::Error::NicknameEmpty); } let Some(index) = value.find(|c: char| c.is_alphabetic() || "[\\]^_`{|}*".find(c).is_some()) else { - return Err("nickname must start with alphabetic or [ \\ ] ^ _ ` { | } *"); + return Err(Self::Error::NicknameInvalidCharacter); }; let (access_levels, rest) = (&value[..index], &value[index..]); diff --git a/src/main.rs b/src/main.rs index 25ff72dd3..397d0e4d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -210,6 +210,7 @@ pub enum Message { AppearanceReloaded(data::appearance::Appearance), ScreenConfigReloaded(Result), Dashboard(dashboard::Message), + IrcError(anyhow::Error), Stream(stream::Update), Help(help::Message), Welcome(welcome::Message), @@ -345,6 +346,7 @@ impl Halloy { self.clients.quit(&server, None); Task::none() } + Some(dashboard::Event::IrcError(e)) => Task::done(Message::IrcError(e)), Some(dashboard::Event::Exit) => { let pending_exit = self.clients.exit(); @@ -489,9 +491,14 @@ impl Halloy { let commands = messages .into_iter() .flat_map(|message| { + let events = match self.clients.receive(&server, message) { + Ok(events) => events, + Err(e) => return vec![Task::done(Message::IrcError(e))], + }; + let mut commands = vec![]; - for event in self.clients.receive(&server, message) { + for event in events { // Resolve a user using client state which stores attributes let resolve_user_attributes = |user: &User, channel: &str| { self.clients @@ -816,9 +823,9 @@ impl Halloy { Task::none() } Message::Tick(now) => { - self.clients.tick(now); - - if let Screen::Dashboard(dashboard) = &mut self.screen { + if let Err(e) = self.clients.tick(now) { + Task::done(Message::IrcError(e)) + } else if let Screen::Dashboard(dashboard) = &mut self.screen { dashboard.tick(now).map(Message::Dashboard) } else { Task::none() @@ -938,6 +945,10 @@ impl Halloy { ) .map(Message::Dashboard) } + Message::IrcError(e) => { + self.modal = Some(Modal::IrcError(e)); + Task::none() + } } } diff --git a/src/modal.rs b/src/modal.rs index 1f3686211..3de38e36f 100644 --- a/src/modal.rs +++ b/src/modal.rs @@ -2,11 +2,12 @@ use crate::widget::Element; use data::{config, Server}; pub mod connect_to_server; -pub mod reload_configuration_error; +pub mod error; #[derive(Debug)] pub enum Modal { ReloadConfigurationError(config::Error), + IrcError(anyhow::Error), ServerConnect { url: String, server: Server, @@ -43,7 +44,8 @@ impl Modal { pub fn view(&self) -> Element { match self { - Modal::ReloadConfigurationError(error) => reload_configuration_error::view(error), + Modal::ReloadConfigurationError(error) => error::view("Error reloading configuration file", error), + Modal::IrcError(error) => error::view("IRC Error", &**error), Modal::ServerConnect { url: raw, config, .. } => connect_to_server::view(raw, config), diff --git a/src/modal/reload_configuration_error.rs b/src/modal/error.rs similarity index 85% rename from src/modal/reload_configuration_error.rs rename to src/modal/error.rs index c6896e1bc..3e352dc09 100644 --- a/src/modal/reload_configuration_error.rs +++ b/src/modal/error.rs @@ -1,4 +1,5 @@ -use data::config; +use std::error; + use iced::{ alignment, widget::{button, column, container, text}, @@ -8,10 +9,10 @@ use iced::{ use super::Message; use crate::{theme, widget::Element}; -pub fn view<'a>(error: &config::Error) -> Element<'a, Message> { +pub fn view<'a>(title: &'a str, error: impl error::Error) -> Element<'a, Message> { container( column![ - text("Error reloading configuration file"), + text(title), text(error.to_string()).style(theme::text::error), button( container(text("Close")) diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index d19161f28..9842232f6 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -66,6 +66,7 @@ pub enum Event { ConfigReloaded(Result), ReloadThemes, QuitServer(Server), + IrcError(anyhow::Error), Exit, } @@ -593,7 +594,9 @@ impl Dashboard { if let Some(((server, target), read_marker)) = kind.server().zip(kind.target()).zip(read_marker) { - clients.send_markread(server, target, read_marker); + if let Err(e) = clients.send_markread(server, target, read_marker) { + return (Task::none(), Some(Event::IrcError(e))); + }; } } history::manager::Event::Exited(results) => { @@ -601,7 +604,9 @@ impl Dashboard { if let Some(((server, target), read_marker)) = kind.server().zip(kind.target()).zip(read_marker) { - clients.send_markread(server, target, read_marker); + if let Err(e) = clients.send_markread(server, target, read_marker) { + return (Task::none(), Some(Event::IrcError(e))); + }; } }