Skip to main content

Member Moderation

The Discord bot supports moderation actions that can be triggered either via Discord commands or from the Heimdall backend dashboard.

Overview

Available Actions

ActionDescriptionDiscord API
WarnSend a warning DM to the userDM message
KickRemove user from the serverGuild.kick_member
BanPermanently ban userGuild.ban_member
TimeoutTemporarily restrict userMember.edit (communication_disabled_until)
Remove TimeoutRemove an active timeoutMember.edit (clear timeout)

Trigger Methods

  1. Discord Commands - Moderators use /warn, /kick, /ban, /timeout commands
  2. Backend Dashboard - Admins moderate from the Heimdall guild management UI
  3. API - Direct GraphQL mutation calls

Discord Commands

Warn Command

/warn @user [reason]

Sends a DM to the user with a warning message.

Permission Required: discord:guild.warn

Kick Command

/kick @user [reason]

Removes the user from the server. They can rejoin with a new invite.

Permission Required: discord:guild.kick

Ban Command

/ban @user [reason] [delete_days]

Permanently bans the user from the server.

ParameterDescription
reasonReason for the ban (shown in audit log)
delete_daysDays of message history to delete (0-7)

Permission Required: discord:guild.ban

Timeout Command

/timeout @user <duration> [reason]

Temporarily restricts the user from sending messages, reacting, joining voice, etc.

ParameterDescription
durationDuration: 60s, 5m, 1h, 1d, 7d, etc.
reasonReason for the timeout

Permission Required: discord:guild.timeout

Remove Timeout Command

/untimeout @user

Removes an active timeout from the user.

Permission Required: discord:guild.timeout

Backend Moderation

The Heimdall backend can trigger moderation actions via WebSocket messages to the bot.

Moderation Flow

┌──────────────────┐        ┌──────────────────┐        ┌──────────────────┐
│ Backend Console │ │ Heimdall API │ │ Discord Bot │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
│ moderateDiscordMember() │ │
│─────────────────────────>│ │
│ │ │
│ │ ModerateDiscordMemberReq │
│ │─────────────────────────>│
│ │ │
│ │ (Bot executes action │
│ │ via Discord API) │
│ │ │
│ │ ModerateDiscordMemberResp │
│ │<─────────────────────────│
│ │ │
│ { success, action } │ │
│<─────────────────────────│ │

Protocol Messages

Moderation Action Enum

enum ModerationAction {
MODERATION_ACTION_UNSPECIFIED = 0;
MODERATION_ACTION_WARN = 1;
MODERATION_ACTION_KICK = 2;
MODERATION_ACTION_BAN = 3;
MODERATION_ACTION_TIMEOUT = 4;
MODERATION_ACTION_REMOVE_TIMEOUT = 5;
}

Request Message

message ModerateDiscordMemberRequest {
string request_id = 1; // Unique request ID
string guild_id = 2; // Discord guild ID (snowflake)
string user_id = 3; // Target user ID (snowflake)
ModerationAction action = 4; // Action to perform
optional string reason = 5; // Reason for the action
optional int32 duration_seconds = 6; // For timeout: duration in seconds
optional int32 delete_message_days = 7; // For ban: days of messages to delete
string actor_user_id = 8; // Heimdall user ID who initiated
}

Response Message

message ModerateDiscordMemberResponse {
string request_id = 1;
bool success = 2;
optional string error = 3;
ModerationAction action = 4;
string guild_id = 5;
string user_id = 6;
}

GraphQL API

Moderate Discord Member Mutation

mutation ModerateDiscordMember($input: ModerateDiscordMemberInput!) {
moderateDiscordMember(input: $input) {
success
error
action
}
}

Variables:

{
"input": {
"guildId": "123456789012345678",
"userId": "987654321098765432",
"action": "TIMEOUT",
"reason": "Spamming in general chat",
"durationSeconds": 3600
}
}

Input Fields

FieldTypeRequiredDescription
guildIdStringYesDiscord guild ID (snowflake)
userIdStringYesTarget Discord user ID (snowflake)
actionEnumYesModeration action to perform
reasonStringNoReason for the action
durationSecondsIntFor timeoutTimeout duration in seconds
deleteMessageDaysIntFor banDays of messages to delete (0-7)

Action Types

ActionGraphQL ValueDescription
WarnWARNSend warning DM
KickKICKKick from server
BanBANBan from server
TimeoutTIMEOUTTemporary communication disable
Remove TimeoutREMOVE_TIMEOUTRemove active timeout

Example: Timeout a User

