Moderation bot (Go)
Kick, ban and timeout slash commands that check both your roles and the bot's before acting.
Go 1.23
♥ 1 like
Environment variables you'll need
| Key | What it is |
|---|---|
DISCORD_TOKEN | Your bot's token from the Discord Developer Portal. Paste it on the Secrets tab; we store it encrypted. |
MOD_LOG_CHANNEL_ID | Optional. Channel where every action is logged |
Files
README.md
# Moderation bot
A Go bot built on discordgo. Your bot token is set on the Secrets tab as
DISCORD_TOKEN and reaches the bot as an environment variable.
## Commands
- `/kick member [reason]`
- `/ban member [reason] [delete_days 0-7]`
- `/timeout member minutes [reason]` (up to 28 days)
- Each command checks that you have the permission, that the target is not you, the server owner or the bot, and that your highest role and the bot's highest role are both above the target's. Server owners skip the check on their own roles.
- Server admins can change who sees the commands under Server Settings, Integrations.
## Setup
1. No privileged gateway intents are needed. Leave Message Content, Server Members
and Presence off in the Discord Developer Portal.
2. Invite the bot with the `bot` and `applications.commands` scopes and these
permissions: Kick Members, Ban Members, Moderate Members, Send Messages.
3. Set these values on the Secrets tab. The ones marked optional can stay empty:
- `MOD_LOG_CHANNEL_ID`: Optional. Channel where every action is logged.
4. Press Deploy. The console shows a line with "Logged in as" once the bot is online.
go.mod
module example.com/moderation-bot-go
go 1.23
require github.com/bwmarrin/discordgo v0.28.1
go.sum
main.go
package main
import (
"fmt"
"log"
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"time"
"github.com/bwmarrin/discordgo"
)
const maxTimeoutMinutes = 28 * 24 * 60 // Discord's limit is 28 days
var (
modLogChannelID = strings.TrimSpace(os.Getenv("MOD_LOG_CHANNEL_ID"))
digits = regexp.MustCompile(`^\d+$`)
noDM = false
zero = 0.0
one = 1.0
kickPerm int64 = discordgo.PermissionKickMembers
banPerm int64 = discordgo.PermissionBanMembers
modPerm int64 = discordgo.PermissionModerateMembers
)
type action struct {
perm int64
label string
}
var actions = map[string]action{
"kick": {kickPerm, "Kick Members"},
"ban": {banPerm, "Ban Members"},
"timeout": {modPerm, "Timeout Members"},
}
func opts(verb string, extra ...*discordgo.ApplicationCommandOption) []*discordgo.ApplicationCommandOption {
list := []*discordgo.ApplicationCommandOption{{Type: discordgo.ApplicationCommandOptionUser,
Name: "member", Description: "Member to " + verb, Required: true}}
list = append(list, extra...)
return append(list, &discordgo.ApplicationCommandOption{Type: discordgo.ApplicationCommandOptionString,
Name: "reason", Description: "Reason for the audit log"})
}
var commands = []*discordgo.ApplicationCommand{
{Name: "kick", Description: "Kick a member", DefaultMemberPermissions: &kickPerm, DMPermission: &noDM,
Options: opts("kick")},
{Name: "ban", Description: "Ban a member", DefaultMemberPermissions: &banPerm, DMPermission: &noDM,
Options: append(opts("ban"), &discordgo.ApplicationCommandOption{Type: discordgo.ApplicationCommandOptionInteger,
Name: "delete_days", Description: "Days of their messages to delete (0 to 7)", MinValue: &zero, MaxValue: 7})},
{Name: "timeout", Description: "Time out a member", DefaultMemberPermissions: &modPerm, DMPermission: &noDM,
Options: opts("time out", &discordgo.ApplicationCommandOption{Type: discordgo.ApplicationCommandOptionInteger,
Name: "minutes", Description: "Length in minutes (max 28 days)", Required: true,
MinValue: &one, MaxValue: maxTimeoutMinutes})},
}
func reply(s *discordgo.Session, i *discordgo.InteractionCreate, content string) error {
return s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{Content: content, Flags: discordgo.MessageFlagsEphemeral}})
}
func highest(roles []string, positions map[string]int) int {
top := 0 // @everyone
for _, id := range roles {
if p, ok := positions[id]; ok && p > top {
top = p
}
}
return top
}
// checkTarget returns an error message, or "" when the action is allowed.
func checkTarget(s *discordgo.Session, i *discordgo.InteractionCreate, target *discordgo.Member, targetID string, a action) string {
actor := i.Member
if actor.Permissions&(a.perm|discordgo.PermissionAdministrator) == 0 {
return "You need the " + a.label + " permission to do that."
}
guild, err := s.State.Guild(i.GuildID)
if err != nil {
return "I could not load this server. Try again in a moment."
}
botID := s.State.User.ID
if perms, err := s.State.UserChannelPermissions(botID, i.ChannelID); err == nil &&
perms&(a.perm|discordgo.PermissionAdministrator) == 0 {
return "I need the " + a.label + " permission to do that."
}
switch {
case target == nil:
return "That user is not a member of this server."
case targetID == actor.User.ID:
return "You cannot use this on yourself."
case targetID == guild.OwnerID:
return "You cannot use this on the server owner."
case targetID == botID:
return "I cannot use this on myself."
}
positions := map[string]int{}
for _, r := range guild.Roles {
positions[r.ID] = r.Position
}
targetTop := highest(target.Roles, positions)
if actor.User.ID != guild.OwnerID && highest(actor.Roles, positions) <= targetTop {
return "Your highest role must be above the target's hig