use std::collections::HashMap; use std::sync::Arc; use chrono::{DateTime, NaiveDateTime, Utc}; use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; use serenity::framework::standard::{CommandError, CommandResult}; use serenity::model::channel::{Message, Reaction}; use serenity::model::id::ChannelId; use serenity::model::user::User; use serenity::prelude::{Context, TypeMap}; use serenity::utils::Colour; use tokio::sync::RwLockReadGuard; use crate::database::models::Event; use crate::discord::{get_db, DraftEvent}; use crate::hypebot_config::HypeBotConfig; use crate::reminder::Reminders; use crate::{discord, INTERESTED_EMOJI, UNINTERESTED_EMOJI}; /// 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(db) = get_db(&ctx.data.read().await).await { let message_id = reaction.message_id.0.to_string(); let event = match db.get_event_by_msg_id(&message_id) { Some(event) => event, None => { return; } }; let event_utc_time = DateTime::::from_utc(event.event_time, 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::strfmt(msg_text, &fmt).unwrap(); 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: &str) { if let Ok(dm_channel) = user.create_dm_channel(ctx).await { dm_channel .send_message(ctx, |m| m.content(message)) .await .ok(); } } pub const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`'); /// Sends the event message to the event channel pub async fn send_event_msg( ctx: &Context, config: &HypeBotConfig, channel_id: u64, event: &Event, react: bool, ) -> serenity::Result { let channel = ctx.http.get_channel(channel_id).await?; let utc_time = DateTime::::from_utc(event.event_time, 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 #[allow(clippy::too_many_arguments)] 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 draft_event = data .get_mut::() .ok_or_else(|| CommandError::from("Unable get draft event!".to_string()))?; let new_draft_event = DraftEvent { event: Event { event_name, event_desc, event_loc: location, organizer, event_time, message_id: "".to_string(), thumbnail_link: thumbnail, }, creator_id, }; *draft_event = Arc::new(new_draft_event); 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 = get_config(&data).await?; let draft_event = discord::get_draft_event(&data).await?; channel .send_message(&ctx, |m| { m.content("Draft message, use the `confirm` command to post it.".to_string()) }) .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: &RwLockReadGuard<'_, TypeMap>, ) -> std::result::Result, CommandError> { let config = data .get::() .ok_or_else(|| CommandError::from("Unable to get config".to_string()))?; Ok(config.clone()) } /// Schedule event reminders pub async fn schedule_event(ctx: &Context, event: &Event) { let config = get_config(&ctx.data.read().await) .await .expect("Unable to get config"); let mut data = ctx.data.write().await; let reminders = data.get_mut::().unwrap(); reminders.add_reminders(event, &*config); } /// Schedule all events pub async fn schedule_all_events(ctx: &Context) { let db = get_db(&ctx.data.read().await) .await .expect("Could not get database"); for event in db.get_all_events() { schedule_event(&ctx, &event).await; } } /// Delete event pub async fn delete_event(ctx: &Context, event: &Event) -> CommandResult { let config = get_config(&ctx.data.read().await) .await .expect("Unable to get config"); let db = get_db(&ctx.data.read().await) .await .expect("Unable to get db"); let message_id = event.message_id.parse::()?; let message = ctx .http .get_message(config.event_channel, message_id) .await?; let cancel_msg = format!("**{}** has been canceled!", event.event_name.clone()); // Only send a cancel message if the even has not already happened if event.event_time > Utc::now().naive_utc() { if let Ok(reaction_users) = message .reaction_users(&ctx.http, INTERESTED_EMOJI, None, None) .await { for user in reaction_users { send_dm_message(ctx, user, &cancel_msg).await; } } } db.remove_event(event)?; message.delete(ctx).await?; if let Some(reminders) = ctx.data.write().await.get_mut::() { reminders.remove_reminders(&event); } Ok(()) } /// Create a countdown link for the event pub fn get_countdown_link(event_name: &str, utc: &DateTime) -> String { let msg = utf8_percent_encode(event_name, FRAGMENT); 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 ) }