From 62f3d066d059e4755354a1baabcc898cd75bab46 Mon Sep 17 00:00:00 2001 From: CircuitSacul Date: Fri, 6 Jan 2023 12:00:22 -0500 Subject: [PATCH] Xproles (#133) * xproles commands * implement xprole updating --- sqlx-data.json | 224 +++++++++++++++--- src/constants.rs | 3 + src/core/mod.rs | 1 + src/core/starboard/reaction_events.rs | 4 +- src/core/stats.rs | 23 +- src/core/xproles.rs | 55 +++++ src/database/models/member.rs | 15 ++ src/database/models/xprole.rs | 50 +++- src/interactions/commands/chat/mod.rs | 1 + .../commands/chat/xproles/delete.rs | 28 +++ src/interactions/commands/chat/xproles/mod.rs | 36 +++ .../commands/chat/xproles/setxp.rs | 64 +++++ .../commands/chat/xproles/view.rs | 48 ++++ src/interactions/commands/handle.rs | 1 + src/interactions/commands/permissions.rs | 4 + src/interactions/commands/register.rs | 1 + 16 files changed, 513 insertions(+), 45 deletions(-) create mode 100644 src/core/xproles.rs create mode 100644 src/interactions/commands/chat/xproles/delete.rs create mode 100644 src/interactions/commands/chat/xproles/mod.rs create mode 100644 src/interactions/commands/chat/xproles/setxp.rs create mode 100644 src/interactions/commands/chat/xproles/view.rs diff --git a/sqlx-data.json b/sqlx-data.json index 01e22e84..d34863dc 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -90,40 +90,6 @@ }, "query": "SELECT * FROM overrides\n WHERE starboard_id=$1 AND\n channel_ids && $2::bigint[]" }, - "0503b7de215f55d8463a73b800bba7a9e1c48f91ba6d67e36c3b105f8b1a1484": { - "describe": { - "columns": [ - { - "name": "role_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "guild_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "required", - "ordinal": 2, - "type_info": "Int2" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Int2" - ] - } - }, - "query": "INSERT INTO xproles\n (role_id, guild_id, required)\n VALUES ($1, $2, $3)\n RETURNING *" - }, "066998b6c842af003c90e4c71ae933524c0bef88b94b3dce92cc14e855852ad3": { "describe": { "columns": [ @@ -168,6 +134,26 @@ }, "query": "DELETE FROM permroles WHERE role_id=$1 RETURNING *" }, + "08d0ded05e0b03c89892109f492fee7fa6e7a47afd18bf4155524593ce92aaca": { + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int8" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT count(*) FROM xproles WHERE guild_id=$1" + }, "0b73a06177a4053dd91a029b196db5f718a2626c09ef951ac8df598ec60759b9": { "describe": { "columns": [ @@ -508,6 +494,45 @@ }, "query": "DELETE FROM votes WHERE message_id=$1 AND starboard_id=$2 AND user_id=$3\n RETURNING *" }, + "2111de885c5b267c61a1f639a990e1f40114a56e5303add22b8e5a348080999d": { + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "guild_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "xp", + "ordinal": 2, + "type_info": "Float4" + }, + { + "name": "autoredeem_enabled", + "ordinal": 3, + "type_info": "Bool" + } + ], + "nullable": [ + false, + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + } + }, + "query": "SELECT * FROM members WHERE guild_id=$1 AND user_id=$2" + }, "234c6ea2740620a51ee59fd0dcbe4a54fa4be45c02312f69c7bdfa81e9e43ee3": { "describe": { "columns": [ @@ -1328,6 +1353,39 @@ }, "query": "DELETE FROM starboards WHERE name=$1 AND guild_id=$2\n RETURNING *" }, + "4210d1f0ce64aca7813b63421f3af67b1a0c3c586e0507f52754bd50c75189f3": { + "describe": { + "columns": [ + { + "name": "role_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "guild_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "required", + "ordinal": 2, + "type_info": "Int2" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Int2", + "Int8" + ] + } + }, + "query": "UPDATE xproles SET required=$1 WHERE role_id=$2 RETURNING *" + }, "4799c2a2b60b149a964d4e435f3991a8b12a9ecdd8f5d413fbbe01649b185470": { "describe": { "columns": [ @@ -1827,6 +1885,38 @@ }, "query": "INSERT INTO patrons (patreon_id) VALUES ($1)\n RETURNING *" }, + "70e4dde509221ec5efbae5241702d3d7876fed9a216872704d2449ccaeeb458c": { + "describe": { + "columns": [ + { + "name": "role_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "guild_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "required", + "ordinal": 2, + "type_info": "Int2" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "SELECT * FROM xproles WHERE guild_id=$1 ORDER BY required DESC" + }, "7584c85655f379fc8a95a4b0fe7e3d060711300ba1ec6e28d01bd7536ba4c53b": { "describe": { "columns": [ @@ -3499,6 +3589,38 @@ }, "query": "SELECT * FROM starboards WHERE guild_id=$1" }, + "c9a0aa39cb29f094a4c208de6e7f5cfb5a26b34a7faed0bd32a1660e98f21bfb": { + "describe": { + "columns": [ + { + "name": "role_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "guild_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "required", + "ordinal": 2, + "type_info": "Int2" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "DELETE FROM xproles WHERE role_id=$1 RETURNING *" + }, "cb9b3ce9131ac41057770317c912a7c71f79dfc2818f9d0f547acb9939516027": { "describe": { "columns": [ @@ -3669,6 +3791,40 @@ }, "query": "INSERT INTO overrides\n (guild_id, name, starboard_id)\n VALUES ($1, $2, $3)\n RETURNING *" }, + "dfc37673f365e1eb8f2b224a2fac0273285af7607978fe5b40a86fd09fbb1bcc": { + "describe": { + "columns": [ + { + "name": "role_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "guild_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "required", + "ordinal": 2, + "type_info": "Int2" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int2" + ] + } + }, + "query": "INSERT INTO xproles (role_id, guild_id, required) VALUES ($1, $2, $3) RETURNING *" + }, "e0d3435ac460506de5e62af8dea8bf9151a123a5e958a706241e15206499076c": { "describe": { "columns": [ diff --git a/src/constants.rs b/src/constants.rs index 42d3f057..7ea033d4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -63,3 +63,6 @@ pub const MAX_OVERRIDES_PER_STARBOARD: i64 = 10; // PermRole Validation pub const MAX_PERMROLES: i64 = 50; + +// XP-based Award Role Validation +pub const MAX_XPROLES: i64 = 50; diff --git a/src/core/mod.rs b/src/core/mod.rs index eb785095..29ea1a83 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -5,3 +5,4 @@ pub mod has_image; pub mod permroles; pub mod starboard; pub mod stats; +pub mod xproles; diff --git a/src/core/starboard/reaction_events.rs b/src/core/starboard/reaction_events.rs index 2e1fede8..d8a903f1 100644 --- a/src/core/starboard/reaction_events.rs +++ b/src/core/starboard/reaction_events.rs @@ -169,7 +169,7 @@ pub async fn handle_reaction_add( } } - refresh_xp(bot, guild_id.get_i64(), author_id).await?; + refresh_xp(bot, guild_id, author_id.into_id()).await?; Ok(()) } @@ -219,7 +219,7 @@ pub async fn handle_reaction_remove( VoteStatus::Ignore | VoteStatus::Remove => (), } - refresh_xp(bot, guild_id.get_i64(), author.user_id).await?; + refresh_xp(bot, guild_id, author.user_id.into_id()).await?; Ok(()) } diff --git a/src/core/stats.rs b/src/core/stats.rs index cb3a4a51..79deb2df 100644 --- a/src/core/stats.rs +++ b/src/core/stats.rs @@ -1,10 +1,17 @@ +use twilight_model::id::{ + marker::{GuildMarker, UserMarker}, + Id, +}; + use crate::{ client::bot::StarboardBot, database::{Member, Starboard}, errors::StarboardResult, - utils::into_id::IntoId, + utils::id_as_i64::GetI64, }; +use super::xproles::refresh_xpr; + #[derive(Default)] pub struct MemberStats { pub xp: f32, @@ -117,21 +124,27 @@ impl MemberStats { } } -pub async fn refresh_xp(bot: &StarboardBot, guild_id: i64, user_id: i64) -> StarboardResult<()> { +pub async fn refresh_xp( + bot: &StarboardBot, + guild_id: Id, + user_id: Id, +) -> StarboardResult<()> { if bot .cooldowns .xp_refresh - .trigger(&(user_id.into_id(), guild_id.into_id())) + .trigger(&(user_id, guild_id)) .is_some() { return Ok(()); } - let Some(stats) = MemberStats::get(&bot.pool, guild_id, user_id).await? else { + let Some(stats) = MemberStats::get(&bot.pool, guild_id.get_i64(), user_id.get_i64()).await? else { return Ok(()); }; - Member::set_xp(&bot.pool, user_id, guild_id, stats.xp).await?; + Member::set_xp(&bot.pool, user_id.get_i64(), guild_id.get_i64(), stats.xp).await?; + + refresh_xpr(bot, guild_id, user_id).await?; Ok(()) } diff --git a/src/core/xproles.rs b/src/core/xproles.rs new file mode 100644 index 00000000..7fd2c14a --- /dev/null +++ b/src/core/xproles.rs @@ -0,0 +1,55 @@ +use twilight_model::id::{ + marker::{GuildMarker, UserMarker}, + Id, +}; + +use crate::{ + client::bot::StarboardBot, + database::{Member, XPRole}, + errors::StarboardResult, + utils::{id_as_i64::GetI64, into_id::IntoId}, +}; + +pub async fn refresh_xpr( + bot: &StarboardBot, + guild_id: Id, + user_id: Id, +) -> StarboardResult<()> { + let member_roles = bot.cache.guilds.with(&guild_id, |_, g| { + let Some(g) = g else { + return None; + }; + g.members.get(&user_id).map(|m| m.roles.to_owned()) + }); + let Some(member_roles) = member_roles else { + return Ok(()); + }; + + let xproles = XPRole::list_by_guild(&bot.pool, guild_id.get_i64()).await?; + let Some(member) = Member::get(&bot.pool, guild_id.get_i64(), user_id.get_i64()).await? else { + return Ok(()); + }; + + for xpr in xproles { + let role_id = xpr.role_id.into_id(); + if member.xp >= xpr.required as f32 { + if member_roles.contains(&role_id) { + continue; + } + + bot.http + .add_guild_member_role(guild_id, user_id, role_id) + .await?; + } else { + if !member_roles.contains(&role_id) { + continue; + } + + bot.http + .remove_guild_member_role(guild_id, user_id, role_id) + .await?; + } + } + + Ok(()) +} diff --git a/src/database/models/member.rs b/src/database/models/member.rs index 3cc77020..96f32131 100644 --- a/src/database/models/member.rs +++ b/src/database/models/member.rs @@ -49,4 +49,19 @@ impl Member { .fetch_all(pool) .await } + + pub async fn get( + pool: &sqlx::PgPool, + guild_id: i64, + user_id: i64, + ) -> sqlx::Result> { + sqlx::query_as!( + Self, + "SELECT * FROM members WHERE guild_id=$1 AND user_id=$2", + guild_id, + user_id, + ) + .fetch_optional(pool) + .await + } } diff --git a/src/database/models/xprole.rs b/src/database/models/xprole.rs index 37d01398..9eef3e1f 100644 --- a/src/database/models/xprole.rs +++ b/src/database/models/xprole.rs @@ -14,10 +14,7 @@ impl XPRole { ) -> sqlx::Result { sqlx::query_as!( Self, - r#"INSERT INTO xproles - (role_id, guild_id, required) - VALUES ($1, $2, $3) - RETURNING *"#, + "INSERT INTO xproles (role_id, guild_id, required) VALUES ($1, $2, $3) RETURNING *", role_id, guild_id, required, @@ -25,4 +22,49 @@ impl XPRole { .fetch_one(pool) .await } + + pub async fn delete(pool: &sqlx::PgPool, role_id: i64) -> sqlx::Result> { + sqlx::query_as!( + Self, + "DELETE FROM xproles WHERE role_id=$1 RETURNING *", + role_id + ) + .fetch_optional(pool) + .await + } + + pub async fn set_required( + pool: &sqlx::PgPool, + role_id: i64, + required: i16, + ) -> sqlx::Result { + sqlx::query_as!( + Self, + "UPDATE xproles SET required=$1 WHERE role_id=$2 RETURNING *", + required, + role_id + ) + .fetch_one(pool) + .await + } + + pub async fn list_by_guild(pool: &sqlx::PgPool, guild_id: i64) -> sqlx::Result> { + sqlx::query_as!( + Self, + "SELECT * FROM xproles WHERE guild_id=$1 ORDER BY required DESC", + guild_id, + ) + .fetch_all(pool) + .await + } + + pub async fn count(pool: &sqlx::PgPool, guild_id: i64) -> sqlx::Result { + Ok( + sqlx::query!("SELECT count(*) FROM xproles WHERE guild_id=$1", guild_id) + .fetch_one(pool) + .await? + .count + .unwrap(), + ) + } } diff --git a/src/interactions/commands/chat/mod.rs b/src/interactions/commands/chat/mod.rs index e920da36..9194b9d9 100644 --- a/src/interactions/commands/chat/mod.rs +++ b/src/interactions/commands/chat/mod.rs @@ -8,3 +8,4 @@ pub mod random; pub mod starboard; pub mod stats; pub mod utils; +pub mod xproles; diff --git a/src/interactions/commands/chat/xproles/delete.rs b/src/interactions/commands/chat/xproles/delete.rs new file mode 100644 index 00000000..b3f6dc6f --- /dev/null +++ b/src/interactions/commands/chat/xproles/delete.rs @@ -0,0 +1,28 @@ +use twilight_interactions::command::{CommandModel, CreateCommand}; +use twilight_model::guild::Role; + +use crate::{ + database::XPRole, errors::StarboardResult, interactions::context::CommandCtx, + utils::id_as_i64::GetI64, +}; + +#[derive(CommandModel, CreateCommand)] +#[command(name = "delete", desc = "Delete an XP-based award role.")] +pub struct Delete { + /// The XPRole to delete. + xprole: Role, +} + +impl Delete { + pub async fn callback(self, mut ctx: CommandCtx) -> StarboardResult<()> { + let role = XPRole::delete(&ctx.bot.pool, self.xprole.id.get_i64()).await?; + + let (msg, ephemeral) = match role { + None => ("That is not an XPRole.", true), + Some(_) => ("XPRole deleted.", false), + }; + ctx.respond_str(msg, ephemeral).await?; + + Ok(()) + } +} diff --git a/src/interactions/commands/chat/xproles/mod.rs b/src/interactions/commands/chat/xproles/mod.rs new file mode 100644 index 00000000..c6c8289e --- /dev/null +++ b/src/interactions/commands/chat/xproles/mod.rs @@ -0,0 +1,36 @@ +mod delete; +mod setxp; +mod view; + +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::{ + errors::StarboardResult, + interactions::{commands::permissions::manage_roles, context::CommandCtx}, +}; + +#[derive(CommandModel, CreateCommand)] +#[command( + name = "xproles", + desc = "View and manage XP-based award roles.", + dm_permission = false, + default_permissions = "manage_roles" +)] +pub enum XPRoles { + #[command(name = "setxp")] + SetXP(setxp::SetXP), + #[command(name = "delete")] + Delete(delete::Delete), + #[command(name = "view")] + View(view::View), +} + +impl XPRoles { + pub async fn callback(self, ctx: CommandCtx) -> StarboardResult<()> { + match self { + Self::SetXP(cmd) => cmd.callback(ctx).await, + Self::Delete(cmd) => cmd.callback(ctx).await, + Self::View(cmd) => cmd.callback(ctx).await, + } + } +} diff --git a/src/interactions/commands/chat/xproles/setxp.rs b/src/interactions/commands/chat/xproles/setxp.rs new file mode 100644 index 00000000..0376900a --- /dev/null +++ b/src/interactions/commands/chat/xproles/setxp.rs @@ -0,0 +1,64 @@ +use twilight_interactions::command::{CommandModel, CreateCommand}; +use twilight_model::guild::Role; + +use crate::{ + constants, database::XPRole, errors::StarboardResult, get_guild_id, + interactions::context::CommandCtx, map_dup_none, utils::id_as_i64::GetI64, +}; + +#[derive(CommandModel, CreateCommand)] +#[command(name = "setxp", desc = "Create or modify an XP-based award role.")] +pub struct SetXP { + /// The role to use as an XP-based award role. + role: Role, + /// How much XP is required to obtain this award role. + #[command(min_value = 1, max_value = 32_767, rename = "required-xp")] + required_xp: i64, +} + +impl SetXP { + pub async fn callback(self, mut ctx: CommandCtx) -> StarboardResult<()> { + let guild_id = get_guild_id!(ctx).get_i64(); + + if self.role.id.get_i64() == guild_id || self.role.managed { + ctx.respond_str("You can't use that role for award roles.", true) + .await?; + return Ok(()); + } + + let count = XPRole::count(&ctx.bot.pool, guild_id).await?; + if count >= constants::MAX_XPROLES { + ctx.respond_str( + &format!( + "You can only have up to {} XP-based award roles.", + constants::MAX_XPROLES + ), + true, + ) + .await?; + return Ok(()); + } + + let role_id = self.role.id.get_i64(); + let xprole = map_dup_none!(XPRole::create( + &ctx.bot.pool, + role_id, + guild_id, + self.required_xp as i16, + ))?; + + if xprole.is_none() { + XPRole::set_required(&ctx.bot.pool, role_id, self.required_xp as i16).await?; + ctx.respond_str( + &format!("Required XP changed to {}.", self.required_xp,), + false, + ) + .await?; + } else { + ctx.respond_str("XP-based award role created.", false) + .await?; + } + + Ok(()) + } +} diff --git a/src/interactions/commands/chat/xproles/view.rs b/src/interactions/commands/chat/xproles/view.rs new file mode 100644 index 00000000..71a5ac6f --- /dev/null +++ b/src/interactions/commands/chat/xproles/view.rs @@ -0,0 +1,48 @@ +use std::fmt::Write; + +use thousands::Separable; +use twilight_interactions::command::{CommandModel, CreateCommand}; + +use crate::{ + database::XPRole, + errors::StarboardResult, + get_guild_id, + interactions::context::CommandCtx, + utils::{embed, id_as_i64::GetI64}, +}; + +#[derive(CommandModel, CreateCommand)] +#[command(name = "view", desc = "View all of your XP-based award roles.")] +pub struct View; + +impl View { + pub async fn callback(self, mut ctx: CommandCtx) -> StarboardResult<()> { + let guild_id = get_guild_id!(ctx).get_i64(); + + let xproles = XPRole::list_by_guild(&ctx.bot.pool, guild_id).await?; + if xproles.is_empty() { + ctx.respond_str("There are no XPRoles.", true).await?; + return Ok(()); + } + + let mut desc = String::new(); + for xpr in xproles { + writeln!( + desc, + "<@&{}> - `{}` XP", + xpr.role_id, + xpr.required.separate_with_commas() + ) + .unwrap(); + } + + let emb = embed::build() + .title("XP-based Award Roles") + .description(desc) + .build(); + + ctx.respond(ctx.build_resp().embeds([emb]).build()).await?; + + Ok(()) + } +} diff --git a/src/interactions/commands/handle.rs b/src/interactions/commands/handle.rs index ea5bd856..757872fc 100644 --- a/src/interactions/commands/handle.rs +++ b/src/interactions/commands/handle.rs @@ -29,6 +29,7 @@ pub async fn handle_command(ctx: CommandCtx) -> StarboardResult<()> { "starboards" => chat::starboard::Starboard, "overrides" => chat::overrides::Overrides, "permroles" => chat::permroles::PermRoles, + "xproles" => chat::xproles::XPRoles, "utils" => chat::utils::Utils, ); diff --git a/src/interactions/commands/permissions.rs b/src/interactions/commands/permissions.rs index 9fc1c2bf..0d74650f 100644 --- a/src/interactions/commands/permissions.rs +++ b/src/interactions/commands/permissions.rs @@ -1,5 +1,9 @@ use twilight_model::guild::Permissions; +pub fn manage_roles() -> Permissions { + Permissions::MANAGE_ROLES +} + pub fn manage_channels() -> Permissions { Permissions::MANAGE_CHANNELS } diff --git a/src/interactions/commands/register.rs b/src/interactions/commands/register.rs index 70b0bd42..08cf97f1 100644 --- a/src/interactions/commands/register.rs +++ b/src/interactions/commands/register.rs @@ -27,6 +27,7 @@ pub async fn post_commands(bot: Arc) { chat::starboard::Starboard, chat::overrides::Overrides, chat::permroles::PermRoles, + chat::xproles::XPRoles, chat::utils::Utils, );