diff --git a/data/src/buffer.rs b/data/src/buffer.rs index 68313b681..14752135b 100644 --- a/data/src/buffer.rs +++ b/data/src/buffer.rs @@ -75,7 +75,7 @@ impl Upstream { Self::Channel(_, channel) => message::Target::Channel { channel, source: message::Source::Server(source), - prefix: None, + prefixes: Default::default(), }, Self::Query(_, nick) => message::Target::Query { nick, diff --git a/data/src/client.rs b/data/src/client.rs index 979a7a2ff..2734e4fa9 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Utc}; use futures::channel::mpsc; use irc::proto::{self, command, Command}; use itertools::{Either, Itertools}; +use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt; use std::time::{Duration, Instant}; @@ -188,6 +189,17 @@ impl Client { } } + fn start_reroute(&self, command: &Command) -> bool { + use Command::*; + + if let MODE(target, _, _) = command { + !self.is_channel(target) + } else { + matches!(command, WHO(..) | WHOIS(..) | WHOWAS(..)) + } + } + + fn send(&mut self, buffer: &buffer::Upstream, mut message: message::Encoded) { if self.supports_labels { use proto::Tag; @@ -204,7 +216,7 @@ impl Client { }]; } - self.reroute_responses_to = start_reroute(&message.command).then(|| buffer.clone()); + self.reroute_responses_to = self.start_reroute(&message.command).then(|| buffer.clone()); if let Err(e) = self.handle.try_send(message.into()) { log::warn!("Error sending message: {e}"); @@ -896,7 +908,7 @@ impl Client { Command::Numeric(RPL_WHOREPLY, args) => { let target = args.get(1)?; - if proto::is_channel(target) { + if self.is_channel(target) { if let Some(channel) = self.chanmap.get_mut(target) { channel.update_user_away(args.get(5)?, args.get(6)?); @@ -915,7 +927,7 @@ impl Client { Command::Numeric(RPL_WHOSPCRPL, args) => { let target = args.get(2)?; - if proto::is_channel(target) { + if self.is_channel(target) { if let Some(channel) = self.chanmap.get_mut(target) { channel.update_user_away(args.get(3)?, args.get(4)?); @@ -947,7 +959,7 @@ impl Client { Command::Numeric(RPL_ENDOFWHO, args) => { let target = args.get(1)?; - if proto::is_channel(target) { + if self.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())); @@ -995,7 +1007,7 @@ impl Client { } } Command::MODE(target, Some(modes), Some(args)) => { - if proto::is_channel(target) { + if self.is_channel(target) { let modes = mode::parse::(modes, args); if let Some(channel) = self.chanmap.get_mut(target) { @@ -1053,7 +1065,7 @@ impl Client { Command::Numeric(RPL_ENDOFNAMES, args) => { let target = args.get(1)?; - if proto::is_channel(target) { + if self.is_channel(target) { if let Some(channel) = self.chanmap.get_mut(target) { if !channel.names_init { channel.names_init = true; @@ -1276,8 +1288,27 @@ impl Client { } } + // TODO allow configuring the "sorting method" + // this function sorts channels together which have similar names when the chantype prefix + // (sometimes multipled) is removed + // e.g. '#chat', '##chat-offtopic' and '&chat-local' all get sorted together instead of in + // wildly different places. + fn compare_channels(&self, a: &str, b: &str) -> Ordering { + let (Some(a_chantype), Some(b_chantype)) = (a.chars().nth(0), b.chars().nth(0)) else { + return a.cmp(b); + }; + + if [a_chantype, b_chantype].iter().all(|c| self.chantypes().contains(c)) { + let ord = a.trim_start_matches(a_chantype).cmp(b.trim_start_matches(b_chantype)); + if ord != Ordering::Equal { + return ord; + } + } + a.cmp(b) + } + fn sync(&mut self) { - self.channels = self.chanmap.keys().cloned().collect(); + self.channels = self.chanmap.keys().cloned().sorted_by(|a, b| self.compare_channels(a, b)).collect(); self.users = self .chanmap .iter() @@ -1395,6 +1426,28 @@ impl Client { } } } + + pub fn chantypes(&self) -> &[char] { + self.isupport.get(&isupport::Kind::CHANTYPES).and_then(|chantypes| { + let isupport::Parameter::CHANTYPES(types) = chantypes else { + unreachable!("Corruption in isupport table.") + }; + types.as_deref() + }).unwrap_or(proto::DEFAULT_CHANNEL_PREFIXES) + } + + pub fn statusmsg(&self) -> &[char] { + self.isupport.get(&isupport::Kind::STATUSMSG).map(|statusmsg| { + let isupport::Parameter::STATUSMSG(prefixes) = statusmsg else { + unreachable!("Corruption in isupport table.") + }; + prefixes.as_ref() + }).unwrap_or(&[]) + } + + pub fn is_channel(&self, target: &str) -> bool { + proto::is_channel(target, self.chantypes()) + } } #[derive(Debug)] @@ -1549,6 +1602,18 @@ impl Map { .unwrap_or_default() } + pub fn get_chantypes<'a>(&'a self, server: &Server) -> &'a [char] { + self.client(server) + .map(|client| client.chantypes()) + .unwrap_or_default() + } + + pub fn get_statusmsg<'a>(&'a self, server: &Server) -> &'a [char] { + self.client(server) + .map(|client| client.statusmsg()) + .unwrap_or_default() + } + pub fn get_server_handle(&self, server: &Server) -> Option<&server::Handle> { self.client(server).map(|client| &client.handle) } @@ -1637,16 +1702,6 @@ fn remove_tag(key: &str, tags: &mut Vec) -> Option { .value } -fn start_reroute(command: &Command) -> bool { - use Command::*; - - if let MODE(target, _, _) = command { - !proto::is_channel(target) - } else { - matches!(command, WHO(..) | WHOIS(..) | WHOWAS(..)) - } -} - fn stop_reroute(command: &Command) -> bool { use command::Numeric::*; diff --git a/data/src/history.rs b/data/src/history.rs index 98d6737ad..c074814f6 100644 --- a/data/src/history.rs +++ b/data/src/history.rs @@ -36,8 +36,8 @@ pub enum Kind { } impl Kind { - pub fn from_target(server: Server, target: String) -> Self { - if proto::is_channel(&target) { + pub fn from_target(server: Server, target: String, chantypes: &[char]) -> Self { + if proto::is_channel(&target, chantypes) { Self::Channel(server, target) } else { Self::Query(server, Nick::from(target)) diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index b8aae6cb5..9957bd195 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -184,10 +184,12 @@ impl Manager { input: Input, user: User, channel_users: &[User], + chantypes: &[char], + statusmsg: &[char], ) -> Vec> { let mut tasks = vec![]; - if let Some(messages) = input.messages(user, channel_users) { + if let Some(messages) = input.messages(user, channel_users, chantypes, statusmsg) { for message in messages { tasks.extend(self.record_message(input.server(), message)); } @@ -603,11 +605,11 @@ impl Data { filtered .iter() .filter_map(|message| { - message.target.prefix().map(|prefix| { + message.target.prefixes().map(|prefixes| { buffer_config .status_message_prefix .brackets - .format(prefix) + .format(prefixes.iter().collect::()) .chars() .count() + 1 diff --git a/data/src/input.rs b/data/src/input.rs index f4b344c35..0420c275a 100644 --- a/data/src/input.rs +++ b/data/src/input.rs @@ -63,13 +63,13 @@ impl Input { self.buffer.server() } - pub fn messages(&self, user: User, channel_users: &[User]) -> Option> { + pub fn messages(&self, user: User, channel_users: &[User], chantypes: &[char], statusmsg: &[char]) -> Option> { let to_target = |target: &str, source| { - if let Some((prefix, channel)) = proto::parse_channel_from_target(target) { + if let Some((prefixes, channel)) = proto::parse_channel_from_target(target, chantypes, statusmsg) { Some(message::Target::Channel { channel, source, - prefix, + prefixes, }) } else if let Ok(user) = User::try_from(target) { Some(message::Target::Query { diff --git a/data/src/isupport.rs b/data/src/isupport.rs index ac4c94474..946f1eec3 100644 --- a/data/src/isupport.rs +++ b/data/src/isupport.rs @@ -1,4 +1,3 @@ -use irc::proto; use std::str::FromStr; // Utilized ISUPPORT parameters should have an associated Kind enum variant @@ -9,6 +8,7 @@ pub enum Kind { AWAYLEN, CHANLIMIT, CHANNELLEN, + CHANTYPES, CNOTICE, CPRIVMSG, ELIST, @@ -88,23 +88,21 @@ impl FromStr for Operation { value.split(',').for_each(|channel_limit| { if let Some((prefix, limit)) = channel_limit.split_once(':') { if limit.is_empty() { - prefix.chars().for_each(|c| { - if proto::CHANNEL_PREFIXES.contains(&c) { - channel_limits.push(ChannelLimit { - prefix: c, - limit: None, - }); - } - }); + for c in prefix.chars() { + // TODO validate after STATUSMSG received + channel_limits.push(ChannelLimit { + prefix: c, + limit: None, + }); + } } else if let Ok(limit) = limit.parse::() { - prefix.chars().for_each(|c| { - if proto::CHANNEL_PREFIXES.contains(&c) { - channel_limits.push(ChannelLimit { - prefix: c, - limit: Some(limit), - }); - } - }); + for c in prefix.chars() { + // TODO validate after STATUSMSG received + channel_limits.push(ChannelLimit { + prefix: c, + limit: Some(limit), + }); + } } } }); @@ -139,14 +137,12 @@ impl FromStr for Operation { parse_required_positive_integer(value)?, ))), "CHANTYPES" => { - if value.is_empty() { + let chars = value.chars().collect::>(); + if chars.is_empty() { Ok(Operation::Add(Parameter::CHANTYPES(None))) - } else if value.chars().all(|c| proto::CHANNEL_PREFIXES.contains(&c)) { - Ok(Operation::Add(Parameter::CHANTYPES(Some( - value.to_string(), - )))) } else { - Err("value must only contain channel types if specified") + // TODO validate after STATUSMSG is received + Ok(Operation::Add(Parameter::CHANTYPES(Some(chars)))) } } "CHATHISTORY" => Ok(Operation::Add(Parameter::CHATHISTORY( @@ -331,13 +327,9 @@ impl FromStr for Operation { let mut prefix_maps = vec![]; if let Some((modes, prefixes)) = value.split_once(')') { - modes.chars().skip(1).zip(prefixes.chars()).for_each( - |(mode, prefix)| { - if proto::CHANNEL_MEMBERSHIP_PREFIXES.contains(&prefix) { - prefix_maps.push(PrefixMap { mode, prefix }) - } - }, - ); + for (mode, prefix) in modes.chars().skip(1).zip(prefixes.chars()) { + prefix_maps.push(PrefixMap { mode, prefix }) + } Ok(Operation::Add(Parameter::PREFIX(prefix_maps))) } else { @@ -350,14 +342,9 @@ impl FromStr for Operation { parse_optional_positive_integer(value)?, ))), "STATUSMSG" => { - if value - .chars() - .all(|c| proto::CHANNEL_MEMBERSHIP_PREFIXES.contains(&c)) - { - Ok(Operation::Add(Parameter::STATUSMSG(value.to_string()))) - } else { - Err("unknown channel membership prefix(es)") - } + let chars = value.chars().collect::>(); + // TODO validate that STATUSMSG ⊂ PREFIX after ISUPPORT ends + Ok(Operation::Add(Parameter::STATUSMSG(chars))) } "TARGMAX" => { let mut command_target_limits = vec![]; @@ -485,6 +472,7 @@ impl Operation { "AWAYLEN" => Some(Kind::AWAYLEN), "CHANLIMIT" => Some(Kind::CHANLIMIT), "CHANNELLEN" => Some(Kind::CHANNELLEN), + "CHANTYPES" => Some(Kind::CHANTYPES), "CNOTICE" => Some(Kind::CNOTICE), "CPRIVMSG" => Some(Kind::CPRIVMSG), "ELIST" => Some(Kind::ELIST), @@ -526,7 +514,7 @@ pub enum Parameter { CHANLIMIT(Vec), CHANMODES(Vec), CHANNELLEN(u16), - CHANTYPES(Option), + CHANTYPES(Option>), CHATHISTORY(u16), CLIENTTAGDENY(Vec), CLIENTVER(u16, u16), @@ -563,7 +551,7 @@ pub enum Parameter { SAFELIST, SECURELIST, SILENCE(Option), - STATUSMSG(String), + STATUSMSG(Vec), TARGMAX(Vec), TOPICLEN(u16), UHNAMES, diff --git a/data/src/message.rs b/data/src/message.rs index 9542b989b..c3678e579 100644 --- a/data/src/message.rs +++ b/data/src/message.rs @@ -111,7 +111,7 @@ pub enum Target { Channel { channel: Channel, source: Source, - prefix: Option, + prefixes: Vec, }, Query { nick: Nick, @@ -126,10 +126,10 @@ pub enum Target { } impl Target { - pub fn prefix(&self) -> Option<&char> { + pub fn prefixes(&self) -> Option<&[char]> { match self { Target::Server { .. } => None, - Target::Channel { prefix, .. } => prefix.as_ref(), + Target::Channel { prefixes, .. } => Some(prefixes), Target::Query { .. } => None, Target::Logs => None, Target::Highlights { .. } => None, @@ -188,6 +188,8 @@ impl Message { config: &'a Config, resolve_attributes: impl Fn(&User, &str) -> Option, channel_users: impl Fn(&str) -> &'a [User], + chantypes: &[char], + statusmsg: &[char], ) -> Option { let server_time = server_time(&encoded); let id = message_id(&encoded); @@ -197,8 +199,9 @@ impl Message { config, &resolve_attributes, &channel_users, + chantypes, )?; - let target = target(encoded, &our_nick, &resolve_attributes)?; + let target = target(encoded, &our_nick, &resolve_attributes, chantypes, statusmsg)?; let received_at = Posix::now(); let hash = Hash::new(&received_at, &content); @@ -610,6 +613,8 @@ fn target( message: Encoded, our_nick: &Nick, resolve_attributes: &dyn Fn(&User, &str) -> Option, + chantypes: &[char], + statusmsg: &[char], ) -> Option { use proto::command::Numeric::*; @@ -617,15 +622,15 @@ fn target( match message.0.command { // Channel - Command::MODE(target, ..) if proto::is_channel(&target) => Some(Target::Channel { + Command::MODE(target, ..) if proto::is_channel(&target, chantypes) => Some(Target::Channel { channel: target, source: source::Source::Server(None), - prefix: None, + prefixes: Default::default(), }), Command::TOPIC(channel, _) | Command::KICK(channel, _, _) => Some(Target::Channel { channel, source: source::Source::Server(None), - prefix: None, + prefixes: Default::default(), }), Command::PART(channel, _) => Some(Target::Channel { channel, @@ -633,7 +638,7 @@ fn target( source::server::Kind::Part, Some(user?.nickname().to_owned()), ))), - prefix: None, + prefixes: Default::default(), }), Command::JOIN(channel, _) => Some(Target::Channel { channel, @@ -641,7 +646,7 @@ fn target( source::server::Kind::Join, Some(user?.nickname().to_owned()), ))), - prefix: None, + prefixes: Default::default(), }), Command::Numeric(RPL_TOPIC | RPL_TOPICWHOTIME, params) => { let channel = params.get(1)?.clone(); @@ -651,7 +656,7 @@ fn target( source::server::Kind::ReplyTopic, None, ))), - prefix: None, + prefixes: Default::default(), }) } Command::Numeric(RPL_CHANNELMODEIS, params) => { @@ -659,7 +664,7 @@ fn target( Some(Target::Channel { channel, source: source::Source::Server(None), - prefix: None, + prefixes: Default::default(), }) } Command::Numeric(RPL_AWAY, params) => { @@ -681,13 +686,13 @@ fn target( } }; - match (proto::parse_channel_from_target(&target), user) { - (Some((prefix, channel)), Some(user)) => { + match (proto::parse_channel_from_target(&target, chantypes, statusmsg), user) { + (Some((prefixes, channel)), Some(user)) => { let source = source(resolve_attributes(&user, &channel).unwrap_or(user)); Some(Target::Channel { channel, source, - prefix, + prefixes, }) } (None, Some(user)) => { @@ -715,13 +720,13 @@ fn target( } }; - match (proto::parse_channel_from_target(&target), user) { - (Some((prefix, channel)), Some(user)) => { + match (proto::parse_channel_from_target(&target, chantypes, statusmsg), user) { + (Some((prefixes, channel)), Some(user)) => { let source = source(resolve_attributes(&user, &channel).unwrap_or(user)); Some(Target::Channel { channel, source, - prefix, + prefixes, }) } (None, Some(user)) => { @@ -833,6 +838,7 @@ fn content<'a>( config: &Config, resolve_attributes: &dyn Fn(&User, &str) -> Option, channel_users: &dyn Fn(&str) -> &'a [User], + chantypes: &[char], ) -> Option { use irc::proto::command::Numeric::*; @@ -902,7 +908,7 @@ fn content<'a>( &[], )) } - Command::MODE(target, modes, args) if proto::is_channel(target) => { + Command::MODE(target, modes, args) if proto::is_channel(target, chantypes) => { let raw_user = message.user()?; let with_access_levels = config.buffer.nickname.show_access_levels; let user = resolve_attributes(&raw_user, target) diff --git a/data/src/message/broadcast.rs b/data/src/message/broadcast.rs index b0269c8d0..29671c28e 100644 --- a/data/src/message/broadcast.rs +++ b/data/src/message/broadcast.rs @@ -47,7 +47,7 @@ fn expand( Target::Channel { channel, source: source.clone(), - prefix: None, + prefixes: Default::default(), }, content.clone(), ) diff --git a/irc/proto/src/lib.rs b/irc/proto/src/lib.rs index a951f09e1..1bd31b29e 100644 --- a/irc/proto/src/lib.rs +++ b/irc/proto/src/lib.rs @@ -47,35 +47,44 @@ pub fn command(command: &str, parameters: Vec) -> Message { command: Command::new(command, parameters), } } - /// Reference: https://defs.ircdocs.horse/defs/chantypes -pub const CHANNEL_PREFIXES: [char; 4] = ['#', '&', '+', '!']; +/// +/// Channel types which should be used if the CHANTYPES ISUPPORT is not returned +pub const DEFAULT_CHANNEL_PREFIXES: &[char] = &['#', '&']; + /// https://modern.ircdocs.horse/#channels /// /// Channel names are strings (beginning with specified prefix characters). Apart from the requirement of /// the first character being a valid channel type prefix character; the only restriction on a channel name /// is that it may not contain any spaces (' ', 0x20), a control G / BELL ('^G', 0x07), or a comma (',', 0x2C) /// (which is used as a list item separator by the protocol). -pub const CHANNEL_BLACKLIST_CHARS: [char; 3] = [',', '\u{07}', ',']; +pub const CHANNEL_BLACKLIST_CHARS: &[char] = &[',', '\u{07}', ',']; -pub fn is_channel(target: &str) -> bool { - target.starts_with(CHANNEL_PREFIXES) && !target.contains(CHANNEL_BLACKLIST_CHARS) +pub fn is_channel(target: &str, chantypes: &[char]) -> bool { + target.starts_with(chantypes) && !target.contains(CHANNEL_BLACKLIST_CHARS) } +/// https://modern.ircdocs.horse/#channels +/// +/// Given a target, split it into a channel name (beginning with a character in `chantypes`) and a +/// possible list of prefixes (given in `statusmsg_prefixes`). If these two lists overlap, the +/// behaviour is unspecified. +pub fn parse_channel_from_target( + target: &str, + chantypes: &[char], + statusmsg_prefixes: &[char], +) -> Option<(Vec, String)> { + // We parse the target by finding the first character in chantypes, and returing (even if that + // character is in statusmsg_prefixes) + // If the characters before the first chantypes character are all valid prefixes, then we have + // a valid channel name with those prefixes. + let chan_index = target.find(chantypes)?; -// Reference: https://defs.ircdocs.horse/defs/chanmembers -pub const CHANNEL_MEMBERSHIP_PREFIXES: [char; 6] = ['~', '&', '!', '@', '%', '+']; - -pub fn parse_channel_from_target(target: &str) -> Option<(Option, String)> { - if target.starts_with(CHANNEL_MEMBERSHIP_PREFIXES) { - let channel = target.strip_prefix(CHANNEL_MEMBERSHIP_PREFIXES)?; - - if is_channel(channel) { - return Some((target.chars().next(), channel.to_string())); - } - } - - if is_channel(target) { - Some((None, target.to_string())) + // This will not panic, since `find` always returns a valid codepoint index. + // We call `find` -> `split_at` because it is an _inclusive_ split, which includes the match. + // We need to return this since the channel target includes its chantype. + let (prefix, chan) = target.split_at(chan_index); + if prefix.chars().all(|ref c| statusmsg_prefixes.contains(c)) { + Some((prefix.chars().collect(), chan.to_owned())) } else { None } @@ -90,3 +99,51 @@ macro_rules! command { $crate::command($c, vec![$($p.into(),)*]) ); } + +#[cfg(test)] +mod tests { + use super::*; + + + // Reference: https://defs.ircdocs.horse/defs/chanmembers + const CHANNEL_MEMBERSHIP_PREFIXES: &[char] = &['~', '&', '!', '@', '%', '+']; + + #[test] + fn is_channel_correct() { + let chantypes = DEFAULT_CHANNEL_PREFIXES; + assert!(is_channel("#foo", chantypes)); + assert!(is_channel("&foo", chantypes)); + assert!(!is_channel("foo", chantypes)); + } + + #[test] + fn empty_chantypes() { + assert!(!is_channel("#foo", &[])); + assert!(!is_channel("&foo", &[])); + } + + #[test] + fn parse_channel() { + let chantypes = DEFAULT_CHANNEL_PREFIXES; + let prefixes = CHANNEL_MEMBERSHIP_PREFIXES; + assert_eq!( + parse_channel_from_target("#foo", chantypes, prefixes), + Some((vec![], "#foo".to_owned())) + ); + assert_eq!( + parse_channel_from_target("+%#foo", chantypes, prefixes), + Some((vec!['+', '%'], "#foo".to_owned())) + ); + assert_eq!( + parse_channel_from_target("&+%foo", chantypes, prefixes), + Some((vec![], "&+%foo".to_owned())) + ); + } + + #[test] + fn invalid_channels() { + let chantypes = DEFAULT_CHANNEL_PREFIXES; + let prefixes = CHANNEL_MEMBERSHIP_PREFIXES; + assert!(parse_channel_from_target("+%foo", chantypes, prefixes).is_none()); + } +} diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index ed79162bc..6dbed4806 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -61,7 +61,7 @@ pub fn view<'a>( selectable_text(timestamp).style(theme::selectable_text::timestamp) }); - let prefix = message.target.prefix().map_or( + let prefixes = message.target.prefixes().map_or( max_nick_width.and_then(|_| { max_prefix_width.map(|width| { selectable_text("") @@ -69,10 +69,10 @@ pub fn view<'a>( .horizontal_alignment(alignment::Horizontal::Right) }) }), - |prefix| { + |prefixes| { let text = selectable_text(format!( "{} ", - config.buffer.status_message_prefix.brackets.format(prefix) + config.buffer.status_message_prefix.brackets.format(String::from_iter(prefixes)) )) .style(theme::selectable_text::tertiary); @@ -145,7 +145,7 @@ pub fn view<'a>( let timestamp_nickname_row = row![] .push_maybe(timestamp) - .push_maybe(prefix) + .push_maybe(prefixes) .push(nick) .push(space); @@ -193,7 +193,7 @@ pub fn view<'a>( container( row![] .push_maybe(timestamp) - .push_maybe(prefix) + .push_maybe(prefixes) .push(marker) .push(space) .push(message), @@ -216,7 +216,7 @@ pub fn view<'a>( container( row![] .push_maybe(timestamp) - .push_maybe(prefix) + .push_maybe(prefixes) .push(marker) .push(space) .push(message), @@ -243,7 +243,7 @@ pub fn view<'a>( container( row![] .push_maybe(timestamp) - .push_maybe(prefix) + .push_maybe(prefixes) .push(marker) .push(space) .push(message), diff --git a/src/buffer/input_view.rs b/src/buffer/input_view.rs index bfca0cd71..29c50f057 100644 --- a/src/buffer/input_view.rs +++ b/src/buffer/input_view.rs @@ -184,6 +184,8 @@ impl State { if let Some(nick) = clients.nickname(buffer.server()) { let mut user = nick.to_owned().into(); let mut channel_users = &[][..]; + let chantypes = clients.get_chantypes(buffer.server()); + let statusmsg = clients.get_statusmsg(buffer.server()); // Resolve our attributes if sending this message in a channel if let buffer::Upstream::Channel(server, channel) = &buffer { @@ -198,7 +200,7 @@ impl State { history_task = Task::batch( history - .record_input(input, user, channel_users) + .record_input(input, user, channel_users, chantypes, statusmsg) .into_iter() .map(Task::future), ); diff --git a/src/buffer/input_view/completion.rs b/src/buffer/input_view/completion.rs index 50c92489a..d0f8e217b 100644 --- a/src/buffer/input_view/completion.rs +++ b/src/buffer/input_view/completion.rs @@ -185,19 +185,20 @@ impl Commands { } } "MSG" => { - let channel_membership_prefixes = if let Some( + let channel_membership_prefixes = + if let Some( isupport::Parameter::STATUSMSG(channel_membership_prefixes), ) = isupport.get(&isupport::Kind::STATUSMSG) { - Some(channel_membership_prefixes) + channel_membership_prefixes.clone() } else { - None + vec![] }; let target_limit = find_target_limit(isupport, "PRIVMSG"); - if channel_membership_prefixes.is_some() || target_limit.is_some() { + if !channel_membership_prefixes.is_empty() || target_limit.is_some() { return msg_command(channel_membership_prefixes, target_limit); } } @@ -1181,27 +1182,23 @@ static MONITOR_STATUS_COMMAND: Lazy = Lazy::new(|| Command { }); fn msg_command( - channel_membership_prefixes: Option<&String>, + channel_membership_prefixes: Vec, target_limit: Option<&isupport::CommandTargetLimit>, ) -> Command { let mut targets_tooltip = String::from( "comma-separated\n {user}: user directly\n {channel}: all users in channel", ); - if let Some(channel_membership_prefixes) = channel_membership_prefixes { - channel_membership_prefixes - .chars() - .for_each( - |channel_membership_prefix| match channel_membership_prefix { - '~' => targets_tooltip.push_str("\n~{channel}: all founders in channel"), - '&' => targets_tooltip.push_str("\n&{channel}: all protected users in channel"), - '!' => targets_tooltip.push_str("\n!{channel}: all protected users in channel"), - '@' => targets_tooltip.push_str("\n@{channel}: all operators in channel"), - '%' => targets_tooltip.push_str("\n%{channel}: all half-operators in channel"), - '+' => targets_tooltip.push_str("\n+{channel}: all voiced users in channel"), - _ => (), - }, - ); + for channel_membership_prefix in channel_membership_prefixes { + match channel_membership_prefix { + '~' => targets_tooltip.push_str("\n~{channel}: all founders in channel"), + '&' => targets_tooltip.push_str("\n&{channel}: all protected users in channel"), + '!' => targets_tooltip.push_str("\n!{channel}: all protected users in channel"), + '@' => targets_tooltip.push_str("\n@{channel}: all operators in channel"), + '%' => targets_tooltip.push_str("\n%{channel}: all half-operators in channel"), + '+' => targets_tooltip.push_str("\n+{channel}: all voiced users in channel"), + _ => (), + } } if let Some(target_limit) = target_limit { diff --git a/src/main.rs b/src/main.rs index 6e73acfa4..679dfd3fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -503,6 +503,9 @@ impl Halloy { self.clients.get_channel_users(&server, channel) }; + let chantypes = self.clients.get_chantypes(&server); + let statusmsg = self.clients.get_statusmsg(&server); + match event { data::client::Event::Single(encoded, our_nick) => { if let Some(message) = data::Message::received( @@ -511,6 +514,8 @@ impl Halloy { &self.config, resolve_user_attributes, channel_users, + chantypes, + statusmsg, ) { commands.push( dashboard @@ -526,6 +531,8 @@ impl Halloy { &self.config, resolve_user_attributes, channel_users, + chantypes, + statusmsg, ) { commands.push( dashboard @@ -642,6 +649,8 @@ impl Halloy { &self.config, resolve_user_attributes, channel_users, + chantypes, + statusmsg, ) { commands.push( dashboard @@ -735,6 +744,7 @@ impl Halloy { history::Kind::from_target( server.clone(), target, + chantypes, ), read_marker, ) diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index b8d89ad17..dd0174446 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -207,6 +207,8 @@ impl Dashboard { if let Some(nick) = clients.nickname(buffer.server()) { let mut user = nick.to_owned().into(); let mut channel_users = &[][..]; + let chantypes = clients.get_chantypes(buffer.server()); + let statusmsg = clients.get_statusmsg(buffer.server()); // Resolve our attributes if sending this message in a channel if let buffer::Upstream::Channel(server, channel) = @@ -225,7 +227,7 @@ impl Dashboard { } if let Some(messages) = - input.messages(user, channel_users) + input.messages(user, channel_users, chantypes, statusmsg) { let mut tasks = vec![task];