mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
Twitch chat integration works
This commit is contained in:
1 parent
cdd2cd1ac4
commit
173446ccce
6 files changed
+233
-1309
No files matched your search
Generated
+14
-1303
File diff suppressed because it is too large.
Load diff
+13
-6
@@ -8,21 +8,28 @@ authors = [
|
||||
]
|
||||
|
||||
[[bin]]
|
||||
name = "twitch-gateway"
|
||||
path = "src/main.rs"
|
||||
name = "twitch-helix-demo"
|
||||
path = "src/main_helix_demo.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "twitch-chat-gateway"
|
||||
path = "src/main_chat_gateway.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
rustls = "0.18"
|
||||
serde = "1.0.123"
|
||||
serde_derive = "1.0.123"
|
||||
smol = "1.2.5"
|
||||
#smol = "1.2.5"
|
||||
tokio = { version = "0.2", features = ["full"] }
|
||||
#tokio = { version = "1.2.0", features = ["full"] }
|
||||
toml = "0.5.8"
|
||||
|
||||
# Twitch Helix etc. API, but no PubSub just yet
|
||||
twitch_api2 = "0.4.1"
|
||||
twitch_oauth2 = "0.4.1"
|
||||
#twitch_api2 = "0.4.1"
|
||||
#twitch_oauth2 = "0.4.1"
|
||||
|
||||
# Fairly mature and robust Twitch chat library
|
||||
twitchchat = { version = "0.14.8", features = ["async", "async-tls", "smol"] }
|
||||
#twitchchat = { version = "0.14.8", features = ["async", "async-tls", "smol"] }
|
||||
twitchchat = { version = "0.14.8", features = ["tokio", "tokio-util", "async", "webpki-roots", "tokio-rustls"] }
|
||||
|
||||
@@ -9,6 +9,8 @@ pub struct Secrets {
|
||||
pub application_client_id: String,
|
||||
/// Application secret (generated)
|
||||
pub application_client_secret: String,
|
||||
/// OAuth access token generated with URL handshake
|
||||
pub oauth_access_token: String,
|
||||
}
|
||||
|
||||
impl Secrets {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
#![allow(dead_code)]
|
||||
use anyhow::Context as _;
|
||||
use twitchchat::{messages, AsyncRunner, Status, UserConfig};
|
||||
use crate::filesystem::secrets::Secrets;
|
||||
|
||||
// some helpers for the demo
|
||||
// fn get_env_var(key: &str) -> anyhow::Result<String> {
|
||||
// std::env::var(key).with_context(|| format!("please set `{}`", key))
|
||||
// }
|
||||
|
||||
pub fn get_user_config(secrets: &Secrets) -> anyhow::Result<twitchchat::UserConfig> {
|
||||
let name = "testytest512".to_string(); //get_env_var("TWITCH_NAME")?;
|
||||
let token = secrets.oauth_access_token.to_string();
|
||||
|
||||
// you need a `UserConfig` to connect to Twitch
|
||||
let config = UserConfig::builder()
|
||||
// the name of the associated twitch account
|
||||
.name(name)
|
||||
// and the provided OAuth token
|
||||
.token(token)
|
||||
// and enable all of the advanced message signaling from Twitch
|
||||
.enable_all_capabilities()
|
||||
.build()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// channels can be either in the form of '#museun' or 'museun'. the crate will internally add the missing #
|
||||
pub fn channels_to_join() -> anyhow::Result<Vec<String>> {
|
||||
let channels = "#vocodes,#testytest512,#ech310n" //get_env_var("TWITCH_CHANNEL")?
|
||||
.split(',')
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
// a 'main loop'
|
||||
pub async fn main_loop(mut runner: AsyncRunner) -> anyhow::Result<()> {
|
||||
loop {
|
||||
match runner.next_message().await? {
|
||||
// this is the parsed message -- across all channels (and notifications from Twitch)
|
||||
Status::Message(msg) => {
|
||||
println!("Handle message time!");
|
||||
handle_message(msg).await;
|
||||
}
|
||||
|
||||
// you signaled a quit
|
||||
Status::Quit => {
|
||||
println!("we signaled we wanted to quit");
|
||||
break;
|
||||
}
|
||||
// the connection closed normally
|
||||
Status::Eof => {
|
||||
println!("we got a 'normal' eof");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// you can generally ignore the lifetime for these types.
|
||||
async fn handle_message(msg: messages::Commands<'_>) {
|
||||
use messages::Commands::*;
|
||||
// All sorts of messages
|
||||
match msg {
|
||||
// This is the one users send to channels
|
||||
Privmsg(msg) => println!("[{}] {}: {}", msg.channel(), msg.name(), msg.data()),
|
||||
|
||||
// This one is special, if twitch adds any new message
|
||||
// types, this will catch it until future releases of
|
||||
// this crate add them.
|
||||
Raw(_) => {}
|
||||
|
||||
// These happen when you initially connect
|
||||
IrcReady(_) => {}
|
||||
Ready(_) => {}
|
||||
Cap(_) => {}
|
||||
|
||||
// and a bunch of other messages you may be interested in
|
||||
ClearChat(_) => {}
|
||||
ClearMsg(_) => {}
|
||||
GlobalUserState(_) => {}
|
||||
HostTarget(_) => {}
|
||||
Join(_) => {}
|
||||
Notice(_) => {}
|
||||
Part(_) => {}
|
||||
Ping(_) => {}
|
||||
Pong(_) => {}
|
||||
Reconnect(_) => {}
|
||||
RoomState(_) => {}
|
||||
UserNotice(_) => {}
|
||||
UserState(_) => {}
|
||||
Whisper(_) => {}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// NOTE: this demo requires `--features="tokio/full tokio-util"`.
|
||||
use twitchchat::{
|
||||
commands, connector, messages,
|
||||
runner::{AsyncRunner, Status},
|
||||
UserConfig,
|
||||
};
|
||||
|
||||
// this is a helper module to reduce code deduplication
|
||||
mod include;
|
||||
use crate::include::{channels_to_join, get_user_config, main_loop};
|
||||
|
||||
mod filesystem;
|
||||
|
||||
async fn connect(user_config: &UserConfig, channels: &[String]) -> anyhow::Result<AsyncRunner> {
|
||||
// create a connector using ``tokio``, this connects to Twitch.
|
||||
// you can provide a different address with `custom`
|
||||
let connector = connector::tokio::Connector::twitch()?;
|
||||
|
||||
println!("we're connecting!");
|
||||
// create a new runner. this is a provided async 'main loop'
|
||||
// this method will block until you're ready
|
||||
let mut runner = AsyncRunner::connect(connector, user_config).await?;
|
||||
println!("..and we're connected");
|
||||
|
||||
// and the identity Twitch gave you
|
||||
println!("our identity: {:#?}", runner.identity);
|
||||
|
||||
for channel in channels {
|
||||
// the runner itself has 'blocking' join/part to ensure you join/leave a channel.
|
||||
// these two methods return whether the connection was closed early.
|
||||
// we'll ignore it for this demo
|
||||
println!("attempting to join '{}'", channel);
|
||||
let _ = runner.join(&channel).await?;
|
||||
println!("joined '{}'!", channel);
|
||||
}
|
||||
|
||||
Ok(runner)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// create a user configuration
|
||||
let user_config = get_user_config()?;
|
||||
// get some channels to join from the environment
|
||||
let channels = channels_to_join()?;
|
||||
|
||||
// connect and join the provided channels
|
||||
let runner = connect(&user_config, &channels).await?;
|
||||
|
||||
// you can get a handle to shutdown the runner
|
||||
let quit_handle = runner.quit_handle();
|
||||
|
||||
// you can get a clonable writer
|
||||
let mut writer = runner.writer();
|
||||
|
||||
// spawn something off in the background that'll exit in 10 seconds
|
||||
tokio::spawn({
|
||||
let mut writer = writer.clone();
|
||||
let channels = channels.clone();
|
||||
async move {
|
||||
|
||||
for channel in &channels {
|
||||
println!("> SENDING HELLO");
|
||||
let cmd = commands::privmsg(&channel, "hello! testing from Rust");
|
||||
writer.encode(cmd).await.unwrap();
|
||||
|
||||
println!("> SENDING HELLO #2");
|
||||
let cmd = commands::me(&channel, "hello! testing from Rust");
|
||||
writer.encode(cmd).await.unwrap();
|
||||
}
|
||||
|
||||
|
||||
println!("in 100 seconds we'll exit");
|
||||
tokio::time::delay_for(std::time::Duration::from_secs(100)).await;
|
||||
|
||||
// send one final message to all channels
|
||||
for channel in &channels {
|
||||
let cmd = commands::privmsg(&channel, "goodbye, world");
|
||||
writer.encode(cmd).await.unwrap();
|
||||
}
|
||||
|
||||
println!("sending quit signal");
|
||||
quit_handle.notify().await;
|
||||
}
|
||||
});
|
||||
|
||||
// you can encode all sorts of 'commands'
|
||||
for channel in &channels {
|
||||
println!("> SENDING HELLO WORLD");
|
||||
writer
|
||||
.encode(commands::privmsg(channel, "hello world!"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
for channel in &channels {
|
||||
println!("> SENDING HELLO AGAIN");
|
||||
writer
|
||||
.encode(commands::privmsg(channel, "hello world again!"))
|
||||
.await?;
|
||||
}
|
||||
|
||||
println!("starting main loop");
|
||||
// your 'main loop'. you'll just call next_message() until you're done
|
||||
main_loop(runner).await
|
||||
}
|
||||
File renamed without changes.
Reference in new issue
Block a user