twitch_message 0.1.2

A parser for Twitch.tv chat messages
Documentation
use std::{
    collections::HashMap,
    io::{BufRead, BufReader},
    net::TcpStream,
    time::{Duration, Instant},
};

use twitch_message::{
    encode::{join, privmsg, register, reply, Encodable, Encode, ALL_CAPABILITIES},
    messages::{Message, MessageKind, Privmsg},
    parse, PingTracker, TWITCH_IRC_ADDRESS,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    simple_env_load::load_env_from([".example.env", ".secrets.env"]);

    fn get(key: &str) -> Result<String, String> {
        std::env::var(key).map_err(|_| format!("please set {key} in .example.env"))
    }

    let name = get("TWITCH_USER_NAME")?;
    let pass = get("TWITCH_OAUTH_TOKEN")?;

    let commands = Commands::default()
        .with("~hello", Bot::hello)
        .with("~uptime", Bot::uptime)
        .with("~github", Bot::send_github);

    eprintln!("connecting");
    Bot::connect(&name, &pass, "museun", commands)?.run();
    eprintln!("disconnected");

    Ok(())
}

struct Bot {
    stream: BufReader<TcpStream>,
    commands: Commands,
    start: Instant,
    ping: PingTracker,
}

impl Bot {
    fn connect(
        name: &str,
        pass: &str,
        channel: &str,
        commands: Commands,
    ) -> Result<Self, Box<dyn std::error::Error>> {
        let mut stream = TcpStream::connect(TWITCH_IRC_ADDRESS)?;
        stream.encode_msg(register(name, pass, ALL_CAPABILITIES))?;
        let mut stream = BufReader::new(stream);

        let ping = PingTracker::new(Duration::from_secs(5 * 60));
        let mut buf = String::new();
        loop {
            let msg = Self::read_message(&mut buf, &mut stream, &ping);
            if let MessageKind::Ready = msg.kind {
                break;
            }
        }

        eprintln!("connected. joining {channel}");
        join(channel).encode(stream.get_ref()).unwrap();

        Ok(Self {
            stream,
            commands,
            start: Instant::now(),
            ping,
        })
    }

    fn run(mut self) {
        let mut buf = String::with_capacity(1024);

        loop {
            let msg = Self::read_message(&mut buf, &mut self.stream, &self.ping);
            if let Some(pm) = msg.as_typed_message::<Privmsg>() {
                eprintln!(
                    "[{channel}] {sender}: {data}",
                    channel = pm.channel,
                    sender = pm.sender,
                    data = pm.data
                );

                if let Some(func) = self.commands.map.get(&*pm.data) {
                    (func)(&self, &pm)
                }
            }
        }
    }

    fn read_message<'a, T>(
        buf: &'a mut String,
        reader: &mut BufReader<T>,
        pt: &PingTracker,
    ) -> Message<'a>
    where
        T: std::io::Read + std::io::Write,
        for<'i> &'i T: std::io::Read + std::io::Write,
    {
        buf.clear();
        let pos = reader.read_line(buf).unwrap();
        let msg = parse(&buf[..pos]).unwrap().message;

        pt.update(&msg);
        if let Some(pong) = pt.should_pong() {
            pong.encode(reader.get_ref()).unwrap()
        }

        msg
    }
}

impl Bot {
    fn hello(&self, pm: &Privmsg<'_>) {
        privmsg(&pm.channel, &format!("hello: {}", pm.sender))
            .encode(self.stream.get_ref())
            .unwrap();
    }

    fn uptime(&self, pm: &Privmsg<'_>) {
        privmsg(
            &pm.channel,
            &format!("uptime is {:.2?}", self.start.elapsed()),
        )
        .encode(self.stream.get_ref())
        .unwrap();
    }

    fn send_github(&self, pm: &Privmsg<'_>) {
        reply(
            pm.msg_id().unwrap(),
            &pm.channel,
            "https://github.com/museun/twitch_message",
        )
        .encode(self.stream.get_ref())
        .unwrap();
    }
}

type Handler = for<'a> fn(&Bot, &Privmsg<'a>);

#[derive(Default)]
struct Commands {
    map: HashMap<String, Handler>,
}

impl Commands {
    fn with(mut self, command: impl ToString, func: Handler) -> Self {
        self.map.insert(command.to_string(), func);
        self
    }
}