Last active
September 23, 2017 07:38
-
-
Save resilar/d8ba3a22aa0500f2218c39e7f9161eaa to your computer and use it in GitHub Desktop.
babby's first irc client in rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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