Add custom response handlers

This commit is contained in:
Xyon 2023-09-27 10:37:53 +01:00
parent a8120c9466
commit 6458598026
Signed by: xyon
GPG Key ID: DD18155D6B18078D
11 changed files with 243 additions and 28 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
*.db
.env

View File

@ -2,6 +2,7 @@
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/migrations/2023-09-20-230922_extend userinfo to include XP/up.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/migrations/2023-09-26-215927_add custom responses table/up.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

3
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"] }

View File

@ -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<ManifoldData, ManifoldError>; 4] {
[add_custom_response(), remove_custom_response(), undelete_custom_response(), list_custom_responses_for_user()]
}

View File

@ -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<Command<ManifoldData, ManifoldError>> {
commands().into_iter()
.chain(custom_responses::commands())
.chain(ranks::commands())
.collect()
}

View File

@ -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<String>) -> 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,

View File

@ -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);

View File

@ -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<bool>,
}
#[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<bool>,
}
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<usize> {
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<Self> {
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<Self> {
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<Self> {
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<Vec<Self>> {
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::<CustomResponse>(&mut conn.get()?)?)
}
pub fn insert(&self, conn: &Db) -> ManifoldResult<usize> {
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))
}
}

View File

@ -1 +1,2 @@
pub mod custom_response;
pub mod xp;

View File

@ -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<Bool>,
}
}
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<Text>,
weather_units -> Nullable<Text>,
timezone -> Nullable<Text>,
last_seen -> Nullable<BigInt>,
#[max_length = 36]
weather_location -> Nullable<Varchar>,
#[max_length = 1]
weather_units -> Nullable<Varchar>,
#[max_length = 8]
timezone -> Nullable<Varchar>,
last_seen -> Nullable<Int8>,
}
}
diesel::table! {
xp (user_id) {
user_id -> BigInt,
user_current_level -> BigInt,
xp_value -> BigInt,
last_given_xp -> Nullable<BigInt>,
rank_track -> BigInt,
rank_track_last_changed -> Nullable<BigInt>,
freeze_rank -> Nullable<BigInt>,
freeze_rank_last_changed -> Nullable<BigInt>,
user_id -> Int8,
user_current_level -> Int8,
xp_value -> Int8,
last_given_xp -> Nullable<Int8>,
rank_track -> Int8,
rank_track_last_changed -> Nullable<Int8>,
freeze_rank -> Nullable<Int8>,
freeze_rank_last_changed -> Nullable<Int8>,
}
}
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,