# twitchchat
[![Documentation][docs_badge]][docs]
[![Crates][crates_badge]][crates]
[![Actions][actions_badge]][actions]
This crate provides a way to interact with [Twitch]'s chat.
Along with parse messages as Rust types, it provides methods for sending messages.
## Demonstration
See [examples/demo.rs][demo] for a larger example
## Configuration features
default | enables `async` and `tokio_native_tls` (the default)
async | enables [`tokio`](https://crates.io/crates/tokio) support (and all of the _async_ methods/types)
tokio_native_tls | uses [`native_tls`](https://crates.io/crates/native-tls) _(OpenSSL, SChannel, SecureTransport)_ for TLS
tokio_rustls | uses [`rusttls`](https://crates.io/crates/rustls) for TLS
serde | enables [`serde`](https://crates.io/crates/serde) Serialize/Deserialize on most of the types
## Connecting to Twitch
This crate allows you connect to Twitch with a TLS stream, or without one.
### With TLS
**NOTE** the async blocks are here so the doctests will work, you'll likely have it in a 'larger' async context
> Connect with a `UserConfig`:
```rust no_run
let user_config = twitchchat::UserConfig::builder().build().unwrap();
let conn = async { twitchchat::native_tls::connect(&user_config).await.unwrap() };
```
> Connect with default capabilities using just a username and oauth token:
```rust no_run
let my_oauth = std::env::var("MY_TWITCH_OAUTH").unwrap();
let conn = async { twitchchat::native_tls::connect_easy("my_name", &my_oauth).await.unwrap() };
```
### Without TLS (an unsecure plain-text connection)
> Connect with a UserConfig:
```rust no_run
let user_config = twitchchat::UserConfig::builder().build().unwrap();
let conn = async { twitchchat::connect_no_tls(&user_config).await.unwrap() };
```
> Connect with default capabilities using just a username and oauth token:
```rust no_run
let my_oauth = std::env::var("MY_TWITCH_OAUTH").unwrap();
let conn = async { twitchchat::connect_easy_no_tls("my_name", &my_oauth).await.unwrap() };
```
You can even connect with an ***anonymous*** users that _doesn't_ require an OAuth token.
This will let you join and read messages from channels, but not write them.
Also you'd be limited in what sort of metadata you can receive.
> With TLS
```rust no_run
let (nick, pass) = twitchchat::ANONYMOUS_LOGIN;
let conn = async { twitchchat::native_tls::connect_easy(nick, pass).await.unwrap() };
```
> Without TLS
```rust no_run
let (nick, pass) = twitchchat::ANONYMOUS_LOGIN;
let conn = async { twitchchat::connect_easy_no_tls(nick, pass).await.unwrap() };
```
---
A ***synchronous*** 'connect' is provided for completionist sake.
This crate is intended to be used with async types, but the `Encoder` and `decode` methods will work without them.
Disabling `async` will only give you these types.
See [`twitchchat::sync`](https://docs.rs/twitchchat/latest/twitchchat/sync/index.html) for synchronous types.
```rust no_run
// make a user config, builder lets you configure it.
let user_config = twitchchat::UserConfig::builder().build().unwrap();
let conn: std::net::TcpStream = twitchchat::sync::connect(&user_config).unwrap();
```
Or
```rust no_run
let my_oauth = std::env::var("MY_TWITCH_OAUTH").unwrap();
let conn: std::net::TcpStream = twitchchat::sync::connect_easy("my_name", &my_oauth).unwrap();
```
---
## Parsing messages
Parsing is done with the `decode(&str)` or `decode_one(&str)` methods. Twitch (IRC) messages are delimited by `CRLF [0xD, 0xA]`
> Parsing potentially many messages.
```rust
let input = "@badge-info=subscriber/8;color=#59517B;tmi-sent-ts=1580932171144;user-type= :tmi.twitch.tv USERNOTICE #justinfan1234\r\n";
for msg in twitchchat::decode(&input).flatten() {
assert_eq!(msg.command, "USERNOTICE");
assert_eq!(msg.args, "#justinfan1234");
assert_eq!(msg.arg(0), Some("#justinfan1234"));
use twitchchat::color::{Color, TwitchColor, RGB};
assert_eq!(msg.tags.get_parsed::<_, Color>("color"), Some(Color {
kind: TwitchColor::Turbo,
rgb: RGB(89, 81, 123)
}));
}
```
> Parsing a single message
```rust
let input =
":tmi.twitch.tv PING 1234567\r\n:museun!museun@museun.tmi.twitch.tv JOIN #museun\r\n";
let (d, message) = twitchchat::decode_one(input).unwrap();
assert!(d > 0);
assert_eq!(message.command, "PING");
let (i, message) = twitchchat::decode_one(&input[d..]).unwrap();
assert_eq!(i, 0);
assert_eq!(message.command, "JOIN");
```
> Parsing a `decode::Message<'a>` into a _subtype_
```rust
use twitchchat::Parse as _;
use twitchchat::{decode, messages};
let input = ":museun!museun@museun.tmi.twitch.tv JOIN #some_test_channel\r\n";
let msg: decode::Message<'_> = twitchchat::decode(input).next().unwrap().unwrap();
let join: messages::Join<'_> = messages::Join::parse(&msg).unwrap();
assert_eq!(join.channel, "#some_test_channel");
assert_eq!(join.name, "museun");
```
> Parsing a `decode::Message<'a>` into an enum of _all_ possible messages
```rust
use twitchchat::Parse as _;
use twitchchat::{decode, messages};
let input = ":museun!museun@museun.tmi.twitch.tv JOIN #some_test_channel\r\n";
let msg: decode::Message<'_> = twitchchat::decode(input).next().unwrap().unwrap();
match messages::AllCommands::parse(&msg).unwrap() {
messages::AllCommands::Join(join) => {
assert_eq!(join.channel, "#some_test_channel");
assert_eq!(join.name, "museun");
}
_ => panic!("not a join message")
}
```
> Taking ownership of a parsed message
```rust
use twitchchat::Parse as _;
use twitchchat::AsOwned as _;
use twitchchat::{decode, messages};
let input = ":museun!museun@museun.tmi.twitch.tv JOIN #some_test_channel\r\n";
let msg: decode::Message<'_> = twitchchat::decode(input).next().unwrap().unwrap();
let owned: decode::Message<'static> = msg.as_owned();
assert_eq!(owned.command, "JOIN");
let join = messages::Join::parse(&msg).unwrap();
let join_owned: messages::Join<'static> = join.as_owned();
assert_eq!(join_owned.channel, "#some_test_channel");
assert_eq!(join_owned.name, "museun");
```
> Getting data out of the _tags_
```rust
use twitchchat::Parse as _;
use twitchchat::{decode, messages};
let input = "@badge-info=subscriber/8;color=#59517B;tmi-sent-ts=1580932171144;user-type= :tmi.twitch.tv USERNOTICE #justinfan1234\r\n";
let msg = decode(&input).next().unwrap().unwrap();
let user_notice = messages::UserNotice::parse(&msg).unwrap();
assert_eq!(
user_notice.color().unwrap(),
"#59517B".parse::<twitchchat::color::Color>().unwrap()
);
let ts = user_notice.tags.get("tmi-sent-ts").unwrap();
assert_eq!(ts, "1580932171144");
let ts = user_notice
.tags
.get_parsed::<_, u64>("tmi-sent-ts")
.unwrap();
assert_eq!(ts, 1580932171144);
```
## Event dispatching/streams
Along with connecting to twitch and parsing strings, this crate can do that for you and provide you typed asynchronous Streams for events you're interested in.
```rust no_run
use twitchchat::{messages, events};
// for working with streams, the futures::stream::StreamExt trait will also work.
use tokio::stream::StreamExt as _;
/// make a new event dispatcher
let dispatcher = twitchchat::Dispatcher::new();
// you can subscribe to an event
// this'll return a Stream which'll produce a `messages` type
// for example, events::Join will produce messages::Join<'static>
let mut joins = dispatcher.subscribe::<events::Join>();
// you can subscribe to the same event multiple times
let mut more_joins = dispatcher.subscribe::<events::Join>();
// you can subscribe to 'All' to get an enum of all possible events
let mut all = dispatcher.subscribe::<events::All>();
// and you can subscribe to 'Raw' to get the 'raw' decode::Message type
let mut raw = dispatcher.subscribe::<events::Raw>();
// dropping these streams will 'unsubscribe' them
let fut = async move {
while let Some(msg) = joins.next().await {
// msg is an Arc<messages::Join<'static>> here
// so if you reborrow it, you can temporarily get rid of the arc;
let msg: &messages::Join<'static> = &*msg;
}
// returning from this task will also unsubscribe this event stream
};
// lets be fancy and use a select over stream
let fut = async move {
loop {
tokio::select! {
Some(join) = &mut more_joins.next() => {}
Some(all) = &mut all.next() => {
match &*all {
messages::AllCommands::Ping(ping) => {}
_ => {}
}
}
Some(raw) = &mut raw.next() => {}
else => { break }
}
}
};
```
> Finally, writing (encoding) messages.
```rust
let (_runner, mut control) = twitchchat::Runner::new(twitchchat::Dispatcher::new());
let mut ctrl_clone = control.clone();
let writer = control.writer();
async move {
writer.privmsg("#museun", "hello world!").await.unwrap();
ctrl_clone.writer().join("#museun").await.unwrap()
};
let writer = control.writer();
let mut w1 = writer.clone();
let mut w2 = w1.clone();
async move {
w1.join("foo").await.unwrap();
w2.part("foo").await.unwrap();
};
```
## You can use the `AsyncEncoder` and `Encoder` types to wrap io types.
> Async
```rust
use std::io::Cursor;
use twitchchat::encode::AsyncEncoder;
async {
let cursor = Cursor::new(vec![]);
let mut encoder = AsyncEncoder::new(cursor);
encoder.privmsg("#museun", "hello world!").await.unwrap();
{ let cursor: &Cursor<Vec<u8>> = encoder.inner(); }
{ let cursor: &mut Cursor<Vec<u8>> = encoder.inner_mut(); }
let cursor: Cursor<Vec<u8>> = encoder.into_inner();
};
```
> Sync
```rust
use std::io::Cursor;
use twitchchat::encode::Encoder;
let cursor = Cursor::new(vec![]);
let mut encoder = Encoder::new(cursor);
encoder.privmsg("#museun", "hello world!").unwrap();
{ let cursor: &Cursor<Vec<u8>> = encoder.inner(); }
{ let cursor: &mut Cursor<Vec<u8>> = encoder.inner_mut(); }
let cursor: Cursor<Vec<u8>> = encoder.into_inner();
```
## Putting it together, a simple "bot"
```rust no_run
use tokio::stream::StreamExt as _;
use twitchchat::{events, messages, Control, Dispatcher, IntoChannel, Runner, Status, Writer};
fn get_user_pass() -> (String, String) {
(std::env::var("TWITCH_NICK").unwrap(), std::env::var("TWITCH_PASS").unwrap())
}
fn get_channel() -> String {
std::env::var("TWITCH_CHANNEL").unwrap()
}
struct Bot {
// you can store the writer (and clone it)
writer: Writer,
// and you can store/clone the Control
control: Control,
start: std::time::Instant,
}
impl Bot {
async fn run(mut self, dispatcher: Dispatcher, channel: impl IntoChannel) {
// subscribe to the events we're interested in
let mut events = dispatcher.subscribe::<events::Privmsg>();
// and wait for a specific event (blocks the current task)
let ready = dispatcher.wait_for::<events::IrcReady>().await.unwrap();
eprintln!("connected! our name is: {}", ready.nickname);
// and then join a channel
eprintln!("joining our channel");
self.writer.join(channel).await.unwrap();
// and then our 'main loop'
while let Some(msg) = events.next().await {
if !self.handle(&*msg).await {
return;
}
}
}
async fn handle(&mut self, msg: &messages::Privmsg<'_>) -> bool {
match &*msg.data {
"!hello" => {
let resp = format!("hello {}!", msg.name);
self.writer.privmsg(&msg.channel, &resp).await.unwrap();
}
"!uptime" => {
let dur = std::time::Instant::now() - self.start;
let resp = format!("I've been running for.. {:.2?}.", dur);
self.writer.privmsg(&msg.channel, &resp).await.unwrap();
}
"!quit" => {
// this'll stop the runner (causing its future to return Ok(Status::Canceled))
self.control.stop();
return false; // to stop the 'Bot'
}
_ => {}
};
true // to keep the 'Bot' running
}
}
#[tokio::main]
async fn main() {
let dispatcher = Dispatcher::new();
let (mut runner, mut control) = Runner::new(dispatcher.clone());
// make a bot and get a future to its main loop
let bot = Bot {
// just to show you can store it
writer: control.writer().clone(),
// but you probably want to store the control instead
control,
start: std::time::Instant::now(),
}
.run(dispatcher, get_channel());
// connect to twitch
// the runner requires a 'connector' factory so reconnect support is possible
let connector = twitchchat::Connector::new(|| async move {
let (user, pass) = get_user_pass();
twitchchat::native_tls::connect_easy(&user, &pass).await
});
// and run the dispatcher/writer loop
let done = runner.run_to_completion(connector);
// and select over our two futures
tokio::select! {
// wait for the bot to complete
_ = bot => { eprintln!("done running the bot") }
// or wait for the runner to complete
status = done => {
match status {
Ok(Status::Canceled) => { eprintln!("runner was canceled") }
Ok(Status::Eof) => { eprintln!("got an eof, exiting") }
Ok(Status::Timeout) => { eprintln!("client timed out, exiting") }
Err(err) => { eprintln!("error running: {}", err) }
}
}
}
}
```
## License
`twitchchat` is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See [LICENSE-APACHE][APACHE] and [LICENSE-MIT][MIT] for details.
[docs_badge]: https://docs.rs/twitchchat/badge.svg
[docs]: https://docs.rs/twitchchat
[crates_badge]: https://img.shields.io/crates/v/twitchchat.svg
[crates]: https://crates.io/crates/twitchchat
[actions_badge]: https://github.com/museun/twitchchat/workflows/Rust/badge.svg
[actions]: https://github.com/museun/twitchchat/actions
[demo]: ./examples/demo.rs
[APACHE]: ./LICENSE-APACHE
[MIT]: ./LICENSE-MIT
[Twitch]: https://dev.twitch.tv