use crate::database::models::{Event, NewEvent}; use crate::database::{get_event_by_msg_id, remove_event}; use crate::hypebot_config::HypeBotConfig; use crate::{INTERESTED_EMOJI, UNINTERESTED_EMOJI}; use chrono::{DateTime, NaiveDateTime, Utc}; use serenity::framework::standard::{CommandError, CommandResult}; use serenity::http::Http; use serenity::model::prelude::{ChannelId, Message, Reaction, User}; use serenity::prelude::{TypeMapKey, TypeMap}; use serenity::prelude::{Context, RwLock}; use serenity::utils::Colour; use serenity::Result; use serenity::framework::standard::macros::hook; use std::collections::HashMap; use std::sync::Arc; use strfmt::strfmt; use diesel::result::Error; pub mod events; /// Struct for storing drafted events #[derive(Clone)] pub struct DraftEvent { pub event: NewEvent, pub creator_id: u64, } impl TypeMapKey for DraftEvent { type Value = DraftEvent; } /// Send a message to a reaction user pub async fn send_message_to_reaction_users(ctx: &Context, reaction: &Reaction, msg_text: &str) { if let Ok(config) = get_config(&ctx.data).await { let db_link = config.db_url.clone(); let message_id = reaction.message_id.0.to_string(); let event = match get_event_by_msg_id(db_link, message_id) { Ok(event) => event, Err(e) => { if !matches!(e, diesel::result::Error::NotFound) { error!("Error getting event from reaction {}", e); } return; } }; let event_utc_time = DateTime::::from_utc(event.event_time.clone(), Utc); let current_utc_time = chrono::offset::Utc::now(); let msg; if event_utc_time > current_utc_time { // Format message let mut fmt = HashMap::new(); fmt.insert("event".to_string(), event.event_name); msg = strfmt(msg_text, &fmt).unwrap(); } else { msg = format!("**{}** has already started!", &event.event_name) } if let Ok(user) = reaction.user(&ctx).await { send_dm_message(&ctx, user, &msg).await; } } } /// Send a DM message to a user pub async fn send_dm_message(ctx: &Context, user: User, message: &String) { if let Ok(dm_channel) = user.create_dm_channel(ctx).await { dm_channel.send_message(ctx, |m| m.content(message)).await.ok(); } } /// Create a countdown link for the event pub fn get_countdown_link(event_name: &String, utc: &DateTime) -> String { let msg = event_name.replace(" ", "+"); let time = utc.format("%G%m%dT%H%M").to_string(); format!( "https://www.timeanddate.com/countdown/generic?iso={}&p0=&msg={}&font=sanserif&csz=1", time, msg ) } /// Sends the event message to the event channel pub async fn send_event_msg( ctx: &Context, config: &HypeBotConfig, channel_id: u64, event: &NewEvent, react: bool, ) -> Result { let channel = ctx.http.get_channel(channel_id).await?; let utc_time = DateTime::::from_utc(event.event_time.clone(), Utc); let native_time = utc_time.with_timezone(&config.event_timezone); let ping_roles = config .ping_roles .clone() .into_iter() .map(|role| format!("<@&{}>", role)) .collect::>() .join(" "); // Send message let msg = channel.id().send_message(&ctx, |m| { m.embed(|e| { e.title(event.event_name.clone()) .color(Colour::PURPLE) .description(format!( "**{}**\n{}\n\n[Click Here For A Countdown]({})\n\nReact with {} below to receive event reminders!", native_time.format("%A, %B %d @ %I:%M %P %t %Z"), event.event_desc, get_countdown_link(&event.event_name, &utc_time), INTERESTED_EMOJI )) .thumbnail(event.thumbnail_link.clone()) .footer(|f| f.text("Local Event Time")) .timestamp(utc_time.to_rfc3339()) .field("Location", &event.event_loc, true) .field("Organizer", &event.organizer, true) }).content(ping_roles) }).await?; if react { // Add reacts msg.react(ctx, INTERESTED_EMOJI).await?; msg.react(ctx, UNINTERESTED_EMOJI).await?; } Ok(msg) } /// Updates the draft event stored in the context data pub async fn update_draft_event( ctx: &Context, event_name: String, event_desc: String, organizer: String, location: String, thumbnail: String, event_time: NaiveDateTime, creator_id: u64, ) -> CommandResult { let mut data = ctx.data.write().await; let mut draft_event = data .get_mut::() .ok_or(CommandError::from("Unable get draft event!".to_string()))?; draft_event.event.event_name = event_name; draft_event.event.event_desc = event_desc; draft_event.event.event_loc = location; draft_event.event.organizer = organizer; draft_event.event.thumbnail_link = thumbnail; draft_event.event.message_id = String::new(); draft_event.event.event_time = event_time; draft_event.creator_id = creator_id; Ok(()) } /// Sends the draft event stored in the context data pub async fn send_draft_event(ctx: &Context, channel: ChannelId) -> CommandResult { let data = ctx.data.read().await; let config = data .get::() .ok_or(CommandError::from("Config not found!".to_string()))?; let draft_event = data .get::() .ok_or(CommandError::from("Draft event not found!".to_string()))?; channel.send_message(&ctx, |m| { m.content(format!( "Draft message, use the `confirm` command to post it." )) }).await?; send_event_msg(ctx, config, channel.0, &draft_event.event, false).await?; Ok(()) } /// Gets the config from context data pub async fn get_config( data: &Arc>, ) -> std::result::Result { let data_read = data.read().await; let config = data_read .get::() .ok_or(CommandError::from("Unable to get config".to_string()))?; Ok(config.clone()) } /// Gets the draft event from context data pub async fn get_draft_event( data: &Arc>, ) -> std::result::Result { let data_read = data.read().await; let draft_event = data_read .get::() .ok_or(CommandError::from("Unable to queued event".to_string()))?; Ok(draft_event.clone()) } /// Logs command errors to the logger #[hook] pub async fn log_error( _ctx: &Context, _msg: &Message, command_name: &str, result: std::result::Result<(), CommandError>, ) { match result { Ok(()) => (), Err(why) => error!("Command '{}' returned error {:?}", command_name, why), }; } /// Checks if the user has permission to use this bot #[hook] pub async fn permission_check(ctx: &Context, msg: &Message, _command_name: &str) -> bool { if let Some(guild_id) = msg.guild_id { if let Ok(config) = get_config(&ctx.data).await { if let Ok(roles) = ctx.http.get_guild_roles(guild_id.0).await { for role in roles { if config.event_roles.contains(&role.id.0) { return match msg.author.has_role(&ctx, guild_id, role).await { Ok(has_role) => has_role, Err(_) => false, }; } } } } } false } /// Schedule event reminders pub async fn schedule_event(ctx: &Context, event: &Event) { let config = get_config(&ctx.data).await.expect("Unable to get config"); if let Some(reminders) = config.reminders { let event_time: DateTime = DateTime::::from_utc(event.event_time.clone(), Utc); for reminder in reminders { let reminder_time = event_time - chrono::Duration::minutes(reminder.reminder_time as i64); if reminder_time > chrono::offset::Utc::now() { let ctx = ctx.clone(); let reminder_msg = reminder.msg.clone(); let event = event.clone(); tokio::task::spawn(async move { send_reminders_task(&ctx, reminder_time, &event, &reminder_msg).await; } ); } } } } /// Send reminders pub async fn send_reminders_task( ctx: &Context, datetime: DateTime, event: &Event, reminder_msg: &String, ) { let duration = datetime - chrono::offset::Utc::now(); tokio::time::sleep(tokio::time::Duration::from_millis(duration.num_milliseconds() as u64)).await; let config = get_config(&ctx.data).await.expect("Unable to get config"); let event_channel_id = config.event_channel; if let Ok(message_id) = event.message_id.parse::() { // Get message id if let Ok(message) = ctx.http.get_message(event_channel_id, message_id).await { let reaction_users = message .reaction_users(ctx, INTERESTED_EMOJI, None, None) .await .unwrap_or(Vec::::new()); // Build reminder message let msg: String = reminder_msg.replace("{EVENT_NAME}", event.event_name.as_str()); // Send reminder to each reacted user for user in reaction_users { send_dm_message(ctx, user, &msg).await; } } } } /// Delete event pub async fn delete_event(http: &Arc, data: &Arc>, event: &Event) { let config = get_config(&data).await.expect("Unable to get config"); remove_event(config.db_url.clone(), event.id).ok(); if let Ok(message_id) = event.message_id.parse::() { http.delete_message(config.event_channel, message_id).await.ok(); } }