Merge pull request 'Simple Bot' (#3) from dev-init-bot into master
Reviewed-on: #3
This commit is contained in:
commit
0c9cd1b3ec
16 changed files with 1554 additions and 36 deletions
15
Cargo.toml
15
Cargo.toml
|
@ -2,8 +2,21 @@
|
||||||
name = "forcebot-rs-v2"
|
name = "forcebot-rs-v2"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
default-run = "forcebot-rs-v2"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
tokio = { version = "1.33.0", features = ["full"] }
|
tokio = { version = "1.33.0", features = ["full"] }
|
||||||
twitch-irc = "5.0.1"
|
twitch-irc = "5.0.1"
|
||||||
|
|
||||||
|
# [[bin]]
|
||||||
|
# name = "simple_bot"
|
||||||
|
# path = "src/simple_bot.rs"
|
||||||
|
|
||||||
|
# [[bin]]
|
||||||
|
# name = "simple_bot_listener"
|
||||||
|
# path = "src/simple_bot_listener.rs"
|
||||||
|
|
||||||
|
# [lib]
|
||||||
|
# name = "botlib"
|
||||||
|
# path = "src/lib.rs"
|
335
readme.md
335
readme.md
|
@ -1,6 +1,335 @@
|
||||||
Twitch bot written in rust
|
Twitch chat bot written in rust
|
||||||
|
|
||||||
|
# Quick Start
|
||||||
|
|
||||||
|
Run a Simple bot with Built in functionality
|
||||||
|
|
||||||
|
1. Generate a twitch access token
|
||||||
|
|
||||||
|
- Get a Bot Chat Token here - https://twitchtokengenerator.com
|
||||||
|
- More Info - https://dev.twitch.tv/docs/authentication
|
||||||
|
|
||||||
|
2. Define an `.env` file with the following
|
||||||
|
|
||||||
|
```
|
||||||
|
login_name=BOTNAME
|
||||||
|
access_token=ACCESS_TOKEN
|
||||||
|
bot_channels=BOTNAME
|
||||||
|
prefix=`
|
||||||
|
bot_admins=ADMIN
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Build & run
|
||||||
|
|
||||||
# Compile & Run
|
|
||||||
```
|
```
|
||||||
cargo run
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Example Bots
|
||||||
|
|
||||||
|
Use the following commands to build and run built-in bots. No coding required!
|
||||||
|
|
||||||
|
## New Bot
|
||||||
|
Run an empty simple bot that logs into chat and has minimum built in functions
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --bin new_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## WIP Customized Fun Bot
|
||||||
|
|
||||||
|
Run a forcebot with fun catered customizations
|
||||||
|
|
||||||
|
*ongoing work in progress*
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --bin fun_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Simple Debug Listener
|
||||||
|
Run a bot that listens to all messages and output to console
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --bin simple_debug_listener
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simple Command Bot
|
||||||
|
Run a bot that uses the `test` chat `Command` . `Commands` are prefixed and must be ran by a chatter with a `vip` badge or above
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --bin simple_command_bot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Moderator Reactor
|
||||||
|
Run a bot that listens for messages with the `moderator` badge, and replies to that mod with an emote
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --bin moderator_reactor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module loaded Bot
|
||||||
|
Run a bot that has a `test` chat `Command`. As the command was loaded through a module, moderators or broadcastors can `enable` or `disable` the module through chat commands
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run --bin simple_module
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# Example Code
|
||||||
|
|
||||||
|
## New Bot
|
||||||
|
|
||||||
|
Uses Env defined variables to create and run the bot
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* 1. Create the bot using env */
|
||||||
|
let bot = Bot::new();
|
||||||
|
|
||||||
|
/* 2. Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customize with Modules
|
||||||
|
|
||||||
|
A `Module` is a group of bot objects (eg `Command`) that elevated users can manage through built in `disable` and `enable` commands
|
||||||
|
|
||||||
|
Create a custom `Module` by :
|
||||||
|
|
||||||
|
1. Defining Functions that create the Custom Bot Objects (eg `Command`)
|
||||||
|
|
||||||
|
2. Define a function that creates a `Module` with the Custom Bot Objects loaded
|
||||||
|
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* load the Module */
|
||||||
|
bot.load_module(custom_mod::new());
|
||||||
|
|
||||||
|
/* Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub mod custom_mod {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module};
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
|
||||||
|
/// Module with a loaded command
|
||||||
|
pub fn new() -> Module {
|
||||||
|
/* 1. Create a new module */
|
||||||
|
let mut custom_mod = Module::new("test".to_string(), "".to_string());
|
||||||
|
|
||||||
|
/* 2. Load the cmd into a new module */
|
||||||
|
custom_mod.load_command(cmd_test());
|
||||||
|
|
||||||
|
custom_mod
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd_test() -> Command {
|
||||||
|
/* 1. Create a new cmd */
|
||||||
|
let mut cmd = Command::new("test".to_string(),"".to_string());
|
||||||
|
|
||||||
|
/* 2. Define exec callback */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
let _= bot.client.say_in_reply_to(
|
||||||
|
&msg, "test return".to_string()).await;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Set Command flags */
|
||||||
|
cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
cmd.set_admin_only(false);
|
||||||
|
cmd.set_min_badge(Badge::Moderator);
|
||||||
|
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simple Debug Listener
|
||||||
|
Bot with a simple listener that listens for all messages and prints in output
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::{asyncfn_box, Bot, Listener};
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* 1. Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* 2a. Create a new blank Listener */
|
||||||
|
let mut listener = Listener::new();
|
||||||
|
|
||||||
|
/* 2b. Set a trigger condition function for listener */
|
||||||
|
listener.set_trigger_cond_fn(
|
||||||
|
|_:Arc<Bot>,_:ServerMessage| true
|
||||||
|
);
|
||||||
|
|
||||||
|
/* 2c. Define an async fn callback execution */
|
||||||
|
async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
dbg!(message);
|
||||||
|
Result::Ok("Success".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2d. Set and Store the execution body using `async_box()` */
|
||||||
|
listener.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 3. Load the listener into the bot */
|
||||||
|
bot.load_listener(listener);
|
||||||
|
|
||||||
|
/* 4. Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Moderator Reactor
|
||||||
|
|
||||||
|
Example listener listens for a moderator badge and reply in chat
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
use forcebot_rs_v2::asyncfn_box;
|
||||||
|
use forcebot_rs_v2::Listener;
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* 1. Create a new blank Listener */
|
||||||
|
let mut listener = Listener::new();
|
||||||
|
|
||||||
|
/* 2. Set a trigger condition function for listener */
|
||||||
|
listener.set_trigger_cond_fn(
|
||||||
|
|_:Arc<Bot>,message:ServerMessage|
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
|
||||||
|
for badge in msg.badges {
|
||||||
|
if matches!(badge, x if x.name == "moderator") {
|
||||||
|
// dbg!("moderator found");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else { false }
|
||||||
|
);
|
||||||
|
|
||||||
|
/* 3. Define an async fn callback execution */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
let _ = bot.client.say_in_reply_to(&msg, "pepeKneel".to_string()).await ;
|
||||||
|
return Result::Ok("Success".to_string()) ;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Set and Store the execution body using `async_box()` */
|
||||||
|
listener.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 5. Load the listener into the bot */
|
||||||
|
bot.load_listener(listener);
|
||||||
|
|
||||||
|
/* Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Simple Test Command
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
use forcebot_rs_v2::asyncfn_box;
|
||||||
|
use forcebot_rs_v2::Command;
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* 1. Create a new blank cmd */
|
||||||
|
let mut cmd = Command::new("test".to_string(),"".to_string());
|
||||||
|
|
||||||
|
/* 2. Define an async fn callback execution */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
let _ = bot.client.say_in_reply_to(&msg, String::from("test success")).await;
|
||||||
|
return Result::Ok("Success".to_string()) ;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Set and Store the execution body using `async_box()` */
|
||||||
|
cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 4. optionally, remove admin only default flag */
|
||||||
|
cmd.set_admin_only(false);
|
||||||
|
|
||||||
|
/* 5. optionally, set min badge*/
|
||||||
|
cmd.set_min_badge("broadcaster".to_string());
|
||||||
|
//
|
||||||
|
/* 6. Load the cmd into the bot */
|
||||||
|
bot.load_command(cmd);
|
||||||
|
|
||||||
|
/* Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
# Crate Rust Documentation
|
||||||
|
|
||||||
|
Create Crate documentation
|
||||||
|
|
||||||
|
Clean Build Documentation
|
||||||
|
```
|
||||||
|
cargo clean && cargo doc
|
||||||
|
```
|
||||||
|
|
||||||
|
Open Crate Doc
|
||||||
|
```
|
||||||
|
cargo doc --open
|
||||||
|
```
|
||||||
|
|
70
src/bin/fun_bot.rs
Normal file
70
src/bin/fun_bot.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
//! WIP Fun forcebot with catered customizations #todo
|
||||||
|
//!
|
||||||
|
//! Custom modules that can be managed in chat through `disable` and `enable` commands
|
||||||
|
//! - funbot
|
||||||
|
//!
|
||||||
|
//! Be sure the followig is defined in `.env`
|
||||||
|
//! - login_name
|
||||||
|
//! - access_token
|
||||||
|
//! - bot_channels
|
||||||
|
//! - prefix
|
||||||
|
//! - bot_admins
|
||||||
|
//!
|
||||||
|
//! Bot access tokens be generated here -
|
||||||
|
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
|
||||||
|
//! - More Info - <https://dev.twitch.tv/docs/authentication>
|
||||||
|
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* 1. Load the module into the bot */
|
||||||
|
bot.load_module(funbot_objs::create_module());
|
||||||
|
|
||||||
|
/* 2. Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub mod funbot_objs {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module};
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
/// Create a Module with a loaded Command object
|
||||||
|
pub fn create_module() -> Module {
|
||||||
|
let mut custom_mod = Module::new("funbot".to_string(), "".to_string());
|
||||||
|
|
||||||
|
custom_mod.load_command(create_cmd_test());
|
||||||
|
|
||||||
|
custom_mod
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a Command Object
|
||||||
|
fn create_cmd_test() -> Command {
|
||||||
|
|
||||||
|
let mut cmd = Command::new("remind besty".to_string(),"annytfYandere ".to_string());
|
||||||
|
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
let _= bot.client.say_in_reply_to(
|
||||||
|
&msg, "annytfYandere he's mine".to_string()).await;
|
||||||
|
return Result::Ok("Success".to_string());
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
cmd.set_admin_only(false);
|
||||||
|
cmd.set_min_badge(Badge::Vip);
|
||||||
|
cmd
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
65
src/bin/moderator_reactor.rs
Normal file
65
src/bin/moderator_reactor.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
//! Simple bot with a custom listeners that listens for moderators and respond to the moderator
|
||||||
|
//!
|
||||||
|
//! Be sure the followig is defined in `.env`
|
||||||
|
//! - login_name
|
||||||
|
//! - access_token
|
||||||
|
//! - bot_channels
|
||||||
|
//! - prefix
|
||||||
|
//! - bot_admins
|
||||||
|
//!
|
||||||
|
//! Bot access tokens be generated here -
|
||||||
|
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
|
||||||
|
//! - More Info - <https://dev.twitch.tv/docs/authentication>
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
use forcebot_rs_v2::asyncfn_box;
|
||||||
|
use forcebot_rs_v2::Listener;
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* 1. Create a new blank Listener */
|
||||||
|
let mut listener = Listener::new();
|
||||||
|
|
||||||
|
/* 2. Set a trigger condition function for listener */
|
||||||
|
|
||||||
|
listener.set_trigger_cond_fn(
|
||||||
|
|_:Arc<Bot>,message:ServerMessage|
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
// dbg!(msg.clone());
|
||||||
|
for badge in msg.badges {
|
||||||
|
if matches!(badge, x if x.name == "moderator") {
|
||||||
|
// dbg!("moderator found");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else { false }
|
||||||
|
);
|
||||||
|
|
||||||
|
/* 3. Define an async fn callback execution */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
let _ = bot.client.say_in_reply_to(&msg, "pepeKneel".to_string()).await ;
|
||||||
|
return Result::Ok("Success".to_string()) ;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Set and Store the execution body using `async_box()` */
|
||||||
|
listener.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 5. Load the listener into the bot */
|
||||||
|
bot.load_listener(listener);
|
||||||
|
|
||||||
|
/* Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
24
src/bin/new_bot.rs
Normal file
24
src/bin/new_bot.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
//! Example simple Binary crate that creates & runs bot based on `.env`
|
||||||
|
//! Be sure the followig is defined in `.env`
|
||||||
|
//! - login_name
|
||||||
|
//! - access_token
|
||||||
|
//! - bot_channels
|
||||||
|
//! - prefix
|
||||||
|
//! - bot_admins
|
||||||
|
//!
|
||||||
|
//! Bot access tokens be generated here -
|
||||||
|
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
|
||||||
|
//! - More Info - <https://dev.twitch.tv/docs/authentication>
|
||||||
|
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* 1. Create the bot using env */
|
||||||
|
let bot = Bot::new();
|
||||||
|
|
||||||
|
/* 2. Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
58
src/bin/simple_command_bot.rs
Normal file
58
src/bin/simple_command_bot.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
//! Bot with custom example commands that responds to caller if allowed
|
||||||
|
//!
|
||||||
|
//! Commands that are passed a blank prefix will use the bot prefix
|
||||||
|
//!
|
||||||
|
//! Be sure the followig is defined in `.env`
|
||||||
|
//! - login_name
|
||||||
|
//! - access_token
|
||||||
|
//! - bot_channels
|
||||||
|
//! - prefix
|
||||||
|
//! - bot_admins
|
||||||
|
//!
|
||||||
|
//! Bot access tokens be generated here -
|
||||||
|
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
|
||||||
|
//! - More Info - <https://dev.twitch.tv/docs/authentication>
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::Badge;
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
use forcebot_rs_v2::asyncfn_box;
|
||||||
|
use forcebot_rs_v2::Command;
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* 1. Create a new blank cmd */
|
||||||
|
let mut cmd = Command::new("test".to_string(),"".to_string());
|
||||||
|
|
||||||
|
/* 2. Define an async fn callback execution */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
let _ = bot.client.say_in_reply_to(&msg, String::from("test success")).await;
|
||||||
|
return Result::Ok("Success".to_string()) ;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Set and Store the execution body using `async_box()` */
|
||||||
|
cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 4. optionally, remove admin only default flag */
|
||||||
|
cmd.set_admin_only(false);
|
||||||
|
|
||||||
|
/* 5. optionally, set min badge*/
|
||||||
|
cmd.set_min_badge(Badge::Moderator);
|
||||||
|
|
||||||
|
/* 6. Load the cmd into the bot */
|
||||||
|
bot.load_command(cmd);
|
||||||
|
|
||||||
|
/* Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
47
src/bin/simple_debug_listener.rs
Normal file
47
src/bin/simple_debug_listener.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
//! Example simple Binary crate that creates & runs bot based on `.env`
|
||||||
|
//! Be sure the followig is defined in `.env`
|
||||||
|
//! - login_name
|
||||||
|
//! - access_token
|
||||||
|
//! - bot_channels
|
||||||
|
//! - prefix
|
||||||
|
//! - bot_admins
|
||||||
|
//!
|
||||||
|
//! Bot access tokens be generated here -
|
||||||
|
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
|
||||||
|
//! - More Info - <https://dev.twitch.tv/docs/authentication>
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::{asyncfn_box, Bot, Listener};
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* 1. Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* 2a. Create a new blank Listener */
|
||||||
|
let mut listener = Listener::new();
|
||||||
|
|
||||||
|
/* 2b. Set a trigger condition function for listener */
|
||||||
|
listener.set_trigger_cond_fn(
|
||||||
|
|_:Arc<Bot>,_:ServerMessage| true
|
||||||
|
);
|
||||||
|
|
||||||
|
/* 2c. Define an async fn callback execution */
|
||||||
|
async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
dbg!(message);
|
||||||
|
Result::Ok("Success".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2d. Set and Store the execution body using `async_box()` */
|
||||||
|
listener.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 3. Load the listener into the bot */
|
||||||
|
bot.load_listener(listener);
|
||||||
|
|
||||||
|
/* 4. Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
76
src/bin/simple_module.rs
Normal file
76
src/bin/simple_module.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
//! Simple Module with a Command
|
||||||
|
//!
|
||||||
|
//! Adding objects through packages provides controls ,
|
||||||
|
//! such as moderators, and brodcasters can disable or enable mods
|
||||||
|
//!
|
||||||
|
//! Here, moderators or above can enable or disable the `test`
|
||||||
|
//! module with the command `<prefix> disable test`
|
||||||
|
//!
|
||||||
|
//! Be sure the followig is defined in `.env`
|
||||||
|
//! - login_name
|
||||||
|
//! - access_token
|
||||||
|
//! - bot_channels
|
||||||
|
//! - prefix
|
||||||
|
//! - bot_admins
|
||||||
|
//!
|
||||||
|
//! Bot access tokens be generated here -
|
||||||
|
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
|
||||||
|
//! - More Info - <https://dev.twitch.tv/docs/authentication>
|
||||||
|
|
||||||
|
use forcebot_rs_v2::Bot;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
pub async fn main() {
|
||||||
|
|
||||||
|
/* Create the bot using env */
|
||||||
|
let mut bot = Bot::new();
|
||||||
|
|
||||||
|
/* load the Module */
|
||||||
|
bot.load_module(custom_mod::new());
|
||||||
|
|
||||||
|
/* Run the bot */
|
||||||
|
bot.run().await;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub mod custom_mod {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module};
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
|
||||||
|
/// Module with a loaded command
|
||||||
|
pub fn new() -> Module {
|
||||||
|
/* 1. Create a new module */
|
||||||
|
let mut custom_mod = Module::new("test".to_string(), "".to_string());
|
||||||
|
|
||||||
|
/* 2. Load the cmd into a new module */
|
||||||
|
custom_mod.load_command(cmd_test());
|
||||||
|
|
||||||
|
custom_mod
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cmd_test() -> Command {
|
||||||
|
/* 1. Create a new cmd */
|
||||||
|
let mut cmd = Command::new("test".to_string(),"".to_string());
|
||||||
|
|
||||||
|
/* 2. Define exec callback */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
let _= bot.client.say_in_reply_to(
|
||||||
|
&msg, "test return".to_string()).await;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Set Command flags */
|
||||||
|
cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
cmd.set_admin_only(false);
|
||||||
|
cmd.set_min_badge(Badge::Moderator);
|
||||||
|
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod bot;
|
||||||
|
pub mod bot_objects;
|
||||||
|
pub mod modules;
|
238
src/botcore/bot.rs
Normal file
238
src/botcore/bot.rs
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
|
||||||
|
|
||||||
|
use tokio::sync::{mpsc::UnboundedReceiver, Mutex};
|
||||||
|
use twitch_irc::{login::StaticLoginCredentials, message::ServerMessage, SecureTCPTransport, TwitchIRCClient};
|
||||||
|
use dotenv::dotenv;
|
||||||
|
use std::{env, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{Command, Listener, Module};
|
||||||
|
|
||||||
|
use super::bot_objects::built_in_objects;
|
||||||
|
|
||||||
|
|
||||||
|
/// Twitch chat bot
|
||||||
|
pub struct Bot
|
||||||
|
{
|
||||||
|
/// Prefix for commands
|
||||||
|
prefix: String,
|
||||||
|
/// inbound chat msg stream
|
||||||
|
incoming_msgs: Mutex<UnboundedReceiver<ServerMessage>>,
|
||||||
|
/// outbound chat client msg stream
|
||||||
|
pub client: TwitchIRCClient<SecureTCPTransport,StaticLoginCredentials>,
|
||||||
|
/// joined channels
|
||||||
|
botchannels: Vec<String>,
|
||||||
|
/// admin chatters
|
||||||
|
admins : Vec<String>,
|
||||||
|
/// listeners
|
||||||
|
listeners: Vec<Listener>,
|
||||||
|
/// commands
|
||||||
|
commands: Vec<Command>,
|
||||||
|
/// modules
|
||||||
|
modules: Vec<Module>,
|
||||||
|
/// channel module status
|
||||||
|
channel_module_status: Mutex<Vec<(String,String,String)>>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Bot
|
||||||
|
{
|
||||||
|
/// Creates a new `Bot` using env variables
|
||||||
|
///
|
||||||
|
/// Be sure the following is defined in an `.env` file
|
||||||
|
/// - login_name
|
||||||
|
/// - access_token
|
||||||
|
/// - bot_channels
|
||||||
|
/// - prefix
|
||||||
|
/// - bot_admins
|
||||||
|
pub fn new() -> Bot {
|
||||||
|
|
||||||
|
dotenv().ok();
|
||||||
|
let bot_login_name = env::var("login_name").unwrap().to_owned();
|
||||||
|
let oauth_token = env::var("access_token").unwrap().to_owned();
|
||||||
|
let prefix = env::var("prefix")
|
||||||
|
.unwrap()
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let mut botchannels = Vec::new();
|
||||||
|
|
||||||
|
for chnl in env::var("bot_channels").unwrap().split(',') {
|
||||||
|
botchannels.push(chnl.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
Bot::new_from(bot_login_name, oauth_token, prefix, botchannels)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new `Bot` using bot information
|
||||||
|
///
|
||||||
|
/// Bot joined channels will include channels from `.env` and `botchannels` argument
|
||||||
|
pub fn new_from(bot_login_name:String,oauth_token:String,prefix:String,botchannels:Vec<String>) -> Bot {
|
||||||
|
|
||||||
|
dotenv().ok();
|
||||||
|
let bot_login_name = bot_login_name;
|
||||||
|
|
||||||
|
let config = twitch_irc::ClientConfig::new_simple(StaticLoginCredentials::new(
|
||||||
|
bot_login_name.to_owned(),
|
||||||
|
Some(oauth_token.to_owned()),
|
||||||
|
));
|
||||||
|
|
||||||
|
let (incoming_messages, client) =
|
||||||
|
TwitchIRCClient::<SecureTCPTransport, StaticLoginCredentials>::new(config);
|
||||||
|
|
||||||
|
let mut botchannels_all = Vec::new();
|
||||||
|
botchannels_all.extend(botchannels);
|
||||||
|
|
||||||
|
for chnl in env::var("bot_channels").unwrap().split(',') {
|
||||||
|
botchannels_all.push(chnl.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let mut admins = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(value) = env::var("bot_admins") {
|
||||||
|
for admin in value.split(',') {
|
||||||
|
admins.push(String::from(admin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let mut bot = Bot {
|
||||||
|
prefix,
|
||||||
|
incoming_msgs : Mutex::new(incoming_messages),
|
||||||
|
client,
|
||||||
|
botchannels : botchannels_all,
|
||||||
|
listeners : vec![],
|
||||||
|
commands : vec![],
|
||||||
|
admins,
|
||||||
|
modules: vec![],
|
||||||
|
channel_module_status: Mutex::new(vec![]),
|
||||||
|
};
|
||||||
|
|
||||||
|
for cmd in built_in_objects::create_commands() {
|
||||||
|
bot.load_command(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
bot
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the bot
|
||||||
|
pub async fn run(self) {
|
||||||
|
|
||||||
|
for chnl in &self.botchannels {
|
||||||
|
self.client.join(chnl.to_owned()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let bot = Arc::new(self);
|
||||||
|
|
||||||
|
let join_handle = tokio::spawn(async move {
|
||||||
|
|
||||||
|
let a = bot.clone();
|
||||||
|
let mut in_msgs_lock = a.incoming_msgs.lock().await;
|
||||||
|
|
||||||
|
while let Some(message) = in_msgs_lock.recv().await {
|
||||||
|
for listener in &(*bot).listeners {
|
||||||
|
|
||||||
|
let a = listener.clone();
|
||||||
|
if a.cond_triggered(bot.clone(),message.clone()) {
|
||||||
|
|
||||||
|
let _ = listener.execute_fn(bot.clone(),message.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ServerMessage::Privmsg(msg) = message.clone() {
|
||||||
|
|
||||||
|
for cmd in &(*bot).commands {
|
||||||
|
|
||||||
|
let a = cmd.clone();
|
||||||
|
if a.command_triggered(bot.clone(),msg.clone()) {
|
||||||
|
|
||||||
|
let _ = cmd.execute_fn(bot.clone(),message.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for module in &(*bot).modules {
|
||||||
|
|
||||||
|
let cms_lock = bot.channel_module_status.lock().await;
|
||||||
|
if cms_lock.contains(&(msg.channel_login.clone(),module.get_name(),"disabled".to_string()))
|
||||||
|
{ continue; }
|
||||||
|
|
||||||
|
for listener in module.get_listeners() {
|
||||||
|
|
||||||
|
let a = listener.clone();
|
||||||
|
if a.cond_triggered(bot.clone(),message.clone()) {
|
||||||
|
|
||||||
|
let _ = listener.execute_fn(bot.clone(),message.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for cmd in module.get_commands() {
|
||||||
|
|
||||||
|
let a = cmd.clone();
|
||||||
|
if a.command_triggered(bot.clone(),msg.clone()) {
|
||||||
|
|
||||||
|
let _ = cmd.execute_fn(bot.clone(),message.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {} ;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
drop(in_msgs_lock);
|
||||||
|
});
|
||||||
|
|
||||||
|
join_handle.await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a `Listener` into the bot
|
||||||
|
pub fn load_listener(&mut self,l : Listener) {
|
||||||
|
self.listeners.push(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a `Command` into the bot
|
||||||
|
pub fn load_command(&mut self,c : Command) {
|
||||||
|
self.commands.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_prefix(&self) -> String {
|
||||||
|
self.prefix.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_admins(&self) -> Vec<String> {
|
||||||
|
self.admins.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// loads a `Module` and its bot objects
|
||||||
|
pub fn load_module(&mut self,m: Module) {
|
||||||
|
self.modules.push(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn disable_module(&self,channel:String,module:String){
|
||||||
|
let mut lock = self.channel_module_status.lock().await;
|
||||||
|
if !lock.contains(&(channel.clone(),module.clone(),"disabled".to_string())) {
|
||||||
|
lock.push((channel,module,"disabled".to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn enable_module(&self,channel:String,module:String){
|
||||||
|
let mut lock = self.channel_module_status.lock().await;
|
||||||
|
if lock.contains(&(channel.clone(),module.clone(),"disabled".to_string())) {
|
||||||
|
|
||||||
|
let index = lock
|
||||||
|
.iter()
|
||||||
|
.position(|x| *x ==
|
||||||
|
(channel.clone(),module.clone(),"disabled".to_string()))
|
||||||
|
.unwrap();
|
||||||
|
lock.remove(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
119
src/botcore/bot_objects.rs
Normal file
119
src/botcore/bot_objects.rs
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
pub mod listener;
|
||||||
|
pub mod command;
|
||||||
|
|
||||||
|
|
||||||
|
use std::boxed::Box;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
use super::bot::Bot;
|
||||||
|
|
||||||
|
|
||||||
|
/// chat badge
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Badge {
|
||||||
|
Moderator,
|
||||||
|
Broadcaster,
|
||||||
|
Vip
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub type ExecBody = Box<
|
||||||
|
dyn Fn(Arc<Bot>,ServerMessage) -> Pin<Box<dyn Future<Output = Result<String,String>> + Send>> + Send + Sync,
|
||||||
|
>;
|
||||||
|
|
||||||
|
pub fn asyncfn_box<T>(f: fn(Arc<Bot>,ServerMessage) -> T) -> ExecBody
|
||||||
|
where
|
||||||
|
T: Future<Output = Result<String,String>> + Send + 'static,
|
||||||
|
{
|
||||||
|
Box::new(move |a,b| Box::pin(f(a,b)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// collection of functions to create built in objects
|
||||||
|
pub mod built_in_objects {
|
||||||
|
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
use crate::{asyncfn_box, Badge, Bot, Command};
|
||||||
|
|
||||||
|
|
||||||
|
/// create a vector of command build in objects
|
||||||
|
pub fn create_commands() -> Vec<Command>
|
||||||
|
{
|
||||||
|
let mut cmds = vec![];
|
||||||
|
|
||||||
|
cmds.push(create_disable_cmd());
|
||||||
|
cmds.push(create_enable_cmd());
|
||||||
|
|
||||||
|
cmds
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_disable_cmd() -> Command {
|
||||||
|
/* 1. Create a new blank cmd */
|
||||||
|
let mut cmd = Command::new("disable".to_string(),"".to_string());
|
||||||
|
|
||||||
|
/* 2. Define an async fn callback execution */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
for (i,arg) in msg.message_text.split(" ").enumerate() {
|
||||||
|
if i > 1 {
|
||||||
|
bot.disable_module(msg.channel_login.clone(), arg.to_string()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = bot.client.say_in_reply_to(&msg, String::from("Disabled!")).await ;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Set and Store the execution body using `async_box()` */
|
||||||
|
cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 4. optionally, remove admin only default flag */
|
||||||
|
cmd.set_admin_only(false);
|
||||||
|
|
||||||
|
/* 5. optionally, set min badge*/
|
||||||
|
cmd.set_min_badge(Badge::Moderator);
|
||||||
|
cmd
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_enable_cmd() -> Command {
|
||||||
|
/* 1. Create a new blank cmd */
|
||||||
|
let mut cmd = Command::new("enable".to_string(),"".to_string());
|
||||||
|
|
||||||
|
/* 2. Define an async fn callback execution */
|
||||||
|
async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
for (i,arg) in msg.message_text.split(" ").enumerate() {
|
||||||
|
if i > 1 {
|
||||||
|
bot.enable_module(msg.channel_login.clone(), arg.to_string()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = bot.client.say_in_reply_to(&msg, String::from("Enabled!")).await ;
|
||||||
|
}
|
||||||
|
Result::Err("Not Valid message type".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Set and Store the execution body using `async_box()` */
|
||||||
|
cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
|
||||||
|
/* 4. optionally, remove admin only default flag */
|
||||||
|
cmd.set_admin_only(false);
|
||||||
|
|
||||||
|
/* 5. optionally, set min badge*/
|
||||||
|
cmd.set_min_badge(Badge::Moderator);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
162
src/botcore/bot_objects/command.rs
Normal file
162
src/botcore/bot_objects/command.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twitch_irc::message::{PrivmsgMessage, ServerMessage};
|
||||||
|
|
||||||
|
use crate::{asyncfn_box, botcore::bot::Bot, Badge};
|
||||||
|
|
||||||
|
use super::ExecBody;
|
||||||
|
|
||||||
|
/// Bot `Command` that stores trigger condition callback and a execution functon
|
||||||
|
///
|
||||||
|
/// A prefix character or phrase can be defined for the bot to evaluate a trigger condition
|
||||||
|
///
|
||||||
|
/// A command or command phrase defines the phrase after the prefix phrase
|
||||||
|
///
|
||||||
|
/// If no min badge role is provided, Broadcaster is defaulted. All commands require at least a vip role
|
||||||
|
///
|
||||||
|
/// AdminOnly commands can only be ran by admin
|
||||||
|
///
|
||||||
|
/// Use `asyncfn_box()` on custom async execution bodies
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Command
|
||||||
|
{
|
||||||
|
command : String,
|
||||||
|
exec_fn : Arc<ExecBody>,
|
||||||
|
min_badge : Badge,
|
||||||
|
admin_only : bool,
|
||||||
|
prefix : String,
|
||||||
|
custom_cond_fn : fn(Arc<Bot>,PrivmsgMessage) -> bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Creates a new empty `Command` using command `String` and prefix `String`
|
||||||
|
/// Pass an empty string prefix if the bot should use the bot default
|
||||||
|
///
|
||||||
|
/// Call `set_trigger_cond_fn()` and `set_exec_fn()` to trigger & execution function callbacks
|
||||||
|
/// if a blank prefix is given, the bot will look for the bot prefix instead
|
||||||
|
///
|
||||||
|
/// By default, the new command is admin_only
|
||||||
|
pub fn new(command:String,prefix:String) -> Command {
|
||||||
|
|
||||||
|
async fn execbody(_:Arc<Bot>,_:ServerMessage) -> Result<String,String>
|
||||||
|
{ Result::Ok("success".to_string()) }
|
||||||
|
|
||||||
|
Command {
|
||||||
|
command ,
|
||||||
|
prefix ,
|
||||||
|
exec_fn : Arc::new(asyncfn_box(execbody)),
|
||||||
|
min_badge : Badge::Vip,
|
||||||
|
admin_only : true,
|
||||||
|
custom_cond_fn : |_:Arc<Bot>,_:PrivmsgMessage| true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set a trigger conditin callback that returns true if the listener shoud trigger
|
||||||
|
pub fn set_custom_cond_fn(&mut self,cond_fn: fn(Arc<Bot>,PrivmsgMessage) -> bool) {
|
||||||
|
self.custom_cond_fn = cond_fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the execution body of the listener for when it triggers
|
||||||
|
///
|
||||||
|
/// Use`asyncfn_box()` on the async fn when storing
|
||||||
|
///
|
||||||
|
///
|
||||||
|
pub fn set_exec_fn(&mut self,exec_fn:ExecBody ) {
|
||||||
|
self.exec_fn = Arc::new(exec_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// checks if the trigger condition is met
|
||||||
|
/// specifically if the message is a valid command and min badge roles provided
|
||||||
|
///
|
||||||
|
pub fn command_triggered(&self,bot:Arc<Bot>,msg:PrivmsgMessage) -> bool {
|
||||||
|
|
||||||
|
fn cmd_called(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool {
|
||||||
|
let mut prefixed_cmd = "".to_string();
|
||||||
|
if cmd.prefix == "" {
|
||||||
|
prefixed_cmd.push_str(&bot.get_prefix());
|
||||||
|
} else {
|
||||||
|
prefixed_cmd.push_str(&cmd.prefix);
|
||||||
|
}
|
||||||
|
prefixed_cmd.push_str(&cmd.command);
|
||||||
|
return message.message_text.starts_with(prefixed_cmd.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fn caller_badge_ok(cmd:&Command,_bot:Arc<Bot>,message:PrivmsgMessage) -> bool {
|
||||||
|
for badge in message.badges {
|
||||||
|
|
||||||
|
match cmd.min_badge {
|
||||||
|
Badge::Broadcaster => {
|
||||||
|
if badge.name == "broadcaster" { return true }
|
||||||
|
else { return false }
|
||||||
|
},
|
||||||
|
Badge::Moderator => {
|
||||||
|
match badge.name.as_str() {
|
||||||
|
"moderator" | "broadcaster" => return true,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Badge::Vip => {
|
||||||
|
match badge.name.as_str() {
|
||||||
|
"vip" | "moderator" | "broadcaster" => return true,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// determines if the command caller can run the command
|
||||||
|
/// based on admin_only flag
|
||||||
|
///
|
||||||
|
/// callers who are admins can run admin_only commands
|
||||||
|
/// callers can run non-admin_only commands
|
||||||
|
fn admin_only_ok(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool {
|
||||||
|
if (cmd.admin_only && bot.get_admins().contains(&message.sender.login)) || !cmd.admin_only {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_cond_ok(cmd:&Command,bot:Arc<Bot>,message:PrivmsgMessage) -> bool {
|
||||||
|
(cmd.custom_cond_fn)(bot,message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbg!(msg.clone());
|
||||||
|
|
||||||
|
// dbg!(caller_badge_ok(self, bot.clone(), msg.clone()));
|
||||||
|
// dbg!(cmd_called(self, bot.clone(), msg.clone()) &&
|
||||||
|
// caller_badge_ok(self, bot.clone(), msg.clone()) &&
|
||||||
|
// admin_only_ok(self, bot.clone(), msg.clone()) &&
|
||||||
|
// custom_cond_ok(self, bot.clone(), msg.clone()));
|
||||||
|
|
||||||
|
cmd_called(self, bot.clone(), msg.clone()) &&
|
||||||
|
caller_badge_ok(self, bot.clone(), msg.clone()) &&
|
||||||
|
admin_only_ok(self, bot.clone(), msg.clone()) &&
|
||||||
|
custom_cond_ok(self, bot, msg)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// executes the listeners executon body
|
||||||
|
pub async fn execute_fn(&self,bot:Arc<Bot>,msg:ServerMessage) -> Result<String, String> {
|
||||||
|
(self.exec_fn)(bot,msg).await
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// sets min_badge to run the cmd
|
||||||
|
// pub fn set_min_badge(&mut self,min_badge:String) {
|
||||||
|
pub fn set_min_badge(&mut self,min_badge:Badge) {
|
||||||
|
self.min_badge = min_badge
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets admin_only
|
||||||
|
pub fn set_admin_only(&mut self,admin_only:bool) {
|
||||||
|
self.admin_only = admin_only
|
||||||
|
}
|
||||||
|
}
|
72
src/botcore/bot_objects/listener.rs
Normal file
72
src/botcore/bot_objects/listener.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use twitch_irc::message::ServerMessage;
|
||||||
|
|
||||||
|
use crate::{asyncfn_box, Bot};
|
||||||
|
|
||||||
|
use super::ExecBody;
|
||||||
|
|
||||||
|
/// Bot `Listener`` that stores trigger condition callback and a execution functon
|
||||||
|
///
|
||||||
|
/// Use `asyncfn_box()` on custom async execution bodies
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Listener
|
||||||
|
{
|
||||||
|
/// trigger condition
|
||||||
|
trigger_cond_fn : fn(Arc<Bot>,ServerMessage) -> bool,
|
||||||
|
/// execution body
|
||||||
|
exec_fn : Arc<ExecBody>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Listener
|
||||||
|
{
|
||||||
|
|
||||||
|
/// Creates a new empty `Listener` .
|
||||||
|
/// Call `set_trigger_cond_fn()` and `set_exec_fn()` to trigger & execution function callbacks
|
||||||
|
pub fn new() -> Listener {
|
||||||
|
|
||||||
|
async fn execbody(_:Arc<Bot>,_:ServerMessage) -> Result<String,String> {Result::Ok("success".to_string()) }
|
||||||
|
|
||||||
|
Listener {
|
||||||
|
trigger_cond_fn : |_:Arc<Bot>,_:ServerMessage| false,
|
||||||
|
exec_fn : Arc::new(asyncfn_box(execbody))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set a trigger conditin callback that returns true if the listener shoud trigger
|
||||||
|
pub fn set_trigger_cond_fn(&mut self,cond_fn: fn(Arc<Bot>,ServerMessage) -> bool) {
|
||||||
|
self.trigger_cond_fn = cond_fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the execution body of the listener for when it triggers
|
||||||
|
///
|
||||||
|
/// Use`asyncfn_box()` on the async fn when storing
|
||||||
|
///
|
||||||
|
/// Example -
|
||||||
|
/// ```rust
|
||||||
|
/// /* 1. Create a new blank Listener */
|
||||||
|
/// let mut listener = Listener::new();
|
||||||
|
///
|
||||||
|
/// /* 2. define an async function */
|
||||||
|
/// async fn execbody(_:Arc<Bot>,_:ServerMessage) -> Result<String,String> {Result::Ok("success".to_string()) }
|
||||||
|
///
|
||||||
|
/// /* 3. Set and Store the execution body using `async_box()` */
|
||||||
|
/// listener.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
pub fn set_exec_fn(&mut self,exec_fn:ExecBody ) {
|
||||||
|
self.exec_fn = Arc::new(exec_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// checks if the trigger condition is met
|
||||||
|
pub fn cond_triggered(&self,bot:Arc<Bot>,msg:ServerMessage) -> bool {
|
||||||
|
(self.trigger_cond_fn)(bot,msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// executes the listeners executon body
|
||||||
|
pub async fn execute_fn(&self,bot:Arc<Bot>,msg:ServerMessage) -> Result<String, String> {
|
||||||
|
// (self.exec_fn)(bot,msg)
|
||||||
|
(self.exec_fn)(bot,msg).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
51
src/botcore/modules.rs
Normal file
51
src/botcore/modules.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
|
||||||
|
use crate::{Command, Listener};
|
||||||
|
|
||||||
|
|
||||||
|
/// Bot `Module` that groups a set of `bot_objects`
|
||||||
|
///
|
||||||
|
/// Elevated chatters can disable modules by their name or chat alias
|
||||||
|
pub struct Module
|
||||||
|
{
|
||||||
|
name: String,
|
||||||
|
_alias: String,
|
||||||
|
listeners: Vec<Listener>,
|
||||||
|
commands: Vec<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module
|
||||||
|
{
|
||||||
|
/// create a new module
|
||||||
|
pub fn new(name:String,alias:String) -> Module {
|
||||||
|
Module {
|
||||||
|
name,
|
||||||
|
_alias: alias,
|
||||||
|
listeners: vec![],
|
||||||
|
commands: vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a `Listener` into the module
|
||||||
|
pub fn load_listener(&mut self,l : Listener) {
|
||||||
|
self.listeners.push(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a `Command` into the module
|
||||||
|
pub fn load_command(&mut self,c : Command) {
|
||||||
|
self.commands.push(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_listeners(&self) -> Vec<Listener> {
|
||||||
|
self.listeners.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_commands(&self) -> Vec<Command> {
|
||||||
|
self.commands.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_name(&self) -> String {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
206
src/lib.rs
Normal file
206
src/lib.rs
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
//! `forcebot-rs-v2` : Twitch chat bot written in rust.
|
||||||
|
//!
|
||||||
|
//! Customize by adding additional bot objects
|
||||||
|
//!
|
||||||
|
//! ## New Bot
|
||||||
|
//!
|
||||||
|
//! Uses Env defined variables to create and run the bot
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use forcebot_rs_v2::Bot;
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! pub async fn main() {
|
||||||
|
//!
|
||||||
|
//! /* 1. Create the bot using env */
|
||||||
|
//! let bot = Bot::new();
|
||||||
|
//!
|
||||||
|
//! /* 2. Run the bot */
|
||||||
|
//! bot.run().await;
|
||||||
|
//!
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! ## Customize with Modules
|
||||||
|
//!
|
||||||
|
//! A `Module` is a group of bot objects (eg `Command`) that elevated users can manage through built in `disable` and `enable` commands
|
||||||
|
//!
|
||||||
|
//! Create a custom `Module` by :
|
||||||
|
//!
|
||||||
|
//! 1. Defining Functions that create the Custom Bot Objects (eg `Command`)
|
||||||
|
//!
|
||||||
|
//! 2. Define a function that creates a `Module` with the Custom Bot Objects loaded
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use forcebot_rs_v2::Bot;
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! pub async fn main() {
|
||||||
|
//!
|
||||||
|
//! /* Create the bot using env */
|
||||||
|
//! let mut bot = Bot::new();
|
||||||
|
//!
|
||||||
|
//! /* load the Module */
|
||||||
|
//! bot.load_module(custom_mod::new());
|
||||||
|
//!
|
||||||
|
//! /* Run the bot */
|
||||||
|
//! bot.run().await;
|
||||||
|
//!
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! pub mod custom_mod {
|
||||||
|
//! use std::sync::Arc;
|
||||||
|
//!
|
||||||
|
//! use forcebot_rs_v2::{asyncfn_box, Badge, Bot, Command, Module};
|
||||||
|
//! use twitch_irc::message::ServerMessage;
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! /// Module with a loaded command
|
||||||
|
//! pub fn new() -> Module {
|
||||||
|
//! /* 1. Create a new module */
|
||||||
|
//! let mut custom_mod = Module::new("test".to_string(), "".to_string());
|
||||||
|
//!
|
||||||
|
//! /* 2. Load the cmd into a new module */
|
||||||
|
//! custom_mod.load_command(cmd_test());
|
||||||
|
//!
|
||||||
|
//! custom_mod
|
||||||
|
//!
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! pub fn cmd_test() -> Command {
|
||||||
|
//! /* 1. Create a new cmd */
|
||||||
|
//! let mut cmd = Command::new("test".to_string(),"".to_string());
|
||||||
|
//!
|
||||||
|
//! /* 2. Define exec callback */
|
||||||
|
//! async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
//! if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
//! let _= bot.client.say_in_reply_to(
|
||||||
|
//! &msg, "test return".to_string()).await;
|
||||||
|
//! }
|
||||||
|
//! Result::Err("Not Valid message type".to_string())
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! /* 3. Set Command flags */
|
||||||
|
//! cmd.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
//! cmd.set_admin_only(false);
|
||||||
|
//! cmd.set_min_badge(Badge::Moderator);
|
||||||
|
//!
|
||||||
|
//! cmd
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Simple Debug Listener
|
||||||
|
//! Bot with a simple listener that listens for all messages and prints in output
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use std::sync::Arc;
|
||||||
|
//!
|
||||||
|
//! use forcebot_rs_v2::{asyncfn_box, Bot, Listener};
|
||||||
|
//! use twitch_irc::message::ServerMessage;
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! pub async fn main() {
|
||||||
|
//!
|
||||||
|
//! /* 1. Create the bot using env */
|
||||||
|
//! let mut bot = Bot::new();
|
||||||
|
//!
|
||||||
|
//! /* 2a. Create a new blank Listener */
|
||||||
|
//! let mut listener = Listener::new();
|
||||||
|
//!
|
||||||
|
//! /* 2b. Set a trigger condition function for listener */
|
||||||
|
//! listener.set_trigger_cond_fn(
|
||||||
|
//! |_:Arc<Bot>,_:ServerMessage| true
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! /* 2c. Define an async fn callback execution */
|
||||||
|
//! async fn execbody(_:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
//! dbg!(message);
|
||||||
|
//! Result::Ok("Success".to_string())
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! /* 2d. Set and Store the execution body using `async_box()` */
|
||||||
|
//! listener.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
//!
|
||||||
|
//! /* 3. Load the listener into the bot */
|
||||||
|
//! bot.load_listener(listener);
|
||||||
|
//!
|
||||||
|
//! /* 4. Run the bot */
|
||||||
|
//! bot.run().await;
|
||||||
|
//!
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## Modertor Reactor
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! use std::sync::Arc;
|
||||||
|
//!
|
||||||
|
//! use forcebot_rs_v2::Bot;
|
||||||
|
//! use forcebot_rs_v2::asyncfn_box;
|
||||||
|
//! use forcebot_rs_v2::Listener;
|
||||||
|
//! use twitch_irc::message::ServerMessage;
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! #[tokio::main]
|
||||||
|
//! pub async fn main() {
|
||||||
|
//!
|
||||||
|
//! /* Create the bot using env */
|
||||||
|
//! let mut bot = Bot::new();
|
||||||
|
//!
|
||||||
|
//! /* 1. Create a new blank Listener */
|
||||||
|
//! let mut listener = Listener::new();
|
||||||
|
//!
|
||||||
|
//! /* 2. Set a trigger condition function for listener */
|
||||||
|
//!
|
||||||
|
//! listener.set_trigger_cond_fn(
|
||||||
|
//! |_:Arc<Bot>,message:ServerMessage|
|
||||||
|
//! if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
//! // dbg!(msg.clone());
|
||||||
|
//! for badge in msg.badges {
|
||||||
|
//! if matches!(badge, x if x.name == "moderator") {
|
||||||
|
//! // dbg!("moderator found");
|
||||||
|
//! return true;
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! false
|
||||||
|
//! } else { false }
|
||||||
|
//! );
|
||||||
|
//!
|
||||||
|
//! /* 3. Define an async fn callback execution */
|
||||||
|
//! async fn execbody(bot:Arc<Bot>,message:ServerMessage) -> Result<String,String> {
|
||||||
|
//! if let ServerMessage::Privmsg(msg) = message {
|
||||||
|
//! let _ = bot.client.say_in_reply_to(&msg, "pepeKneel".to_string()).await ;
|
||||||
|
//! return Result::Ok("Success".to_string()) ;
|
||||||
|
//! }
|
||||||
|
//! Result::Err("Not Valid message type".to_string())
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! /* 4. Set and Store the execution body using `async_box()` */
|
||||||
|
//! listener.set_exec_fn(asyncfn_box(execbody));
|
||||||
|
//!
|
||||||
|
//! /* 5. Load the listener into the bot */
|
||||||
|
//! bot.load_listener(listener);
|
||||||
|
//!
|
||||||
|
//! /* Run the bot */
|
||||||
|
//! bot.run().await;
|
||||||
|
//!
|
||||||
|
//! }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub mod botcore;
|
||||||
|
pub use crate::botcore::bot::Bot;
|
||||||
|
pub use crate::botcore::bot_objects::asyncfn_box;
|
||||||
|
pub use crate::botcore::bot_objects::listener::Listener;
|
||||||
|
pub use crate::botcore::bot_objects::command::Command;
|
||||||
|
pub use crate::botcore::modules::Module;
|
||||||
|
pub use crate::botcore::bot_objects::Badge;
|
||||||
|
|
49
src/main.rs
49
src/main.rs
|
@ -1,40 +1,25 @@
|
||||||
use dotenv::dotenv;
|
//! Example simple Binary crate that creates & runs bot based on `.env`
|
||||||
use twitch_irc::{login::StaticLoginCredentials, ClientConfig, SecureTCPTransport, TwitchIRCClient};
|
//! Be sure the followig is defined in `.env`
|
||||||
use std::env;
|
//! - login_name
|
||||||
|
//! - access_token
|
||||||
|
//! - bot_channels
|
||||||
|
//! - prefix
|
||||||
|
//! - bot_admins
|
||||||
|
//!
|
||||||
|
//! Bot access tokens be generated here -
|
||||||
|
//! - Get a Bot Chat Token here - <https://twitchtokengenerator.com>
|
||||||
|
//! - More Info - <https://dev.twitch.tv/docs/authentication>
|
||||||
|
|
||||||
|
use forcebot_rs_v2::botcore::bot::Bot;
|
||||||
|
|
||||||
mod botcore;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn main() {
|
pub async fn main() {
|
||||||
|
|
||||||
dotenv().ok();
|
|
||||||
let login_name = env::var("login_name").unwrap().to_owned();
|
|
||||||
let oauth_token = env::var("access_token").unwrap().to_owned();
|
|
||||||
|
|
||||||
let mut botchannels = Vec::new();
|
/* 1. Create the bot using env */
|
||||||
|
let bot = Bot::new();
|
||||||
|
|
||||||
for chnl in env::var("bot_channels").unwrap().split(',') {
|
/* 2. Run the bot */
|
||||||
botchannels.push(chnl.to_owned());
|
bot.run().await;
|
||||||
}
|
|
||||||
|
|
||||||
let config = ClientConfig::new_simple(StaticLoginCredentials::new(
|
|
||||||
login_name.to_owned(),
|
|
||||||
Some(oauth_token.to_owned()),
|
|
||||||
));
|
|
||||||
|
|
||||||
let (mut incoming_messages, client) =
|
|
||||||
TwitchIRCClient::<SecureTCPTransport, StaticLoginCredentials>::new(config);
|
|
||||||
|
|
||||||
for chnl in botchannels {
|
|
||||||
client.join(chnl.to_owned()).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let join_handle = tokio::spawn(async move {
|
|
||||||
while let Some(message) = incoming_messages.recv().await {
|
|
||||||
println!("Received message: {:?}", message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
join_handle.await.unwrap();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue