use std::collections::HashSet;
use std::io::ErrorKind;
use std::net::TcpStream;
use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
use std::thread;
use std::time::Duration;
use tungstenite::stream::MaybeTlsStream;
use tungstenite::Message as WsMsg;
const IRC_URL: &str = "wss://irc-ws.chat.twitch.tv:443";
const READ_TIMEOUT: Duration = Duration::from_millis(400);
pub type Send = (String, String);
pub fn spawn(user: String, oauth: String) -> Sender<Send> {
let (tx, rx) = mpsc::channel();
thread::spawn(move || run(user, oauth, rx));
tx
}
fn run(user: String, oauth: String, rx: Receiver<Send>) {
let mut backoff = Duration::from_secs(1);
loop {
match session(&user, &oauth, &rx) {
End::Closed => return, End::Dropped => {
thread::sleep(backoff);
backoff = (backoff * 2).min(Duration::from_secs(15));
}
}
}
}
enum End {
Closed,
Dropped,
}
fn session(user: &str, oauth: &str, rx: &Receiver<Send>) -> End {
let mut ws = match tungstenite::connect(IRC_URL) {
Ok((ws, _)) => ws,
Err(_) => return End::Dropped,
};
if let Some(sock) = tcp_of(&mut ws) {
let _ = sock.set_read_timeout(Some(READ_TIMEOUT));
}
if ws
.send(WsMsg::Text(format!("PASS oauth:{oauth}\r\n")))
.is_err()
{
return End::Dropped;
}
if ws
.send(WsMsg::Text(format!("NICK {}\r\n", user.to_lowercase())))
.is_err()
{
return End::Dropped;
}
let mut joined: HashSet<String> = HashSet::new();
loop {
match ws.read() {
Ok(WsMsg::Text(t)) => {
if t.as_str().starts_with("PING") {
let _ = ws.send(WsMsg::Text("PONG :tmi.twitch.tv\r\n".into()));
}
}
Ok(WsMsg::Ping(p)) => {
let _ = ws.send(WsMsg::Pong(p));
}
Ok(WsMsg::Close(_)) => return End::Dropped,
Ok(_) => {}
Err(tungstenite::Error::Io(e))
if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
Err(_) => return End::Dropped,
}
loop {
match rx.try_recv() {
Ok((channel, text)) => {
let chan = channel.to_lowercase();
if joined.insert(chan.clone())
&& ws.send(WsMsg::Text(format!("JOIN #{chan}\r\n"))).is_err()
{
return End::Dropped;
}
let line = format!("PRIVMSG #{chan} :{text}\r\n");
if ws.send(WsMsg::Text(line)).is_err() {
return End::Dropped;
}
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => return End::Closed,
}
}
}
}
fn tcp_of(ws: &mut tungstenite::WebSocket<MaybeTlsStream<TcpStream>>) -> Option<&TcpStream> {
match ws.get_mut() {
MaybeTlsStream::Plain(s) => Some(s),
MaybeTlsStream::Rustls(s) => Some(s.get_ref()),
_ => None,
}
}