1948 lines
64 KiB
Rust
1948 lines
64 KiB
Rust
//! A simple, thread-safe, and async-friendly IRC client library.
|
|
//!
|
|
//! This API provides the ability to connect to an IRC server via the
|
|
//! [`Client`](struct.Client.html) type. The [`Client`](trait.Client.html) trait that
|
|
//! [`Client`](struct.Client.html) implements provides methods for communicating with the
|
|
//! server.
|
|
//!
|
|
//! # Examples
|
|
//!
|
|
//! Using these APIs, we can connect to a server and send a one-off message (in this case,
|
|
//! identifying with the server).
|
|
//!
|
|
//! ```no_run
|
|
//! # extern crate irc;
|
|
//! use irc::client::prelude::Client;
|
|
//!
|
|
//! # #[tokio::main]
|
|
//! # async fn main() -> irc::error::Result<()> {
|
|
//! let client = Client::new("config.toml").await?;
|
|
//! client.identify()?;
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
//!
|
|
//! We can then use functions from [`Client`](trait.Client.html) to receive messages from the
|
|
//! server in a blocking fashion and perform any desired actions in response. The following code
|
|
//! performs a simple call-and-response when the bot's name is mentioned in a channel.
|
|
//!
|
|
//! ```no_run
|
|
//! use irc::client::prelude::*;
|
|
//! use futures::*;
|
|
//!
|
|
//! # #[tokio::main]
|
|
//! # async fn main() -> irc::error::Result<()> {
|
|
//! let mut client = Client::new("config.toml").await?;
|
|
//! let mut stream = client.stream()?;
|
|
//! client.identify()?;
|
|
//!
|
|
//! while let Some(message) = stream.next().await.transpose()? {
|
|
//! if let Command::PRIVMSG(channel, message) = message.command {
|
|
//! if message.contains(client.current_nickname()) {
|
|
//! client.send_privmsg(&channel, "beep boop").unwrap();
|
|
//! }
|
|
//! }
|
|
//! }
|
|
//! # Ok(())
|
|
//! # }
|
|
//! ```
|
|
|
|
#[cfg(feature = "ctcp")]
|
|
use chrono::prelude::*;
|
|
use futures_channel::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
|
use futures_util::{
|
|
future::{FusedFuture, Future},
|
|
ready,
|
|
stream::{FusedStream, Stream},
|
|
};
|
|
use futures_util::{
|
|
sink::Sink as _,
|
|
stream::{SplitSink, SplitStream, StreamExt as _},
|
|
};
|
|
use parking_lot::RwLock;
|
|
use std::{
|
|
collections::HashMap,
|
|
fmt,
|
|
path::Path,
|
|
pin::Pin,
|
|
sync::Arc,
|
|
task::{Context, Poll},
|
|
};
|
|
|
|
use crate::{
|
|
client::{
|
|
conn::Connection,
|
|
data::{Config, User},
|
|
},
|
|
error,
|
|
proto::{
|
|
mode::ModeType,
|
|
CapSubCommand::{END, LS, REQ},
|
|
Capability, ChannelMode, Command,
|
|
Command::{
|
|
ChannelMODE, AUTHENTICATE, CAP, INVITE, JOIN, KICK, KILL, NICK, NICKSERV, NOTICE, OPER,
|
|
PART, PASS, PONG, PRIVMSG, QUIT, SAMODE, SANICK, TOPIC, USER,
|
|
},
|
|
Message, Mode, NegotiationVersion, Response,
|
|
},
|
|
};
|
|
|
|
pub mod conn;
|
|
pub mod data;
|
|
mod mock;
|
|
pub mod prelude;
|
|
pub mod transport;
|
|
|
|
macro_rules! pub_state_base {
|
|
() => {
|
|
/// Changes the modes for the specified target.
|
|
pub fn send_mode<S, T>(&self, target: S, modes: &[Mode<T>]) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
T: ModeType,
|
|
{
|
|
self.send(T::mode(&target.to_string(), modes))
|
|
}
|
|
|
|
/// Joins the specified channel or chanlist.
|
|
pub fn send_join<S>(&self, chanlist: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send(JOIN(chanlist.to_string(), None, None))
|
|
}
|
|
|
|
/// Joins the specified channel or chanlist using the specified key or keylist.
|
|
pub fn send_join_with_keys<S1, S2>(&self, chanlist: &str, keylist: &str) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
self.send(JOIN(chanlist.to_string(), Some(keylist.to_string()), None))
|
|
}
|
|
|
|
/// Sends a notice to the specified target.
|
|
pub fn send_notice<S1, S2>(&self, target: S1, message: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
let message = message.to_string();
|
|
for line in message.split("\r\n") {
|
|
self.send(NOTICE(target.to_string(), line.to_string()))?
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! pub_sender_base {
|
|
() => {
|
|
/// Sends a request for a list of server capabilities for a specific IRCv3 version.
|
|
pub fn send_cap_ls(&self, version: NegotiationVersion) -> error::Result<()> {
|
|
self.send(Command::CAP(
|
|
None,
|
|
LS,
|
|
match version {
|
|
NegotiationVersion::V301 => None,
|
|
NegotiationVersion::V302 => Some("302".to_owned()),
|
|
},
|
|
None,
|
|
))
|
|
}
|
|
|
|
/// Sends an IRCv3 capabilities request for the specified extensions.
|
|
pub fn send_cap_req(&self, extensions: &[Capability]) -> error::Result<()> {
|
|
let append = |mut s: String, c| {
|
|
s.push_str(c);
|
|
s.push(' ');
|
|
s
|
|
};
|
|
let mut exts = extensions
|
|
.iter()
|
|
.map(|c| c.as_ref())
|
|
.fold(String::new(), append);
|
|
let len = exts.len() - 1;
|
|
exts.truncate(len);
|
|
self.send(CAP(None, REQ, None, Some(exts)))
|
|
}
|
|
|
|
/// Sends a SASL AUTHENTICATE message with the specified data.
|
|
pub fn send_sasl<S: fmt::Display>(&self, data: S) -> error::Result<()> {
|
|
self.send(AUTHENTICATE(data.to_string()))
|
|
}
|
|
|
|
/// Sends a SASL AUTHENTICATE request to use the PLAIN mechanism.
|
|
pub fn send_sasl_plain(&self) -> error::Result<()> {
|
|
self.send_sasl("PLAIN")
|
|
}
|
|
|
|
/// Sends a SASL AUTHENTICATE request to use the EXTERNAL mechanism.
|
|
pub fn send_sasl_external(&self) -> error::Result<()> {
|
|
self.send_sasl("EXTERNAL")
|
|
}
|
|
|
|
/// Sends a SASL AUTHENTICATE request to abort authentication.
|
|
pub fn send_sasl_abort(&self) -> error::Result<()> {
|
|
self.send_sasl("*")
|
|
}
|
|
|
|
/// Sends a PONG with the specified message.
|
|
pub fn send_pong<S>(&self, msg: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send(PONG(msg.to_string(), None))
|
|
}
|
|
|
|
/// Parts the specified channel or chanlist.
|
|
pub fn send_part<S>(&self, chanlist: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send(PART(chanlist.to_string(), None))
|
|
}
|
|
|
|
/// Attempts to oper up using the specified username and password.
|
|
pub fn send_oper<S1, S2>(&self, username: S1, password: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
self.send(OPER(username.to_string(), password.to_string()))
|
|
}
|
|
|
|
/// Sends a message to the specified target. If the message contains IRC newlines (`\r\n`), it
|
|
/// will automatically be split and sent as multiple separate `PRIVMSG`s to the specified
|
|
/// target. If you absolutely must avoid this behavior, you can do
|
|
/// `client.send(PRIVMSG(target, message))` directly.
|
|
pub fn send_privmsg<S1, S2>(&self, target: S1, message: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
let message = message.to_string();
|
|
for line in message.split("\r\n") {
|
|
self.send(PRIVMSG(target.to_string(), line.to_string()))?
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Sets the topic of a channel or requests the current one.
|
|
/// If `topic` is an empty string, it won't be included in the message.
|
|
pub fn send_topic<S1, S2>(&self, channel: S1, topic: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
let topic = topic.to_string();
|
|
self.send(TOPIC(
|
|
channel.to_string(),
|
|
if topic.is_empty() { None } else { Some(topic) },
|
|
))
|
|
}
|
|
|
|
/// Kills the target with the provided message.
|
|
pub fn send_kill<S1, S2>(&self, target: S1, message: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
self.send(KILL(target.to_string(), message.to_string()))
|
|
}
|
|
|
|
/// Kicks the listed nicknames from the listed channels with a comment.
|
|
/// If `message` is an empty string, it won't be included in the message.
|
|
pub fn send_kick<S1, S2, S3>(
|
|
&self,
|
|
chanlist: S1,
|
|
nicklist: S2,
|
|
message: S3,
|
|
) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
S3: fmt::Display,
|
|
{
|
|
let message = message.to_string();
|
|
self.send(KICK(
|
|
chanlist.to_string(),
|
|
nicklist.to_string(),
|
|
if message.is_empty() {
|
|
None
|
|
} else {
|
|
Some(message)
|
|
},
|
|
))
|
|
}
|
|
|
|
/// Changes the mode of the target by force.
|
|
/// If `modeparams` is an empty string, it won't be included in the message.
|
|
pub fn send_samode<S1, S2, S3>(&self, target: S1, mode: S2, modeparams: S3) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
S3: fmt::Display,
|
|
{
|
|
let modeparams = modeparams.to_string();
|
|
self.send(SAMODE(
|
|
target.to_string(),
|
|
mode.to_string(),
|
|
if modeparams.is_empty() {
|
|
None
|
|
} else {
|
|
Some(modeparams)
|
|
},
|
|
))
|
|
}
|
|
|
|
/// Forces a user to change from the old nickname to the new nickname.
|
|
pub fn send_sanick<S1, S2>(&self, old_nick: S1, new_nick: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
self.send(SANICK(old_nick.to_string(), new_nick.to_string()))
|
|
}
|
|
|
|
/// Invites a user to the specified channel.
|
|
pub fn send_invite<S1, S2>(&self, nick: S1, chan: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
self.send(INVITE(nick.to_string(), chan.to_string()))
|
|
}
|
|
|
|
/// Quits the server entirely with a message.
|
|
/// This defaults to `Powered by Rust.` if none is specified.
|
|
pub fn send_quit<S>(&self, msg: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
let msg = msg.to_string();
|
|
self.send(QUIT(Some(if msg.is_empty() {
|
|
"Powered by Rust.".to_string()
|
|
} else {
|
|
msg
|
|
})))
|
|
}
|
|
|
|
/// Sends a CTCP-escaped message to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_ctcp<S1, S2>(&self, target: S1, msg: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
let msg = msg.to_string();
|
|
for line in msg.split("\r\n") {
|
|
self.send(PRIVMSG(target.to_string(), format!("\u{001}{}\u{001}", line)))?
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Sends an action command to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_action<S1, S2>(&self, target: S1, msg: S2) -> error::Result<()>
|
|
where
|
|
S1: fmt::Display,
|
|
S2: fmt::Display,
|
|
{
|
|
self.send_ctcp(target, &format!("ACTION {}", msg.to_string())[..])
|
|
}
|
|
|
|
/// Sends a finger request to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_finger<S: fmt::Display>(&self, target: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send_ctcp(target, "FINGER")
|
|
}
|
|
|
|
/// Sends a version request to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_version<S>(&self, target: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send_ctcp(target, "VERSION")
|
|
}
|
|
|
|
/// Sends a source request to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_source<S>(&self, target: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send_ctcp(target, "SOURCE")
|
|
}
|
|
|
|
/// Sends a user info request to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_user_info<S>(&self, target: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send_ctcp(target, "USERINFO")
|
|
}
|
|
|
|
/// Sends a finger request to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_ctcp_ping<S>(&self, target: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
let time = Local::now();
|
|
self.send_ctcp(target, &format!("PING {}", time.timestamp())[..])
|
|
}
|
|
|
|
/// Sends a time request to the specified target.
|
|
/// This requires the CTCP feature to be enabled.
|
|
#[cfg(feature = "ctcp")]
|
|
pub fn send_time<S>(&self, target: S) -> error::Result<()>
|
|
where
|
|
S: fmt::Display,
|
|
{
|
|
self.send_ctcp(target, "TIME")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A stream of `Messages` received from an IRC server via an `Client`.
|
|
///
|
|
/// Interaction with this stream relies on the `futures` API, but is only expected for less
|
|
/// traditional use cases. To learn more, you can view the documentation for the
|
|
/// [`futures`](https://docs.rs/futures/) crate, or the tutorials for
|
|
/// [`tokio`](https://tokio.rs/docs/getting-started/futures/).
|
|
#[derive(Debug)]
|
|
pub struct ClientStream {
|
|
state: Arc<ClientState>,
|
|
stream: SplitStream<Connection>,
|
|
// In case the client stream also handles outgoing messages.
|
|
outgoing: Option<Outgoing>,
|
|
}
|
|
|
|
impl ClientStream {
|
|
/// collect stream and collect all messages available.
|
|
pub async fn collect(mut self) -> error::Result<Vec<Message>> {
|
|
let mut output = Vec::new();
|
|
|
|
while let Some(message) = self.next().await {
|
|
match message {
|
|
Ok(message) => output.push(message),
|
|
Err(e) => return Err(e),
|
|
}
|
|
}
|
|
|
|
Ok(output)
|
|
}
|
|
}
|
|
|
|
impl FusedStream for ClientStream {
|
|
fn is_terminated(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl Stream for ClientStream {
|
|
type Item = Result<Message, error::Error>;
|
|
|
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
if let Some(outgoing) = self.as_mut().outgoing.as_mut() {
|
|
match Pin::new(outgoing).poll(cx) {
|
|
Poll::Ready(Ok(())) => {
|
|
// assure that we wake up again to check the incoming stream.
|
|
cx.waker().wake_by_ref();
|
|
return Poll::Ready(None);
|
|
}
|
|
Poll::Ready(Err(e)) => {
|
|
cx.waker().wake_by_ref();
|
|
return Poll::Ready(Some(Err(e)));
|
|
}
|
|
Poll::Pending => (),
|
|
}
|
|
}
|
|
|
|
match ready!(Pin::new(&mut self.as_mut().stream).poll_next(cx)) {
|
|
Some(Ok(msg)) => {
|
|
self.state.handle_message(&msg)?;
|
|
return Poll::Ready(Some(Ok(msg)));
|
|
}
|
|
other => Poll::Ready(other),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Thread-safe internal state for an IRC server connection.
|
|
#[derive(Debug)]
|
|
struct ClientState {
|
|
sender: Sender,
|
|
/// The configuration used with this connection.
|
|
config: Config,
|
|
/// A thread-safe map of channels to the list of users in them.
|
|
chanlists: RwLock<HashMap<String, Vec<User>>>,
|
|
/// A thread-safe index to track the current alternative nickname being used.
|
|
alt_nick_index: RwLock<usize>,
|
|
/// Default ghost sequence to send if one is required but none is configured.
|
|
default_ghost_sequence: Vec<String>,
|
|
}
|
|
|
|
impl ClientState {
|
|
fn new(sender: Sender, config: Config) -> ClientState {
|
|
ClientState {
|
|
sender,
|
|
config,
|
|
chanlists: RwLock::new(HashMap::new()),
|
|
alt_nick_index: RwLock::new(0),
|
|
default_ghost_sequence: vec![String::from("GHOST")],
|
|
}
|
|
}
|
|
|
|
fn config(&self) -> &Config {
|
|
&self.config
|
|
}
|
|
|
|
fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()> {
|
|
let msg = msg.into();
|
|
self.handle_sent_message(&msg)?;
|
|
Ok(self.sender.send(msg)?)
|
|
}
|
|
|
|
/// Gets the current nickname in use.
|
|
fn current_nickname(&self) -> &str {
|
|
let alt_nicks = self.config().alternate_nicknames();
|
|
let index = self.alt_nick_index.read();
|
|
|
|
match *index {
|
|
0 => self
|
|
.config()
|
|
.nickname()
|
|
.expect("current_nickname should not be callable if nickname is not defined."),
|
|
i => alt_nicks[i - 1].as_str(),
|
|
}
|
|
}
|
|
|
|
/// Handles sent messages internally for basic client functionality.
|
|
fn handle_sent_message(&self, msg: &Message) -> error::Result<()> {
|
|
log::trace!("[SENT] {}", msg.to_string());
|
|
|
|
match msg.command {
|
|
PART(ref chan, _) => {
|
|
let _ = self.chanlists.write().remove(chan);
|
|
}
|
|
_ => (),
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Handles received messages internally for basic client functionality.
|
|
fn handle_message(&self, msg: &Message) -> error::Result<()> {
|
|
log::trace!("[RECV] {}", msg.to_string());
|
|
match msg.command {
|
|
JOIN(ref chan, _, _) => self.handle_join(msg.source_nickname().unwrap_or(""), chan),
|
|
PART(ref chan, _) => self.handle_part(msg.source_nickname().unwrap_or(""), chan),
|
|
KICK(ref chan, ref user, _) => self.handle_part(user, chan),
|
|
QUIT(_) => self.handle_quit(msg.source_nickname().unwrap_or("")),
|
|
NICK(ref new_nick) => {
|
|
self.handle_nick_change(msg.source_nickname().unwrap_or(""), new_nick)
|
|
}
|
|
ChannelMODE(ref chan, ref modes) => self.handle_mode(chan, modes),
|
|
PRIVMSG(ref target, ref body) => {
|
|
if body.starts_with('\u{001}') {
|
|
let tokens: Vec<_> = {
|
|
let end = if body.ends_with('\u{001}') && body.len() > 1 {
|
|
body.len() - 1
|
|
} else {
|
|
body.len()
|
|
};
|
|
body[1..end].split(' ').collect()
|
|
};
|
|
if target.starts_with('#') {
|
|
self.handle_ctcp(target, &tokens)?
|
|
} else if let Some(user) = msg.source_nickname() {
|
|
self.handle_ctcp(user, &tokens)?
|
|
}
|
|
}
|
|
}
|
|
Command::Response(Response::RPL_NAMREPLY, ref args) => self.handle_namreply(args),
|
|
Command::Response(Response::RPL_ENDOFMOTD, _)
|
|
| Command::Response(Response::ERR_NOMOTD, _) => {
|
|
self.send_nick_password()?;
|
|
self.send_umodes()?;
|
|
|
|
let config_chans = self.config().channels();
|
|
for chan in config_chans {
|
|
match self.config().channel_key(chan) {
|
|
Some(key) => self.send_join_with_keys::<&str, &str>(chan, key)?,
|
|
None => self.send_join(chan)?,
|
|
}
|
|
}
|
|
let joined_chans = self.chanlists.read();
|
|
for chan in joined_chans
|
|
.keys()
|
|
.filter(|x| config_chans.iter().find(|c| c == x).is_none())
|
|
{
|
|
self.send_join(chan)?
|
|
}
|
|
}
|
|
Command::Response(Response::ERR_NICKNAMEINUSE, _)
|
|
| Command::Response(Response::ERR_ERRONEOUSNICKNAME, _) => {
|
|
let alt_nicks = self.config().alternate_nicknames();
|
|
let mut index = self.alt_nick_index.write();
|
|
|
|
if *index >= alt_nicks.len() {
|
|
return Err(error::Error::NoUsableNick);
|
|
} else {
|
|
self.send(NICK(alt_nicks[*index].to_owned()))?;
|
|
*index += 1;
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn send_nick_password(&self) -> error::Result<()> {
|
|
if self.config().nick_password().is_empty() {
|
|
Ok(())
|
|
} else {
|
|
let mut index = self.alt_nick_index.write();
|
|
|
|
if self.config().should_ghost() && *index != 0 {
|
|
let seq = match self.config().ghost_sequence() {
|
|
Some(seq) => seq,
|
|
None => &*self.default_ghost_sequence,
|
|
};
|
|
|
|
for s in seq {
|
|
self.send(NICKSERV(vec![
|
|
s.to_string(),
|
|
self.config().nickname()?.to_string(),
|
|
self.config().nick_password().to_string(),
|
|
]))?;
|
|
}
|
|
*index = 0;
|
|
self.send(NICK(self.config().nickname()?.to_owned()))?
|
|
}
|
|
|
|
self.send(NICKSERV(vec![
|
|
"IDENTIFY".to_string(),
|
|
self.config().nick_password().to_string(),
|
|
]))
|
|
}
|
|
}
|
|
|
|
fn send_umodes(&self) -> error::Result<()> {
|
|
if self.config().umodes().is_empty() {
|
|
Ok(())
|
|
} else {
|
|
self.send_mode(
|
|
self.current_nickname(),
|
|
&Mode::as_user_modes(
|
|
self.config()
|
|
.umodes()
|
|
.split(' ')
|
|
.collect::<Vec<_>>()
|
|
.as_ref(),
|
|
)
|
|
.map_err(|e| error::Error::InvalidMessage {
|
|
string: format!(
|
|
"MODE {} {}",
|
|
self.current_nickname(),
|
|
self.config().umodes()
|
|
),
|
|
cause: e,
|
|
})?,
|
|
)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
fn handle_join(&self, _: &str, _: &str) {}
|
|
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
fn handle_join(&self, src: &str, chan: &str) {
|
|
if let Some(vec) = self.chanlists.write().get_mut(&chan.to_owned()) {
|
|
if !src.is_empty() {
|
|
vec.push(User::new(src))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
fn handle_part(&self, _: &str, _: &str) {}
|
|
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
fn handle_part(&self, src: &str, chan: &str) {
|
|
if let Some(vec) = self.chanlists.write().get_mut(&chan.to_owned()) {
|
|
if !src.is_empty() {
|
|
if let Some(n) = vec.iter().position(|x| x.get_nickname() == src) {
|
|
vec.swap_remove(n);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
fn handle_quit(&self, _: &str) {}
|
|
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
fn handle_quit(&self, src: &str) {
|
|
if src.is_empty() {
|
|
return;
|
|
}
|
|
|
|
for vec in self.chanlists.write().values_mut() {
|
|
if let Some(p) = vec.iter().position(|x| x.get_nickname() == src) {
|
|
vec.swap_remove(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
fn handle_nick_change(&self, _: &str, _: &str) {}
|
|
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
fn handle_nick_change(&self, old_nick: &str, new_nick: &str) {
|
|
if old_nick.is_empty() || new_nick.is_empty() {
|
|
return;
|
|
}
|
|
|
|
for (_, vec) in self.chanlists.write().iter_mut() {
|
|
if let Some(n) = vec.iter().position(|x| x.get_nickname() == old_nick) {
|
|
let new_entry = User::new(new_nick);
|
|
vec[n] = new_entry;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
fn handle_mode(&self, _: &str, _: &[Mode<ChannelMode>]) {}
|
|
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
fn handle_mode(&self, chan: &str, modes: &[Mode<ChannelMode>]) {
|
|
for mode in modes {
|
|
match *mode {
|
|
Mode::Plus(_, Some(ref user)) | Mode::Minus(_, Some(ref user)) => {
|
|
if let Some(vec) = self.chanlists.write().get_mut(chan) {
|
|
if let Some(n) = vec.iter().position(|x| x.get_nickname() == user) {
|
|
vec[n].update_access_level(mode)
|
|
}
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
fn handle_namreply(&self, _: &[String]) {}
|
|
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
fn handle_namreply(&self, args: &[String]) {
|
|
if args.len() == 4 {
|
|
let chan = &args[2];
|
|
for user in args[3].split(' ') {
|
|
self.chanlists
|
|
.write()
|
|
.entry(chan.clone())
|
|
.or_insert_with(Vec::new)
|
|
.push(User::new(user))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ctcp")]
|
|
fn handle_ctcp(&self, resp: &str, tokens: &[&str]) -> error::Result<()> {
|
|
if tokens.is_empty() {
|
|
return Ok(());
|
|
}
|
|
if tokens[0].eq_ignore_ascii_case("FINGER") {
|
|
self.send_ctcp_internal(
|
|
resp,
|
|
&format!(
|
|
"FINGER :{} ({})",
|
|
self.config().real_name(),
|
|
self.config().username()
|
|
),
|
|
)
|
|
} else if tokens[0].eq_ignore_ascii_case("VERSION") {
|
|
self.send_ctcp_internal(resp, &format!("VERSION {}", self.config().version()))
|
|
} else if tokens[0].eq_ignore_ascii_case("SOURCE") {
|
|
self.send_ctcp_internal(resp, &format!("SOURCE {}", self.config().source()))
|
|
} else if tokens[0].eq_ignore_ascii_case("PING") && tokens.len() > 1 {
|
|
self.send_ctcp_internal(resp, &format!("PING {}", tokens[1]))
|
|
} else if tokens[0].eq_ignore_ascii_case("TIME") {
|
|
self.send_ctcp_internal(resp, &format!("TIME :{}", Local::now().to_rfc2822()))
|
|
} else if tokens[0].eq_ignore_ascii_case("USERINFO") {
|
|
self.send_ctcp_internal(resp, &format!("USERINFO :{}", self.config().user_info()))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "ctcp")]
|
|
fn send_ctcp_internal(&self, target: &str, msg: &str) -> error::Result<()> {
|
|
self.send_notice(target, &format!("\u{001}{}\u{001}", msg))
|
|
}
|
|
|
|
#[cfg(not(feature = "ctcp"))]
|
|
fn handle_ctcp(&self, _: &str, _: &[&str]) -> error::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub_state_base!();
|
|
}
|
|
|
|
/// Thread-safe sender that can be used with the client.
|
|
#[derive(Debug, Clone)]
|
|
pub struct Sender {
|
|
tx_outgoing: UnboundedSender<Message>,
|
|
}
|
|
|
|
impl Sender {
|
|
/// Send a single message to the unbounded queue.
|
|
pub fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()> {
|
|
Ok(self.tx_outgoing.unbounded_send(msg.into())?)
|
|
}
|
|
|
|
pub_state_base!();
|
|
pub_sender_base!();
|
|
}
|
|
|
|
/// Future to handle outgoing messages.
|
|
///
|
|
/// Note: this is essentially the same as a version of [SendAll](https://github.com/rust-lang-nursery/futures-rs/blob/master/futures-util/src/sink/send_all.rs) that owns it's sink and stream.
|
|
#[derive(Debug)]
|
|
pub struct Outgoing {
|
|
sink: SplitSink<Connection, Message>,
|
|
stream: UnboundedReceiver<Message>,
|
|
buffered: Option<Message>,
|
|
}
|
|
|
|
impl Outgoing {
|
|
fn try_start_send(
|
|
&mut self,
|
|
cx: &mut Context<'_>,
|
|
message: Message,
|
|
) -> Poll<Result<(), error::Error>> {
|
|
debug_assert!(self.buffered.is_none());
|
|
|
|
match Pin::new(&mut self.sink).poll_ready(cx)? {
|
|
Poll::Ready(()) => Poll::Ready(Pin::new(&mut self.sink).start_send(message)),
|
|
Poll::Pending => {
|
|
self.buffered = Some(message);
|
|
Poll::Pending
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FusedFuture for Outgoing {
|
|
fn is_terminated(&self) -> bool {
|
|
// NB: outgoing stream never terminates.
|
|
// TODO: should it terminate if rx_outgoing is terminated?
|
|
false
|
|
}
|
|
}
|
|
|
|
impl Future for Outgoing {
|
|
type Output = error::Result<()>;
|
|
|
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
let this = &mut *self;
|
|
|
|
if let Some(message) = this.buffered.take() {
|
|
ready!(this.try_start_send(cx, message))?
|
|
}
|
|
|
|
loop {
|
|
match this.stream.poll_next_unpin(cx) {
|
|
Poll::Ready(Some(message)) => ready!(this.try_start_send(cx, message))?,
|
|
Poll::Ready(None) => {
|
|
ready!(Pin::new(&mut this.sink).poll_flush(cx))?;
|
|
return Poll::Ready(Ok(()));
|
|
}
|
|
Poll::Pending => {
|
|
ready!(Pin::new(&mut this.sink).poll_flush(cx))?;
|
|
return Poll::Pending;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The canonical implementation of a connection to an IRC server.
|
|
///
|
|
/// For a full example usage, see [`irc::client`](./index.html).
|
|
#[derive(Debug)]
|
|
pub struct Client {
|
|
/// The internal, thread-safe server state.
|
|
state: Arc<ClientState>,
|
|
incoming: Option<SplitStream<Connection>>,
|
|
outgoing: Option<Outgoing>,
|
|
sender: Sender,
|
|
#[cfg(test)]
|
|
/// A view of the logs for a mock connection.
|
|
view: Option<self::transport::LogView>,
|
|
}
|
|
|
|
impl Client {
|
|
/// Creates a new `Client` from the configuration at the specified path, connecting
|
|
/// immediately. This function is short-hand for loading the configuration and then calling
|
|
/// `Client::from_config` and consequently inherits its behaviors.
|
|
///
|
|
/// # Example
|
|
/// ```no_run
|
|
/// # use irc::client::prelude::*;
|
|
/// # #[tokio::main]
|
|
/// # async fn main() -> irc::error::Result<()> {
|
|
/// let client = Client::new("config.toml").await?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub async fn new<P: AsRef<Path>>(config: P) -> error::Result<Client> {
|
|
Client::from_config(Config::load(config)?).await
|
|
}
|
|
|
|
/// Creates a `Future` of an `Client` from the specified configuration and on the event loop
|
|
/// corresponding to the given handle. This can be used to set up a number of `Clients` on a
|
|
/// single, shared event loop. It can also be used to take more control over execution and error
|
|
/// handling. Connection will not occur until the event loop is run.
|
|
pub async fn from_config(config: Config) -> error::Result<Client> {
|
|
let (tx_outgoing, rx_outgoing) = mpsc::unbounded();
|
|
let conn = Connection::new(&config, tx_outgoing.clone()).await?;
|
|
|
|
#[cfg(test)]
|
|
let view = conn.log_view();
|
|
|
|
let (sink, incoming) = conn.split();
|
|
|
|
let sender = Sender { tx_outgoing };
|
|
|
|
Ok(Client {
|
|
sender: sender.clone(),
|
|
state: Arc::new(ClientState::new(sender, config)),
|
|
incoming: Some(incoming),
|
|
outgoing: Some(Outgoing {
|
|
sink,
|
|
stream: rx_outgoing,
|
|
buffered: None,
|
|
}),
|
|
#[cfg(test)]
|
|
view,
|
|
})
|
|
}
|
|
|
|
/// Gets the log view from the internal transport. Only used for unit testing.
|
|
#[cfg(test)]
|
|
fn log_view(&self) -> &self::transport::LogView {
|
|
self.view
|
|
.as_ref()
|
|
.expect("there should be a log during testing")
|
|
}
|
|
|
|
/// Take the outgoing future in order to drive it yourself.
|
|
///
|
|
/// Must be called before `stream` if you intend to drive this future
|
|
/// yourself.
|
|
pub fn outgoing(&mut self) -> Option<Outgoing> {
|
|
self.outgoing.take()
|
|
}
|
|
|
|
/// Get access to a thread-safe sender that can be used with the client.
|
|
pub fn sender(&self) -> Sender {
|
|
self.sender.clone()
|
|
}
|
|
|
|
/// Gets the configuration being used with this `Client`.
|
|
fn config(&self) -> &Config {
|
|
&self.state.config
|
|
}
|
|
|
|
/// Gets a stream of incoming messages from the `Client`'s connection. This is only necessary
|
|
/// when trying to set up more complex clients, and requires use of the `futures` crate. Most
|
|
/// IRC bots should be able to get by using only `for_each_incoming` to handle received
|
|
/// messages. You can find some examples of more complex setups using `stream` in the
|
|
/// [GitHub repository](https://github.com/aatxe/irc/tree/stable/examples).
|
|
///
|
|
/// **Note**: The stream can only be returned once. Subsequent attempts will cause a panic.
|
|
// FIXME: when impl traits stabilize, we should change this return type.
|
|
pub fn stream(&mut self) -> error::Result<ClientStream> {
|
|
let stream = self
|
|
.incoming
|
|
.take()
|
|
.ok_or_else(|| error::Error::StreamAlreadyConfigured)?;
|
|
|
|
Ok(ClientStream {
|
|
state: Arc::clone(&self.state),
|
|
stream,
|
|
outgoing: self.outgoing.take(),
|
|
})
|
|
}
|
|
|
|
/// Gets a list of currently joined channels. This will be `None` if tracking is disabled
|
|
/// altogether via the `nochanlists` feature.
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
pub fn list_channels(&self) -> Option<Vec<String>> {
|
|
Some(
|
|
self.state
|
|
.chanlists
|
|
.read()
|
|
.keys()
|
|
.map(|k| k.to_owned())
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
pub fn list_channels(&self) -> Option<Vec<String>> {
|
|
None
|
|
}
|
|
|
|
/// Gets a list of [`Users`](./data/user/struct.User.html) in the specified channel. If the
|
|
/// specified channel hasn't been joined or the `nochanlists` feature is enabled, this function
|
|
/// will return `None`.
|
|
///
|
|
/// For best results, be sure to request `multi-prefix` support from the server. This will allow
|
|
/// for more accurate tracking of user rank (e.g. oper, half-op, etc.).
|
|
/// # Requesting multi-prefix support
|
|
/// ```no_run
|
|
/// # use irc::client::prelude::*;
|
|
/// use irc::proto::caps::Capability;
|
|
///
|
|
/// # #[tokio::main]
|
|
/// # async fn main() -> irc::error::Result<()> {
|
|
/// # let client = Client::new("config.toml").await?;
|
|
/// client.send_cap_req(&[Capability::MultiPrefix])?;
|
|
/// client.identify()?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
pub fn list_users(&self, chan: &str) -> Option<Vec<User>> {
|
|
self.state.chanlists.read().get(&chan.to_owned()).cloned()
|
|
}
|
|
|
|
#[cfg(feature = "nochanlists")]
|
|
pub fn list_users(&self, _: &str) -> Option<Vec<User>> {
|
|
None
|
|
}
|
|
|
|
/// Gets the current nickname in use. This may be the primary username set in the configuration,
|
|
/// or it could be any of the alternative nicknames listed as well. As a result, this is the
|
|
/// preferred way to refer to the client's nickname.
|
|
pub fn current_nickname(&self) -> &str {
|
|
self.state.current_nickname()
|
|
}
|
|
|
|
/// Sends a [`Command`](../proto/command/enum.Command.html) as this `Client`. This is the
|
|
/// core primitive for sending messages to the server.
|
|
///
|
|
/// # Example
|
|
/// ```no_run
|
|
/// # use irc::client::prelude::*;
|
|
/// # #[tokio::main]
|
|
/// # async fn main() {
|
|
/// # let client = Client::new("config.toml").await.unwrap();
|
|
/// client.send(Command::NICK("example".to_owned())).unwrap();
|
|
/// client.send(Command::USER("user".to_owned(), "0".to_owned(), "name".to_owned())).unwrap();
|
|
/// # }
|
|
/// ```
|
|
pub fn send<M: Into<Message>>(&self, msg: M) -> error::Result<()> {
|
|
self.state.send(msg)
|
|
}
|
|
|
|
/// Sends a CAP END, NICK and USER to identify.
|
|
pub fn identify(&self) -> error::Result<()> {
|
|
// Send a CAP END to signify that we're IRCv3-compliant (and to end negotiations!).
|
|
self.send(CAP(None, END, None, None))?;
|
|
if self.config().password() != "" {
|
|
self.send(PASS(self.config().password().to_owned()))?;
|
|
}
|
|
self.send(NICK(self.config().nickname()?.to_owned()))?;
|
|
self.send(USER(
|
|
self.config().username().to_owned(),
|
|
"0".to_owned(),
|
|
self.config().real_name().to_owned(),
|
|
))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub_state_base!();
|
|
pub_sender_base!();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use std::{collections::HashMap, default::Default, thread, time::Duration};
|
|
|
|
use super::Client;
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
use crate::client::data::User;
|
|
use crate::{
|
|
client::data::Config,
|
|
error::Error,
|
|
proto::{
|
|
command::Command::{Raw, PRIVMSG},
|
|
ChannelMode, IrcCodec, Mode,
|
|
},
|
|
};
|
|
use anyhow::Result;
|
|
use futures::prelude::*;
|
|
|
|
pub fn test_config() -> Config {
|
|
Config {
|
|
owners: vec![format!("test")],
|
|
nickname: Some(format!("test")),
|
|
alt_nicks: vec![format!("test2")],
|
|
server: Some(format!("irc.test.net")),
|
|
channels: vec![format!("#test"), format!("#test2")],
|
|
user_info: Some(format!("Testing.")),
|
|
use_mock_connection: true,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn get_client_value(client: Client) -> String {
|
|
// We sleep here because of synchronization issues.
|
|
// We can't guarantee that everything will have been sent by the time of this call.
|
|
thread::sleep(Duration::from_millis(100));
|
|
client
|
|
.log_view()
|
|
.sent()
|
|
.unwrap()
|
|
.iter()
|
|
.fold(String::new(), |mut acc, msg| {
|
|
// NOTE: we have to sanitize here because sanitization happens in IrcCodec after the
|
|
// messages are converted into strings, but our transport logger catches messages before
|
|
// they ever reach that point.
|
|
acc.push_str(&IrcCodec::sanitize(msg.to_string()));
|
|
acc
|
|
})
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stream() -> Result<()> {
|
|
let exp = "PRIVMSG test :Hi!\r\nPRIVMSG test :This is a test!\r\n\
|
|
:test!test@test JOIN #test\r\n";
|
|
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(exp.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
|
|
client.stream()?.collect().await?;
|
|
// assert_eq!(&messages[..], exp);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_message() -> Result<()> {
|
|
let value = ":irc.test.net 376 test :End of /MOTD command.\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"JOIN #test\r\nJOIN #test2\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_end_motd_with_nick_password() -> Result<()> {
|
|
let value = ":irc.test.net 376 test :End of /MOTD command.\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
nick_password: Some(format!("password")),
|
|
channels: vec![format!("#test"), format!("#test2")],
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NICKSERV IDENTIFY password\r\nJOIN #test\r\n\
|
|
JOIN #test2\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_end_motd_with_chan_keys() -> Result<()> {
|
|
let value = ":irc.test.net 376 test :End of /MOTD command\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
nickname: Some(format!("test")),
|
|
channels: vec![format!("#test"), format!("#test2")],
|
|
channel_keys: {
|
|
let mut map = HashMap::new();
|
|
map.insert(format!("#test2"), format!("password"));
|
|
map
|
|
},
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"JOIN #test\r\nJOIN #test2 password\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_end_motd_with_ghost() -> Result<()> {
|
|
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\
|
|
:irc.test.net 376 test2 :End of /MOTD command.\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
nickname: Some(format!("test")),
|
|
alt_nicks: vec![format!("test2")],
|
|
nick_password: Some(format!("password")),
|
|
channels: vec![format!("#test"), format!("#test2")],
|
|
should_ghost: true,
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NICK test2\r\nNICKSERV GHOST test password\r\n\
|
|
NICK test\r\nNICKSERV IDENTIFY password\r\nJOIN #test\r\nJOIN #test2\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_end_motd_with_ghost_seq() -> Result<()> {
|
|
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\
|
|
:irc.test.net 376 test2 :End of /MOTD command.\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
nickname: Some(format!("test")),
|
|
alt_nicks: vec![format!("test2")],
|
|
nick_password: Some(format!("password")),
|
|
channels: vec![format!("#test"), format!("#test2")],
|
|
should_ghost: true,
|
|
ghost_sequence: Some(vec![format!("RECOVER"), format!("RELEASE")]),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NICK test2\r\nNICKSERV RECOVER test password\
|
|
\r\nNICKSERV RELEASE test password\r\nNICK test\r\nNICKSERV IDENTIFY password\
|
|
\r\nJOIN #test\r\nJOIN #test2\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_end_motd_with_umodes() -> Result<()> {
|
|
let value = ":irc.test.net 376 test :End of /MOTD command.\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
nickname: Some(format!("test")),
|
|
umodes: Some(format!("+B")),
|
|
channels: vec![format!("#test"), format!("#test2")],
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"MODE test +B\r\nJOIN #test\r\nJOIN #test2\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn nickname_in_use() -> Result<()> {
|
|
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "NICK test2\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn ran_out_of_nicknames() -> Result<()> {
|
|
let value = ":irc.pdgn.co 433 * test :Nickname is already in use.\r\n\
|
|
:irc.pdgn.co 433 * test2 :Nickname is already in use.\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
let res = client.stream()?.try_collect::<Vec<_>>().await;
|
|
if let Err(Error::NoUsableNick) = res {
|
|
()
|
|
} else {
|
|
panic!("expected error when no valid nicks were specified")
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
assert!(client
|
|
.send(PRIVMSG(format!("#test"), format!("Hi there!")))
|
|
.is_ok());
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG #test :Hi there!\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_no_newline_injection() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
assert!(client
|
|
.send(PRIVMSG(format!("#test"), format!("Hi there!\r\nJOIN #bad")))
|
|
.is_ok());
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG #test :Hi there!\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_raw_is_really_raw() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
assert!(client
|
|
.send(Raw("PASS".to_owned(), vec!["password".to_owned()]))
|
|
.is_ok());
|
|
assert!(client
|
|
.send(Raw("NICK".to_owned(), vec!["test".to_owned()]))
|
|
.is_ok());
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PASS password\r\nNICK test\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
async fn channel_tracking_names() -> Result<()> {
|
|
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(client.list_channels().unwrap(), vec!["#test".to_owned()]);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
async fn channel_tracking_names_part() -> Result<()> {
|
|
use crate::proto::command::Command::PART;
|
|
|
|
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
|
|
client.stream()?.collect().await?;
|
|
|
|
assert_eq!(client.list_channels(), Some(vec!["#test".to_owned()]));
|
|
// we ignore the result, as soon as we queue an outgoing message we
|
|
// update client state, regardless if the queue is available or not.
|
|
let _ = client.send(PART(format!("#test"), None));
|
|
assert_eq!(client.list_channels(), Some(vec![]));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
async fn user_tracking_names() -> Result<()> {
|
|
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
client.list_users("#test").unwrap(),
|
|
vec![User::new("test"), User::new("~owner"), User::new("&admin")]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
async fn user_tracking_names_join() -> Result<()> {
|
|
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\
|
|
:test2!test@test JOIN #test\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
client.list_users("#test").unwrap(),
|
|
vec![
|
|
User::new("test"),
|
|
User::new("~owner"),
|
|
User::new("&admin"),
|
|
User::new("test2"),
|
|
]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
async fn user_tracking_names_kick() -> Result<()> {
|
|
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\
|
|
:owner!test@test KICK #test test\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
client.list_users("#test").unwrap(),
|
|
vec![User::new("&admin"), User::new("~owner"),]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
async fn user_tracking_names_part() -> Result<()> {
|
|
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n\
|
|
:owner!test@test PART #test\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
client.list_users("#test").unwrap(),
|
|
vec![User::new("test"), User::new("&admin")]
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(not(feature = "nochanlists"))]
|
|
async fn user_tracking_names_mode() -> Result<()> {
|
|
let value = ":irc.test.net 353 test = #test :+test ~owner &admin\r\n\
|
|
:test!test@test MODE #test +o test\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
client.list_users("#test").unwrap(),
|
|
vec![User::new("@test"), User::new("~owner"), User::new("&admin")]
|
|
);
|
|
let mut exp = User::new("@test");
|
|
exp.update_access_level(&Mode::Plus(ChannelMode::Voice, None));
|
|
assert_eq!(
|
|
client.list_users("#test").unwrap()[0].highest_access_level(),
|
|
exp.highest_access_level()
|
|
);
|
|
// The following tests if the maintained user contains the same entries as what is expected
|
|
// but ignores the ordering of these entries.
|
|
let mut levels = client.list_users("#test").unwrap()[0].access_levels();
|
|
levels.retain(|l| exp.access_levels().contains(l));
|
|
assert_eq!(levels.len(), exp.access_levels().len());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "nochanlists")]
|
|
async fn no_user_tracking() -> Result<()> {
|
|
let value = ":irc.test.net 353 test = #test :test ~owner &admin\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert!(client.list_users("#test").is_none());
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_single_soh() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG #test :\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
nickname: Some(format!("test")),
|
|
channels: vec![format!("#test"), format!("#test2")],
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn finger_response() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG test :\u{001}FINGER\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NOTICE test :\u{001}FINGER :test (test)\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn version_response() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG test :\u{001}VERSION\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
&format!(
|
|
"NOTICE test :\u{001}VERSION {}\u{001}\r\n",
|
|
crate::VERSION_STR,
|
|
)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn source_response() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG test :\u{001}SOURCE\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NOTICE test :\u{001}SOURCE https://github.com/aatxe/irc\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn ctcp_ping_response() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG test :\u{001}PING test\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NOTICE test :\u{001}PING test\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn time_response() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG test :\u{001}TIME\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
let val = get_client_value(client);
|
|
assert!(val.starts_with("NOTICE test :\u{001}TIME :"));
|
|
assert!(val.ends_with("\u{001}\r\n"));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn user_info_response() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG test :\u{001}USERINFO\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NOTICE test :\u{001}USERINFO :Testing.\u{001}\
|
|
\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn ctcp_ping_no_timestamp() -> Result<()> {
|
|
let value = ":test!test@test PRIVMSG test \u{001}PING\u{001}\r\n";
|
|
let mut client = Client::from_config(Config {
|
|
mock_initial_value: Some(value.to_owned()),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn identify() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.identify()?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"CAP END\r\nNICK test\r\n\
|
|
USER test 0 * test\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn identify_with_password() -> Result<()> {
|
|
let mut client = Client::from_config(Config {
|
|
nickname: Some(format!("test")),
|
|
password: Some(format!("password")),
|
|
..test_config()
|
|
})
|
|
.await?;
|
|
client.identify()?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"CAP END\r\nPASS password\r\nNICK test\r\n\
|
|
USER test 0 * test\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_pong() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_pong("irc.test.net")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "PONG irc.test.net\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_join() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_join("#test,#test2,#test3")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"JOIN #test,#test2,#test3\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_part() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_part("#test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "PART #test\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_oper() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_oper("test", "test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "OPER test test\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_privmsg() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_privmsg("#test", "Hi, everybody!")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG #test :Hi, everybody!\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_notice() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_notice("#test", "Hi, everybody!")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"NOTICE #test :Hi, everybody!\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_topic_no_topic() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_topic("#test", "")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "TOPIC #test\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_topic() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_topic("#test", "Testing stuff.")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"TOPIC #test :Testing stuff.\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_kill() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_kill("test", "Testing kills.")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"KILL test :Testing kills.\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_kick_no_message() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_kick("#test", "test", "")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "KICK #test test\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_kick() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_kick("#test", "test", "Testing kicks.")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"KICK #test test :Testing kicks.\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_mode_no_modeparams() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_mode("#test", &[Mode::Plus(ChannelMode::InviteOnly, None)])?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "MODE #test +i\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_mode() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_mode(
|
|
"#test",
|
|
&[Mode::Plus(ChannelMode::Oper, Some("test".to_owned()))],
|
|
)?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "MODE #test +o test\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_samode_no_modeparams() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_samode("#test", "+i", "")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "SAMODE #test +i\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_samode() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_samode("#test", "+o", "test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "SAMODE #test +o test\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_sanick() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_sanick("test", "test2")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "SANICK test test2\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_invite() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_invite("test", "#test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(&get_client_value(client)[..], "INVITE test #test\r\n");
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_ctcp() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_ctcp("test", "LINE1\r\nLINE2\r\nLINE3")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG test \u{001}LINE1\u{001}\r\nPRIVMSG test \u{001}LINE2\u{001}\r\nPRIVMSG test \u{001}LINE3\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_action() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_action("test", "tests.")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG test :\u{001}ACTION tests.\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_finger() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_finger("test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG test \u{001}FINGER\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_version() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_version("test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG test \u{001}VERSION\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_source() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_source("test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG test \u{001}SOURCE\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_user_info() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_user_info("test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG test \u{001}USERINFO\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_ctcp_ping() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_ctcp_ping("test")?;
|
|
client.stream()?.collect().await?;
|
|
let val = get_client_value(client);
|
|
println!("{}", val);
|
|
assert!(val.starts_with("PRIVMSG test :\u{001}PING "));
|
|
assert!(val.ends_with("\u{001}\r\n"));
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[cfg(feature = "ctcp")]
|
|
async fn send_time() -> Result<()> {
|
|
let mut client = Client::from_config(test_config()).await?;
|
|
client.send_time("test")?;
|
|
client.stream()?.collect().await?;
|
|
assert_eq!(
|
|
&get_client_value(client)[..],
|
|
"PRIVMSG test \u{001}TIME\u{001}\r\n"
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|