Git Product home page Git Product logo

twitchchat's Introduction

twitchchat

Documentation Crates 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.

It also provides an 'event' loop which you can use to make a bot.

Opt-in features

By default, this crate depends on zero external crates -- but it makes it rather limited in scope. It can just parse/decode/encode to standard trait types (std::io::{Read, Write}).

To use the AsyncRunner (an async-event loop) you must able the async feature.

NOTE This is a breaking change from 0.12 which had the async stuff enabled by default.

twitchchat = { version = "0.14", features = ["async"] }

To use a specific TcpStream/TlStream refer to the runtime table below.

Serde support

To enable serde support, simply enable the optional serde feature

Runtime

This crate is runtime agonostic. To use..

Read/Write provider Features
async_io async-io
smol smol
async_std async-std
tokio tokio and tokio-util

TLS

If you want TLS supports, enable the above runtime and also enable the cooresponding features:

Read/Write provider Runtime Features TLS backend
async_io async_io "async-tls" rustls
smol smol "async-tls" rustls
async_std async_std "async-tls" rustls
tokio tokio "tokio-util", "tokio-rustls", "webpki-roots" rustls
tokio tokio "tokio-util", "tokio-native-tls", "native-tls" native-tls
tokio tokio "tokio-util", "tokio-openssl", "openssl" openssl

Examples

Using async_io to connect with..

Using async_std to connect with..

Using smol to connect with..

Using tokio to connect with..

How to use the crate as just a message parser(decoder)/encoder

An a simple example of how one could built a bot with this

License

twitchchat is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0).

See LICENSE-APACHE and LICENSE-MIT for details.

twitchchat's People

Contributors

chronophylos avatar emilgardis avatar gagbo avatar greenbigfrog avatar halzy avatar heliozoa avatar horki avatar lakelezz avatar museun avatar olback avatar taq5z6ee avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

twitchchat's Issues

Discussion: restructuring the module

I want to flatten the layout out.

for example:

    match msg {
        twitchchat::twitch::Message::PrivMsg(
            msg @ twitchchat::twitch::commands::PrivMsg { channel, .. },
        ) if channel == "museun" => {
            do_something(&msg);
        }
        _ => {}
    }

could just be:

    match msg {
        twitchchat::Message::Privmsg(msg @ twitchchat::PrivMsg { channel, .. })
            if channel == "museun" =>
        {
            do_something(&msg);
        }
        _ => {}
    }

with local imports:

    use twitchchat::twitch::commands::PrivMsg;
    use twitchchat::twitch::Message;
    match msg {
        Message::PrivMsg(msg @ PrivMsg { channel, .. }) 
            if channel == "museun" => 
        {
            do_something(&msg);
        }
        _ => {}
    }

compared to:

    use twitchchat::*;
    match msg {
        Message::Privmsg(msg @ PrivMsg { channel, .. }) 
            if channel == "museun" => 
        {
            do_something(&msg);
        }
        _ => {}
    }

This would probably break the current api, I can duplicate it all (just pub use whatever::* in the root namespace), but I'd rather just move everything. Having the root crate act like a prelude is definitely an ergonomic gain.

I still want the irc module to be in its own namespace so it doesn't clash and isn't emphasized for usage.

More types need Serialize/Deserialize

for example:

/// Configuration used to complete the 'registration' with the irc server
pub struct UserConfig {

and:
/// Capabilities allow you to get more data from twitch
///
/// The default, `generic` is very simplistic (basically just read/write PRIVMSGs for a channel)
///
/// While enabling `membership` + `commands` + `tags` will allow you to get a much more rich set of messages
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
pub enum Capability {

(also, don't need to export the UserConfigBuilder)

Determine whether the Client should split long messages into 512-byte chunks

IRC Messages are limited to 512 bytes (including the trailing \r\n). When a user does a .send() or a .raw() there's no checks to make sure this fits within that 512 bytes, which also includes all the Command and Target information.

One solution would be to just provide a split function that the core .write() uses to break up long lines into multiple lines. With this split function, a rate-limit method could be added which can adhere to the Twitch guidelines for message sending limitation.s

from the docs

Limit Applies to โ€ฆ
20 per 30 seconds Users sending commands or messages to channels in which they do not have Moderator or Operator status
100 per 30 seconds Users sending commands or messages to channels in which they have Moderator or Operator status
50 per 30 seconds Known bots
7500 per 30 seconds Verified bots

To implement rate limiting, a token bucket could be used -- or even a timer wheel.
Other parts of the crate need to bring in chrono for doing "time-like" operations. Keeping track of the last send per window would count against the available tokens.

A major design decision and question would be what to do when we've exceeded this. Do we block the thread?

Such as:

let next_window = some_calculation();
std::thread::sleep(std::time::Duration::from_millis(next_window));

Or do we have the caller decide when to 'try' again? By returning some "rate limit" style error and internally buffer it, we can allow the caller to figure out what to do w.r.t to blocking and retrying.

    enum RateLimit {
        Limited {
            reqs_remain: usize,
            preferred_time: std::time::Duration,
        },
        Continue, // or None or something
    }

    // drain queue here first (FIFO (recursive call this function maybe))

    let next_window = some_calculation();
    if self.is_too_soon(next_window) {
        self.queue.push(their_data);
        return Ok(RateLimit::Limited {
            reqs_remain: self.queue.len(),
            preferred_time: self.next_best_time(),
        });
    }
    Ok(RateLimit::Continue)

lib.rs documentation has errors

Here:

twitchchat/src/lib.rs

Lines 71 to 76 in ee06b62

//! let (nick, pass) = (std::env::var("MY_TWITCH_OAUTH_TOKEN").unwrap(), "my_name");
//! let config = UserConfig::builder()
//! .token(pass)
//! .nick(nick)
//! .build()
//! .unwrap();

It has nick/token swapped.

It should also join() a channel before the .run()

twitchchat/src/lib.rs

Lines 95 to 102 in ee06b62

//! client.on(|msg: PrivMsg| {
//! // print out name: msg
//! let name = msg.display_name().unwrap_or_else(|| msg.irc_name());
//! println!("{}: {}", name, msg.message())
//! });
//!
//! // blocks the thread, but any callbacks set in the .on handlers will get their messages
//! client.run();

otherwise the demo won't do anything

Upgrade deps

hashbrown is now in the stdlib
parking_lot's changes are important

There is too much ceremony to get this going.

just a way to get PrivMsgs into a crossbeam channel

struct Client {
    writer: twitchchat::Writer,
    recv: channel::Receiver<Message>,
    user: twitchchat::LocalUser,
    handle: std::thread::JoinHandle<()>,
}
impl Client {
    fn connect(nick: &str, channel: &str, token: &str) -> Result<Self, Error> {
        use twitchchat::commands::PrivMsg;
        use twitchchat::*;

        let config = UserConfig::with_caps()
            .token(token)
            .nick(nick)
            .build()
            .unwrap();

        let stream = TcpStream::connect(TWITCH_IRC_ADDRESS) //
            .map_err(crate::Error::Connect)?;

        let (read, write) = sync_adapters(stream.try_clone().unwrap(), stream);
        let mut client = Client::new(read, write);

        let (tx, rx) = channel::unbounded();

        struct ReadMessage {
            send: channel::Sender<crate::Message>,
        }
        impl Handler for ReadMessage {
            fn on_priv_msg(&mut self, msg: std::sync::Arc<PrivMsg>) {
                let msg = msg.into();
                // this should shut down the client if we cannot send
                // instead of unwrapping, that way the caller can just drop this channel
                // and ideally it would close the connection, thus return from the thread below
                self.send.send(msg).unwrap(); 
            }
        }
        client.handler(ReadMessage { send: tx });

        client.register(config).map_err(crate::Error::Register)?;
        let user = client.wait_for_ready().map_err(crate::Error::NotReady)?;

        let writer = client.writer();
        // TODO we need to know if its a bad channel by waiting for the joined message
        writer.join(channel).unwrap();

        let handle = std::thread::spawn(move || {
            let _ = client.run(); // this needs to signal that we've lost the connect
        });

        Ok(Self {
            writer,
            recv: rx,
            user,
            handle,
        })
    }
}

an "Easy" path should be added

let client = EasyClient::connect(nick, token)?; // name pending
let writer = client.writer();
for msg in client {
    match msg {
        _ => writer.stuff()?
    }
}

EasyClient (tm) could allow for a builder pattern to set up handlers/callbacks to mimic libraries in other languages. and then just return an iterator over the filtered messages

or just pseudo-iterator adapters..

let client = Client::new(nick, token).with(commands::PrivMsg).filter(commands::Join);
// the iterator will return only PrivMsgs and Joins

another idea:
Writer should have a shutdown method that'll (gracefully) shut down the client

a new Client that isn't generic over a ReadAdapter would also make it much simpler just to pass it around instead of dealing with (the likely realistic) Client<SyncReadAdapter<TcpStream>>

Into<Channel> should be a specific trait. e.g. AsChannel

This will allow for better coherence. The blanket Into from std lib for coherence breaks the current attempt.

Currently something like the following has problems:

struct S;
impl Display for S {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "foobar")
    }
}

fn foo(d: impl Into<Channel>) { 
    let ch = d.into();
}

foo("foobar"); // works
foo(S{}); // does not work
foo(S{}.to_string()); // works

And a bit of bikeshed. ToChannel vs AsChannel (e.g. consuming or borrowing). With the way the Writer works, it doesn't need ownership because its just going to format a string in the end, so everything can be borrowed which would be concatenated with some static data.

Rethink cloning the Client

Ideally, there should only ever be 1 "reader" in the client, but many "writers".

We could do a ReadHalf/WriteHalf style API like in Futures, where ReadHalf moves and isn't clonable, but WriteHalf is clonable and thread safe.

Right now: its a bad idea (tm) to do the read-operations concurrently on cloned Clients. The write stuff is fine, but we can reduce lock contention by using some stuff from crossbeam. Cloned writers would just publish to an internal lock-free queue and a single internal writer will pull from it.

One major problem with simply splitting it into ReadHalf/WriteHalf would be that read_message() writes back to the client, internally, to handle things like PINGs automatically. This would have to be considered if we were to "split the responsibility". The ReadHalf could have its own internal clone of a WriteHalf for example.

Perhaps a channels-based API should be discussed. Replace the inspect/filter functions with channels, and then operate internally on read/write channels.

Some methods should take AsRef<str> instead of &str

these on the Writer:

fn ban(&self, username: &str, reason: Option<&str>) -> Result<(), Error>;
fn op(&self, username: &str) -> Result<(), Error>;
fn unmod(&self, username: &str) -> Result<(), Error>;
fn unban(&self, username: &str) -> Result<(), Error>;
fn untimeout(&self, username: &str) -> Result<(), Error>;
fn vip(&self, username: &str) -> Result<(), Error>;
fn unvip(&self, username: &str) -> Result<(), Error>;
fn whisper(&self, username: &str, message: &str) -> Result<(), Error>;

and probably more

Provide some API for taking ownership of the strings inside of 'commands'

struct Bar {
    user: String,
    data: String,
}

fn foo(msg: commands::PrivMsg) -> Bar{
    // except for indv. fields (or some .into() style function)
    let (user, data) = msg.take_whatever();
    Bar {
        user, 
        data
    }
}

Currently, only references are obtainable from the types. even if you own it,
so there should be an IntoInner trait that is applied to all of the types in the crate that allow for extraction

Something like this, maybe. (either tuples or dedicated Owned$name structs)

trait IntoInner {
    type Inner;
    fn into_inner(self) -> Self::Inner;
}

struct Foo {
    foo: String, 
    bar: String 
}

impl IntoInner for Foo {
    type Inner = (String,String); 
    fn into_inner(self) -> Self::Inner {
        (self.foo, self.bar)
    }
}

Twitch limits joins based on the channel count, not the line count

pub fn join_many<'a, I>(&self, channels: I) -> Result<(), Error>
where
I: IntoIterator + 'a,
I::Item: AsRef<str> + 'a,
{
let mut buf = String::with_capacity(512);
for channel in channels {
let channel = channel.as_ref();
let len = if channel.starts_with('#') {
channel.len()
} else {
channel.len() + 1
};
if buf.len() + len + 1 > 510 {
self.write_line(&buf)?;
buf.clear();
}
if buf.is_empty() {
buf.push_str("JOIN ");
} else {
buf.push(',');
}
if !channel.starts_with('#') {
buf.push_str(&["#", channel].concat());
} else {
buf.push_str(&channel);
}
}
if !buf.is_empty() {
self.write_line(&buf)?;
}
Ok(())
}

This should be replaced with just

pub fn join_many<'a, I>(&self, channels: I) -> Result<(), Error>
    where
        I: IntoIterator + 'a,
        I::Item: AsRef<str> + 'a,
{
    for channel in channels {
        self.join(channel)?;
    }
    Ok(())
}

Abstract the the interface of the Client.

This will allow for easier integration with AsyncRead/AsyncWrite-style clients, using something like this:

pub trait Adapter {
    // owned for simplicity but perhaps a &'a [u8] (and use of the `bytes` crate)
    // <= 1024 string ending with \r\n
    fn read_line(&mut self) -> std::io::Result<String>;  
    // <= 512 buffer ending with \r\n
    fn write_line(&mut self, data: &[u8]) -> std::io::Result<usize>; 
}

And something like will allow people to use other irc parsing crates to provide the twitchchat::irc::types::Message that'll be used with the twitchchat::Message types:

// TODO: perhaps ToOwned / Borrowed (Cow) for these
// @tags :prefix command args :data\r\n
pub trait ToMessage {
  fn tags(&self) -> Option<&str>;
  fn prefix(&self) -> Option<&str>;
  fn command(&self) -> Option<&str>;
  fn args(&self) -> Option<&[&str]>;
  fn data(&self) -> Option<&str>;
}

// twitchchat::Message
impl Message {
  fn parse(msg: impl ToMessage) -> Self {
    let tags = msg.tags();
    let prefix = msg.prefix();
    let command = msg.command();
    let args = msg.args();
    let data = msg.data();

    // do parsing here
  }
}

(which will allow for removing the whole irc module and providing an optional one that implements this trait)

More logging.

There should be more (and actually useful) logging.

Change the Twitch color + RGB split

pub enum Twitch {
/// RGB (hex): #0000FF
Blue,
/// RGB (hex): #8A2BE2
BlueViolet,
/// RGB (hex): #5F9EA0
CadetBlue,
/// RGB (hex): #D2691E
Chocolate,
/// RGB (hex): #FF7F50
Coral,
/// RGB (hex): #1E90FF
DodgerBlue,
/// RGB (hex): #B22222
Firebrick,
/// RGB (hex): #DAA520
GoldenRod,
/// RGB (hex): #008000
Green,
/// RGB (hex): #FF69B4
HotPink,
/// RGB (hex): #FF4500
OrangeRed,
/// RGB (hex): #FF0000
Red,
/// RGB (hex): #2E8B57
SeaGreen,
/// RGB (hex): #00FF7F
SpringGreen,
/// RGB (hex): #ADFF2F
YellowGreen,
/// Turbo colors are custom user-selected colors
Turbo(RGB),
}

involving two .into() is annoying, and it complicates the ser/de stuff
make a concrete struct with an enum to denote the named color.

should be something like

struct Color {
    name: TwitchColor,
    rgb: RGB,
}
// with TwitchColor being
enum TwitchColor {
    Blue,
    BlueViolet,
    CadetBlue,
    Chocolate,
    Coral,
    DodgerBlue,
    Firebrick,
    GoldenRod,
    Green,
    HotPink,
    OrangeRed,
    Red,
    SeaGreen,
    SpringGreen,
    YellowGreen,
    Turbo,
}

Write a readme

Perhaps generate it from the lib.rs documentation:

twitchchat/src/lib.rs

Lines 1 to 109 in ee06b62

//! # 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.
//!
//! # Organization of project
//! This crate is split into two top-level modules, `irc` and `twitch`.
//!
//! The [`irc`](./irc/index.html) module contains a **very** simplistic
//! representation of the IRC protocol, but can be used to write simplistic
//! clients for interacting with the twitch module
//!
//! The [`twitch`](./twitch/index.html) module contains many data types that
//! encapsulate the various functionality of the irc-portion of Twitch's chat
//! system
//!
//! # 'Theory' of operation
//! First, by creating a [`Client`](./twitch/struct.Client.html) from a
//! Read/Write pair (such as a cloned TcpStream) then calling
//! [`Client::register`](./twitch/struct.Client.html#method.register) with a
//! filled-out [`UserConfig`](./struct.UserConfig.html) will connect you to
//! Twitch. Once connected, you can
//! [`Client::wait_for_ready`](./twitch/struct.Client.html#method.wait_for_ready)
//! and the client will read **(blocking)** until Twitch sends a
//! [`GlobalUserState`](./twitch/commands/struct.GlobalUserState.html) message,
//! which it'll fill out a [`LocalUser`](./twitch/struct.LocalUser.html) with
//! various information.
//!
//! Once connected, you can
//! - use [`Client::join`](./twitch/struct.Client.html#method.join) to join a
//! channel.
//! - use [`Client::on`](./twitch/struct.Client.html#method.on) to set up a
//! message filter.
//! - use [`Client::read_message`](./twitch/struct.Client.html#method.read_message)
//! to read a message (and pump the filters).
//! - or do various [*other things*](./twitch/struct.Client.html#method.host)
//!
//! # 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`](./twitch/struct.Client.html#method.on) method. It uses the
//! type of the closures argument, one of
//! [*these*](./twitch/commands/index.html#structs) to create a filter
//!
//! When [`Client::read_message`](./twitch/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`](./twitch/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`](./twitch/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 elsehwere
//!
//! # A simple example
//! ```no_run
//! use std::net::TcpStream;
//! use twitchchat::twitch::{commands::PrivMsg, Capability, Client};
//! 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 (nick, pass) = (std::env::var("MY_TWITCH_OAUTH_TOKEN").unwrap(), "my_name");
//! let config = UserConfig::builder()
//! .token(pass)
//! .nick(nick)
//! .build()
//! .unwrap();
//!
//! // client takes a std::io::Read 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| {
//! // print out name: msg
//! let name = msg.display_name().unwrap_or_else(|| msg.irc_name());
//! println!("{}: {}", name, msg.message())
//! });
//!
//! // blocks the thread, but any callbacks set in the .on handlers will get their messages
//! client.run();
//! # }
//! ```
//!
//! # TestStream
//! [`TestStream`](./struct.TestStream.html) is a simple TcpStream-like thing
//! that lets you inject/read its internal buffers, allowing you to easily write
//! unit tests for the [`Client`](./twitch/struct.Client.html)

Make some (all?) of the enums have a non-exhaustive variant

recently changes to BadgeKind requires a version bump for adding new variants
its recommended for public enums to have a variant such as

