/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
//! IRC command implemenatations
//!
//! These try to follow the RFCs, modified in order for our P2P stack.
//! Copied from
//!
//! Unimplemented commands:
//! * `AWAY`
//! * `CONNECT`
//! * `DIE`
//! * `ERROR`
//! * `INVITE`
//! * `ISON`
//! * `KICK`
//! * `KILL`
//! * `NOTICE`
//! * `OPER`
//! * `PASS`
//! * `RESTART`
//! * `SERVICE`
//! * `SERVLIST`
//! * `SERVER`
//! * `SQUERY`
//! * `SQUIT`
//! * `SUMMON`
//! * `TRACE`
//! * `USERHOST`
//! * `WALLOPS`
//! * `WHO`
//! * `WHOIS`
//! * `WHOWAS`
//!
//! Some of the above commands could actually be implemented and could
//! work in respect to the P2P network.
use std::{collections::HashSet, sync::atomic::Ordering::SeqCst};
use darkfi::Result;
use darkfi_serial::deserialize_async_partial;
use log::{error, info};
use super::{
client::{Client, ReplyType},
rpl::*,
server::MAX_NICK_LEN,
IrcChannel, SERVER_NAME,
};
impl Client {
/// `ADMIN []`
///
/// Asks the server for information about the administrator of the server.
pub async fn handle_cmd_admin(&self, _args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.to_string();
let replies = vec![
ReplyType::Server((
RPL_ADMINME,
format!("{} {} :Administrative info", nick, SERVER_NAME),
)),
ReplyType::Server((RPL_ADMINLOC1, format!("{} :", nick))),
ReplyType::Server((RPL_ADMINLOC2, format!("{} :", nick))),
ReplyType::Server((RPL_ADMINEMAIL, format!("{} :anon@darkirc", nick))),
];
Ok(replies)
}
/// `CAP `
pub async fn handle_cmd_cap(&self, args: &str) -> Result> {
let mut tokens = args.split_ascii_whitespace();
let Some(subcommand) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", self.nickname.read().await, INVALID_SYNTAX),
))])
};
let caps_keys: Vec = self.caps.read().await.keys().cloned().collect();
let nick = self.nickname.read().await.to_string();
match subcommand.to_uppercase().as_str() {
"LS" => {
/*
let Some(_version) = tokens.next() else {
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", self.nickname.read().await, INVALID_SYNTAX),
))])
};
*/
self.reg_paused.store(true, SeqCst);
return Ok(vec![ReplyType::Cap(format!("CAP * LS :{}", caps_keys.join(" ")))])
}
"REQ" => {
let Some(substr_idx) = args.find(':') else {
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", nick, INVALID_SYNTAX),
))])
};
if substr_idx >= args.len() {
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", nick, INVALID_SYNTAX),
))])
}
let cap_reqs: Vec<&str> = args[substr_idx + 1..].split(' ').collect();
let mut ack_list = vec![];
let mut nak_list = vec![];
let mut available_caps = self.caps.write().await;
for cap in cap_reqs {
if available_caps.contains_key(cap) {
available_caps.insert(cap.to_string(), true);
ack_list.push(cap);
} else {
nak_list.push(cap);
}
}
let mut replies = vec![];
if !ack_list.is_empty() {
replies.push(ReplyType::Cap(format!(
"CAP {} ACK :{}",
nick,
ack_list.join(" ")
)));
}
if !nak_list.is_empty() {
replies.push(ReplyType::Cap(format!(
"CAP {} NAK :{}",
nick,
nak_list.join(" ")
)));
}
return Ok(replies)
}
"LIST" => {
let enabled_caps: Vec = self
.caps
.read()
.await
.clone()
.into_iter()
.filter(|(_, v)| *v)
.map(|(k, _)| k)
.collect();
return Ok(vec![ReplyType::Cap(format!(
"CAP {} LIST :{}",
nick,
enabled_caps.join(" ")
))])
}
"END" => {
// At CAP END, if we have USER and NICK, we can welcome them.
self.reg_paused.store(false, SeqCst);
if self.registered.load(SeqCst) {
return Ok(self.welcome().await)
}
return Ok(vec![])
}
_ => {}
}
self.penalty.fetch_add(1, SeqCst);
Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} CAP :{}", nick, INVALID_SYNTAX),
))])
}
/// `INFO []`
///
/// Gives information about the `` server, or the current server if
/// `` is not used. The information includes the server's version,
/// when it was compiled, the patch level, when it was started, and any
/// other information which might be relevant.
pub async fn handle_cmd_info(&self, _args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.clone();
let replies = vec![
ReplyType::Server((
RPL_INFO,
format!("{} :DarkIRC {}", nick, env!("CARGO_PKG_VERSION")),
)),
ReplyType::Server((RPL_ENDOFINFO, format!("{} :End of INFO list", nick))),
];
Ok(replies)
}
/// `JOIN []`
///
/// Makes the client join the channels in the list ``.
/// Passwords can be used in the list ``. If the channels do not
/// exist, they will be created.
pub async fn handle_cmd_join(&self, args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
// Client's (already) active channels
let mut active_channels = self.channels.write().await;
// Here we'll hold valid channel names.
let mut channels = HashSet::new();
// Let's scan through our channels. For now we'll only support
// channel names starting with a single '#' character.
let nick = self.nickname.read().await.to_string();
let tokens = args.split_ascii_whitespace();
for channel in tokens {
if !channel.starts_with('#') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} JOIN :{}", nick, INVALID_SYNTAX),
))])
}
if !active_channels.contains(channel) {
channels.insert(channel.to_string());
}
}
// We need at least one channel.
if channels.is_empty() {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} JOIN :{}", nick, INVALID_SYNTAX),
))])
}
// Weechat sends channels as `#chan1,#chan2,#chan3`. Handle it.
if channels.len() == 1 {
let list = channels.iter().next().unwrap().clone();
channels.remove(list.as_str());
for channel in list.split(',') {
if !channel.starts_with('#') || channel.as_bytes().len() > MAX_NICK_LEN {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} JOIN :{}", nick, INVALID_SYNTAX),
))])
}
channels.insert(channel.to_string());
}
}
// Create new channels for this client and construct replies.
let mut server_channels = self.server.channels.write().await;
let mut replies = vec![];
for channel in channels.iter() {
// Insert the channel name into the set of client's active channels
active_channels.insert(channel.clone());
// Create or update the channel on the server side.
if let Some(server_chan) = server_channels.get_mut(channel) {
server_chan.nicks.insert(nick.clone());
} else {
let chan = IrcChannel {
topic: String::new(),
nicks: HashSet::from([nick.clone()]),
saltbox: None,
};
server_channels.insert(channel.clone(), chan);
}
// Create the replies
replies.push(ReplyType::Client((nick.clone(), format!("JOIN :{}", channel))));
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, channel, nick),
)));
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} {} :End of NAMES list", nick, channel),
)));
if let Some(chan) = server_channels.get(channel) {
if !chan.topic.is_empty() {
replies.push(ReplyType::Client((
nick.clone(),
format!("TOPIC {} :{}", channel, chan.topic),
)));
}
}
}
// Drop the locks as they're used in get_history()
drop(active_channels);
drop(server_channels);
// Potentially extend the replies with channel history
replies.append(&mut self.get_history(&channels).await.unwrap());
Ok(replies)
}
/// `LIST [ []]`
///
/// List all channels on the server. If the list `` is given, it
/// will return the channel topics. If `` is given, the command will
/// be sent to `` for evaluation.
pub async fn handle_cmd_list(&self, _args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.to_string();
let mut list = vec![];
for (name, channel) in self.server.channels.read().await.iter() {
list.push(format!("{} {} {} :{}", nick, name, channel.nicks.len(), channel.topic));
}
let mut replies = vec![];
replies.push(ReplyType::Server((RPL_LISTSTART, format!("{} Channel :Users Name", nick))));
for chan in list {
replies.push(ReplyType::Server((RPL_LIST, chan)));
}
replies.push(ReplyType::Server((RPL_LISTEND, format!("{} :End of /LIST", nick))));
Ok(replies)
}
/// `MODE `
/// `MODE `
///
/// The MODE command has two uses. It can be used to set both user and
/// channel modes.
pub async fn handle_cmd_mode(&self, args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.to_string();
let mut tokens = args.split_ascii_whitespace();
let Some(target) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} MODE :{}", nick, INVALID_SYNTAX),
))])
};
if target == nick {
return Ok(vec![ReplyType::Server((RPL_UMODEIS, format!("{} +", nick)))])
}
if !target.starts_with('#') {
return Ok(vec![ReplyType::Server((
ERR_USERSDONTMATCH,
format!("{} :Can't set/get mode for other users", nick),
))])
}
if !self.server.channels.read().await.contains_key(target) {
return Ok(vec![ReplyType::Server((
ERR_NOSUCHNICK,
format!("{} {} :No such nick or channel name", nick, target),
))])
}
Ok(vec![ReplyType::Server((RPL_CHANNELMODEIS, format!("{} {} +", nick, target)))])
}
/// `MOTD []`
///
/// Returns the message of the day on `` or the current server if
/// it is not stated.
pub async fn handle_cmd_motd(&self, _args: &str) -> Result> {
let nick = self.nickname.read().await.to_string();
Ok(vec![
ReplyType::Server((
RPL_MOTDSTART,
format!("{} :- {} message of the day", nick, SERVER_NAME),
)),
ReplyType::Server((RPL_MOTD, format!("{} :Let there be dark!", nick))),
ReplyType::Server((RPL_ENDOFMOTD, format!("{} :End of /MOTD command.", nick))),
])
}
/// `NAMES []`
///
/// Returns a list of who is on the list of ``, by channel name.
/// If `` is not used, all users are shown. They are grouped by
/// channel name with all users who are not on a channel being shown as
/// part of channel "*".
pub async fn handle_cmd_names(&self, args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.to_string();
let mut tokens = args.split_ascii_whitespace();
let mut replies = vec![];
// If a channel was requested, reply only with that one.
// Otherwise, return info for all known channels.
if let Some(req_chan) = tokens.next() {
if let Some(chan) = self.server.channels.read().await.get(req_chan) {
let nicks: Vec = chan.nicks.iter().cloned().collect();
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, req_chan, nicks.join(" ")),
)));
}
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} {} :End of NAMES list", nick, req_chan),
)));
Ok(replies)
} else {
for (name, chan) in self.server.channels.read().await.iter() {
let nicks: Vec = chan.nicks.iter().cloned().collect();
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, name, nicks.join(" ")),
)));
}
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} * :End of NAMES list", nick),
)));
Ok(replies)
}
}
/// `NICK `
///
/// Allows a client to change their IRC nickname.
pub async fn handle_cmd_nick(&self, args: &str) -> Result> {
// Parse the line
let mut tokens = args.split_ascii_whitespace();
// Reference the current nickname
let old_nick = self.nickname.read().await.to_string();
let Some(nickname) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} NICK :{}", old_nick, INVALID_SYNTAX),
))])
};
// Forbid disallowed characters.
// The next() call is done to check for ASCII whitespace in the nick.
if tokens.next().is_some() || nickname.starts_with(':') || nickname.starts_with('#') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_ERRONEOUSNICKNAME,
format!("{} {} :Erroneous nickname", old_nick, nickname),
))])
}
// Disallow too long nicks
if nickname.as_bytes().len() > MAX_NICK_LEN {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_ERRONEOUSNICKNAME,
format!("{} {} :Nickname too long", old_nick, nickname),
))])
}
// Set the new nickname
*self.nickname.write().await = nickname.to_string();
// If the username is set, we can complete the registration
if *self.username.read().await != "*" && !self.registered.load(SeqCst) {
self.registered.store(true, SeqCst);
if self.reg_paused.load(SeqCst) {
return Ok(vec![])
} else {
return Ok(self.welcome().await)
}
}
// If we were registered, we send a client reply about it.
if self.registered.load(SeqCst) {
Ok(vec![ReplyType::Client((old_nick, format!("NICK :{}", nickname)))])
} else {
// Otherwise, we don't reply.
Ok(vec![])
}
}
/// `PART `
///
/// Causes a user to leave the channel ``.
pub async fn handle_cmd_part(&self, args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.to_string();
let mut tokens = args.split_ascii_whitespace();
let Some(channel) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} PART :{}", nick, INVALID_SYNTAX),
))])
};
if !channel.starts_with('#') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} PART :{}", nick, INVALID_SYNTAX),
))])
}
let mut active_channels = self.channels.write().await;
if !active_channels.contains(channel) {
return Ok(vec![ReplyType::Server((
ERR_NOSUCHCHANNEL,
format!("{} {} :No such channel", nick, channel),
))])
}
// Remove the channel from the client's channel list
active_channels.remove(channel);
let replies = vec![ReplyType::Client((nick, format!("PART {} :Bye", channel)))];
Ok(replies)
}
/// `PING `
///
/// Tests a connection. A PING message results in a PONG reply.
pub async fn handle_cmd_ping(&self, args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let mut tokens = args.split_ascii_whitespace();
let Some(origin) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOORIGIN,
format!("{} :No origin specified", self.nickname.read().await),
))])
};
Ok(vec![ReplyType::Pong(origin.to_string())])
}
/// `PRIVMSG `
///
/// Sends `` to ``. The target is usually a user or
/// a channel.
pub async fn handle_cmd_privmsg(&self, args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.to_string();
let mut tokens = args.split_ascii_whitespace();
let Some(target) = tokens.next() else {
return Ok(vec![ReplyType::Server((
ERR_NORECIPIENT,
format!("{} :No recipient given (PRIVMSG)", nick),
))])
};
let Some(message) = tokens.next() else {
return Ok(vec![ReplyType::Server((
ERR_NOTEXTTOSEND,
format!("{} :No text to send", nick),
))])
};
if !message.starts_with(':') {
return Ok(vec![ReplyType::Server((
ERR_NOTEXTTOSEND,
format!("{} :No text to send", nick),
))])
}
// We only send a client reply if the message is for ourself.
// Anything else is rendered by the IRC client and not supposed
// to be echoed by the IRC serer.
if target == nick {
return Ok(vec![ReplyType::Client((
target.to_string(),
format!("PRIVMSG {} {}", target, message),
))])
}
// If it's a DM and we don't have an encryption key, we will
// refuse to send it. Send ERR_NORECIPIENT to the client.
if !target.starts_with('#') && !self.server.contacts.read().await.contains_key(target) {
return Ok(vec![ReplyType::Server((ERR_NOSUCHNICK, format!("{} :{}", nick, target)))])
}
Ok(vec![])
}
/// `REHASH`
///
/// Causes the server to re-read and re-process its configuration file(s).
pub async fn handle_cmd_rehash(&self, _args: &str) -> Result> {
info!("Attempting to rehash server...");
if let Err(e) = self.server.rehash().await {
error!("Failed to rehash server: {}", e);
}
Ok(vec![])
}
/// `TOPIC []`
///
/// Used to get the channel topic on ``. If `` is given, it
/// sets the channel topic to ``.
pub async fn handle_cmd_topic(&self, args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let nick = self.nickname.read().await.to_string();
let mut tokens = args.split_ascii_whitespace();
let Some(channel) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} TOPIC :{}", nick, INVALID_SYNTAX),
))])
};
if !self.server.channels.read().await.contains_key(channel) {
return Ok(vec![ReplyType::Server((
ERR_NOSUCHCHANNEL,
format!("{} {} :No such channel", nick, channel),
))])
}
// If there's a topic, we'll set it, otherwise return the set topic.
let Some(topic) = tokens.next() else {
let topic = self.server.channels.read().await.get(channel).unwrap().topic.clone();
if topic.is_empty() {
return Ok(vec![ReplyType::Server((
RPL_NOTOPIC,
format!("{} {} :No topic is set", nick, channel),
))])
} else {
return Ok(vec![ReplyType::Server((
RPL_TOPIC,
format!("{} {} :{}", nick, channel, topic),
))])
}
};
// Set the new topic
self.server.channels.write().await.get_mut(channel).unwrap().topic =
topic.strip_prefix(':').unwrap().to_string();
// Send reply
let replies = vec![ReplyType::Client((nick, format!("TOPIC {} {}", channel, topic)))];
Ok(replies)
}
/// `USER `
///
/// This command is used at the beginning of a connection to specify the
/// username, hostname, real name, and the initial user modes of the
/// connecting client. `` may contain spaces, and thus must be
/// prefixed with a colon.
pub async fn handle_cmd_user(&self, args: &str) -> Result> {
if self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_ALREADYREGISTERED,
format!("{} :{}", self.nickname.read().await, ALREADY_REGISTERED),
))])
}
// Parse the line
let nick = self.nickname.read().await.to_string();
let mut tokens = args.split_ascii_whitespace();
let Some(username) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", nick, INVALID_SYNTAX),
))])
};
// Mode syntax is currently ignored, but should be part of the command
let Some(_mode) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", nick, INVALID_SYNTAX),
))])
};
// Next token is unused per RFC, but should be part of the command
let Some(_unused) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", nick, INVALID_SYNTAX),
))])
};
// The final token should be realname and should start with a colon
let Some(realname) = tokens.next() else {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", nick, INVALID_SYNTAX),
))])
};
if !realname.starts_with(':') {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NEEDMOREPARAMS,
format!("{} USER :{}", nick, INVALID_SYNTAX),
))])
}
*self.username.write().await = username.to_string();
*self.realname.write().await = realname.to_string();
// If the nickname is set, we can complete the registration
if nick != "*" {
self.registered.store(true, SeqCst);
if self.reg_paused.load(SeqCst) {
return Ok(vec![])
} else {
return Ok(self.welcome().await)
}
}
// Otherwise, we don't have to reply.
Ok(vec![])
}
/// `VERSION`
///
/// Returns the version of the server.
pub async fn handle_cmd_version(&self, _args: &str) -> Result> {
if !self.registered.load(SeqCst) {
self.penalty.fetch_add(1, SeqCst);
return Ok(vec![ReplyType::Server((
ERR_NOTREGISTERED,
format!("* :{}", NOT_REGISTERED),
))])
}
let replies = vec![ReplyType::Server((
RPL_VERSION,
format!(
"{} {} {} :Let there be dark!",
self.nickname.read().await,
env!("CARGO_PKG_VERSION"),
SERVER_NAME
),
))];
Ok(replies)
}
/// Internal function that constructs the welcome message.
async fn welcome(&self) -> Vec {
let nick = self.nickname.read().await.to_string();
let mut replies = vec![
ReplyType::Server((RPL_WELCOME, format!("{} :{}", nick, WELCOME))),
ReplyType::Server((
RPL_YOURHOST,
format!(
"{} :Your host is irc.dark.fi, running version {}",
nick,
env!("CARGO_PKG_VERSION")
),
)),
];
// Append the MOTD
replies.append(&mut self.handle_cmd_motd("").await.unwrap());
// If we have any configured autojoin channels, let's join the user
// and set their topics, if any.
let mut config_chans = self.server.channels.write().await;
let mut autojoin_chans = HashSet::new();
for channel in self.server.autojoin.read().await.iter() {
autojoin_chans.insert(channel.clone());
}
for channel in autojoin_chans.iter() {
replies.push(ReplyType::Client((nick.clone(), format!("JOIN :{}", channel))));
replies.push(ReplyType::Server((
RPL_NAMREPLY,
format!("{} = {} :{}", nick, channel, nick),
)));
replies.push(ReplyType::Server((
RPL_ENDOFNAMES,
format!("{} {} :End of NAMES list", nick, channel),
)));
if let Some(chan) = config_chans.get_mut(channel) {
if !chan.topic.is_empty() {
replies.push(ReplyType::Client((
nick.clone(),
format!("TOPIC {} :{}", channel, chan.topic),
)));
}
// Insert the client into the channel nicklist
chan.nicks.insert(nick.clone());
}
}
// Drop the write lock, it's used in get_history()
drop(config_chans);
// Potentially extend replies with history
autojoin_chans.insert(self.nickname.read().await.to_string());
replies.append(&mut self.get_history(&autojoin_chans).await.unwrap());
replies
}
/// Internal function that scans the DAG and returns events for
/// given channels. Will return empty if no_history CAP is requested.
async fn get_history(&self, channels: &HashSet) -> Result> {
if channels.is_empty() || *self.caps.read().await.get("no-history").unwrap() {
return Ok(vec![])
}
// Fetch and order all the events from the DAG
let dag_events = self.server.darkirc.event_graph.order_events().await;
// Here we'll hold the events in order we'll push to the client
let mut replies = vec![];
for event_id in dag_events.iter() {
// If it was seen, skip
match self.is_seen(event_id).await {
Ok(true) => continue,
Ok(false) => {}
Err(e) => {
error!("[IRC CLIENT] (get_history) self.is_seen({}) failed: {}", event_id, e);
return Err(e)
}
}
// Get the event from the DAG
let event = self.server.darkirc.event_graph.dag_get(event_id).await.unwrap().unwrap();
// Try to deserialize it. (Here we skip errors)
let Ok((mut privmsg, _)) = deserialize_async_partial(event.content()).await else {
continue
};
// Potentially decrypt the privmsg
self.server.try_decrypt(&mut privmsg).await;
// If the privmsg is intented for any of the given channels, add it as
// a reply and mark it as seen in the seen_events tree.
if !channels.contains(&privmsg.channel) {
continue
}
let msg = format!("PRIVMSG {} :{}", privmsg.channel, privmsg.msg);
replies.push(ReplyType::Client((privmsg.nick, msg)));
if let Err(e) = self.mark_seen(event_id).await {
error!("[IRC CLIENT] (get_history) self.mark_seen({}) failed: {}", event_id, e);
return Err(e)
}
}
Ok(replies)
}
}