Quellcode durchsuchen

darkirc: Code refactor

This is a big rewrite of the DarkIRC code with an improved and more
robust architecture. We use proper client-server architecture without
having to rely on internal channels.

* The server now performs more correctly (although not fully) per the RFC.

* Removed obsolete code and made lots of performance and readability
  improvements.

* The network now propagates events over the Event Graph (DAG).

* Clients are more robust and stateful and have better separation.
parazyd vor 2 Jahren
Ursprung
Commit
4da846bf03

+ 5 - 5
Cargo.lock

@@ -1742,14 +1742,14 @@ version = "0.4.1"
 dependencies = [
  "async-rustls",
  "async-trait",
+ "blake3",
  "bs58",
- "chrono",
- "clap 4.4.1",
  "crypto_box",
  "darkfi",
  "darkfi-serial",
  "easy-parallel",
  "futures",
+ "libc",
  "log",
  "rand 0.8.5",
  "rustls-pemfile",
@@ -1757,10 +1757,10 @@ dependencies = [
  "signal-hook",
  "signal-hook-async-std",
  "simplelog",
+ "sled",
  "smol",
  "structopt",
  "structopt-toml",
- "tinyjson",
  "toml 0.7.6",
  "url",
 ]
@@ -3320,9 +3320,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
 
 [[package]]
 name = "libc"
-version = "0.2.147"
+version = "0.2.148"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
 
 [[package]]
 name = "libloading"

+ 2 - 1
Cargo.toml

@@ -138,7 +138,8 @@ prettytable-rs = "0.10.0"
 
 # -----BEGIN LIBRARY FEATURES-----
 [features]
-async-daemonize = []
+async-daemonize = ["system"]
+
 async-serial = ["darkfi-serial/async"]
 
 async-sdk = [

+ 7 - 5
bin/darkirc/Cargo.toml

@@ -9,8 +9,12 @@ homepage = "https://dark.fi"
 repository = "https://github.com/darkrenaissance/darkfi"
 
 [dependencies]
-darkfi = {path = "../../", features = ["bs58", "toml", "async-daemonize", "event-graph", "rpc"]}
-darkfi-serial = {path = "../../src/serial"}
+darkfi = {path = "../../", features = ["async-daemonize", "event-graph", "net", "util", "system", "rpc"]}
+darkfi-serial = {path = "../../src/serial", features = ["async"]}
+libc = "0.2.148"
+
+# Event Graph DB
+sled = "0.34.7"
 
 # TLS
 async-rustls = "0.4.0"
@@ -19,18 +23,16 @@ futures = "0.3.28"
 rustls-pemfile = "1.0.3"
 
 # Crypto
+blake3 = "1.4.1"
 crypto_box = {version = "0.9.1", features = ["std", "chacha20"]}
 rand = "0.8.5"
 
 # Misc
-clap = {version = "4.4.1", features = ["derive"]}
-chrono = "0.4.26"
 log = "0.4.20"
 url = "2.4.1"
 
 # Encoding and parsing
 bs58 = "0.5.0"
-tinyjson= "2.5.1"
 toml = "0.7.6"
 
 # Daemon

+ 3 - 10
bin/darkirc/README.md

@@ -1,11 +1,4 @@
-# darkirc
+darkirc
+=======
 
-see [Darkfi Book](https://darkrenaissance.github.io/darkfi/misc/darkirc/darkirc.html) for the installation guide.
-
-## Services
-
-To operate with `darkirc` using IRC clients, we can implement special
-namespaces which we are then able to query and use that as the
-client's interactive communication with the server/daemon:
-
-* `nickserv` - Account management
+[Installation Guide](https://darkrenaissance.github.io/darkfi/misc/darkirc/darkirc.html)

+ 7 - 3
bin/darkirc/darkirc_config.toml

@@ -51,6 +51,9 @@ seeds = [
     "tcp+tls://lilith1.dark.fi:5262",
 ]
 
+## Manual peers to connect to
+#peers = []
+
 # Whitelisted transports for outbound connections
 allowed_transports = ["tcp+tls"]
 
@@ -106,7 +109,8 @@ topic = "/b/"
 ## You can generate a keypair with: darkirc --gen-keypair
 ## and replace the secret key below with the generated one.
 ## **You should never share this secret key with anyone**
-#[secret_key."AKfyoKxnHb8smqP2zt9BVvXkcN7pm9GnqqyuYRmxmWtR"]
+#[crypto]
+#dm_chacha_secret = "AKfyoKxnHb8smqP2zt9BVvXkcN7pm9GnqqyuYRmxmWtR"
 
 ## This is where you put other people's public keys. The format is:
 ## [contact."nickname"]. "nickname" can be anything you want.
@@ -114,7 +118,7 @@ topic = "/b/"
 ##
 ## Example (set as many as you want):
 #[contact."satoshi"]
-#public_key = "C9vC6HNDfGQofWCapZfQK5MkV1JR8Cct839RDUCqbDGK"
+#dm_chacha_public = "C9vC6HNDfGQofWCapZfQK5MkV1JR8Cct839RDUCqbDGK"
 #
 #[contact."anon"]
-#public_key = "7iTddcopP2pkvszFjbFUr7MwTcMSKZkYP6zUan22pxfX"
+#dm_chacha_public = "7iTddcopP2pkvszFjbFUr7MwTcMSKZkYP6zUan22pxfX"

+ 24 - 145
bin/darkirc/src/crypto/saltbox.rs

@@ -16,168 +16,47 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{collections::HashMap, fmt};
-
 use crypto_box::{
     aead::{Aead, AeadCore},
     ChaChaBox,
 };
 use rand::rngs::OsRng;
 
-use crate::{
-    privmsg::PrivMsgEvent,
-    settings::{ChannelInfo, ContactInfo, MAXIMUM_LENGTH_OF_NICK_CHAN_CNT},
-};
-
-#[derive(serde::Serialize)]
-pub struct KeyPair {
-    pub public: String,
-    pub secret: String,
-}
-
-impl fmt::Display for KeyPair {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "Public key: {}\nSecret key: {}", self.public, self.secret)
-    }
-}
-
-/// The format we're using is nonce+ciphertext, where nonce is 24 bytes.
-fn try_decrypt(salt_box: &ChaChaBox, ciphertext: &str) -> Option<String> {
-    let bytes = match bs58::decode(ciphertext).into_vec() {
-        Ok(v) => v,
-        Err(_) => return None,
-    };
-
-    if bytes.len() < 25 {
-        return None
-    }
-
-    // Try extracting the nonce
-    let nonce = match bytes[0..24].try_into() {
-        Ok(v) => v,
-        Err(_) => return None,
-    };
-
-    // Take the remaining ciphertext
-    let message = &bytes[24..];
-
-    // Try decrypting the message
-    match salt_box.decrypt(nonce, message) {
-        Ok(v) => Some(String::from_utf8_lossy(&v).to_string()),
-        Err(_) => None,
-    }
-}
-
-/// The format we're using is nonce+ciphertext, where nonce is 24 bytes.
+/// Encrypt given data using the given `ChaChaBox`.
+/// Returns base58-encoded string of the ciphertext.
+/// Panics if encryption fails.
+///
+/// The encryption format we're using with `ChaChaBox` is `nonce||ciphertext`,
+/// where nonce is 24 bytes large, and the remaining data should be the ciphertext.
 pub fn encrypt(salt_box: &ChaChaBox, plaintext: &[u8]) -> String {
+    // Generate the nonce
     let nonce = ChaChaBox::generate_nonce(&mut OsRng);
+
+    // Encrypt
     let mut ciphertext = salt_box.encrypt(&nonce, plaintext).unwrap();
 
-    let mut concat = vec![];
+    // Concatenate
+    let mut concat = Vec::with_capacity(24 + ciphertext.len());
     concat.append(&mut nonce.as_slice().to_vec());
     concat.append(&mut ciphertext);
 
+    // Encode
     bs58::encode(concat).into_string()
 }
 
-pub fn decrypt_target(
-    contact: &mut String,
-    privmsg: &mut PrivMsgEvent,
-    configured_chans: &HashMap<String, ChannelInfo>,
-    configured_contacts: &HashMap<String, ContactInfo>,
-) {
-    for chan_name in configured_chans.keys() {
-        let chan_info = configured_chans.get(chan_name).unwrap();
-
-        if let Some(salt_box) = &chan_info.salt_box {
-            let decrypted_target = try_decrypt(salt_box, &privmsg.target);
-            if decrypted_target.is_none() {
-                continue
-            }
-
-            let target =
-                String::from_utf8_lossy(&unpad(decrypted_target.unwrap().into())).to_string();
-            if *chan_name == target {
-                privmsg.target = target;
-                return
-            }
-        }
-    }
-
-    for cnt_name in configured_contacts.keys() {
-        let cnt_info = configured_contacts.get(cnt_name).unwrap();
-
-        if let Some(salt_box) = &cnt_info.salt_box {
-            let decrypted_target = try_decrypt(salt_box, &privmsg.target);
-            if decrypted_target.is_none() {
-                continue
-            }
-
-            let target =
-                String::from_utf8_lossy(&unpad(decrypted_target.unwrap().into())).to_string();
-            privmsg.target = target;
-            *contact = cnt_name.into();
-            return
-        }
-    }
-}
-
-/// Decrypt PrivMsg nickname and message
-pub fn decrypt_privmsg(salt_box: &ChaChaBox, privmsg: &mut PrivMsgEvent) {
-    let decrypted_nick = try_decrypt(salt_box, &privmsg.nick);
-    let decrypted_msg = try_decrypt(salt_box, &privmsg.msg);
-
-    if decrypted_nick.is_none() && decrypted_msg.is_none() {
-        return
-    }
-
-    privmsg.nick = String::from_utf8_lossy(&unpad(decrypted_nick.unwrap().into())).to_string();
-    privmsg.msg = decrypted_msg.unwrap();
-}
-
-/// Encrypt PrivMsg
-pub fn encrypt_privmsg(salt_box: &ChaChaBox, privmsg: &mut PrivMsgEvent) {
-    privmsg.nick = encrypt(salt_box, &pad(privmsg.nick.clone().into()));
-    privmsg.target = encrypt(salt_box, &pad(privmsg.target.clone().into()));
-    privmsg.msg = encrypt(salt_box, privmsg.msg.as_bytes());
-}
-
-fn pad(data: Vec<u8>) -> Vec<u8> {
-    if data.len() == MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
-        return data
+/// Attempt to decrypt given ciphertext using the given `ChaChaBox`.
+/// Returns a lossy utf-8 string on success, and `None` on failure.
+///
+/// The encryption format we're using with `ChaChaBox` is `nonce||ciphertext`,
+/// where nonce is 24 bytes large, and the remaining data should be the ciphertext.
+pub fn try_decrypt(salt_box: &ChaChaBox, ciphertext: &[u8]) -> Option<String> {
+    // Make sure we have enough bytes to work with
+    if ciphertext.len() < 25 {
+        return None
     }
 
-    assert!(data.len() < MAXIMUM_LENGTH_OF_NICK_CHAN_CNT);
-    let padding = vec![0u8; MAXIMUM_LENGTH_OF_NICK_CHAN_CNT - data.len()];
-
-    let mut data = data;
-    data.extend_from_slice(&padding);
-    data
-}
-
-fn unpad(data: Vec<u8>) -> Vec<u8> {
-    assert!(data.len() == MAXIMUM_LENGTH_OF_NICK_CHAN_CNT);
-    match data.iter().position(|&x| x == 0u8) {
-        Some(idx) => data[..idx].to_vec(),
-        None => data,
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_pad_unpad() {
-        let nick = String::from("terry-davis");
-
-        let padded = pad(nick.clone().into());
-        assert!(padded.len() == 32);
-        assert_eq!(nick, String::from_utf8_lossy(&unpad(padded)));
-
-        let nick = String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
-        let padded = pad(nick.clone().into());
-        assert_eq!(nick, String::from_utf8_lossy(&padded));
-        assert_eq!(nick, String::from_utf8_lossy(&unpad(padded)));
+    match salt_box.decrypt((&ciphertext[0..24]).into(), &ciphertext[24..]) {
+        Ok(v) => Some(String::from_utf8_lossy(&v).into()),
+        Err(_) => None,
     }
 }

+ 256 - 571
bin/darkirc/src/irc/client.rs

@@ -16,636 +16,321 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{collections::HashSet, sync::Arc};
+use std::{
+    collections::{HashMap, HashSet},
+    sync::{
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
+        Arc,
+    },
+};
 
 use darkfi::{
-    event_graph::{model::Event, EventMsg},
+    event_graph2::{proto::EventPut, Event, NULL_ID},
     system::Subscription,
     Error, Result,
 };
+use darkfi_serial::{deserialize_async_partial, serialize_async};
 use futures::FutureExt;
-use log::{debug, error, info, warn};
+use log::{debug, error, warn};
 use smol::{
-    io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf},
+    io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader},
     lock::Mutex,
     net::SocketAddr,
+    prelude::{AsyncRead, AsyncWrite},
 };
 
-use crate::{
-    crypto::saltbox::{decrypt_privmsg, decrypt_target, encrypt_privmsg},
-    settings,
-    settings::{Nick, UserMode, RPL},
-    ChannelInfo, PrivMsgEvent,
-};
-
-use super::{ClientSubMsg, IrcConfig, NotifierMsg};
+use super::{server::IrcServer, Privmsg, SERVER_NAME};
 
-pub struct IrcClient<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> {
-    // network stream
-    write_stream: WriteHalf<C>,
-    read_stream: BufReader<ReadHalf<C>>,
-    pub address: SocketAddr,
+const PENALTY_LIMIT: usize = 5;
 
-    // irc config
-    irc_config: IrcConfig,
-
-    /// Joined channels, mapped here for better SIGHUP UX.
-    channels_joined: HashSet<String>,
-
-    server_notifier: smol::channel::Sender<(NotifierMsg, usize)>,
-    subscription: Subscription<ClientSubMsg>,
+/// Reply types, we can either send server replies, or client replies.
+pub enum ReplyType {
+    /// Server reply, we have to use numerics
+    Server((u16, String)),
+    /// Client reply, message from someone to some{one,where}
+    Client((String, String)),
+    /// Pong reply, we just use server origin
+    Pong(String),
+    /// CAP reply
+    Cap(String),
+}
 
-    missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
+/// Stateful IRC client, used for each client connection
+pub struct Client {
+    /// Pointer to parent `IrcServer`
+    pub server: Arc<IrcServer>,
+    /// Subscription for incoming events
+    pub incoming: Subscription<Event>,
+    /// Client socket addr
+    pub addr: SocketAddr,
+    /// ID of the last sent event
+    pub last_sent: Mutex<blake3::Hash>,
+    /// Active (joined) channels for this client
+    pub channels: Mutex<HashSet<String>>,
+    /// Penalty counter, when limit is reached, disconnect client
+    pub penalty: AtomicUsize,
+    /// Registration marker
+    pub registered: AtomicBool,
+    /// Registration pause marker
+    pub reg_paused: AtomicBool,
+    /// Client username
+    pub username: Mutex<String>,
+    /// Client nickname
+    pub nickname: Mutex<String>,
+    /// Client realname
+    pub realname: Mutex<String>,
+    /// Client caps
+    pub caps: Mutex<HashMap<String, bool>>,
 }
 
-impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> IrcClient<C> {
-    pub fn new(
-        write_stream: WriteHalf<C>,
-        read_stream: BufReader<ReadHalf<C>>,
-        address: SocketAddr,
-        irc_config: IrcConfig,
-        server_notifier: smol::channel::Sender<(NotifierMsg, usize)>,
-        subscription: Subscription<ClientSubMsg>,
-        missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
-    ) -> Self {
-        Self {
-            write_stream,
-            read_stream,
-            address,
-            irc_config,
-            channels_joined: HashSet::new(),
-            subscription,
-            server_notifier,
-            missed_events,
-        }
+impl Client {
+    /// Instantiate a new Client.
+    pub async fn new(
+        server: Arc<IrcServer>,
+        incoming: Subscription<Event>,
+        addr: SocketAddr,
+    ) -> Result<Self> {
+        let caps = HashMap::from([("no-history".to_string(), false)]);
+
+        Ok(Self {
+            server,
+            incoming,
+            addr,
+            last_sent: Mutex::new(NULL_ID),
+            channels: Mutex::new(HashSet::new()),
+            penalty: AtomicUsize::new(0),
+            registered: AtomicBool::new(false),
+            reg_paused: AtomicBool::new(false),
+            username: Mutex::new(String::from("*")),
+            nickname: Mutex::new(String::from("*")),
+            realname: Mutex::new(String::from("*")),
+            caps: Mutex::new(caps),
+        })
     }
 
-    /// Start listening for messages came from View or irc client
-    pub async fn listen(&mut self) {
-        loop {
-            let mut line = String::new();
+    /// This function handles a single IRC client. We listen to messages from the
+    /// IRC client and relay them to the network, and we also get notified of
+    /// incoming messages and relay them to the IRC client. The notifications come
+    /// from events being inserted into the Event Graph.
+    pub async fn multiplex_connection<S>(&self, stream: S) -> Result<()>
+    where
+        S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
+    {
+        let (reader, mut writer) = io::split(stream);
+        let mut reader = BufReader::new(reader);
+
+        // Our buffer for the client line
+        let mut line = String::new();
 
+        loop {
             futures::select! {
-                // Process msg from View or other client connnected to the same irc server
-                msg = self.subscription.receive().fuse() => {
-                    match msg {
-                        ClientSubMsg::Privmsg(m) => {
-                            if let Err(e) = self.process_msg(&m).await {
-                                error!("[CLIENT {}] Process msg: {}",  self.address, e);
-                                break
-                            }
-                        }
-                        ClientSubMsg::Config(c) => self.update_config(c).await,
+                // Process message from the IRC client
+                r = reader.read_line(&mut line).fuse() => {
+                    // If something failed during reading, we disconnect.
+                    if let Err(e) = r {
+                        error!("[IRC CLIENT] Read failed for {}: {}", self.addr, e);
+                        self.incoming.unsubscribe().await;
+                        return Err(Error::ChannelStopped)
                     }
 
-                }
-                // Process msg from IRC client
-                err = self.read_stream.read_line(&mut line).fuse() => {
-                    if let Err(e) = err {
-                        error!("[CLIENT {}] Read line error: {}", self.address, e);
-                        break
-                    }
-                    if let Err(e) = self.process_line(line).await {
-                        error!("[CLIENT {}] Process line failed: {}",  self.address, e);
-                        break
+                    // If the penalty limit is reached, disconnect the client.
+                    if self.penalty.load(SeqCst) == PENALTY_LIMIT {
+                        self.incoming.unsubscribe().await;
+                        return Err(Error::ChannelStopped)
                     }
-                }
-            }
-        }
-
-        warn!("[CLIENT {}] Close connection", self.address);
-        self.subscription.unsubscribe().await;
-    }
-
-    pub async fn update_config(&mut self, new_config: IrcConfig) {
-        info!("[CLIENT {}] Updating config...", self.address);
-
-        let old_config = self.irc_config.clone();
-        let mut _chans_to_replay = HashSet::new();
-        let mut _contacts_to_replay = HashSet::new();
-
-        for (name, new_info) in new_config.channels.iter() {
-            let Some(old_info) = old_config.channels.get(name) else {
-                // New channel wasn't in old config, replay it
-                _chans_to_replay.insert(name);
-                continue
-            };
-
-            // TODO: Maybe if the salt_box changed, replay it, although
-            // kinda hard to do since there's no Eq/PartialEq on there,
-            // and we probably shouldn't keep the secret key laying around.
-
-            // We got some secret key for this channel, replay it
-            if old_info.salt_box.is_none() && new_info.salt_box.is_some() {
-                _chans_to_replay.insert(name);
-                continue
-            }
-        }
-
-        for (name, _new_info) in new_config.contacts.iter() {
-            let Some(_old_info) = old_config.contacts.get(name) else {
-                // New contact wasn't in old config, replay it
-                _contacts_to_replay.insert(name);
-                continue
-            };
-        }
-
-        self.irc_config.channels.extend(new_config.channels);
-        self.irc_config.contacts.extend(new_config.contacts);
-        self.irc_config.pass = new_config.pass;
-
-        if self.on_receive_join(self.irc_config.channels.keys().cloned().collect()).await.is_err() {
-            warn!("Error joining updated channels");
-        } else {
-            info!("[CLIENT {}] Config updated", self.address);
-        }
-    }
-
-    pub async fn process_msg(&mut self, msg: &PrivMsgEvent) -> Result<()> {
-        debug!("[CLIENT {}] msg from View: {:?}", self.address, msg.to_string());
-
-        let mut msg = msg.clone();
-        let mut contact = String::new();
-
-        decrypt_target(
-            &mut contact,
-            &mut msg,
-            &self.irc_config.channels,
-            &self.irc_config.contacts,
-        );
-
-        // The message is a channel message
-        if msg.target.starts_with('#') {
-            // Try to potentially decrypt the incoming message.
-            if !self.irc_config.channels.contains_key(&msg.target) {
-                return Ok(())
-            }
-
-            // Skip everything if we're not joined in the channel
-            if !self.channels_joined.contains(&msg.target) {
-                return Ok(())
-            }
-
-            let chan_info = self.irc_config.channels.get_mut(&msg.target).unwrap();
-
-            // We use this flag to mark if the message has been encrypted or not.
-            // Depending on it, we set a specific usermode to the nickname in the
-            // channel so in UI we can tell who is sending encrypted messages.
-            let mut encrypted = false;
-
-            if let Some(salt_box) = &chan_info.salt_box {
-                decrypt_privmsg(salt_box, &mut msg);
-                encrypted = true;
-                debug!("[P2P] Decrypted received message: {:?}", msg);
-            }
-
-            // Add the nickname to the channel's names
-            let mut nick: Nick = msg.nick.clone().into();
-            let _mode_change = if chan_info.names.contains(&nick) {
-                let mut n = chan_info.names.get(&nick).unwrap().clone();
-                let mode_change = if encrypted {
-                    n.set_mode(UserMode::Voice)
-                } else {
-                    n.unset_mode(UserMode::Voice)
-                };
-                chan_info.names.insert(n);
-                mode_change
-            } else {
-                let mode_change = if encrypted { nick.set_mode(UserMode::Voice) } else { None };
-                chan_info.names.insert(nick);
-                mode_change
-            };
-
-            self.reply(&msg.to_string()).await?;
-            return Ok(())
-        }
-
-        // The message is not a channel message, handle accordingly.
-        if self.irc_config.is_cap_end && self.irc_config.is_nick_init {
-            if !self.irc_config.contacts.contains_key(&contact) {
-                return Ok(())
-            }
-
-            let contact_info = self.irc_config.contacts.get(&contact).unwrap();
-            if let Some(salt_box) = &contact_info.salt_box {
-                decrypt_privmsg(salt_box, &mut msg);
-                // This is for /query
-                msg.nick = contact;
-                debug!("[P2P] Decrypted received message: {:?}", msg);
-            }
-
-            self.reply(&msg.to_string()).await?;
-            return Ok(())
-        }
-
-        Ok(())
-    }
-
-    pub async fn process_line(&mut self, line: String) -> Result<()> {
-        let irc_msg = match clean_input_line(line) {
-            Ok(msg) => msg,
-            Err(e) => {
-                warn!("[CLIENT {}] Connection error: {}", self.address, e);
-                return Err(Error::ChannelStopped)
-            }
-        };
-
-        debug!("[CLIENT {}] Process msg: {}", self.address, irc_msg);
-
-        if let Err(e) = self.update(irc_msg).await {
-            warn!("[CLIENT {}] Connection error: {}", self.address, e);
-            return Err(Error::ChannelStopped)
-        }
-        Ok(())
-    }
-
-    async fn update(&mut self, line: String) -> Result<()> {
-        if line.len() > settings::MAXIMUM_LENGTH_OF_MESSAGE {
-            return Err(Error::MalformedPacket)
-        }
-
-        let (command, value) = parse_line(&line)?;
-        let (command, value) = (command.as_str(), value.as_str());
-
-        match command {
-            "PASS" => self.on_receive_pass(value).await?,
-            "USER" => self.on_receive_user().await?,
-            "NAMES" => self.on_receive_names(value.split(',').map(String::from).collect()).await?,
-            "NICK" => self.on_receive_nick(value).await?,
-            "JOIN" => self.on_receive_join(value.split(',').map(String::from).collect()).await?,
-            "PART" => self.on_receive_part(value.split(',').map(String::from).collect()).await?,
-            "TOPIC" => self.on_receive_topic(&line, value).await?,
-            "PING" => self.on_ping(value).await?,
-            "PRIVMSG" => self.on_receive_privmsg(&line, value).await?,
-            "CAP" => self.on_receive_cap(&line, &value.to_uppercase()).await?,
-            "QUIT" => self.on_quit()?,
-            _ => warn!("[CLIENT {}] Unimplemented `{}` command", self.address, command),
-        }
-
-        self.register().await?;
-        Ok(())
-    }
-
-    async fn register(&mut self) -> Result<()> {
-        if !self.irc_config.is_pass_init && self.irc_config.pass.is_empty() {
-            self.irc_config.is_pass_init = true
-        }
-
-        if !self.irc_config.is_registered &&
-            self.irc_config.is_cap_end &&
-            self.irc_config.is_nick_init &&
-            self.irc_config.is_user_init
-        {
-            debug!("Initializing peer connection");
-            let register_reply =
-                format!(":darkfi 001 {} :Let there be dark\r\n", self.irc_config.nick);
-            self.reply(&register_reply).await?;
-            self.irc_config.is_registered = true;
-
-            // join all channels
-            self.on_receive_join(self.irc_config.auto_channels.clone()).await?;
-            self.on_receive_join(self.irc_config.channels.keys().cloned().collect()).await?;
-        }
-        Ok(())
-    }
-
-    async fn reply(&mut self, message: &str) -> Result<()> {
-        self.write_stream.write_all(message.as_bytes()).await?;
-        debug!("Sent {}", message.trim_end());
-        Ok(())
-    }
-
-    fn on_quit(&self) -> Result<()> {
-        // Close the connection
-        Err(Error::NetworkServiceStopped)
-    }
-
-    async fn on_receive_user(&mut self) -> Result<()> {
-        // We can stuff any extra things like public keys in here.
-        // Ignore it for now.
-        if self.irc_config.is_pass_init {
-            self.irc_config.is_user_init = true;
-        } else {
-            // Close the connection
-            warn!("[CLIENT {}] Password is required", self.address);
-            return self.on_quit()
-        }
-        Ok(())
-    }
-
-    async fn on_receive_pass(&mut self, pass: &str) -> Result<()> {
-        if self.irc_config.pass == pass {
-            self.irc_config.is_pass_init = true
-        } else {
-            // Close the connection
-            warn!("[CLIENT {}] Password is not correct!", self.address);
-            return self.on_quit()
-        }
-        Ok(())
-    }
 
-    async fn on_receive_nick(&mut self, nickname: &str) -> Result<()> {
-        if nickname.len() >= settings::MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
-            return Ok(())
-        }
-
-        self.irc_config.is_nick_init = true;
-        let old_nick = std::mem::replace(&mut self.irc_config.nick, nickname.to_string());
-
-        let nick_reply = format!(":{}!anon@dark.fi NICK {}\r\n", old_nick, self.irc_config.nick);
-        self.reply(&nick_reply).await
-    }
-
-    async fn on_receive_part(&mut self, channels: Vec<String>) -> Result<()> {
-        for chan in channels.iter() {
-            let part_reply = format!(":{}!anon@dark.fi PART {}\r\n", self.irc_config.nick, chan);
-            self.reply(&part_reply).await?;
-            self.channels_joined.remove(chan);
-        }
-        Ok(())
-    }
-
-    async fn on_receive_topic(&mut self, line: &str, channel: &str) -> Result<()> {
-        if let Some(substr_idx) = line.find(':') {
-            // Client is setting the topic
-            if substr_idx >= line.len() {
-                return Err(Error::MalformedPacket)
-            }
+                    // We'll be strict here and disconnect the client
+                    // in case line processing failed in any way.
+                    match self.process_client_line(&line, &mut writer).await {
+                        // If we got an event back, we should broadcast it.
+                        // This means we add it to our DAG, and the DAG will
+                        // handle the rest of the propagation.
+                        Ok(Some(event)) => {
+                            // Update the last sent event.
+                            *self.last_sent.lock().await = event.id();
+
+                            // If it fails for some reason, for now, we just note it
+                            // and pass.
+                            if let Err(e) = self.server.darkirc.event_graph.dag_insert(event.clone()).await {
+                                error!("[IRC CLIENT] Failed inserting new event to DAG: {}", e);
+                            } else {
+                                // Otherwise, broadcast it
+                                self.server.darkirc.p2p.broadcast(&EventPut(event)).await;
+                            }
+                        }
 
-            let topic = &line[substr_idx + 1..];
-            let chan_info = self.irc_config.channels.get_mut(channel).unwrap();
-            chan_info.topic = Some(topic.to_string());
+                        // If we got nothing, we just pass.
+                        Ok(None) => {}
 
-            let topic_reply =
-                format!(":{}!anon@dark.fi TOPIC {} :{}\r\n", self.irc_config.nick, channel, topic);
-            self.reply(&topic_reply).await?;
-        } else {
-            // Client is asking or the topic
-            let chan_info = self.irc_config.channels.get(channel).unwrap();
-            let topic_reply = if let Some(topic) = &chan_info.topic {
-                format!("{} {} {} :{}\r\n", RPL::Topic as u32, self.irc_config.nick, channel, topic)
-            } else {
-                const TOPIC: &str = "No topic is set";
-                format!(
-                    "{} {} {} :{}\r\n",
-                    RPL::NoTopic as u32,
-                    self.irc_config.nick,
-                    channel,
-                    TOPIC
-                )
-            };
-            self.reply(&topic_reply).await?;
-        }
-        Ok(())
-    }
-
-    async fn on_ping(&mut self, value: &str) -> Result<()> {
-        let pong = format!("PONG {}\r\n", value);
-        self.reply(&pong).await
-    }
+                        // If we got an error, we disconnect the client.
+                        Err(e) => {
+                            self.incoming.unsubscribe().await;
+                            return Err(e)
+                        }
+                    }
 
-    async fn on_receive_cap(&mut self, line: &str, subcommand: &str) -> Result<()> {
-        self.irc_config.is_cap_end = false;
+                    // Clear the line buffer
+                    line = String::new();
+                    continue
+                }
 
-        let caps_keys: Vec<String> = self.irc_config.caps.keys().cloned().collect();
+                // Process message from the network. These should only be PRIVMSG.
+                r = self.incoming.receive().fuse() => {
+                    // We will skip this if it's our own message.
+                    if *self.last_sent.lock().await == r.id() {
+                        continue
+                    }
 
-        match subcommand {
-            "LS" => {
-                let cap_ls_reply = format!(
-                    ":{}!anon@dark.fi CAP * LS :{}\r\n",
-                    self.irc_config.nick,
-                    caps_keys.join(" ")
-                );
-                self.reply(&cap_ls_reply).await?;
-            }
+                    // Try to deserialize the `Event`'s content into a `Privmsg`
+                    let mut privmsg: Privmsg = match deserialize_async_partial(r.content()).await {
+                        Ok((v, _)) => v,
+                        Err(e) => {
+                            error!("[IRC CLIENT] Failed deserializing incoming Privmsg event: {}", e);
+                            continue
+                        }
+                    };
 
-            "REQ" => {
-                let substr_idx = line.find(':').ok_or(Error::MalformedPacket)?;
+                    // If successful, potentially decrypt it:
+                    self.server.try_decrypt(&mut privmsg).await;
 
-                if substr_idx >= line.len() {
-                    return Err(Error::MalformedPacket)
-                }
+                    // If we have this channel, or it's a DM to our nickname, forward
+                    // it to the client.
+                    let have_channel = self.channels.lock().await.contains(&privmsg.channel);
+                    let msg_for_self = *self.nickname.lock().await == privmsg.channel;
 
-                let cap: Vec<&str> = line[substr_idx + 1..].split(' ').collect();
+                    if have_channel || msg_for_self {
+                        // Add the nickname to the list of nicks on the channel
+                        (*self.server.channels.lock().await).get_mut(&privmsg.channel)
+                            .unwrap().nicks.insert(privmsg.nick.clone());
 
-                let mut ack_list = vec![];
-                let mut nak_list = vec![];
+                        // Format the message
+                        let msg = format!("PRIVMSG {} :{}", privmsg.channel, privmsg.msg);
 
-                for c in cap {
-                    if self.irc_config.caps.contains_key(c) {
-                        self.irc_config.caps.insert(c.to_string(), true);
-                        ack_list.push(c);
-                    } else {
-                        nak_list.push(c);
+                        // Send it to the client
+                        let reply = ReplyType::Client((privmsg.nick, msg));
+                        if let Err(e) = self.reply(&mut writer, &reply).await {
+                            error!("[IRC CLIENT] Failed writing PRIVMSG to client: {}", e);
+                            continue
+                        }
                     }
                 }
-
-                let cap_ack_reply = format!(
-                    ":{}!anon@dark.fi CAP * ACK :{}\r\n",
-                    self.irc_config.nick,
-                    ack_list.join(" ")
-                );
-
-                let cap_nak_reply = format!(
-                    ":{}!anon@dark.fi CAP * NAK :{}\r\n",
-                    self.irc_config.nick,
-                    nak_list.join(" ")
-                );
-
-                self.reply(&cap_ack_reply).await?;
-                self.reply(&cap_nak_reply).await?;
-            }
-
-            "LIST" => {
-                let enabled_caps: Vec<String> = self
-                    .irc_config
-                    .caps
-                    .clone()
-                    .into_iter()
-                    .filter(|(_, v)| *v)
-                    .map(|(k, _)| k)
-                    .collect();
-
-                let cap_list_reply = format!(
-                    ":{}!anon@dark.fi CAP * LIST :{}\r\n",
-                    self.irc_config.nick,
-                    enabled_caps.join(" ")
-                );
-                self.reply(&cap_list_reply).await?;
-            }
-
-            "END" => {
-                self.irc_config.is_cap_end = true;
             }
-            _ => {}
         }
-        Ok(())
     }
 
-    async fn on_receive_names(&mut self, channels: Vec<String>) -> Result<()> {
-        for chan in channels.iter() {
-            if !chan.starts_with('#') {
-                continue
-            }
-            if self.irc_config.channels.contains_key(chan) {
-                let chan_info = self.irc_config.channels.get(chan).unwrap();
-
-                if chan_info.names.is_empty() {
-                    return Ok(())
-                }
-
-                let names_reply = format!(
-                    ":{}!anon@dark.fi {} = {} : {}\r\n",
-                    self.irc_config.nick,
-                    RPL::NameReply as u32,
-                    chan,
-                    chan_info.names()
-                );
+    /// Send a reply to the IRC client. Matches on the reply type.
+    async fn reply<W>(&self, writer: &mut W, reply: &ReplyType) -> Result<()>
+    where
+        W: AsyncWrite + Unpin,
+    {
+        let r = match reply {
+            ReplyType::Server((rpl, msg)) => format!(":{} {:03} {}", SERVER_NAME, rpl, msg),
+            ReplyType::Client((nick, msg)) => format!(":{}!~anon@darkirc {}", nick, msg),
+            ReplyType::Pong(origin) => format!(":{} PONG :{}", SERVER_NAME, origin),
+            ReplyType::Cap(msg) => format!(":{} {}", SERVER_NAME, msg),
+        };
 
-                self.reply(&names_reply).await?;
+        debug!("[{}] <-- {}", self.addr, r);
 
-                let end_of_names = format!(
-                    ":DarkFi {:03} {} {} :End of NAMES list\r\n",
-                    RPL::EndOfNames as u32,
-                    self.irc_config.nick,
-                    chan
-                );
+        writer.write(r.as_bytes()).await?;
+        writer.write(b"\r\n").await?;
+        writer.flush().await?;
 
-                self.reply(&end_of_names).await?;
-            }
-        }
         Ok(())
     }
 
-    async fn on_receive_privmsg(&mut self, line: &str, target: &str) -> Result<()> {
-        let substr_idx = line.find(':').ok_or(Error::MalformedPacket)?;
-
-        if substr_idx >= line.len() {
-            return Err(Error::MalformedPacket)
+    /// Handle the incoming line given sent by the IRC client
+    async fn process_client_line<W>(&self, line: &str, writer: &mut W) -> Result<Option<Event>>
+    where
+        W: AsyncWrite + Unpin,
+    {
+        if line.is_empty() || line == "\n" || line == "\r\n" {
+            return Err(Error::ParseFailed("Line is empty"))
         }
 
-        let message = line[substr_idx + 1..].to_string();
-
-        debug!("[CLIENT {}] (Plain) PRIVMSG {} :{}", self.address, target, message,);
-
-        let mut privmsg = PrivMsgEvent::new();
-
-        privmsg.nick = self.irc_config.nick.clone();
-        privmsg.target = target.to_string();
-        privmsg.msg = message.clone();
-
-        if target.starts_with('#') {
-            if !self.irc_config.channels.contains_key(target) {
-                return Ok(())
-            }
-
-            if !self.channels_joined.contains(target) {
-                return Ok(())
-            }
-
-            let channel_info = self.irc_config.channels.get(target).unwrap();
+        let mut line = line.to_string();
 
-            if let Some(salt_box) = &channel_info.salt_box {
-                encrypt_privmsg(salt_box, &mut privmsg);
-                debug!("[CLIENT {}] (Encrypted) PRIVMSG: {:?}", self.address, privmsg);
-            }
+        // Remove CRLF
+        if &line[(line.len() - 2)..] == "\r\n" {
+            line.pop();
+            line.pop();
+        } else if &line[(line.len() - 1)..] == "\n" {
+            line.pop();
         } else {
-            if !self.irc_config.contacts.contains_key(target) {
-                return Ok(())
-            }
-
-            let contact_info = self.irc_config.contacts.get(target).unwrap();
-            if let Some(salt_box) = &contact_info.salt_box {
-                encrypt_privmsg(salt_box, &mut privmsg);
-                debug!("[CLIENT {}] (Encrypted) PRIVMSG: {:?}", self.address, privmsg);
-            }
+            return Err(Error::ParseFailed("Line doesn't end with CR/LF"))
         }
 
-        self.server_notifier
-            .send((NotifierMsg::Privmsg(privmsg), self.subscription.get_id()))
-            .await?;
-
-        Ok(())
-    }
-
-    async fn on_receive_join(&mut self, channels: Vec<String>) -> Result<()> {
-        for chan in channels.iter() {
-            if !chan.starts_with('#') {
-                continue
-            }
-
-            if !self.irc_config.channels.contains_key(chan) {
-                let mut chan_info = ChannelInfo::new()?;
-                chan_info.topic = Some("n/a".to_string());
-                self.irc_config.channels.insert(chan.to_string(), chan_info);
-            }
-
-            if !self.channels_joined.insert(chan.to_string()) {
-                return Ok(())
+        // Parse the line
+        let mut tokens = line.split_ascii_whitespace();
+        // Commands can begin with :garbage, but we will reject clients
+        // doing that for now to keep the protocol simple and focused.
+        let cmd = tokens.next().ok_or(Error::ParseFailed("Invalid command line"))?;
+        let args = line.replacen(cmd, "", 1);
+        let cmd = cmd.to_uppercase();
+
+        debug!("[{}] --> {}{}", self.addr, cmd, args);
+
+        // Handle the command. These implementations are in `command.rs`.
+        let replies: Vec<ReplyType> = match cmd.as_str() {
+            "ADMIN" => self.handle_cmd_admin(&args).await?,
+            "CAP" => self.handle_cmd_cap(&args).await?,
+            "INFO" => self.handle_cmd_info(&args).await?,
+            "JOIN" => self.handle_cmd_join(&args).await?,
+            "LIST" => self.handle_cmd_list(&args).await?,
+            "MODE" => self.handle_cmd_mode(&args).await?,
+            "MOTD" => self.handle_cmd_motd(&args).await?,
+            "NAMES" => self.handle_cmd_names(&args).await?,
+            "NICK" => self.handle_cmd_nick(&args).await?,
+            "PART" => self.handle_cmd_part(&args).await?,
+            "PING" => self.handle_cmd_ping(&args).await?,
+            "PRIVMSG" => self.handle_cmd_privmsg(&args).await?,
+            "REHASH" => self.handle_cmd_rehash(&args).await?,
+            "TOPIC" => self.handle_cmd_topic(&args).await?,
+            "USER" => self.handle_cmd_user(&args).await?,
+            "VERSION" => self.handle_cmd_version(&args).await?,
+            "QUIT" => return Err(Error::ChannelStopped),
+            _ => {
+                warn!("[IRC CLIENT] Unimplemented \"{}\" command", cmd);
+                vec![]
             }
+        };
 
-            let chan_info = self.irc_config.channels.get_mut(chan).unwrap();
-
-            let topic =
-                if let Some(topic) = chan_info.topic.clone() { topic } else { "n/a".to_string() };
-            chan_info.topic = Some(topic.to_string());
-
-            {
-                let j = format!(":{}!anon@dark.fi JOIN {}\r\n", self.irc_config.nick, chan);
-                let t = format!(":DarkFi TOPIC {} :{}\r\n", chan, topic);
-                self.reply(&j).await?;
-                self.reply(&t).await?;
-            }
-        }
-
-        if *self.irc_config.caps.get("no-history").unwrap() {
-            return Ok(())
+        // Depending on the reply type, we send according messages.
+        for reply in replies.iter() {
+            self.reply(writer, reply).await?;
         }
 
-        // Process missed messages if any (sorted by event's timestamp)
-        let mut hash_vec = self.missed_events.lock().await.clone();
-        hash_vec.sort_by(|a, b| a.timestamp.0.cmp(&b.timestamp.0));
+        // If the command was a PRIVMSG the client sent, we need to encrypt it and
+        // create an Event to broadcast and return it from this function. So let's try.
+        // We also do not allow sending unencrypted DMs. In that case, we send a notice
+        // to the client to inform them that the feature is not enabled.
+
+        // NOTE: This is not the most performant way to do this, probably not even
+        // TODO: the best place to do it. Patches welcome. It's also a bit fragile
+        // since we assume that `handle_cmd_privmsg()` won't return any replies.
+        if cmd.as_str() == "PRIVMSG" && replies.is_empty() {
+            let channel = args.split_ascii_whitespace().next().unwrap().to_string();
+            let msg_offset = args.find(':').unwrap() + 1;
+            let (_, msg) = args.split_at(msg_offset);
+            let mut privmsg = Privmsg {
+                channel,
+                nick: self.nickname.lock().await.to_string(),
+                msg: msg.to_string(),
+            };
 
-        for event in hash_vec {
-            if let Err(e) = self.process_msg(&event.action).await {
-                error!("[CLIENT {}] Process msg: {}", self.address, e);
-                continue
-            }
-        }
-        Ok(())
-    }
-}
+            // Encrypt the Privmsg if an encryption method is available.
+            self.server.try_encrypt(&mut privmsg).await;
 
-//
-// Helper functions
-//
-fn clean_input_line(mut line: String) -> Result<String> {
-    if line.is_empty() {
-        return Err(Error::ChannelStopped)
-    }
+            // Build a DAG event and return it.
+            let event = Event::new(
+                serialize_async(&privmsg).await,
+                self.server.darkirc.event_graph.clone(),
+            )
+            .await;
 
-    if line == "\n" || line == "\r\n" {
-        return Err(Error::ChannelStopped)
-    }
+            return Ok(Some(event))
+        }
 
-    if &line[(line.len() - 2)..] == "\r\n" {
-        // Remove CRLF
-        line.pop();
-        line.pop();
-    } else if &line[(line.len() - 1)..] == "\n" {
-        line.pop();
-    } else {
-        return Err(Error::ChannelStopped)
+        Ok(None)
     }
-
-    Ok(line.clone())
-}
-
-fn parse_line(line: &str) -> Result<(String, String)> {
-    let mut tokens = line.split_ascii_whitespace();
-    // Commands can begin with :garbage but we will reject clients doing
-    // that for now to keep the protocol simple and focused.
-    let command = tokens.next().ok_or(Error::MalformedPacket)?.to_uppercase();
-    let value = tokens.next().ok_or(Error::MalformedPacket)?;
-    Ok((command, value.to_owned()))
 }

+ 881 - 0
bin/darkirc/src/irc/command.rs

@@ -0,0 +1,881 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+//! IRC command implemenatations
+//!
+//! These try to follow the RFCs, modified in order for our P2P stack.
+//! Copied from https://simple.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands
+//!
+//! 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 log::{error, info};
+
+use super::{
+    client::{Client, ReplyType},
+    rpl::*,
+    IrcChannel, SERVER_NAME,
+};
+
+impl Client {
+    /// `ADMIN [<server>]`
+    ///
+    /// Asks the server for information about the administrator of the server.
+    pub async fn handle_cmd_admin(&self, _args: &str) -> Result<Vec<ReplyType>> {
+        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.lock().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 <args>`
+    pub async fn handle_cmd_cap(&self, args: &str) -> Result<Vec<ReplyType>> {
+        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.lock().await, INVALID_SYNTAX),
+            ))])
+        };
+
+        let caps_keys: Vec<String> = self.caps.lock().await.keys().cloned().collect();
+        let nick = self.nickname.lock().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.lock().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.lock().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<String> = self
+                    .caps
+                    .lock()
+                    .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 :{}", self.nickname.lock().await, INVALID_SYNTAX),
+        ))])
+    }
+
+    /// `INFO [<target>]`
+    ///
+    /// Gives information about the `<target>` server, or the current server if
+    /// `<target>` 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<Vec<ReplyType>> {
+        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.lock().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 <channels> [<keys>]`
+    ///
+    /// Makes the client join the channels in the list `<channels>`.
+    /// Passwords can be used in the list `<keys>`. If the channels do not
+    /// exist, they will be created.
+    pub async fn handle_cmd_join(&self, args: &str) -> Result<Vec<ReplyType>> {
+        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.lock().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 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 :{}", self.nickname.lock().await, 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 :{}", self.nickname.lock().await, INVALID_SYNTAX),
+            ))])
+        }
+
+        // Create new channels for this client and construct replies.
+        let mut server_channels = self.server.channels.lock().await;
+        let mut replies = vec![];
+        let nick = self.nickname.lock().await.to_string();
+        for channel in channels {
+            // 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),
+                    )));
+                }
+            }
+        }
+
+        Ok(replies)
+    }
+
+    /// `LIST [<channels> [<server>]]`
+    ///
+    /// List all channels on the server. If the list `<channels>` is given, it
+    /// will return the channel topics. If <server> is given, the command will
+    /// be sent to `<server>` for evaluation.
+    pub async fn handle_cmd_list(&self, _args: &str) -> Result<Vec<ReplyType>> {
+        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.lock().await.to_string();
+
+        let mut list = vec![];
+        for (name, channel) in self.server.channels.lock().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 <nickname> <flags>`
+    /// `MODE <channel> <flags>`
+    ///
+    /// 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<Vec<ReplyType>> {
+        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(target) = tokens.next() else {
+            self.penalty.fetch_add(1, SeqCst);
+            return Ok(vec![ReplyType::Server((
+                ERR_NEEDMOREPARAMS,
+                format!("{} MODE :{}", self.nickname.lock().await, INVALID_SYNTAX),
+            ))])
+        };
+
+        let nick = self.nickname.lock().await.to_string();
+
+        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.lock().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 [<server>]`
+    ///
+    /// Returns the message of the day on <server> or the current server if
+    /// it is not stated.
+    pub async fn handle_cmd_motd(&self, _args: &str) -> Result<Vec<ReplyType>> {
+        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 [<channel>]`
+    ///
+    /// Returns a list of who is on the list of <channel>, by channel name.
+    /// If <channel> 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<Vec<ReplyType>> {
+        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.lock().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.lock().await.get(req_chan) {
+                let nicks: Vec<String> = 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.lock().await.iter() {
+                let nicks: Vec<String> = 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 <nickname>`
+    ///
+    /// Allows a client to change their IRC nickname.
+    pub async fn handle_cmd_nick(&self, args: &str) -> Result<Vec<ReplyType>> {
+        // Parse the line
+        let mut tokens = args.split_ascii_whitespace();
+
+        // Reference the current nickname
+        let old_nick = self.nickname.lock().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),
+            ))])
+        }
+
+        // Set the new nickname
+        *self.nickname.lock().await = nickname.to_string();
+
+        // If the username is set, we can complete the registration
+        if *self.username.lock().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 :{}", self.nickname.lock().await)))])
+        } else {
+            // Otherwise, we don't reply.
+            Ok(vec![])
+        }
+    }
+
+    /// `PART <channel>`
+    ///
+    /// Causes a user to leave the channel <channel>.
+    pub async fn handle_cmd_part(&self, args: &str) -> Result<Vec<ReplyType>> {
+        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(channel) = tokens.next() else {
+            self.penalty.fetch_add(1, SeqCst);
+            return Ok(vec![ReplyType::Server((
+                ERR_NEEDMOREPARAMS,
+                format!("{} PART :{}", self.nickname.lock().await, INVALID_SYNTAX),
+            ))])
+        };
+
+        if !channel.starts_with('#') {
+            self.penalty.fetch_add(1, SeqCst);
+            return Ok(vec![ReplyType::Server((
+                ERR_NEEDMOREPARAMS,
+                format!("{} PART :{}", self.nickname.lock().await, INVALID_SYNTAX),
+            ))])
+        }
+
+        let mut active_channels = self.channels.lock().await;
+        if !active_channels.contains(channel) {
+            return Ok(vec![ReplyType::Server((
+                ERR_NOSUCHCHANNEL,
+                format!("{} {} :No such channel", self.nickname.lock().await, channel),
+            ))])
+        }
+
+        // Remove the channel from the client's channel list
+        active_channels.remove(channel);
+
+        let replies = vec![ReplyType::Client((
+            self.nickname.lock().await.to_string(),
+            format!("PART {} :Bye", channel),
+        ))];
+
+        Ok(replies)
+    }
+
+    /// `PING <server1>`
+    ///
+    /// Tests a connection. A PING message results in a PONG reply.
+    pub async fn handle_cmd_ping(&self, args: &str) -> Result<Vec<ReplyType>> {
+        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.lock().await),
+            ))])
+        };
+
+        Ok(vec![ReplyType::Pong(origin.to_string())])
+    }
+
+    /// `PRIVMSG <msgtarget> <message>`
+    ///
+    /// Sends <message> to <msgtarget>. The target is usually a user or
+    /// a channel.
+    pub async fn handle_cmd_privmsg(&self, args: &str) -> Result<Vec<ReplyType>> {
+        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(target) = tokens.next() else {
+            return Ok(vec![ReplyType::Server((
+                ERR_NORECIPIENT,
+                format!("{} :No recipient given (PRIVMSG)", self.nickname.lock().await),
+            ))])
+        };
+
+        let Some(message) = tokens.next() else {
+            return Ok(vec![ReplyType::Server((
+                ERR_NOTEXTTOSEND,
+                format!("{} :No text to send", self.nickname.lock().await),
+            ))])
+        };
+
+        if !message.starts_with(':') {
+            return Ok(vec![ReplyType::Server((
+                ERR_NOTEXTTOSEND,
+                format!("{} :No text to send", self.nickname.lock().await),
+            ))])
+        }
+
+        // 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 == *self.nickname.lock().await {
+            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.lock().await.contains_key(target) {
+            return Ok(vec![ReplyType::Server((
+                ERR_NOSUCHNICK,
+                format!("{} :{}", self.nickname.lock().await, 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<Vec<ReplyType>> {
+        info!("Attempting to rehash server...");
+        if let Err(e) = self.server.rehash().await {
+            error!("Failed to rehash server: {}", e);
+        }
+
+        Ok(vec![])
+    }
+
+    /// `TOPIC <channel> [<topic>]`
+    ///
+    /// Used to get the channel topic on <channel>. If <topic> is given, it
+    /// sets the channel topic to <topic>.
+    pub async fn handle_cmd_topic(&self, args: &str) -> Result<Vec<ReplyType>> {
+        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(channel) = tokens.next() else {
+            self.penalty.fetch_add(1, SeqCst);
+            return Ok(vec![ReplyType::Server((
+                ERR_NEEDMOREPARAMS,
+                format!("{} TOPIC :{}", self.nickname.lock().await, INVALID_SYNTAX),
+            ))])
+        };
+
+        if !self.server.channels.lock().await.contains_key(channel) {
+            return Ok(vec![ReplyType::Server((
+                ERR_NOSUCHCHANNEL,
+                format!("{} {} :No such channel", self.nickname.lock().await, 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.lock().await.get(channel).unwrap().topic.clone();
+            if topic.is_empty() {
+                return Ok(vec![ReplyType::Server((
+                    RPL_NOTOPIC,
+                    format!("{} {} :No topic is set", self.nickname.lock().await, channel),
+                ))])
+            } else {
+                return Ok(vec![ReplyType::Server((
+                    RPL_TOPIC,
+                    format!("{} {} :{}", self.nickname.lock().await, channel, topic),
+                ))])
+            }
+        };
+
+        // Set the new topic
+        self.server.channels.lock().await.get_mut(channel).unwrap().topic =
+            topic.strip_prefix(':').unwrap().to_string();
+
+        // Send reply
+        let replies = vec![ReplyType::Client((
+            self.nickname.lock().await.to_string(),
+            format!("TOPIC {} {}", channel, topic),
+        ))];
+
+        Ok(replies)
+    }
+
+    /// `USER <user> <mode> <unused> <realname>`
+    ///
+    /// 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. <realname> may contain spaces, and thus must be
+    /// prefixed with a colon.
+    pub async fn handle_cmd_user(&self, args: &str) -> Result<Vec<ReplyType>> {
+        if self.registered.load(SeqCst) {
+            self.penalty.fetch_add(1, SeqCst);
+            return Ok(vec![ReplyType::Server((
+                ERR_ALREADYREGISTERED,
+                format!("{} :{}", self.nickname.lock().await, ALREADY_REGISTERED),
+            ))])
+        }
+
+        // Parse the line
+        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 :{}", self.nickname.lock().await, 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 :{}", self.nickname.lock().await, 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 :{}", self.nickname.lock().await, 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 :{}", self.nickname.lock().await, INVALID_SYNTAX),
+            ))])
+        };
+
+        if !realname.starts_with(':') {
+            self.penalty.fetch_add(1, SeqCst);
+            return Ok(vec![ReplyType::Server((
+                ERR_NEEDMOREPARAMS,
+                format!("{} USER :{}", self.nickname.lock().await, INVALID_SYNTAX),
+            ))])
+        }
+
+        *self.username.lock().await = username.to_string();
+        *self.realname.lock().await = realname.to_string();
+
+        // If the nickname is set, we can complete the registration
+        if *self.nickname.lock().await != "*" {
+            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<Vec<ReplyType>> {
+        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.lock().await,
+                env!("CARGO_PKG_VERSION"),
+                SERVER_NAME
+            ),
+        ))];
+
+        Ok(replies)
+    }
+
+    /// Internal function that constructs the welcome message.
+    async fn welcome(&self) -> Vec<ReplyType> {
+        let nick = self.nickname.lock().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.lock().await;
+        for channel in self.server.autojoin.lock().await.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());
+            }
+        }
+
+        replies
+    }
+}

