#![allow(clippy::redundant_pub_crate)]
use super::helpers::add_local_event;
use crate::app::App;
#[allow(clippy::too_many_lines)]
pub(crate) fn cmd_connect(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(
app,
"Usage: /connect <server-id|label|address>[:<port>] [-tls] [-bind=<ip>]",
);
return;
}
let target = args[0].to_lowercase();
let mut flag_tls = false;
let mut flag_bind: Option<String> = None;
for arg in args.iter().skip(1) {
if arg == "-tls" {
flag_tls = true;
} else if let Some(ip) = arg.strip_prefix("-bind=") {
flag_bind = Some(ip.to_string());
}
}
if let Some(server_config) = app.config.servers.get(&target) {
let mut cfg = server_config.clone();
if flag_tls {
cfg.tls = true;
}
if let Some(ip) = flag_bind {
cfg.bind_ip = Some(ip);
}
spawn_connection(app, &target, &cfg);
return;
}
{
let found = app
.config
.servers
.iter()
.find(|(_, srv)| srv.label.to_lowercase() == target);
if let Some((id, srv)) = found {
let id = id.clone();
let mut cfg = srv.clone();
if flag_tls {
cfg.tls = true;
}
if let Some(ip) = flag_bind {
cfg.bind_ip = Some(ip);
}
spawn_connection(app, &id, &cfg);
return;
}
}
let raw_target = &args[0]; let mut address = raw_target.clone();
let mut port: u16 = 6667;
let mut tls = flag_tls;
if let Some(colon_pos) = raw_target.rfind(':') {
let port_str = &raw_target[colon_pos + 1..];
if let Ok(p) = port_str.parse::<u16>() {
address = raw_target[..colon_pos].to_string();
port = p;
}
}
if args.len() > 1
&& !args[1].starts_with('-')
&& let Ok(p) = args[1].parse::<u16>()
{
port = p;
}
if tls && port == 6667 {
port = 6697;
}
if port == 6697 && !tls {
tls = true;
}
let conn_id: String = address
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
if app.irc_handles.contains_key(&conn_id) {
add_local_event(app, &format!("Already connected to {address}"));
return;
}
let adhoc_config = crate::config::ServerConfig {
label: address.clone(),
address,
port,
tls,
tls_verify: true,
autoconnect: false,
channels: vec![],
nick: None,
username: None,
realname: None,
password: None,
sasl_user: None,
sasl_pass: None,
bind_ip: flag_bind,
encoding: None,
auto_reconnect: None,
reconnect_delay: None,
reconnect_max_retries: None,
autosendcmd: None,
sasl_mechanism: None,
client_cert_path: None,
};
spawn_connection(app, &conn_id, &adhoc_config);
}
fn spawn_connection(app: &mut App, conn_id: &str, server_config: &crate::config::ServerConfig) {
if app.irc_handles.contains_key(conn_id) {
add_local_event(
app,
&format!("Already connected to {}", server_config.label),
);
return;
}
app.setup_connection(conn_id, server_config);
let general = app.config.general.clone();
let tx = app.irc_tx.clone();
let id = conn_id.to_string();
let cfg = server_config.clone();
tokio::spawn(async move {
match crate::irc::connect_server(&id, &cfg, &general).await {
Ok((handle, mut rx)) => {
let _ = tx
.send(crate::irc::IrcEvent::HandleReady(
handle.conn_id.clone(),
handle.sender,
handle.local_ip,
handle.outgoing_handle,
))
.await;
while let Some(event) = rx.recv().await {
if tx.send(event).await.is_err() {
break;
}
}
}
Err(e) => {
let _ = tx
.send(crate::irc::IrcEvent::Disconnected(id, Some(e.to_string())))
.await;
}
}
});
}
pub(crate) fn cmd_disconnect(app: &mut App, args: &[String]) {
let default_quit = crate::constants::default_quit_message();
let joined_args;
let quit_msg = if args.is_empty() {
default_quit.as_str()
} else {
joined_args = args.join(" ");
joined_args.as_str()
};
let Some(conn_id) = app.active_conn_id().map(str::to_owned) else {
add_local_event(app, "No active connection");
return;
};
if let Some(conn) = app.state.connections.get_mut(&conn_id) {
conn.should_reconnect = false;
conn.next_reconnect = None;
}
if let Some(handle) = app.irc_handles.get(&conn_id) {
let _ = handle.sender.send_quit(quit_msg);
}
}
pub(crate) fn cmd_join(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /join <channel> [key]");
return;
}
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let mut i = 0;
while i < args.len() {
let mut channel = args[i].clone();
if !channel.starts_with('#')
&& !channel.starts_with('&')
&& !channel.starts_with('+')
&& !channel.starts_with('!')
{
channel = format!("#{channel}");
}
let key = if i + 1 < args.len()
&& !args[i + 1].starts_with('#')
&& !args[i + 1].starts_with('&')
&& !args[i + 1].starts_with('+')
&& !args[i + 1].starts_with('!')
{
i += 1;
Some(args[i].clone())
} else {
None
};
let result = key.map_or_else(
|| sender.send_join(&channel),
|key| sender.send(irc::proto::Command::JOIN(channel.clone(), Some(key), None)),
);
if let Err(e) = result {
add_local_event(app, &format!("Failed to join {channel}: {e}"));
}
i += 1;
}
}
pub(crate) fn cmd_part(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let (channel, reason) = if args.is_empty() {
let Some(buf) = app.state.active_buffer() else {
return;
};
(buf.name.clone(), None)
} else if args.len() == 1 {
if crate::irc::formatting::is_channel(&args[0]) {
(args[0].clone(), None)
} else {
let Some(buf) = app.state.active_buffer() else {
return;
};
(buf.name.clone(), Some(args[0].as_str()))
}
} else {
(args[0].clone(), Some(args[1].as_str()))
};
let default_part = crate::constants::default_quit_message();
let part_reason = reason.unwrap_or(default_part.as_str());
let result = sender.send(irc::proto::Command::PART(
channel,
Some(part_reason.to_string()),
));
if let Err(e) = result {
add_local_event(app, &format!("Failed to part: {e}"));
}
}
pub(crate) fn cmd_topic(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
if args.is_empty() {
if let Some(buf) = app.state.active_buffer() {
match &buf.topic {
Some(topic) => {
let setter = buf.topic_set_by.as_deref().unwrap_or("unknown");
add_local_event(
app,
&format!("Topic for {}: {} (set by {setter})", buf.name, topic),
);
}
None => {
add_local_event(app, &format!("No topic set for {}", buf.name));
}
}
}
return;
}
let (channel, topic_args) = if crate::irc::formatting::is_channel(&args[0]) {
(args[0].clone(), &args[1..])
} else {
let Some(buf) = app.state.active_buffer() else {
return;
};
(buf.name.clone(), args)
};
if topic_args.is_empty() {
let _ = sender.send(irc::proto::Command::TOPIC(channel, None));
return;
}
let topic = topic_args.join(" ");
if let Err(e) = sender.send(irc::proto::Command::TOPIC(channel, Some(topic))) {
add_local_event(app, &format!("Failed to set topic: {e}"));
}
}
pub(crate) fn cmd_kick(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /kick [#channel] <nick> [reason]");
return;
}
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let (channel, remaining) = if crate::irc::formatting::is_channel(&args[0]) && args.len() >= 2 {
(args[0].clone(), &args[1..])
} else {
let Some(buf) = app.state.active_buffer() else {
return;
};
(buf.name.clone(), args)
};
let nick = remaining[0].clone();
let reason = if remaining.len() > 1 {
Some(remaining[1..].join(" "))
} else {
None
};
if let Err(e) = sender.send(irc::proto::Command::KICK(channel, nick.clone(), reason)) {
add_local_event(app, &format!("Failed to kick {nick}: {e}"));
}
}
pub(crate) fn cmd_invite(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /invite <nick> [channel]");
return;
}
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let nick = &args[0];
let channel = if args.len() > 1 {
args[1].clone()
} else {
let Some(buf) = app.state.active_buffer() else {
return;
};
buf.name.clone()
};
if let Err(e) = sender.send(irc::proto::Command::INVITE(nick.clone(), channel)) {
add_local_event(app, &format!("Failed to invite {nick}: {e}"));
}
}
pub(crate) fn cmd_names(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let channel = if args.is_empty() {
let Some(buf) = app.state.active_buffer() else {
return;
};
buf.name.clone()
} else {
args[0].clone()
};
if let Err(e) = sender.send(irc::proto::Command::NAMES(Some(channel), None)) {
add_local_event(app, &format!("Failed to send NAMES: {e}"));
}
}
pub(crate) fn cmd_mode(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
if args.is_empty() {
let Some(conn_id) = app.active_conn_id() else {
add_local_event(app, "Not connected");
return;
};
let nick = app
.state
.connections
.get(conn_id)
.map(|c| c.nick.clone())
.unwrap_or_default();
let _ = sender.send(irc::proto::Command::Raw("MODE".to_string(), vec![nick]));
return;
}
let first = &args[0];
if (first.starts_with('+') || first.starts_with('-'))
&& let Some(buf) = app.state.active_buffer()
&& (buf.name.starts_with('#') || buf.name.starts_with('&') || buf.name.starts_with('!'))
{
let mut full_args = vec![buf.name.clone()];
full_args.extend_from_slice(args);
let _ = sender.send(irc::proto::Command::Raw("MODE".to_string(), full_args));
return;
}
let _ = sender.send(irc::proto::Command::Raw("MODE".to_string(), args.to_vec()));
}
fn set_nick_mode(app: &mut App, mode_char: char, adding: bool, args: &[String]) {
if args.is_empty() {
let cmd = match (mode_char, adding) {
('o', true) => "op",
('o', false) => "deop",
('v', true) => "voice",
('v', false) => "devoice",
_ => "mode",
};
add_local_event(app, &format!("Usage: /{cmd} <nick> [nick2...]"));
return;
}
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let channel = match app.state.active_buffer() {
Some(b) if b.buffer_type == crate::state::buffer::BufferType::Channel => b.name.clone(),
_ => {
add_local_event(app, "Not in a channel");
return;
}
};
let sign = if adding { "+" } else { "-" };
let modes: String = std::iter::repeat_n(mode_char, args.len()).collect();
let mut cmd_args = vec![channel, format!("{sign}{modes}")];
cmd_args.extend(args.iter().cloned());
let _ = sender.send(irc::proto::Command::Raw("MODE".to_string(), cmd_args));
}
pub(crate) fn cmd_op(app: &mut App, args: &[String]) {
set_nick_mode(app, 'o', true, args);
}
pub(crate) fn cmd_deop(app: &mut App, args: &[String]) {
set_nick_mode(app, 'o', false, args);
}
pub(crate) fn cmd_voice(app: &mut App, args: &[String]) {
set_nick_mode(app, 'v', true, args);
}
pub(crate) fn cmd_devoice(app: &mut App, args: &[String]) {
set_nick_mode(app, 'v', false, args);
}
pub(crate) fn cmd_ban(app: &mut App, args: &[String]) {
if args.len() >= 2 && args[0] == "-a" {
let account = &args[1];
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let channel = match app.state.active_buffer() {
Some(b) if b.buffer_type == crate::state::buffer::BufferType::Channel => b.name.clone(),
_ => {
add_local_event(app, "Not in a channel");
return;
}
};
let conn_id = app
.state
.active_buffer()
.map(|b| b.connection_id.clone())
.unwrap_or_default();
let extban_info = app
.state
.connections
.get(&conn_id)
.and_then(|c| c.isupport_parsed.extban());
let Some((prefix, types)) = extban_info else {
add_local_event(app, "Server does not advertise EXTBAN support");
return;
};
if !types.contains('a') {
add_local_event(app, "Server EXTBAN does not support account type ('a')");
return;
}
let mask = crate::irc::extban::compose_account_ban(account, Some(prefix));
let _ = sender.send(irc::proto::Command::Raw(
"MODE".to_string(),
vec![channel, "+b".to_string(), mask],
));
return;
}
list_mode_set(app, args, 'b');
}
pub(crate) fn cmd_unban(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /unban <number|mask|wildcard> [...]");
return;
}
list_mode_unset_smart(app, args, 'b', "unban");
}
pub(crate) fn cmd_kickban(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /kb [#channel] <nick> [reason]");
return;
}
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let (channel, remaining) = if crate::irc::formatting::is_channel(&args[0]) && args.len() >= 2 {
(args[0].clone(), &args[1..])
} else {
let Some(buf) = app.state.active_buffer() else {
add_local_event(app, "Not in a channel");
return;
};
if buf.buffer_type != crate::state::buffer::BufferType::Channel {
add_local_event(app, "Not in a channel");
return;
}
(buf.name.clone(), args)
};
let nick = remaining[0].clone();
let reason = if remaining.len() > 1 {
remaining[1..].join(" ")
} else {
nick.clone()
};
let ban_mask = app
.state
.active_buffer()
.and_then(|buf| buf.users.get(&nick.to_lowercase()))
.and_then(|entry| match (&entry.ident, &entry.host) {
(Some(ident), Some(host)) => Some(format!("*!*{ident}@{host}")),
_ => None,
})
.unwrap_or_else(|| format!("{nick}!*@*"));
let _ = sender.send(irc::proto::Command::KICK(
channel.clone(),
nick,
Some(reason),
));
let _ = sender.send(irc::proto::Command::Raw(
"MODE".to_string(),
vec![channel, "+b".to_string(), ban_mask],
));
}
fn list_mode_set(app: &mut App, args: &[String], mode_char: char) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let channel = match app.state.active_buffer() {
Some(b) if b.buffer_type == crate::state::buffer::BufferType::Channel => b.name.clone(),
_ => {
add_local_event(app, "Not in a channel");
return;
}
};
if args.is_empty() {
if let Some(buf) = app.state.active_buffer_mut() {
buf.list_modes.remove(&mode_char.to_string());
}
let _ = sender.send(irc::proto::Command::Raw(
"MODE".to_string(),
vec![channel, format!("+{mode_char}")],
));
} else {
let _ = sender.send(irc::proto::Command::Raw(
"MODE".to_string(),
vec![channel, format!("+{mode_char}"), args[0].clone()],
));
}
}
fn list_mode_unset_smart(app: &mut App, args: &[String], mode_char: char, cmd_name: &str) {
if args.is_empty() {
add_local_event(
app,
&format!("Usage: /{cmd_name} <number|mask|wildcard> [...]"),
);
return;
}
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let channel = match app.state.active_buffer() {
Some(b) if b.buffer_type == crate::state::buffer::BufferType::Channel => b.name.clone(),
_ => {
add_local_event(app, "Not in a channel");
return;
}
};
let mode_key = mode_char.to_string();
let entries: Vec<crate::state::buffer::ListEntry> = app
.state
.active_buffer()
.and_then(|b| b.list_modes.get(&mode_key))
.cloned()
.unwrap_or_default();
let mut masks: Vec<String> = Vec::new();
for arg in args {
if let Ok(num) = arg.parse::<usize>() {
if num >= 1 && num <= entries.len() {
masks.push(entries[num - 1].mask.clone());
} else {
add_local_event(
app,
&format!("{cmd_name}: #{num} out of range (1-{})", entries.len()),
);
}
} else if arg.contains('*') || arg.contains('?') {
let re = crate::irc::ignore::wildcard_to_regex(arg);
let mut found = false;
for entry in &entries {
if re.is_match(&entry.mask) {
masks.push(entry.mask.clone());
found = true;
}
}
if !found {
add_local_event(app, &format!("{cmd_name}: no entries matching '{arg}'"));
}
} else {
masks.push(arg.clone());
}
}
for mask in &masks {
let _ = sender.send(irc::proto::Command::Raw(
"MODE".to_string(),
vec![channel.clone(), format!("-{mode_char}"), mask.clone()],
));
}
}
pub(crate) fn cmd_except(app: &mut App, args: &[String]) {
list_mode_set(app, args, 'e');
}
pub(crate) fn cmd_unexcept(app: &mut App, args: &[String]) {
list_mode_unset_smart(app, args, 'e', "unexcept");
}
pub(crate) fn cmd_invex(app: &mut App, args: &[String]) {
list_mode_set(app, args, 'I');
}
pub(crate) fn cmd_uninvex(app: &mut App, args: &[String]) {
list_mode_unset_smart(app, args, 'I', "uninvex");
}
pub(crate) fn cmd_reop(app: &mut App, args: &[String]) {
list_mode_set(app, args, 'R');
}
pub(crate) fn cmd_unreop(app: &mut App, args: &[String]) {
list_mode_unset_smart(app, args, 'R', "unreop");
}
pub(crate) fn cmd_cycle(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let (channel, reason) = if args.is_empty() {
let Some(buf) = app.state.active_buffer() else {
return;
};
if buf.buffer_type != crate::state::buffer::BufferType::Channel {
add_local_event(app, "Not in a channel");
return;
}
(buf.name.clone(), None)
} else if crate::irc::formatting::is_channel(&args[0]) {
let reason = if args.len() > 1 {
Some(args[1].as_str())
} else {
None
};
(args[0].clone(), reason)
} else {
let Some(buf) = app.state.active_buffer() else {
return;
};
if buf.buffer_type != crate::state::buffer::BufferType::Channel {
add_local_event(app, "Not in a channel");
return;
}
(buf.name.clone(), Some(args[0].as_str()))
};
let key = app
.state
.active_buffer()
.and_then(|b| b.mode_params.as_ref())
.and_then(|p| p.get("k").cloned());
let part_result = reason.map_or_else(
|| sender.send(irc::proto::Command::PART(channel.clone(), None)),
|reason| {
sender.send(irc::proto::Command::PART(
channel.clone(),
Some(reason.to_string()),
))
},
);
if let Err(e) = part_result {
add_local_event(app, &format!("Failed to cycle {channel}: {e}"));
return;
}
let join_result = key.map_or_else(
|| sender.send_join(&channel),
|key| sender.send(irc::proto::Command::JOIN(channel.clone(), Some(key), None)),
);
if let Err(e) = join_result {
add_local_event(app, &format!("Failed to rejoin {channel}: {e}"));
}
}
fn ensure_query_buffer(app: &mut App, conn_id: &str, target: &str, skip_channels: bool) -> String {
let buffer_id = crate::state::buffer::make_buffer_id(conn_id, target);
let should_create = !app.state.buffers.contains_key(&buffer_id)
&& (!skip_channels || !crate::irc::formatting::is_channel(target));
if should_create {
app.state.add_buffer(crate::state::buffer::Buffer {
id: buffer_id.clone(),
connection_id: conn_id.to_string(),
buffer_type: crate::state::buffer::BufferType::Query,
name: target.to_string(),
messages: std::collections::VecDeque::new(),
activity: crate::state::buffer::ActivityLevel::None,
unread_count: 0,
last_read: chrono::Utc::now(),
topic: None,
topic_set_by: None,
users: std::collections::HashMap::new(),
modes: None,
mode_params: None,
list_modes: std::collections::HashMap::new(),
last_speakers: Vec::new(),
peer_handle: None,
});
}
buffer_id
}
pub(crate) fn cmd_msg(app: &mut App, args: &[String]) {
if args.len() < 2 {
add_local_event(app, "Usage: /msg <target> <message>");
return;
}
let target = &args[0];
let text = &args[1];
if let Some(dcc_nick) = target.strip_prefix('=') {
if let Some(record) = app.dcc.find_connected(dcc_nick) {
let record_id = record.id.clone();
let conn_id = record.conn_id.clone();
if let Err(e) = app.dcc.send_chat_line(&record_id, text) {
add_local_event(app, &format!("DCC send error: {e}"));
return;
}
let buf_name = format!("={dcc_nick}");
let buffer_id = crate::state::buffer::make_buffer_id(&conn_id, &buf_name);
let our_nick = app
.state
.connections
.values()
.next()
.map(|c| c.nick.clone())
.unwrap_or_default();
let msg_id = app.state.next_message_id();
app.state.add_message(
&buffer_id,
crate::state::buffer::Message {
id: msg_id,
timestamp: chrono::Utc::now(),
message_type: crate::state::buffer::MessageType::Message,
nick: Some(our_nick),
nick_mode: None,
text: text.clone(),
highlight: false,
event_key: None,
event_params: None,
log_msg_id: None,
log_ref_id: None,
tags: None,
},
);
} else {
add_local_event(app, &format!("No active DCC CHAT session with {dcc_nick}"));
}
return;
}
let (conn_id, nick) = {
let Some(conn_id) = app.active_conn_id().map(str::to_owned) else {
add_local_event(app, "No active connection");
return;
};
let nick = app
.state
.connections
.get(&conn_id)
.map(|c| c.nick.clone())
.unwrap_or_default();
(conn_id, nick)
};
let buffer_id = ensure_query_buffer(app, &conn_id, target, true);
let echo_message_enabled = app
.state
.connections
.get(&conn_id)
.is_some_and(|c| c.enabled_caps.contains("echo-message"));
let chunks = crate::irc::split_irc_message(text, crate::irc::MESSAGE_MAX_BYTES);
let own_mode = app.state.nick_prefix(&buffer_id, &nick);
for chunk in chunks {
if let Some(handle) = app.irc_handles.get(&conn_id)
&& let Err(e) = handle.sender.send_privmsg(target, &chunk)
{
add_local_event(app, &format!("Failed to send message: {e}"));
return;
}
if !echo_message_enabled {
let id = app.state.next_message_id();
app.state.add_message(
&buffer_id,
crate::state::buffer::Message {
id,
timestamp: chrono::Utc::now(),
message_type: crate::state::buffer::MessageType::Message,
nick: Some(nick.clone()),
nick_mode: own_mode.map(|c| c.to_string()),
text: chunk,
highlight: false,
event_key: None,
event_params: None,
log_msg_id: None,
log_ref_id: None,
tags: None,
},
);
}
}
}
pub(crate) fn cmd_query(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /query <nick> [message]");
return;
}
let target = &args[0];
let Some(conn_id) = app.active_conn_id().map(str::to_owned) else {
add_local_event(app, "No active connection");
return;
};
let buffer_id = ensure_query_buffer(app, &conn_id, target, false);
app.state.set_active_buffer(&buffer_id);
if args.len() >= 2 {
let text = &args[1];
let nick = app
.state
.connections
.get(&conn_id)
.map(|c| c.nick.clone())
.unwrap_or_default();
if let Some(handle) = app.irc_handles.get(&conn_id)
&& let Err(e) = handle.sender.send_privmsg(target, text)
{
add_local_event(app, &format!("Failed to send message: {e}"));
return;
}
let echo_message_enabled = app
.state
.connections
.get(&conn_id)
.is_some_and(|c| c.enabled_caps.contains("echo-message"));
if !echo_message_enabled {
let own_mode = app.state.nick_prefix(&buffer_id, &nick);
let id = app.state.next_message_id();
app.state.add_message(
&buffer_id,
crate::state::buffer::Message {
id,
timestamp: chrono::Utc::now(),
message_type: crate::state::buffer::MessageType::Message,
nick: Some(nick),
nick_mode: own_mode.map(|c| c.to_string()),
text: text.clone(),
highlight: false,
event_key: None,
event_params: None,
log_msg_id: None,
log_ref_id: None,
tags: None,
},
);
}
}
}
pub(crate) fn cmd_me(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /me <action>");
return;
}
let action_text = &args[0];
let Some(buf) = app.state.active_buffer() else {
return;
};
let target = buf.name.clone();
let conn_id = buf.connection_id.clone();
let buf_type = buf.buffer_type.clone();
if buf_type == crate::state::buffer::BufferType::DccChat {
let dcc_nick = target.strip_prefix('=').unwrap_or(&target);
if let Some(record) = app.dcc.find_connected(dcc_nick) {
let record_id = record.id.clone();
let ctcp = format!("\x01ACTION {action_text}\x01");
if let Err(e) = app.dcc.send_chat_line(&record_id, &ctcp) {
add_local_event(app, &format!("DCC send error: {e}"));
return;
}
let our_nick = app
.state
.connections
.values()
.next()
.map(|c| c.nick.clone())
.unwrap_or_default();
let buffer_id = app.state.active_buffer_id.clone().unwrap_or_default();
let msg_id = app.state.next_message_id();
app.state.add_message(
&buffer_id,
crate::state::buffer::Message {
id: msg_id,
timestamp: chrono::Utc::now(),
message_type: crate::state::buffer::MessageType::Action,
nick: Some(our_nick),
nick_mode: None,
text: action_text.clone(),
highlight: false,
event_key: None,
event_params: None,
log_msg_id: None,
log_ref_id: None,
tags: None,
},
);
} else {
add_local_event(app, "No active DCC CHAT session for this buffer");
}
return;
}
let nick = app
.state
.connections
.get(&conn_id)
.map(|c| c.nick.clone())
.unwrap_or_default();
let Some(handle) = app.irc_handles.get(&conn_id) else {
add_local_event(app, "Not connected");
return;
};
let ctcp = format!("\x01ACTION {action_text}\x01");
if let Err(e) = handle.sender.send_privmsg(&target, &ctcp) {
add_local_event(app, &format!("Failed to send action: {e}"));
return;
}
let echo_message_enabled = app
.state
.connections
.get(&conn_id)
.is_some_and(|c| c.enabled_caps.contains("echo-message"));
if !echo_message_enabled {
let buffer_id = app.state.active_buffer_id.clone().unwrap_or_default();
let own_mode = app.state.nick_prefix(&buffer_id, &nick);
let id = app.state.next_message_id();
app.state.add_message(
&buffer_id,
crate::state::buffer::Message {
id,
timestamp: chrono::Utc::now(),
message_type: crate::state::buffer::MessageType::Action,
nick: Some(nick),
nick_mode: own_mode.map(|c| c.to_string()),
text: action_text.clone(),
highlight: false,
event_key: None,
event_params: None,
log_msg_id: None,
log_ref_id: None,
tags: None,
},
);
}
}
pub(crate) fn cmd_nick(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /nick <new_nick>");
return;
}
let new_nick = &args[0];
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
if let Err(e) = sender.send(irc::proto::Command::NICK(new_nick.clone())) {
add_local_event(app, &format!("Failed to change nick: {e}"));
}
}
pub(crate) fn cmd_notice(app: &mut App, args: &[String]) {
if args.len() < 2 {
add_local_event(app, "Usage: /notice <target> <message>");
return;
}
let target = &args[0];
let text = &args[1];
if let Some(sender) = app.active_irc_sender() {
if let Err(e) = sender.send_notice(target, text) {
add_local_event(app, &format!("Failed to send notice: {e}"));
}
} else {
add_local_event(app, "Not connected");
}
}
pub(crate) fn cmd_whois(app: &mut App, args: &[String]) {
let nick = if args.is_empty() {
whois_default_nick(app)
} else {
Some(args[0].clone())
};
let Some(nick) = nick else {
add_local_event(app, "Usage: /whois <nick>");
return;
};
if let Some(sender) = app.active_irc_sender() {
if let Err(e) = sender.send(irc::proto::Command::WHOIS(None, nick)) {
add_local_event(app, &format!("Failed to send WHOIS: {e}"));
}
} else {
add_local_event(app, "Not connected");
}
}
pub(crate) fn cmd_wii(app: &mut App, args: &[String]) {
let nick = if args.is_empty() {
whois_default_nick(app)
} else {
Some(args[0].clone())
};
let Some(nick) = nick else {
add_local_event(app, "Usage: /wii <nick>");
return;
};
if let Some(sender) = app.active_irc_sender() {
if let Err(e) = sender.send(irc::proto::Command::WHOIS(Some(nick.clone()), nick)) {
add_local_event(app, &format!("Failed to send WHOIS: {e}"));
}
} else {
add_local_event(app, "Not connected");
}
}
fn whois_default_nick(app: &App) -> Option<String> {
use crate::state::buffer::BufferType;
let buf = app.state.active_buffer()?;
if buf.buffer_type == BufferType::Query {
return Some(buf.name.clone());
}
let conn = app.state.connections.get(&buf.connection_id)?;
Some(conn.nick.clone())
}
pub(crate) fn cmd_version(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
if args.is_empty() {
let _ = sender.send(irc::proto::Command::Raw("VERSION".to_string(), vec![]));
} else {
let ctcp = "\x01VERSION\x01".to_string();
let _ = sender.send_privmsg(&args[0], &ctcp);
}
}
pub(crate) fn cmd_quote(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /quote <raw command>");
return;
}
let raw = &args[0];
if let Some(sender) = app.active_irc_sender() {
let parts: Vec<&str> = raw.splitn(2, ' ').collect();
let command = parts[0].to_string();
#[allow(clippy::option_if_let_else)]
let args_vec: Vec<String> = if parts.len() > 1 {
let rest = parts[1];
if let Some(colon_pos) = rest.find(" :") {
let before_trailing = &rest[..colon_pos];
let trailing = &rest[colon_pos + 2..];
let mut args: Vec<String> = before_trailing
.split_whitespace()
.map(String::from)
.collect();
args.push(trailing.to_string());
args
} else if let Some(trailing) = rest.strip_prefix(':') {
vec![trailing.to_string()]
} else {
rest.split_whitespace().map(String::from).collect()
}
} else {
vec![]
};
if let Err(e) = sender.send(irc::proto::Command::Raw(command, args_vec)) {
add_local_event(app, &format!("Failed to send: {e}"));
}
} else {
add_local_event(app, "Not connected");
}
}
pub(crate) fn cmd_away(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let result = if args.is_empty() {
sender.send(irc::proto::Command::AWAY(None))
} else {
sender.send(irc::proto::Command::AWAY(Some(args[0].clone())))
};
if let Err(e) = result {
add_local_event(app, &format!("Failed to send AWAY: {e}"));
}
}
pub(crate) fn cmd_list(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let result = if args.is_empty() {
sender.send(irc::proto::Command::LIST(None, None))
} else {
sender.send(irc::proto::Command::LIST(Some(args[0].clone()), None))
};
if let Err(e) = result {
add_local_event(app, &format!("Failed to send LIST: {e}"));
}
}
pub(crate) fn cmd_who(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /who <target>");
return;
}
let Some(conn_id) = app.active_conn_id().map(str::to_owned) else {
add_local_event(app, "No active connection");
return;
};
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let target = &args[0];
let result = if let Some((who_target, fields)) =
crate::irc::events::build_whox_who(&mut app.state, &conn_id, target, false)
{
sender.send(irc::proto::Command::Raw(
"WHO".to_string(),
vec![who_target, fields],
))
} else {
sender.send(irc::proto::Command::WHO(Some(target.clone()), None))
};
if let Err(e) = result {
add_local_event(app, &format!("Failed to send WHO: {e}"));
}
}
pub(crate) fn cmd_whowas(app: &mut App, args: &[String]) {
if args.is_empty() {
add_local_event(app, "Usage: /whowas <nick>");
return;
}
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
if let Err(e) = sender.send(irc::proto::Command::WHOWAS(args[0].clone(), None, None)) {
add_local_event(app, &format!("Failed to send WHOWAS: {e}"));
}
}
pub(crate) fn cmd_info(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let server = args.first().cloned();
if let Err(e) = sender.send(irc::proto::Command::INFO(server)) {
add_local_event(app, &format!("Failed to send INFO: {e}"));
}
}
pub(crate) fn cmd_admin(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let server = args.first().cloned();
if let Err(e) = sender.send(irc::proto::Command::ADMIN(server)) {
add_local_event(app, &format!("Failed to send ADMIN: {e}"));
}
}
pub(crate) fn cmd_lusers(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let mask = args.first().cloned();
let server = args.get(1).cloned();
if let Err(e) = sender.send(irc::proto::Command::LUSERS(mask, server)) {
add_local_event(app, &format!("Failed to send LUSERS: {e}"));
}
}
pub(crate) fn cmd_time(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let server = args.first().cloned();
if let Err(e) = sender.send(irc::proto::Command::TIME(server)) {
add_local_event(app, &format!("Failed to send TIME: {e}"));
}
}
pub(crate) fn cmd_links(app: &mut App, args: &[String]) {
let Some(sender) = app.active_irc_sender().cloned() else {
add_local_event(app, "Not connected");
return;
};
let remote = args.first().cloned();
let mask = args.get(1).cloned();
if let Err(e) = sender.send(irc::proto::Command::LINKS(remote, mask)) {
add_local_event(app, &format!("Failed to send LINKS: {e}"));
}
}