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}