Skip to content

Instantly share code, notes, and snippets.

@Lokathor
Last active April 25, 2018 03:23
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lokathor/133eeacad79e86b0229900004ec94e53 to your computer and use it in GitHub Desktop.
Save Lokathor/133eeacad79e86b0229900004ec94e53 to your computer and use it in GitHub Desktop.
A demo of an IRC bot that can asynchronously send and receive messages. No need for any fancy external crates or frameworks.
// This demo program is placed into the Public Domain.
use std::net::TcpStream;
use std::io::{Read, Write};
use std::string::String;
use std::thread;
use std::time::Duration;
/// We can expand on this later, but for now we just use an alias.
type IRCMessage = String;
/// The byte for '\r'.
const R: u8 = b'\r';
/// The byte for '\n'.
const N: u8 = b'\n';
/// Parses any possible messages out of the buffer and spare data. Returns a
/// vector of the messages obtained. After this is done, any spare data will
/// be left in spare, and the entire buffer will be available for overwriting.
fn parse_irc_messages(buffer: &Vec<u8>, count: usize, spare: &mut Vec<u8>) -> Vec<IRCMessage> {
let mut results: Vec<String> = Vec::new();
// dump the incoming data into our "spare" space so that we can focus our
// attention all in one place. We must be sure to only dump in as many
// bytes as we were told to, so we can't just use "spare.append(buffer);"
spare.extend_from_slice(&buffer[..count]);
let mut i = 0;
while spare.len() > 0 && i < spare.len() - 1 {
// Is 'i' pointed to the first byte of our two byte seperator?
if spare[i] == R && spare[i + 1] == N {
// Grab out the message.
let temp: Vec<u8> = spare.drain(..i).collect();
// We hope that it's utf8, but of course it might not be, and this
// will convert any invalid byte sequences into U+FFFD.
results.push(String::from_utf8_lossy(&temp).to_string());
// Also drain out the RN that's now at the start of spare
spare.drain(..2);
// and, of course, i must be reset.
i = 0;
} else {
i += 1;
}
}
results
}
fn main() {
let mut stream_in = TcpStream::connect("irc.freenode.net:6667")
.expect("Couldn't connect to the IRC server.");
let mut stream_out = stream_in.try_clone()
.expect("Couldn't clone the socket.");
// The IRC Standard suggests that messages be no longer than 512,
// but sometimes they will be anyway, #yolo
let mut buffer = vec![0; 512];
let mut spare_data = Vec::new();
let (in_send, in_recv) = std::sync::mpsc::channel::<IRCMessage>();
let (out_send, out_recv) = std::sync::mpsc::channel::<IRCMessage>();
let worker_out_send = out_send.clone();
let main_out_send = out_send.clone();
let in_thread = thread::spawn(move || loop {
let count = stream_in.read(&mut buffer)
.expect("The socket seems to have closed.");
if count == 0 {
println!("No bytes read, terminating inbound.");
break;
}
let messages = parse_irc_messages(&buffer, count, &mut spare_data);
for message in messages {
in_send.send(message)
.expect("Couldn't transfer messages to the worker thread.");
}
});
let worker_thread = thread::spawn(move || loop {
for message in in_recv.iter() {
println!("I {}", message);
// This is only a demo, but we want to at least respond to PING
// commands so that we don't get disconnected.
if message.starts_with("PING") {
let mut pong_message = format!("PONG ");
pong_message.push_str(&message[5..]);
worker_out_send.send(pong_message)
.expect("Worker couldn't send to the Outbound thread.");
}
}
});
let out_thread = thread::spawn(move || loop {
for outbound in out_recv.iter() {
println!("O {}", outbound);
stream_out.write_all(&outbound.into_bytes())
.expect("Couldn't send on the socket.");
stream_out.write_all(&[R, N])
.expect("Couldn't send on the socket.");
}
});
// Give it a moment to start up, and then send our openers.
thread::sleep(Duration::new(1, 0));
main_out_send.send(format!("NICK farmbotirc"))
.expect("Main couldn't sent to the outbound channel.");
main_out_send.send(format!("USER farmbotirc 8 * :farmbotirc the bot"))
.expect("Main couldn't sent to the outbound channel.");
main_out_send.send(format!("JOIN #lokathor"))
.expect("Main couldn't sent to the outbound channel.");
// Wait a while and then say some stuff in the channel unprompted. This is
// just an example, a complete bot could have a timer thread or whatever
// you like that's producing output. The only important part for this
// example is that output can be produced asynchronously rather than
// exclusively in response to a message from the server.
thread::sleep(Duration::new(20, 0));
main_out_send.send(format!("PRIVMSG #lokathor :Hello hello."))
.expect("Main couldn't sent to the outbound channel.");
// We want to join in this order so that if for some reason our inbound
// data feed cuts but we're still able to send data, we'll still process
// all pending messages and return our results. It's more likely that no
// one is listening at that point though, and that a crash in one thread
// will nearly instantly cause the other threads to all crash as well.
in_thread.join().unwrap();
worker_thread.join().unwrap();
out_thread.join().unwrap();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment