Last active
April 25, 2018 03:23
-
-
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 file contains hidden or 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
// 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