async_circe/
lib.rs

1//! A simple IRC crate written in rust
2//! ```run_fut
3//! use async_circe::{commands::Command, Client, Config};
4//!
5//! #[tokio::main(flavor = "current_thread")]
6//! async fn main() -> Result<(), tokio::io::Error> {
7//!     let config = Config::default();
8//!     let mut client = Client::new(config).await.unwrap();
9//!     client.identify().await.unwrap();
10//!
11//!     loop {
12//!         if let Some(command) = client.read().await? {
13//!             if let Command::PRIVMSG(nick, channel, message) = command {
14//!                 println!("{} in {}: {}", nick, channel, message);
15//!             }
16//!         }
17//!     }
18//! }
19//! ```
20//!
21//! The crate requires `tokio` with the `macros` and `rt` feature enabled to function.<br>
22//! For more examples (connecting to IRCs that dont use SSL, getting the config from
23//! a toml file, debugging or just in general an overview how a project with async-circe
24//! is structured) see the [examples](https://git.karx.xyz/circe/async-circe/src/branch/master/examples) folder on our git.
25
26#![warn(missing_docs)] // We want everything documented
27#![allow(clippy::needless_return)] // Wants to remove a return statement, but when it's removed the code doesn't compile
28#![feature(doc_cfg)]
29
30use tokio::io::BufReader;
31use tokio::io::Error;
32use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
33use tokio::net::TcpStream;
34
35#[cfg(feature = "tls")]
36use native_tls::TlsConnector;
37
38#[cfg(feature = "toml_config")]
39use serde_derive::Deserialize;
40#[cfg(feature = "toml_config")]
41use std::fs::File;
42#[cfg(feature = "toml_config")]
43use std::io::Read;
44#[cfg(any(doc, feature = "toml_config"))]
45use std::path::Path;
46#[cfg(feature = "toml_config")]
47use tokio::io::ErrorKind;
48
49pub mod commands;
50
51/// An IRC client
52pub struct Client {
53    config: Config,
54    #[cfg(not(feature = "tls"))]
55    buf_reader: BufReader<TcpStream>,
56    #[cfg(feature = "tls")]
57    buf_reader: BufReader<tokio_native_tls::TlsStream<tokio::net::TcpStream>>,
58}
59
60/// Config for the IRC client<br>
61/// For more information about what arguments the IRC commands take, see [`commands::Command`].
62#[derive(Clone, Debug, Default)]
63#[cfg_attr(feature = "toml_config", derive(Deserialize))]
64pub struct Config {
65    channels: Vec<String>,
66    host: String,
67    mode: Option<String>,
68    nickname: Option<String>,
69    port: u16,
70    username: String,
71}
72
73impl Client {
74    /// Creates a new client with a given [`Config`].
75    /// # Example
76    /// ```run_fut
77    /// # let config = Default::default();
78    /// let mut client = Client::new(config).await?;
79    /// # Ok(())
80    /// ```
81    /// # Errors
82    /// Returns error if the client could not connect to the host.
83    /// # Panics
84    /// Panics if the client can't connect to the given host.
85    pub async fn new(config: Config) -> Result<Self, Error> {
86        tracing::debug!("Creating TcpStream");
87        let stream = TcpStream::connect(format!("{}:{}", config.host, config.port))
88            .await
89            .unwrap();
90        tracing::debug!("New TcpStream created");
91
92        #[cfg(feature = "tls")]
93        {
94            let connector = TlsConnector::builder().build().unwrap();
95            let async_connector = tokio_native_tls::TlsConnector::from(connector);
96
97            let sslstream = async_connector
98                .connect(config.host.as_str(), stream)
99                .await
100                .unwrap();
101
102            let buf_reader = BufReader::new(sslstream);
103
104            return Ok(Self { config, buf_reader });
105        };
106
107        #[cfg(not(feature = "tls"))]
108        {
109            let buf_reader = BufReader::new(stream);
110            return Ok(Self { config, buf_reader });
111        };
112    }
113
114    /// Identify user and joins the in the [`Config`] specified channels.
115    /// # Example
116    /// ```run_fut
117    /// # let config = Default::default();
118    /// # let mut client = Client::new(config).await?;
119    /// client.identify().await?;
120    /// # Ok(())
121    /// ```
122    /// # Errors
123    /// Returns error if the client could not write to the stream.
124    pub async fn identify(&mut self) -> Result<(), Error> {
125        self.cap_mode(commands::CapMode::LS).await?;
126        self.cap_mode(commands::CapMode::END).await?;
127
128        self.user(
129            self.config.username.clone(),
130            "*",
131            "*",
132            self.config.username.clone(),
133        )
134        .await?;
135
136        if let Some(nick) = self.config.nickname.clone() {
137            self.nick(&nick).await?;
138        } else {
139            self.nick(&self.config.username.clone()).await?;
140        }
141
142        loop {
143            if let Some(command) = self.read().await? {
144                match command {
145                    commands::Command::PING(code) => {
146                        self.pong(&code).await?;
147                    }
148                    commands::Command::OTHER(line) => {
149                        tracing::trace!("{}", line);
150                        if line.contains("001") {
151                            break;
152                        }
153                    }
154                    _ => {}
155                }
156            }
157        }
158
159        let config = self.config.clone();
160        let user = {
161            if let Some(nick) = config.nickname {
162                nick
163            } else {
164                config.username
165            }
166        };
167
168        if let Some(mode) = config.mode {
169            self.mode(&user, Some(&mode)).await?;
170        }
171
172        for channel in &config.channels {
173            self.join(channel).await?;
174        }
175
176        tracing::info!("async-circe has identified");
177        Ok(())
178    }
179
180    async fn read_string(&mut self) -> Result<Option<String>, tokio::io::Error> {
181        let mut buffer = String::new();
182
183        let num_bytes = match self.buf_reader.read_line(&mut buffer).await {
184            Ok(b) => b,
185            Err(e) => {
186                tracing::error!("{}", e);
187                return Err(e);
188            }
189        };
190
191        if num_bytes == 0 {
192            return Ok(None);
193        }
194
195        Ok(Some(buffer))
196    }
197
198    /// Read data coming from the IRC as a [`commands::Command`].
199    /// # Example
200    /// ```run_fut
201    /// # use async_circe::*;
202    /// # let config = Default::default();
203    /// # let mut client = Client::new(config).await?;
204    /// # client.identify().await?;
205    /// loop {
206    ///    if let Some(ref command) = client.read().await? {
207    ///        if let commands::Command::PRIVMSG(nick, channel, message) = command {
208    ///            println!("{} in {}: {}", nick, channel, message);
209    ///        }
210    ///    }
211    /// # break;
212    /// }
213    /// # Ok(())
214    /// ```
215    /// # Errors
216    /// Returns IO errors from the TcpStream.
217    pub async fn read(&mut self) -> Result<Option<commands::Command>, tokio::io::Error> {
218        if let Some(string) = self.read_string().await? {
219            let command = commands::Command::command_from_str(&string).await;
220
221            if let commands::Command::PING(command) = command {
222                if let Err(_e) = self.pong(&command).await {
223                    return Ok(None);
224                }
225                return Ok(Some(commands::Command::PONG("".to_string())));
226            }
227
228            return Ok(Some(command));
229        }
230
231        Ok(None)
232    }
233
234    async fn write(&mut self, data: String) -> Result<(), Error> {
235        tracing::trace!("{:?}", data);
236        self.buf_reader.write_all(data.as_bytes()).await?;
237
238        Ok(())
239    }
240
241    /// Request information about the admin of a given server.
242    /// # Example
243    /// ```run_fut
244    /// # use async_circe::*;
245    /// # let config = Default::default();
246    /// # let mut client = Client::new(config).await?;
247    /// # client.identify().await?;
248    /// client.admin("libera.chat").await?;
249    /// # Ok(())
250    /// ```
251    /// # Errors
252    /// Returns IO errors from the TcpStream.
253    pub async fn admin(&mut self, target: &str) -> Result<(), Error> {
254        self.write(format!("ADMIN {}\r\n", target)).await?;
255        Ok(())
256    }
257
258    /// Set the status of the client.
259    /// # Example
260    /// ```run_fut
261    /// # let config = Default::default();
262    /// # let mut client = Client::new(config).await?;
263    /// # client.identify().await?;
264    /// client.away("afk").await?;
265    /// # Ok(())
266    /// ```
267    /// # Errors
268    /// Returns IO errors from the TcpStream.
269    pub async fn away(&mut self, message: &str) -> Result<(), Error> {
270        self.write(format!("AWAY {}\r\n", message)).await?;
271        Ok(())
272    }
273
274    #[doc(hidden)]
275    pub async fn cap_mode(&mut self, mode: commands::CapMode) -> Result<(), Error> {
276        let cap_mode = match mode {
277            commands::CapMode::LS => "CAP LS 302\r\n",
278            commands::CapMode::END => "CAP END\r\n",
279        };
280        self.write(cap_mode.to_string()).await?;
281        Ok(())
282    }
283
284    /// Invite someone to a channel.
285    /// # Example
286    /// ```run_fut
287    /// # let config = Default::default();
288    /// # let mut client = Client::new(config).await?;
289    /// # client.identify().await?;
290    /// client.invite("liblemonirc", "#async-circe").await?;
291    /// # Ok(())
292    /// ```
293    /// # Errors
294    /// Returns IO errors from the TcpStream.
295    pub async fn invite(&mut self, username: &str, channel: &str) -> Result<(), Error> {
296        self.write(format!("INVITE {} {}\r\n", username, channel))
297            .await?;
298        Ok(())
299    }
300
301    /// Join a channel.
302    /// # Example
303    /// ```run_fut
304    /// # let config = Default::default();
305    /// # let mut client = Client::new(config).await?;
306    /// # client.identify().await?;
307    /// client.join("#chaos").await?;
308    /// # Ok(())
309    /// ```
310    /// # Errors
311    /// Returns IO errors from the TcpStream.
312    pub async fn join(&mut self, channel: &str) -> Result<(), Error> {
313        self.write(format!("JOIN {}\r\n", channel)).await?;
314        Ok(())
315    }
316
317    /// List available channels on an IRC, or users in a channel.
318    /// # Example
319    /// ```run_fut
320    /// # let config = Default::default();
321    /// # let mut client = Client::new(config).await?;
322    /// # client.identify().await?;
323    /// client.list(None, None).await?;
324    /// # Ok(())
325    /// ```
326    /// # Errors
327    /// Returns IO errors from the TcpStream.
328    pub async fn list(&mut self, channel: Option<&str>, server: Option<&str>) -> Result<(), Error> {
329        let mut formatted = "LIST".to_string();
330        if let Some(channel) = channel {
331            formatted.push_str(format!(" {}", channel).as_str());
332        }
333        if let Some(server) = server {
334            formatted.push_str(format!(" {}", server).as_str());
335        }
336        formatted.push_str("\r\n");
337        self.write(formatted).await?;
338        Ok(())
339    }
340
341    /// Set the mode for a user.
342    /// # Example
343    /// ```run_fut
344    /// # let config = Default::default();
345    /// # let mut client = Client::new(config).await?;
346    /// # client.identify().await?;
347    /// client.mode("test", Some("+B")).await?;
348    /// # Ok(())
349    /// ```
350    /// # Errors
351    /// Returns IO errors from the TcpStream.
352    pub async fn mode(&mut self, target: &str, mode: Option<&str>) -> Result<(), Error> {
353        if let Some(mode) = mode {
354            self.write(format!("MODE {} {}\r\n", target, mode,)).await?;
355        } else {
356            self.write(format!("MODE {}\r\n", target)).await?;
357        }
358        Ok(())
359    }
360
361    /// Get all the people online in channels.
362    /// # Example
363    /// ```run_fut
364    /// # let config = Default::default();
365    /// # let mut client = Client::new(config).await?;
366    /// # client.identify().await?;
367    /// client.names("#chaos,#async-circe", None).await?;
368    /// # Ok(())
369    /// ```
370    /// # Errors
371    /// Returns IO errors from the TcpStream.
372    pub async fn names(&mut self, channel: &str, server: Option<&str>) -> Result<(), Error> {
373        if let Some(server) = server {
374            self.write(format!("NAMES {} {}\r\n", channel, server))
375                .await?;
376        } else {
377            self.write(format!("NAMES {}\r\n", channel)).await?;
378        }
379        Ok(())
380    }
381
382    /// Change your nickname on a server.
383    /// # Example
384    /// ```run_fut
385    /// # let config = Default::default();
386    /// # let mut client = Client::new(config).await?;
387    /// # client.identify().await?;
388    /// client.nick("Not async-circe").await?;
389    /// # Ok(())
390    /// ```
391    /// # Errors
392    /// Returns IO errors from the TcpStream.
393    pub async fn nick(&mut self, nickname: &str) -> Result<(), Error> {
394        self.write(format!("NICK {}\r\n", nickname)).await?;
395        Ok(())
396    }
397
398    /// Authentificate as an operator on a server.
399    /// # Example
400    /// ```run_fut
401    /// # let config = Default::default();
402    /// # let mut client = Client::new(config).await?;
403    /// # client.identify().await?;
404    /// client.oper("username", "password").await?;
405    /// # Ok(())
406    /// ```
407    /// # Errors
408    /// Returns IO errors from the TcpStream.
409    pub async fn oper(&mut self, username: &str, password: &str) -> Result<(), Error> {
410        self.write(format!("OPER {} {}\r\n", username, password))
411            .await?;
412        Ok(())
413    }
414
415    /// Leave a channel.
416    /// # Example
417    /// ```run_fut
418    /// # let config = Default::default();
419    /// # let mut client = Client::new(config).await?;
420    /// # client.identify().await?;
421    /// client.part("#chaos").await?;
422    /// # Ok(())
423    /// ```
424    /// # Errors
425    /// Returns IO errors from the TcpStream.
426    pub async fn part(&mut self, channel: &str) -> Result<(), Error> {
427        self.write(format!("PART {}\r\n", channel)).await?;
428        Ok(())
429    }
430
431    #[doc(hidden)]
432    pub async fn pass(&mut self, password: &str) -> Result<(), Error> {
433        self.write(format!("PASS {}\r\n", password)).await?;
434        Ok(())
435    }
436
437    /// Tests the presence of a connection to a server.
438    /// # Example
439    /// ```run_fut
440    /// # let config = Default::default();
441    /// # let mut client = Client::new(config).await?;
442    /// # client.identify().await?;
443    /// client.ping("libera.chat", None).await?;
444    /// # Ok(())
445    /// ```
446    /// # Errors
447    /// Returns IO errors from the TcpStream.
448    pub async fn ping(&mut self, server1: &str, server2: Option<&str>) -> Result<(), Error> {
449        if let Some(server2) = server2 {
450            self.write(format!("PING {} {}\r\n", server1, server2))
451                .await?;
452        } else {
453            self.write(format!("PING {}\r\n", server1)).await?;
454        }
455        // TODO look if we actually get a PONG back
456        Ok(())
457    }
458
459    #[doc(hidden)]
460    pub async fn pong(&mut self, ping: &str) -> Result<(), Error> {
461        self.write(format!("PONG {}\r\n", ping)).await?;
462        Ok(())
463    }
464
465    /// Send a message to a channel.
466    /// # Example
467    /// ```run_fut
468    /// # let config = Default::default();
469    /// # let mut client = Client::new(config).await?;
470    /// # client.identify().await?;
471    /// client.privmsg("#chaos", "Hello").await?;
472    /// # Ok(())
473    /// ```
474    /// # Errors
475    /// Returns IO errors from the TcpStream.
476    pub async fn privmsg(&mut self, channel: &str, message: &str) -> Result<(), Error> {
477        self.write(format!("PRIVMSG {} {}\r\n", channel, message))
478            .await?;
479        Ok(())
480    }
481
482    /// Leave the IRC server you are connected to.
483    /// # Example
484    /// ```run_fut
485    /// # use async_circe::*;
486    /// # let config = Default::default();
487    /// # let mut client = Client::new(config).await?;
488    /// # client.identify().await?;
489    /// client.quit(None).await?;
490    /// # Ok(())
491    /// ```
492    /// # Errors
493    /// Returns IO errors from the TcpStream.
494    pub async fn quit(&mut self, message: Option<&str>) -> Result<(), Error> {
495        if let Some(message) = message {
496            self.write(format!("QUIT :{}\r\n", message)).await?;
497        } else {
498            self.write(format!(
499                "QUIT :async-circe {} (https://crates.io/crates/async-circe)\r\n",
500                env!("CARGO_PKG_VERSION")
501            ))
502            .await?;
503        }
504        tracing::info!("async-circe shutting down");
505        self.buf_reader.shutdown().await?;
506        Ok(())
507    }
508
509    /// Get the topic of a channel.
510    /// # Example
511    /// ```run_fut
512    /// # use async_circe::*;
513    /// # let config = Default::default();
514    /// # let mut client = Client::new(config).await?;
515    /// # client.identify().await?;
516    /// client.topic("#chaos", None).await?;
517    /// # Ok(())
518    /// ```
519    /// Set the topic of a channel.
520    /// # Example
521    /// ```run_fut
522    /// # let config = Default::default();
523    /// # let mut client = Client::new(config).await?;
524    /// # client.identify().await?;
525    /// client.topic("#chaos", Some("CHAOS")).await?;
526    /// # Ok(())
527    /// ```
528    /// # Errors
529    /// Returns IO errors from the TcpStream.
530    pub async fn topic(&mut self, channel: &str, topic: Option<&str>) -> Result<(), Error> {
531        if let Some(topic) = topic {
532            self.write(format!("TOPIC {} :{}\r\n", channel, topic))
533                .await?;
534        } else {
535            self.write(format!("TOPIC {}\r\n", channel)).await?;
536        }
537        Ok(())
538    }
539
540    #[doc(hidden)]
541    pub async fn user(
542        &mut self,
543        username: String,
544        s1: &str,
545        s2: &str,
546        realname: String,
547    ) -> Result<(), Error> {
548        self.write(format!("USER {} {} {} :{}\r\n", username, s1, s2, realname))
549            .await?;
550        Ok(())
551    }
552}
553
554impl Config {
555    /// Create a new config for the client
556    /// # Example
557    /// ```rust
558    /// # use async_circe::Config;
559    /// let config = Config::new(
560    ///     &["#chaos", "#async-circe"],
561    ///     "karx.xyz",
562    ///     Some("+B"),
563    ///     Some("async-circe"),
564    ///     6697,
565    ///     "circe",
566    /// );
567    /// ```
568    #[must_use]
569    pub fn new(
570        channels: &[&'static str],
571        host: &str,
572        mode: Option<&'static str>,
573        nickname: Option<&'static str>,
574        port: u16,
575        username: &str,
576    ) -> Self {
577        tracing::info!("New async-circe client config");
578        // Conversion from &'static str to String
579        let channels_config = channels
580            .iter()
581            .map(|channel| (*channel).to_string())
582            .collect();
583
584        let mode_config: Option<String>;
585        if let Some(mode) = mode {
586            mode_config = Some(mode.to_string());
587        } else {
588            mode_config = None;
589        }
590
591        let nickname_config: Option<String>;
592        if let Some(nickname) = nickname {
593            nickname_config = Some(nickname.to_string());
594        } else {
595            nickname_config = Some(username.to_string());
596        }
597
598        let config = Self {
599            channels: channels_config,
600            host: host.into(),
601            mode: mode_config,
602            nickname: nickname_config,
603            port,
604            username: username.into(),
605        };
606
607        tracing::debug!("async-circe config: {:?}", config);
608        config
609    }
610
611    /// Allows for configuring async-circe at runtime.
612    /// # Example
613    /// ```run_fut
614    /// # use async_circe::Config;
615    /// let config = Config::runtime_config(
616    ///     vec!["#chaos".to_string(), "#async-circe".to_string()],
617    ///     "karx.xyz".to_string(),
618    ///     Some("+B".to_string()),
619    ///     Some("async-circe".to_string()),
620    ///     6697,
621    ///     "circe".to_string(),
622    /// );
623    /// ```
624    #[must_use]
625    pub fn runtime_config(
626        channels: Vec<String>,
627        host: String,
628        mode: Option<String>,
629        nickname: Option<String>,
630        port: u16,
631        username: String,
632    ) -> Self {
633        tracing::info!("New async-circe client config");
634        let mode_config: Option<String>;
635        if let Some(mode) = mode {
636            mode_config = Some(mode);
637        } else {
638            mode_config = None;
639        }
640
641        let nickname_config: Option<String>;
642        if let Some(nickname) = nickname {
643            nickname_config = Some(nickname);
644        } else {
645            nickname_config = Some(username.to_string());
646        }
647
648        let config = Self {
649            channels,
650            host,
651            mode: mode_config,
652            nickname: nickname_config,
653            port,
654            username,
655        };
656
657        tracing::debug!("async-circe config: {:?}", config);
658        config
659    }
660
661    /// Create a config from a toml file
662    /// # Example
663    /// ```toml
664    /// #Config.toml
665    /// channels = ["#chaos", "#async-circe"]
666    /// host = "karx.xyz"
667    /// mode = "+B"
668    /// nickname = "async-circe"
669    /// port = 6667
670    /// username = "circe"
671    /// ```
672    /// ```run_fut
673    /// let config = Config::from_toml("Config.toml")?;
674    /// ```
675    /// # Errors
676    /// Returns an Error if the file cannot be opened or if the TOML is invalid
677    #[cfg(any(doc, feature = "toml_config"))]
678    #[doc(cfg(feature = "toml_config"))]
679    pub fn from_toml<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
680        tracing::info!("New async-circe client config");
681        let mut file = File::open(&path)?;
682        let mut data = String::new();
683        file.read_to_string(&mut data)?;
684
685        let config = toml::from_str(&data)
686            .map_err(|e| Error::new(ErrorKind::Other, format!("Invalid TOML: {}", e)));
687        tracing::debug!("async-circe config: {:?}", config);
688        config
689    }
690}