From e38b430578fd3405eafe926e31c73ac46b901bd5 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:00:12 -0700 Subject: [PATCH 01/30] Delete src/commands/cowboard directory --- src/commands/cowboard/cowboard_config.rs | 292 --------------- src/commands/cowboard/cowboard_db.rs | 123 ------ src/commands/cowboard/cowboard_db_models.rs | 31 -- src/commands/cowboard/cowboard_handler.rs | 390 -------------------- src/commands/cowboard/mod.rs | 17 - 5 files changed, 853 deletions(-) delete mode 100644 src/commands/cowboard/cowboard_config.rs delete mode 100644 src/commands/cowboard/cowboard_db.rs delete mode 100644 src/commands/cowboard/cowboard_db_models.rs delete mode 100644 src/commands/cowboard/cowboard_handler.rs delete mode 100644 src/commands/cowboard/mod.rs diff --git a/src/commands/cowboard/cowboard_config.rs b/src/commands/cowboard/cowboard_config.rs deleted file mode 100644 index 1512d0a..0000000 --- a/src/commands/cowboard/cowboard_config.rs +++ /dev/null @@ -1,292 +0,0 @@ -use log::error; -use serenity::{ - framework::standard::{ - macros::command, Args, CommandResult, - }, - model::channel::Message, client::Context -}; -use serenity::model::channel::ReactionType; -use serenity::model::id::ChannelId; -use serenity::utils::MessageBuilder; -use crate::{Database, db}; - -#[command] -#[description = "Get the current settings for the cowboard."] -#[only_in(guilds)] -pub async fn info(ctx: &Context, msg: &Message) -> CommandResult { - let db = db!(ctx); - - if let Some(guild_id) = msg.guild_id { - if let Ok(config) = db.get_cowboard_config(guild_id).await { - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| - e - .title("Cowboard Settings") - .description("If the emote doesn't display properly below, you probably want to use a different one!") - .field("Emote", &config.emote, true) - .field("Raw Emote", MessageBuilder::new().push_mono(&config.emote).build(), true) - .field("Channel", config.channel.map(|o| format!("<#{}>", o)).unwrap_or_else(|| "No Cowboard Channel".to_string()), true) - .field("Add Threshold", MessageBuilder::new().push_mono(config.add_threshold).build(), true) - .field("Remove Threshold", MessageBuilder::new().push_mono(config.remove_threshold).build(), true) - .field("Webhook", if config.webhook_id.is_some() && config.webhook_token.is_some() { "Enabled" } else { "Disabled" }, true) - )).await?; - } else { - msg.channel_id.say(&ctx.http, "Failed to fetch Cowboard settings for this server...").await?; - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "Set the emote reaction to trigger a cowboard message."] -#[usage = "An emote, preferably one on the server or a default Discord emoji."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn emote(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - - if args.is_empty() { - msg.channel_id.say(&ctx.http, "You need to pass an emote to this command, like :cow:.").await?; - return Ok(()); - } - - if let Ok(emoji) = args.single::() { - if let Some(guild_id) = msg.guild_id { - match db.get_cowboard_config(guild_id).await { - Ok(mut config) => { - config.emote = emoji.to_string(); - if let Err(ex) = db.update_cowboard(&config).await { - msg.channel_id.say(&ctx.http, "We couldn't update the cowboard, sorry... Try again later?").await?; - error!("Failed to update emote for cowboard: {}", ex); - } else { - msg.channel_id.say(&ctx.http, "Successfully updated emote!").await?; - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "We couldn't get the cowboard settings... try again later?").await?; - error!("Failed to get cowboard: {}", ex); - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - } else { - msg.channel_id.say(&ctx.http, "Failed to process an emote from the given message...").await?; - return Ok(()); - } - - Ok(()) -} - -#[command] -#[description = "Set the minimum amount of reactions to post a message to the cowboard."] -#[usage = "A positive number, greater than the removal bound."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn addthreshold(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - - if args.is_empty() { - msg.channel_id.say(&ctx.http, "You need to pass in a positive number for the minimum amount of reactions.").await?; - return Ok(()); - } - - if let Ok(add_threshold) = args.single::() { - if add_threshold <= 0 { - msg.channel_id.say(&ctx.http, "The given number must be positive.").await?; - return Ok(()) - } - - if let Some(guild_id) = msg.guild_id { - match db.get_cowboard_config(guild_id).await { - Ok(mut config) => { - if add_threshold < config.remove_threshold { - msg.channel_id.say(&ctx.http, format!("The minimum number of reactions required to add must be greater than or equal to the removal limit (currently set to {}).", config.remove_threshold)).await?; - return Ok(()) - } - - config.add_threshold = add_threshold; - - if let Err(ex) = db.update_cowboard(&config).await { - msg.channel_id.say(&ctx.http, "We couldn't update the cowboard, sorry... Try again later?").await?; - error!("Failed to update cowboard: {}", ex); - } else { - msg.channel_id.say(&ctx.http, "Successfully updated minimum add threshold!").await?; - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "We couldn't get the cowboard settings... try again later?").await?; - error!("Failed to get cowboard: {}", ex); - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - } else { - msg.channel_id.say(&ctx.http, "The given value is not a valid number.").await?; - return Ok(()); - } - - Ok(()) -} - -#[command] -#[description = "Set the maximum amount of reactions before removing a message from the cowboard."] -#[usage = "A positive number, less than the addition bound."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn removethreshold(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - - if args.is_empty() { - msg.channel_id.say(&ctx.http, "You need to pass in a positive number (or zero) for the removal reaction count.").await?; - return Ok(()); - } - - if let Ok(remove_threshold) = args.single::() { - if remove_threshold < 0 { - msg.channel_id.say(&ctx.http, "The given number must be positive or zero.").await?; - return Ok(()) - } - - if let Some(guild_id) = msg.guild_id { - match db.get_cowboard_config(guild_id).await { - Ok(mut config) => { - if remove_threshold > config.add_threshold { - msg.channel_id.say(&ctx.http, format!("The maximum number of reactions required to remove must be less than or equal to the add limit (currently set to {}).", config.add_threshold)).await?; - return Ok(()) - } - - config.remove_threshold = remove_threshold; - - if let Err(ex) = db.update_cowboard(&config).await { - msg.channel_id.say(&ctx.http, "We couldn't update the cowboard, sorry... Try again later?").await?; - error!("Failed to update cowboard: {}", ex); - } else { - msg.channel_id.say(&ctx.http, "Successfully updated maximum removal threshold!").await?; - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "We couldn't get the cowboard settings... try again later?").await?; - error!("Failed to get cowboard: {}", ex); - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - } else { - msg.channel_id.say(&ctx.http, "The given value is not a valid number.").await?; - return Ok(()); - } - - Ok(()) -} - -#[command] -#[description = "Sets the Cowboard channel to pin messages."] -#[usage = "Either uses the current channel or a provided channel."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn channel(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - - let mut channel = msg.channel_id; - - if !args.is_empty() { - let custom_channel = args.single::(); - if custom_channel.is_err() { - msg.channel_id.say(&ctx.http, "Could not get a channel from your input!").await?; - return Ok(()) - } - channel = custom_channel.unwrap(); - } - - if let Some(guild_id) = msg.guild_id { - if !msg.guild(ctx).await.map(|g| g.channels.contains_key(&channel)).unwrap_or(false) { - msg.channel_id.say(&ctx.http, "Could not find channel in this server!").await?; - return Ok(()) - } - - match db.get_cowboard_config(guild_id).await { - Ok(mut config) => { - config.channel = Some(channel.0); - config.webhook_id = None; - config.webhook_token = None; - - if let Err(ex) = db.update_cowboard(&config).await { - msg.channel_id.say(&ctx.http, "We couldn't update the cowboard, sorry... Try again later?").await?; - error!("Failed to update cowboard: {}", ex); - } else { - msg.channel_id.say(&ctx.http, "Successfully updated channel! You may want to check webhooks; try using `.cowboard webhook` to enable it.").await?; - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "We couldn't get the cowboard settings... try again later?").await?; - error!("Failed to get cowboard: {}", ex); - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "Toggle webhook usage for the cowboard, versus the bot sending the messages."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn webhook(ctx: &Context, msg: &Message) -> CommandResult { - let db = db!(ctx); - - if let Some(guild) = msg.guild(ctx).await { - match db.get_cowboard_config(guild.id).await { - Ok(mut config) => { - if config.channel == None { - msg.channel_id.say(&ctx.http, "Cowboard channel is not set up!").await?; - return Ok(()); - } - let channel = ChannelId::from(config.channel.unwrap()); - if let Some(guild_channel) = guild.channels.get(&channel) { - if config.webhook_id == None { - match guild_channel.create_webhook(&ctx.http, "MooganCowboard").await { - Ok(webhook) => { - config.webhook_id = Some(webhook.id.0); - config.webhook_token = Some(webhook.token.unwrap()) - } - Err(ex) => { - msg.channel_id.say(&ctx.http, format!("Failed to add webhook; maybe I do not have permissions for the channel <#{}>?", channel)).await?; - error!("Failed to create webhook: {}", ex); - return Ok(()) - } - }; - } else { - config.webhook_id = None; - config.webhook_token = None; - } - - if let Err(ex) = db.update_cowboard(&config).await { - msg.channel_id.say(&ctx.http, "We couldn't update the cowboard, sorry... Try again later?").await?; - error!("Failed to update cowboard: {}", ex); - } else if config.webhook_id == None { - msg.channel_id.say(&ctx.http, format!("Disabled webhooks for <#{}>.", channel)).await?; - } else { - msg.channel_id.say(&ctx.http, format!("Enabled webhooks for <#{}>.", channel)).await?; - } - } else { - msg.channel_id.say(&ctx.http, format!("We don't have access to <#{}>... maybe it's hidden for us?", channel)).await?; - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "We couldn't get the cowboard settings... try again later?").await?; - error!("Failed to get cowboard: {}", ex); - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} \ No newline at end of file diff --git a/src/commands/cowboard/cowboard_db.rs b/src/commands/cowboard/cowboard_db.rs deleted file mode 100644 index 87011f1..0000000 --- a/src/commands/cowboard/cowboard_db.rs +++ /dev/null @@ -1,123 +0,0 @@ -use serenity::{ - model::id::{ - GuildId, - ChannelId - } -}; -use rust_decimal::{ - Decimal, - prelude::FromPrimitive -}; -use rust_decimal::prelude::ToPrimitive; -use serenity::model::id::MessageId; - -use crate::Database; -use crate::commands::cowboard::cowboard_db_models::*; - -// Separating the database into different modules so it doesn't become a 2000 line file. -impl Database { - pub async fn get_cowboard_config(&self, server_id: GuildId) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let res = conn.query( - "SELECT channel, add_threshold, remove_threshold, emote, webhook_id, webhook_token FROM [Cowboard].[Server] WHERE id = @P1", - &[&server]) - .await? - .into_row() - .await?; - - let mut out = Cowboard::new(server_id.0); - - if let Some(item) = res { - let channel_id: Option = item.get(0); - let emote_str: &str = item.get(3).unwrap(); - let webhook_id: Option = item.get(4); - let webhook_token: Option<&str> = item.get(5); - out = Cowboard { - id: server_id.0, - channel: channel_id.and_then(|o| o.to_u64()), - add_threshold: item.get(1).unwrap(), - remove_threshold: item.get(2).unwrap(), - emote: emote_str.to_string(), - webhook_id: webhook_id.and_then(|o| o.to_u64()), - webhook_token: webhook_token.map(|o| o.to_string()) - }; - } - - Ok(out) - } - - pub async fn update_cowboard(&self, config: &Cowboard) -> Result<(), Box> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(config.id).unwrap(); - let channel = config.channel.map(|o| Decimal::from_u64(o).unwrap()); - let webhook_id = config.webhook_id.map(|o| Decimal::from_u64(o).unwrap()); - - conn.query( - "EXEC [Cowboard].[UpdateServer] @id = @P1, @channel = @P2, @add_threshold = @P3, @remove_threshold = @P4, @emote = @P5, @webhook_id = @P6, @webhook_token = @P7", - &[&server, &channel, &config.add_threshold, &config.remove_threshold, &config.emote, &webhook_id, &config.webhook_token]) - .await?; - - Ok(()) - } - - pub async fn get_cowboard_message(&self, message: MessageId, channel: ChannelId, guild: GuildId) -> Result, Box> { - let mut conn = self.pool.get().await?; - let message_decimal = Decimal::from_u64(message.0).unwrap(); - let channel_decimal = Decimal::from_u64(channel.0).unwrap(); - let server_decimal = Decimal::from_u64(guild.0).unwrap(); - let res = conn.query( - "SELECT post_id, post_channel_id FROM [Cowboard].[Message] WHERE message_id = @P1 AND message_channel_id = @P2 AND guild_id = @P3", - &[&message_decimal, &channel_decimal, &server_decimal]) - .await? - .into_row() - .await?; - - let mut out: Option = None; - - if let Some(item) = res { - let post_id = item.get(0).and_then(|u: rust_decimal::Decimal| u.to_u64()).unwrap(); - let post_channel_id = item.get(1).and_then(|u: rust_decimal::Decimal| u.to_u64()).unwrap(); - - out = Some(CowboardMessage { - message_id: message.0, - message_channel_id: channel.0, - post_id, - post_channel_id, - guild_id: guild.0 - }); - } - - Ok(out) - } - - pub async fn moo_message(&self, message: MessageId, channel: ChannelId, post_message: MessageId, post_channel: ChannelId, guild: GuildId) -> Result<(), Box> { - let mut conn = self.pool.get().await?; - let message = Decimal::from_u64(message.0).unwrap(); - let channel = Decimal::from_u64(channel.0).unwrap(); - let post_message = Decimal::from_u64(post_message.0).unwrap(); - let post_channel = Decimal::from_u64(post_channel.0).unwrap(); - let server = Decimal::from_u64(guild.0).unwrap(); - - conn.query( - "INSERT INTO [Cowboard].[Message] (message_id, message_channel_id, post_id, post_channel_id, guild_id) VALUES (@P1, @P2, @P3, @P4, @P5)", - &[&message, &channel, &post_message, &post_channel, &server]) - .await?; - - Ok(()) - } - - pub async fn unmoo_message(&self, message: MessageId, channel: ChannelId, guild: GuildId) -> Result<(), Box> { - let mut conn = self.pool.get().await?; - let message = Decimal::from_u64(message.0).unwrap(); - let channel = Decimal::from_u64(channel.0).unwrap(); - let server = Decimal::from_u64(guild.0).unwrap(); - - conn.query( - "DELETE FROM [Cowboard].[Message] WHERE message_id = @P1 AND message_channel_id = @P2 AND guild_id = @P3", - &[&message, &channel, &server]) - .await?; - - Ok(()) - } -} \ No newline at end of file diff --git a/src/commands/cowboard/cowboard_db_models.rs b/src/commands/cowboard/cowboard_db_models.rs deleted file mode 100644 index 60e627e..0000000 --- a/src/commands/cowboard/cowboard_db_models.rs +++ /dev/null @@ -1,31 +0,0 @@ -pub struct Cowboard { - pub id: u64, - pub channel: Option, - pub add_threshold: i32, - pub remove_threshold: i32, - pub emote: String, - pub webhook_id: Option, - pub webhook_token: Option -} - -impl Cowboard { - pub fn new(id: u64) -> Self { - Cowboard { - id, - channel: None, - add_threshold: 5, - remove_threshold: 4, - emote: "🐮".to_string(), - webhook_id: None, - webhook_token: None - } - } -} - -pub struct CowboardMessage { - pub message_id: u64, - pub message_channel_id: u64, - pub post_id: u64, - pub post_channel_id: u64, - pub guild_id: u64 -} \ No newline at end of file diff --git a/src/commands/cowboard/cowboard_handler.rs b/src/commands/cowboard/cowboard_handler.rs deleted file mode 100644 index 91f4e00..0000000 --- a/src/commands/cowboard/cowboard_handler.rs +++ /dev/null @@ -1,390 +0,0 @@ -use std::path::Path; -use tokio::fs::File; -use log::error; -use serenity::client::Context; -use serenity::http::AttachmentType; -use serenity::model::channel::{Embed, Message, Reaction, ReactionType}; -use serenity::model::id::{ChannelId, GuildId, MessageId, UserId}; -use tokio::io::AsyncWriteExt; -use crate::{Database, db}; -use crate::commands::cowboard::cowboard_db_models::{Cowboard}; - -async fn count_reactions(ctx: &Context, message: &Message, config: &Cowboard) -> Result>{ - let config_emote = ReactionType::try_from(config.emote.as_str())?; - let matched_reaction = message.reactions.iter().find(|o|o.reaction_type.eq(&config_emote)); - if let Some(reaction) = matched_reaction { - let count = reaction.count; - let people = message.reaction_users(&ctx.http, config_emote, None, UserId::from(message.author.id.0 - 2)).await?; - if people.iter().any(|o| o.id == message.author.id) { - return Ok(count - 1); - } - return Ok(count); - } - - Ok(0) -} - -pub async fn add_reaction(ctx: &Context, added_reaction: &Reaction) { - if added_reaction.guild_id.is_none() { - return; - } - let guild_id = added_reaction.guild_id.unwrap(); - let db = db!(ctx); - match db.get_cowboard_config(guild_id).await { - Ok(mut config) => { - if config.channel.is_none() { - // No cowboard, why even check? - return; - } - - match added_reaction.message(&ctx.http).await { - Ok(message) => { - match count_reactions(ctx, &message, &config).await { - Ok(count) => { - // Pray that the database's constraints work. - if count >= config.add_threshold as u64 { - let post_message = db.get_cowboard_message(message.id, message.channel_id, guild_id).await; - if let Ok(Some(post)) = post_message { - match ctx.http.get_message(post.post_channel_id, post.post_id).await { - Ok(mut post) => { - update_moo(ctx, &message, &mut post, &mut config).await; - } - Err(ex) => { - error!("Failed to get old cowboard message: {}", ex); - // Create a new copy - add_moo(ctx, guild_id, added_reaction, &message, &mut config).await; - } - } - } else if let Err(ex) = post_message { - error!("Failed to get message from database: {}", ex); - } else { - // Moo that thing! - add_moo(ctx, guild_id, added_reaction, &message, &mut config).await; - } - } - } - Err(ex) => { - error!("Failed to count reactions: {}", ex); - } - } - } - Err(ex) => { - error!("Failed to get reacted message: {}", ex); - } - } - } - Err(ex) => { - error!("Failed to get cowboard config: {}", ex); - } - } -} - -async fn add_moo(ctx: &Context, guild_id: GuildId, reaction: &Reaction, message: &Message, config: &mut Cowboard) { - let db = db!(ctx); - - let message_result = if config.webhook_id.is_some() && config.webhook_token.is_some() { - send_webhook_message(ctx, message, config).await - } else { - send_bot_message(ctx, message, config).await - }; - - if let Err(ex) = message_result { - error!("Failed to send cowboard message: {}", ex); - return; - } - - let post_message = message_result.unwrap(); - - if let Err(ex) = db.moo_message(message.id, reaction.channel_id, post_message.id, post_message.channel_id, guild_id).await { - error!("Failed to moo a message in the database: {}", ex); - } -} - -async fn update_moo(ctx: &Context, message: &Message, post_message: &mut Message, config: &mut Cowboard) { - if config.webhook_id.is_some() && config.webhook_token.is_some() { - update_webhook_message(ctx, message, post_message, config).await - } else { - update_bot_message(ctx, message, post_message, config).await - }; -} - -async fn send_bot_message(ctx: &Context, message: &Message, config: &Cowboard) -> Result> { - let channel = ChannelId::from(config.channel.unwrap()); - let output_username = format_username(ctx, message).await; - let safe_content = message.content_safe(ctx).await; - - let attachments = download_image_attachments(message).await; - - let reacts = count_reactions(ctx, message, config).await?; - let link = message.link_ensured(&ctx.http).await; - - let message_output = channel.send_message(&ctx.http, |m| - { - let execution = m - .content(format!("{} {} | <#{}>\n{}", reacts, &config.emote, message.channel_id, link)) - .embed(|e| { - let temp = e - .author(|a| - a.name(&output_username).icon_url(message.author.face())) - .description(&safe_content) - .timestamp(&message.timestamp) - .footer(|f| f.text(format!("Message ID: {} / User ID: {}", message.id, message.author.id))); - - if !attachments.is_empty() { - let (name, _) = &attachments[0]; - temp.attachment(name); - } - - temp - }); - - for (_, path) in &attachments { - execution.add_file(AttachmentType::Path(Path::new(path))); - } - - execution - } - ).await; - - delete_image_attachments(message).await; - match message_output { - Ok(message) => { - Ok(message) - } - Err(ex) => { - Err(Box::new(ex)) - } - } -} - -async fn update_bot_message(ctx: &Context, message: &Message, post_message: &mut Message, config: &mut Cowboard) { - match count_reactions(ctx, message, config).await { - Ok(reacts) => { - let link = message.link_ensured(&ctx.http).await; - if let Err(ex) = post_message.edit(&ctx.http, - |m| m.content(format!("{} {} | <#{}>\n{}", reacts, &config.emote, message.channel_id, link))).await { - error!("Failed to edit post message??? {}", ex); - } - } - Err(ex) => { - error!("Failed to count reactions: {}", ex); - } - } -} - -async fn send_webhook_message(ctx: &Context, message: &Message, config: &mut Cowboard) -> Result> { - let token = config.webhook_token.clone().unwrap(); - if let Ok(webhook) = ctx.http.get_webhook_with_token(config.webhook_id.unwrap(), &*token).await { - let output_username = format_username(ctx, message).await; - let safe_content = message.content_safe(ctx).await; - - let attachments = download_image_attachments(message).await; - - let embeds = vec![ - Embed::fake(|e| - { - let temp = e - .author(|a| - a.name(&output_username).icon_url(message.author.face())) - .description(&safe_content) - .timestamp(&message.timestamp) - .footer(|f| f.text(format!("Message ID: {} / User ID: {}", message.id, message.author.id))); - - if !attachments.is_empty() { - let (name, _) = &attachments[0]; - temp.attachment(name); - } - - temp - } - ) - ]; - - let reacts = count_reactions(ctx, message, config).await?; - let link = message.link_ensured(&ctx.http).await; - if let Ok(Some(webhook_message)) = webhook.execute(&ctx.http, true, |m| - { - let execution = m - .content(format!("{} {} | <#{}>\n{}", reacts, &config.emote, message.channel_id, link)) - .embeds(embeds) - .avatar_url(message.author.face()) - .username(output_username); - - for (_, path) in &attachments { - execution.add_file(AttachmentType::Path(Path::new(path))); - } - - execution - } - ).await { - delete_image_attachments(message).await; - return Ok(webhook_message); - } - } - - delete_image_attachments(message).await; - disable_webhook(ctx, config).await; - send_bot_message(ctx, message, config).await -} - -async fn download_image_attachments(message: &Message) -> Vec<(String, String)> { - let mut out: Vec<(String, String)> = Vec::new(); - - let directory = format!("cowboard/{}", message.id); - - if let Err(ex) = tokio::fs::create_dir_all(&directory).await { - error!("Failed to create temporary directory: {}", ex); - return out; - } - - let mut size_limit: u64 = 8 * 1024 * 1024; - - for item in message.attachments.iter() { - if item.dimensions().is_some() && size_limit >= item.size { - // Is an image that we can upload! - let content = match item.download().await { - Ok(content) => content, - Err(ex) => { - error!("Error downloading file: {}", ex); - continue; - } - }; - - let file_path = format!("{}/{}", &directory, &item.filename); - let mut file = match File::create(&file_path).await { - Ok(file) => file, - Err(ex) => { - error!("Error creating file: {}", ex); - continue; - } - }; - - if let Err(ex) = file.write_all(&content).await { - error!("Error saving image: {}", ex); - continue; - } - - size_limit -= item.size; - out.push((item.filename.clone(), file_path)); - } - } - - out -} - -async fn delete_image_attachments(message: &Message) { - let directory = format!("cowboard/{}", message.id); - - if let Err(ex) = tokio::fs::remove_dir_all(&directory).await { - error!("Failed to remove directory: {}", ex); - } -} - -async fn format_username(ctx: &Context, message: &Message) -> String { - let username = format!("{}#{}", message.author.name, message.author.discriminator); - let nickname = message.author_nick(&ctx.http).await; - - if let Some(nick) = nickname { - format!("{} ({})", nick, username) - } else { - username - } -} - -async fn update_webhook_message(ctx: &Context, message: &Message, post_message: &Message, config: &mut Cowboard) { - let token = config.webhook_token.clone().unwrap(); - if let Ok(webhook) = ctx.http.get_webhook_with_token(config.webhook_id.unwrap(), &*token).await { - match count_reactions(ctx, message, config).await { - Ok(reacts) => { - let link = message.link_ensured(&ctx.http).await; - if let Err(ex) = webhook.edit_message(&ctx.http, post_message.id, - |m| m.content(format!("{} {} | <#{}>\n{}", reacts, &config.emote, message.channel_id, link))).await { - error!("Failed to edit post message??? {}", ex); - } - } - Err(ex) => { - error!("Failed to count reactions: {}", ex); - } - } - } else { - disable_webhook(ctx, config).await; - } -} - -async fn disable_webhook(ctx: &Context, config: &mut Cowboard) { - let db = db!(ctx); - - config.webhook_id = None; - config.webhook_token = None; - if let Err(ex) = db.update_cowboard(config).await { - error!("Failed to update cowboard settings: {}", ex); - } -} - -pub async fn remove_reaction(ctx: &Context, removed_reaction: &Reaction) { - if removed_reaction.guild_id.is_none() { - return; - } - - let guild_id = removed_reaction.guild_id.unwrap(); - let db = db!(ctx); - match db.get_cowboard_config(guild_id).await { - Ok(mut config) => { - match removed_reaction.message(&ctx.http).await { - Ok(message) => { - match count_reactions(ctx, &message, &config).await { - Ok(count) => { - let post_message = db.get_cowboard_message(message.id, message.channel_id, guild_id).await; - // Pray that the database's constraints work. - if count < config.remove_threshold as u64 { - // Unmoo that thing! - remove_moo(ctx, guild_id, removed_reaction.channel_id, removed_reaction.message_id).await; - } else if let Ok(Some(post)) = post_message { - if let Ok(mut post) = ctx.http.get_message(post.post_channel_id, post.post_id).await { - update_moo(ctx, &message, &mut post, &mut config).await; - } - } - } - Err(ex) => { - error!("Failed to count reactions: {}", ex); - } - } - } - Err(ex) => { - error!("Failed to get reacted message: {}", ex); - } - } - } - Err(ex) => { - error!("Failed to get cowboard config: {}", ex); - } - } -} - -pub async fn reaction_remove_all(ctx: &Context, channel_id: ChannelId, message: MessageId) { - let guild_id = channel_id.message(&ctx.http, message).await.ok().and_then(|o| o.guild_id); - if let Some(guild) = guild_id { - remove_moo(ctx, guild, channel_id, message).await; - } -} - -async fn remove_moo(ctx: &Context, guild_id: GuildId, channel_id: ChannelId, message: MessageId) { - let db = db!(ctx); - - match db.get_cowboard_message(message, channel_id, guild_id).await { - Ok(message_info) => { - if let Some(cowboard_message) = message_info { - if let Err(ex) = ctx.http.delete_message(cowboard_message.post_channel_id, cowboard_message.post_id).await { - error!("Failed to delete message: {} {} {}", ex, cowboard_message.post_channel_id, cowboard_message.post_id); - } - } - } - Err(ex) => { - error!("Failed to query cowboard message: {}", ex); - } - } - - if let Err(ex) = db.unmoo_message(message, channel_id, guild_id).await { - error!("Failed to unmoo a message in the database: {}", ex); - } -} \ No newline at end of file diff --git a/src/commands/cowboard/mod.rs b/src/commands/cowboard/mod.rs deleted file mode 100644 index b89aba5..0000000 --- a/src/commands/cowboard/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod cowboard_config; -mod cowboard_db; -mod cowboard_db_models; -pub mod cowboard_handler; - -use serenity::framework::standard::macros::group; - -use cowboard_config::*; - -#[group] -#[prefixes("cowboard")] -#[description = "Commands for modifying how the cowboard (starboard) functions."] -#[summary = "Cowboard"] -#[default_command(info)] -#[commands(info, emote, addthreshold, removethreshold, channel, webhook)] -struct Cowboard; - From f96134372689e5812df9c40bf793aa516642322a Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:00:26 -0700 Subject: [PATCH 02/30] Delete src/commands/general directory --- src/commands/general/ban.rs | 73 ---------------- src/commands/general/info.rs | 19 ----- src/commands/general/mod.rs | 15 ---- src/commands/general/rank.rs | 157 ----------------------------------- 4 files changed, 264 deletions(-) delete mode 100644 src/commands/general/ban.rs delete mode 100644 src/commands/general/info.rs delete mode 100644 src/commands/general/mod.rs delete mode 100644 src/commands/general/rank.rs diff --git a/src/commands/general/ban.rs b/src/commands/general/ban.rs deleted file mode 100644 index 68468cd..0000000 --- a/src/commands/general/ban.rs +++ /dev/null @@ -1,73 +0,0 @@ -use serenity::client::Context; -use serenity::framework::standard::{Args, CommandResult}; -use serenity::model::channel::{Message}; -use serenity::framework::standard::macros::{command}; - -#[command] -#[only_in(guilds)] -#[required_permissions("BAN_MEMBERS")] -async fn banleagueplayers(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - if args.is_empty() { - return ban_game_players(ctx, msg, 356869127241072640, "Playing League? Cringe.").await; - } - - ban_game_players(ctx, msg, 356869127241072640, args.message()).await -} - -#[command] -#[only_in(guilds)] -#[required_permissions("BAN_MEMBERS")] -async fn banvalorantplayers(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - if args.is_empty() { - return ban_game_players(ctx, msg, 700136079562375258, "Playing VALORANT? Cringe.").await; - } - ban_game_players(ctx, msg, 700136079562375258, args.message()).await -} - -#[command] -#[only_in(guilds)] -#[required_permissions("BAN_MEMBERS")] -async fn bangenshinplayers(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - if args.is_empty() { - return ban_game_players(ctx, msg, 762434991303950386, "Playing Genshin? Cringe.").await; - } - - ban_game_players(ctx, msg, 762434991303950386, args.message()).await -} - -#[command] -#[only_in(guilds)] -#[required_permissions("BAN_MEMBERS")] -async fn banoverwatchplayers(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - if args.is_empty() { - return ban_game_players(ctx, msg, 356875221078245376, "Dead Game.").await; - } - - ban_game_players(ctx, msg, 356875221078245376, args.message()).await -} - -async fn ban_game_players(ctx: &Context, msg: &Message, game_id: u64, message: impl AsRef) -> CommandResult { - if let Some(guild) = msg.guild(&ctx).await { - let mut degenerates: Vec = Vec::new(); - for (_, presence) in guild.presences.iter() { - if presence.activities.iter() - .filter_map(|o| o.application_id) - .any(|o| o == game_id) { - degenerates.push(u64::from(presence.user_id)); - if let Ok(dm_channel) = presence.user_id.create_dm_channel(&ctx.http).await { - dm_channel.say(&ctx.http, "You have been banned for playing haram games.").await?; - } - let _ = guild.ban_with_reason(&ctx.http, presence.user_id, 0, &message).await; - } - } - - let list = degenerates.iter().map(|o| format!("<@{}>", o)).reduce(|a, b| format!("{}, {}", a, b)); - if let Some(output) = list { - msg.channel_id.say(&ctx.http, format!("Successfully banned these degenerates: {}", output)).await?; - } else { - msg.channel_id.say(&ctx.http, "No haram activities detected.").await?; - } - } - - Ok(()) -} diff --git a/src/commands/general/info.rs b/src/commands/general/info.rs deleted file mode 100644 index 485409a..0000000 --- a/src/commands/general/info.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serenity::{ - client::Context, - model::channel::Message, - framework::standard::{ - CommandResult, - macros::{ - command - } - } -}; - -#[command] -#[description = "Info about this bot."] -pub async fn info(ctx: &Context, msg: &Message) -> CommandResult { - const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); - let content = format!("Cow v{} - A Discord bot written by HelloAndrew and DoggySazHi", VERSION.unwrap_or("")); - msg.channel_id.send_message(&ctx.http, |m| {m.content(content)}).await?; - Ok(()) -} \ No newline at end of file diff --git a/src/commands/general/mod.rs b/src/commands/general/mod.rs deleted file mode 100644 index abfff3b..0000000 --- a/src/commands/general/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod info; -mod rank; -mod ban; - -use serenity::framework::standard::macros::group; - -use info::*; -use rank::*; -use ban::*; - -#[group] -#[commands(info, rank, disablexp, levels, bangenshinplayers, banleagueplayers, banvalorantplayers, banoverwatchplayers)] -#[description = "General commands for miscellaneous tasks."] -#[summary = "Basic commands"] -struct General; diff --git a/src/commands/general/rank.rs b/src/commands/general/rank.rs deleted file mode 100644 index 6e097ef..0000000 --- a/src/commands/general/rank.rs +++ /dev/null @@ -1,157 +0,0 @@ -use serenity::{ - client::Context, - model::{ - channel::Message, - id::{ - UserId, - GuildId - }, - user::User - }, - framework::standard::{ - CommandResult, - macros::{ - command - }, - Args - }, - utils::MessageBuilder -}; -use crate::{Database, db}; -use log::{error}; - -async fn rank_embed(ctx: &Context, msg: &Message, server_id: &GuildId, user: &User) { - let db = db!(ctx); - - let experience = db.get_xp(*server_id, user.id).await.unwrap(); - let xp = experience.xp; - let level = experience.level; - let next_level_xp = db.calculate_level(level).await.unwrap(); - - let current_role = db.get_highest_role(*server_id, level).await.unwrap(); - let mut current_role_str: String = String::from("No role"); - if let Some(current_role_id) = current_role { - current_role_str = format!("Current role: <@&{}>", current_role_id); - } - - let mut pfp_url = user.default_avatar_url(); - if let Some(pfp_custom) = user.avatar_url() { - pfp_url = pfp_custom; - } - - let mut rank_str = String::from("(Unranked)"); - if let Some(rank) = db.rank_within_members(*server_id, user.id).await.unwrap() { - rank_str = format!("#{}", rank); - } - - if let Err(ex) = msg.channel_id.send_message(&ctx.http, |m| {m.embed(|e| { - e - .title( - MessageBuilder::new() - .push_safe(user.name.as_str()) - .push("#") - .push(user.discriminator) - .push("'s Ranking") - .build() - ) - .description(current_role_str) - .field("Level", level, true) - .field("XP", format!("{}/{}", xp, next_level_xp), true) - .field("Rank", rank_str, true) - .thumbnail(pfp_url) - })}).await { - error!("Failed to send embed: {}", ex); - } -} - -#[command] -#[description = "Get your current rank."] -#[only_in(guilds)] -pub async fn rank(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let other = args.single::(); - if let Some(server_id) = msg.guild_id { - if let Ok(other_id) = other { - if let Ok(other_user) = other_id.to_user(&ctx.http).await { - rank_embed(ctx, msg, &server_id, &other_user).await; - } else { - msg.channel_id.say(&ctx.http, "Could not find user...").await?; - } - } else { - rank_embed(ctx, msg, &server_id, &msg.author).await; - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "Disable/enable experience from being collected in the current channel."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -#[aliases("enablexp")] -pub async fn disablexp(ctx: &Context, msg: &Message) -> CommandResult { - let db = db!(ctx); - if let Some(server_id) = msg.guild_id { - let mut content: String; - match db.toggle_channel_xp(server_id, msg.channel_id).await { - Ok(toggle) => { - if toggle { - content = "Disabled".to_string(); - } else { - content = "Enabled".to_string(); - } - content += &*format!(" collecting experience in <#{}>.", msg.channel_id.as_u64()); - }, - Err(ex) => { - content = "Failed to toggle channel xp status.".to_string(); - error!("Failed to toggle channel xp status: {}", ex); - } - } - - msg.channel_id.send_message(&ctx.http, |m| {m.content(content)}).await?; - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "Get the current rankings in the server."] -#[only_in(guilds)] -pub async fn levels(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - if let Some(server_id) = msg.guild_id { - let page = args.single::().unwrap_or(1).max(1); - match db.top_members(server_id, page - 1).await { - Ok(pagination) => { - let content = pagination.members.into_iter() - .enumerate() - .into_iter() - .map(|o| { - let (index, member) = o; - format!("`#{}` <@{}> - Level {}, {} xp", (index as i32) + 10 * (page - 1) + 1, member.id, member.exp.level, member.exp.xp) - }) - .reduce(|a, b| {format!("{}\n{}", a, b)}) - .unwrap_or_else(|| "There is nothing on this page.".to_string()); - msg.channel_id.send_message(&ctx.http, |m| { - m.embed(|e| - e - .title("Top Users") - .description(content) - .footer(|e| e.text(format!("Page {}/{}", page, pagination.last_page))) - )}).await?; - }, - Err(ex) => { - msg.channel_id.say(&ctx.http, "Failed to get rankings.".to_string()).await?; - error!("Failed to get rankings: {}", ex); - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} \ No newline at end of file From fac311616c100100d6417a8a2dc469d8508a09ee Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:00:37 -0700 Subject: [PATCH 03/30] Delete src/commands/music directory --- src/commands/music/mod.rs | 13 - src/commands/music/music_commands.rs | 425 --------------------------- 2 files changed, 438 deletions(-) delete mode 100644 src/commands/music/mod.rs delete mode 100644 src/commands/music/music_commands.rs diff --git a/src/commands/music/mod.rs b/src/commands/music/mod.rs deleted file mode 100644 index 9f0047d..0000000 --- a/src/commands/music/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod music_commands; - -use serenity::framework::standard::macros::group; - -use music_commands::*; - -#[group] -#[prefixes("music")] -#[description = "Commands for playing music."] -#[summary = "Music"] -#[default_command(help)] -#[commands(help, join, leave, play, playlist, pause, now_playing, skip, queue)] -struct Music; \ No newline at end of file diff --git a/src/commands/music/music_commands.rs b/src/commands/music/music_commands.rs deleted file mode 100644 index 4397d92..0000000 --- a/src/commands/music/music_commands.rs +++ /dev/null @@ -1,425 +0,0 @@ -use lavalink_rs::model::{TrackQueue}; -use log::error; -use regex::Regex; -use serenity::client::Context; -use serenity::framework::standard::{CommandResult, Args}; -use serenity::model::channel::{Message}; -use serenity::framework::standard::macros::{command}; -use serenity::utils::MessageBuilder; -use crate::Lavalink; - -#[command] -#[aliases(p)] -async fn help(ctx: &Context, msg: &Message) -> CommandResult { - msg.channel_id.say(&ctx.http, "`help, join, leave, play, playlist, pause, now_playing, skip, queue`").await?; - - Ok(()) -} - -async fn join_interactive(ctx: &Context, msg: &Message) -> CommandResult { - let guild = msg.guild(&ctx.cache).await.unwrap(); - let guild_id = guild.id; - - let channel_id = guild - .voice_states - .get(&msg.author.id) - .and_then(|voice_state| voice_state.channel_id); - - let connect_to = match channel_id { - Some(channel) => channel, - None => { - msg.channel_id.say(&ctx.http, "Join a voice channel first.").await?; - return Ok(()); - } - }; - - let manager = songbird::get(ctx).await.unwrap().clone(); - - let (_, handler) = manager.join_gateway(guild_id, connect_to).await; - - match handler { - Ok(connection_info) => { - let lava_client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - lava_client.create_session(&connection_info).await?; - msg.channel_id.say(&ctx.http, format!("Joined <#{}>", connect_to)).await?; - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "Failed to join your VC...").await?; - error!("Error joining the channel: {}", ex) - } - } - - Ok(()) -} - -#[command] -#[only_in(guilds)] -async fn join(ctx: &Context, msg: &Message) -> CommandResult { - join_interactive(ctx, msg).await -} - -#[command] -#[only_in(guilds)] -async fn leave(ctx: &Context, msg: &Message) -> CommandResult { - let guild = msg.guild(&ctx.cache).await.unwrap(); - let guild_id = guild.id; - - let manager = songbird::get(ctx).await.unwrap().clone(); - let has_handler = manager.get(guild_id).is_some(); - - if has_handler { - if let Err(ex) = manager.remove(guild_id).await { - error!("Failed to disconnect: {}", ex); - } - - { - // Free up the LavaLink client. - let data = ctx.data.read().await; - let lava_client = data.get::().unwrap().clone(); - lava_client.destroy(guild_id).await?; - } - - msg.channel_id.say(&ctx.http, "Disconnected from VC. Goodbye!").await?; - } else { - msg.channel_id.say(&ctx.http, "I'm not in a VC.").await?; - } - - Ok(()) -} - -#[command] -#[only_in(guilds)] -async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - - if args.is_empty() { - msg.channel_id.say(&ctx.http, "Please enter a query or link.").await?; - } - - let query = args.message().to_string(); - - let guild_id = match ctx.cache.guild_channel(msg.channel_id).await { - Some(channel) => channel.guild_id, - None => { - - msg.channel_id.say(&ctx.http, "Error finding channel info").await?; - - return Ok(()); - } - }; - - let lava_client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - let manager = songbird::get(ctx).await.unwrap().clone(); - - if manager.get(guild_id).is_none() { - if let Err(ex) = join_interactive(ctx, msg).await { - msg.channel_id.say(&ctx.http, "Failed to connect to voice channel; maybe I don't have permissions?").await?; - error!("Failed to connect to vc: {}", ex); - return Ok(()); - } - } - - if let Some(_handler) = manager.get(guild_id) { - let query_information = lava_client.auto_search_tracks(&query).await?; - - if query_information.tracks.is_empty() { - msg.channel_id.say(&ctx, "Could not find any video of the search query.").await?; - return Ok(()); - } - - if let Err(why) = &lava_client.play(guild_id, query_information.tracks[0].clone()).queue() - .await - { - error!("Failed to queue: {}", why); - return Ok(()); - }; - - let message = MessageBuilder::new().push("Added to queue: ").push_mono_safe(&query_information.tracks[0].info.as_ref().unwrap().title).build(); - if let Ok(tracks) = lava_client.get_tracks(query).await { - if tracks.tracks.len() > 1 { - msg.channel_id.say(&ctx.http, "Note: This seems to be a playlist. If you want to add all tracks at once, use `playlist` instead of `play`.\n".to_string() + &*message).await?; - return Ok(()) - } - } - msg.channel_id.say(&ctx.http, message).await?; - } - - Ok(()) -} - -#[command] -#[only_in(guilds)] -async fn playlist(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - - if let Some(guild_id) = msg.guild_id { - if args.is_empty() { - msg.channel_id.say(&ctx.http, "Please enter a query or link.").await?; - return Ok(()) - } - - let query = args.message().to_string(); - - - - let lava_client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - let manager = songbird::get(ctx).await.unwrap().clone(); - - if manager.get(guild_id).is_none() { - if let Err(ex) = join_interactive(ctx, msg).await { - msg.channel_id.say(&ctx.http, "Failed to connect to voice channel; maybe I don't have permissions?").await?; - error!("Failed to connect to vc: {}", ex); - return Ok(()); - } - } - - if let Some(_handler) = manager.get(guild_id) { - match lava_client.get_tracks(&query).await { - Ok(tracks) => { - for track in &tracks.tracks { - if let Err(why) = &lava_client.play(guild_id, track.clone()).queue() - .await - { - error!("Failed to queue from playlist: {}", why); - }; - } - - if let Some(info) = &tracks.playlist_info { - if let Some(name) = &info.name { - msg.channel_id.say(&ctx.http, MessageBuilder::new().push("Added to the queue ").push(tracks.tracks.len()).push(" tracks from ").push_mono_safe(name).push(".")).await?; - } else { - msg.channel_id.say(&ctx.http, format!("Added to the queue {} tracks.", tracks.tracks.len())).await?; - } - } else { - msg.channel_id.say(&ctx.http, format!("Added to the queue {} tracks.", tracks.tracks.len())).await?; - } - } - Err(ex) => { - error!("Failed to load tracks: {}", ex); - msg.channel_id.say(&ctx, "Could not load any tracks from the given input.").await?; - } - } - } - } - - Ok(()) -} - -#[command] -#[only_in(guilds)] -async fn pause(ctx: &Context, msg: &Message) -> CommandResult { - if let Some(guild_id) = msg.guild_id { - let lava_client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - if let Some(node) = lava_client.nodes().await.get(&guild_id.0) { - if node.is_paused { - if let Err(ex) = lava_client.set_pause(guild_id, false).await { - error!("Failed to unpause music: {}", ex); - } else { - msg.channel_id.say(&ctx.http, "Unpaused the player.").await?; - } - } else if let Err(ex) = lava_client.pause(guild_id).await { - error!("Failed to pause music: {}", ex); - } else { - msg.channel_id.say(&ctx.http, "Paused the player.").await?; - } - } - } - - Ok(()) -} - -#[command] -#[only_in(guilds)] -#[aliases(np, nowplaying)] -async fn now_playing(ctx: &Context, msg: &Message) -> CommandResult { - let lava_client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - let guild_id = msg.guild_id.unwrap(); - - if let Some(node) = lava_client.nodes().await.get(&guild_id.0) { - if let Some(track) = &node.now_playing { - let info = track.track.info.as_ref().unwrap(); - let re = Regex::new(r#"(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^"&?/\s]{11})"#).unwrap(); - let caps = re.captures(&*info.uri).unwrap(); - let id = caps.get(1).map(|m| m.as_str()); - let server_name = guild_id.name(&ctx).await; - - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e - .author(|a| a.name(match server_name { - Some(name) => format!("Now Playing in {}", name), - None => "Now Playing".to_string() - })) - .title(&info.title) - .url(&info.uri) - .field("Artist", &info.author, true) - .field("Duration", format!("{}/{}", crate::util::from_ms(info.position), crate::util::from_ms(info.length)), true); - - - if let Some(requester) = track.requester { - e.field("Requested By", format!("<@{}>", requester), true); - } - - if let Some(yt_id) = id { - e.thumbnail(format!("https://img.youtube.com/vi/{}/maxresdefault.jpg", yt_id)); - } - - e - } - )).await?; - } else { - msg.channel_id.say(&ctx.http, "Nothing is playing at the moment.").await?; - } - } else { - msg.channel_id.say(&ctx.http, "Nothing is playing at the moment.").await?; - } - - Ok(()) -} - -#[command] -#[only_in(guilds)] -async fn skip(ctx: &Context, msg: &Message) -> CommandResult { - let lava_client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - if let Some(track) = lava_client.skip(msg.guild_id.unwrap()).await { - msg.channel_id.say(&ctx.http, MessageBuilder::new().push("Skipped: ").push_mono_line_safe(&track.track.info.as_ref().unwrap().title)).await?; - - // Need to check if it's empty, so we can stop playing (can crash if we don't check) - if let Some(node) = lava_client.nodes().await.get(&msg.guild_id.unwrap().0) { - if node.now_playing.is_none() { - if let Err(ex) = lava_client.stop(msg.guild_id.unwrap()).await { - error!("Failed to stop music: {}", ex); - } - } - } - } else { - msg.channel_id.say(&ctx.http, "There is nothing to skip.").await?; - } - - Ok(()) -} - -fn generate_line(song: &TrackQueue) -> String { - let info = song.track.info.as_ref().unwrap(); - - if let Some(person) = song.requester { - format!("{} - {} | ``{}`` Requested by: <@{}>\n\n", info.title, info.author, crate::util::from_ms(info.length), person) - } else { - format!("{} - {} | ``{}``\n\n", info.title, info.author, crate::util::from_ms(info.length)) - } -} - -fn generate_queue(queue: &[TrackQueue]) -> Vec { - let mut output: Vec = Vec::new(); - - if queue.is_empty() { - output.push("There are no songs queued.".to_string()); - } - - let mut index = 0; - while index < queue.len() { - let mut page = String::new(); - - // Max on one page is 10 just as a hard limit - for _ in 1..=10 { - if index >= queue.len() { - break; - } - - let song = &queue[index]; - index += 1; - let next_line = format!("``{}.`` {}", index, generate_line(song)); - - if page.len() + next_line.len() > 1024 { - index -= 1; - break; - } - - page.push_str(&*next_line); - } - - output.push(page); - } - - output -} - -#[command] -#[only_in(guilds)] -#[aliases(q)] -async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let lava_client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - let mut page_num = if let Ok(arg_page) = args.single::() { - arg_page - } else { - 1 - }; - - let guild_id = msg.guild_id.unwrap(); - if let Some(node) = lava_client.nodes().await.get(&guild_id.0) { - let queue = &node.queue; - let pages = generate_queue(queue); - - if page_num > pages.len() { - page_num = pages.len(); - } else if page_num == 0 { - page_num = 1; - } - - let page = &pages[page_num - 1]; - let server_name = guild_id.name(&ctx).await; - - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e - .author(|a| { - if let Some(server) = server_name { - a.name(format!("Player Queue | Page {}/{} | Playing in {}", page_num, pages.len(), server)); - } else { - a.name(format!("Player Queue | Page {}/{}", page_num, pages.len())); - } - - a - }) - .title("Now Playing") - .field("Queued", page, false); - - if let Some(now_playing) = &node.now_playing { - e.description(generate_line(now_playing)); - } else { - e.description("Nothing is playing."); - } - - e - })).await?; - - } else { - msg.channel_id.say(&ctx.http, "Nothing is playing at the moment.").await?; - } - - Ok(()) -} \ No newline at end of file From 0796e8b2f2fb2a1e818f07d96a8463301fa7e58c Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:00:54 -0700 Subject: [PATCH 04/30] Delete src/commands/rank_config directory --- src/commands/rank_config/diagnostics.rs | 220 --------------------- src/commands/rank_config/mod.rs | 15 -- src/commands/rank_config/roles.rs | 156 --------------- src/commands/rank_config/toggle_ranking.rs | 0 4 files changed, 391 deletions(-) delete mode 100644 src/commands/rank_config/diagnostics.rs delete mode 100644 src/commands/rank_config/mod.rs delete mode 100644 src/commands/rank_config/roles.rs delete mode 100644 src/commands/rank_config/toggle_ranking.rs diff --git a/src/commands/rank_config/diagnostics.rs b/src/commands/rank_config/diagnostics.rs deleted file mode 100644 index a73bc0c..0000000 --- a/src/commands/rank_config/diagnostics.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message, - id::{ - RoleId - } - }, - framework::standard::{ - CommandResult, - macros::{ - command - }, - Args - }, - utils::MessageBuilder -}; -use crate::{Database, db}; - -#[command] -#[description = "Scan for discrepancies between server member roles and the stored info."] -#[only_in(guilds)] -#[bucket = "diagnostics"] -#[required_permissions("ADMINISTRATOR")] -pub async fn scan(ctx: &Context, msg: &Message) -> CommandResult { - let db = db!(ctx); - if let Some(guild_id) = msg.guild_id { - let mut message = MessageBuilder::new(); - - let mut discord_message = msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| e - .title("Member Scan") - .description("Now processing, please wait warmly...") - )).await?; - - let roles = db.get_roles(guild_id).await?; - let role_set = roles.into_iter().filter_map(|r| r.role_id).collect::>(); - let users = db.get_users(guild_id).await?; - for u in users { - if let Ok(member) = guild_id.member(&ctx.http, u.user).await { - let member_role_set: HashSet = HashSet::from_iter(member.roles.iter().cloned()); - let intersection = role_set.intersection(&member_role_set).collect::>(); - if let Some(expected_role) = u.role_id { - if intersection.contains(&expected_role) && intersection.len() == 1 { - continue; // Correct: one role and it's the expected one - } - // Either doesn't have the role, wrong role, or too many roles - message.push("<@").push(u.user).push("> should have ").role(expected_role); - if intersection.is_empty() { - message.push(" but doesn't"); - } else { - message.push(" but has: "); - intersection.into_iter().for_each(|r| { message.push(" ").role(r).push(" "); }); - } - message.push("\n"); - } else { - if intersection.is_empty() { - continue; // Correct: no roles - } - // Has a role, when they shouldn't - message.push("<@").push(u.user).push("> has excess roles: "); - intersection.into_iter().for_each(|r| { message.push(" ").role(r).push(" "); }); - message.push("\n"); - } - } - } - - let mut content = message.build(); - if content.is_empty() { - content = "There were no discrepancies between our database and the server members.".to_string(); - } - - discord_message.edit(&ctx.http, |m| m.embed(|e| e - .title("Member Scan") - .description(content) - )).await?; - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "Fix any discrepancies between server member roles and the stored info. By default, this will only affect"] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -#[bucket = "diagnostics"] -#[usage = "\"multiple\" to fix users with multiple roles, \"remove\" to remove roles from users, and \"demote\" to modify ranks downwards."] -pub async fn fix(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - if let Some(guild_id) = msg.guild_id { - /* - There are several invalid cases we have to worry about: - - The user shouldn't have the role, and yet they do have conflicting roles (non-trivial) -> remove - - The user should have the role, and: - - they *do not* have any conflicting roles (trivial) - - they have one conflicting role - - and they should be higher up (trivial) - - and they should be lower down (non-trivial) -> demote - - they have multiple conflicting roles (non-trivial) -> multiple - - The trivial cases will be done by default, and the non-trivial cases can be done by options. - */ - - let (mut count_trivial, mut count_multiple, mut count_remove, mut count_demote, mut count_error, mut total_error, mut total) = (0, 0, 0, 0, 0, 0, 0); - - let (mut option_multiple, mut option_remove, mut option_demote) = (false, false, false); - - while !args.is_empty() { - let arg = args.single::().unwrap().to_lowercase(); - option_multiple |= arg.contains("multiple"); - option_remove |= arg.contains("remove"); - option_demote |= arg.contains("demote"); - } - - let mut discord_message = msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| e - .title("Role Auto-fix") - .description("Now fixing roles, please wait warmly...") - )).await?; - - let roles = db.get_roles(guild_id).await?; - let role_map = roles.into_iter().filter(|r| r.role_id.is_some()).map(|r| (r.role_id.unwrap(), r.min_level)).collect::>(); - let role_set: HashSet = role_map.keys().cloned().collect(); // Mildly disgusting. - let users = db.get_users(guild_id).await?; - for u in users { - if let Ok(mut member) = guild_id.member(&ctx.http, u.user).await { - total += 1; - - let member_role_set: HashSet = HashSet::from_iter(member.roles.iter().cloned()); - let intersection = role_set.intersection(&member_role_set).collect::>(); - if let Some(expected_role) = u.role_id { - if intersection.contains(&expected_role) && intersection.len() == 1 { - continue; // Correct: one role and it's the expected one - } - total_error += 1; - - if intersection.is_empty() { // They do not have the role, and need it - if let Err(ex) = member.add_role(&ctx.http, expected_role).await { - error!("Failed to add role: {}", ex); - count_error += 1; - } else { - count_trivial += 1; - } - } else if intersection.len() == 1 { // They have another role in place - let existing_role = intersection.into_iter().next().unwrap(); - let promote = role_map[existing_role] < role_map[&expected_role]; - if promote || option_demote { // Promote them - if let Err(ex) = member.remove_role(&ctx.http, existing_role).await { - error!("Failed to remove role for demoting: {}", ex); - count_error += 1; - } - - if let Err(ex) = member.add_role(&ctx.http, expected_role).await { - error!("Failed to add role for promoting/demoting: {}", ex); - count_error += 1; - } else if promote { - count_trivial += 1; - } else { - count_demote += 1; - } - } - } else if option_multiple { // We have multiple to deal with - for r in intersection { - if *r == expected_role { - continue; - } - - if let Err(ex) = member.remove_role(&ctx.http, r).await { - error!("Failed to remove excess roles: {}", ex); - count_error += 1; - } - } - - if !member.roles.contains(&expected_role) { - if let Err(ex) = member.add_role(&ctx.http, expected_role).await { - error!("Failed to add role: {}", ex); - count_error += 1; - } - } - - count_multiple += 1; - } - } else { - if intersection.is_empty() { - continue; // Correct: no roles - } - - total_error += 1; - - if option_remove { - for r in intersection { - if let Err(ex) = member.remove_role(&ctx.http, r).await { - error!("Failed to remove role: {}", ex); - count_error += 1; - } else { - count_remove += 1; - } - } - } - } - } - } - - discord_message.edit(&ctx.http, |m| m.embed(|e| e - .title("Role Auto-fix") - .description(format!("Processed {} members in the database with {} errors found:\n\ - - Trivial fixes: {}\n\ - - Fixes for multiple roles: {}\n\ - - Members with their roles fully revoked: {}\n\ - - Members demoted: {}\n\ - - Errors adding/removing roles: {}", total, total_error, count_trivial, count_multiple, count_remove, count_demote, count_error)) - )).await?; - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} \ No newline at end of file diff --git a/src/commands/rank_config/mod.rs b/src/commands/rank_config/mod.rs deleted file mode 100644 index b7f46b4..0000000 --- a/src/commands/rank_config/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod roles; -mod diagnostics; - -use serenity::framework::standard::macros::group; - -use roles::*; -use diagnostics::*; - -#[group] -#[prefixes("rankconfig", "rc")] -#[description = "Configuration to manage ranks and levelling on the server."] -#[summary = "Rank configuration"] -#[default_command(list)] -#[commands(list, add, remove, scan, fix)] -struct RankConfig; \ No newline at end of file diff --git a/src/commands/rank_config/roles.rs b/src/commands/rank_config/roles.rs deleted file mode 100644 index 88020a5..0000000 --- a/src/commands/rank_config/roles.rs +++ /dev/null @@ -1,156 +0,0 @@ -use serenity::{ - client::Context, - model::{ - channel::Message, - id::{ - RoleId - }, - guild::Guild - }, - framework::standard::{ - CommandResult, - macros::{ - command - }, - Args - }, - utils::{ - MessageBuilder - } -}; -use crate::{Database, db}; -use log::{error}; - -// Parameters: rankconfig add [min_level] [rank] - -async fn get_role(ctx: &Context, msg: &Message, guild: &Guild, args: &Args) -> Option<(RoleId, String)> { - let role_id: RoleId; - let mut role_text: String; - - if let Ok(role) = args.parse::() { - role_id = role; - if let Some(role) = guild.roles.get(&role) { - role_text = role.name.clone(); - } else { - if let Err(ex) = msg.channel_id.say(&ctx.http, format!("Could not find a role on this server matching <@&{}>!", role_id.as_u64())).await { - error!("Failed to send message: {}", ex); - } - return None - } - } else { - role_text = args.rest().to_string(); - if let Some(role) = guild.role_by_name(&*role_text) { - role_id = role.id; - role_text = role.name.clone(); // Just to make it exact. - } else { - let content = MessageBuilder::new().push("Could not find a role on this server matching \"").push_safe(role_text).push("\"!").build(); - if let Err(ex) = msg.channel_id.say(&ctx.http, content).await { - error!("Failed to send message: {}", ex); - } - return None - } - } - - Some((role_id, role_text)) -} - -#[command] -#[description = "Add a rank to the configuration."] -#[only_in(guilds)] -#[usage = " "] -#[required_permissions("ADMINISTRATOR")] -pub async fn add(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - // So much nesting... - if let Some(guild) = msg.guild(&ctx.cache).await { - if let Ok(min_level) = args.single::() { - if let Some((role_id, role_text)) = get_role(ctx, msg, &guild, &args).await { - // Both min_level and role_id are initialized by this point - match db.add_role(guild.id, &role_text, role_id, min_level).await { - Ok(success) => { - if success { - msg.channel_id.say(&ctx.http, format!("Successfully added <@&{}> with minimum level {}.", role_id.as_u64(), min_level)).await?; - } else { - msg.channel_id.say(&ctx.http, format!("There is a duplicate role with minimum level {}.", min_level)).await?; - } - } - Err(ex) => { - error!("Failed to add role for server: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to add role to the server.").await?; - } - } - } - } else { - msg.channel_id.say(&ctx.http, "The first argument should be a positive integer, representing the minimum level for this rank.").await?; - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "Remove a rank from the configuration."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn remove(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let db = db!(ctx); - // So much nesting... - if let Some(guild) = msg.guild(&ctx.cache).await { - if let Some((role_id, _)) = get_role(ctx, msg, &guild, &args).await { - match db.remove_role(guild.id, role_id).await { - Ok(success) => { - if success { - msg.channel_id.say(&ctx.http, format!("Successfully removed <@&{}>.", role_id.as_u64())).await?; - } else { - msg.channel_id.say(&ctx.http, "A rank didn't exist for this role.".to_string()).await?; - } - } - Err(ex) => { - error!("Failed to remove role for server: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to remove role from the server.").await?; - } - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "List the current ranks on this server."] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn list(ctx: &Context, msg: &Message) -> CommandResult { - let db = db!(ctx); - if let Some(guild_id) = msg.guild_id { - match db.get_roles(guild_id).await { - Ok(items) => { - if let Err(ex) = msg.channel_id.send_message(&ctx.http, |m| {m.embed(|e| { - e.title("Rank to Level Mapping") - .description( - items.into_iter() - .map(|i| { - let mut content = format!("{}: at level {}", i.name, i.min_level); - if let Some(role_id) = i.role_id { - content = format!("{}: <@&{}> at level {}", i.name, role_id, i.min_level); - } - content - }) - .reduce(|a, b| {format!("{}\n{}", a, b)}) - .unwrap_or_else(|| "No roles are registered on this server.".to_string()) - )})}).await { - error!("Failed to send message to server: {}", ex); - } - }, - Err(ex) => error!("Failed to get roles for server: {}", ex) - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} \ No newline at end of file diff --git a/src/commands/rank_config/toggle_ranking.rs b/src/commands/rank_config/toggle_ranking.rs deleted file mode 100644 index e69de29..0000000 From 661321602680edc843220fe8bac45c2b67791956 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:01:09 -0700 Subject: [PATCH 05/30] Delete src/commands/timeout directory --- src/commands/timeout/mod.rs | 14 ------ src/commands/timeout/timeout_config.rs | 61 -------------------------- 2 files changed, 75 deletions(-) delete mode 100644 src/commands/timeout/mod.rs delete mode 100644 src/commands/timeout/timeout_config.rs diff --git a/src/commands/timeout/mod.rs b/src/commands/timeout/mod.rs deleted file mode 100644 index 253cbe9..0000000 --- a/src/commands/timeout/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod timeout_config; - -use serenity::framework::standard::macros::group; - -use timeout_config::*; - -#[group] -#[prefixes("timeout")] -#[description = "Commands for viewing and settinge the cooldown for chat xp."] -#[summary = "Timeouts"] -#[default_command(get)] -#[commands(set, get)] -struct Timeout; - diff --git a/src/commands/timeout/timeout_config.rs b/src/commands/timeout/timeout_config.rs deleted file mode 100644 index d4d0d00..0000000 --- a/src/commands/timeout/timeout_config.rs +++ /dev/null @@ -1,61 +0,0 @@ -use log::error; -use serenity::{ - framework::standard::{ - macros::command, Args, CommandResult, - }, - model::channel::Message, client::Context -}; - -use crate::{Database, db}; -use crate::util::{ to_ms, from_ms }; - -#[command] -#[description = "Sets server-wide cooldown for messaging xp gain."] -#[usage = "<#m#d#s#h> in any order"] -#[only_in(guilds)] -#[required_permissions("ADMINISTRATOR")] -pub async fn set(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let db = db!(ctx); - // nesting part 2 - if let Some(server_id) = msg.guild_id { - if let Ok(timeout) = args.single::() { - if let Some(timeout) = to_ms(timeout) { - match db.set_timeout(server_id, timeout).await { - Ok(_) => { msg.reply(&ctx.http, format!("Set timeout to {}.", from_ms(timeout as u64))).await?; } - Err(err) => { - msg.reply(&ctx.http, "Could not set timeout").await?; - error!("Could not set timeout: {}", err); - } - } - } else { - msg.reply(&ctx.http, "The timeout must be in the form #s#m#h#d").await?; - } - } else { - msg.reply(&ctx.http, "The timeout must be in the form #s#m#h#d").await?; - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} - -#[command] -#[description = "Gets the server-wide cooldown for messaging xp gain."] -#[only_in(guilds)] -pub async fn get(ctx: &Context, msg: &Message) -> CommandResult { - let db = db!(ctx); - if let Some(server_id) = msg.guild_id { - match db.get_timeout(server_id).await { - Ok(timeout) => { msg.reply(&ctx.http, format!("The timeout is {}.", from_ms(timeout as u64))).await?; } - Err(err) => { - msg.reply(&ctx.http, "Could not set timeout").await?; - error!("Could not get timeout: {}", err); - } - } - } else { - msg.reply(&ctx.http, "This command can only be run in a server.").await?; - } - - Ok(()) -} \ No newline at end of file From ad07171dae4bf7cadae6d0e9b50eddae4ff2aa17 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:01:26 -0700 Subject: [PATCH 06/30] Delete src/commands/ucm/reminders directory --- .../ucm/reminders/course_reminders.rs | 156 ------------------ src/commands/ucm/reminders/mod.rs | 59 ------- 2 files changed, 215 deletions(-) delete mode 100644 src/commands/ucm/reminders/course_reminders.rs delete mode 100644 src/commands/ucm/reminders/mod.rs diff --git a/src/commands/ucm/reminders/course_reminders.rs b/src/commands/ucm/reminders/course_reminders.rs deleted file mode 100644 index 40666af..0000000 --- a/src/commands/ucm/reminders/course_reminders.rs +++ /dev/null @@ -1,156 +0,0 @@ -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - }, Args - } -}; - -use crate::{db, Database}; -use crate::commands::ucm::courses_db_models::Reminder; - -#[command] -#[description = "List the reminders set."] -pub async fn list(ctx: &Context, msg: &Message) -> CommandResult { - let db = db!(ctx); - - match db.get_user_reminders(msg.author.id).await { - Ok(reminders) => { - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e.title("Your Course Reminders"); - - if reminders.is_empty() { - e.description("You do not have any reminders set. Add some using `reminders add`."); - } else { - for reminder in reminders { - e.field(format!("CRN {}", reminder.course_reference_number), - format!("Minimum Trigger: `{}`\nFor Waitlist: `{}`\nTriggered: `{}`", reminder.min_trigger, reminder.for_waitlist, reminder.triggered), - false); - } - } - - e - })).await?; - } - Err(ex) => { - error!("Failed to get reminders for user: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to get your reminders... try again later?").await?; - } - } - - Ok(()) -} - -#[command] -#[description = "Control reminders for class seats."] -#[usage = "[CRN] "] -pub async fn add(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if args.is_empty() { - msg.channel_id.say(&ctx.http, "You need to pass in a valid CRN.\n\ - You can also pass in the minimum amount of seats to trigger the reminder, as well.\n\ - If you want, you can also make it trigger on waitlist seats instead (true/false), however you must have done the previous part beforehand.\n\ - Ex. `reminders add 31415 1 true`").await?; - return Ok(()); - } - - let mut min_trigger = 1; - let mut for_waitlist = false; - - let course_reference_number = match args.single::() { - Ok(value) => { value } - Err(_) => { - msg.channel_id.say(&ctx.http, "You need to pass in a valid CRN for the first value.").await?; - return Ok(()); - } - }; - - if !args.is_empty() { - match args.single::() { - Ok(value) => { - if value < 1 { - msg.channel_id.say(&ctx.http, "Your minimum trigger must be greater than or equal to 1 seat.").await?; - return Ok(()); - } - min_trigger = value; - } - Err(_) => { - msg.channel_id.say(&ctx.http, "You need to pass in a positive integer for minimum trigger.").await?; - return Ok(()); - } - } - } - - if !args.is_empty() { - match args.single::() { - Ok(value) => { - for_waitlist = value; - } - Err(_) => { - msg.channel_id.say(&ctx.http, "Put \"true\" if you want to trigger on waitlist slots, otherwise omit this field (or put \"false\").").await?; - return Ok(()); - } - } - } - - let reminder = Reminder { - user_id: msg.author.id.0, - course_reference_number, - min_trigger, - for_waitlist, - triggered: false - }; - - let db = db!(ctx); - - if let Ok(Some(class)) = db.get_class(course_reference_number).await { - if let Err(ex) = db.add_reminder(&reminder).await { - error!("Failed to add reminder: {}", ex); - msg.channel_id.say(&ctx.http, "Error adding your reminder. Maybe you have a duplicate?").await?; - } else { - msg.channel_id.say(&ctx.http, format!("Successfully added your reminder for {}: {}!", - class.course_number, - class.course_title.unwrap_or_else(|| "".to_string()) - )).await?; - } - } else { - msg.channel_id.say(&ctx.http, "Could not find this CRN... did you type it right?").await?; - } - - Ok(()) -} - -#[command] -#[description = "Control reminders for class seats."] -pub async fn remove(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if args.is_empty() { - msg.channel_id.say(&ctx.http, "You need to pass in a valid CRN for a reminder you set up.").await?; - return Ok(()); - } - - if let Ok(course_reference_number) = args.single::() { - let db = db!(ctx); - match db.remove_reminder(msg.author.id, course_reference_number).await { - Ok(success) => { - if success { - msg.channel_id.say(&ctx.http, "Successfully removed your reminder.").await?; - } else { - msg.channel_id.say(&ctx.http, "You did not have a reminder with this CRN.").await?; - } - } - Err(ex) => { - error!("Failed to remove reminder: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to remove your reminder... try again later?").await?; - } - } - } else { - msg.channel_id.say(&ctx.http, "That is not a valid CRN.").await?; - } - - Ok(()) -} \ No newline at end of file diff --git a/src/commands/ucm/reminders/mod.rs b/src/commands/ucm/reminders/mod.rs deleted file mode 100644 index b4c07e2..0000000 --- a/src/commands/ucm/reminders/mod.rs +++ /dev/null @@ -1,59 +0,0 @@ -mod course_reminders; - -use serenity::framework::standard::macros::group; - -use course_reminders::*; -use std::sync::Arc; -use std::time::Duration; -use log::error; -use serenity::{ - CacheAndHttp, - prelude::TypeMap -}; -use tokio::sync::RwLock; -use tokio::time; -use crate::{Database}; - -#[group] -#[prefixes("reminders", "reminder", "remind")] -#[description = "Set up reminders for class registration, based off seats or waitlist."] -#[summary = "UCM Course Waitlist"] -#[default_command(list)] -#[commands(add, remove, list)] -struct Reminders; - -pub async fn check_reminders(data: Arc>, ctx: Arc) { - let mut interval_min = time::interval(Duration::from_secs(60)); - loop { - interval_min.tick().await; - let ctx_global = data.read().await; - let db = ctx_global.get::().expect("Couldn't find database").clone(); - match db.trigger_reminders().await { - Ok(triggers) => { - for trigger in triggers { - if let Ok(user) = ctx.http.get_user(trigger.user_id).await { - if let Ok(Some(class)) = db.get_class(trigger.course_reference_number).await { - if let Err(ex) = user.direct_message(&ctx.http, |m| { - m.embed(|e| e - .title("Reminder Triggered~") - .description(class.course_title.unwrap_or_else(|| "".to_string())) - .field("Course Number", class.course_number, true) - .field("Course Reference Number", class.course_reference_number, true) - .field("Seats Available/Total", format!("{}/{}", class.seats_available, class.maximum_enrollment), true) - .field("Waitlist Available/Total", format!("{}/{}", class.wait_available, class.wait_capacity), true) - ) - }).await { - error!("Failed to send DM to user: {}", ex); - } - } - } else { - error!("Failed to get user"); - } - } - }, - Err(ex) => { - error!("Failed to query reminders: {}", ex); - } - } - } -} \ No newline at end of file From 0048dd5b6b24c4ac24da1c190e33e69965cc9023 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:01:39 -0700 Subject: [PATCH 07/30] Delete calendar.rs --- src/commands/ucm/calendar.rs | 149 ----------------------------------- 1 file changed, 149 deletions(-) delete mode 100644 src/commands/ucm/calendar.rs diff --git a/src/commands/ucm/calendar.rs b/src/commands/ucm/calendar.rs deleted file mode 100644 index 15746e7..0000000 --- a/src/commands/ucm/calendar.rs +++ /dev/null @@ -1,149 +0,0 @@ -use chrono::{Datelike, Local}; -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - } - } -}; -use scraper::{Html, Selector}; -use serenity::framework::standard::Args; - -pub struct Semester { - pub name: String, - pub dates: Vec<(String, String)> -} - -pub struct AcademicCalendar { - pub name: String, - pub semesters: Vec -} - -fn process_calendar(data: &str) -> Option { - let page = Html::parse_document(data); - - let select_page_name = Selector::parse("h1").unwrap(); - let select_table_name = Selector::parse("h2").unwrap(); - let select_table = Selector::parse("table").unwrap(); - let select_row = Selector::parse("tr").unwrap(); - let select_column = Selector::parse("td").unwrap(); - - let page_name = page.select(&select_page_name).next().map(|o| o.text().next().map(|o| o.to_string())); - - // Ensure this is a calendar page, not some other weird thing. - if let Some(Some(ref name)) = page_name { - if !name.to_lowercase().contains("calendar") { - return None; - } - } else { - return None; - } - - let title_names = page - .select(&select_table_name).flat_map(|o| o.text() - .filter(|p| { - let lowercase = p.to_lowercase(); - lowercase.contains("semester") || lowercase.contains("session") - })) - .map(|o| o.to_string()); - - let tables = page - .select(&select_table) - .map(|table| table - .select(&select_row) - .map(|row| { - let items = row - .select(&select_column) - .take(2) - .map(|col| col.text().next().map(|o| o.to_string()).unwrap_or_else(|| "".to_string())) - .collect::>(); - - (items.get(0).map(|o| o.to_string()).unwrap_or_else(|| "".to_string()), - items.get(1).map(|o| o.to_string()).unwrap_or_else(|| "".to_string())) - }) - .collect::>() - ); - - let semesters = title_names.zip(tables) - .map(|o| { - let (name, dates) = o; - Semester { name, dates } - }) - .collect::>(); - - Some(AcademicCalendar { name: page_name.unwrap().unwrap(), semesters }) -} - -async fn print_schedule(ctx: &Context, msg: &Message, schedule: &AcademicCalendar) -> CommandResult { - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e.title(&schedule.name); - - for semester in &schedule.semesters { - let output = semester.dates.iter() - .map(|o| { - let (l, r) = o; - format!("{} - {}", l, r) - }) - .reduce(|a, b| format!("{}\n{}", a, b)) - .unwrap_or_else(|| "Nothing was written...".to_string()); - - e.field(&semester.name, output, false); - } - - e - })).await?; - - Ok(()) -} - -#[command] -#[aliases(cal, academiccalendar)] -#[description = "Get the academic calendar for the year."] -pub async fn calendar(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let now = Local::now(); - let mut year = now.year(); - - if now.month() <= 7 { // Spring or summer semester are still on the previous year. - year -= 1; - } - - while !args.is_empty() { - if let Ok(maybe_year) = args.single::() { - if maybe_year >= 2005 { - year = maybe_year; - } - } - } - - let url = format!("https://registrar.ucmerced.edu/schedules/academic-calendar/academic-calendar-{}-{}", year, year + 1); - match reqwest::get(url).await { - Ok(response) => { - match response.text().await { - Ok(data) => { - let schedules = process_calendar(&*data); - if let Some(calendar) = schedules { - print_schedule(ctx, msg, &calendar).await?; - } else { - msg.channel_id.say(&ctx.http, "Either you inputted an invalid year, or the website did not give us reasonable data.").await?; - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "UC Merced gave us weird data, try again later?").await?; - error!("Failed to process calendar: {}", ex); - } - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "Failed to connect to the UC Merced website, try again later?").await?; - error!("Failed to get food truck schedule: {}", ex); - } - } - - Ok(()) -} \ No newline at end of file From 964fa5d5fa5d7d1ed5916f6ccfd67d605771fe42 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:01:46 -0700 Subject: [PATCH 08/30] Delete course_models.rs --- src/commands/ucm/course_models.rs | 61 ------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/commands/ucm/course_models.rs diff --git a/src/commands/ucm/course_models.rs b/src/commands/ucm/course_models.rs deleted file mode 100644 index 788af88..0000000 --- a/src/commands/ucm/course_models.rs +++ /dev/null @@ -1,61 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -#[serde(rename_all="camelCase")] -pub struct Course { - // there's no guarantee which one can be null - pub id: u64, - pub term_effective: Option, - pub course_number: Option, - pub subject: Option, - pub subject_code: Option, - pub college: Option, - pub college_code: Option, - pub department: Option, - pub department_code: Option, - pub course_title: Option, - pub credit_hour_indicator: Option, - pub subject_description: Option, - pub course_description: Option, - pub division: Option, - pub term_start: Option, - pub term_end: Option -} - -#[derive(Debug, Deserialize)] -pub struct CourseSearchConfig { - pub config: Option, - pub display: Option, - pub title: Option, - pub required: bool, - pub width: Option -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all="camelCase")] -pub struct DisplaySettings { - pub enrollment_display: Option, - pub waitlist_display: Option, - pub cross_list_display: Option -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all="camelCase")] -pub struct CourseList { - pub success: bool, - pub total_count: u64, - pub data: Vec, - pub page_offset: u64, - pub page_max_size: u64, - pub path_mode: Option, - pub course_search_results_configs: Vec, - pub display_settings: DisplaySettings, - pub is_plan_by_crn_set_for_term: bool -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all="camelCase")] -pub struct Semester { - pub code: String, - pub description: String -} \ No newline at end of file From d15baf6f4d10d40652f44709bf9871d53230d2fd Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:01:55 -0700 Subject: [PATCH 09/30] Delete courses.rs --- src/commands/ucm/courses.rs | 229 ------------------------------------ 1 file changed, 229 deletions(-) delete mode 100644 src/commands/ucm/courses.rs diff --git a/src/commands/ucm/courses.rs b/src/commands/ucm/courses.rs deleted file mode 100644 index 4fc53a6..0000000 --- a/src/commands/ucm/courses.rs +++ /dev/null @@ -1,229 +0,0 @@ -use chrono::{Datelike, DateTime, Local, TimeZone, Utc}; -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - }, Args - } -}; -use crate::commands::ucm::courses_db_models::*; -use crate::{Database, db}; - -fn fix_time(time: &str) -> String { - let hour_str = &time[..2]; - let minute_str = &time[2..]; - let hour = hour_str.parse::().unwrap(); - - if hour == 0 { - return format!("12:{} AM", minute_str); - } - if hour == 12 { - return format!("12:{} PM", minute_str); - } - if hour < 12 { - return format!("{}:{} AM", hour, minute_str); - } - format!("{}:{} PM", hour - 12, minute_str) -} - -pub fn format_term(term: i32) -> String { - let semester = match term % 100 { - 30 => "Fall", - 20 => "Summer", - 10 => "Spring", - _ => "Unknown" - }; - - format!("{} {}", semester, term / 100) -} - -pub fn semester_from_text(input: &str) -> Option { - match input.to_lowercase().as_str() { - "fall" => Some(30), - "summer" => Some(20), - "spring" => Some(10), - _ => None - } -} - -async fn course_embed(ctx: &Context, msg: &Message, class: &Class) -> CommandResult { - let db = db!(ctx); - let professors = db.get_professors_for_class(class.id).await; - let meetings = db.get_meetings_for_class(class.id).await; - let stats = db.get_stats().await; - - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e.title(format!("{}: {}", &class.course_number, class.course_title.clone().unwrap_or_else(|| "".to_string()))); - e.description("Enrollment and Waitlist are in terms of seats available/seats taken/max seats."); - e.field("CRN", class.course_reference_number, true); - e.field("Credit Hours", class.credit_hours, true); - e.field("Term", format_term(class.term), true); - e.field("Enrollment", format!("{}/{}/{}", class.seats_available, class.enrollment, class.maximum_enrollment), true); - e.field("Waitlist", format!("{}/{}/{}", class.wait_available, class.wait_capacity - class.wait_available, class.wait_capacity), true); - - if let Ok(professors) = professors { - e.field("Professor(s)", - professors.iter() - .map(|o| format!("- {}", o.full_name.clone())) - .reduce(|a, b| format!("{}\n{}", a, b)) - .unwrap_or_else(|| "No professors are assigned to this course.".to_string()), - false); - } - - if let Ok(meetings) = meetings { - e.field("Meeting(s)", - meetings.iter() - .map(|o| { - let output = format!("- {}: {} {}", - o.meeting_type, o.building_description.clone().unwrap_or_else(|| "".to_string()), o.room.clone().unwrap_or_else(|| "".to_string())); - if o.begin_time.is_some() && o.end_time.is_some() { - let begin_time = o.begin_time.clone().unwrap(); - let end_time = o.end_time.clone().unwrap(); - return format!("{} ({} - {}) from {} to {} on {}", output, o.begin_date, o.end_date, fix_time(&begin_time), fix_time(&end_time), o.in_session); - } - - output - }) - .reduce(|a, b| format!("{}\n{}", a, b)) - .unwrap_or_else(|| "No meetings are assigned to this course.".to_string()), - false); - } - - if let Ok(stats) = stats { - if let Some(class_update) = stats.get("class") { - let local_time: DateTime = Local.from_local_datetime(class_update).unwrap(); - let utc_time: DateTime = DateTime::from(local_time); - e.footer(|f| f.text("Last updated at")); - e.timestamp(utc_time); - } - } - - e - })).await?; - - Ok(()) -} - -#[command] -#[description = "Search for courses in a term."] -#[aliases("course", "class", "classes")] -#[usage = " [Semester] [Year]"] -pub async fn courses(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - if args.is_empty() { - msg.channel_id.say(&ctx.http, "Type the CRN, course number, or name of the class to look it up.").await?; - return Ok(()); - } - - let current_date = Local::now().date(); - let mut year = current_date.year(); - // You are required to specify if you want a summer class. Baka. - let mut semester = if current_date.month() >= 3 && current_date.month() <= 10 { 30 } else { 10 }; - let mut search_query = String::new(); - - while !args.is_empty() { - if let Ok(numeric) = args.parse::() { - // Make sure it's not a year lol - if numeric >= 10000 { - let db = db!(ctx); - match db.get_class(numeric).await { - Ok(option_class) => { - if let Some(class) = option_class { - course_embed(ctx, msg, &class).await?; - } else { - msg.channel_id.say(&ctx.http, format!("Could not find a class with the CRN `{}`.", numeric)).await?; - } - } - Err(ex) => { - error!("Failed to get class: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to query our database... try again later?").await?; - } - } - return Ok(()) - } else if numeric >= 2005 { - year = numeric; - args.advance(); - continue; - } - } - - let text = args.single::().unwrap(); - if let Some(sem) = semester_from_text(&text) { - semester = sem; - } else { - search_query.push(' '); - search_query.push_str(&text); - } - } - - let term = year * 100 + semester; - match search_course_by_number(ctx, msg, &search_query, term).await { - Ok(any) => { - if !any { - match search_course_by_name(ctx, msg, &search_query, term).await { - Ok(any) => { - if !any { - msg.channel_id.say(&ctx.http, "Failed to find any classes with the given query. Did you mistype the input?").await?; - } - } - Err(ex) => { - error!("Failed to search by name: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to search for classes... try again later?").await?; - } - } - } - } - Err(ex) => { - error!("Failed to search by name: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to search for classes... try again later?").await?; - } - } - - Ok(()) -} - -async fn search_course_by_number(ctx: &Context, msg: &Message, search_query: &str, term: i32) -> Result> { - let db = db!(ctx); - let classes = db.search_class_by_number(search_query, term).await?; - print_matches(ctx, msg, &classes).await?; - - Ok(!classes.is_empty()) -} - -async fn search_course_by_name(ctx: &Context, msg: &Message, search_query: &str, term: i32) -> Result> { - let db = db!(ctx); - let classes = db.search_class_by_name(search_query, term).await?; - print_matches(ctx, msg, &classes).await?; - - Ok(!classes.is_empty()) -} - -async fn print_matches(ctx: &Context, msg: &Message, classes: &[PartialClass]) -> Result<(), Box> { - if classes.is_empty() { return Ok(()); } - - if classes.len() == 1 { - let db = db!(ctx); - let class = db.get_class(classes[0].course_reference_number).await?.unwrap(); - course_embed(ctx, msg, &class).await?; - } else { - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e.title("Class Search").description("Multiple results were found for your query. Search again using the CRN for a particular class."); - e.field(format!("Classes Matched (totalling {})", classes.len()), - classes - .iter() - .take(10) - .map(|o| format!("`{}` - {}: {}", o.course_reference_number, o.course_number, o.course_title.clone().unwrap_or_else(|| "".to_string()))) - .reduce(|a, b| format!("{}\n{}", a, b)) - .unwrap(), - false); - e - })).await?; - } - - Ok(()) -} \ No newline at end of file From 4fa588cd2e646045948e5225ff910cf8495d9ea0 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:02:06 -0700 Subject: [PATCH 10/30] Delete courses_db.rs --- src/commands/ucm/courses_db.rs | 351 --------------------------------- 1 file changed, 351 deletions(-) delete mode 100644 src/commands/ucm/courses_db.rs diff --git a/src/commands/ucm/courses_db.rs b/src/commands/ucm/courses_db.rs deleted file mode 100644 index 98418b0..0000000 --- a/src/commands/ucm/courses_db.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::collections::HashMap; -use chrono::NaiveDateTime; -use num_traits::ToPrimitive; -use serenity::{ - model::id::{ - UserId - } -}; -use rust_decimal::{ - Decimal, - prelude::FromPrimitive -}; - -use crate::Database; -use crate::commands::ucm::courses_db_models::*; - -impl Database { - pub async fn get_user_reminders(&self, user_id: UserId) -> Result, Box> { - let mut conn = self.pool.get().await?; - let user_decimal = Decimal::from_u64(user_id.0).unwrap(); - let res = conn.query( - "SELECT course_reference_number, min_trigger, for_waitlist, triggered FROM [UniScraper].[UCM].[reminder] WHERE user_id = @P1", - &[&user_decimal]) - .await? - .into_first_result() - .await?; - - let mut out: Vec = Vec::new(); - - for reminder in res { - out.push(Reminder { - user_id: user_id.0, - course_reference_number: reminder.get(0).unwrap(), - min_trigger: reminder.get(1).unwrap(), - for_waitlist: reminder.get(2).unwrap(), - triggered: reminder.get(3).unwrap() - }); - } - - Ok(out) - } - - pub async fn add_reminder(&self, reminder: &Reminder) -> Result<(), Box> { - let mut conn = self.pool.get().await?; - let user_decimal = Decimal::from_u64(reminder.user_id).unwrap(); - - // Will panic if there is a duplicate, since I have uniqueness set. - conn.execute( - "INSERT INTO [UniScraper].[UCM].[reminder] (user_id, course_reference_number, min_trigger, for_waitlist, triggered) VALUES (@P1, @P2, @P3, @P4, @P5)", - &[&user_decimal, &reminder.course_reference_number, &reminder.min_trigger, &reminder.for_waitlist, &reminder.triggered]) - .await?; - - Ok(()) - } - - pub async fn remove_reminder(&self, user_id: UserId, course_reference_number: i32) -> Result> { - let mut conn = self.pool.get().await?; - let user_decimal = Decimal::from_u64(user_id.0).unwrap(); - - let total = conn.execute( - "DELETE FROM [UniScraper].[UCM].[reminder] WHERE user_id = @P1 AND course_reference_number = @P2", - &[&user_decimal, &course_reference_number]) - .await?.total(); - - Ok(total > 0) - } - - pub async fn trigger_reminders(&self) -> Result, Box> { - let mut conn = self.pool.get().await?; - - let res = conn.simple_query( - "EXEC [UniScraper].[UCM].[TriggerReminders]") - .await? - .into_first_result() - .await?; - - let mut out: Vec = Vec::new(); - - for reminder in res { - let user_id: Decimal = reminder.get(0).unwrap(); - out.push(Trigger { - user_id: user_id.to_u64().unwrap(), - course_reference_number: reminder.get(1).unwrap(), - min_trigger: reminder.get(2).unwrap() - }); - } - - Ok(out) - } - - pub async fn get_class(&self, course_reference_number: i32) -> Result, Box> { - let mut conn = self.pool.get().await?; - let res = conn.query( - "SELECT id, term, course_number, campus_description, course_title, credit_hours, maximum_enrollment, enrollment, seats_available, wait_capacity, wait_available FROM [UniScraper].[UCM].[class] WHERE course_reference_number = @P1", - &[&course_reference_number]) - .await? - .into_row() - .await?; - - let mut out: Option = None; - - if let Some(class) = res { - let course_number: &str = class.get(2).unwrap(); - let campus_description: Option<&str> = class.get(3); - let course_title: Option<&str> = class.get(4); - out = Some(Class { - id: class.get(0).unwrap(), - term: class.get(1).unwrap(), - course_reference_number, - course_number: course_number.to_string(), - campus_description: campus_description.map(|o| o.to_string()), - course_title: course_title.map(|o| o.to_string()), - credit_hours: class.get(5).unwrap(), - maximum_enrollment: class.get(6).unwrap(), - enrollment: class.get(7).unwrap(), - seats_available: class.get(8).unwrap(), - wait_capacity: class.get(9).unwrap(), - wait_available: class.get(10).unwrap() - }); - } - - Ok(out) - } - - // Note: class_id is referring to an ID stored in the database, not the CRN. Fetch this through get_class. - pub async fn get_professors_for_class(&self, class_id: i32) -> Result, Box> { - let mut conn = self.pool.get().await?; - let res = conn.query( - "SELECT professor.id, rmp_id, last_name, first_name, middle_name, email, department, num_ratings, rating, full_name FROM [UniScraper].[UCM].[professor] INNER JOIN [UniScraper].[UCM].[faculty] ON professor.id = faculty.professor_id WHERE class_id = @P1;", - &[&class_id]) - .await? - .into_first_result() - .await?; - - let mut out: Vec = Vec::new(); - - for professor in res { - let last_name: &str = professor.get(2).unwrap(); - let first_name: &str = professor.get(3).unwrap(); - let middle_name: Option<&str> = professor.get(4); - let email: Option<&str> = professor.get(5); - let department: Option<&str> = professor.get(6); - let full_name: &str = professor.get(9).unwrap(); - out.push(Professor { - id: professor.get(0).unwrap(), - rmp_id: professor.get(1), - last_name: last_name.to_string(), - first_name: first_name.to_string(), - middle_name: middle_name.map(|o| o.to_string()), - email: email.map(|o| o.to_string()), - department: department.map(|o| o.to_string()), - num_ratings: professor.get(7).unwrap(), - rating: professor.get(8).unwrap(), - full_name: full_name.to_string() - }); - } - - Ok(out) - } - - // Note: class_id is referring to an ID stored in the database, not the CRN. Fetch this through get_class. - pub async fn get_meetings_for_class(&self, class_id: i32) -> Result, Box> { - let mut conn = self.pool.get().await?; - let res = conn.query( - "SELECT begin_time, end_time, begin_date, end_date, building, building_description, campus, campus_description, room, credit_hour_session, hours_per_week, in_session, meeting_type FROM [UniScraper].[UCM].[meeting] WHERE class_id = @P1;", - &[&class_id]) - .await? - .into_first_result() - .await?; - - let mut out: Vec = Vec::new(); - - for meeting in res { - let begin_time: Option<&str> = meeting.get(0); - let end_time: Option<&str> = meeting.get(1); - let begin_date: &str = meeting.get(2).unwrap(); - let end_date: &str = meeting.get(3).unwrap(); - let building: Option<&str> = meeting.get(4); - let building_description: Option<&str> = meeting.get(5); - let campus: Option<&str> = meeting.get(6); - let campus_description: Option<&str> = meeting.get(7); - let room: Option<&str> = meeting.get(8); - let meeting_type: u8 = meeting.get(12).unwrap(); - out.push(Meeting { - class_id, - begin_time: begin_time.map(|o| o.to_string()), - end_time: end_time.map(|o| o.to_string()), - begin_date: begin_date.to_string(), - end_date: end_date.to_string(), - building: building.map(|o| o.to_string()), - building_description: building_description.map(|o| o.to_string()), - campus: campus.map(|o| o.to_string()), - campus_description: campus_description.map(|o| o.to_string()), - room: room.map(|o| o.to_string()), - credit_hour_session: meeting.get(9).unwrap(), - hours_per_week: meeting.get(10).unwrap(), - in_session: Days::from_bits(meeting.get(11).unwrap()).unwrap(), - meeting_type: MeetingType::try_from(meeting_type).unwrap() - }); - } - - Ok(out) - } - - // Course number is like CSE-031. - pub async fn search_class_by_number(&self, course_number: &str, term: i32) -> Result, Box> { - self.general_class_search(course_number, term, - "SELECT id, course_reference_number, course_number, course_title \ - FROM UniScraper.UCM.class \ - WHERE term = @P1 AND CONTAINS(course_number, @P2);").await - } - - // Course name is like Computer Organization and Assembly. - pub async fn search_class_by_name(&self, course_name: &str, term: i32) -> Result, Box> { - self.general_class_search(course_name, term, - "SELECT id, course_reference_number, course_number, course_title FROM \ - (SELECT id, course_reference_number, course_number, course_title, term, ROW_NUMBER() \ - OVER (PARTITION BY course_title ORDER BY course_reference_number) AS RowNumber \ - FROM UniScraper.UCM.class WHERE term = @P1 AND CONTAINS(course_title, @P2)) AS mukyu \ - WHERE mukyu.RowNumber = 1;").await - } - - fn create_full_text_query(&self, search_query: &str) -> String { - search_query - .trim() - .split(' ') - .map(|o| o.replace('(', "").replace(')', "").replace('\"', "").replace('\'', "")) // *unqueries your query* - .map(|o| format!("\"*{}*\"", o)) // Wildcards - .reduce(|a, b| format!("{} AND {}", a, b)) - .unwrap() - } - - async fn general_class_search(&self, search_query: &str, term: i32, sql: &str) -> Result, Box> { - let mut conn = self.pool.get().await?; - - let input = self.create_full_text_query(search_query); - - let res = conn.query(sql, &[&term, &input]) - .await? - .into_first_result() - .await?; - - let mut out: Vec = Vec::new(); - - for class in res { - let course_number: &str = class.get(2).unwrap(); - let course_title: Option<&str> = class.get(3); - - let item = PartialClass { - id: class.get(0).unwrap(), - course_reference_number: class.get(1).unwrap(), - course_number: course_number.to_string(), - course_title: course_title.map(|o| o.to_string()) - }; - - if search_query == course_number || course_title.map(|o| o == search_query).unwrap_or(false) { - // Return early with one item - return Ok(vec![item]); - } else { - out.push(item); - } - } - - Ok(out) - } - - pub async fn search_professor(&self, search_query: &str) -> Result, Box> { - let mut conn = self.pool.get().await?; - - let input = self.create_full_text_query(search_query); - - let res = conn.query("SELECT id, rmp_id, last_name, first_name, middle_name, email, department, num_ratings, rating, full_name FROM [UniScraper].[UCM].[professor] WHERE CONTAINS(full_name, @P1);", &[&input]) - .await? - .into_first_result() - .await?; - - let mut out: Vec = Vec::new(); - - for professor in res { - let last_name: &str = professor.get(2).unwrap(); - let first_name: &str = professor.get(3).unwrap(); - let middle_name: Option<&str> = professor.get(4); - let email: Option<&str> = professor.get(5); - let department: Option<&str> = professor.get(6); - let full_name: &str = professor.get(9).unwrap(); - out.push(Professor { - id: professor.get(0).unwrap(), - rmp_id: professor.get(1), - last_name: last_name.to_string(), - first_name: first_name.to_string(), - middle_name: middle_name.map(|o| o.to_string()), - email: email.map(|o| o.to_string()), - department: department.map(|o| o.to_string()), - num_ratings: professor.get(7).unwrap(), - rating: professor.get(8).unwrap(), - full_name: full_name.to_string() - }); - } - - Ok(out) - } - - pub async fn get_classes_for_professor(&self, professor_id: i32, term: i32) -> Result, Box> { - let mut conn = self.pool.get().await?; - - let res = conn.query("SELECT class.id, class.course_reference_number, class.course_number, class.course_title FROM [UniScraper].[UCM].[professor] \ - INNER JOIN [UniScraper].[UCM].[faculty] ON professor.id = faculty.professor_id \ - INNER JOIN [UniScraper].[UCM].[class] ON class.id = faculty.class_id \ - WHERE class.term = @P1 AND professor.id = @P2", &[&term, &professor_id]) - .await? - .into_first_result() - .await?; - - let mut out: Vec = Vec::new(); - - for class in res { - let course_number: &str = class.get(2).unwrap(); - let course_title: Option<&str> = class.get(3); - - let item = PartialClass { - id: class.get(0).unwrap(), - course_reference_number: class.get(1).unwrap(), - course_number: course_number.to_string(), - course_title: course_title.map(|o| o.to_string()) - }; - - out.push(item); - } - - Ok(out) - } - - pub async fn get_stats(&self) -> Result, Box> { - let mut conn = self.pool.get().await?; - let res = conn.simple_query( - "SELECT table_name, last_update FROM [UniScraper].[UCM].[stats];") - .await? - .into_first_result() - .await?; - - let mut out = HashMap::new(); - - for meeting in res { - let table_name: &str = meeting.get(0).unwrap(); - let last_update: NaiveDateTime = meeting.get(1).unwrap(); - out.insert(table_name.to_string(), last_update); - } - - Ok(out) - } -} \ No newline at end of file From 72a15eaeb93c29b72b26744bab1a32db33369cec Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:02:13 -0700 Subject: [PATCH 11/30] Delete courses_db_models.rs --- src/commands/ucm/courses_db_models.rs | 147 -------------------------- 1 file changed, 147 deletions(-) delete mode 100644 src/commands/ucm/courses_db_models.rs diff --git a/src/commands/ucm/courses_db_models.rs b/src/commands/ucm/courses_db_models.rs deleted file mode 100644 index ea8f053..0000000 --- a/src/commands/ucm/courses_db_models.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::fmt::{Display, Formatter}; -use bitflags::bitflags; -use num_derive::FromPrimitive; -use num_traits::FromPrimitive; - -pub struct Reminder { - pub user_id: u64, - pub course_reference_number: i32, - pub min_trigger: i32, - pub for_waitlist: bool, - pub triggered: bool -} - -pub struct Trigger { - pub user_id: u64, - pub course_reference_number: i32, - pub min_trigger: i32 -} - -pub struct Class { - pub id: i32, - pub term: i32, - pub course_reference_number: i32, - pub course_number: String, - pub campus_description: Option, - pub course_title: Option, - pub credit_hours: u8, - pub maximum_enrollment: i16, - pub enrollment: i16, - pub seats_available: i16, - pub wait_capacity: i16, - pub wait_available: i16 -} - -pub struct PartialClass { - pub id: i32, - pub course_reference_number: i32, - pub course_number: String, - pub course_title: Option -} - -bitflags! { - pub struct Days: u8 { - const BASE = 0; - const SUNDAY = 1; - const MONDAY = 2; - const TUESDAY = 4; - const WEDNESDAY = 8; - const THURSDAY = 16; - const FRIDAY = 32; - const SATURDAY = 64; - } -} - -impl Display for Days { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let days_copy = *self; - if days_copy == Days::BASE { - return write!(f, ""); - } - - let mut days = String::new(); - - if days_copy.contains(Days::SUNDAY) { days.push_str("Sunday, "); } - if days_copy.contains(Days::MONDAY) { days.push_str("Monday, "); } - if days_copy.contains(Days::TUESDAY) { days.push_str("Tuesday, "); } - if days_copy.contains(Days::WEDNESDAY) { days.push_str("Wednesday, "); } - if days_copy.contains(Days::THURSDAY) { days.push_str("Thursday, "); } - if days_copy.contains(Days::FRIDAY) { days.push_str("Friday, "); } - if days_copy.contains(Days::SATURDAY) { days.push_str("Saturday, "); } - - write!(f, "{}", &days[0..days.len() - 2]) - } -} - -#[repr(u8)] -#[derive(FromPrimitive)] -pub enum MeetingType { - Lecture = 1, - Discussion = 2, - Lab = 3, - Fieldwork = 4, - Seminar = 5, - IndividualStudy = 6, - Tutorial = 7, - Studio = 8, - Practicum = 9, - Exam = 10, - Project = 11, - Internship = 12 -} - -impl Display for MeetingType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - MeetingType::Lecture => write!(f, "Lecture"), - MeetingType::Discussion => write!(f, "Discussion"), - MeetingType::Lab => write!(f, "Lab"), - MeetingType::Fieldwork => write!(f, "Fieldwork"), - MeetingType::Seminar => write!(f, "Seminar"), - MeetingType::IndividualStudy => write!(f, "Individual Study"), - MeetingType::Tutorial => write!(f, "Tutorial"), - MeetingType::Studio => write!(f, "Studio"), - MeetingType::Practicum => write!(f, "Practicum"), - MeetingType::Exam => write!(f, "Exam"), - MeetingType::Project => write!(f, "Project"), - MeetingType::Internship => write!(f, "Internship") - } - } -} - -impl TryFrom for MeetingType { - type Error = (); - fn try_from(v: u8) -> Result { - FromPrimitive::from_u8(v).ok_or(()) - } -} - -pub struct Meeting { - pub class_id: i32, - pub begin_time: Option, - pub end_time: Option, - pub begin_date: String, - pub end_date: String, - pub building: Option, - pub building_description: Option, - pub campus: Option, - pub campus_description: Option, - pub room: Option, - pub credit_hour_session: f32, - pub hours_per_week: f32, - pub in_session: Days, - pub meeting_type: MeetingType -} - -pub struct Professor { - pub id: i32, - pub rmp_id: Option, - pub last_name: String, - pub first_name: String, - pub middle_name: Option, - pub full_name: String, - pub email: Option, - pub department: Option, - pub num_ratings: i32, - pub rating: f32 -} \ No newline at end of file From 24ca87472b33ff5a43aaf439f63cf004de2764c9 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:02:24 -0700 Subject: [PATCH 12/30] Delete courses_old.rs --- src/commands/ucm/courses_old.rs | 105 -------------------------------- 1 file changed, 105 deletions(-) delete mode 100644 src/commands/ucm/courses_old.rs diff --git a/src/commands/ucm/courses_old.rs b/src/commands/ucm/courses_old.rs deleted file mode 100644 index 53d1454..0000000 --- a/src/commands/ucm/courses_old.rs +++ /dev/null @@ -1,105 +0,0 @@ -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - }, Args - } -}; -use chrono::Datelike; -use crate::commands::ucm::course_models::{CourseList}; - -#[command] -#[description = "Get the course list for a major"] -#[usage = " "] -pub async fn courses_old(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - let client = reqwest::Client::builder() - .cookie_store(true) - .build()?; - - let term = match args.single::() { - Ok(selected_sem) => { - let now = chrono::Utc::now(); - let sem_code = match selected_sem.to_lowercase().as_str() { - "fall" => "10", - "spring" => "20", - "summer" => "30", - _ => "00" - }; - - format!("{}{}", now.year(), sem_code) - }, - - Err(_) => { - msg.channel_id.say(&ctx.http, "Please use the semester names 'fall', 'spring', or 'summer'.").await?; - return Ok(()); - } - }; - - // setting the session cookies - let term_url = format!("https://reg-prod.ec.ucmerced.edu/StudentRegistrationSsb/ssb/term/search?\ - mode=courseSearch\ - &term={}\ - &studyPath=\ - &studyPathText=\ - &startDatepicker=\ - &endDatepicker=", term); - let search_url = "https://reg-prod.ec.ucmerced.edu/StudentRegistrationSsb/ssb/courseSearch/courseSearch"; - - client.get(term_url).send().await?; - client.get(search_url).send().await?; - - let major = args.single::() - .unwrap_or_else(|_| "".into()) - .to_uppercase(); - - let url = format!("https://reg-prod.ec.ucmerced.edu/StudentRegistrationSsb/ssb/courseSearchResults/courseSearchResults?\ - txt_subject={}\ - &txt_term={}\ - &startDatepicker=\ - &endDatepicker=\ - &pageOffset=0\ - &pageMaxSize=10\ - &sortColumn=subjectDescription\ - &sortDirection=asc", major, term); - - match client.get(url).send().await { - Ok(response) => { - // TODO: add pagination for courses - match response.json::().await { - Ok(course_list) => { - msg.channel_id.send_message(&ctx.http, |m| { - m.embed(|e| { - e - .title("Course List") - .description(format!("For major: {}", major)); - - for course in course_list.data { - let title = course.course_title.unwrap_or_else(|| "No Title".into()); - e.field(format!("{} {}-{}", major, course.course_number.unwrap_or_else(|| "000".into()), title), - course.course_description.unwrap_or_else(|| "No description".into())+"...", false); - } - - e - }) - }).await?; - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "The course search gave us weird data, try again later?").await?; - error!("Failed to process course search: {}", ex); - } - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "Failed to connect to the course search API, try again later?").await?; - error!("Failed to get course search: {}", ex); - } - } - - Ok(()) -} \ No newline at end of file From 3ecf5baf7935f9272d425db6b28c6c284c3d0708 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:02:33 -0700 Subject: [PATCH 13/30] Delete gym.rs --- src/commands/ucm/gym.rs | 123 ---------------------------------------- 1 file changed, 123 deletions(-) delete mode 100644 src/commands/ucm/gym.rs diff --git a/src/commands/ucm/gym.rs b/src/commands/ucm/gym.rs deleted file mode 100644 index aa80903..0000000 --- a/src/commands/ucm/gym.rs +++ /dev/null @@ -1,123 +0,0 @@ -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - } - } -}; -use scraper::{Html, Selector}; - -fn process_hours(data: &str) -> Vec<(String, String)> { - let mut output: Vec<(String, String)> = Vec::new(); - - let page = Html::parse_document(data); - let text = Selector::parse(".content h3, .content p").unwrap(); - - let mut temporary_name: Option = None; - let mut temporary_values: Vec = Vec::new(); - for text in page.select(&text) { - let text_data = text - .text() - .map(|o| o.trim()) - .filter(|o| !o.is_empty()) - .map(|o| o.to_string()) - .reduce(|a, b| format!("{}\n{}", a, b)) - .unwrap_or_default(); - - if text.value().name() == "h3" { - // New header, push values. - extractor(&mut output, &temporary_name, &mut temporary_values); - temporary_name = Some(text_data); - } else if temporary_name != None { - temporary_values.push(text_data); - } else { - // Can't read if there's no header. - break; - } - } - // Clear buffer if necessary. - extractor(&mut output, &temporary_name, &mut temporary_values); - - output -} - -fn extractor(output: &mut Vec<(String, String)>, temporary_name: &Option, temporary_values: &mut Vec) { - if let Some(temp_name) = temporary_name { - if !temporary_values.is_empty() { - output.push((temp_name.clone(), temporary_values - .iter() - .map(|o| o.to_string()) - .reduce(|a, b| format!("{}\n{}", a, b)) - .unwrap())); - - temporary_values.clear(); - } else { - output.push((temp_name.clone(), String::new())); - } - } - // The "Some" condition should always be true in this case. -} - -#[command] -#[description = "Get the hours for recreation and atheletic facilities."] -pub async fn gym(ctx: &Context, msg: &Message) -> CommandResult { - const TITLE: &str = "Recreation and Athletic Facility Hours"; - - let mut sent_msg = msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e - .title(TITLE) - .description("Now loading, please wait warmly...") - })).await?; - - const URL: &str = "https://recreation.ucmerced.edu/Facility-Hours"; - const EMPTY: &str = "\u{200b}"; - - match reqwest::get(URL).await { - Ok(response) => { - match response.text().await { - Ok(data) => { - let hours = process_hours(&*data); - - if !hours.is_empty() { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).fields(hours.iter().map(|o| { - let (name, value) = o; - - if value.is_empty() { - (name.as_str(), EMPTY, false) - } else { - (name.as_str(), value.as_str(), false) - } - })) - })).await?; - } else { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).description("Could not get any hours... Did the website change layout?") - })).await?; - error!("Unable to read athletics website"); - } - } - Err(ex) => { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).description("UC Merced gave us weird data, try again later?") - })).await?; - error!("Failed to process hours: {}", ex); - } - } - } - Err(ex) => { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).description("Failed to connect to the UC Merced website, try again later?") - })).await?; - error!("Failed to get athletics hours: {}", ex); - } - } - - Ok(()) -} \ No newline at end of file From 27a4d01901125edd02b3c9a6d3abd453084742ad Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:02:45 -0700 Subject: [PATCH 14/30] Delete foodtrucks.rs --- src/commands/ucm/foodtrucks.rs | 72 ---------------------------------- 1 file changed, 72 deletions(-) delete mode 100644 src/commands/ucm/foodtrucks.rs diff --git a/src/commands/ucm/foodtrucks.rs b/src/commands/ucm/foodtrucks.rs deleted file mode 100644 index b05d977..0000000 --- a/src/commands/ucm/foodtrucks.rs +++ /dev/null @@ -1,72 +0,0 @@ -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - } - } -}; -use scraper::{Html, Selector}; - -fn process_schedules(data: &str) -> Option { - let page = Html::parse_document(data); - let image = Selector::parse("p img").unwrap(); - - page.select(&image) - .next() - .map(|o| o.value().attr("src").unwrap().to_string()) -} - -#[command] -#[aliases(foodtruck)] -#[description = "Get the current food truck schedule."] -pub async fn foodtrucks(ctx: &Context, msg: &Message) -> CommandResult { - const TITLE: &str = "Food Truck Schedule"; - - let mut sent_msg = msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e - .title(TITLE) - .description("Now loading, please wait warmly...") - })).await?; - - const URL: &str = "https://dining.ucmerced.edu/food-trucks"; - match reqwest::get(URL).await { - Ok(response) => { - match response.text().await { - Ok(data) => { - let image_url = process_schedules(&*data); - - if let Some(schedule) = image_url { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).image(schedule) - })).await?; - } else { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).description("Could not get any valid schedules... Did the website change layout?") - })).await?; - error!("Unable to read food truck website"); - } - } - Err(ex) => { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).description("UC Merced gave us weird data, try again later?") - })).await?; - error!("Failed to process calendar: {}", ex); - } - } - } - Err(ex) => { - sent_msg.edit(&ctx.http, |m| m.embed(|e| { - e.title(TITLE).description("Failed to connect to the UC Merced website, try again later?") - })).await?; - error!("Failed to get food truck schedule: {}", ex); - } - } - - Ok(()) -} \ No newline at end of file From a6938495f68a2c032c55f8675f1bcf0940169e9f Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:02:57 -0700 Subject: [PATCH 15/30] Delete libcal_models.rs --- src/commands/ucm/libcal_models.rs | 61 ------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/commands/ucm/libcal_models.rs diff --git a/src/commands/ucm/libcal_models.rs b/src/commands/ucm/libcal_models.rs deleted file mode 100644 index 54332f6..0000000 --- a/src/commands/ucm/libcal_models.rs +++ /dev/null @@ -1,61 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct Hours { - pub from: String, - pub to: String -} - -#[derive(Debug, Deserialize)] -pub struct Times { - pub note: Option, - pub status: String, - pub hours: Option>, - pub currently_open: Option -} - -#[derive(Debug, Deserialize)] -pub struct Day { - pub date: String, - pub times: Times, - pub rendered: String -} - -#[derive(Debug, Deserialize)] -pub struct Week { - #[serde(rename = "Sunday")] - pub sunday: Day, - #[serde(rename = "Monday")] - pub monday: Day, - #[serde(rename = "Tuesday")] - pub tuesday: Day, - #[serde(rename = "Wednesday")] - pub wednesday: Day, - #[serde(rename = "Thursday")] - pub thursday: Day, - #[serde(rename = "Friday")] - pub friday: Day, - #[serde(rename = "Saturday")] - pub saturday: Day -} - -#[derive(Debug, Deserialize)] -pub struct Location { - pub lid: u16, - pub name: String, - pub category: String, - pub url: String, - pub contact: String, - pub lat: String, - pub long: String, - pub color: String, - #[serde(rename = "fn")] // I have no idea what the field is for. - pub f: Option, - pub parent_lid: Option, - pub weeks: Vec -} - -#[derive(Debug, Deserialize)] -pub struct Calendar { - pub locations: Vec -} \ No newline at end of file From d871c1565aefb965db1ec0e9ed1c320aaa148448 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:03:08 -0700 Subject: [PATCH 16/30] Delete library.rs --- src/commands/ucm/library.rs | 56 ------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 src/commands/ucm/library.rs diff --git a/src/commands/ucm/library.rs b/src/commands/ucm/library.rs deleted file mode 100644 index 4aa4fdd..0000000 --- a/src/commands/ucm/library.rs +++ /dev/null @@ -1,56 +0,0 @@ -use chrono::Datelike; -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - } - } -}; -use crate::commands::ucm::libcal_models::Calendar; - -#[command] -#[description = "Get the hours for the Kolligian Library."] -pub async fn library(ctx: &Context, msg: &Message) -> CommandResult { - let date = chrono::offset::Local::now(); - let url = format!("https://api3.libcal.com/api_hours_grid.php?iid=4052&lid=0&format=json&date={}-{:0>2}-{:0>2}", date.year(), date.month(), date.day()); - match reqwest::get(url).await { - Ok(response) => { - match response.json::().await { - Ok(data) => { - msg.channel_id.send_message(&ctx.http, |m| { - let library = &data.locations[0].weeks[0]; - let start_date = chrono::NaiveDate::parse_from_str(&*library.sunday.date, "%Y-%m-%d").unwrap(); - m.embed(|e| { - e - .title("Kolligian Library Hours") - .description(format!("For the week of {}", start_date.format("%B %d, %Y"))) - .field("Sunday", &library.sunday.rendered, false) - .field("Monday", &library.monday.rendered, false) - .field("Tuesday", &library.tuesday.rendered, false) - .field("Wednesday", &library.wednesday.rendered, false) - .field("Thursday", &library.thursday.rendered, false) - .field("Friday", &library.friday.rendered, false) - .field("Saturday", &library.saturday.rendered, false) - }) - }).await?; - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "The library gave us weird data, try again later?").await?; - error!("Failed to process calendar: {}", ex); - } - } - } - Err(ex) => { - msg.channel_id.say(&ctx.http, "Failed to connect to the library API, try again later?").await?; - error!("Failed to get calendar: {}", ex); - } - } - - Ok(()) -} \ No newline at end of file From 85b8f08cfb14c856dce993951d34a982309614d2 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:03:26 -0700 Subject: [PATCH 17/30] Delete store.rs --- src/commands/ucm/store.rs | 102 -------------------------------------- 1 file changed, 102 deletions(-) delete mode 100644 src/commands/ucm/store.rs diff --git a/src/commands/ucm/store.rs b/src/commands/ucm/store.rs deleted file mode 100644 index f719a2e..0000000 --- a/src/commands/ucm/store.rs +++ /dev/null @@ -1,102 +0,0 @@ -use reqwest::Client; -use serenity::{ - client::Context, - model::{channel::Message}, - framework::standard::{ - CommandResult, - macros::{ - command - } - } -}; -use serde::Deserialize; -use log::error; - -#[derive(Debug, Deserialize)] -#[serde(rename_all="camelCase")] -pub struct StoreConfig { - pub store_hours: StoreHours -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all="camelCase")] -pub struct StoreHours { - pub store_hours: Vec -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all="camelCase")] -pub struct StoreHoursWeek { - pub note: Option, - pub description: String, - pub sunday: String, - pub monday: String, - pub tuesday: String, - pub wednesday: String, - pub thursday: String, - pub friday: String, - pub saturday: String, -} - -async fn fetch_hours(client: &Client) -> Result> { - let response = client - .get("https://svc.bkstr.com/store/config?storeName=ucmercedstore") - .header("User-Agent", "Moogan/0.1.43") - .send() - .await? - .text() - .await?; - - let result: StoreConfig = serde_json::from_str(&*response)?; - - Ok(result) -} - -fn read_hours(config: &StoreHours) -> Vec<(String, String)> { - let mut output: Vec<(String, String)> = Vec::new(); - - for week in config.store_hours.iter() { - let week_str = if let Some(note) = week.note.as_ref() { - format!("Note: {}\n\n", note) - } else { - String::new() - }; - - let schedule = format!("{}Sunday: {}\nMonday: {}\nTuesday: {}\nWednesday: {}\nThursday: {}\nFriday: {}\nSaturday: {}", - week_str, week.sunday, week.monday, week.tuesday, week.wednesday, week.thursday, week.friday, week.saturday); - - output.push((week.description.clone(), schedule)); - } - - output -} - -#[command] -#[description = "Get the times of the UC Merced store."] -pub async fn store(ctx: &Context, msg: &Message) -> CommandResult { - const TITLE: &str = "UC Merced University Store Hours"; - let mut loading_message = msg.channel_id.send_message(&ctx.http, |m| - m.embed(|e| e.title(TITLE).description("Now loading, please wait warmly...")) - ).await?; - - let client = Client::new(); - match fetch_hours(&client).await { - Ok(hours) => { - let schedules = read_hours(&hours.store_hours); - loading_message.edit(&ctx.http, |m| - m.embed(|e| e.title(TITLE).fields(schedules.iter().map(|o| { - let (description, hours) = o; - (description, hours, false) - }))) - ).await?; - } - Err(ex) => { - error!("Failed to load UCM store hours: {}", ex); - loading_message.edit(&ctx.http, |m| - m.embed(|e| e.title(TITLE).description("Failed to load store hours. Try again later?")) - ).await?; - } - } - - Ok(()) -} \ No newline at end of file From d4b3960d18c2b9a1c987040fa38ec483c4e7d887 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:03:33 -0700 Subject: [PATCH 18/30] Delete professors.rs --- src/commands/ucm/professors.rs | 102 --------------------------------- 1 file changed, 102 deletions(-) delete mode 100644 src/commands/ucm/professors.rs diff --git a/src/commands/ucm/professors.rs b/src/commands/ucm/professors.rs deleted file mode 100644 index 55b2840..0000000 --- a/src/commands/ucm/professors.rs +++ /dev/null @@ -1,102 +0,0 @@ -use chrono::{Datelike, DateTime, Local, TimeZone, Utc}; -use log::error; -use serenity::{ - client::Context, - model::{ - channel::Message - }, - framework::standard::{ - CommandResult, - macros::{ - command - }, Args - } -}; -use crate::commands::ucm::courses_db_models::*; -use crate::{Database, db}; - -async fn professor_embed(ctx: &Context, msg: &Message, professor: &Professor) -> CommandResult { - let db = db!(ctx); - - let current_date = Local::now().date(); - let year = current_date.year(); - let semester = if current_date.month() >= 3 && current_date.month() <= 10 { 30 } else { 10 }; - let term = year * 100 + semester; - - let classes = db.get_classes_for_professor(professor.id, term).await; - let stats = db.get_stats().await; - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e.title(&professor.full_name); - e.description("Note: this uses Rate My Professor, which may be off at times~"); - e.field("Rating Score", professor.rating, true); - e.field("Number of Ratings", professor.num_ratings, true); - e.field("Email", professor.email.clone().unwrap(), true); - - - if let Ok(classes) = classes { - e.field(format!("Classes for {} (totalling {})", crate::commands::ucm::format_term(term), classes.len()), - classes.iter() - .map(|o| format!("- {} (`{}`): {}", &o.course_number, o.course_reference_number, o.course_title.clone().unwrap_or_else(|| "".to_string()))) - .reduce(|a, b| if a.len() < 1000 { format!("{}\n{}", a, b) } else {a}) - .unwrap_or_else(|| "This person is not teaching any classes for this term.".to_string()), - false); - } - - if let Ok(stats) = stats { - if let Some(class_update) = stats.get("professor") { - let local_time: DateTime = Local.from_local_datetime(class_update).unwrap(); - let utc_time: DateTime = DateTime::from(local_time); - e.footer(|f| f.text("Last updated at")); - e.timestamp(utc_time); - } - } - - e - })).await?; - - Ok(()) -} - -#[command] -#[description = "Search for a professor."] -#[aliases("professor")] -#[usage = ""] -pub async fn professors(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - let search_query = args.message(); - - let db = db!(ctx); - match db.search_professor(search_query).await { - Ok(professors) => { - print_matches(ctx, msg, &professors).await?; - } - Err(ex) => { - error!("Failed to search by name: {}", ex); - msg.channel_id.say(&ctx.http, "Failed to search for professors... try again later?").await?; - } - } - - Ok(()) -} - -async fn print_matches(ctx: &Context, msg: &Message, professors: &[Professor]) -> Result<(), Box> { - if professors.is_empty() { - msg.channel_id.say(&ctx.http, "No matches were found. Check your query for typos, or generalize it. Or, we may not have the person logged.").await?; - } else if professors.len() == 1 { - professor_embed(ctx, msg, professors.get(0).unwrap()).await?; - } else { - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| { - e.title("Professor Search").description("Multiple results were found for your query. Try refining your input."); - e.field(format!("Professors Matched (totalling {})", professors.len()), - professors - .iter() - .take(10) - .map(|o| format!("`{}` - {}", o.full_name, o.department.clone().unwrap_or_else(|| "".to_string()))) - .reduce(|a, b| format!("{}\n{}", a, b)) - .unwrap(), - false); - e - })).await?; - } - - Ok(()) -} \ No newline at end of file From ecbc15ed480a75b05fe2f8c369fa41ac423dceb8 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:07:14 -0700 Subject: [PATCH 19/30] Update mod.rs --- src/commands/ucm/mod.rs | 52 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/commands/ucm/mod.rs b/src/commands/ucm/mod.rs index d95c120..97f3857 100644 --- a/src/commands/ucm/mod.rs +++ b/src/commands/ucm/mod.rs @@ -1,37 +1,37 @@ -mod library; -mod libcal_models; -mod courses; -mod courses_old; -mod professors; -mod course_models; + + + + + + mod pavilion; mod pav_models; -pub mod reminders; -mod courses_db; -mod courses_db_models; -mod foodtrucks; -mod calendar; -mod gym; -mod store; + + + + + + + use serenity::framework::standard::macros::group; -use crate::commands::ucm::reminders::REMINDERS_GROUP; -use library::*; -use courses::*; -use courses_old::*; + + + + use pavilion::*; -use professors::*; -use foodtrucks::*; -use calendar::*; -use gym::*; -use store::*; + + + + + #[group] #[prefixes("ucm", "ucmerced")] -#[description = "Get information about UC Merced's services and facilities."] +#[description = "Get information about UC Merced's pavilion."] #[summary = "UC Merced info"] -#[commands(library, courses, courses_old, pavilion, professors, foodtrucks, calendar, gym, store)] -#[sub_groups(reminders)] -struct UCM; \ No newline at end of file +#[commands(pavilion)] + +struct UCM; From 5c61b36ceea74b7cc15652aacbdd1c6f27f3dd48 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:07:48 -0700 Subject: [PATCH 20/30] Delete db_models.rs --- src/models/db_models.rs | 54 ----------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 src/models/db_models.rs diff --git a/src/models/db_models.rs b/src/models/db_models.rs deleted file mode 100644 index 58475ca..0000000 --- a/src/models/db_models.rs +++ /dev/null @@ -1,54 +0,0 @@ -use serenity::model::id::{RoleId, UserId}; - -pub struct LevelUp { - pub level: i32, - pub old_rank: Option, - pub new_rank: Option -} - -impl LevelUp { - pub fn new() -> Self { - LevelUp { - level: 0, - old_rank: None, - new_rank: None - } - } -} - -pub struct Experience { - pub level: i32, - pub xp: i32 -} - -impl Experience { - pub fn new() -> Self { - Experience { - level: 0, - xp: 0 - } - } -} - -pub struct Member { - pub id: UserId, - pub exp: Experience, -} - -pub struct FullMember { - pub user: UserId, - pub exp: Experience, - pub role_id: Option -} - -pub struct Rank { - pub name: String, - pub role_id: Option, - pub min_level: i32 -} - -pub struct MemberPagination { - pub members: Vec, - pub current_page: i32, - pub last_page: i32 -} \ No newline at end of file From 0e2b381ed2b196272ef7516323cefe73cbdab221 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 21:07:59 -0700 Subject: [PATCH 21/30] Delete macros.rs --- src/models/macros.rs | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/models/macros.rs diff --git a/src/models/macros.rs b/src/models/macros.rs deleted file mode 100644 index 970a5a2..0000000 --- a/src/models/macros.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[macro_export] -macro_rules! db{ - ($ctx: expr) => { - { - let ctx_global = $ctx.data.read().await; - let out = ctx_global.get::().expect("Couldn't find database").clone(); - - out - } - } -} \ No newline at end of file From bcf0dd4c4cadaac2ec7f2da5376e69be89165ad1 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:37:12 -0700 Subject: [PATCH 22/30] Update config.rs --- src/models/config.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/models/config.rs b/src/models/config.rs index 221bdf8..fd251a9 100644 --- a/src/models/config.rs +++ b/src/models/config.rs @@ -3,11 +3,5 @@ use serde::Deserialize; #[derive(Debug, Deserialize)] pub struct Config { pub token: String, - pub sql_server_ip: String, - pub sql_server_port: u16, - pub sql_server_username: String, - pub sql_server_password: String, - pub cmd_prefix: String, - pub lavalink_ip: String, - pub lavalink_password: String, -} \ No newline at end of file + pub cmd_prefix: String +} From 84968494d5871b10263943292427945bd19648ce Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:37:30 -0700 Subject: [PATCH 23/30] Delete src/util directory --- src/util/duration.rs | 42 ------------------------------------------ src/util/mod.rs | 4 ---- 2 files changed, 46 deletions(-) delete mode 100644 src/util/duration.rs delete mode 100644 src/util/mod.rs diff --git a/src/util/duration.rs b/src/util/duration.rs deleted file mode 100644 index b0bbd29..0000000 --- a/src/util/duration.rs +++ /dev/null @@ -1,42 +0,0 @@ -pub fn to_ms>(s: S) -> Option { - let mut ms = 0; - let mut digits = 0; - for c in s.into().chars() { - if c.is_ascii_digit() { - digits *= 10; - digits += c.to_digit(10).unwrap(); - } else { - ms += match c { - 's' => digits * 1000, - 'm' => digits * 60 * 1000, - 'h' => digits * 60 * 60 * 1000, - 'd' => digits * 24 * 60 * 60 * 1000, - _ => { return None; } - }; - - digits = 0; - } - } - - Some(ms as i32) -} - -pub fn from_ms(ms: u64) -> String { - let mut s = ms / 1000; - let days = s / 3600 / 24; - s -= days * 3600 * 24; - let hours = s / 3600; - s -= hours * 3600; - let mins = s / 60; - s -= mins * 60; - - if days != 0 { - format!("{}d {}h {}m {}s", days, hours, mins, s) - } else if hours != 0 { - format!("{}h {}m {}s", hours, mins, s) - } else if mins != 0 { - format!("{}m {}s", mins, s) - } else { - format!("{}s", s) - } -} \ No newline at end of file diff --git a/src/util/mod.rs b/src/util/mod.rs deleted file mode 100644 index e254ebf..0000000 --- a/src/util/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod duration; - -pub use duration::to_ms; -pub use duration::from_ms; \ No newline at end of file From 561518520f7a40b746f73673faf97bba02804219 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:38:07 -0700 Subject: [PATCH 24/30] Delete bot_init.rs --- src/services/bot_init.rs | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/services/bot_init.rs diff --git a/src/services/bot_init.rs b/src/services/bot_init.rs deleted file mode 100644 index b9ce19b..0000000 --- a/src/services/bot_init.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serenity::{ - client::Context, - model::{ - gateway::Ready, - interactions::application_command::ApplicationCommand - } -}; - - -use log::{error, info}; - -async fn register_slash_commands(ctx: &Context, _: &Ready) { - if let Err(ex) = ApplicationCommand::create_global_application_command(&ctx.http, |cmd| { - cmd.name("info").description("Get information about this bot.") - }).await { - error!("Cannot create slash command: {}", ex) - } else { - info!("Finished creating slash commands.") - } -} - -pub async fn ready(ctx: &Context, ready: &Ready) { - info!("Logged in as {}", ready.user.name); - register_slash_commands(ctx, ready).await; -} \ No newline at end of file From caf91439e3c83de6c12b7119297c15980f83a238 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:38:14 -0700 Subject: [PATCH 25/30] Delete cow_framework.rs --- src/services/cow_framework.rs | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/services/cow_framework.rs diff --git a/src/services/cow_framework.rs b/src/services/cow_framework.rs deleted file mode 100644 index e9ba9c0..0000000 --- a/src/services/cow_framework.rs +++ /dev/null @@ -1,16 +0,0 @@ -use serenity::client::Context; -use serenity::framework::{Framework, StandardFramework}; -use serenity::model::channel::Message; -use async_trait::async_trait; - - -pub struct CowFramework { - internal_framework: StandardFramework -} - -#[async_trait] -impl Framework for CowFramework { - async fn dispatch(&self, ctx: Context, msg: Message) { - - } -} \ No newline at end of file From caa5f0edefdd3b7785f16a7acfedeef3ab4689c5 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:38:27 -0700 Subject: [PATCH 26/30] Delete database.rs --- src/services/database.rs | 379 --------------------------------------- 1 file changed, 379 deletions(-) delete mode 100644 src/services/database.rs diff --git a/src/services/database.rs b/src/services/database.rs deleted file mode 100644 index d05a8ea..0000000 --- a/src/services/database.rs +++ /dev/null @@ -1,379 +0,0 @@ -use bb8::Pool; -use bb8_tiberius::ConnectionManager; -use std::sync::Arc; -use serenity::{ - model::id::{ - UserId, - GuildId, - ChannelId, RoleId - }, - prelude::TypeMapKey -}; -use tiberius::{AuthMethod, Config}; -use rust_decimal::{ - Decimal, - prelude::FromPrimitive -}; -use rust_decimal::prelude::ToPrimitive; -use crate::models::db_models::*; - -pub struct Database { - pub(crate) pool: Pool -} - -impl TypeMapKey for Database { - type Value = Arc; -} - -impl Database { - pub async fn new(ip: &str, port: u16, usr: &str, pwd: &str) -> Result { - // The password is stored in a file; using secure strings is probably not going to make much of a difference. - let mut config = Config::new(); - - config.host(ip); - config.port(port); - config.authentication(AuthMethod::sql_server(usr, pwd)); - // Default schema needs to be Cow - config.database("Cow"); - config.trust_cert(); - - let manager = ConnectionManager::build(config)?; - let pool = Pool::builder().max_size(8).build(manager).await?; - - Ok(Database { pool }) - } - - pub async fn provide_exp(&self, server_id: GuildId, user_id: UserId) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let user = Decimal::from_u64(*user_id.as_u64()).unwrap(); - let res = conn.query( - "EXEC Ranking.ProvideExp @serverid = @P1, @userid = @P2", - &[&server, &user]) - .await? - .into_row() - .await?; - - let mut out = LevelUp::new(); - - if let Some(row) = res { - let mut old_rank_id: Option = None; - let mut new_rank_id: Option = None; - - if let Some(old_rank_id_row) = row.get(1) { - let old_rank_id_dec: rust_decimal::Decimal = old_rank_id_row; - old_rank_id = old_rank_id_dec.to_u64(); - } - if let Some(new_rank_id_row) = row.get(2) { - let new_rank_id_dec: rust_decimal::Decimal = new_rank_id_row; - new_rank_id = new_rank_id_dec.to_u64(); - } - - out = LevelUp { - level: row.get(0).unwrap(), - old_rank: old_rank_id, - new_rank: new_rank_id - }; - } - - Ok(out) - } - - pub async fn get_xp(&self, server_id: GuildId, user_id: UserId) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let user = Decimal::from_u64(*user_id.as_u64()).unwrap(); - let res = conn.query( - "SELECT xp, level FROM [Ranking].[Level] WHERE server_id = @P1 AND [user_id] = @P2", - &[&server, &user]) - .await? - .into_row() - .await?; - - let mut out = Experience::new(); - - if let Some(item) = res { - out = Experience { - xp: item.get(0).unwrap(), - level: item.get(1).unwrap() - }; - } - - Ok(out) - } - - pub async fn get_highest_role(&self, server_id: GuildId, level: i32) -> Result, Box> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let res = conn.query( - "SELECT TOP 1 role_id FROM [Ranking].[Role] WHERE server_id = @P1 AND min_level <= @P2 ORDER BY min_level DESC", - &[&server, &level]) - .await? - .into_row() - .await?; - - let mut out: Option = None; - - if let Some(item) = res { - let id: rust_decimal::Decimal = item.get(0).unwrap(); - out = id.to_u64().and_then(|u| Option::from(RoleId::from(u))); - } - - Ok(out) - } - - pub async fn calculate_level(&self, level: i32) -> Result> { - let mut conn = self.pool.get().await?; - let res = conn.query( - "EXEC [Ranking].[CalculateLevel] @level = @P1", - &[&level]) - .await? - .into_row() - .await?; - - let mut out: i32 = 0; - - if let Some(item) = res { - out = item.get(0).unwrap(); - } - - Ok(out) - } - - // True: disabled False: enabled - // Because by default a channel should be enabled, right? - pub async fn toggle_channel_xp(&self, server_id: GuildId, channel_id: ChannelId) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let channel = Decimal::from_u64(*channel_id.as_u64()).unwrap(); - let res = conn.query( - "EXEC [Ranking].[ToggleChannel] @serverid = @P1, @channelid = @P2", - &[&server, &channel]) - .await? - .into_row() - .await?; - - let mut out: bool = false; - - if let Some(item) = res { - out = item.get(0).unwrap(); - } - - Ok(out) - } - - pub async fn channel_disabled(&self, server_id: GuildId, channel_id: ChannelId) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let channel = Decimal::from_u64(*channel_id.as_u64()).unwrap(); - let res = conn.query( - "SELECT CAST(1 AS BIT) FROM [Ranking].[DisabledChannel] WHERE server_id = @P1 AND channel_id = @P2", - &[&server, &channel]) - .await? - .into_row() - .await?; - - let mut out: bool = false; - - if let Some(item) = res { - out = item.get(0).unwrap(); - } - - Ok(out) - } - - // Page number is zero-indexed. - pub async fn top_members(&self, server_id: GuildId, page: i32) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - const ROWS_FETCHED: i32 = 10; - let mut offset = page * ROWS_FETCHED; - offset = offset.max(0); - let res = conn.query( - "SELECT user_id, level, xp FROM [Ranking].[Level] WHERE server_id = @P1 ORDER BY level DESC, xp DESC OFFSET @P2 ROWS FETCH NEXT @P3 ROWS ONLY; SELECT COUNT(1) FROM [Ranking].[Level] WHERE server_id = @P1", - &[&server, &offset, &ROWS_FETCHED]) - .await? - .into_results() - .await?; - - let count: i32 = res.get(1).unwrap().get(0).unwrap().get(0).unwrap(); - - let members = res.get(0).unwrap().iter() - .map(|row| { - let id: rust_decimal::Decimal = row.get(0).unwrap(); - Member { - id: UserId::from(id.to_u64().unwrap()), - exp: Experience { - level: row.get(1).unwrap(), - xp: row.get(2).unwrap() - } - } - }) - .collect::>(); - - let pages = (count / ROWS_FETCHED) + ((count % ROWS_FETCHED != 0) as i32); // Divide, then round if not perfect division - - Ok(MemberPagination { - members, - current_page: page, - last_page: pages - }) - } - - pub async fn rank_within_members(&self, server_id: GuildId, user_id: UserId) -> Result, Box> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let user = Decimal::from_u64(*user_id.as_u64()).unwrap(); - let res = conn.query( - "SELECT row_number FROM (SELECT user_id, ROW_NUMBER() OVER (ORDER BY level DESC, xp DESC) AS row_number FROM [Ranking].[Level] WHERE server_id = @P1) mukyu WHERE user_id = @P2", - &[&server, &user]) - .await? - .into_row() - .await?; - - let mut out: Option = None; - - if let Some(item) = res { - // Apparently it's an i64. Cool. - out = item.get(0); - } - - Ok(out) - } - - pub async fn get_roles(&self, server_id: GuildId) -> Result, Box> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let res = conn.query( - "SELECT role_name, role_id, min_level FROM [Ranking].[Role] WHERE server_id = @P1 ORDER BY min_level ASC", - &[&server]) - .await? - .into_first_result() - .await? - .into_iter() - .map(|row| { - let name: &str = row.get(0).unwrap(); - let mut id: Option = None; - if let Some(row) = row.get(1) { - let id_dec: rust_decimal::Decimal = row; - id = id_dec.to_u64().and_then(|u| Option::from(RoleId::from(u))); - } - - Rank { - name: name.to_string(), - role_id: id, - min_level: row.get(2).unwrap() - } - }) - .collect::>(); - - Ok(res) - } - - // will also set role - pub async fn add_role(&self, server_id: GuildId, role_name: &str, role_id: RoleId, min_level: i32) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let role = Decimal::from_u64(*role_id.as_u64()).unwrap(); - let res = conn.query( - "EXEC [Ranking].[AddRole] @server_id = @P1, @role_name = @P2, @role_id = @P3, @min_level = @P4", - &[&server, &role_name, &role, &Decimal::from_i32(min_level).unwrap()]) - .await? - .into_row() - .await?; - - let mut out: bool = false; - - if let Some(item) = res { - out = item.get(0).unwrap(); - } - - Ok(out) - } - - pub async fn remove_role(&self, server_id: GuildId, role_id: RoleId) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let role = Decimal::from_u64(*role_id.as_u64()).unwrap(); - let res = conn.query( - "EXEC [Ranking].[RemoveRole] @serverid = @P1, @roleid = @P2", - &[&server, &role]) - .await? - .into_row() - .await?; - - let mut out: bool = false; - - if let Some(item) = res { - out = item.get(0).unwrap(); - } - - Ok(out) - } - - pub async fn set_timeout(&self, server_id: GuildId, timeout: i32) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let timeout = Decimal::from_i32(timeout).unwrap(); - let res = conn.query( - "EXEC [Ranking].[SetServerTimeout] @serverid = @P1, @timeout = @P2", - &[&server, &timeout]) - .await? - .into_row() - .await?; - - let mut out: bool = false; - - if let Some(item) = res { - out = item.get(0).unwrap(); - } - - Ok(out) - } - - pub async fn get_timeout(&self, server_id: GuildId) -> Result> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let res = conn.query( - "SELECT TOP 1 timeout FROM [Ranking].[Server] WHERE id=@P1", - &[&server]) - .await? - .into_row() - .await?; - - let mut out: i32 = -1; - - if let Some(item) = res { - out = item.get(0).unwrap(); - } - - Ok(out) - } - - pub async fn get_users(&self, server_id: GuildId) -> Result, Box> { - let mut conn = self.pool.get().await?; - let server = Decimal::from_u64(*server_id.as_u64()).unwrap(); - let res = conn.query( - "EXEC [Ranking].[GetAllUsers] @serverid = @P1", - &[&server]) - .await? - .into_first_result() - .await? - .into_iter() - .map(|row| { - let id: UserId = row.get(0).and_then(|u: rust_decimal::Decimal| u.to_u64()).map(UserId::from).unwrap(); - let role_id: Option = row.get(3).and_then(|u: rust_decimal::Decimal| u.to_u64()).map(RoleId::from); - FullMember { - user: id, - exp: Experience { - level: row.get(1).unwrap(), - xp: row.get(2).unwrap() - }, - role_id - } - }) - .collect::>(); - - Ok(res) - } -} \ No newline at end of file From b05f604f07191ab59f1f6c3c1df21014e896b4a8 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:38:38 -0700 Subject: [PATCH 27/30] Delete interaction_handler.rs --- src/services/interaction_handler.rs | 89 ----------------------------- 1 file changed, 89 deletions(-) delete mode 100644 src/services/interaction_handler.rs diff --git a/src/services/interaction_handler.rs b/src/services/interaction_handler.rs deleted file mode 100644 index 3947ed3..0000000 --- a/src/services/interaction_handler.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::sync::Arc; -use serenity::{ - client::Context, - model::interactions::{ - Interaction, - InteractionResponseType - }, - framework::Framework, - utils::CustomMessage -}; -use log::error; -use async_trait::async_trait; -use chrono::{Utc}; -use serenity::builder::CreateMessage; -use serenity::http::Http; -use serenity::model::channel::Message; -use serenity::model::interactions::application_command::ApplicationCommandInteractionDataOptionValue; - -// Use these methods to automatically forward messages, depending on how they were invoked. -#[async_trait] -pub trait AutoResponse { - async fn send_message<'a, F>(self, http: impl AsRef, f: F) -> Result> - where for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>; - async fn say(self, http: impl AsRef, content: impl std::fmt::Display) -> Result>; -} - -/* -#[async_trait] -impl AutoResponse for Message { - async fn send_message<'a, F>(self, http: impl AsRef, f: F) -> Result where for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a> { - self.channel_id.send_message(http, f).await - } - - async fn say(self, http: impl AsRef, content: impl Display) -> Result { - self.send_message(&http, |m| m.content(content)).await - } -}*/ - -pub async fn interaction(ctx: &Context, interaction: &Interaction, framework: &Arc>) { - if let Interaction::ApplicationCommand(command) = interaction { - let app_id = command.application_id.as_u64(); - let cmd_name = command.data.name.as_str(); - // Ping the bot and append the command name, so we can trick it into thinking of a text command. - let mut content = format!("<@!{}> {}", app_id, cmd_name); - let arguments = command.data.options.iter() - .filter(|o| o.value.is_some() && o.resolved.is_some()) - .map(|o| { - match o.resolved.clone().unwrap() { - ApplicationCommandInteractionDataOptionValue::String(s) => {s}, - ApplicationCommandInteractionDataOptionValue::Integer(i) => {i.to_string()}, - ApplicationCommandInteractionDataOptionValue::Boolean(b) => {b.to_string()}, - ApplicationCommandInteractionDataOptionValue::User(u, _) => {format!("<@{}>", u.id.0)}, - ApplicationCommandInteractionDataOptionValue::Channel(c) => {format!("<#{}>", c.id.0)}, - ApplicationCommandInteractionDataOptionValue::Role(r) => {format!("<@&{}", r.id.0)}, - ApplicationCommandInteractionDataOptionValue::Number(n) => {n.to_string()}, - _ => String::new() - } - }) - .reduce(|a, b| format!("{} {}", a, b)); - - if let Some(args) = arguments { - content += ""; - content += &*args; - } - - let mut dummy_message = CustomMessage::new(); - - dummy_message.channel_id(command.channel_id) - .content(content) - .author(command.user.clone()) - .timestamp(Utc::now()); - - if let Some(guild_id) = command.guild_id { - dummy_message.guild_id(guild_id); - } - - (*framework).dispatch(ctx.clone(), dummy_message.build()).await; - - if let Err(ex) = command - .create_interaction_response(&ctx.http, |response| { - response - .kind(InteractionResponseType::UpdateMessage) - }) - .await - { - error!("Failed to respond to slash command: {}", ex); - } - } -} \ No newline at end of file From 824c382111cf4754a471cb2801cc0980fd2ee6ac Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:39:02 -0700 Subject: [PATCH 28/30] Delete src/services directory --- src/services/message_handler.rs | 99 --------------------------------- src/services/mod.rs | 5 -- 2 files changed, 104 deletions(-) delete mode 100644 src/services/message_handler.rs delete mode 100644 src/services/mod.rs diff --git a/src/services/message_handler.rs b/src/services/message_handler.rs deleted file mode 100644 index 37e6b8a..0000000 --- a/src/services/message_handler.rs +++ /dev/null @@ -1,99 +0,0 @@ -use serenity::{ - client::Context, - model::{channel::Message, id::{RoleId, GuildId}, guild::Member} -}; -use log::error; -use crate::{Database, db}; - -pub async fn message(_: &Context, _msg: &Message) { - // This is basically useless for most cases. -} - -pub async fn non_command(ctx: &Context, msg: &Message) { - if msg.author.bot { - return; - } - - let db = db!(ctx); - - if let Some(server_id) = msg.guild_id { - match db.channel_disabled(server_id, msg.channel_id).await { - Err(ex) => { - error!("Failed checking if the current channel was disabled: {}", ex); - }, - Ok(result) => { - if result { - return; - } - } - } - - match db.provide_exp(server_id, msg.author.id).await { - Err(ex) => { - error!("Failed providing exp to user: {}", ex) - }, - Ok(data) => { - if data.level < 0 { - return; - } - - let mut content = format!("<@{}> leveled up from {} to {}.", msg.author.id.as_u64(), data.level - 1, data.level); - if let Some(new_rank_id) = data.new_rank { - content += &*format!("\nYou are now a <@&{}>.", new_rank_id); - - let mut error = false; - let guild = msg.guild(&ctx).await.unwrap(); - let mut member = guild.member(&ctx.http, msg.author.id).await.unwrap(); - - if let Some(old_rank_id) = data.old_rank { - let old_rank = RoleId::from(old_rank_id); - if member.roles.contains(&old_rank) { - // We know we're in a guild, so an error is probably an API issue. - if let Err(ex) = member.remove_role(&ctx.http, old_rank).await { - error = true; - content += "\n(We failed to update your roles; maybe we don't have permission?)"; - error!("Failed to remove role from user: {}", ex); - } - } - } - - if let Err(ex) = member.add_role(&ctx.http, RoleId::from(new_rank_id)).await { - if !error { - content += "\n(We failed to update your roles; maybe we don't have permission?)"; - } - error!("Failed to add role to user: {}", ex); - } - } - - if let Err(ex2) = - msg.channel_id.send_message(&ctx.http, |m| m.embed(|e| e - .title("Level Up!") - .description(content) - )).await { - error!("Error sending level-up message: {}", ex2) - }; - } - } - } -} - -pub async fn on_join(ctx: &Context, guild_id: &GuildId, new_member: &Member) { - if new_member.user.bot { - return; - } - - let db = db!(ctx); - let mut member = new_member.clone(); - - let experience = db.get_xp(*guild_id, member.user.id).await.unwrap(); - let current_role = db.get_highest_role(*guild_id, experience.level).await.unwrap(); - if let Some(current_role_id) = current_role { - if let Err(ex) = member.add_role(&ctx.http, current_role_id).await { - error!("Failed to add role for server {}: {}", guild_id, ex); - if let Err(ex2) = member.user.direct_message(&ctx.http, |m| - m.content("I tried to re-add your roles, but the server didn't let me. Sorry~")).await { - error!("Failed to send error message to user {}: {}", member.user.id, ex2); - } - } - } -} \ No newline at end of file diff --git a/src/services/mod.rs b/src/services/mod.rs deleted file mode 100644 index 6e759cf..0000000 --- a/src/services/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod interaction_handler; -pub mod message_handler; -pub mod bot_init; -pub mod database; -pub mod cow_framework; \ No newline at end of file From 6f2dd65fd5ca9bd6fcff6380acc80f66d8e03eb7 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:41:48 -0700 Subject: [PATCH 29/30] Update main.rs --- src/main.rs | 88 +---------------------------------------------------- 1 file changed, 1 insertion(+), 87 deletions(-) diff --git a/src/main.rs b/src/main.rs index f5d8194..b1b692d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,12 +6,10 @@ mod util; use std::collections::{HashSet}; use commands::{get_framework}; use models::config::Config; -use services::{*, database::Database}; use std::fs; use std::sync::Arc; use std::env; use env_logger::Env; -use lavalink_rs::{LavalinkClient, gateway::LavalinkEventHandler}; use serenity::{ async_trait, client::{Client, Context, EventHandler, bridge::gateway::GatewayIntents}, @@ -21,54 +19,6 @@ use serenity::{ prelude::TypeMapKey }; use log::{error, info}; -use songbird::SerenityInit; - -struct Handler { - framework: Arc>, - database: Arc -} - -struct Lavalink; - -impl TypeMapKey for Lavalink { - type Value = LavalinkClient; -} - -struct LavalinkHandler; - -#[async_trait] -impl LavalinkEventHandler for LavalinkHandler { } - -#[async_trait] -impl EventHandler for Handler { - async fn guild_member_addition(&self, ctx: Context, guild_id: GuildId, new_member: Member) { - message_handler::on_join(&ctx, &guild_id, &new_member).await; - } - - async fn message(&self, ctx: Context, msg: Message) { - message_handler::message(&ctx, &msg).await; - } - - async fn reaction_add(&self, ctx: Context, added_reaction: Reaction) { - crate::commands::cowboard::cowboard_handler::add_reaction(&ctx, &added_reaction).await; - } - - async fn reaction_remove(&self, ctx: Context, removed_reaction: Reaction) { - crate::commands::cowboard::cowboard_handler::remove_reaction(&ctx, &removed_reaction).await; - } - - async fn reaction_remove_all(&self, ctx: Context, channel_id: ChannelId, removed_from_message_id: MessageId) { - crate::commands::cowboard::cowboard_handler::reaction_remove_all(&ctx, channel_id, removed_from_message_id).await; - } - - async fn ready(&self, ctx: Context, ready: Ready) { - bot_init::ready(&ctx, &ready).await; - } - - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - interaction_handler::interaction(&ctx, &interaction, &self.framework).await; - } -} async fn init_logger() -> std::io::Result<()> { let env = Env::default().default_filter_or("warning"); @@ -117,12 +67,6 @@ async fn main() -> Result<(), Box> { let token = config.token; let (app_id, owners) = fetch_bot_info(&token).await; let framework = get_framework(&config.cmd_prefix, app_id, owners).await; - - let event_handler = Handler { - framework: framework.clone(), - database: Arc::new(Database::new(&*config.sql_server_ip, config.sql_server_port, &*config.sql_server_username, &*config.sql_server_password).await.unwrap()) - }; - let db_clone = event_handler.database.clone(); let mut client = Client::builder(&token) @@ -130,42 +74,12 @@ async fn main() -> Result<(), Box> { .application_id(*app_id.as_u64()) .framework_arc(framework) .intents(GatewayIntents::all()) - .register_songbird() .await .expect("Discord failed to initialize"); - let lavalink_enabled = !config.lavalink_ip.is_empty() && !config.lavalink_password.is_empty(); - - if lavalink_enabled { - match LavalinkClient::builder(*app_id.as_u64()) - .set_host(config.lavalink_ip) - .set_password( - config.lavalink_password, - ) - .build(LavalinkHandler) - .await { - Ok(lava_client) => { - let mut data = client.data.write().await; - data.insert::(lava_client); - } - Err(ex) => { - error!("Failed to initialize LavaLink. {}", ex); - } - } - } - - { - let mut data = client.data.write().await; - // Should I wrap it with an RwLock? ...it's pooled and async is nice, but... - data.insert::(db_clone); - } - - // Start our reminder task and forget about it. - let _ = tokio::task::spawn(crate::commands::ucm::reminders::check_reminders(client.data.clone(), client.cache_and_http.clone())); - if let Err(ex) = client.start().await { error!("Discord bot client error: {:?}", ex); } Ok(()) -} \ No newline at end of file +} From 0746379688140575b84e3a7e4189b2ca4076ac35 Mon Sep 17 00:00:00 2001 From: William Le Date: Tue, 30 Aug 2022 22:44:42 -0700 Subject: [PATCH 30/30] Update mod.rs --- src/commands/mod.rs | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0b3873f..4b089d1 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,9 +1,4 @@ -mod general; -mod rank_config; -mod timeout; pub mod ucm; -pub mod cowboard; -mod music; use std::{collections::HashSet}; use std::sync::Arc; @@ -28,12 +23,7 @@ use serenity:: { client::Context }; -use crate::commands::general::GENERAL_GROUP; -use crate::commands::rank_config::RANKCONFIG_GROUP; -use crate::commands::timeout::TIMEOUT_GROUP; use crate::commands::ucm::UCM_GROUP; -use crate::commands::cowboard::COWBOARD_GROUP; -use crate::commands::music::MUSIC_GROUP; #[help] #[individual_command_tip = "Cow help command\n\n\ @@ -60,18 +50,6 @@ async fn non_command(ctx: &Context, msg: &Message) { crate::message_handler::non_command(ctx, msg).await; } -#[hook] -async fn on_error(ctx: &Context, msg: &Message, error: DispatchError) { - if let DispatchError::Ratelimited(info) = error { - if info.is_first_try { - // Why round up when we can add one? - if let Err(ex) = msg.channel_id.say(&ctx.http, &format!("This command is rate-limited, please try this again in {} seconds.", info.as_secs() + 1)).await { - error!("Failed to send rate-limit message: {}", ex); - } - } - } -} - pub async fn get_framework(pref: &str, app_id: UserId, owners: HashSet) -> Arc> { Arc::new(Box::new(StandardFramework::new() .configure(|c| c @@ -79,17 +57,7 @@ pub async fn get_framework(pref: &str, app_id: UserId, owners: HashSet) .on_mention(Some(app_id)) .owners(owners) ) - .normal_message(non_command) - .on_dispatch_error(on_error) - .bucket("diagnostics", |b| b.limit(2).time_span(15 * 60) // 15 minute delay for scan and fix. - .limit_for(LimitedFor::Guild) - .await_ratelimits(0)).await // Don't delay, force them to re-execute since we don't want to hang the bot .help(&COW_HELP) - .group(&GENERAL_GROUP) - .group(&RANKCONFIG_GROUP) - .group(&TIMEOUT_GROUP) .group(&UCM_GROUP) - .group(&COWBOARD_GROUP) - .group(&MUSIC_GROUP) )) -} \ No newline at end of file +}