Skip to content

Commit

Permalink
feat: YouTube Live Chat (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
decahedron1 authored Feb 9, 2024
2 parents 58e54a2 + d1fa005 commit 3b49271
Show file tree
Hide file tree
Showing 18 changed files with 1,667 additions and 106 deletions.
28 changes: 20 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
[package]
name = "brainrot"
description = "A live chat interface for Twitch & YouTube"
version = "0.1.0"
authors = [ "Carson M. <carson@pyke.io>" ]
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", 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 }
url = { version = "2.5", optional = true }
rand = { version = "0.8", optional = true }
regex = { version = "1.10", optional = true }
async-stream = "0.3"

[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:rand", "dep:serde", "dep:url", "dep:regex", "dep:serde-aux" ]
serde = [ "dep:serde", "chrono/serde", "uuid?/serde" ]
tls-native = [ "irc?/tls-native" ]
#tls-rust = [ "irc?/tls-rust" ]
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
# `brainrot`
A Twitch IRC client.
A live chat interface for Twitch & YouTube written in Rust.

## Features
- <img src="https://www.twitch.tv/favicon.ico" width="14" /> **Twitch**
* ⚡ Live IRC
* 🔓 No authentication required
- <img src="https://www.youtube.com/favicon.ico" width="14" /> **YouTube**
* 🏆 Receive chats in real time - first library to do so
* ⚡ Low latency
* ⏪ Supports VODs
* 🔓 No authentication required

## Usage
See [`examples/main.rs`](https://github.com/vitri-ent/brainrot/blob/examples/main.rs).
See [`examples/twitch.rs`](https://github.com/vitri-ent/brainrot/blob/examples/twitch.rs) & [`examples/youtube.rs`](https://github.com/vitri-ent/brainrot/blob/examples/youtube.rs).

```shell
$ cargo run --example main -- sinder
$ cargo run --example twitch -- sinder
Spartan_N1ck: Very Generous
luisfelipee23: GIGACHAD
wifi882: GIGACHAD
Expand All @@ -15,4 +25,15 @@ buddy_boy_joe: @sharkboticus ah LOL fair enough sinder6Laugh sinder6Laugh sinder
KateRosaline14: Merry Christmas
ThrillGamer2002: FirstTimeChatter
...

$ cargo run --example youtube -- "@FUWAMOCOch"
Konami Code: makes sense
Wicho4568🐾: thank you biboo
retro: Lol
GLC H 🪐: Thanks Biboo? :face-blue-smiling::FUWAhm:
Ar5eN Vines: lol
Jic: HAHAHA
Rukh 397: :FUWAhm:
PaakType: :FUWApat::MOCOpat::FUWApat::MOCOpat:
...
```
17 changes: 0 additions & 17 deletions examples/main.rs

This file was deleted.

31 changes: 31 additions & 0 deletions examples/twitch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2024 pyke.io
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::env::args;

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

#[tokio::main]
async fn main() -> anyhow::Result<()> {
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 TwitchChatEvent::Message { user, contents, .. } = message {
println!("{}: {}", user.display_name, contents.iter().map(|c| c.to_string()).collect::<String>());
}
}

Ok(())
}
39 changes: 39 additions & 0 deletions examples/youtube.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2024 pyke.io
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::env::args;

use brainrot::youtube::{self, Action, ChatItem};
use futures_util::StreamExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let context =
youtube::ChatContext::new_from_channel(args().nth(1).as_deref().unwrap_or("@miyukiwei"), youtube::ChannelSearchOptions::LatestLiveOrUpcoming).await?;
let mut stream = youtube::stream(&context).await?;
while let Some(Ok(c)) = stream.next().await {
if let Action::AddChatItem {
item: ChatItem::TextMessage { message_renderer_base, message },
..
} = c
{
println!(
"{}: {}",
message_renderer_base.author_name.unwrap().simple_text,
message.unwrap().runs.into_iter().map(|c| c.to_chat_string()).collect::<String>()
);
}
}
Ok(())
}
97 changes: 20 additions & 77 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,80 +1,23 @@
use std::{
pin::Pin,
task::{Context, Poll}
};
// Copyright 2024 pyke.io
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use futures_util::{Stream, StreamExt};
use irc::{
client::{prelude::Config, Client, ClientStream},
proto::Capability
};
#[cfg(feature = "twitch")]
pub mod twitch;
#[cfg(feature = "twitch")]
pub use self::twitch::{Chat as TwitchChat, ChatEvent as TwitchChatEvent, MessageSegment as TwitchMessageSegment, TwitchIdentity};

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
}
#[cfg(feature = "youtube")]
pub mod youtube;

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
}
}
}
pub(crate) mod util;
14 changes: 14 additions & 0 deletions src/event.rs → src/twitch/event.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2024 pyke.io
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{
collections::HashMap,
num::{NonZeroU16, NonZeroU32}
Expand Down
16 changes: 15 additions & 1 deletion src/identity.rs → src/twitch/identity.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright 2024 pyke.io
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Represents a type that can be used to identify the client.
pub trait TwitchIdentity {
/// Converts this type into a tuple of `(username, Option<auth_key>)`.
Expand All @@ -23,7 +37,7 @@ impl TwitchIdentity for Anonymous {
/// use [`Anonymous`] instead.
///
/// ```no_run
/// use brainrot::{Authenticated, Chat};
/// use brainrot::twitch::{Authenticated, Chat};
///
/// # #[tokio::main]
/// # async fn main() -> anyhow::Result<()> {
Expand Down
Loading

0 comments on commit 3b49271

Please sign in to comment.