Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion crates/rustmail/src/handlers/guild_interaction_handler.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::prelude::commands::*;
use crate::prelude::config::*;
use crate::prelude::errors::*;
use crate::prelude::features::*;
use crate::prelude::modules::*;
use crate::prelude::types::*;
use crate::utils::{MessageBuilder, defer_response};
use crate::utils::{
MessageBuilder, defer_response, defer_response_ephemeral, is_staff_member, is_staff_user,
};
use serenity::all::{Context, EventHandler, Interaction, Permissions};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
Expand Down Expand Up @@ -119,6 +122,34 @@ impl EventHandler for InteractionHandler {
}
}
Interaction::Command(command) => {
// Every command is staff-only. Discord registration alone doesn't enforce it:
// in single mode everything lives in the shared guild, and community commands
// are visible to every community member. Community commands may be run from
// either guild, the others only from the staff guild.
let community_command = self
.registry
.get(command.data.name.as_str())
.is_some_and(|c| c.as_community().is_some());
let is_staff = match command.guild_id {
Some(_) if community_command => {
is_staff_user(&ctx, &self.config, command.user.id).await
}
Some(guild_id) => {
is_staff_member(&ctx, &self.config, guild_id, command.user.id).await
}
None => true,
};
if !is_staff {
if defer_response_ephemeral(&ctx, &command).await.is_ok()
&& let Some(error_handler) = &self.config.error_handler
{
let _ = error_handler
.reply_to_command_with_error(&ctx, &command, &permission_denied())
.await;
}
return;
}

if self.maintenance_mode.load(Ordering::Relaxed)
&& let Some(guild_id) = command.guild_id
&& !is_interaction_user_maintenance_exempt(
Expand Down
8 changes: 8 additions & 0 deletions crates/rustmail/src/handlers/guild_messages_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ impl EventHandler for GuildMessagesHandler {
command_name = &message_content[self.config.command.prefix.len()..i];
}

// Text commands are staff-only, like the slash commands (which are not even
// registered in the community guild). Ignore everyone else silently.
if let Some(guild_id) = msg.guild_id
&& !is_staff_member(&ctx, &self.config, guild_id, msg.author.id).await
{
return;
}

if self.maintenance_mode.load(Ordering::Relaxed)
&& !is_user_maintenance_exempt(&ctx, &msg, &self.config).await
{
Expand Down
2 changes: 2 additions & 0 deletions crates/rustmail/src/utils/command/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
pub mod category;
pub mod defer_response;
pub mod extract_reply_content;
pub mod staff_access;
pub mod wrap_command;

pub use category::*;
pub use defer_response::*;
pub use extract_reply_content::*;
pub use staff_access::*;
68 changes: 68 additions & 0 deletions crates/rustmail/src/utils/command/staff_access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::prelude::config::*;
use serenity::all::{ChannelId, Context, GuildId, UserId};

/// Whether `user_id` may use staff commands in `guild_id`: the staff guild only, and only for
/// staff (see [`is_staff_user`]).
pub async fn is_staff_member(
ctx: &Context,
config: &Config,
guild_id: GuildId,
user_id: UserId,
) -> bool {
guild_id.get() == config.bot.get_staff_guild_id() && is_staff_user(ctx, config, user_id).await
}

/// Whether `user_id` is staff, wherever the command was invoked from.
///
/// Dual mode: staff are the members of the staff guild.
/// Single mode: staff are the members who can see the inbox category (which is also who can see
/// the tickets). Configured super admins are always staff.
pub async fn is_staff_user(ctx: &Context, config: &Config, user_id: UserId) -> bool {
if config.bot.panel_super_admin_users.contains(&user_id.get()) {
return true;
}

let guild_id = GuildId::new(config.bot.get_staff_guild_id());

let member = match guild_id.member(ctx, user_id).await {
Ok(m) => m,
Err(_) => return false,
};

if config.bot.is_dual_mode()
|| member
.roles
.iter()
.any(|role_id| config.bot.panel_super_admin_roles.contains(&role_id.get()))
{
return true;
}

let inbox_category_id = ChannelId::new(config.thread.inbox_category_id);

let cached = ctx.cache.guild(guild_id).and_then(|guild| {
guild
.channels
.get(&inbox_category_id)
.map(|category| guild.user_permissions_in(category, &member))
});

let permissions = match cached {
Some(p) => p,
None => {
let category = match inbox_category_id.to_channel(ctx).await {
Ok(channel) => match channel.guild() {
Some(c) => c,
None => return false,
},
Err(_) => return false,
};
match guild_id.to_partial_guild(ctx).await {
Ok(guild) => guild.user_permissions_in(&category, &member),
Err(_) => return false,
}
}
};

permissions.view_channel()
}
16 changes: 11 additions & 5 deletions docs/guides/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,14 @@ Combinations are supported: `1d12h` = 1 day and 12 hours.

## Command Permissions

Commands are available to users with access to ticket channels. The bot respects Discord's permission system:

- Only staff with channel access can use ticket commands
- Super admins (configured in `config.toml`) have full access
- Additional permissions can be configured through the panel
Commands (text and slash) are reserved to staff:

- **Dual-server mode**: commands only work in the staff server. Slash commands are not registered in the community
server, and text commands sent there are ignored. The one exception is the *New Thread* user context menu
(right-click a member → Apps), which is available in the community server too, but only for members of the staff
server.
- **Single-server mode**: staff are the members who can see the inbox category (`inbox_category_id`), which is also
who can see the tickets. Other members are ignored by text commands and get a "permission denied" reply to slash
commands. To also hide slash commands from them, restrict the bot's commands in *Server Settings → Integrations*.
- Super admins (`panel_super_admin_users` / `panel_super_admin_roles` in `config.toml`) always count as staff.
- Additional panel permissions can be configured through the panel