    #[doc(hidden)]
    __Nonexhaustive,

so the consumer of the enum has to have a _ => () arm in their pattern matching of the enum
this allows variants to be added to the enum in the future, in a non-breaking way.

also one day (tm), rust-lang/rust#44109 will be usable

finally, perhaps hide the fields of some (all?) of the structs to aid in future proofing.
these changes should decided before the next API break (0.4.0)

Simplify the nesting/re-exporting

I don't, personally, like the twitchchat::twitch::commands::PrivMsg level of nesting. the top-level twitchchat::twitch could be reduced. Commonly used things can either be re-exported into the root, a prelude-style module.

I don't like the UserConfigBuilder

It should be less verbose or maybe provide some 'canned' configs where the user just has to provide their nick and their oauth token.

    let config = twitchchat::UserConfig::builder()
        .commands()
        .membership()
        .tags()
        .nick(&opts.nick)
        .token(pass)
        .build()
        .expect("valid config"); // why is this an option?

could just be

    let config = UserConfig::all_caps()
        .with_nick("foo")
        .with_token("bar")
        .build();

And instead of returning an Option (which I assume most people would just unwrap anyway), panic because this should be a hard error. The UserConfig type won't be built dynamically, so if a required field is missing (nick and/or token) it should yell at the user rather then continuing.

Create a trait for the filter closures

the .on() method should take a trait in

and the trait should be implemented for

Fn(Message)
FnMut(Message)
Fn(Message, Client)
FnMut(Message, Client)
// and others in the future

this enables way more flexibility

Maybe use a type alias for these Results

in this section:

pub fn host<C>(&self, channel: C) -> Result<(), Error>
where
C: Into<Channel>,
{
let channel = Channel::validate(channel)?;
self.command(format!("/host {}", *channel))
}
/// Stop hosting another channel.
pub fn unhost(&self) -> Result<(), Error> {
self.command("/unhost")
}
/// Adds a stream marker (with an optional comment, max 140 characters) at the current timestamp.
///
/// You can use markers in the Highlighter for easier editing.
pub fn marker(&self, comment: Option<&str>) -> Result<(), Error> {
match comment {
Some(comment) => {
// TODO use https://github.com/unicode-rs/unicode-width
let cmd = if comment.len() <= 140 {
format!("/marker {}", comment)
} else {
let comment = comment.chars().take(140).collect::<String>();
format!("/marker {}", comment)
};
self.command(cmd)
}
_ => self.command("/marker"),
}
}
/// Raid another channel.
///
/// Use [`Writer::unraid`](./struct.Writer.html#method.unraid) to cancel the Raid.
pub fn raid<C>(&self, channel: C) -> Result<(), Error>
where
C: Into<Channel>,
{
let channel = Channel::validate(channel)?;
self.command(format!("/raid {}", *channel))
}
/// Cancel the Raid.
pub fn unraid(&self) -> Result<(), Error> {
self.command("/unraid")
}
/// Change your username color.
pub fn color<C>(&self, color: C) -> Result<(), Error>
where
C: Into<TwitchColor>,
{
self.command(format!("/color {}", color.into()))
}
/// Reconnects to chat.
pub fn disconnect(&self) -> Result<(), Error> {
self.command("/disconnect")
}
/// Lists the commands available to you in this room.
pub fn help(&self) -> Result<(), Error> {
self.command("/help")
}
/// Lists the moderators of this channel.
pub fn mods(&self) -> Result<(), Error> {
self.command("/mods")
}
/// Lists the VIPs of this channel.
pub fn vips(&self) -> Result<(), Error> {
self.command("/vips")
}
/// Triggers a commercial.
///
/// Length (optional) must be a positive number of seconds.
pub fn commercial(&self, length: Option<usize>) -> Result<(), Error> {
match length {
Some(n) => self.command(format!("/commercial {}", n)),
None => self.command("/commercial"),
}
}
/// Permanently prevent a user from chatting.
/// Reason is optional and will be shown to the target user and other moderators.
///
/// Use [`Writer::unban`](./struct.Writer.html#method.unban) to remove a ban.
pub fn ban(&self, username: &str, reason: Option<&str>) -> Result<(), Error> {
match reason {
Some(reason) => self.command(format!("/ban {} {}", username, reason)),
None => self.command(format!("/ban {}", username)),
}
}
/// Removes a ban on a user.
pub fn unban(&self, username: &str) -> Result<(), Error> {
self.command(format!("/unban {}", username))
}
/// Clear chat history for all users in this room.
pub fn clear(&self) -> Result<(), Error> {
self.command("/clear")
}
// ???
// pub fn delete(&self) -> Result<(), Error> {
// unimplemented!()
// }
/// Enables emote-only mode (only emoticons may be used in chat).
///
/// Use [`Writer::emoteonlyoff`](./struct.Writer.html#method.emoteonlyoff) to disable.
pub fn emoteonly(&self) -> Result<(), Error> {
self.command("/emoteonly")
}
/// Disables emote-only mode.
pub fn emoteonlyoff(&self) -> Result<(), Error> {
self.command("/emoteonlyoff")
}
/// Enables followers-only mode (only users who have followed for 'duration' may chat).
///
/// Examples: "30m", "1 week", "5 days 12 hours".
///
/// Must be less than 3 months.
pub fn followers(&self, duration: &str) -> Result<(), Error> {
// TODO use https://docs.rs/chrono/0.4.6/chrono/#duration
// and verify its < 3 months
self.command(&format!("/followers {}", duration))
}
/// Disables followers-only mode.
pub fn followersoff(&self) -> Result<(), Error> {
self.command("/followersoff")
}
/// Grant moderator status to a user.
///
/// Use [`Writer::mods`](./struct.Writer.html#method.mods) to list the moderators of this channel.
///
/// (**NOTE**: renamed to `op` because r#mod is annoying to type)
pub fn op(&self, username: &str) -> Result<(), Error> {
self.command(&format!("/mod {}", username))
}
/// Revoke moderator status from a user.
///
/// Use [`Writer::mods`](./struct.Writer.html#method.mods) to list the moderators of this channel.
pub fn unmod(&self, username: &str) -> Result<(), Error> {
self.command(&format!("/unmod {}", username))
}
/// Enables r9k mode.
///
/// Use [`Writer::r9kbetaoff`](./struct.Writer.html#method.r9kbetaoff) to disable.
pub fn r9kbeta(&self) -> Result<(), Error> {
self.command("/r9kbeta")
}
/// Disables r9k mode.
pub fn r9kbetaoff(&self) -> Result<(), Error> {
self.command("/r9kbetaoff")
}
/// Enables slow mode (limit how often users may send messages).
///
/// Duration (optional, default=120) must be a positive number of seconds.
///
/// Use [`Writer::slowoff`](./struct.Writer.html#method.slowoff) to disable.
pub fn slow(&self, duration: Option<usize>) -> Result<(), Error> {
// TODO use https://docs.rs/chrono/0.4.6/chrono/#duration
match duration {
Some(dur) => self.command(format!("/slow {}", dur)),
None => self.command("/slow"),
}
}
/// Disables slow mode.
pub fn slowoff(&self) -> Result<(), Error> {
self.command("/slowoff")
}
/// Enables subscribers-only mode (only subscribers may chat in this channel).
///
/// Use [`Writer::subscribersoff`](./struct.Writer.html#method.subscribersoff) to disable.
pub fn subscribers(&self) -> Result<(), Error> {
self.command("/subscribers")
}
/// Disables subscribers-only mode.
pub fn subscribersoff(&self) -> Result<(), Error> {
self.command("/subscribersoff")
}
/// Temporarily prevent a user from chatting.
///
/// * duration (*optional*, default=`10 minutes`) must be a positive integer.
/// * time unit (*optional*, default=`s`) must be one of
/// * s
/// * m
/// * h
/// * d
/// * w
/// * maximum duration is `2 weeks`.
///
/// Combinations like `1d2h` are also allowed.
///
/// Reason is optional and will be shown to the target user and other moderators.
///
/// Use [`Writer::untimeout`](./struct.Writer.html#method.untimeout) to remove a timeout.
pub fn timeout(
&self,
username: &str,
duration: Option<&str>,
reason: Option<&str>,
) -> Result<(), Error> {
// TODO use https://docs.rs/chrono/0.4.6/chrono/#duration
// and verify the duration stuff
let timeout = match (duration, reason) {
(Some(dur), Some(reason)) => format!("/timeout {} {} {}", username, dur, reason),
(None, Some(reason)) => format!("/timeout {} {}", username, reason),
(Some(dur), None) => format!("/timeout {} {}", username, dur),
(None, None) => format!("/timeout {}", username),
};
self.command(timeout)
}
/// Removes a timeout on a user.
pub fn untimeout(&self, username: &str) -> Result<(), Error> {
self.command(format!("/untimeout {}", username))
}
/// Grant VIP status to a user.
///
/// Use [`Writer::vips`](./struct.Writer.html#method.vips) to list the VIPs of this channel.
pub fn vip(&self, username: &str) -> Result<(), Error> {
self.command(format!("/vip {}", username))
}
/// Revoke VIP status from a user.
///
/// Use [`Writer::vips`](./struct.Writer.html#method.vips) to list the VIPs of this channel.
pub fn unvip(&self, username: &str) -> Result<(), Error> {
self.command(format!("/unvip {}", username))
}
/// Whispers the message to the username.
pub fn whisper(&self, username: &str, message: &str) -> Result<(), Error> {
self.command(format!("/w {} {}", username, message))
}
/// Joins a `channel`
///
/// This ensures the channel name is lowercased and begins with a '#'.
///
/// The following are equivilant
/// ```no_run
/// # use twitchchat::{helpers::TestStream, Client};
/// # let mut stream = TestStream::new();
/// # let (r, w) = (stream.clone(), stream.clone());
/// # let mut client = Client::new(r, w);
/// let w = client.writer();
/// w.join("museun").unwrap();
/// w.join("#museun").unwrap();
/// w.join("Museun").unwrap();
/// w.join("#MUSEUN").unwrap();
/// ```
pub fn join<C>(&self, channel: C) -> Result<(), Error>
where
C: Into<Channel>,
{
let channel = Channel::validate(channel)?;
self.raw(format!("JOIN {}", *channel))
}
/// Parts a `channel`
///
/// This ensures the channel name is lowercased and begins with a '#'.
///
/// The following are equivilant
/// ```no_run
/// # use twitchchat::{helpers::TestStream, Client};
/// # let mut stream = TestStream::new();
/// # let (r, w) = (stream.clone(), stream.clone());
/// # let mut client = Client::new(r, w);
/// let w = client.writer();
/// w.part("museun").unwrap();
/// w.part("#museun").unwrap();
/// w.part("Museun").unwrap();
/// w.part("#MUSEUN").unwrap();
/// ```
pub fn part<C>(&self, channel: C) -> Result<(), Error>
where
C: Into<Channel>,
{
let channel = Channel::validate(channel)?;
self.raw(format!("PART {}", *channel))
}
/// Sends an "emote" `message` in the third person to the `channel`
///
/// This ensures the channel name is lowercased and begins with a '#'.
pub fn me<C, S>(&self, channel: C, message: S) -> Result<(), Error>
where
C: Into<Channel>,
S: AsRef<str>,
{
let channel = Channel::validate(channel)?;
self.send(channel, format!("/me {}", message.as_ref()))
}
/// Sends the `message` to the `channel`
///
/// This ensures the channel name is lowercased and begins with a '#'.
///
/// Same as [`send`](./struct.Client.html#method.send)
pub fn privmsg<C, S>(&self, channel: C, message: S) -> Result<(), Error>
where
C: Into<Channel>,
S: AsRef<str>,
{
let channel = Channel::validate(channel)?;
self.raw(format!("PRIVMSG {} :{}", *channel, message.as_ref()))
}
/// Sends the `message` to the `channel`
///
/// This ensures the channel name is lowercased and begins with a '#'.
///
/// Same as [`privmsg`](./struct.Client.html#method.privmsg)
pub fn send<C, S>(&self, channel: C, message: S) -> Result<(), Error>
where
C: Into<Channel>,
S: AsRef<str>,
{
self.privmsg(channel, message)
}
/// Sends the command: `data` (e.g. `/color #FFFFFF`)
pub fn command<S>(&self, data: S) -> Result<(), Error>
where
S: AsRef<str>,
{
self.raw(format!("PRIVMSG jtv :{}", data.as_ref()))
}
/// Sends a raw line (appends the required `\r\n`)
pub fn raw<S>(&self, data: S) -> Result<(), Error>
where
S: AsRef<str>,
{
self.write_line(data.as_ref())
}

technically it'd be a

pub type Result = std::result::Result<(), Error>;
// with -> Result {

or a

pub type Result<E> = std::result::Result<(), E>;
// with -> Result<Error> {

both of which are non-idiomatic, though

Provide a way to remove 'message filters'

The on method should return a unique Id that can be stored and used later to remove that filter.
While this is being done, determine whether the name should be a combination of on/off or filter/remove_filter

the Writer should block, in prep. for futures

this should be valid:
let ch = client.writer().join("foo").unwrap()

join should block until we get a message that we joined

challenges:
twitch's irc is a bit weird, so a lot of the times this is only possible with specific capabilities
and somteimes twitch doesn't reply like a normal server would

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.