+ 28 - 67
bin/darkirc/src/irc/mod.rs

@@ -16,83 +16,44 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::collections::HashMap;
+use std::{collections::HashSet, sync::Arc};
 
-use darkfi::{util::path::get_config_path, Result};
+use crypto_box::ChaChaBox;
+use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
 
-use crate::{
-    settings::{
-        parse_configured_channels, parse_configured_contacts, Args, ChannelInfo, ContactInfo,
-        CONFIG_FILE, MAXIMUM_LENGTH_OF_NICK_CHAN_CNT,
-    },
-    PrivMsgEvent,
-};
+/// IRC client state
+pub(crate) mod client;
 
-mod client;
-pub use client::IrcClient;
+/// IRC server implementation
+pub(crate) mod server;
 
-mod server;
-pub use server::IrcServer;
+/// IRC command handler
+pub(crate) mod command;
 
-#[derive(Clone)]
-pub struct IrcConfig {
-    // init bool
-    pub is_nick_init: bool,
-    pub is_user_init: bool,
-    pub is_registered: bool,
-    pub is_cap_end: bool,
-    pub is_pass_init: bool,
-
-    // user config
-    pub nick: String,
-    pub pass: String,
-    pub caps: HashMap<String, bool>,
-
-    // channels and contacts
-    pub auto_channels: Vec<String>,
-    pub channels: HashMap<String, ChannelInfo>,
-    pub contacts: HashMap<String, ContactInfo>,
-}
+/// IRC numerics and server replies
+pub(crate) mod rpl;
 
-impl IrcConfig {
-    pub fn new(settings: &Args) -> Result<Self> {
-        let pass = settings.password.as_ref().unwrap_or(&String::new()).clone();
+/// Hardcoded server name
+const SERVER_NAME: &str = "irc.dark.fi";
 
-        let mut auto_channels = settings.autojoin.clone();
-        auto_channels.retain(|chan| chan.len() <= MAXIMUM_LENGTH_OF_NICK_CHAN_CNT);
-
-        // Pick up channel settings from the TOML configuration
-        let cfg_path = get_config_path(settings.config.clone(), CONFIG_FILE)?;
-        let toml_contents = std::fs::read_to_string(cfg_path)?;
-        let channels = parse_configured_channels(&toml_contents)?;
-        let contacts = parse_configured_contacts(&toml_contents)?;
-
-        let mut caps = HashMap::new();
-        caps.insert("no-history".to_string(), false);
-
-        Ok(Self {
-            is_nick_init: false,
-            is_user_init: false,
-            is_registered: false,
-            is_cap_end: true,
-            is_pass_init: false,
-            nick: "anon".to_string(),
-            pass,
-            auto_channels,
-            channels,
-            contacts,
-            caps,
-        })
-    }
+/// IRC PRIVMSG
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct Privmsg {
+    pub channel: String,
+    pub nick: String,
+    pub msg: String,
 }
 
+/// IRC channel definition
 #[derive(Clone)]
-pub enum ClientSubMsg {
-    Privmsg(PrivMsgEvent),
-    Config(IrcConfig),
+pub struct IrcChannel {
+    pub topic: String,
+    pub nicks: HashSet<String>,
+    pub saltbox: Option<Arc<ChaChaBox>>,
 }
+
+/// IRC contact definition
 #[derive(Clone)]
-pub enum NotifierMsg {
-    Privmsg(PrivMsgEvent),
-    UpdateConfig,
+pub struct IrcContact {
+    pub saltbox: Option<Arc<ChaChaBox>>,
 }

+ 205 - 0
bin/darkirc/src/irc/rpl.rs

@@ -0,0 +1,205 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+#![allow(clippy::zero_prefixed_literal)]
+
+/// The welcome message sent upon successful registration
+pub const WELCOME: &str = "Welcome to the DarkIRC network";
+/// The message sent to the client when they are not registered
+pub const NOT_REGISTERED: &str = "You have not registered";
+/// The message sent to the client when they are already registered
+pub const ALREADY_REGISTERED: &str = "You may not reregister";
+/// The message sent to the client when command params could not parse
+pub const INVALID_SYNTAX: &str = "Syntax error";
+
+/// `<client> :Welcome to the DarkIRC network`
+///
+/// The first message sent after client registration.
+pub const RPL_WELCOME: u16 = 001;
+
+/// `<client> :Your host is <servername>, running version <version`
+///
+/// Part of the post-registration greeting.
+pub const RPL_YOURHOST: u16 = 002;
+
+/// `<client> <user modes>`
+///
+/// Sent to a client to inform that client of their currently-set user modes.
+pub const RPL_UMODEIS: u16 = 221;
+
+/// `<client> [<server>] :Administrative info`
+///
+/// Sent as a reply to an ADMIN command, this numeric establishes the
+/// name of the server whose administrative info is being provided.
+pub const RPL_ADMINME: u16 = 256;
+
+/// `<client> :<info>`
+///
+/// Sent as a reply to an ADMIN command. <info> is a string intended to
+/// provide information about the location of the server.
+pub const RPL_ADMINLOC1: u16 = 257;
+
+/// `<client> :<info>`
+///
+/// Sent as a reply to an ADMIN command. <info> is a string intended to
+/// provide information about whoever runs the server.
+pub const RPL_ADMINLOC2: u16 = 258;
+
+/// `<client> :<info>`
+///
+/// Sent as a reply to an ADMIN command. <info> MUST contain the email
+/// address to contact the administrator(s) of the server.
+pub const RPL_ADMINEMAIL: u16 = 259;
+
+/// `<client> Channel :Users  Name`
+///
+/// Sent as a reply to the LIST command, this numeric marks the start
+/// of a channel list.
+pub const RPL_LISTSTART: u16 = 321;
+
+/// `<client> <channel> <client count> :<topic>`
+///
+/// Sent as a reply to the LIST command, this numeric sends information
+/// about a channel to the client. <channel> is the name of the channel.
+/// <client count> is an integer indicating how many clients are joined
+/// to that channel. <topic> is the channel’s topic.
+pub const RPL_LIST: u16 = 322;
+
+/// <client> :End of /LIST
+///
+/// Sent as a reply to the LIST command, this numeric indicates the end
+/// of a LIST response.
+pub const RPL_LISTEND: u16 = 323;
+
+/// `<client> <channel> <modestring> <mode arguments>...`
+///
+/// Sent to a client to inform them of the currently-set modes of a channel.
+/// <channel> is the name of the channel.
+pub const RPL_CHANNELMODEIS: u16 = 324;
+
+/// `<client> <channel> :No topic is set`
+///
+/// Sent to a client when joining a channel to inform them that the channel
+/// with the name <channel> does not have any topic set.
+pub const RPL_NOTOPIC: u16 = 331;
+
+/// `<client> <channel> :<topic>`
+///
+/// Sent to a client when joining the <channel> to inform them of the
+/// current topic of the channel.
+pub const RPL_TOPIC: u16 = 332;
+
+/// `<client> <version> <server> :<comments>`
+///
+/// Sent as a reply to the VERSION command.
+pub const RPL_VERSION: u16 = 351;
+
+/// `<client> <symbol> <channel> :[prefix]<nick>{ [prefix]<nick>}`
+///
+/// Sent as a reply to the NAMES command
+pub const RPL_NAMREPLY: u16 = 353;
+
+/// `<client> <channel> :End of /NAMES list`
+///
+/// Sent as a reply to the NAMES command
+pub const RPL_ENDOFNAMES: u16 = 366;
+
+/// `<client> :<string>`
+///
+/// Sent as the reply to the INFO command.
+pub const RPL_INFO: u16 = 371;
+
+/// `<client> :End of INFO list`
+///
+/// Indicates the end of an INFO response.
+pub const RPL_ENDOFINFO: u16 = 374;
+
+/// `<client> :- <server> Message of the day -`
+///
+/// Indicates the start of the Message of the Day to the client.
+pub const RPL_MOTDSTART: u16 = 375;
+
+/// `<client> :<line of the motd>`
+///
+/// When sending the Message of the Day to the client, servers reply
+/// with each line of the MOTD as this numeric.
+pub const RPL_MOTD: u16 = 372;
+
+/// `<client> :End of /MOTD command.`
+///
+/// Indicates the end of the Message of the Day to the client.
+pub const RPL_ENDOFMOTD: u16 = 376;
+
+/// `<client> <nickname> :No such nick/channel`
+///
+/// Indicates that no client can be found for the supplied nickname.
+pub const ERR_NOSUCHNICK: u16 = 401;
+
+/// `<client> <channel> :No such channel`
+///
+/// Indicates that no channel can be found for the supplied channel name.
+pub const ERR_NOSUCHCHANNEL: u16 = 403;
+
+/// `<client> :No origin specified`
+///
+/// Indicates a PING or PONG message missing the originator parameter
+/// which is required by old IRC servers. Nowadays, this may be used by
+/// some servers when the PING <token> is empty.
+pub const ERR_NOORIGIN: u16 = 409;
+
+/// `<client> :No recipient given (<command>)`
+///
+/// Returned by the PRIVMSG command to indicate the message wasn’t
+/// delivered because there was no recipient given.
+pub const ERR_NORECIPIENT: u16 = 411;
+
+/// `<client> :No text to send`
+///
+/// Returned by the PRIVMSG command to indicate the message wasn’t
+/// delivered because there was no text to send.
+pub const ERR_NOTEXTTOSEND: u16 = 412;
+
+/// <client> <nick> :Erroneus nickname
+///
+/// Returned when a NICK command cannot be successfully completed as
+/// the desired nickname contains characters that are disallowed by the server.
+pub const ERR_ERRONEOUSNICKNAME: u16 = 432;
+
+/// `<client> :You have not registered`
+///
+/// Returned when a client command cannot be parsed because they are
+/// not registered.
+pub const ERR_NOTREGISTERED: u16 = 451;
+
+/// `<client> <command> :Not enough parameters`
+///
+/// Returned when a client command cannot be parsed because not enough
+/// parameters were supplied.
+pub const ERR_NEEDMOREPARAMS: u16 = 461;
+
+/// `<client> :You may not reregister`
+///
+/// Returned when a client tries to change a detail that can only be
+/// set during registration.
+pub const ERR_ALREADYREGISTERED: u16 = 462;
+
+/// `<client> :Cant change mode for other users`
+///
+/// Indicates that a MODE command affecting a user failed because they
+/// were trying to set or view modes for other users.
+pub const ERR_USERSDONTMATCH: u16 = 502;

+ 260 - 258
bin/darkirc/src/irc/server.rs

@@ -16,324 +16,326 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-use std::{fs::File, sync::Arc};
+use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf, sync::Arc};
 
 use async_rustls::{rustls, TlsAcceptor};
-use log::{error, info};
+use darkfi::{
+    event_graph2::Event,
+    system::{StoppableTask, StoppableTaskPtr, Subscription},
+    util::path::expand_path,
+    Error, Result,
+};
+use log::{debug, error, info};
 use smol::{
-    io::{self, AsyncRead, AsyncWrite, BufReader},
+    fs,
     lock::Mutex,
     net::{SocketAddr, TcpListener},
+    prelude::{AsyncRead, AsyncWrite},
+    Executor,
 };
+use url::Url;
 
-use darkfi::{
-    event_graph::{
-        model::{Event, EventId, ModelPtr},
-        protocol_event::{Seen, SeenPtr},
-        view::ViewPtr,
-    },
-    net::P2pPtr,
-    system::{StoppableTask, SubscriberPtr},
-    util::{path::expand_path, time::Timestamp},
-    Error, Result,
+use super::{client::Client, IrcChannel, IrcContact, Privmsg};
+use crate::{
+    crypto::saltbox,
+    settings::{parse_autojoin_channels, parse_configured_channels, parse_configured_contacts},
+    DarkIrc,
 };
 
-use super::{ClientSubMsg, IrcClient, IrcConfig, NotifierMsg};
-
-use crate::{settings::Args, PrivMsgEvent};
-
-mod nickserv;
-use nickserv::NickServ;
-
-const NICK_NICKSERV: &str = "nickserv";
-
+/// IRC server instance
 pub struct IrcServer {
-    settings: Args,
-    p2p: P2pPtr,
-    model: ModelPtr<PrivMsgEvent>,
-    view: ViewPtr<PrivMsgEvent>,
-    clients_subscriptions: SubscriberPtr<ClientSubMsg>,
-    seen: SeenPtr<EventId>,
-    missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
-    /// nickserv service
-    pub nickserv: NickServ,
+    /// DarkIrc instance
+    pub darkirc: Arc<DarkIrc>,
+    /// Path to the darkirc config file
+    config_path: PathBuf,
+    /// TCP listener
+    listener: TcpListener,
+    /// TLS acceptor
+    acceptor: Option<TlsAcceptor>,
+    /// Configured autojoin channels
+    pub autojoin: Mutex<Vec<String>>,
+    /// Configured IRC channels
+    pub channels: Mutex<HashMap<String, IrcChannel>>,
+    /// Configured IRC contacts
+    pub contacts: Mutex<HashMap<String, IrcContact>>,
+    /// Active client connections
+    clients: Mutex<HashMap<u16, StoppableTaskPtr>>,
 }
 
 impl IrcServer {
+    /// Instantiate a new IRC server. This function will try to bind a TCP socket,
+    /// and optionally load a TLS certificate and key. To start the listening loop,
+    /// call `IrcServer::listen()`.
     pub async fn new(
-        settings: Args,
-        p2p: P2pPtr,
-        model: ModelPtr<PrivMsgEvent>,
-        view: ViewPtr<PrivMsgEvent>,
-        clients_subscriptions: SubscriberPtr<ClientSubMsg>,
-    ) -> Result<Self> {
-        let seen = Seen::new();
-        let missed_events = Arc::new(Mutex::new(vec![]));
-        Ok(Self {
-            settings,
-            p2p,
-            model,
-            view,
-            clients_subscriptions,
-            seen,
-            missed_events,
-            nickserv: NickServ::default(),
-        })
-    }
+        darkirc: Arc<DarkIrc>,
+        listen: Url,
+        tls_cert: Option<String>,
+        tls_secret: Option<String>,
+        config_path: PathBuf,
+    ) -> Result<Arc<Self>> {
+        let scheme = listen.scheme();
+        if scheme != "tcp" && scheme != "tcp+tls" {
+            error!("IRC server supports listening only on tcp:// or tcp+tls://");
+            return Err(Error::BindFailed(listen.to_string()))
+        }
 
-    pub async fn start(&self, executor: Arc<smol::Executor<'_>>) -> Result<()> {
-        let (msg_notifier, msg_recv) = smol::channel::unbounded();
-
-        // Listen to msgs from clients
-        StoppableTask::new().start(
-            Self::listen_to_msgs(
-                self.p2p.clone(),
-                self.model.clone(),
-                self.seen.clone(),
-                msg_recv,
-                self.missed_events.clone(),
-                self.clients_subscriptions.clone(),
-            ),
-            |res| async {
-                match res {
-                    Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
-                    Err(e) => error!(target: "darkirc::irc::server::start", "Failed starting listen to msgs: {}", e),
-                }
-            },
-            Error::DetachedTaskStopped,
-            executor.clone(),
-        );
+        if scheme == "tcp+tls" && (tls_cert.is_none() || tls_secret.is_none()) {
+            error!("You must provide a TLS certificate and key if you want a TLS server");
+            return Err(Error::BindFailed(listen.to_string()))
+        }
 
-        // Listen to msgs from View
-        StoppableTask::new().start(
-            Self::listen_to_view(
-                self.view.clone(),
-                self.seen.clone(),
-                self.missed_events.clone(),
-                self.clients_subscriptions.clone(),
-            ),
-            |res| async {
-                match res {
-                    Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
-                    Err(e) => error!(target: "darkirc::irc::server::start", "Failed starting listen to view: {}", e),
-                }
-            },
-            Error::DetachedTaskStopped,
-            executor.clone(),
-        );
+        // Bind listener
+        let listen_addr = listen.socket_addrs(|| None)?[0];
+        let listener = TcpListener::bind(listen_addr).await?;
+        let acceptor = match scheme {
+            "tcp+tls" => {
+                // openssl genpkey -algorithm ED25519 > example.com.key
+                // openssl req -new -out example.com.csr -key example.com.key
+                // openssl x509 -req -in example.com.csr -signkey example.com.key -out example.com.crt
+                let f = File::open(expand_path(tls_secret.as_ref().unwrap())?)?;
+                let mut reader = BufReader::new(f);
+                let secret = &rustls_pemfile::pkcs8_private_keys(&mut reader)?[0];
+                let secret = rustls::PrivateKey(secret.clone());
 
-        // Start listening for new connections
-        self.listen(msg_notifier, executor).await?;
+                let f = File::open(expand_path(tls_cert.as_ref().unwrap())?)?;
+                let mut reader = BufReader::new(f);
+                let cert = &rustls_pemfile::certs(&mut reader)?[0];
+                let cert = rustls::Certificate(cert.clone());
 
-        Ok(())
-    }
+                let config = rustls::ServerConfig::builder()
+                    .with_safe_defaults()
+                    .with_no_client_auth()
+                    .with_single_cert(vec![cert], secret)?;
 
-    async fn listen_to_view(
-        view: ViewPtr<PrivMsgEvent>,
-        seen: SeenPtr<EventId>,
-        missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
-        clients_subscriptions: SubscriberPtr<ClientSubMsg>,
-    ) -> Result<()> {
-        loop {
-            let event = view.lock().await.process().await?;
-            if !seen.push(&event.hash()).await {
-                continue
+                let acceptor = TlsAcceptor::from(Arc::new(config));
+                Some(acceptor)
             }
+            _ => None,
+        };
 
-            missed_events.lock().await.push(event.clone());
-
-            let msg = event.action.clone();
-
-            clients_subscriptions.notify(ClientSubMsg::Privmsg(msg)).await;
-        }
+        let self_ = Arc::new(Self {
+            darkirc,
+            config_path,
+            listener,
+            acceptor,
+            autojoin: Mutex::new(Vec::new()),
+            channels: Mutex::new(HashMap::new()),
+            contacts: Mutex::new(HashMap::new()),
+            clients: Mutex::new(HashMap::new()),
+        });
+
+        // Load any channel/contact configuration.
+        self_.rehash().await?;
+
+        Ok(self_)
     }
 
-    /// Start listening to msgs from irc clients
-    pub async fn listen_to_msgs(
-        p2p: P2pPtr,
-        model: ModelPtr<PrivMsgEvent>,
-        seen: SeenPtr<EventId>,
-        recv: smol::channel::Receiver<(NotifierMsg, usize)>,
-        missed_events: Arc<Mutex<Vec<Event<PrivMsgEvent>>>>,
-        clients_subscriptions: SubscriberPtr<ClientSubMsg>,
-    ) -> Result<()> {
-        loop {
-            let (msg, subscription_id) = recv.recv().await?;
-
-            match msg {
-                NotifierMsg::Privmsg(msg) => {
-                    // First check if we're communicating with any services.
-                    // If not, then we proceed with behaving like it's a normal
-                    // message.
-                    // TODO: This needs to be protected from adversaries doing
-                    //       remote execution.
-                    #[allow(clippy::single_match)]
-                    match msg.target.to_lowercase().as_str() {
-                        NICK_NICKSERV => {
-                            //self.nickserv.act(msg);
-                            continue
-                        }
-
-                        _ => {} // pass
-                    }
-
-                    let event = Event {
-                        previous_event_hash: model.lock().await.get_head_hash()?,
-                        action: msg.clone(),
-                        timestamp: Timestamp::current_time(),
-                    };
+    /// Reload the darkirc configuration file and reconfigure channels and contacts.
+    pub async fn rehash(&self) -> Result<()> {
+        let contents = fs::read_to_string(&self.config_path).await?;
+        let contents = match toml::from_str(&contents) {
+            Ok(v) => v,
+            Err(e) => {
+                error!("Failed parsing TOML config: {}", e);
+                return Err(Error::ParseFailed("Failed parsing TOML config"))
+            }
+        };
 
-                    // Since this will be added to the View directly, other clients connected to irc
-                    // server must get informed about this new msg
-                    clients_subscriptions
-                        .notify_with_exclude(ClientSubMsg::Privmsg(msg), &[subscription_id])
-                        .await;
+        // Parse autojoin channels
+        let autojoin = parse_autojoin_channels(&contents)?;
 
-                    if !seen.push(&event.hash()).await {
-                        continue
-                    }
+        // Parse configured channels
+        let channels = parse_configured_channels(&contents)?;
 
-                    missed_events.lock().await.push(event.clone());
+        // Parse configured contacts
+        let contacts = parse_configured_contacts(&contents)?;
 
-                    p2p.broadcast(&event).await;
-                }
+        // FIXME: This will remove clients' joined channels. They need to stay.
+        // Only if everything is fine, replace.
+        *self.autojoin.lock().await = autojoin;
+        *self.channels.lock().await = channels;
+        *self.contacts.lock().await = contacts;
 
-                NotifierMsg::UpdateConfig => {
-                    //
-                    // load and parse the new settings from configuration file and pass it to all
-                    // irc clients
-                    //
-                    // let new_config = IrcConfig::new()?;
-                    // clients_subscriptions.notify(ClientSubMsg::Config(new_config)).await;
-                }
-            }
-        }
+        Ok(())
     }
 
-    /// Start listening to new connections from irc clients
-    pub async fn listen(
-        &self,
-        notifier: smol::channel::Sender<(NotifierMsg, usize)>,
-        executor: Arc<smol::Executor<'_>>,
-    ) -> Result<()> {
-        let (listener, acceptor) = self.setup_listener().await?;
-        info!("[IRC SERVER] listening on {}", self.settings.irc_listen);
-
+    /// Start accepting new IRC connections.
+    pub async fn listen(self: Arc<Self>, ex: Arc<Executor<'_>>) -> Result<()> {
         loop {
-            let (stream, peer_addr) = match listener.accept().await {
+            let (stream, peer_addr) = match self.listener.accept().await {
                 Ok((s, a)) => (s, a),
+
+                // As per usual accept(2) recommendations
+                Err(e) if e.raw_os_error().is_some() => match e.raw_os_error().unwrap() {
+                    libc::EAGAIN | libc::ECONNABORTED | libc::EPROTO | libc::EINTR => continue,
+                    _ => {
+                        error!("[IRC SERVER] Failed accepting connection: {}", e);
+                        return Err(e.into())
+                    }
+                },
+
                 Err(e) => {
-                    error!("[IRC SERVER] Failed accepting new connections: {}", e);
+                    error!("[IRC SERVER] Failed accepting new connection: {}", e);
                     continue
                 }
             };
 
-            let result = if let Some(acceptor) = acceptor.clone() {
-                // TLS connection
-                let stream = match acceptor.accept(stream).await {
-                    Ok(s) => s,
-                    Err(e) => {
-                        error!("[IRC SERVER] Failed accepting TLS connection: {}", e);
+            match &self.acceptor {
+                // Expecting encrypted TLS connection
+                Some(acceptor) => {
+                    let stream = match acceptor.accept(stream).await {
+                        Ok(s) => s,
+                        Err(e) => {
+                            error!("[IRC SERVER] Failed accepting new TLS connection: {}", e);
+                            continue
+                        }
+                    };
+
+                    // Subscribe to incoming events and set up the connection.
+                    let incoming = self.darkirc.event_graph.event_sub.clone().subscribe().await;
+                    if let Err(e) = self
+                        .clone()
+                        .process_connection(stream, peer_addr, incoming, ex.clone())
+                        .await
+                    {
+                        error!("[IRC SERVER] Failed processing new connection: {}", e);
                         continue
-                    }
-                };
-                self.process_connection(stream, peer_addr, notifier.clone(), executor.clone()).await
-            } else {
-                // TCP connection
-                self.process_connection(stream, peer_addr, notifier.clone(), executor.clone()).await
-            };
+                    };
+                }
 
-            if let Err(e) = result {
-                error!("[IRC SERVER] Failed processing connection {}: {}", peer_addr, e);
-                continue
-            };
+                // Expecting plain TCP connection
+                None => {
+                    // Subscribe to incoming events and set up the connection.
+                    let incoming = self.darkirc.event_graph.event_sub.clone().subscribe().await;
+                    if let Err(e) = self
+                        .clone()
+                        .process_connection(stream, peer_addr, incoming, ex.clone())
+                        .await
+                    {
+                        error!("[IRC SERVER] Failed processing new connection: {}", e);
+                        continue
+                    };
+                }
+            }
 
-            info!("[IRC SERVER] Accept new connection: {}", peer_addr);
+            info!("[IRC SERVER] Accepted new client connection at: {}", peer_addr);
         }
     }
 
-    /// On every new connection create new IrcClient
+    /// IRC client connection process.
+    /// Sets up multiplexing between the server and client.
+    /// Detaches the connection as a `StoppableTask`.
     async fn process_connection<C: AsyncRead + AsyncWrite + Send + Unpin + 'static>(
-        &self,
+        self: Arc<Self>,
         stream: C,
         peer_addr: SocketAddr,
-        notifier: smol::channel::Sender<(NotifierMsg, usize)>,
-        executor: Arc<smol::Executor<'_>>,
+        incoming: Subscription<Event>,
+        ex: Arc<Executor<'_>>,
     ) -> Result<()> {
-        let (reader, writer) = io::split(stream);
-        let reader = BufReader::new(reader);
-
-        // Subscription for the new client
-        let client_subscription = self.clients_subscriptions.clone().subscribe().await;
-
-        // new irc configuration
-        let irc_config = IrcConfig::new(&self.settings)?;
-
-        // New irc client
-        let mut client = IrcClient::new(
-            writer,
-            reader,
-            peer_addr,
-            irc_config,
-            notifier,
-            client_subscription,
-            self.missed_events.clone(),
-        );
+        let port = peer_addr.port();
+        let client = Client::new(self.clone(), incoming, peer_addr).await?;
 
-        // Start listening and detach
-        StoppableTask::new().start(
-            // Weird hack to prevent lifetimes hell
-            async move {client.listen().await; Ok(())},
-            |res| async {
-                match res {
-                    Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
-                    Err(e) => error!(target: "darkirc::irc::server::process_connection", "Failed starting client listen: {}", e),
-                }
+        let conn_task = StoppableTask::new();
+        self.clients.lock().await.insert(port, conn_task.clone());
+
+        conn_task.clone().start(
+            async move { client.multiplex_connection(stream).await },
+            move |_| async move {
+                info!("[IRC SERVER] Disconnected client from {}", peer_addr);
+                self.clone().clients.lock().await.remove(&port);
             },
-            Error::DetachedTaskStopped,
-            executor,
+            Error::ChannelStopped,
+            ex,
         );
 
         Ok(())
     }
 
-    /// Setup a listener for irc server
-    async fn setup_listener(&self) -> Result<(TcpListener, Option<TlsAcceptor>)> {
-        let listenaddr = self.settings.irc_listen.socket_addrs(|| None)?[0];
-        let listener = TcpListener::bind(listenaddr).await?;
+    /// Try encrypting a given `Privmsg` if there is such a channel/contact.
+    pub async fn try_encrypt(&self, privmsg: &mut Privmsg) {
+        if let Some((name, channel)) = self.channels.lock().await.get_key_value(&privmsg.channel) {
+            if let Some(saltbox) = &channel.saltbox {
+                privmsg.channel = saltbox::encrypt(saltbox, privmsg.channel.as_bytes());
+                privmsg.nick = saltbox::encrypt(saltbox, privmsg.nick.as_bytes());
+                privmsg.msg = saltbox::encrypt(saltbox, privmsg.msg.as_bytes());
+                debug!("Successfully encrypted message for {}", name);
+                return
+            }
+        };
 
-        let acceptor = match self.settings.irc_listen.scheme() {
-            "tcp+tls" => {
-                // openssl genpkey -algorithm ED25519 > example.com.key
-                // openssl req -new -out example.com.csr -key example.com.key
-                // openssl x509 -req -days 700 -in example.com.csr -signkey example.com.key -out example.com.crt
+        if let Some((name, contact)) = self.contacts.lock().await.get_key_value(&privmsg.channel) {
+            if let Some(saltbox) = &contact.saltbox {
+                privmsg.channel = saltbox::encrypt(saltbox, privmsg.channel.as_bytes());
+                privmsg.nick = saltbox::encrypt(saltbox, privmsg.nick.as_bytes());
+                privmsg.msg = saltbox::encrypt(saltbox, privmsg.msg.as_bytes());
+                debug!("Successfully encrypted message for {}", name);
+            }
+        };
+    }
 
-                if self.settings.irc_tls_secret.is_none() || self.settings.irc_tls_cert.is_none() {
-                    error!("[IRC SERVER] To listen using TLS, please set irc_tls_secret and irc_tls_cert in your config file.");
-                    return Err(Error::KeypairPathNotFound)
-                }
+    /// Try decrypting a given potentially encrypted `Privmsg` object.
+    pub async fn try_decrypt(&self, privmsg: &mut Privmsg) {
+        // If all fields have base58, then we can consider decrypting.
+        let channel_ciphertext = match bs58::decode(&privmsg.channel).into_vec() {
+            Ok(v) => v,
+            Err(_) => return,
+        };
 
-                let file =
-                    File::open(expand_path(self.settings.irc_tls_secret.as_ref().unwrap())?)?;
-                let mut reader = std::io::BufReader::new(file);
-                let secret = &rustls_pemfile::pkcs8_private_keys(&mut reader)?[0];
-                let secret = rustls::PrivateKey(secret.clone());
+        let nick_ciphertext = match bs58::decode(&privmsg.nick).into_vec() {
+            Ok(v) => v,
+            Err(_) => return,
+        };
 
-                let file = File::open(expand_path(self.settings.irc_tls_cert.as_ref().unwrap())?)?;
-                let mut reader = std::io::BufReader::new(file);
-                let certificate = &rustls_pemfile::certs(&mut reader)?[0];
-                let certificate = rustls::Certificate(certificate.clone());
+        let msg_ciphertext = match bs58::decode(&privmsg.msg).into_vec() {
+            Ok(v) => v,
+            Err(_) => return,
+        };
 
-                let config = rustls::ServerConfig::builder()
-                    .with_safe_defaults()
-                    .with_no_client_auth()
-                    .with_single_cert(vec![certificate], secret)?;
+        // Now go through all 3 ciphertexts. We'll use intermediate buffers
+        // for decryption, and only if all passes, we will return a modified
+        // (i.e. decrypted) privmsg, otherwise we return the original.
+        for (name, channel) in self.channels.lock().await.iter() {
+            if let Some(saltbox) = &channel.saltbox {
+                let Some(channel_dec) = saltbox::try_decrypt(saltbox, &channel_ciphertext) else {
+                    continue
+                };
 
-                let acceptor = TlsAcceptor::from(Arc::new(config));
-                Some(acceptor)
+                let Some(nick_dec) = saltbox::try_decrypt(saltbox, &nick_ciphertext) else {
+                    continue
+                };
+
+                let Some(msg_dec) = saltbox::try_decrypt(saltbox, &msg_ciphertext) else {
+                    continue
+                };
+
+                privmsg.channel = channel_dec;
+                privmsg.nick = nick_dec;
+                privmsg.msg = msg_dec;
+                debug!("Successfuly decrypted message for {}", name);
+                return
             }
-            _ => None,
-        };
-        Ok((listener, acceptor))
+        }
+
+        for (name, contact) in self.contacts.lock().await.iter() {
+            if let Some(saltbox) = &contact.saltbox {
+                let Some(channel_dec) = saltbox::try_decrypt(saltbox, &channel_ciphertext) else {
+                    continue
+                };
+
+                let Some(nick_dec) = saltbox::try_decrypt(saltbox, &nick_ciphertext) else {
+                    continue
+                };
+
+                let Some(msg_dec) = saltbox::try_decrypt(saltbox, &msg_ciphertext) else {
+                    continue
+                };
+
+                privmsg.channel = channel_dec;
+                privmsg.nick = nick_dec;
+                privmsg.msg = msg_dec;
+                debug!("Successfully decrypted message from {}", name);
+                return
+            }
+        }
     }
 }

+ 0 - 97
bin/darkirc/src/irc/server/nickserv.rs

@@ -1,97 +0,0 @@
-/* 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 <https://www.gnu.org/licenses/>.
- */
-
-use std::collections::BTreeMap;
-
-use crate::PrivMsgEvent;
-
-#[derive(Debug, Clone, Default)]
-pub struct NickServ {
-    _db: BTreeMap<Vec<u8>, Vec<u8>>,
-}
-
-impl NickServ {
-    fn usage() -> Vec<String> {
-        let r = vec![
-            "***** nickserv help *****",
-            "",
-            "nickserv allows clients to 'register' an account. An account",
-            "registration is necessary to be able to join and send messages",
-            "to the p2p network.",
-            "",
-            "The following commands are available:",
-            "",
-            "    CREATE      Create a new account",
-            "    LIST        List available accounts",
-            "    REGISTER    Register a new account",
-            "    IDENTIFY    Identify and pick an account to use",
-            "",
-            "***** end of help *****",
-        ];
-
-        r.iter().map(|x| x.to_string()).collect()
-    }
-
-    // Here because we might consider returning the actual full protocol PRIVMSG
-    // in a vec. So we can use the result of this and feed it directly to the
-    // client as messages. Dunno if necessary, just a thought.
-    fn reply(msg: String) -> Vec<String> {
-        vec![msg]
-    }
-
-    /// Parse an incoming nickserv message
-    pub fn act(&mut self, ev: PrivMsgEvent) -> Result<Vec<String>, Vec<String>> {
-        assert_eq!(ev.target.to_lowercase().as_str(), super::NICK_NICKSERV);
-
-        let parts: Vec<String> = ev.msg.split(' ').map(|x| x.to_string()).collect();
-
-        match parts[0].to_uppercase().as_str() {
-            "CREATE" => self.create(),
-
-            "LIST" => self.list(),
-
-            "REGISTER" => self.register(),
-
-            "IDENTIFY" => self.identify(),
-
-            "HELP" => Ok(Self::usage()),
-
-            c => Err(vec![format!("Invalid command {}", c), "Type HELP to get help".to_string()]),
-        }
-    }
-
-    /// Create a new account
-    fn create(&mut self) -> Result<Vec<String>, Vec<String>> {
-        Ok(Self::reply("Account created successfully.".to_string()))
-    }
-
-    /// List available accounts
-    fn list(&self) -> Result<Vec<String>, Vec<String>> {
-        Ok(Self::reply("Accounts: ...".to_string()))
-    }
-
-    /// Register a created but unregistered account
-    fn register(&mut self) -> Result<Vec<String>, Vec<String>> {
-        Ok(Self::reply("Account created successfully.".to_string()))
-    }
-
-    /// Pick an account to use
-    fn identify(&mut self) -> Result<Vec<String>, Vec<String>> {
-        Ok(Self::reply("Using account 0.".to_string()))
-    }
-}

+ 192 - 234
bin/darkirc/src/main.rs

@@ -15,205 +15,177 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-
-use std::{
-    collections::{HashMap, HashSet},
-    sync::Arc,
-};
-
-use chrono::{Duration, Utc};
-use irc::ClientSubMsg;
-use log::{debug, error, info};
-use rand::rngs::OsRng;
-use smol::{fs::create_dir_all, lock::Mutex, stream::StreamExt};
-use structopt_toml::StructOptToml;
-use tinyjson::JsonValue;
+use std::{collections::HashSet, sync::Arc};
 
 use darkfi::{
-    async_daemonize,
-    event_graph::{
-        events_queue::EventsQueue,
-        model::{Model, ModelPtr},
-        protocol_event::{ProtocolEvent, Seen},
-        view::View,
-    },
-    net,
+    async_daemonize, cli_desc,
+    event_graph2::{proto::ProtocolEventGraph, EventGraph, EventGraphPtr},
+    net::{settings::SettingsOpt, P2p, P2pPtr, SESSION_ALL},
     rpc::{
         jsonrpc::JsonSubscriber,
         server::{listen_and_serve, RequestHandler},
     },
-    system::{sleep, StoppableTask, Subscriber, SubscriberPtr},
-    util::{file::save_json_file, path::expand_path, time::Timestamp},
+    system::{StoppableTask, StoppableTaskPtr},
+    util::path::{expand_path, get_config_path},
     Error, Result,
 };
+use log::{debug, error, info};
+use rand::rngs::OsRng;
+use smol::{fs, lock::Mutex, stream::StreamExt, Executor};
+use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
+use url::Url;
 
-pub mod crypto;
-pub mod irc;
-pub mod privmsg;
-pub mod rpc;
-pub mod settings;
-
-use crate::{
-    crypto::saltbox::KeyPair,
-    irc::{IrcConfig, IrcServer},
-    privmsg::PrivMsgEvent,
-    rpc::JsonRpcInterface,
-    settings::{Args, ChannelInfo, CONFIG_FILE, CONFIG_FILE_CONTENTS},
-};
+const CONFIG_FILE: &str = "darkirc_config.toml";
+const CONFIG_FILE_CONTENTS: &str = include_str!("../darkirc_config.toml");
 
-async fn parse_signals(
-    sighup_sub: SubscriberPtr<Args>,
-    client_sub: SubscriberPtr<ClientSubMsg>,
-) -> Result<()> {
-    debug!("Started signal parsing handler");
-    let subscription = sighup_sub.subscribe().await;
-    loop {
-        let args = subscription.receive().await;
-        let new_config = IrcConfig::new(&args)?;
-        client_sub.notify(ClientSubMsg::Config(new_config)).await;
-    }
-}
+/// IRC server and client handler implementation
+mod irc;
+use irc::server::IrcServer;
 
-// Removes events older than one week ,then sleeps untill next midnight
-async fn remove_old_events(model: ModelPtr<PrivMsgEvent>) -> Result<()> {
-    loop {
-        let now = Utc::now();
+/// Cryptography utilities
+mod crypto;
 
-        // clocks are valid, safe to unwrap
-        let next_midnight = (now + Duration::days(1)).date_naive().and_hms_opt(0, 0, 0).unwrap();
+/// JSON-RPC methods
+mod rpc;
 
-        let duration = next_midnight.signed_duration_since(now.naive_utc()).to_std().unwrap();
+/// Settings utilities
+mod settings;
 
-        let week_old_datetime =
-            (now - Duration::weeks(1)).date_naive().and_hms_opt(0, 0, 0).unwrap();
-        let timestamp = week_old_datetime.timestamp() as u64;
+#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
+#[serde(default)]
+#[structopt(name = "darkirc", about = cli_desc!())]
+struct Args {
+    #[structopt(short, parse(from_occurrences))]
+    /// Increase verbosity (-vvv supported)
+    verbose: u8,
 
-        model.lock().await.remove_old_events(Timestamp(timestamp))?;
-        info!("Removing old events");
+    #[structopt(short, long)]
+    /// Configuration file to use
+    config: Option<String>,
 
-        sleep(duration.as_secs() + 1).await;
-    }
+    #[structopt(long)]
+    /// Set log file output
+    log: Option<String>,
+
+    #[structopt(long, default_value = "tcp://127.0.0.1:26660")]
+    /// RPC server listen address
+    rpc_listen: Url,
+
+    #[structopt(long, default_value = "tcp://127.0.0.1:6667")]
+    /// IRC server listen address
+    irc_listen: Url,
+
+    /// Optional TLS certificate file path if `irc_listen` uses TLS
+    irc_tls_cert: Option<String>,
+
+    /// Optional TLS certificate key file path if `irc_listen` uses TLS
+    irc_tls_secret: Option<String>,
+
+    #[structopt(short, long, default_value = "~/.local/darkfi/darkirc_db")]
+    /// Datastore (DB) path
+    datastore: String,
+
+    /// Generate a new NaCl keypair and exit
+    #[structopt(long)]
+    gen_chacha_keypair: bool,
+
+    /// Generate a new encrypted channel NaCl secret and exit
+    #[structopt(long)]
+    gen_channel_secret: bool,
+
+    /// Recover NaCl public key from a secret key
+    #[structopt(long)]
+    get_chacha_pubkey: Option<String>,
+
+    /// P2P network settings
+    #[structopt(flatten)]
+    net: SettingsOpt,
 }
 
-async_daemonize!(realmain);
-async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Result<()> {
-    let datastore_path = expand_path(&settings.datastore)?;
-
-    // mkdir datastore_path if not exists
-    create_dir_all(datastore_path.clone()).await?;
-
-    // Signal handling for config reload and graceful termination.
-    let (signals_handler, signals_task) = SignalHandler::new(executor.clone())?;
-    let client_sub = Subscriber::new();
-    executor.spawn(parse_signals(signals_handler.sighup_sub.clone(), client_sub.clone())).detach();
-
-    ////////////////////
-    // Generate new keypair and exit
-    ////////////////////
-    if settings.gen_keypair {
-        let secret_key = crypto_box::SecretKey::generate(&mut OsRng);
-        let public_key = secret_key.public_key();
-        let secret = bs58::encode(secret_key.to_bytes()).into_string();
-        let public = bs58::encode(public_key.as_bytes()).into_string();
-
-        let kp = KeyPair { secret, public };
-
-        if settings.output.is_some() {
-            let datastore = expand_path(&settings.output.unwrap())?;
-            let kp_enc = JsonValue::Object(HashMap::from([
-                ("public".to_string(), JsonValue::String(kp.public)),
-                ("secret".to_string(), JsonValue::String(kp.secret)),
-            ]));
-            save_json_file(&datastore, &kp_enc, false)?;
-        } else {
-            println!("Generated keypair:\n{}", kp);
-        }
+pub struct DarkIrc {
+    /// P2P network pointer
+    p2p: P2pPtr,
+    /// Event Graph instance
+    event_graph: EventGraphPtr,
+    /// JSON-RPC connection tracker
+    rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
+    /// dnet JSON-RPC subscriber
+    dnet_sub: JsonSubscriber,
+}
 
-        return Ok(())
+impl DarkIrc {
+    fn new(p2p: P2pPtr, event_graph: EventGraphPtr, dnet_sub: JsonSubscriber) -> Self {
+        Self { p2p, event_graph, rpc_connections: Mutex::new(HashSet::new()), dnet_sub }
     }
+}
 
-    if settings.secret.is_some() {
-        let secret = settings.secret.clone().unwrap();
-        let bytes: [u8; 32] = bs58::decode(secret).into_vec()?.try_into().unwrap();
-        let secret = crypto_box::SecretKey::from(bytes);
-        let pubkey = secret.public_key();
-        let pub_encoded = bs58::encode(pubkey.as_bytes()).into_string();
-
-        if settings.output.is_some() {
-            let datastore = expand_path(&settings.output.unwrap())?;
-            save_json_file(&datastore, &JsonValue::String(pub_encoded), false)?;
-        } else {
-            println!("Public key recovered: {}", pub_encoded);
-        }
-
+async_daemonize!(realmain);
+async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
+    if args.gen_chacha_keypair {
+        let secret = crypto_box::SecretKey::generate(&mut OsRng);
+        let public = secret.public_key();
+        let secret = bs58::encode(secret.to_bytes()).into_string();
+        let public = bs58::encode(public.to_bytes()).into_string();
+        println!("Place this in your config file:\n");
+        println!("[crypto]");
+        println!("#dm_chacha_public = \"{}\"", public);
+        println!("dm_chacha_secret = \"{}\"", secret);
         return Ok(())
     }
 
-    if settings.gen_secret {
-        let secret_key = crypto_box::SecretKey::generate(&mut OsRng);
-        let encoded = bs58::encode(secret_key.to_bytes());
-        println!("{}", encoded.into_string());
+    if args.gen_channel_secret {
+        let secret = crypto_box::SecretKey::generate(&mut OsRng);
+        let secret = bs58::encode(secret.to_bytes()).into_string();
+        println!("Place this in your config file:\n");
+        println!("[channel.\"#yourchannelname\"]");
+        println!("secret = \"{}\"", secret);
         return Ok(())
     }
 
-    ////////////////////
-    // Initialize the base structures
-    ////////////////////
-    let events_queue = EventsQueue::<PrivMsgEvent>::new();
-    let model = Arc::new(Mutex::new(Model::new(events_queue.clone())));
-    let view = Arc::new(Mutex::new(View::new(events_queue.clone())));
-    let model_clone = model.clone();
-    let model_clone2 = model.clone();
-
-    {
-        // Temporarly load model and check if the loaded head is not
-        // older than one week (already removed from other node's tree)
-        let now = Utc::now();
-
-        let now_datetime = (now - Duration::weeks(1)).date_naive().and_hms_opt(0, 0, 0).unwrap();
-        let timestamp = Timestamp(now_datetime.timestamp() as u64);
-
-        let mut loaded_model = Model::new(events_queue.clone());
-        loaded_model.load_tree(&datastore_path)?;
-
-        if loaded_model
-            .get_event(&loaded_model.get_head_hash()?)
-            .is_some_and(|event| event.timestamp >= timestamp)
-        {
-            model.lock().await.load_tree(&datastore_path)?;
+    if let Some(chacha_secret) = args.get_chacha_pubkey {
+        let bytes = match bs58::decode(chacha_secret).into_vec() {
+            Ok(v) => v,
+            Err(e) => {
+                println!("Error: {}", e);
+                return Err(Error::ParseFailed("Secret key parsing failed"))
+            }
+        };
+
+        if bytes.len() != 32 {
+            return Err(Error::ParseFailed("Decoded base58 is not 32 bytes long"))
         }
+
+        let secret: [u8; 32] = bytes.try_into().unwrap();
+        let secret = crypto_box::SecretKey::from(secret);
+        println!("{}", bs58::encode(secret.public_key().to_bytes()).into_string());
+        return Ok(())
     }
 
-    ////////////////////
-    // P2p setup
-    ////////////////////
-    // Buffers
-    let seen_event = Seen::new();
-    let seen_inv = Seen::new();
+    info!("Initializing DarkIRC node");
 
-    // Check the version
-    let net_settings = settings.net.clone();
+    // Create datastore path if not there already.
+    let datastore = expand_path(&args.datastore)?;
+    fs::create_dir_all(&datastore).await?;
 
-    // New p2p
-    let p2p = net::P2p::new(net_settings.into(), executor.clone()).await;
+    info!("Instantiating event DAG");
+    let sled_db = sled::open(datastore)?;
+    let p2p = P2p::new(args.net.into(), ex.clone()).await;
+    let event_graph =
+        EventGraph::new(p2p.clone(), sled_db.clone(), "darkirc_dag", 1, ex.clone()).await?;
 
-    // Register the protocol_event
+    info!("Registering EventGraph P2P protocol");
+    let event_graph_ = Arc::clone(&event_graph);
     let registry = p2p.protocol_registry();
     registry
-        .register(net::SESSION_ALL, move |channel, p2p| {
-            let seen_event = seen_event.clone();
-            let seen_inv = seen_inv.clone();
-            let model = model.clone();
-            async move { ProtocolEvent::init(channel, p2p, model, seen_event, seen_inv).await }
+        .register(SESSION_ALL, move |channel, _| {
+            let event_graph_ = event_graph_.clone();
+            async move { ProtocolEventGraph::init(event_graph_, channel).await.unwrap() }
         })
         .await;
 
-    // ==============
-    // p2p dnet setup
-    // ==============
-    info!(target: "darkirc", "Starting dnet subs task");
-    let json_sub = JsonSubscriber::new("dnet.subscribe_events");
-    let json_sub_ = json_sub.clone();
+    info!("Starting dnet subs task");
+    let dnet_sub = JsonSubscriber::new("dnet.subscribe_events");
+    let dnet_sub_ = dnet_sub.clone();
     let p2p_ = p2p.clone();
     let dnet_task = StoppableTask::new();
     dnet_task.clone().start(
@@ -222,117 +194,103 @@ async fn realmain(settings: Args, executor: Arc<smol::Executor<'static>>) -> Res
             loop {
                 let event = dnet_sub.receive().await;
                 debug!("Got dnet event: {:?}", event);
-                json_sub_.notify(vec![event.into()]).await;
+                dnet_sub_.notify(vec![event.into()]).await;
             }
         },
         |res| async {
             match res {
                 Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
-                Err(e) => {
-                    error!(target: "darkirc", "Failed starting remove old events task: {}", e)
-                }
+                Err(e) => panic!("{}", e),
             }
         },
         Error::DetachedTaskStopped,
-        executor.clone(),
+        ex.clone(),
     );
 
-    ////////////////////
-    // RPC interface setup
-    ////////////////////
-    let rpc_listen_addr = settings.rpc_listen.clone();
-    info!(target: "darkirc", "Starting JSON-RPC server on {}", rpc_listen_addr);
-    let rpc_interface = Arc::new(JsonRpcInterface {
-        addr: rpc_listen_addr.clone(),
-        p2p: p2p.clone(),
-        dnet_sub: json_sub,
-        rpc_connections: Mutex::new(HashSet::new()),
-    });
-
+    info!("Starting JSON-RPC server");
+    let darkirc = Arc::new(DarkIrc::new(p2p.clone(), event_graph.clone(), dnet_sub));
+    let darkirc_ = Arc::clone(&darkirc);
     let rpc_task = StoppableTask::new();
-    let rpc_interface_ = rpc_interface.clone();
     rpc_task.clone().start(
-        listen_and_serve(rpc_listen_addr, rpc_interface, None, executor.clone()),
+        listen_and_serve(args.rpc_listen, darkirc.clone(), None, ex.clone()),
         |res| async move {
             match res {
-                Ok(()) | Err(Error::RpcServerStopped) => rpc_interface_.stop_connections().await,
-                Err(e) => error!(target: "darkirc", "Failed starting JSON-RPC server: {}", e),
+                Ok(()) | Err(Error::RpcServerStopped) => darkirc_.stop_connections().await,
+                Err(e) => error!("Failed stopping JSON-RPC server: {}", e),
             }
         },
         Error::RpcServerStopped,
-        executor.clone(),
+        ex.clone(),
     );
 
-    ////////////////////
-    // Start P2P network
-    ////////////////////
-    info!(target: "darkirc", "Starting P2P network");
-    p2p.clone().start().await?;
-
-    ////////////////////
-    // IRC server
-    ////////////////////
-    info!(target: "darkirc", "Starting IRC server");
+    info!("Starting IRC server");
+    let config_path = get_config_path(args.config, CONFIG_FILE)?;
     let irc_server = IrcServer::new(
-        settings.clone(),
-        p2p.clone(),
-        model_clone.clone(),
-        view.clone(),
-        client_sub,
+        darkirc.clone(),
+        args.irc_listen,
+        args.irc_tls_cert,
+        args.irc_tls_secret,
+        config_path,
     )
     .await?;
-    let irc_server_task = StoppableTask::new();
-    let executor_ = executor.clone();
-    irc_server_task.clone().start(
-        // Weird hack to prevent lifetimes hell
-        async move { irc_server.start(executor_).await },
-        |res| async {
+
+    let irc_task = StoppableTask::new();
+    let ex_ = ex.clone();
+    irc_task.clone().start(
+        irc_server.clone().listen(ex_),
+        |res| async move {
             match res {
-                Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
-                Err(e) => error!(target: "darkirc", "Failed starting IRC server: {}", e),
+                Ok(()) | Err(Error::DetachedTaskStopped) => { /* TODO: */ }
+                Err(e) => error!("Failed stopping IRC server: {}", e),
             }
         },
         Error::DetachedTaskStopped,
-        executor.clone(),
+        ex.clone(),
     );
 
-    // Reset root task
-    info!(target: "darkirc", "Starting remove old events task");
-    let remove_old_events_task = StoppableTask::new();
-    remove_old_events_task.clone().start(
-        remove_old_events(model_clone2),
-        |res| async {
-            match res {
-                Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
-                Err(e) => {
-                    error!(target: "darkirc", "Failed starting remove old events task: {}", e)
+    info!("Starting P2P network");
+    p2p.clone().start().await?;
+
+    /*
+    // We'll attempt to sync 5 times
+    for i in 1..=6 {
+        info!("Syncing event DAG (attempt #{})", i);
+        match event_graph.dag_sync().await {
+            Ok(()) => break,
+            Err(e) => {
+                if i == 6 {
+                    error!("Failed syncing DAG. Exiting.");
+                    p2p.stop().await;
+                    return Err(Error::DagSyncFailed)
+                } else {
+                    // TODO: Maybe at this point we should prune or something?
+                    // TODO: Or maybe just tell the user to delete the DAG from FS.
+                    error!("Failed syncing DAG ({}), retrying in 10s...", e);
+                    sleep(10).await;
                 }
             }
-        },
-        Error::DetachedTaskStopped,
-        executor,
-    );
+        }
+    }
+    */
 
-    // Wait for termination signal
+    // Signal handling for graceful termination.
+    let (signals_handler, signals_task) = SignalHandler::new(ex)?;
     signals_handler.wait_termination(signals_task).await?;
     info!("Caught termination signal, cleaning up and exiting...");
 
-    model_clone.lock().await.save_tree(&datastore_path)?;
-
-    info!(target: "darkirc", "Stopping dnet subs task...");
-    dnet_task.stop().await;
+    info!("Stopping P2P network");
+    p2p.stop().await;
 
-    info!(target: "darkirc", "Stopping JSON-RPC server...");
+    info!("Stopping JSON-RPC server");
     rpc_task.stop().await;
+    dnet_task.stop().await;
 
-    info!(target: "darkirc", "Stopping P2P network");
-    p2p.stop().await;
-
-    info!(target: "darkirc", "Stopping IRC server...");
-    irc_server_task.stop().await;
+    info!("Stopping IRC server");
+    irc_task.stop().await;
 
-    info!(target: "darkirc", "Stopping remove old events task...");
-    remove_old_events_task.stop().await;
+    info!("Flushing sled");
+    sled_db.flush_async().await?;
 
+    info!("Shut down successfully");
     Ok(())
 }

+ 0 - 43
bin/darkirc/src/privmsg.rs

@@ -1,43 +0,0 @@
-/* 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 <https://www.gnu.org/licenses/>.
- */
-
-use darkfi::event_graph::EventMsg;
-use darkfi_serial::{async_trait, SerialDecodable, SerialEncodable};
-
-#[derive(SerialEncodable, SerialDecodable, Clone, Debug)]
-pub struct PrivMsgEvent {
-    pub nick: String,
-    pub msg: String,
-    pub target: String,
-}
-
-impl std::string::ToString for PrivMsgEvent {
-    fn to_string(&self) -> String {
-        format!(":{}!anon@dark.fi PRIVMSG {} :{}\r\n", self.nick, self.target, self.msg)
-    }
-}
-
-impl EventMsg for PrivMsgEvent {
-    fn new() -> Self {
-        Self {
-            nick: "root".to_string(),
-            msg: "Let there be dark".to_string(),
-            target: "root".to_string(),
-        }
-    }
-}

+ 11 - 20
bin/darkirc/src/rpc.rs

@@ -15,35 +15,26 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
-
 use std::collections::HashSet;
 
 use async_trait::async_trait;
-use log::debug;
-use smol::lock::{Mutex, MutexGuard};
-use tinyjson::JsonValue;
-use url::Url;
-
 use darkfi::{
-    net,
+    net::P2pPtr,
     rpc::{
-        jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult, JsonSubscriber},
+        jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
         p2p_method::HandlerP2p,
         server::RequestHandler,
+        util::JsonValue,
     },
     system::StoppableTaskPtr,
 };
+use log::debug;
+use smol::lock::MutexGuard;
 
-pub struct JsonRpcInterface {
-    pub addr: Url,
-    pub p2p: net::P2pPtr,
-    pub dnet_sub: JsonSubscriber,
-    /// JSON-RPC connection tracker
-    pub rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
-}
+use super::DarkIrc;
 
 #[async_trait]
-impl RequestHandler for JsonRpcInterface {
+impl RequestHandler for DarkIrc {
     async fn handle_request(&self, req: JsonRequest) -> JsonResult {
         debug!(target: "darkirc::rpc", "--> {}", req.stringify().unwrap());
 
@@ -51,7 +42,7 @@ impl RequestHandler for JsonRpcInterface {
             "ping" => self.pong(req.id, req.params).await,
             "dnet.switch" => self.dnet_switch(req.id, req.params).await,
             "dnet.subscribe_events" => self.dnet_subscribe_events(req.id, req.params).await,
-            // TODO: make this optional
+            // TODO: Make this optional
             "p2p.get_info" => self.p2p_get_info(req.id, req.params).await,
             _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
         }
@@ -62,7 +53,7 @@ impl RequestHandler for JsonRpcInterface {
     }
 }
 
-impl JsonRpcInterface {
+impl DarkIrc {
     // RPCAPI:
     // Activate or deactivate dnet in the P2P stack.
     // By sending `true`, dnet will be activated, and by sending `false` dnet
@@ -104,8 +95,8 @@ impl JsonRpcInterface {
     }
 }
 
-impl HandlerP2p for JsonRpcInterface {
-    fn p2p(&self) -> net::P2pPtr {
+impl HandlerP2p for DarkIrc {
+    fn p2p(&self) -> P2pPtr {
         self.p2p.clone()
     }
 }

+ 110 - 349
bin/darkirc/src/settings.rs

@@ -21,416 +21,177 @@ use std::{
     sync::Arc,
 };
 
-use crypto_box::ChaChaBox;
-use log::{info, warn};
-use serde::{self, Deserialize};
-use structopt::StructOpt;
-use structopt_toml::StructOptToml;
-use toml::Value;
-use url::Url;
-
-use darkfi::{net::settings::SettingsOpt, Result};
-
-// Location for config file
-pub const CONFIG_FILE: &str = "darkirc_config.toml";
-pub const CONFIG_FILE_CONTENTS: &str = include_str!("../darkirc_config.toml");
-
-// Msg config
-pub const MAXIMUM_LENGTH_OF_MESSAGE: usize = 1024;
-pub const MAXIMUM_LENGTH_OF_NICK_CHAN_CNT: usize = 32;
-
-// IRC Client
-pub enum RPL {
-    NoTopic = 331,
-    Topic = 332,
-    NameReply = 353,
-    EndOfNames = 366,
-}
-
-/// darkirc cli
-#[derive(Clone, Deserialize, StructOpt, StructOptToml)]
-#[serde(default)]
-#[structopt(name = "darkirc")]
-pub struct Args {
-    /// Sets a custom config file
-    #[structopt(long)]
-    pub config: Option<String>,
-
-    /// Sets Datastore Path
-    #[structopt(long, default_value = "~/.local/darkfi/darkirc")]
-    pub datastore: String,
-
-    /// JSON-RPC listen URL
-    #[structopt(long = "rpc", default_value = "tcp://127.0.0.1:26660")]
-    pub rpc_listen: Url,
-
-    /// IRC listen URL
-    #[structopt(long = "irc", default_value = "tcp://127.0.0.1:6667")]
-    pub irc_listen: Url,
-
-    /// Optional TLS certificate file path if `irc_listen` uses TLS
-    pub irc_tls_cert: Option<String>,
-
-    /// Optional TLS certificate key file path if `irc_listen` uses TLS
-    pub irc_tls_secret: Option<String>,
-
-    /// Generate a new NaCl keypair and exit
-    #[structopt(long)]
-    pub gen_keypair: bool,
-
-    /// Generate a new NaCl secret for an encrypted channel and exit
-    #[structopt(long)]
-    pub gen_secret: bool,
-
-    /// Recover public key from secret key
-    #[structopt(long = "recover_pubkey")]
-    pub secret: Option<String>,
-
-    /// Path to save keypair in
-    #[structopt(short)]
-    pub output: Option<String>,
-
-    /// Autojoin channels
-    #[structopt(long)]
-    pub autojoin: Vec<String>,
-
-    /// Password
-    #[structopt(long)]
-    pub password: Option<String>,
-
-    /// Network settings
-    #[structopt(flatten)]
-    pub net: SettingsOpt,
+use darkfi::{Error::ParseFailed, Result};
+use log::info;
 
-    #[structopt(short, long)]
-    /// Set log file to ouput into
-    pub log: Option<String>,
+use crate::irc::{IrcChannel, IrcContact};
 
-    /// Increase verbosity
-    #[structopt(short, parse(from_occurrences))]
-    pub verbose: u8,
-}
-
-/// This struct holds information about preconfigured contacts.
-/// In the TOML configuration file, we can configure contacts as such:
+/// Parse configured autojoin channels from a TOML map.
 ///
 /// ```toml
-/// [contact."nick"]
-/// pubkey = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
+/// autojoin = ["#dev", "#memes"]
 /// ```
-#[derive(Clone)]
-pub struct ContactInfo {
-    /// Optional NaCl box for the channel, used for {en,de}cryption.
-    pub salt_box: Option<Arc<ChaChaBox>>,
-}
+pub fn parse_autojoin_channels(data: &toml::Value) -> Result<Vec<String>> {
+    let mut ret = vec![];
 
-impl ContactInfo {
-    pub fn new() -> Result<Self> {
-        Ok(Self { salt_box: None })
-    }
-}
-
-/// Defined user modes
-#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
-pub enum UserMode {
-    None,
-    Op,
-    Voice,
-    HalfOp,
-    Admin,
-    Owner,
-}
-
-impl std::fmt::Display for UserMode {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
-        match self {
-            Self::None => write!(f, ""),
-            Self::Op => write!(f, "@"),
-            Self::Voice => write!(f, "+"),
-            Self::HalfOp => write!(f, "%"),
-            Self::Admin => write!(f, "&"),
-            Self::Owner => write!(f, "~"),
-        }
-    }
-}
-
-/// This struct holds info about a specific nickname within a channel.
-/// We usually use it to implement modes.
-#[derive(Debug, Clone, Eq)]
-pub struct Nick {
-    name: String,
-    mode: UserMode,
-}
+    let Some(autojoin) = data.get("autojoin") else { return Ok(ret) };
+    let Some(autojoin) = autojoin.as_array() else {
+        return Err(ParseFailed("autojoin not an array"))
+    };
 
-impl Nick {
-    pub fn new(name: String) -> Self {
-        Self { name, mode: UserMode::None }
-    }
+    for item in autojoin {
+        let Some(channel) = item.as_str() else {
+            return Err(ParseFailed("autojoin channel not a string"))
+        };
 
-    pub fn set_mode(&mut self, mode: UserMode) -> Option<String> {
-        if self.mode == mode {
-            return None
+        if !channel.starts_with('#') {
+            return Err(ParseFailed("autojoin channel not a valid channel"))
         }
 
-        self.mode = mode;
-        Some(format!("+{}", mode))
-    }
-
-    pub fn unset_mode(&mut self, mode: UserMode) -> Option<String> {
-        if self.mode != mode {
-            return None
+        if ret.contains(&channel.to_string()) {
+            return Err(ParseFailed("Duplicate autojoin channel found"))
         }
 
-        self.mode = mode;
-        Some(format!("-{}", mode))
+        ret.push(channel.to_string());
     }
-}
 
-impl PartialEq for Nick {
-    fn eq(&self, other: &Self) -> bool {
-        self.name == other.name
-    }
+    Ok(ret)
 }
 
-impl From<String> for Nick {
-    fn from(name: String) -> Self {
-        Self { name, mode: UserMode::None }
-    }
-}
+/// Parse a DM secret key from a TOML map.
+///
+/// ```toml
+/// [crypto]
+/// dm_chacha_secret = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
+/// ```
+fn parse_dm_chacha_secret(data: &toml::Value) -> Result<Option<crypto_box::SecretKey>> {
+    let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) };
+    let Some(crypto) = table.get("crypto") else { return Ok(None) };
+    let Some(crypto) = crypto.as_table() else { return Err(ParseFailed("`crypto` not a map")) };
 
-impl std::hash::Hash for Nick {
-    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
-        state.write(&self.name.clone().into_bytes());
+    if !crypto.contains_key("dm_chacha_secret") {
+        return Ok(None)
     }
-}
 
-impl std::fmt::Display for Nick {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
-        write!(f, "{}{}", self.mode, self.name)
-    }
-}
+    let Some(secret_str) = crypto["dm_chacha_secret"].as_str() else {
+        return Err(ParseFailed("dm_chacha_secret not a string"))
+    };
 
-/// This struct holds information about preconfigured channels.
-/// In the TOML configuration file, we can configure channels as such:
-/// ```toml
-/// [channel."#dev"]
-/// secret = "GvH4kno3kUu6dqPrZ8zjMhqxTUDZ2ev16EdprZiZJgj1"
-/// topic = "DarkFi Development Channel"
-/// ```
-/// Having a secret will enable a NaCl box that is able to encrypt and
-/// decrypt messages in this channel using this set shared secret.
-/// The secret should be shared OOB, via a secure channel.
-/// Having a topic set is useful if one wants to have a topic in the
-/// configured channel. It is not shared with others, but it is useful
-/// for personal reference.
-#[derive(Default, Clone)]
-pub struct ChannelInfo {
-    /// Optional topic for the channel
-    pub topic: Option<String>,
-    /// Optional NaCl box for the channel, used for {en,de}cryption.
-    pub salt_box: Option<Arc<ChaChaBox>>,
-    /// All nicknames which are visible on the channel
-    pub names: HashSet<Nick>,
-}
+    let Ok(secret_bytes) = bs58::decode(secret_str).into_vec() else {
+        return Err(ParseFailed("dm_chacha_secret not valid base58"))
+    };
 
-impl ChannelInfo {
-    pub fn new() -> Result<Self> {
-        Ok(Self { topic: None, salt_box: None, names: HashSet::new() })
+    if secret_bytes.len() != 32 {
+        return Err(ParseFailed("dm_chacha_secret not 32 bytes long"))
     }
 
-    pub fn names(&self) -> String {
-        self.names.iter().map(|n| n.to_string()).collect::<Vec<String>>().join(" ")
-    }
+    let secret_bytes: [u8; 32] = secret_bytes.try_into().unwrap();
+
+    Ok(Some(crypto_box::SecretKey::from(secret_bytes)))
 }
 
-/// Parse a TOML string for any configured channels and return
-/// a map containing said configurations.
+/// Parse configured contacts from a TOML map.
 ///
 /// ```toml
-/// [channel."#memes"]
-/// secret = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
-/// topic = "Dank Memes"
+/// [contact."anon"]
+/// dm_chacha_public = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
 /// ```
-pub fn parse_configured_channels(data: &str) -> Result<HashMap<String, ChannelInfo>> {
+pub fn parse_configured_contacts(data: &toml::Value) -> Result<HashMap<String, IrcContact>> {
     let mut ret = HashMap::new();
 
-    let map = match toml::from_str(data)? {
-        Value::Table(m) => m,
-        _ => return Ok(ret),
+    let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) };
+    let Some(contacts) = table.get("contact") else { return Ok(ret) };
+    let Some(contacts) = contacts.as_table() else {
+        return Err(ParseFailed("`contact` not a map"))
     };
 
-    if !map.contains_key("channel") {
-        return Ok(ret)
-    }
+    let Some(secret) = parse_dm_chacha_secret(data)? else {
+        return Err(ParseFailed("Did not find a valid chacha secret"))
+    };
 
-    if !map["channel"].is_table() {
-        return Ok(ret)
-    }
+    for (name, items) in contacts {
+        let Some(public_str) = items.get("dm_chacha_public") else {
+            return Err(ParseFailed("Invalid contact configuration"))
+        };
 
-    for chan in map["channel"].as_table().unwrap() {
-        if chan.0.len() > MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
-            warn!("Channel name is too long, skipping...");
-            continue
-        }
-        info!("Found configuration for channel {}", chan.0);
-        let mut channel_info = ChannelInfo::new()?;
+        let Some(public_str) = public_str.as_str() else {
+            return Err(ParseFailed("Invalid contact configuration"))
+        };
+
+        let Ok(public_bytes) = bs58::decode(public_str).into_vec() else {
+            return Err(ParseFailed("Invalid base58 for contact pubkey"))
+        };
 
-        if chan.1.as_table().unwrap().contains_key("topic") {
-            let topic = chan.1["topic"].as_str().unwrap().to_string();
-            info!("Found topic for channel {}: {}", chan.0, topic);
-            channel_info.topic = Some(topic);
+        if public_bytes.len() != 32 {
+            return Err(ParseFailed("Invalid contact pubkey (not 32 bytes)"))
         }
 
-        if chan.1.as_table().unwrap().contains_key("secret") {
-            // Build the NaCl box
-            if let Some(s) = chan.1["secret"].as_str() {
-                let salt_box = salt_box_from_shared_secret(s)?;
-                channel_info.salt_box = Some(Arc::new(salt_box));
-                info!("Instantiated NaCl box for channel {}", chan.0);
-            }
+        let public_bytes: [u8; 32] = public_bytes.try_into().unwrap();
+
+        let public = crypto_box::PublicKey::from(public_bytes);
+        let saltbox = Some(Arc::new(crypto_box::ChaChaBox::new(&public, &secret)));
+
+        if ret.contains_key(name) {
+            return Err(ParseFailed("Duplicate contact found"))
         }
 
-        ret.insert(chan.0.to_string(), channel_info);
+        info!("Instantiated ChaChaBox for contact \"{}\"", name);
+        ret.insert(name.to_string(), IrcContact { saltbox });
     }
 
     Ok(ret)
 }
 
-/// Parse a TOML string for any configured contact list and return
+/// Parse a TOML string for any configured channels and return
 /// a map containing said configurations.
 ///
 /// ```toml
-/// [contact."nick"]
-/// public_key = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
+/// [channel."#memes"]
+/// secret = "7CkVuFgwTUpJn5Sv67Q3fyEDpa28yrSeL5Hg2GqQ4jfM"
+/// topic = "Dank Memes"
 /// ```
-pub fn parse_configured_contacts(data: &str) -> Result<HashMap<String, ContactInfo>> {
+pub fn parse_configured_channels(data: &toml::Value) -> Result<HashMap<String, IrcChannel>> {
     let mut ret = HashMap::new();
 
-    let map = match toml::from_str(data) {
-        Ok(Value::Table(m)) => m,
-        _ => {
-            warn!("Invalid TOML string passed as argument to parse_configured_contacts()");
-            return Ok(ret)
-        }
-    };
+    let Some(table) = data.as_table() else { return Err(ParseFailed("TOML not a map")) };
+    let Some(chans) = table.get("channel") else { return Ok(ret) };
+    let Some(chans) = chans.as_table() else { return Err(ParseFailed("`channel` not a map")) };
 
-    if !map.contains_key("contact") {
-        return Ok(ret)
-    }
+    for (name, items) in chans {
+        let mut chan = IrcChannel { topic: String::new(), nicks: HashSet::new(), saltbox: None };
 
-    if !map["contact"].is_table() {
-        warn!("TOML configuration contains a \"contact\" field, but it is not a table.");
-        return Ok(ret)
-    }
-
-    let contacts = map["contact"].as_table().unwrap();
-
-    // Our secret key for NaCl boxes.
-    let found_secret = match parse_secret_key(data) {
-        Ok(v) => v,
-        Err(_) => {
-            info!("Did not find secret key in config, skipping contact configuration.");
-            return Ok(ret)
-        }
-    };
-
-    let bytes: [u8; 32] = match bs58::decode(found_secret).into_vec() {
-        Ok(v) => {
-            if v.len() != 32 {
-                warn!("Decoded base58 secret key string is not 32 bytes");
-                warn!("Skipping private contact configuration");
-                return Ok(ret)
+        if let Some(topic) = items.get("topic") {
+            if let Some(topic) = topic.as_str() {
+                info!("Found configured topic for {}: {}", name, topic);
+                chan.topic = topic.to_string();
+            } else {
+                return Err(ParseFailed("Channel topic not a string"))
             }
-            v.try_into().unwrap()
-        }
-        Err(e) => {
-            warn!("Failed to decode base58 secret key from string: {}", e);
-            warn!("Skipping private contact configuration");
-            return Ok(ret)
         }
-    };
-
-    let secret = crypto_box::SecretKey::from(bytes);
-
-    for cnt in contacts {
-        if cnt.0.len() > MAXIMUM_LENGTH_OF_NICK_CHAN_CNT {
-            warn!("Contact name is too long, skipping...");
-            continue
-        }
-        info!("Found configuration for contact {}", cnt.0);
-        let mut contact_info = ContactInfo::new()?;
 
-        if !cnt.1.is_table() {
-            warn!("Config for contact {} isn't a TOML table", cnt.0);
-            continue
-        }
-
-        let table = cnt.1.as_table().unwrap();
-        if table.is_empty() {
-            warn!("Configuration for contact {} is empty.", cnt.0);
-            continue
-        }
+        if let Some(secret) = items.get("secret") {
+            if let Some(secret) = secret.as_str() {
+                let Ok(secret_bytes) = bs58::decode(secret).into_vec() else {
+                    return Err(ParseFailed("Channel secret not valid base58"))
+                };
 
-        // Build the NaCl box
-        if !table.contains_key("public_key") || !table["public_key"].is_str() {
-            warn!("Contact {} doesn't have `public_key` set or is not a valid string.", cnt.0);
-            continue
-        }
-
-        let pub_str = table["public_key"].as_str().unwrap();
-        let bytes: [u8; 32] = match bs58::decode(pub_str).into_vec() {
-            Ok(v) => {
-                if v.len() != 32 {
-                    warn!("Decoded base58 string is not 32 bytes");
-                    continue
+                if secret_bytes.len() != 32 {
+                    return Err(ParseFailed("Channel secret not 32 bytes long"))
                 }
 
-                v.try_into().unwrap()
-            }
-            Err(e) => {
-                warn!("Failed to decode base58 pubkey from string: {}", e);
-                continue
+                let secret_bytes: [u8; 32] = secret_bytes.try_into().unwrap();
+                let secret = crypto_box::SecretKey::from(secret_bytes);
+                let public = secret.public_key();
+                chan.saltbox = Some(Arc::new(crypto_box::ChaChaBox::new(&public, &secret)));
+                info!("Configured NaCl box for channel {}", name);
+            } else {
+                return Err(ParseFailed("Channel secret not a string"))
             }
-        };
+        }
 
-        let public = crypto_box::PublicKey::from(bytes);
-        contact_info.salt_box = Some(Arc::new(ChaChaBox::new(&public, &secret)));
-        ret.insert(cnt.0.to_string(), contact_info);
-        info!("Instantiated NaCl box for contact \"{}\"", cnt.0);
+        info!("Configured channel {}", name);
+        ret.insert(name.to_string(), chan);
     }
 
     Ok(ret)
 }
-
-fn salt_box_from_shared_secret(s: &str) -> Result<ChaChaBox> {
-    let bytes: [u8; 32] = bs58::decode(s).into_vec()?.try_into().unwrap();
-    let secret = crypto_box::SecretKey::from(bytes);
-    let public = secret.public_key();
-    Ok(ChaChaBox::new(&public, &secret))
-}
-
-fn parse_secret_key(data: &str) -> Result<String> {
-    let mut sk = String::new();
-
-    let map = match toml::from_str(data)? {
-        Value::Table(m) => m,
-        _ => return Ok(sk),
-    };
-
-    if !map.contains_key("secret_key") {
-        return Ok(sk)
-    }
-
-    if !map["secret_key"].is_table() {
-        return Ok(sk)
-    }
-
-    let secret_keys = map["secret_key"].as_table().unwrap();
-
-    for key in secret_keys {
-        sk = key.0.into();
-    }
-
-    info!("Found secret key in config, noted it down.");
-    Ok(sk)
-}