1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*!
# A simple "connection-less" twitch chat crate
This crate simply read lines from an [`std::io::Read`](https://doc.rust-lang.org/std/io/trait.Read.html) and produces data types for the corresponding messages, and takes an [`std::io::Write`](https://doc.rust-lang.org/std/io/trait.Write.html) which can produce messages that twitch understands.

# 'Theory' of operation
First, by creating a [`Client`](./struct.Client.html) from a Read/Write pair (such as a cloned TcpStream) then calling [`Client::register`](./struct.Client.html#method.register) with a filled-out [`UserConfig`](./userconfig/struct.UserConfig.html) will connect you to Twitch. Once connected, you can [`Client::wait_for_ready`](./struct.Client.html#method.wait_for_ready) and the client will read **(blocking)** until Twitch sends a [`GlobalUserState`](./commands/struct.GlobalUserState.html) message, which it'll fill out a [`LocalUser`](./struct.LocalUser.html) with various information.

Once connected, you can
- use [`Writer::join`](./struct.Writer.html#method.join) to join a
channel.
- use [`Client::on`](./struct.Client.html#method.on) to set up a
message filter.
- use [`Client::read_message`](./struct.Client.html#method.read_message)
to read a message (and pump the filters).
- or do various [*other things*](./struct.Writer.html#method.host)

# Thread-safe writing:
Use [`Client::writer`](./struct.Client.html#method.writer) to get a clonable, thread-safe [`Writer`](./struct.Writer.html) that is used to writer messages to the `Client`.

# Message filters, and why blocking in them is a bad idea
The client provides a very simplistic callback registration system

To use it, you simply just `register` a closure with the client via its
[`Client::on`](./struct.Client.html#method.on) method. It uses the type of the closures argument, one of [*these*](./commands/index.html#structs) to create a filter

It also gives you a clone of the [`Writer`](./struct.Writer.html) so you don't need to move one into the closure.

When [`Client::read_message`](./struct.Client.html#method.read_message) is called, it'll check these filters and send a clone of the requested message to the callback. Because it does this on same thread as the [`Client::read_message`](./struct.Client.html#method.read_message) call, you can lock up the system by simplying diverging.

The client is thread safe, and clonable so one could call [`Client::read_message`](./struct.Client.html#method.read_message) with ones own sychronization scheme to allow for a simplistic thread pool, but its best just to send the message off to a channel elsewhere


# A simple example
```no_run
use std::net::TcpStream;
use twitchchat::{commands::PrivMsg, Capability, Client, Writer, SyncReadAdapter};
use twitchchat::{TWITCH_IRC_ADDRESS, UserConfig};
# fn main() {
// create a simple TcpStream
let read = TcpStream::connect(TWITCH_IRC_ADDRESS).expect("to connect");
let write = read
    .try_clone()
    .expect("must be able to clone the tcpstream");

// your password and your nickname
// the twitch oauth token must be prefixed with `oauth:your_token_here`
let (pass, nick) = (std::env::var("MY_TWITCH_OAUTH_TOKEN").unwrap(), "my_name");
let config = UserConfig::builder()
                .token(pass)
                .nick(nick)
                .membership()    // this enables the membership CAP
                .commands()      // this enables the commands CAP
                .tags()          // this enables the tags CAP
                .build()
                .unwrap();

// a sync read adapter (for wrapping std::io::Read into something the client will use)
let read = SyncReadAdapter::new(read);

// client takes a ReadAdapter and an std::io::Write
let mut client = Client::new(read, write);

// register with the user configuration
client.register(config).unwrap();

// wait for everything to be ready (blocks)
let user = client.wait_for_ready().unwrap();
println!(
    "connected with {} (id: {}). our color is: {}",
    user.display_name.unwrap(),
    user.user_id,
    user.color.unwrap_or_default()
);

// when we receive a commands::PrivMsg print out who sent it, and the message
// this can be done at any time, but its best to do it early
client.on(|msg: PrivMsg, _: Writer<_>| {
    // this prints out name: msg
    let name = msg.display_name().unwrap_or_else(|| msg.user());
    println!("{}: {}", name, msg.message())
});

let w = client.writer();
// join a channel
w.join("museun").unwrap();

// sends a message to the channel
w.send("museun", "VoHiYo").unwrap();

// blocks the thread, but any callbacks set in the .on handlers will get their messages
client.run();
# }
```

# TestStream
[`TestStream`](./helpers/struct.TestStream.html) is a simple TcpStream-like mock.

It lets you inject/read its internal buffers, allowing you to easily write
unit tests for the [`Client`](./struct.Client.html)

# UserConfig
[`UserConfig`](./struct.UserConfig.html) is required to [`Client::register`](./struct.Client.html#method.register)
(e.g. complete the connection) with Twitch

```no_run
use twitchchat::UserConfig;
let my_token = std::env::var("MY_TWITCH_OAUTH_TOKEN").unwrap();
let my_name = "my_name_123";
let config = UserConfig::builder()
    .nick(my_name)   // sets you nick
    .token(my_token) // sets you password (e.g. oauth token. must start with `oauth:`)
    // capabilities these are disabled by default. so using these "toggles" the flag (e.g. flips a boolean)
    .membership()    // this enables the membership CAP
    .commands()      // this enables the commands CAP
    .tags()          // this enables the tags CAP
    .build()         // create the config
    .unwrap();       // returns an Option, None if nick/token aren't semi-valid
```

# The irc module
The [`irc`](./irc/index.html) module contains a **very** simplistic representation of the IRC protocol.
*/
#![warn(missing_docs)]
#![deny(unsafe_code)]
#![deny(unused_lifetimes)]
#![deny(unused_qualifications)]
#![deny(unused_results)]

/// IRC-related stuff
pub mod irc;

mod tags;
/// IRCv3 Tags
pub use tags::Tags;

/// Types associated with twitch
mod twitch;
pub use twitch::*;

pub use self::twitch::UserConfig;

mod tee;
mod teststream;

mod ratelimit;

/// Helpers for writing tests
pub mod helpers {
    pub use super::ratelimit::RateLimit;
    pub use super::tee::{TeeReader, TeeWriter};
    pub use super::teststream::TestStream;
}

#[allow(dead_code)]
pub(crate) const VERSION_STR: &str =
    concat!(env!("CARGO_PKG_NAME"), ":", env!("CARGO_PKG_VERSION"));

/// The Twitch IRC address for non-TLS connections
pub const TWITCH_IRC_ADDRESS: &str = "irc.chat.twitch.tv:6667";
/// The Twitch IRC address for TLS connections
pub const TWITCH_IRC_ADDRESS_TLS: &str = "irc.chat.twitch.tv:6697";

/// Convert an IRC-like message type into something that the Twitch commands can be parsed from
///
/// Refer to this form when implementing this trait:
///
/// raw string form: `@tags :prefix command args :data\r\n`
pub trait ToMessage {
    /// Get the tags portion of the IRC message
    fn tags(&self) -> Option<TagType<'_>>;
    /// Get the prefix portion of the IRC message
    fn prefix(&self) -> Option<&str>;
    /// Get the command portion of the IRC message
    fn command(&self) -> Option<&str>;
    /// Get the args portion of the IRC message
    fn args(&self) -> Option<ArgsType<'_>>;
    /// Get the data portion of the IRC message
    fn data(&self) -> Option<&str>;
}

#[cfg(feature = "hashbrown")]
use hashbrown::HashMap;
#[cfg(not(feature = "hashbrown"))]
use std::collections::HashMap;

/// A representation of IRCv3 tags, a raw string or a Vec of Key-Vals
pub enum TagType<'a> {
    /// Raw string
    Raw(&'a str),
    /// List of Key -> Values (owned)
    List(&'a Vec<(String, String)>),
    /// Map of Key -> Values (owned)
    Map(&'a HashMap<String, String>),
}

/// A representation of the args list portion of the IRC message
pub enum ArgsType<'a> {
    /// A raw string
    Raw(&'a str),
    /// A list of parts parsed from the whitespace-separated raw string
    List(&'a Vec<String>),
}

use crate::irc::types::Message as IrcMessage;
impl ToMessage for IrcMessage {
    fn tags(&self) -> Option<TagType<'_>> {
        match self {
            IrcMessage::Unknown { tags, .. } => Some(TagType::Map(&tags.0)),
            _ => None,
        }
    }
    fn prefix(&self) -> Option<&str> {
        match self {
            IrcMessage::Unknown {
                prefix: Some(crate::irc::types::Prefix::User { nick, .. }),
                ..
            } => Some(&nick),
            _ => None,
        }
    }
    fn command(&self) -> Option<&str> {
        match self {
            IrcMessage::Unknown { head, .. } => Some(&head),
            _ => None,
        }
    }
    fn args(&self) -> Option<ArgsType<'_>> {
        match self {
            IrcMessage::Unknown { args, .. } => Some(ArgsType::List(&args)),
            _ => None,
        }
    }
    fn data(&self) -> Option<&str> {
        match self {
            IrcMessage::Unknown { tail, .. } => tail.as_ref().map(|s| s.as_str()),
            _ => None,
        }
    }
}