From cdd5778ac54f958154f5dbbee24102bd5370b40a Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Sun, 4 Aug 2024 11:18:15 -0700 Subject: [PATCH 1/2] Support for IRCv3 `account-notify` and `extended-join`. --- CHANGELOG.md | 2 +- README.md | 2 + book/src/README.md | 2 + data/src/client.rs | 97 ++++++++++++++++++++++++++++++++++++++-- data/src/message.rs | 1 + data/src/user.rs | 21 +++++++++ irc/proto/src/command.rs | 8 +++- 7 files changed, 128 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce8882f01..bfcdc1207 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Added: - Small icon in sidemenu when a new release is available -- Enable support for IRCv3 `chghost` +- Enable support for IRCv3 `chghost`, `account-notify`, and `extended-join` Removed: diff --git a/README.md b/README.md index c2bd01b91..b9caac634 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ Halloy is also available from [Flathub](https://flathub.org/apps/org.squidowl.ha * [`WHOX`](https://ircv3.net/specs/extensions/whox) * [`UTF8ONLY`](https://ircv3.net/specs/extensions/utf8-only) * [chghost](https://ircv3.net/specs/extensions/chghost) + * [account-notify](https://ircv3.net/specs/extensions/account-notify) + * [extended-join](https://ircv3.net/specs/extensions/extended-join) * SASL support * DCC Send * Keyboard shortcuts diff --git a/book/src/README.md b/book/src/README.md index a0a3117e6..6b96031ca 100644 --- a/book/src/README.md +++ b/book/src/README.md @@ -19,6 +19,8 @@ * [`WHOX`](https://ircv3.net/specs/extensions/whox) * [`UTF8ONLY`](https://ircv3.net/specs/extensions/utf8-only) * [chghost](https://ircv3.net/specs/extensions/chghost) + * [account-notify](https://ircv3.net/specs/extensions/account-notify) + * [extended-join](https://ircv3.net/specs/extensions/extended-join) * SASL support * DCC Send * Keyboard shortcuts diff --git a/data/src/client.rs b/data/src/client.rs index 9c0e1f47b..06c08a523 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -94,6 +94,8 @@ pub struct Client { listed_caps: Vec, supports_labels: bool, supports_away_notify: bool, + supports_account_notify: bool, + supports_extended_join: bool, highlight_blackout: HighlightBlackout, registration_required_channels: Vec, isupport: HashMap, @@ -144,6 +146,8 @@ impl Client { listed_caps: vec![], supports_labels: false, supports_away_notify: false, + supports_account_notify: false, + supports_extended_join: false, highlight_blackout: HighlightBlackout::Blackout(Instant::now()), registration_required_channels: vec![], isupport: HashMap::new(), @@ -333,6 +337,13 @@ impl Client { if contains("chghost") { requested.push("chghost"); } + if contains("account-notify") { + requested.push("account-notify"); + + if contains("extended-join") { + requested.push("extended-join"); + } + } if contains("batch") { requested.push("batch"); } @@ -376,6 +387,12 @@ impl Client { if caps.contains(&"away-notify") { self.supports_away_notify = true; } + if caps.contains(&"account-notify") { + self.supports_account_notify = true; + } + if caps.contains(&"extended-join") { + self.supports_extended_join = true; + } let supports_sasl = caps.iter().any(|cap| cap.contains("sasl")); @@ -428,6 +445,15 @@ impl Client { if newly_contains("chghost") { requested.push("chghost"); } + if contains("account-notify") || newly_contains("account-notify") { + if newly_contains("account-notify") { + requested.push("account-notify"); + } + + if newly_contains("extended-join") { + requested.push("extended-join"); + } + } if newly_contains("batch") { requested.push("batch"); } @@ -465,6 +491,12 @@ impl Client { if del_caps.contains(&"away-notify") { self.supports_away_notify = false; } + if del_caps.contains(&"account-notify") { + self.supports_account_notify = false; + } + if del_caps.contains(&"extended-join") { + self.supports_extended_join = false; + } self.listed_caps .retain(|cap| !del_caps.iter().any(|del_cap| del_cap == cap)); @@ -738,7 +770,7 @@ impl Client { channel.users.remove(&user); } } - Command::JOIN(channel, _) => { + Command::JOIN(channel, accountname) => { let user = message.user()?; if user.nickname() == self.nickname() { @@ -747,12 +779,19 @@ impl Client { if let Some(state) = self.chanmap.get_mut(channel) { // Sends WHO to get away state on users. if self.isupport.contains_key(&isupport::Kind::WHOX) { + let fields = if self.supports_account_notify { + "tcnfa" + } else { + "tcnf" + }; + let _ = self.handle.try_send(command!( "WHO", channel, - "tcnf", + fields, isupport::WHO_POLL_TOKEN.to_owned() )); + state.last_who = Some(WhoStatus::Requested( Instant::now(), Some(isupport::WHO_POLL_TOKEN), @@ -764,6 +803,14 @@ impl Client { log::debug!("[{}] {channel} - WHO requested", self.server); } } 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| { + user.with_accountname(accountname) + }) + } else { + user + }; + channel.users.insert(user); } } @@ -802,6 +849,12 @@ impl Client { if let Some(channel) = self.chanmap.get_mut(target) { channel.update_user_away(args.get(3)?, args.get(4)?); + if self.supports_account_notify { + if let (Some(user), Some(accountname)) = (args.get(3), args.get(5)) { + channel.update_user_accountname(user, accountname); + } + } + if let Ok(token) = args.get(1)?.parse::() { if let Some(WhoStatus::Requested(_, Some(request_token))) = channel.last_who @@ -1020,6 +1073,29 @@ impl Client { Command::TAGMSG(_) => { return None; } + Command::ACCOUNT(accountname) => { + let old_user = message.user()?; + + self.chanmap.values_mut().for_each(|channel| { + if let Some(user) = channel.users.take(&old_user) { + channel.users.insert(user.with_accountname(accountname)); + } + }); + + if old_user.nickname() == self.nickname() + && accountname != "*" + && !self.registration_required_channels.is_empty() + { + for message in group_joins( + &self.registration_required_channels, + &self.config.channel_keys, + ) { + let _ = self.handle.try_send(message); + } + + self.registration_required_channels.clear(); + } + } Command::CHGHOST(new_username, new_hostname) => { let old_user = message.user()?; @@ -1136,12 +1212,19 @@ impl Client { if let Some(request) = request { if self.isupport.contains_key(&isupport::Kind::WHOX) { + let fields = if self.supports_account_notify { + "tcnfa" + } else { + "tcnf" + }; + let _ = self.handle.try_send(command!( "WHO", channel, - "tcnf", + fields, isupport::WHO_POLL_TOKEN.to_owned() )); + state.last_who = Some(WhoStatus::Requested( Instant::now(), Some(isupport::WHO_POLL_TOKEN), @@ -1438,6 +1521,14 @@ impl Channel { } } } + + pub fn update_user_accountname(&mut self, user: &str, accountname: &str) { + let user = User::from(Nick::from(user)); + + if let Some(user) = self.users.take(&user) { + self.users.insert(user.with_accountname(accountname)); + } + } } #[derive(Default, Debug, Clone)] diff --git a/data/src/message.rs b/data/src/message.rs index e2e476980..9c64f7224 100644 --- a/data/src/message.rs +++ b/data/src/message.rs @@ -498,6 +498,7 @@ fn target( | Command::USERHOST(_) | Command::CAP(_, _, _, _) | Command::AUTHENTICATE(_) + | Command::ACCOUNT(_) | Command::BATCH(_, _) | Command::CHGHOST(_, _) | Command::CNOTICE(_, _, _) diff --git a/data/src/user.rs b/data/src/user.rs index cf4e60a66..20228b1a5 100644 --- a/data/src/user.rs +++ b/data/src/user.rs @@ -19,6 +19,7 @@ pub struct User { nickname: Nick, username: Option, hostname: Option, + accountname: Option, access_levels: HashSet, away: bool, } @@ -93,6 +94,7 @@ impl<'a> TryFrom<&'a str> for User { nickname: Nick::from(nickname), username, hostname, + accountname: None, access_levels, away: false, }) @@ -118,6 +120,7 @@ impl From for User { nickname, username: None, hostname: None, + accountname: None, access_levels: HashSet::default(), away: false, } @@ -157,6 +160,10 @@ impl User { self.hostname.as_deref() } + pub fn accountname(&self) -> Option<&str> { + self.accountname.as_deref() + } + pub fn with_nickname(self, nickname: Nick) -> Self { Self { nickname, ..self } } @@ -169,6 +176,19 @@ impl User { } } + pub fn with_accountname(self, accountname: &str) -> Self { + let accountname = if accountname == "*" || accountname == "0" { + None + } else { + Some(accountname.to_string()) + }; + + Self { + accountname, + ..self + } + } + pub fn highest_access_level(&self) -> AccessLevel { self.access_levels .iter() @@ -221,6 +241,7 @@ impl From for User { nickname: Nick::from(user.nickname), username: user.username, hostname: user.hostname, + accountname: None, access_levels: HashSet::default(), away: false, } diff --git a/irc/proto/src/command.rs b/irc/proto/src/command.rs index b328b047a..afc7b987f 100644 --- a/irc/proto/src/command.rs +++ b/irc/proto/src/command.rs @@ -24,7 +24,8 @@ pub enum Command { ERROR(String), /* Channel Operations */ - /// {,} [{,}] + /// {,} [{,}] (send) + /// {,} [] (receive [extended-join]) JOIN(String, Option), /// {,} [] PART(String, Option), @@ -94,6 +95,8 @@ pub enum Command { /// WALLOPS(String), + /// + ACCOUNT(String), /* IRC extensions */ BATCH(String, Vec), /// @@ -194,6 +197,7 @@ impl Command { "LINKS" => LINKS, "USERHOST" => USERHOST(params.collect()), "WALLOPS" if len > 0 => WALLOPS(req!()), + "ACCOUNT" if len > 0 => ACCOUNT(req!()), "BATCH" if len > 0 => BATCH(req!(), params.collect()), "CHGHOST" if len > 1 => CHGHOST(req!(), req!()), "CNOTICE" if len > 2 => CNOTICE(req!(), req!(), req!()), @@ -249,6 +253,7 @@ impl Command { Command::LINKS => vec![], Command::USERHOST(params) => params, Command::WALLOPS(a) => vec![a], + Command::ACCOUNT(a) => vec![a], Command::BATCH(a, rest) => std::iter::once(a).chain(rest).collect(), Command::CHGHOST(a, b) => vec![a, b], Command::CNOTICE(a, b, c) => vec![a, b, c], @@ -306,6 +311,7 @@ impl Command { LINKS => "LINKS".to_string(), USERHOST(_) => "USERHOST".to_string(), WALLOPS(_) => "WALLOPS".to_string(), + ACCOUNT(_) => "ACCOUNT".to_string(), BATCH(_, _) => "BATCH".to_string(), CHGHOST(_, _) => "CHGHOST".to_string(), CNOTICE(_, _, _) => "CNOTICE".to_string(), From deb2be7ea9d4a903ba0184c9fc1d16cacb3c871e Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Sun, 4 Aug 2024 11:37:54 -0700 Subject: [PATCH 2/2] Update `accountname` via `RPL_LOGGEDIN` and `RPL_LOGGEDOUT` when `account-notify` is not available. --- data/src/client.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/data/src/client.rs b/data/src/client.rs index 06c08a523..443365e0f 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -510,7 +510,7 @@ impl Client { let _ = self.handle.try_send(command!("CAP", "END")); } } - Command::Numeric(RPL_LOGGEDIN, _) => { + Command::Numeric(RPL_LOGGEDIN, args) => { log::info!("[{}] logged in", self.server); if !self.registration_required_channels.is_empty() { @@ -523,6 +523,31 @@ impl Client { self.registration_required_channels.clear(); } + + if !self.supports_account_notify { + let accountname = args.first()?; + + let old_user = User::from(self.nickname().to_owned()); + + self.chanmap.values_mut().for_each(|channel| { + if let Some(user) = channel.users.take(&old_user) { + channel.users.insert(user.with_accountname(accountname)); + } + }); + } + } + Command::Numeric(RPL_LOGGEDOUT, _) => { + log::info!("[{}] logged out", self.server); + + if !self.supports_account_notify { + let old_user = User::from(self.nickname().to_owned()); + + self.chanmap.values_mut().for_each(|channel| { + if let Some(user) = channel.users.take(&old_user) { + channel.users.insert(user.with_accountname("*")); + } + }); + } } Command::PRIVMSG(channel, text) | Command::NOTICE(channel, text) => { if let Some(user) = message.user() {