From a8120c9466db297839d52e71b4ae483bb6b49468 Mon Sep 17 00:00:00 2001 From: Xyon Date: Tue, 26 Sep 2023 22:58:25 +0100 Subject: [PATCH 1/3] Don't promote users if their rank is frozen --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/badgey/events.rs | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a38ce8..101a98e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ dependencies = [ [[package]] name = "badgey" -version = "3.0.1" +version = "3.0.2" dependencies = [ "built", "clap", diff --git a/Cargo.toml b/Cargo.toml index e8f6707..0cf535a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "badgey" -version = "3.0.2" +version = "3.0.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/badgey/events.rs b/src/badgey/events.rs index 04671b9..a339003 100644 --- a/src/badgey/events.rs +++ b/src/badgey/events.rs @@ -63,6 +63,13 @@ impl BadgeyHandler { xp.last_given_xp = Some(chrono::Utc::now().timestamp()); xp.xp_value += &xp_reward; let calculated_level = xp.get_level_from_xp(); + + if xp.freeze_rank.is_some() { + xp.user_current_level = calculated_level.clone(); + xp.insert(db)?; + return Ok(()) + } + if (xp.user_current_level != calculated_level) || xp.user_current_level == 1 { if let Some(guild) = msg.guild(&ctx.cache) { let mut member = guild.member(&ctx, msg.author.id).await?; -- 2.30.2 From 6458598026db17bb312a3e60e30e31e8521a9027 Mon Sep 17 00:00:00 2001 From: Xyon Date: Wed, 27 Sep 2023 10:37:53 +0100 Subject: [PATCH 2/3] Add custom response handlers --- .gitignore | 1 + .idea/sqldialects.xml | 1 + Cargo.lock | 3 +- Cargo.toml | 3 +- src/badgey/commands/custom_responses.rs | 74 +++++++++++++++ src/badgey/commands/mod.rs | 2 + src/badgey/commands/ranks.rs | 8 +- src/badgey/events.rs | 7 +- src/badgey/models/custom_response.rs | 114 ++++++++++++++++++++++++ src/badgey/models/mod.rs | 1 + src/badgey/schema.rs | 57 ++++++++---- 11 files changed, 243 insertions(+), 28 deletions(-) create mode 100644 src/badgey/commands/custom_responses.rs create mode 100644 src/badgey/models/custom_response.rs diff --git a/.gitignore b/.gitignore index 2c4918c..e09f1ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target *.db +.env diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 5a52f7b..c2adfc3 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 101a98e..a2fdb01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ dependencies = [ [[package]] name = "badgey" -version = "3.0.2" +version = "3.0.3" dependencies = [ "built", "clap", @@ -170,6 +170,7 @@ dependencies = [ "manifold", "poise", "rand 0.8.5", + "regex 1.9.5", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 0cf535a..af1641d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "badgey" -version = "3.0.3" +version = "3.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -19,4 +19,5 @@ manifold = { git = "https://code.orbiter-radio.uk/discord/manifold.git" } # manifold = { path = "/home/xyon/Workspace/manifold/" } poise = { version = "0.5.*", features = [ "cache" ] } rand = { version = "0.8.5", features = [ "small_rng" ] } +regex = "1.9.5" tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] } diff --git a/src/badgey/commands/custom_responses.rs b/src/badgey/commands/custom_responses.rs new file mode 100644 index 0000000..6edbe97 --- /dev/null +++ b/src/badgey/commands/custom_responses.rs @@ -0,0 +1,74 @@ +use clap::error::ContextKind::Custom; +use poise::serenity_prelude as serenity; + +use manifold::error::{ManifoldError, ManifoldResult}; +use manifold::{ManifoldContext, ManifoldData}; +use poise::serenity_prelude::Mentionable; +use crate::badgey::models::custom_response::{CustomResponse, CustomResponseInserter}; + + +#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS" )] +async fn add_custom_response(ctx: ManifoldContext<'_>, target: serenity::User, trigger: String, response: String) -> ManifoldResult<()> { + let db = &ctx.data().database; + + match CustomResponseInserter::new(trigger, response, target, ctx.author().clone()).insert(db) { + Ok(_) => ctx.reply("Custom response successfully added").await?, + Err(e) => ctx.reply(format!("Custom response NOT added, an error happened. You should probably complain to Xyon about that. The error was {}", e)).await?, + }; + + Ok(()) +} + +#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")] +async fn remove_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldResult<()> { + let db = &ctx.data().database; + + let mut response = CustomResponse::find_by_id(db, &id)?; + + response.deleted = Some(true); + response.insert(db)?; + + ctx.reply(format!("Successfully marked custom response with ID {} as deleted.", id)).await?; + + Ok(()) +} + +#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")] +async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldResult<()> { + let db = &ctx.data().database; + + let mut response = CustomResponse::find_by_id(db, &id)?; + + if let Some(d) = response.deleted { + response.deleted = None; + response.insert(db)?; + ctx.reply(format!("Undeleted custom response with ID: {}", &id)).await?; + } else { + ctx.reply(format!("Can't undelete custom response with ID {} because it was not deleted in the first place.", &id)).await?; + } + + Ok(()) +} + +#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")] +async fn list_custom_responses_for_user(ctx: ManifoldContext<'_>, target: serenity::User) -> ManifoldResult<()> { + let db = &ctx.data().database; + + let mut answer: String = "".to_string(); + + CustomResponse::find_by_user(db, &target)?.iter().for_each(|f| { + answer.push_str(format!("{}{}", "\n", f.to_string()).as_str()); + }); + + if answer.len() == 0 { + answer.push_str("\nNone found"); + } + + ctx.reply(format!("I found the following custom responses for {}: {}", &target.mention(), answer)).await?; + + Ok(()) +} + +pub fn commands() -> [poise::Command; 4] { + [add_custom_response(), remove_custom_response(), undelete_custom_response(), list_custom_responses_for_user()] +} diff --git a/src/badgey/commands/mod.rs b/src/badgey/commands/mod.rs index c915187..a5c50a2 100644 --- a/src/badgey/commands/mod.rs +++ b/src/badgey/commands/mod.rs @@ -2,10 +2,12 @@ use manifold::{ManifoldData}; use manifold::error::{ManifoldError}; use poise::Command; +pub mod custom_responses; pub mod ranks; pub fn collect_commands() -> Vec> { commands().into_iter() + .chain(custom_responses::commands()) .chain(ranks::commands()) .collect() } diff --git a/src/badgey/commands/ranks.rs b/src/badgey/commands/ranks.rs index 4062b67..27e7ded 100644 --- a/src/badgey/commands/ranks.rs +++ b/src/badgey/commands/ranks.rs @@ -4,7 +4,7 @@ use manifold::{ManifoldContext, ManifoldData}; use poise::serenity_prelude::{Mentionable, RoleId}; use crate::badgey::models::xp::{Rank, Track, Xp}; -#[poise::command(prefix_command, slash_command)] +#[poise::command(prefix_command, slash_command, user_cooldown = 86400)] async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to switch to: officer or enlisted"] track: String) -> ManifoldResult<()> { let db = &ctx.data().database; let user_id_i64 = ctx.author().id.as_u64().clone() as i64; @@ -14,8 +14,6 @@ async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to s Err(_) => Xp::new(&user_id_i64) }; - Xp::check_timeout(&xp.rank_track_last_changed, &86400)?; - let parsed_track = Track::get_track_by_name(db, &track)?; if xp.rank_track != parsed_track.track_id { let current_level_rank = Rank::get_rank_for_level(db, &xp.user_current_level, &xp.rank_track)?; @@ -38,7 +36,7 @@ async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to s Ok(()) } -#[poise::command(prefix_command, slash_command)] +#[poise::command(prefix_command, slash_command, user_cooldown = 86400)] async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to lock yourself at. Omit to unfreeze your progression."] rank: Option) -> ManifoldResult<()> { let db = &ctx.data().database; let user_id_i64 = ctx.author().id.as_u64().clone() as i64; @@ -48,8 +46,6 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to Err(_) => Xp::new(&user_id_i64) }; - Xp::check_timeout(&xp.freeze_rank_last_changed, &86400)?; - if let Some(r) = rank { let frozen_rank = match Rank::get_rank_by_name(db, &r, &xp.rank_track) { Ok(r) => r, diff --git a/src/badgey/events.rs b/src/badgey/events.rs index a339003..ca9a02c 100644 --- a/src/badgey/events.rs +++ b/src/badgey/events.rs @@ -6,6 +6,7 @@ use poise::{async_trait, FrameworkContext, Event}; use poise::serenity_prelude::{Context, Mentionable, Message, RoleId}; use rand::rngs::SmallRng; use rand::{Rng, SeedableRng}; +use crate::badgey::models::custom_response::CustomResponse; use crate::badgey::models::xp::{Rank, Xp}; @@ -31,13 +32,17 @@ impl BadgeyHandler { if msg.author.id == ctx.http.get_current_user().await?.id || msg.content.starts_with(&fctx.user_data().await.bot_config.prefix) { return Ok(()) } + let db = &fctx.user_data().await.database; + + if let Ok(r) = CustomResponse::test_content(db, &msg.content_safe(&ctx.cache), &"!".to_string()) { + msg.channel_id.say(ctx, r.response).await?; + } if fctx.user_data().await.user_info.lock().await.get_mut(&msg.author.id.as_u64()).is_none() { debug!("Tried to add XP to a user we don't know about, aborting."); return Ok(()) } - let db = &fctx.user_data().await.database; let mut rng = SmallRng::from_entropy(); let xp_reward = rng.gen_range(1..30).clone(); drop(rng); diff --git a/src/badgey/models/custom_response.rs b/src/badgey/models/custom_response.rs new file mode 100644 index 0000000..f24c06c --- /dev/null +++ b/src/badgey/models/custom_response.rs @@ -0,0 +1,114 @@ +use std::fmt::{Display, Formatter}; +use built::chrono::{NaiveDateTime, Utc}; +use diesel::prelude::*; +use diesel::insert_into; +use regex::Regex; +use poise::serenity_prelude as serenity; + +use manifold::Db; +use manifold::error::{ManifoldError, ManifoldResult}; +use manifold::models::user::UserInfo; +use manifold::schema::userinfo; + +use crate::badgey::schema::*; + +#[derive(Queryable, Selectable, Identifiable, Insertable, AsChangeset, Associations, Debug, PartialEq, Clone)] +#[diesel(belongs_to(UserInfo, foreign_key = added_for))] +#[diesel(table_name = custom_responses)] +#[diesel(treat_none_as_null = true)] +pub struct CustomResponse { + pub id: i32, + pub trigger: String, + pub response: String, + pub added_by: i64, + pub added_on: i64, + pub added_for: i64, + pub deleted: Option, +} + +#[derive(Insertable, Debug, PartialEq, Clone)] +#[diesel(table_name = custom_responses)] +pub struct CustomResponseInserter { + pub trigger: String, + pub response: String, + pub added_by: i64, + pub added_on: i64, + pub added_for: i64, + pub deleted: Option, +} + +impl CustomResponseInserter { + pub fn new(trigger: String, response: String, target: serenity::User, caller: serenity::User) -> Self { + Self { + trigger, + response, + added_by: caller.id.as_u64().clone() as i64, + added_on: Utc::now().timestamp(), + added_for: target.id.as_u64().clone() as i64, + deleted: None, + } + } + + pub fn insert(&self, conn: &Db) -> ManifoldResult { + insert_into(custom_responses::table) + .values(self) + .execute(&mut conn.get()?) + .map_err(|e| ManifoldError::from(e)) + } +} + + +impl CustomResponse { + + pub fn test_content(conn: &Db, needle: &String, prefix: &String) -> ManifoldResult { + let pattern = Regex::new(format!("^{}(\\S*)", prefix).as_str())?; + match pattern.captures(needle) { + Some(hit) => CustomResponse::find_by_trigger(conn, hit.get(1).unwrap().as_str()), + None => Err("")? + } + } + + pub fn find_by_id(conn: &Db, needle: &i32) -> ManifoldResult { + Ok(custom_responses::table + .filter(custom_responses::id.eq(needle)) + .limit(1) + .select(CustomResponse::as_select()) + .get_result(&mut conn.get()?)?) + } + + pub fn find_by_trigger(conn: &Db, needle: &str) -> ManifoldResult { + Ok(custom_responses::table + .filter(custom_responses::trigger.ilike(needle)) + .filter(custom_responses::deleted.is_null()) + .limit(1) + .select(CustomResponse::as_select()) + .get_result(&mut conn.get()?)?) + } + + pub fn find_by_user(conn: &Db, needle: &serenity::User) -> ManifoldResult> { + let user = userinfo::table + .filter(userinfo::user_id.eq(needle.id.as_u64().clone() as i64)) + .select(UserInfo::as_select()) + .get_result(&mut conn.get()?)?; + + Ok(CustomResponse::belonging_to(&user) + .select(CustomResponse::as_select()) + .load::(&mut conn.get()?)?) + } + + pub fn insert(&self, conn: &Db) -> ManifoldResult { + insert_into(custom_responses::table) + .values(self) + .on_conflict(custom_responses::id) + .do_update() + .set(self) + .execute(&mut conn.get()?) + .map_err(|e| ManifoldError::from(e)) + } +} + +impl Display for CustomResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ID: {}, trigger: {}, response: {}, added by: {}, added on: {}, deleted: {}", self.id, self.trigger, self.response, serenity::UserId::from(self.added_by as u64), NaiveDateTime::from_timestamp_opt(self.added_on, 0).unwrap(), self.deleted.unwrap_or(false)) + } +} \ No newline at end of file diff --git a/src/badgey/models/mod.rs b/src/badgey/models/mod.rs index 06e0b45..bdc39b0 100644 --- a/src/badgey/models/mod.rs +++ b/src/badgey/models/mod.rs @@ -1 +1,2 @@ +pub mod custom_response; pub mod xp; diff --git a/src/badgey/schema.rs b/src/badgey/schema.rs index a37b0bb..21ba434 100644 --- a/src/badgey/schema.rs +++ b/src/badgey/schema.rs @@ -1,48 +1,67 @@ // @generated automatically by Diesel CLI. +diesel::table! { + custom_responses (id) { + id -> Int4, + trigger -> Text, + response -> Text, + added_by -> Int8, + added_on -> Int8, + added_for -> Int8, + deleted -> Nullable, + } +} + diesel::table! { ranks (role_id) { - role_id -> BigInt, - required_level -> BigInt, - rank_track -> BigInt, - rank_name -> Text, + role_id -> Int8, + required_level -> Int8, + rank_track -> Int8, + #[max_length = 128] + rank_name -> Varchar, } } diesel::table! { tracks (track_id) { - track_id -> BigInt, - track_name -> Text, + track_id -> Int8, + #[max_length = 128] + track_name -> Varchar, } } diesel::table! { userinfo (user_id) { - user_id -> BigInt, + user_id -> Int8, username -> Text, - weather_location -> Nullable, - weather_units -> Nullable, - timezone -> Nullable, - last_seen -> Nullable, + #[max_length = 36] + weather_location -> Nullable, + #[max_length = 1] + weather_units -> Nullable, + #[max_length = 8] + timezone -> Nullable, + last_seen -> Nullable, } } diesel::table! { xp (user_id) { - user_id -> BigInt, - user_current_level -> BigInt, - xp_value -> BigInt, - last_given_xp -> Nullable, - rank_track -> BigInt, - rank_track_last_changed -> Nullable, - freeze_rank -> Nullable, - freeze_rank_last_changed -> Nullable, + user_id -> Int8, + user_current_level -> Int8, + xp_value -> Int8, + last_given_xp -> Nullable, + rank_track -> Int8, + rank_track_last_changed -> Nullable, + freeze_rank -> Nullable, + freeze_rank_last_changed -> Nullable, } } +diesel::joinable!(custom_responses -> userinfo (added_for)); diesel::joinable!(xp -> userinfo (user_id)); diesel::allow_tables_to_appear_in_same_query!( + custom_responses, ranks, tracks, userinfo, -- 2.30.2 From 09ff09d48f5bd33efa67597333d0d5cbf0eb8f32 Mon Sep 17 00:00:00 2001 From: Xyon Date: Wed, 27 Sep 2023 10:38:42 +0100 Subject: [PATCH 3/3] Deal with compiler warnings --- Cargo.lock | 2 +- src/badgey/commands/custom_responses.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2fdb01..19b843d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ dependencies = [ [[package]] name = "badgey" -version = "3.0.3" +version = "3.1.0" dependencies = [ "built", "clap", diff --git a/src/badgey/commands/custom_responses.rs b/src/badgey/commands/custom_responses.rs index 6edbe97..5419b5e 100644 --- a/src/badgey/commands/custom_responses.rs +++ b/src/badgey/commands/custom_responses.rs @@ -1,4 +1,3 @@ -use clap::error::ContextKind::Custom; use poise::serenity_prelude as serenity; use manifold::error::{ManifoldError, ManifoldResult}; @@ -39,7 +38,7 @@ async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> Manifold let mut response = CustomResponse::find_by_id(db, &id)?; - if let Some(d) = response.deleted { + if let Some(_) = response.deleted { response.deleted = None; response.insert(db)?; ctx.reply(format!("Undeleted custom response with ID: {}", &id)).await?; -- 2.30.2