Initial commit

This commit is contained in:
mzntori 2024-05-15 16:50:23 +02:00
commit fd6ff84d18
8 changed files with 1547 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
/.idea
*.env

1339
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "deepdip_bot"
version = "0.1.0"
edition = "2021"
[dependencies]
dotenv = "0.15.0"
reqwest = { version = "0.12.4", features = ["json"] }
serde = { version = "1.0.201", features = ["derive"]}
serde_json = "1.0.117"
thiserror = "1.0.60"
tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"] }
twitch-irc = "5.0.1"

129
src/bot/mod.rs Normal file
View file

@ -0,0 +1,129 @@
use std::sync::Arc;
use dotenv;
use tokio::sync::{mpsc::UnboundedReceiver, Mutex};
use twitch_irc::{{ClientConfig, SecureTCPTransport, TwitchIRCClient},
login::StaticLoginCredentials,
message::ServerMessage};
use twitch_irc::message::PrivmsgMessage;
use crate::deepdip_api::get_today_run;
use crate::deepdip_model::Checkpoint;
use crate::error::BotError;
type IncomingMessages = UnboundedReceiver<ServerMessage>;
type Client = TwitchIRCClient<SecureTCPTransport, StaticLoginCredentials>;
#[derive(Debug)]
pub struct Payload {
last_checkpoint: Checkpoint,
check_floor: i32,
above_on_last_check: bool
}
impl Payload {
pub fn new() -> Payload {
Payload {
last_checkpoint: Checkpoint::default(),
check_floor: 0,
above_on_last_check: false,
}
}
}
fn irc_setup() -> (IncomingMessages, Client) {
let config = ClientConfig::new_simple(
StaticLoginCredentials::new(
dotenv::var("BOT_USERNAME").unwrap(),
dotenv::var("BOT_OAUTH").ok(),
)
);
TwitchIRCClient::<SecureTCPTransport, StaticLoginCredentials>::new(config)
}
pub async fn run_bot() -> Result<(), BotError> {
let (mut incoming_msg, client) = irc_setup();
client.join("gaygebot".into())?;
let client_ar: Arc<Mutex<Client>> = Arc::new(Mutex::new(client));
let payload_ar: Arc<Mutex<Payload>> = Arc::new(Mutex::new(Payload::new()));
let join_handle = tokio::spawn(async move {
while let Some(message) = incoming_msg.recv().await {
match message {
ServerMessage::Pong(_) => {
pong_handler(Arc::clone(&client_ar), Arc::clone(&payload_ar)).await;
}
ServerMessage::Privmsg(priv_msg) => {
privmsg_handler(priv_msg, Arc::clone(&client_ar), Arc::clone(&payload_ar)).await;
}
_ => {}
}
}
});
join_handle.await?;
Ok(())
}
async fn privmsg_handler(message: PrivmsgMessage, client_ar: Arc<Mutex<Client>>, payload_ar: Arc<Mutex<Payload>>) {
let message_parts: Vec<&str> = message.message_text.split(" ").collect();
if message.message_text == "!ping" {
let client = client_ar.lock().await;
client.say("gaygebot".to_string(), "Pong!".to_string()).await.unwrap_or_default();
return;
}
if message.message_text == "!cp" {
let client = client_ar.lock().await;
let payload = payload_ar.lock().await;
client.say(
"gaygebot".to_string(),
format!("Height: {} Floor: {}", payload.last_checkpoint.height, payload.last_checkpoint.floor)
).await.unwrap_or_default();
}
if message_parts.get(0).unwrap_or(&"") == &"!floor" {
if let Ok(floor) = message_parts
.get(1)
.unwrap_or(&"")
.parse::<i32>() {
let mut payload = payload_ar.lock().await;
payload.check_floor = floor;
drop(payload);
let client = client_ar.lock().await;
client.say("gaygebot".to_string(), format!("Set floor to check to {}!", floor)).await.unwrap_or_default();
}
}
}
async fn pong_handler(client_ar: Arc<Mutex<Client>>, payload_ar: Arc<Mutex<Payload>>) {
let run_result = get_today_run().await;
if run_result.is_err() {
return;
}
let mut run = run_result.unwrap();
if let Some(cp) = run.checkpoints.pop() {
let mut payload = payload_ar.lock().await;
payload.last_checkpoint = cp;
if !payload.above_on_last_check && cp.floor >= payload.check_floor {
let client = client_ar.lock().await;
client.say(
"gaygebot".to_string(),
format!("@mzntori Wirtual just reached floor {}", cp.floor)
).await.unwrap();
payload.above_on_last_check = true;
} else if payload.above_on_last_check && cp.floor < payload.check_floor {
payload.above_on_last_check = false;
}
}
}

21
src/deepdip_api.rs Normal file
View file

@ -0,0 +1,21 @@
use crate::{
deepdip_model::{Run, Checkpoint},
error::BotError,
};
/// Fetches newest run.
pub async fn get_today_run() -> Result<Run, BotError> {
let checkpoints: Vec<Checkpoint> = reqwest::get("https://files.deepdip2.com/todayRun.json").await?.json().await?;
Ok(Run::from(checkpoints))
}
#[cfg(test)]
pub mod tests {
use super::*;
#[tokio::test]
async fn test() {
dbg!(get_today_run().await.unwrap());
}
}

22
src/deepdip_model.rs Normal file
View file

@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
pub struct Checkpoint {
pub height: i32,
pub timestamp: i32,
pub floor: i32
}
/// Better representation of a run instead of using `Vec<Checkpoint>`
#[derive(Debug, Clone, PartialEq)]
pub struct Run {
pub checkpoints: Vec<Checkpoint>
}
impl From<Vec<Checkpoint>> for Run {
fn from(value: Vec<Checkpoint>) -> Self {
Run {
checkpoints: value
}
}
}

11
src/error.rs Normal file
View file

@ -0,0 +1,11 @@
use thiserror::Error;
#[derive(Debug, Error)]
pub enum BotError {
#[error("error with requesting data")]
RequestError(#[from] reqwest::Error),
#[error("error with twitch")]
TwitchValidateError(#[from] twitch_irc::validate::Error),
#[error("failed to run join handle")]
JoinError(#[from] tokio::task::JoinError)
}

9
src/main.rs Normal file
View file

@ -0,0 +1,9 @@
pub mod bot;
pub mod deepdip_model;
pub mod deepdip_api;
pub mod error;
#[tokio::main]
async fn main() {
bot::run_bot().await.unwrap();
}