diff --git a/migrations/2025-02-17-204047_channels-table/down.sql b/migrations/2025-02-17-204047_channels-table/down.sql new file mode 100644 index 0000000..d0c3c38 --- /dev/null +++ b/migrations/2025-02-17-204047_channels-table/down.sql @@ -0,0 +1,18 @@ +ALTER TABLE IF EXISTS "channels" + RENAME TO "quarantine_channels"; + +-- Make the quarantine channels table hold more generic channel info +ALTER TABLE IF EXISTS "quarantine_channels" + RENAME COLUMN "channel_id" TO "qc_channel_id"; + +-- Channels with this flag should retain old qc behaviour +ALTER TABLE IF EXISTS "quarantine_channels" + DROP COLUMN "is_quarantine_channel"; + +-- Setting flag to false allows channels to be excluded from XP even if not QC (though QC channels via the above flag imply this flag too) +ALTER TABLE IF EXISTS "quarantine_channels" + DROP COLUMN "is_valid_for_xp"; + +-- Ensure that the role ID can be null for non-qc channels +ALTER TABLE IF EXISTS "quarantine_channels" + ALTER COLUMN "qc_role_id" SET NOT NULL; diff --git a/migrations/2025-02-17-204047_channels-table/up.sql b/migrations/2025-02-17-204047_channels-table/up.sql new file mode 100644 index 0000000..cfea033 --- /dev/null +++ b/migrations/2025-02-17-204047_channels-table/up.sql @@ -0,0 +1,19 @@ +-- Make the quarantine channels table hold more generic channel info +ALTER TABLE IF EXISTS "quarantine_channels" + RENAME COLUMN "qc_channel_id" TO "channel_id"; + +-- Channels with this flag should retain old qc behaviour +ALTER TABLE IF EXISTS "quarantine_channels" + ADD COLUMN "is_quarantine_channel" BOOL NOT NULL DEFAULT FALSE; + +-- Setting flag to false allows channels to be excluded from XP even if not QC (though QC channels via the above flag imply this flag too) +ALTER TABLE IF EXISTS "quarantine_channels" + ADD COLUMN "is_valid_for_xp" BOOL NOT NULL DEFAULT TRUE; + +-- Ensure that the role ID can be null for non-qc channels +ALTER TABLE IF EXISTS "quarantine_channels" + ALTER COLUMN "qc_role_id" DROP NOT NULL; + +-- Rename table to something more suitable +ALTER TABLE IF EXISTS "quarantine_channels" + RENAME TO "channels"; diff --git a/src/badgey/commands/moderation.rs b/src/badgey/commands/moderation.rs index 7550d5c..a6064cc 100644 --- a/src/badgey/commands/moderation.rs +++ b/src/badgey/commands/moderation.rs @@ -2,7 +2,7 @@ use built::chrono; use manifold::error::{ManifoldError, ManifoldResult}; use manifold::{ManifoldContext, ManifoldData}; use poise::serenity_prelude as serenity; -use crate::badgey::models::quarantine_channel::QuarantineChannel; +use crate::badgey::models::quarantine_channel::Channel; #[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")] async fn void_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> { @@ -38,25 +38,44 @@ async fn security_record(ctx: ManifoldContext<'_>) -> ManifoldResult<()> { #[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")] async fn add_quarantine_channel(ctx: ManifoldContext<'_>, channel: serenity::ChannelId, role: serenity::RoleId) -> ManifoldResult<()> { - let qc = QuarantineChannel::new(role.as_u64().clone() as i64, channel.as_u64().clone() as i64); + let qc = Channel::new(Some(role.as_u64().clone() as i64), channel.as_u64().clone() as i64, true, false); qc.insert(&ctx.data().database)?; - ctx.reply(format!("OK: Quarantine channel {channel} defined with quarantine role {role}", channel = channel, role = role)).await?; + ctx.reply(format!("OK: Quarantine channel <#{channel}> defined with quarantine role <@{role}>", channel = channel, role = role)).await?; Ok(()) } #[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")] async fn remove_quarantine_channel(ctx: ManifoldContext<'_>, channel: serenity::ChannelId) -> ManifoldResult<()> { - let db = &ctx.data().database; - QuarantineChannel::get(db, channel.as_u64().clone() as i64)?.remove(db)?; + Channel::get_by_channel_id(db, channel.as_u64().clone() as i64)?.remove(db)?; - ctx.reply(format!("OK: Quarantine channel {channel} is no longer defined.", channel = channel)).await?; + ctx.reply(format!("OK: Quarantine channel <#{channel}> is no longer defined.", channel = channel)).await?; Ok(()) } -pub fn commands() -> [poise::Command; 7] { - [void_user(), unvoid_user(), airlock_user(), record_chronicle(), security_record(), add_quarantine_channel(), remove_quarantine_channel()] +#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")] +async fn ignore_channel_for_xp(ctx: ManifoldContext<'_>, channel: serenity::ChannelId) -> ManifoldResult<()> { + let qc = Channel::new(None, channel.as_u64().clone() as i64, false, false); + qc.insert(&ctx.data().database)?; + + ctx.reply(format!("OK: Channel <#{channel}> will no longer allow users to accrue XP.", channel = channel)).await?; + + Ok(()) +} + +#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")] +async fn unignore_channel_for_xp(ctx: ManifoldContext<'_>, channel: serenity::ChannelId) -> ManifoldResult<()> { + let db = &ctx.data().database; + Channel::get_by_channel_id(db, channel.as_u64().clone() as i64)?.remove(db)?; + + ctx.reply(format!("OK: Channel <#{channel}> will now permit users to accrue XP.", channel = channel)).await?; + + Ok(()) +} + +pub fn commands() -> [poise::Command; 9] { + [void_user(), unvoid_user(), airlock_user(), record_chronicle(), security_record(), add_quarantine_channel(), remove_quarantine_channel(), ignore_channel_for_xp(), unignore_channel_for_xp()] } diff --git a/src/badgey/handlers/airlock.rs b/src/badgey/handlers/airlock.rs index 6a15c06..37a2956 100644 --- a/src/badgey/handlers/airlock.rs +++ b/src/badgey/handlers/airlock.rs @@ -3,7 +3,7 @@ use manifold::ManifoldData; use poise::FrameworkContext; use poise::serenity_prelude::{ChannelId, Context, Member, Mentionable}; -use crate::badgey::models::quarantine_channel::QuarantineChannel; +use crate::badgey::models::quarantine_channel::Channel; pub async fn airlock_handler(ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, old: &Option, new: &Member) -> ManifoldResult<()> { @@ -11,17 +11,17 @@ pub async fn airlock_handler(ctx: &Context, fctx: &FrameworkContext<'_, Manifold // Is this user quarantined? for role in new.roles.iter() { - if let Ok(channel) = QuarantineChannel::get(db, role.as_u64().clone() as i64) { + if let Ok(channel) = Channel::get_by_qc_role_id(db, role.as_u64().clone() as i64) { // Was this user JUST added to a quarantine channel? if let Some(m) = old { for role in m.roles.iter() { - if let Ok(_) = QuarantineChannel::get(db, role.as_u64().clone() as i64) { + if let Ok(_) = Channel::get_by_qc_role_id(db, role.as_u64().clone() as i64) { // User already had quarantine role, return early return Ok(()) } } - let target_channel: ChannelId = ChannelId::from(channel.qc_channel_id as u64); + let target_channel: ChannelId = ChannelId::from(channel.channel_id as u64); // User was just added to a quarantine channel. Post the initial ping message. target_channel.say(ctx, format!("{mention}, please respond to this message", mention = new.mention())).await?; diff --git a/src/badgey/models/quarantine_channel.rs b/src/badgey/models/quarantine_channel.rs index 7664ecd..dbb21b9 100644 --- a/src/badgey/models/quarantine_channel.rs +++ b/src/badgey/models/quarantine_channel.rs @@ -3,45 +3,57 @@ use diesel::dsl::delete; use diesel::prelude::*; use manifold::Db; use manifold::error::{ManifoldError, ManifoldResult}; -use crate::badgey::schema::quarantine_channels as quarantine_table; +use crate::badgey::schema::channels as channel_table; use crate::badgey::schema::*; #[derive(Queryable, Selectable, Identifiable, Insertable, Debug, Clone)] -#[diesel(table_name = quarantine_channels)] -pub struct QuarantineChannel { +#[diesel(table_name = channels)] +pub struct Channel { #[diesel(deserialize_as = i32)] pub id: Option, - pub qc_role_id: i64, - pub qc_channel_id: i64, + pub qc_role_id: Option, + pub channel_id: i64, + pub is_quarantine_channel: bool, + pub is_valid_for_xp: bool, } -impl QuarantineChannel { - pub fn new(role_id: i64, channel_id: i64) -> Self { - QuarantineChannel { +impl Channel { + pub fn new(qc_role_id: Option, channel_id: i64, is_quarantine_channel: bool, is_valid_for_xp: bool) -> Self { + Channel { id: None, - qc_role_id: role_id, - qc_channel_id: channel_id + qc_role_id, + channel_id, + is_quarantine_channel, + is_valid_for_xp, } } pub fn insert(&self, conn: &Db) -> ManifoldResult { - insert_into(quarantine_table::dsl::quarantine_channels) + insert_into(channel_table::dsl::channels) .values(self) .execute(&mut conn.get()?) .map_err(|e| ManifoldError::from(e)) } pub fn remove(&self, conn: &Db) -> ManifoldResult { - Ok(delete(quarantine_table::dsl::quarantine_channels) - .filter(quarantine_table::qc_role_id.eq(self.qc_role_id)) + Ok(delete(channel_table::dsl::channels) + .filter(channel_table::qc_role_id.eq(self.qc_role_id)) .execute(&mut conn.get()?)?) } - pub fn get(conn: &Db, needle: i64) -> ManifoldResult { - Ok(quarantine_table::dsl::quarantine_channels - .filter(quarantine_table::qc_role_id.eq(needle)) + pub fn get_by_channel_id(conn: &Db, needle: i64) -> ManifoldResult { + Ok(channel_table::dsl::channels + .filter(channel_table::channel_id.eq(needle)) .limit(1) - .select(QuarantineChannel::as_select()) + .select(Channel::as_select()) + .get_result(&mut conn.get()?)?) + } + + pub fn get_by_qc_role_id(conn: &Db, needle: i64) -> ManifoldResult { + Ok(channel_table::dsl::channels + .filter(channel_table::qc_role_id.eq(needle)) + .limit(1) + .select(Channel::as_select()) .get_result(&mut conn.get()?)?) } } \ No newline at end of file diff --git a/src/badgey/models/xp.rs b/src/badgey/models/xp.rs index 612d3b7..432c402 100644 --- a/src/badgey/models/xp.rs +++ b/src/badgey/models/xp.rs @@ -11,7 +11,7 @@ use manifold::models::user::UserInfo; use manifold::schema::userinfo; use poise::FrameworkContext; use poise::serenity_prelude::{Context, Mentionable, Message, RoleId}; -use crate::badgey::models::quarantine_channel::QuarantineChannel; +use crate::badgey::models::quarantine_channel::Channel; use crate::badgey::schema::xp as xp_table; use crate::badgey::schema::*; @@ -171,18 +171,25 @@ impl Xp { let xp_reward = rng.gen_range(1..30).clone(); drop(rng); let user_id_i64 = msg.author.id.as_u64().clone() as i64; + let channel_id_i64 = msg.channel_id.as_u64().clone() as i64; + + let mut valid_channel = true; + + if let Ok(c) = Channel::get_by_channel_id(&fctx.user_data.database, channel_id_i64) { + valid_channel = c.is_valid_for_xp && !c.is_quarantine_channel; + } let mut xp = match Xp::get(db, &user_id_i64) { Ok(x) => x, Err(_) => Xp::new(&user_id_i64) }; - let valid = match xp.last_given_xp { - Some(t) => (chrono::Utc::now().timestamp() - 60) > t && QuarantineChannel::get(&fctx.user_data.database, msg.channel_id.as_u64().clone() as i64).is_err(), + let valid_user = match xp.last_given_xp { + Some(t) => (chrono::Utc::now().timestamp() - 60) > t, None => true }; - if valid { + if valid_user && valid_channel { xp.last_given_xp = Some(chrono::Utc::now().timestamp()); xp.xp_value += &xp_reward; let calculated_level = xp.get_level_from_xp(&db, None); diff --git a/src/badgey/schema.rs b/src/badgey/schema.rs index ea48fda..83b791a 100644 --- a/src/badgey/schema.rs +++ b/src/badgey/schema.rs @@ -1,5 +1,15 @@ // @generated automatically by Diesel CLI. +diesel::table! { + channels (id) { + id -> Int4, + qc_role_id -> Nullable, + channel_id -> Int8, + is_quarantine_channel -> Bool, + is_valid_for_xp -> Bool, + } +} + diesel::table! { custom_responses (id) { id -> Int4, @@ -12,14 +22,6 @@ diesel::table! { } } -diesel::table! { - quarantine_channels (id) { - id -> Int4, - qc_role_id -> Int8, - qc_channel_id -> Int8, - } -} - diesel::table! { ranks (role_id) { role_id -> Int8, @@ -72,8 +74,8 @@ diesel::joinable!(custom_responses -> userinfo (added_for)); diesel::joinable!(xp -> userinfo (user_id)); diesel::allow_tables_to_appear_in_same_query!( + channels, custom_responses, - quarantine_channels, ranks, tracks, userinfo,