Skip to content

Instantly share code, notes, and snippets.

@resilar
Last active September 23, 2017 07:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save resilar/d8ba3a22aa0500f2218c39e7f9161eaa to your computer and use it in GitHub Desktop.
Save resilar/d8ba3a22aa0500f2218c39e7f9161eaa to your computer and use it in GitHub Desktop.
babby's first irc client in rust
use irc::command::{IrcCommand};
use futures::stream::{self, Stream};
use futures::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use futures::{future, Future};
use tokio_core::net::TcpStream;
use tokio_core::reactor::Handle;
use tokio_io::{io, AsyncRead};
use std::io::{Error, ErrorKind, BufReader};
use std::net::{SocketAddr};
use std::iter;
use regex::Regex;
#[derive(Clone, Debug)]
pub struct IrcConfig {
pub server: SocketAddr,
pub channel: String,
pub nick: String,
pub admin: Regex,
pub trigger: String
}
#[derive(Debug)]
pub enum IrcError {
ConnectionError
}
pub type IrcClient = (UnboundedSender<String>, UnboundedReceiver<String>);
pub fn connect(config: IrcConfig, handle: Handle) -> Box<Future<Item = IrcClient, Error = IrcError>> {
let connector = TcpStream::connect(&config.server, &handle)
.map_err(|e| {
eprintln!("TcpStream::connect() error: {}", e);
IrcError::ConnectionError
})
.and_then(move |stream| {
let (tcprx, tcptx) = stream.split();
let (tx1, rx1) = mpsc::unbounded::<String>(); // writer
let (tx2, rx2) = mpsc::unbounded::<String>(); // reader
let client: IrcClient = (tx1.clone(), rx2);
let socket_writer = rx1.fold(tcptx, |writer, msg| {
eprintln!(">{}", msg.trim_right());
io::write_all(writer, msg.into_bytes())
.map(|(writer, _)| writer).map_err(|_| ())
}).map(|_| ());
let nick = config.nick.clone();
tx1.unbounded_send(IrcCommand::Nick(nick.clone()).serialize()).unwrap();
tx1.unbounded_send(IrcCommand::User(nick.clone(), nick).serialize()).unwrap();
let reader = BufReader::new(tcprx);
let iter = stream::iter_ok::<_, Error>(iter::repeat(()));
let socket_reader = iter.fold(reader, move |reader, _| {
let (mut tx1, mut tx2) = (tx1.clone(), tx2.clone());
let config = config.clone();
io::read_until(reader, b'\n', Vec::new()).and_then(move |(reader, vec)| {
if vec.len() != 0 {
if let Ok(line) = String::from_utf8(vec) {
eprintln!("<{}", line.trim_right());
irc_handler(config, &line, &mut tx1, &mut tx2);
} else {
eprintln!("bad utf8 from irc server :(");
}
Ok(reader)
} else {
tx2.unbounded_send(String::new()).expect("shutdown error");
Err(Error::new(ErrorKind::BrokenPipe, "broken pipe"))
}
})
}).map_err(|_| ()).map(|_| ());
handle.spawn(socket_reader.select(socket_writer).then(|_| Ok(())));
future::ok::<IrcClient, IrcError>(client)
});
Box::new(connector)
}
fn irc_handler(config: IrcConfig, line: &str,
irc_tx: &mut UnboundedSender<String>,
out_tx: &mut UnboundedSender<String>) {
let (cmd, who) = IrcCommand::deserialize(&line);
let reply = match cmd {
IrcCommand::Ping(payload) => {
Some(IrcCommand::Pong(payload).serialize())
},
IrcCommand::BadNick(why) => {
eprintln!("bad nick: {}", why);
out_tx.unbounded_send(String::new()).expect("broken pipe");
Some(String::new())
},
IrcCommand::PrivMsg(channel, msg) => {
if channel == config.channel && msg.starts_with(&config.trigger)
&& config.admin.is_match(&who.unwrap_or("stupid".to_string())) {
out_tx
.unbounded_send(msg[config.trigger.len()..].to_string())
.expect("broken pipe");
}
None
},
IrcCommand::NamesEnd() => {
let hello = format!("hello {}", config.channel);
Some(IrcCommand::PrivMsg(config.channel.clone(), hello).serialize())
},
IrcCommand::MotdEnd() => {
Some(IrcCommand::Join(config.channel).serialize())
},
_ => None
};
if let Some(msg) = reply {
irc_tx.unbounded_send(msg).expect("broken irc connection");
}
}
pub mod command {
#[derive(Debug, Clone, PartialEq)]
pub enum IrcCommand {
Generic(String), // any unrecognized message
Ping(String), // ping + payload
Pong(String), // pong + payload
Nick(String), // nick
BadNick(String), // bad nick reason
User(String, String), // username, realname
Join(String), // join channel
PrivMsg(String, String), // message (recipient, message)
Part(String, String), // channel, reason
Quit(String), // reason
NamesEnd(), // end of names list
MotdEnd() // end of MOTD
}
impl IrcCommand {
pub fn serialize(&self) -> String {
use self::IrcCommand::*;
let msg = match *self {
Generic(ref s) => s.clone(),
Ping(ref p) => format!("PING {}\r\n", p),
Pong(ref p) => format!("PONG {}\r\n", p),
Nick(ref n) => format!("NICK {}\r\n", n),
User(ref u, ref r) => format!("USER {} hostname.wtf servername.wtf :{}\r\n", u, r),
Join(ref c) => format!("JOIN {}\r\n", c),
PrivMsg(ref c, ref m) => format!("PRIVMSG {} :{}\r\n", c, m),
Part(ref c, ref r) => format!("PART {} :{}\r\n", c, r),
Quit(ref r) => format!("QUIT {}\r\n", r),
_ => panic!("cant serialize {:?}", *self)
};
msg
}
pub fn deserialize(msg: &str) -> (IrcCommand, Option<String>) {
let mut source: Option<String> = None;
let mut words: Vec<&str> = msg.split_whitespace().collect();
if !words.is_empty() && words[0].chars().next() == Some(':') {
source = Some(words[0][1..].to_string());
words.remove(0);
}
let cmd = match words[0] {
"PING" => IrcCommand::Ping(words[1..].join(" ")),
"PONG" => IrcCommand::Pong(words[1..].join(" ")),
"JOIN" => IrcCommand::Join(words[1].to_string()),
"PRIVMSG" => {
let message = msg[msg[1..].find(':').unwrap()+2..].to_string();
IrcCommand::PrivMsg(words[1].to_string(), message)
}
"366" => IrcCommand::NamesEnd(),
"376" | "422" => IrcCommand::MotdEnd(),
"433" => IrcCommand::BadNick(words[1..].join(" ")),
_ => IrcCommand::Generic(msg.to_string())
};
return (cmd, source);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment