Add transit betting
This commit is contained in:
9 files changed
+445
-55
No files matched your search
@@ -2,6 +2,7 @@ use crate::album_manager::AlbumManager;
|
||||
use crate::error::Error;
|
||||
use crate::migrations::{CURRENT_DB_VERSION, do_migration};
|
||||
use config::{Config, File};
|
||||
use cta_api::CTAClient;
|
||||
use j_db::database::Database;
|
||||
use j_db::metadata::DBMetadata;
|
||||
use poise::serenity_prelude::model::id::ChannelId;
|
||||
@@ -42,6 +43,7 @@ pub struct BotConfig {
|
||||
pub toys: Vec<String>,
|
||||
pub effect_role_duration: i64,
|
||||
pub raas_server: String,
|
||||
pub cta_key: String,
|
||||
pub picox: PicOxConfig,
|
||||
}
|
||||
|
||||
@@ -75,6 +77,7 @@ pub struct GlobalData {
|
||||
pub bot_state: Mutex<BotState>,
|
||||
pub db: Database,
|
||||
pub picox: AlbumManager,
|
||||
pub cta: Mutex<CTAClient>,
|
||||
pub speak_lock: Mutex<()>,
|
||||
}
|
||||
|
||||
@@ -100,6 +103,7 @@ impl GlobalData {
|
||||
db,
|
||||
cfg: cfg.clone(),
|
||||
picox: AlbumManager::new(cfg.picox.api_base_url, &cfg.picox.token),
|
||||
cta: Mutex::new(CTAClient::new(cfg.cta_key)),
|
||||
speak_lock: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
+4
-1
@@ -10,6 +10,7 @@ mod little_fren;
|
||||
mod motivate;
|
||||
mod role;
|
||||
pub(crate) mod shop;
|
||||
mod transit;
|
||||
pub(crate) mod voices;
|
||||
|
||||
use crate::config::GlobalData;
|
||||
@@ -314,6 +315,7 @@ pub async fn run_bot(global_data: GlobalData) {
|
||||
shop::sell_item(),
|
||||
shop::shop(),
|
||||
shop::use_item(),
|
||||
transit::cta_bets(),
|
||||
voices::list_voices(),
|
||||
voices::list_words(),
|
||||
voices::say(),
|
||||
@@ -347,7 +349,8 @@ pub async fn run_bot(global_data: GlobalData) {
|
||||
let intents = serenity::GatewayIntents::non_privileged()
|
||||
| serenity::GatewayIntents::DIRECT_MESSAGES
|
||||
| serenity::GatewayIntents::GUILDS
|
||||
| serenity::GatewayIntents::MESSAGE_CONTENT;
|
||||
| serenity::GatewayIntents::MESSAGE_CONTENT
|
||||
| serenity::GatewayIntents::GUILD_MESSAGE_REACTIONS;
|
||||
|
||||
let mut client = serenity::ClientBuilder::new(token, intents)
|
||||
.framework(framework)
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
use crate::discord::Context;
|
||||
use crate::error::Error;
|
||||
use crate::user::User;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use cta_api::models::eta::EtaRequest;
|
||||
use log::debug;
|
||||
use poise::serenity_prelude::{
|
||||
FormattedTimestampStyle, Mentionable, MessageBuilder, ReactionType, Timestamp, UserId,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::Duration;
|
||||
|
||||
const CALI_STATION_ID: u32 = 30111;
|
||||
|
||||
#[derive(Debug, PartialOrd, PartialEq, Copy, Clone)]
|
||||
pub enum CtaBetType {
|
||||
OnTime,
|
||||
Early,
|
||||
Late,
|
||||
}
|
||||
|
||||
impl CtaBetType {
|
||||
pub fn emoji(&self) -> String {
|
||||
match self {
|
||||
CtaBetType::OnTime => "🟢".to_string(),
|
||||
CtaBetType::Early => "🟡".to_string(),
|
||||
CtaBetType::Late => "🔴".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn outcome(est_time: DateTime<Utc>, actual_time: DateTime<Utc>) -> Self {
|
||||
let diff = est_time - actual_time;
|
||||
if diff.num_seconds().abs() <= 1 {
|
||||
CtaBetType::OnTime
|
||||
} else if diff.num_seconds().is_positive() {
|
||||
CtaBetType::Early
|
||||
} else {
|
||||
CtaBetType::Late
|
||||
}
|
||||
}
|
||||
|
||||
pub fn payout_factor(&self) -> f32 {
|
||||
match self {
|
||||
CtaBetType::OnTime => 10.0,
|
||||
CtaBetType::Early => 2.0,
|
||||
CtaBetType::Late => 1.5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bet on the CTA
|
||||
#[poise::command(prefix_command, category = "Transit", guild_only)]
|
||||
pub async fn cta_bets(ctx: Context<'_>) -> Result<(), Error> {
|
||||
let mut cta_api = ctx.data().cta.lock().await;
|
||||
|
||||
let eta = cta_api
|
||||
.fetch_eta(EtaRequest {
|
||||
mapid: None,
|
||||
stpid: Some(CALI_STATION_ID),
|
||||
max: None,
|
||||
rt: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
if eta.eta.is_empty() {
|
||||
ctx.reply("No trains found, either the CTA is closed or borked. 50/50 shot really")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let next_train_eta = eta.eta.iter().find(|eta| eta.is_app == "0").unwrap();
|
||||
|
||||
let arrival_time =
|
||||
chrono::NaiveDateTime::parse_from_str(&next_train_eta.arr_t, "%Y-%m-%dT%H:%M:%S").unwrap();
|
||||
let arrival_time = chrono_tz::America::Chicago
|
||||
.from_local_datetime(&arrival_time)
|
||||
.unwrap();
|
||||
let arrival_time = poise::serenity_prelude::model::Timestamp::from(arrival_time.to_utc());
|
||||
|
||||
let mut msg = MessageBuilder::new();
|
||||
|
||||
msg.push_line("# It's time to bet on public transit!");
|
||||
msg.push_line("The next train to arrive to the California station on the Blue Line is:");
|
||||
msg.push_line(format!("* **Train Run Number**: {}", next_train_eta.rn));
|
||||
msg.push_line(format!(
|
||||
"* **Is Train Delayed?**: {}",
|
||||
next_train_eta.is_dly == "1"
|
||||
));
|
||||
msg.push_line(format!(
|
||||
"* **Expected Arrival Time**: {}",
|
||||
poise::serenity_prelude::utils::FormattedTimestamp::new(
|
||||
arrival_time,
|
||||
Some(FormattedTimestampStyle::LongTime)
|
||||
)
|
||||
));
|
||||
msg.push_line("");
|
||||
msg.push_line("Bets are 100 Fren Coins");
|
||||
msg.push_bold_line("Only bet once, I'll know...");
|
||||
msg.push_line("* React 🟢 if the train will be exactly on time (10x bet)");
|
||||
msg.push_line("* React 🟡 if the train will be early (2x bet)");
|
||||
msg.push_line("* React 🔴 if the train will be late (1.5x bet)");
|
||||
|
||||
let bet_msg = ctx.reply(msg.build()).await?;
|
||||
let msg = bet_msg.message().await?;
|
||||
|
||||
msg.react(ctx, ReactionType::Unicode(CtaBetType::OnTime.emoji()))
|
||||
.await?;
|
||||
msg.react(ctx, ReactionType::Unicode(CtaBetType::Early.emoji()))
|
||||
.await?;
|
||||
msg.react(ctx, ReactionType::Unicode(CtaBetType::Late.emoji()))
|
||||
.await?;
|
||||
|
||||
let mut arr_time = None;
|
||||
|
||||
// Try for 30 minutes
|
||||
for _ in 0..(60 * 30) {
|
||||
let train_status = cta_api
|
||||
.fetch_train_schedule(next_train_eta.rn.parse().unwrap())
|
||||
.await?;
|
||||
let cali_eta = train_status
|
||||
.eta
|
||||
.iter()
|
||||
.find(|schedule| {
|
||||
let sta_id: u32 = schedule.stp_id.parse().unwrap();
|
||||
sta_id == CALI_STATION_ID
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
if cali_eta.is_app == "1" {
|
||||
debug!("Train has arrived!");
|
||||
arr_time = Some(Utc::now());
|
||||
break;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
|
||||
if arr_time.is_none() {
|
||||
ctx.reply("Yeah I don't think this train is coming, oh well RIP Chicago")
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let outcome = CtaBetType::outcome(*arrival_time, arr_time.unwrap());
|
||||
|
||||
let user_id = ctx.cache().current_user().id;
|
||||
|
||||
let msg = bet_msg.message().await?;
|
||||
let reactions_on_time = msg
|
||||
.reaction_users(
|
||||
ctx,
|
||||
ReactionType::Unicode(CtaBetType::OnTime.emoji()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let reactions_early = msg
|
||||
.reaction_users(
|
||||
ctx,
|
||||
ReactionType::Unicode(CtaBetType::Early.emoji()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let reactions_late = msg
|
||||
.reaction_users(
|
||||
ctx,
|
||||
ReactionType::Unicode(CtaBetType::Late.emoji()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut bets: HashMap<UserId, CtaBetType> = HashMap::new();
|
||||
let mut punish_users: HashSet<UserId> = HashSet::new();
|
||||
|
||||
add_bets(
|
||||
&user_id,
|
||||
&CtaBetType::OnTime,
|
||||
&reactions_on_time,
|
||||
&mut bets,
|
||||
&mut punish_users,
|
||||
);
|
||||
add_bets(
|
||||
&user_id,
|
||||
&CtaBetType::Late,
|
||||
&reactions_late,
|
||||
&mut bets,
|
||||
&mut punish_users,
|
||||
);
|
||||
add_bets(
|
||||
&user_id,
|
||||
&CtaBetType::Early,
|
||||
&reactions_early,
|
||||
&mut bets,
|
||||
&mut punish_users,
|
||||
);
|
||||
|
||||
for user in bets.keys() {
|
||||
User::take_funds(&ctx.data().db, *user, 100)?;
|
||||
}
|
||||
|
||||
let winners: Vec<UserId> = bets
|
||||
.iter()
|
||||
.filter(|(_, bet)| **bet == outcome)
|
||||
.filter(|(user, _)| !punish_users.contains(user))
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
|
||||
let actual_timestamp = Timestamp::from(arr_time.unwrap());
|
||||
ctx.reply(format!(
|
||||
"# {} Train has arrived @ {}",
|
||||
outcome.emoji(),
|
||||
poise::serenity_prelude::utils::FormattedTimestamp::new(
|
||||
actual_timestamp,
|
||||
Some(FormattedTimestampStyle::LongTime)
|
||||
)
|
||||
))
|
||||
.await?;
|
||||
|
||||
if winners.is_empty() {
|
||||
ctx.reply("Looks like no one won, too bad...").await?;
|
||||
} else {
|
||||
let payout = (100.0 * outcome.payout_factor()) as u32;
|
||||
let mut winner_msg = MessageBuilder::new();
|
||||
winner_msg.push_line("## Congrats to our winners:");
|
||||
winner_msg.push_line(format!("They each get {} Fren Coins!", payout));
|
||||
for winner in &winners {
|
||||
User::give_funds(&ctx.data().db, *winner, payout)?;
|
||||
winner_msg.push_line(format!("* {}", winner.mention()));
|
||||
}
|
||||
|
||||
ctx.reply(winner_msg.build()).await?;
|
||||
}
|
||||
|
||||
if !punish_users.is_empty() {
|
||||
let mut cheater_msg = MessageBuilder::new();
|
||||
|
||||
cheater_msg
|
||||
.push_line("## NOW FOR YOU CHEATING FUCKS, HOPE YOU ENJOY NOT WINNING ANYTHING:");
|
||||
|
||||
for cheater in &punish_users {
|
||||
cheater_msg.push_line(format!("* {}", cheater.mention()));
|
||||
}
|
||||
|
||||
ctx.reply(cheater_msg.build()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_bets(
|
||||
bot_user: &UserId,
|
||||
bet_outcome: &CtaBetType,
|
||||
reactions: &Vec<poise::serenity_prelude::User>,
|
||||
bets: &mut HashMap<UserId, CtaBetType>,
|
||||
punish_users: &mut HashSet<UserId>,
|
||||
) {
|
||||
for user in reactions {
|
||||
if &user.id == bot_user {
|
||||
continue;
|
||||
}
|
||||
|
||||
if bets.contains_key(&user.id) {
|
||||
debug!("Punishing user id {}", user.id);
|
||||
punish_users.insert(user.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug!("Adding User id {} to {}", user.id, bet_outcome.emoji());
|
||||
bets.insert(user.id, *bet_outcome);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ pub enum Error {
|
||||
CommandError(String),
|
||||
IoError(std::io::Error),
|
||||
VoiceError(VoiceError),
|
||||
CTAError(cta_api::Error),
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
@@ -79,6 +80,12 @@ impl From<VoiceError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<cta_api::Error> for Error {
|
||||
fn from(value: cta_api::Error) -> Self {
|
||||
Self::CTAError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
@@ -95,6 +102,7 @@ impl Display for Error {
|
||||
Error::CommandError(err_msg) => write!(f, "{}", err_msg),
|
||||
Error::IoError(err) => write!(f, "IO Error: {}", err),
|
||||
Error::VoiceError(err) => write!(f, "Voice error: {}", err),
|
||||
Error::CTAError(err) => write!(f, "The CTA had an error, I'm shocked: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -21,6 +21,7 @@ static START: Once = Once::new();
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args: Args = Args::from_args();
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let cfg = match BotConfig::new(&args.cfg_path) {
|
||||
Ok(cfg) => cfg,
|
||||
@@ -30,8 +31,6 @@ async fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
START.call_once(|| {
|
||||
magick_wand_genesis();
|
||||
});
|
||||
|
||||
+5
-1
@@ -111,8 +111,12 @@ impl Task {
|
||||
}
|
||||
TaskType::CheckBirthdays => {
|
||||
info!("Checking Birthdays");
|
||||
|
||||
let time =
|
||||
chrono_tz::America::Chicago.from_utc_datetime(&Utc::now().naive_utc());
|
||||
|
||||
let todays_birthdays =
|
||||
BirthdayEntry::todays_birthdays(&data.db, chrono::Utc::now().date_naive())?;
|
||||
BirthdayEntry::todays_birthdays(&data.db, time.date_naive())?;
|
||||
|
||||
for birth in todays_birthdays {
|
||||
if let Ok(user) =
|
||||
|
||||
@@ -97,6 +97,16 @@ impl User {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn take_funds(db: &Database, discord_id: UserId, amount: u32) -> Result<(), Error> {
|
||||
let mut wallet = Self::get_user(db, discord_id)?;
|
||||
|
||||
wallet.coin_count -= amount as i64;
|
||||
|
||||
db.insert::<User>(wallet)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn give_item(
|
||||
db: &Database,
|
||||
discord_id: UserId,
|
||||
|
||||
Reference in new issue
Block a user