Skip to content

Commit

Permalink
initial youtube live chat support
Browse files Browse the repository at this point in the history
  • Loading branch information
decahedron1 committed Jan 26, 2024
1 parent 58e54a2 commit ab95185
Show file tree
Hide file tree
Showing 11 changed files with 789 additions and 89 deletions.
25 changes: 17 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
[package]
name = "brainrot"
description = "A live chat interface for Twitch & YouTube"
version = "0.1.0"
authors = [ "Carson M. <[email protected]>" ]
repository = "https://github.com/vitri-ent/brainrot"
edition = "2021"
rust-version = "1.64"
rust-version = "1.75"

[dependencies]
irc = { version = "0.15", default-features = false }
tokio = { version = "1", features = [ "net" ] }
irc = { version = "0.15", optional = true, default-features = false }
tokio = { version = "1.0", default-features = false, features = [ "net" ] }
futures-util = { version = "0.3", default-features = false }
thiserror = "1.0"
chrono = { version = "0.4", default-features = false, features = [ "clock", "std" ] }
serde = { version = "1.0", optional = true, features = [ "derive" ] }
uuid = "1.5"
serde-aux = { version = "4.4", optional = true }
uuid = { version = "1.5", optional = true }
reqwest = { version = "0.11", optional = true }
simd-json = { version = "0.13", optional = true }
regex = { version = "1.10", optional = true }

[dev-dependencies]
anyhow = "1.0"
tokio = { version = "1", features = [ "rt", "rt-multi-thread", "macros", "net" ] }

[features]
default = [ "tls-native" ]
serde = [ "dep:serde", "chrono/serde", "uuid/serde" ]
tls-native = [ "irc/tls-native" ]
tls-rust = [ "irc/tls-rust" ]
default = [ "tls-native", "twitch", "youtube" ]
twitch = [ "dep:irc", "dep:uuid" ]
youtube = [ "dep:simd-json", "dep:reqwest", "dep:serde", "dep:regex", "dep:serde-aux" ]
serde = [ "dep:serde", "chrono/serde", "uuid?/serde" ]
tls-native = [ "irc?/tls-native" ]
#tls-rust = [ "irc?/tls-rust" ]
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# `brainrot`
A Twitch IRC client.
A live chat interface for Twitch & YouTube written in Rust.

## Usage
See [`examples/main.rs`](https://github.com/vitri-ent/brainrot/blob/examples/main.rs).
Expand Down
6 changes: 3 additions & 3 deletions examples/main.rs → examples/twitch.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use std::env::args;

use brainrot::ChatEvent;
use brainrot::{twitch, TwitchChat, TwitchChatEvent};
use futures_util::StreamExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut client = brainrot::Chat::new(args().nth(1).as_deref().unwrap_or("miyukiwei"), brainrot::Anonymous).await?;
let mut client = TwitchChat::new(args().nth(1).as_deref().unwrap_or("miyukiwei"), twitch::Anonymous).await?;

while let Some(message) = client.next().await.transpose()? {
if let ChatEvent::Message { user, contents, .. } = message {
if let TwitchChatEvent::Message { user, contents, .. } = message {
println!("{}: {}", user.display_name, contents.iter().map(|c| c.to_string()).collect::<String>());
}
}
Expand Down
12 changes: 12 additions & 0 deletions examples/youtube.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use std::env::args;

use brainrot::youtube;
use futures_util::StreamExt;

Check warning on line 4 in examples/youtube.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unused import: `futures_util::StreamExt`

Check warning on line 4 in examples/youtube.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unused import: `futures_util::StreamExt`

Check warning on line 4 in examples/youtube.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unused import: `futures_util::StreamExt`

Check warning on line 4 in examples/youtube.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unused import: `futures_util::StreamExt`

Check warning on line 4 in examples/youtube.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unused import: `futures_util::StreamExt`

Check warning on line 4 in examples/youtube.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unused import: `futures_util::StreamExt`

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let (options, cont) = youtube::get_options_from_live_page("e-5D_Shoozk").await?;
let initial_chat = youtube::fetch_yt_chat_page(&options, cont).await?;
youtube::subscribe_to_events(&options, &initial_chat).await?;
Ok(())
}
83 changes: 6 additions & 77 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,80 +1,9 @@
use std::{
pin::Pin,
task::{Context, Poll}
};
#[cfg(feature = "twitch")]
pub mod twitch;
#[cfg(feature = "twitch")]
pub use self::twitch::{Chat as TwitchChat, ChatEvent as TwitchChatEvent, MessageSegment as TwitchMessageSegment, TwitchIdentity};

use futures_util::{Stream, StreamExt};
use irc::{
client::{prelude::Config, Client, ClientStream},
proto::Capability
};
#[cfg(feature = "youtube")]
pub mod youtube;

pub mod identity;
pub use self::identity::{Anonymous, Authenticated, TwitchIdentity};
mod event;
pub use self::event::{ChatEvent, MessageSegment, User, UserRole};
pub(crate) mod util;

const TWITCH_SECURE_IRC: (&str, u16) = ("irc.chat.twitch.tv", 6697);
const TWITCH_CAPABILITY_TAGS: Capability = Capability::Custom("twitch.tv/tags");
const TWITCH_CAPABILITY_MEMBERSHIP: Capability = Capability::Custom("twitch.tv/membership");
const TWITCH_CAPABILITY_COMMANDS: Capability = Capability::Custom("twitch.tv/commands");

/// A connection to a Twitch IRC channel.
///
/// In order for the connection to stay alive, the IRC client must be able to receive and respond to ping messages, thus
/// you must poll the stream for as long as you wish the client to stay alive. If that isn't possible, start a dedicated
/// thread for the client and send chat events back to your application over an `mpsc` or other channel.
#[derive(Debug)]
pub struct Chat {
stream: ClientStream
}

impl Chat {
/// Connect to a Twitch IRC channel.
///
/// ```no_run
/// use brainrot::{Anonymous, Chat};
///
/// # #[tokio::main]
/// # async fn main() -> anyhow::Result<()> {
/// let mut client = Chat::new("miyukiwei", Anonymous).await?;
/// # Ok(())
/// # }
/// ```
pub async fn new(channel: impl AsRef<str>, auth: impl TwitchIdentity) -> irc::error::Result<Self> {
let (username, password) = auth.as_identity();
let mut client = Client::from_config(Config {
server: Some(TWITCH_SECURE_IRC.0.to_string()),
port: Some(TWITCH_SECURE_IRC.1),
nickname: Some(username.to_string()),
password: password.map(|c| format!("oauth:{c}")),
channels: vec![format!("#{}", channel.as_ref())],
..Default::default()
})
.await?;
client.send_cap_req(&[TWITCH_CAPABILITY_COMMANDS, TWITCH_CAPABILITY_MEMBERSHIP, TWITCH_CAPABILITY_TAGS])?;
client.identify()?;
Ok(Self { stream: client.stream()? })
}
}

impl Stream for Chat {
type Item = irc::error::Result<ChatEvent>;

fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let next = self.stream.poll_next_unpin(cx);
match next {
Poll::Ready(Some(Ok(r))) => match self::event::to_chat_event(r) {
Some(ev) => Poll::Ready(Some(Ok(ev))),
None => {
cx.waker().wake_by_ref();
Poll::Pending
}
},
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending
}
}
}
File renamed without changes.
File renamed without changes.
79 changes: 79 additions & 0 deletions src/twitch/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use std::{
pin::Pin,
task::{Context, Poll}
};