mutation {
moderateDiscordMember(input: {
guildId: "123456789012345678"
userId: "987654321098765432"
action: TIMEOUT
reason: "Excessive caps usage"
durationSeconds: 1800 # 30 minutes
}) {
success
error
action
}
}

Example: Ban a User

mutation {
moderateDiscordMember(input: {
guildId: "123456789012345678"
userId: "987654321098765432"
action: BAN
reason: "Repeated rule violations"
deleteMessageDays: 1
}) {
success
error
action
}
}

Permissions

Permission Hierarchy

PermissionAllows
discord:guild.warnWarn action only
discord:guild.kickKick action only
discord:guild.banBan action only
discord:guild.timeoutTimeout and remove timeout actions
discord:guild.readView Discord guild data, members, channels, and roles
discord:guild.editEdit Discord guild data and member information
discord:guild.syncResync data for a specific guild (channels, roles, members)

Permission Check Flow

┌────────────────────────┐
│ Moderation Request │
└───────────┬────────────┘


┌────────────────────────┐
│ Check action-specific │
│ permission │
└───────────┬────────────┘

┌──────┴──────┐
│ │
Yes No
│ │
▼ ▼
┌─────────┐ ┌──────────────┐
│ Execute │ │ Permission │
│ Action │ │ Denied Error │
└─────────┘ └──────────────┘

Audit Logging

All moderation actions are logged to the Heimdall audit system.

Audit Event Structure

FieldDescription
event_typebot_moderation_action
actor_idHeimdall user ID (if linked) or Discord user ID
resource_typediscord_member
resource_id{guild_id}:{user_id}
descriptionHuman-readable action description
metadataJSON with action details

Example Audit Event

{
"event_type": "bot_moderation_action",
"actor_id": "heimdall-user-uuid",
"resource_type": "discord_member",
"resource_id": "123456789:987654321",
"description": "Timed out @johndoe for 1 hour in My Server",
"metadata": {
"action": "timeout",
"guild_id": "123456789012345678",
"guild_name": "My Server",
"user_id": "987654321098765432",
"username": "johndoe",
"reason": "Spamming",
"duration_seconds": 3600,
"source_service": "discord_bot"
}
}

Bot Implementation

Handling Moderation Requests

The bot handles ModerateDiscordMemberRequest messages from the WebSocket:

async fn handle_moderation_request(
http: &Http,
cache: &Cache,
request: ModerateDiscordMemberRequest,
) -> ModerateDiscordMemberResponse {
let guild_id = GuildId::new(request.guild_id.parse().unwrap());
let user_id = UserId::new(request.user_id.parse().unwrap());

let result = match request.action() {
ModerationAction::Warn => {
send_warning_dm(http, user_id, &request.reason).await
}
ModerationAction::Kick => {
guild_id.kick_with_reason(http, user_id, &request.reason).await
}
ModerationAction::Ban => {
let delete_days = request.delete_message_days.unwrap_or(0) as u8;
guild_id.ban_with_reason(http, user_id, delete_days, &request.reason).await
}
ModerationAction::Timeout => {
let duration = Duration::from_secs(request.duration_seconds.unwrap_or(3600) as u64);
let until = Utc::now() + chrono::Duration::from_std(duration).unwrap();
guild_id.edit_member(http, user_id, EditMember::new()
.disable_communication_until(until.into())
).await
}
ModerationAction::RemoveTimeout => {
guild_id.edit_member(http, user_id, EditMember::new()
.enable_communication()
).await
}
_ => Err(SerenityError::Other("Unknown action"))
};

ModerateDiscordMemberResponse {
request_id: request.request_id,
success: result.is_ok(),
error: result.err().map(|e| e.to_string()),
action: request.action,
guild_id: request.guild_id,
user_id: request.user_id,
}
}

Error Handling

Common Errors

ErrorCauseResolution
Missing PermissionsBot lacks Discord permissionsGrant bot role with required permissions
Cannot moderate higher roleTarget has equal/higher roleEnsure bot role is above target
User not foundInvalid user ID or left serverVerify user is in guild
Cannot DM userUser has DMs disabledWarn action will partially fail
Rate limitedToo many actionsAutomatic retry with backoff

Error Response Example

{
"data": {
"moderateDiscordMember": {
"success": false,
"error": "Missing Permissions: Cannot kick members with equal or higher role",
"action": "KICK"
}
}
}

Best Practices

  1. Always provide a reason - Helps with audit trails and transparency
  2. Use appropriate timeout durations - Start small, escalate if needed
  3. Delete messages sparingly - Only when necessary to remove harmful content
  4. Check member roles - Ensure bot can moderate the target
  5. Review audit logs - Monitor for abuse or mistakes