use futures_util::{Stream, StreamExt};
use irc::{
client::{prelude::Config, Client, ClientStream},
proto::Capability
};

pub mod identity;
pub use self::identity::{Anonymous, Authenticated, TwitchIdentity};
mod event;
pub use self::event::{ChatEvent, MessageSegment, User, UserRole};

const TWITCH_SECURE_IRC: (&str, u16) = ("irc.chat.twitch.tv", 6697);
const TWITCH_CAPABILITY_TAGS: Capability = Capability::Custom("twitch.tv/tags");
const TWITCH_CAPABILITY_MEMBERSHIP: Capability = Capability::Custom("twitch.tv/membership");
const TWITCH_CAPABILITY_COMMANDS: Capability = Capability::Custom("twitch.tv/commands");

/// A connection to a Twitch IRC channel.
///
/// In order for the connection to stay alive, the IRC client must be able to receive and respond to ping messages, thus
/// you must poll the stream for as long as you wish the client to stay alive. If that isn't possible, start a dedicated
/// thread for the client and send chat events back to your application over an `mpsc` or other channel.
#[derive(Debug)]
pub struct Chat {
stream: ClientStream
}

impl Chat {
/// Connect to a Twitch IRC channel.
///
/// ```no_run
/// use brainrot::{Anonymous, Chat};
///

Check failure on line 37 in src/twitch/mod.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unresolved imports `brainrot::Anonymous`, `brainrot::Chat`

Check failure on line 37 in src/twitch/mod.rs

View workflow job for this annotation

GitHub Actions / Build and test (ubuntu-latest, stable)

unresolved imports `brainrot::Anonymous`, `brainrot::Chat`
/// # #[tokio::main]
/// # async fn main() -> anyhow::Result<()> {
/// let mut client = Chat::new("miyukiwei", Anonymous).await?;
/// # Ok(())
/// # }
/// ```
pub async fn new(channel: impl AsRef<str>, auth: impl TwitchIdentity) -> irc::error::Result<Self> {
let (username, password) = auth.as_identity();
let mut client = Client::from_config(Config {
server: Some(TWITCH_SECURE_IRC.0.to_string()),
port: Some(TWITCH_SECURE_IRC.1),
nickname: Some(username.to_string()),
password: password.map(|c| format!("oauth:{c}")),
channels: vec![format!("#{}", channel.as_ref())],
..Default::default()
})
.await?;
client.send_cap_req(&[TWITCH_CAPABILITY_COMMANDS, TWITCH_CAPABILITY_MEMBERSHIP, TWITCH_CAPABILITY_TAGS])?;
client.identify()?;
Ok(Self { stream: client.stream()? })
}
}

impl Stream for Chat {
type Item = irc::error::Result<ChatEvent>;

fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let next = self.stream.poll_next_unpin(cx);
match next {
Poll::Ready(Some(Ok(r))) => match self::event::to_chat_event(r) {
Some(ev) => Poll::Ready(Some(Ok(ev))),
None => {
cx.waker().wake_by_ref();
Poll::Pending
}
},
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending
}
}
}
Loading

0 comments on commit ab95185

Please sign in to comment.