Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
483dc666d1 | ||
|
|
3585fa0658 | ||
|
|
d1be60e201 | ||
|
|
26e1d9b094 | ||
|
|
461f8435c0 | ||
|
|
1b47bd13a3 | ||
|
|
035e383db2 | ||
|
|
2b14079e50 | ||
|
|
a92c4131db |
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
*.md
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# Build stage
|
||||
FROM golang:1.26.1-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /velox-bot .
|
||||
|
||||
# Run stage
|
||||
FROM alpine:3.19
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /velox-bot .
|
||||
|
||||
ENTRYPOINT ["./velox-bot"]
|
||||
@@ -10,8 +10,12 @@ It currently includes:
|
||||
- Scheduling (1:1 sessions with DM invites + reaction-based accept/decline/reschedule)
|
||||
- Projects (list/create projects + helpers)
|
||||
- Music (Lavalink-powered `/play`, `/queue`, `/volume`)
|
||||
- Twitch live notifications (per-guild `/config` setup)
|
||||
- Welcome messages, DMs, and default role assignment (per-guild `/config` setup)
|
||||
- Twitch live notifications (per-guild `/config` setup)
|
||||
- Welcome messages, DMs, and default role assignment (per-guild `/config` setup)
|
||||
- Focused moderation tools (`/moderation purge|timeout|untimeout|kick|ban|unban|slowmode`)
|
||||
- Server logging system (edits/deletes/joins/leaves/roles/ban/kick/mod actions)
|
||||
|
||||
When the bot is added to a new server for the first time, toggleable systems are defaulted to **disabled** and a setup DM is sent to the inviter (or server owner fallback) with enable instructions.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -96,6 +100,14 @@ just down # stop postgres
|
||||
- `/projects list`, `/projects create`, `/projects add-helper`
|
||||
- **Music** (requires **DJ** role)
|
||||
- `/play`, `/queue`, `/volume`
|
||||
- **Moderation** (focused + practical)
|
||||
- `/moderation purge amount:<1-100>` — Fast recent message cleanup (**Manage Messages**)
|
||||
- `/moderation timeout user:@someone duration:<10m|1h|...> [reason]` — Temporary member timeout (**Moderate Members**)
|
||||
- `/moderation kick user:@someone [reason]` — Kick member (**Kick Members**)
|
||||
- `/moderation ban user:@someone [delete_days] [reason]` — Ban member (**Ban Members**)
|
||||
- `/moderation unban user_id:<id> [reason]` — Unban by user ID (**Ban Members**)
|
||||
- `/moderation untimeout user:@someone [reason]` — Remove timeout (**Moderate Members**)
|
||||
- `/moderation slowmode channel:#channel seconds:<0-21600>` — Control channel flood (**Manage Channels**)
|
||||
- **Twitch live notifications** (**Manage Server**)
|
||||
- `/config addstreamer username:<twitch-name>` — Add a streamer for notifications
|
||||
- `/config removestreamer username:<twitch-name>` — Remove a streamer
|
||||
@@ -106,6 +118,10 @@ just down # stop postgres
|
||||
- `/config setwelcomedm message:"..."` — DM welcome template (supports `{user}`, `{server}`)
|
||||
- `/config setwelcomegif url:<image-url>` — GIF/image used in welcome embed
|
||||
- `/config setdefaultrole role:@role` — Role automatically granted on join
|
||||
- **Logging** (**Manage Server**)
|
||||
- `/config setlogchannel channel:#channel` — Set where logs are sent
|
||||
- `/config enablelogging` — Enable event/moderation logs
|
||||
- `/config disablelogging` — Disable event/moderation logs
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
+19
-2
@@ -19,5 +19,22 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
velox_pgdata:
|
||||
bot:
|
||||
build: .
|
||||
container_name: velox-bot
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
DB_HOST: ${DB_HOST:-postgres://velox:velox_pwd@db:5432/velox?sslmode=disable}
|
||||
LAVALINK_HOST: ${LAVALINK_HOST:-ws://lavalink:2333}
|
||||
LAVALINK_PASSWORD: ${LAVALINK_PASSWORD:-youshallnotpass}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
- lavalink
|
||||
|
||||
networks:
|
||||
lavalink:
|
||||
external: true
|
||||
@@ -34,8 +34,10 @@ func NewBot(token, appID, guildID, lavalinkHost, lavalinkPass string, cmds []*di
|
||||
// - VoiceStateUpdate (meeting lobby): GuildVoiceStates
|
||||
// - MessageReactionAdd (scheduling via reactions): GuildMessageReactions + DirectMessageReactions
|
||||
session.Identify.Intents = discordgo.IntentsGuilds |
|
||||
discordgo.IntentsGuildBans |
|
||||
discordgo.IntentsGuildMembers |
|
||||
discordgo.IntentsGuildMessages |
|
||||
discordgo.IntentsMessageContent |
|
||||
discordgo.IntentsGuildVoiceStates |
|
||||
discordgo.IntentsGuildMessageReactions |
|
||||
discordgo.IntentsDirectMessages |
|
||||
@@ -76,7 +78,39 @@ func (b *Bot) Start() error {
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
|
||||
events.HandleBotAddedOnGuild(s, m, b.Services)
|
||||
|
||||
// Ignore bot members for welcome/log onboarding of normal users.
|
||||
if m != nil && m.Member != nil && m.User != nil && m.User.Bot {
|
||||
return
|
||||
}
|
||||
|
||||
events.HandleGuildMemberAdd(s, m, b.Services)
|
||||
events.HandleGuildMemberAddLog(s, m, b.Services)
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
|
||||
events.HandleGuildMemberRemoveLog(s, m, b.Services)
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
|
||||
events.HandleGuildMemberUpdateLog(s, m, b.Services)
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageUpdate) {
|
||||
events.HandleMessageUpdateLog(s, m, b.Services)
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageDelete) {
|
||||
events.HandleMessageDeleteLog(s, m, b.Services)
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.GuildBanAdd) {
|
||||
events.HandleGuildBanAddLog(s, m, b.Services)
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.GuildBanRemove) {
|
||||
events.HandleGuildBanRemoveLog(s, m, b.Services)
|
||||
})
|
||||
|
||||
b.Session.AddHandler(func(s *discordgo.Session, vs *discordgo.VoiceStateUpdate) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"velox-bot/internal/commands/help"
|
||||
cmdlevel "velox-bot/internal/commands/level"
|
||||
"velox-bot/internal/commands/meeting"
|
||||
"velox-bot/internal/commands/moderation"
|
||||
cmdmusic "velox-bot/internal/commands/music"
|
||||
"velox-bot/internal/commands/projects"
|
||||
"velox-bot/internal/commands/schedule"
|
||||
@@ -32,6 +33,7 @@ var AllCommands = []*discordgo.ApplicationCommand{
|
||||
cmdmusic.Queue,
|
||||
cmdmusic.Volume,
|
||||
meeting.Meeting,
|
||||
moderation.Moderation,
|
||||
timezone.Timezone,
|
||||
schedule.Schedule,
|
||||
projects.Projects,
|
||||
@@ -52,6 +54,7 @@ var handlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCre
|
||||
"leaderboard": cmdlevel.LeaderboardHandler,
|
||||
"rewards": cmdlevel.RewardsHandler,
|
||||
"meeting": meeting.MeetingHandler,
|
||||
"moderation": moderation.ModerationHandler,
|
||||
"timezone": timezone.TimezoneHandler,
|
||||
"schedule": schedule.ScheduleHandler,
|
||||
"projects": projects.ProjectsHandler,
|
||||
|
||||
@@ -121,6 +121,30 @@ var Config = &discordgo.ApplicationCommand{
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "setlogchannel",
|
||||
Description: "Set the moderation log channel",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
Name: "channel",
|
||||
Description: "Text channel for logs",
|
||||
Required: true,
|
||||
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "enablelogging",
|
||||
Description: "Enable server logging events",
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "disablelogging",
|
||||
Description: "Disable server logging events",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -169,6 +193,15 @@ func ConfigHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
case "setdefaultrole":
|
||||
log.Printf("defaultrole: /config setdefaultrole invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
|
||||
handleSetDefaultRole(s, i)
|
||||
case "setlogchannel":
|
||||
log.Printf("log: /config setlogchannel invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
|
||||
handleSetLogChannel(s, i)
|
||||
case "enablelogging":
|
||||
log.Printf("log: /config enablelogging invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
|
||||
handleSetLoggingEnabled(s, i, true)
|
||||
case "disablelogging":
|
||||
log.Printf("log: /config disablelogging invoked by %s in guild %s", i.Member.User.ID, i.GuildID)
|
||||
handleSetLoggingEnabled(s, i, false)
|
||||
default:
|
||||
respondEphemeral(s, i, "Unknown subcommand.")
|
||||
}
|
||||
@@ -460,6 +493,79 @@ func handleSetDefaultRole(s *discordgo.Session, i *discordgo.InteractionCreate)
|
||||
respondEphemeral(s, i, "Default role set to "+role.Mention()+".")
|
||||
}
|
||||
|
||||
func handleSetLogChannel(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if services.Global.LogSettings == nil {
|
||||
respondEphemeral(s, i, "Log settings service is not available.")
|
||||
return
|
||||
}
|
||||
|
||||
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
|
||||
if err != nil {
|
||||
respondEphemeral(s, i, "Invalid guild ID.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var chOpt *discordgo.ApplicationCommandInteractionDataOption
|
||||
for _, o := range opt.Options {
|
||||
if o.Name == "channel" {
|
||||
chOpt = o
|
||||
break
|
||||
}
|
||||
}
|
||||
if chOpt == nil {
|
||||
respondEphemeral(s, i, "Missing channel option.")
|
||||
return
|
||||
}
|
||||
|
||||
ch := chOpt.ChannelValue(s)
|
||||
if ch == nil {
|
||||
respondEphemeral(s, i, "Invalid channel.")
|
||||
return
|
||||
}
|
||||
|
||||
channelID, err := strconv.ParseInt(ch.ID, 10, 64)
|
||||
if err != nil {
|
||||
respondEphemeral(s, i, "Invalid channel ID.")
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.Global.LogSettings.SetChannel(context.Background(), guildID, channelID); err != nil {
|
||||
respondEphemeral(s, i, "Failed to set log channel.")
|
||||
return
|
||||
}
|
||||
if err := services.Global.LogSettings.SetEnabled(context.Background(), guildID, true); err != nil {
|
||||
respondEphemeral(s, i, "Log channel set, but failed to enable logging.")
|
||||
return
|
||||
}
|
||||
|
||||
respondEphemeral(s, i, "Logging enabled in "+ch.Mention()+".")
|
||||
}
|
||||
|
||||
func handleSetLoggingEnabled(s *discordgo.Session, i *discordgo.InteractionCreate, enabled bool) {
|
||||
if services.Global.LogSettings == nil {
|
||||
respondEphemeral(s, i, "Log settings service is not available.")
|
||||
return
|
||||
}
|
||||
|
||||
guildID, err := strconv.ParseInt(i.GuildID, 10, 64)
|
||||
if err != nil {
|
||||
respondEphemeral(s, i, "Invalid guild ID.")
|
||||
return
|
||||
}
|
||||
|
||||
if err := services.Global.LogSettings.SetEnabled(context.Background(), guildID, enabled); err != nil {
|
||||
respondEphemeral(s, i, "Failed to update logging state.")
|
||||
return
|
||||
}
|
||||
|
||||
if enabled {
|
||||
respondEphemeral(s, i, "Logging is now enabled.")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, "Logging is now disabled.")
|
||||
}
|
||||
|
||||
func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
|
||||
@@ -94,6 +94,17 @@ func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
"`/meeting invite user:@someone` — Allow a user to join\n" +
|
||||
"`/meeting uninvite user:@someone` — Remove a user's permission",
|
||||
},
|
||||
{
|
||||
Name: "Moderation (focused tools)",
|
||||
Value: "" +
|
||||
"`/moderation purge amount:<1-100>` — Delete recent messages (**Manage Messages**)\n" +
|
||||
"`/moderation timeout user:@someone duration:<10m|1h|...> [reason]` — Timeout member (**Moderate Members**)\n" +
|
||||
"`/moderation kick user:@someone [reason]` — Kick member (**Kick Members**)\n" +
|
||||
"`/moderation ban user:@someone [delete_days] [reason]` — Ban member (**Ban Members**)\n" +
|
||||
"`/moderation unban user_id:<id> [reason]` — Unban by user ID (**Ban Members**)\n" +
|
||||
"`/moderation untimeout user:@someone [reason]` — Remove timeout (**Moderate Members**)\n" +
|
||||
"`/moderation slowmode channel:#channel seconds:<0-21600>` — Set slowmode (**Manage Channels**)",
|
||||
},
|
||||
{
|
||||
Name: "Leveling",
|
||||
Value: "" +
|
||||
@@ -122,6 +133,13 @@ func HelpHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
"`/config setwelcomegif url:<image-url>` — GIF/image used in welcome embed\n" +
|
||||
"`/config setdefaultrole role:@role` — Role granted to new members",
|
||||
},
|
||||
{
|
||||
Name: "Logging (**Manage Server**)",
|
||||
Value: "" +
|
||||
"`/config setlogchannel channel:#channel` — Set log output channel\n" +
|
||||
"`/config enablelogging` — Enable event/moderation logging\n" +
|
||||
"`/config disablelogging` — Disable event/moderation logging",
|
||||
},
|
||||
{
|
||||
Name: "Other",
|
||||
Value: "`/help` — Show this help in DMs",
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"velox-bot/internal/db/services"
|
||||
"velox-bot/internal/modlog"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var Moderation = &discordgo.ApplicationCommand{
|
||||
Name: "moderation",
|
||||
Description: "High-value moderation tools",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "purge",
|
||||
Description: "Delete recent messages in this channel (max 100)",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionInteger,
|
||||
Name: "amount",
|
||||
Description: "How many recent messages to delete (1-100)",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "timeout",
|
||||
Description: "Temporarily timeout a member",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionUser,
|
||||
Name: "user",
|
||||
Description: "User to timeout",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "duration",
|
||||
Description: "Duration like 10m, 1h, 2h30m (max 28d)",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "reason",
|
||||
Description: "Optional reason",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "kick",
|
||||
Description: "Kick a member from the server",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionUser,
|
||||
Name: "user",
|
||||
Description: "User to kick",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "reason",
|
||||
Description: "Optional reason",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "ban",
|
||||
Description: "Ban a member from the server",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionUser,
|
||||
Name: "user",
|
||||
Description: "User to ban",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionInteger,
|
||||
Name: "delete_days",
|
||||
Description: "Delete message history (0-7 days)",
|
||||
Required: false,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "reason",
|
||||
Description: "Optional reason",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "unban",
|
||||
Description: "Unban a user by ID",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "user_id",
|
||||
Description: "User ID to unban",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "reason",
|
||||
Description: "Optional reason",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "untimeout",
|
||||
Description: "Remove timeout from a member",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionUser,
|
||||
Name: "user",
|
||||
Description: "User to untimeout",
|
||||
Required: true,
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionString,
|
||||
Name: "reason",
|
||||
Description: "Optional reason",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionSubCommand,
|
||||
Name: "slowmode",
|
||||
Description: "Set channel slowmode (0 disables)",
|
||||
Options: []*discordgo.ApplicationCommandOption{
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionChannel,
|
||||
Name: "channel",
|
||||
Description: "Text channel to update",
|
||||
Required: true,
|
||||
ChannelTypes: []discordgo.ChannelType{discordgo.ChannelTypeGuildText, discordgo.ChannelTypeGuildNews},
|
||||
},
|
||||
{
|
||||
Type: discordgo.ApplicationCommandOptionInteger,
|
||||
Name: "seconds",
|
||||
Description: "Slowmode delay in seconds (0-21600)",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func ModerationHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if i.GuildID == "" {
|
||||
respondEphemeral(s, i, "This command can only be used in a server.")
|
||||
return
|
||||
}
|
||||
|
||||
data := i.ApplicationCommandData()
|
||||
if len(data.Options) == 0 {
|
||||
respondEphemeral(s, i, "Missing subcommand.")
|
||||
return
|
||||
}
|
||||
|
||||
switch data.Options[0].Name {
|
||||
case "purge":
|
||||
handlePurge(s, i)
|
||||
case "timeout":
|
||||
handleTimeout(s, i)
|
||||
case "kick":
|
||||
handleKick(s, i)
|
||||
case "ban":
|
||||
handleBan(s, i)
|
||||
case "unban":
|
||||
handleUnban(s, i)
|
||||
case "untimeout":
|
||||
handleUntimeout(s, i)
|
||||
case "slowmode":
|
||||
handleSlowmode(s, i)
|
||||
default:
|
||||
respondEphemeral(s, i, "Unknown subcommand.")
|
||||
}
|
||||
}
|
||||
|
||||
func handlePurge(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !hasPerm(i, discordgo.PermissionManageMessages) {
|
||||
respondEphemeral(s, i, "You need **Manage Messages** for this command.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var amount int64
|
||||
for _, o := range opt.Options {
|
||||
if o.Name == "amount" {
|
||||
amount = o.IntValue()
|
||||
break
|
||||
}
|
||||
}
|
||||
if amount < 1 || amount > 100 {
|
||||
respondEphemeral(s, i, "Amount must be between 1 and 100.")
|
||||
return
|
||||
}
|
||||
|
||||
msgs, err := s.ChannelMessages(i.ChannelID, int(amount), "", "", "")
|
||||
if err != nil {
|
||||
respondEphemeral(s, i, "Failed to fetch messages.")
|
||||
return
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
respondEphemeral(s, i, "No messages found to delete.")
|
||||
return
|
||||
}
|
||||
|
||||
tooOldCutoff := time.Now().Add(-14 * 24 * time.Hour)
|
||||
ids := make([]string, 0, len(msgs))
|
||||
for _, m := range msgs {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
if m.Timestamp.Before(tooOldCutoff) {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, m.ID)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
respondEphemeral(s, i, "No messages can be bulk deleted (older than 14 days).")
|
||||
return
|
||||
}
|
||||
|
||||
if len(ids) == 1 {
|
||||
if err := s.ChannelMessageDelete(i.ChannelID, ids[0]); err != nil {
|
||||
respondEphemeral(s, i, "Failed to delete message.")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, "Deleted 1 message.")
|
||||
sendModerationActionLog(s, i, "Purge", fmt.Sprintf("Deleted 1 message in <#%s>.", i.ChannelID), "")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.ChannelMessagesBulkDelete(i.ChannelID, ids); err != nil {
|
||||
respondEphemeral(s, i, "Failed to bulk delete messages.")
|
||||
return
|
||||
}
|
||||
|
||||
respondEphemeral(s, i, fmt.Sprintf("Deleted %d messages.", len(ids)))
|
||||
sendModerationActionLog(s, i, "Purge", fmt.Sprintf("Deleted %d messages in <#%s>.", len(ids), i.ChannelID), "")
|
||||
}
|
||||
|
||||
func handleTimeout(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !hasPerm(i, discordgo.PermissionModerateMembers) {
|
||||
respondEphemeral(s, i, "You need **Moderate Members** for this command.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
||||
var durationInput string
|
||||
var reason string
|
||||
for _, o := range opt.Options {
|
||||
switch o.Name {
|
||||
case "user":
|
||||
userOpt = o
|
||||
case "duration":
|
||||
durationInput = strings.TrimSpace(o.StringValue())
|
||||
case "reason":
|
||||
reason = strings.TrimSpace(o.StringValue())
|
||||
}
|
||||
}
|
||||
if userOpt == nil {
|
||||
respondEphemeral(s, i, "Missing user option.")
|
||||
return
|
||||
}
|
||||
target := userOpt.UserValue(s)
|
||||
if target == nil {
|
||||
respondEphemeral(s, i, "Invalid user.")
|
||||
return
|
||||
}
|
||||
|
||||
dur, err := time.ParseDuration(durationInput)
|
||||
if err != nil || dur <= 0 {
|
||||
respondEphemeral(s, i, "Invalid duration. Examples: `10m`, `1h`, `2h30m`.")
|
||||
return
|
||||
}
|
||||
maxTimeout := 28 * 24 * time.Hour
|
||||
if dur > maxTimeout {
|
||||
respondEphemeral(s, i, "Duration cannot exceed 28 days.")
|
||||
return
|
||||
}
|
||||
|
||||
until := time.Now().UTC().Add(dur)
|
||||
if err := s.GuildMemberTimeout(i.GuildID, target.ID, &until); err != nil {
|
||||
respondEphemeral(s, i, "Failed to timeout user. Check role hierarchy and permissions.")
|
||||
return
|
||||
}
|
||||
|
||||
if reason == "" {
|
||||
respondEphemeral(s, i, fmt.Sprintf("Timed out %s for `%s`.", target.Mention(), dur.String()))
|
||||
sendModerationActionLog(s, i, "Timeout", fmt.Sprintf("Target: %s\nDuration: `%s`", target.Mention(), dur.String()), "")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, fmt.Sprintf("Timed out %s for `%s`. Reason: %s", target.Mention(), dur.String(), reason))
|
||||
sendModerationActionLog(s, i, "Timeout", fmt.Sprintf("Target: %s\nDuration: `%s`", target.Mention(), dur.String()), reason)
|
||||
}
|
||||
|
||||
func handleKick(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !hasPerm(i, discordgo.PermissionKickMembers) {
|
||||
respondEphemeral(s, i, "You need **Kick Members** for this command.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
||||
var reason string
|
||||
for _, o := range opt.Options {
|
||||
switch o.Name {
|
||||
case "user":
|
||||
userOpt = o
|
||||
case "reason":
|
||||
reason = strings.TrimSpace(o.StringValue())
|
||||
}
|
||||
}
|
||||
if userOpt == nil {
|
||||
respondEphemeral(s, i, "Missing user option.")
|
||||
return
|
||||
}
|
||||
target := userOpt.UserValue(s)
|
||||
if target == nil {
|
||||
respondEphemeral(s, i, "Invalid user.")
|
||||
return
|
||||
}
|
||||
if i.Member != nil && i.Member.User != nil && target.ID == i.Member.User.ID {
|
||||
respondEphemeral(s, i, "You cannot kick yourself.")
|
||||
return
|
||||
}
|
||||
if s != nil && s.State != nil && s.State.User != nil && target.ID == s.State.User.ID {
|
||||
respondEphemeral(s, i, "I can't kick myself.")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.GuildMemberDeleteWithReason(i.GuildID, target.ID, reason); err != nil {
|
||||
respondEphemeral(s, i, "Failed to kick user. Check role hierarchy and permissions.")
|
||||
return
|
||||
}
|
||||
|
||||
if reason == "" {
|
||||
respondEphemeral(s, i, "Kicked "+target.Mention()+".")
|
||||
sendModerationActionLog(s, i, "Kick", "Target: "+target.Mention(), "")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, "Kicked "+target.Mention()+". Reason: "+reason)
|
||||
sendModerationActionLog(s, i, "Kick", "Target: "+target.Mention(), reason)
|
||||
}
|
||||
|
||||
func handleBan(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !hasPerm(i, discordgo.PermissionBanMembers) {
|
||||
respondEphemeral(s, i, "You need **Ban Members** for this command.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
||||
var deleteDays int64
|
||||
var reason string
|
||||
for _, o := range opt.Options {
|
||||
switch o.Name {
|
||||
case "user":
|
||||
userOpt = o
|
||||
case "delete_days":
|
||||
deleteDays = o.IntValue()
|
||||
case "reason":
|
||||
reason = strings.TrimSpace(o.StringValue())
|
||||
}
|
||||
}
|
||||
if userOpt == nil {
|
||||
respondEphemeral(s, i, "Missing user option.")
|
||||
return
|
||||
}
|
||||
target := userOpt.UserValue(s)
|
||||
if target == nil {
|
||||
respondEphemeral(s, i, "Invalid user.")
|
||||
return
|
||||
}
|
||||
if deleteDays < 0 || deleteDays > 7 {
|
||||
respondEphemeral(s, i, "delete_days must be between 0 and 7.")
|
||||
return
|
||||
}
|
||||
if i.Member != nil && i.Member.User != nil && target.ID == i.Member.User.ID {
|
||||
respondEphemeral(s, i, "You cannot ban yourself.")
|
||||
return
|
||||
}
|
||||
if s != nil && s.State != nil && s.State.User != nil && target.ID == s.State.User.ID {
|
||||
respondEphemeral(s, i, "I can't ban myself.")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.GuildBanCreateWithReason(i.GuildID, target.ID, reason, int(deleteDays)); err != nil {
|
||||
respondEphemeral(s, i, "Failed to ban user. Check role hierarchy and permissions.")
|
||||
return
|
||||
}
|
||||
|
||||
if reason == "" {
|
||||
respondEphemeral(s, i, fmt.Sprintf("Banned %s (deleted %d days of messages).", target.Mention(), deleteDays))
|
||||
sendModerationActionLog(s, i, "Ban", fmt.Sprintf("Target: %s\nDeleted message days: %d", target.Mention(), deleteDays), "")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, fmt.Sprintf("Banned %s (deleted %d days of messages). Reason: %s", target.Mention(), deleteDays, reason))
|
||||
sendModerationActionLog(s, i, "Ban", fmt.Sprintf("Target: %s\nDeleted message days: %d", target.Mention(), deleteDays), reason)
|
||||
}
|
||||
|
||||
func handleUnban(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !hasPerm(i, discordgo.PermissionBanMembers) {
|
||||
respondEphemeral(s, i, "You need **Ban Members** for this command.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var userID string
|
||||
var reason string
|
||||
for _, o := range opt.Options {
|
||||
switch o.Name {
|
||||
case "user_id":
|
||||
userID = strings.TrimSpace(o.StringValue())
|
||||
case "reason":
|
||||
reason = strings.TrimSpace(o.StringValue())
|
||||
}
|
||||
}
|
||||
if userID == "" {
|
||||
respondEphemeral(s, i, "Missing user ID.")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.GuildBanDelete(i.GuildID, userID); err != nil {
|
||||
respondEphemeral(s, i, "Failed to unban user. Make sure the user ID is valid and banned.")
|
||||
return
|
||||
}
|
||||
|
||||
if reason == "" {
|
||||
respondEphemeral(s, i, "Unbanned user ID `"+userID+"`.")
|
||||
sendModerationActionLog(s, i, "Unban", "Target user ID: `"+userID+"`", "")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, "Unbanned user ID `"+userID+"`. Reason: "+reason)
|
||||
sendModerationActionLog(s, i, "Unban", "Target user ID: `"+userID+"`", reason)
|
||||
}
|
||||
|
||||
func handleUntimeout(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !hasPerm(i, discordgo.PermissionModerateMembers) {
|
||||
respondEphemeral(s, i, "You need **Moderate Members** for this command.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var userOpt *discordgo.ApplicationCommandInteractionDataOption
|
||||
var reason string
|
||||
for _, o := range opt.Options {
|
||||
switch o.Name {
|
||||
case "user":
|
||||
userOpt = o
|
||||
case "reason":
|
||||
reason = strings.TrimSpace(o.StringValue())
|
||||
}
|
||||
}
|
||||
if userOpt == nil {
|
||||
respondEphemeral(s, i, "Missing user option.")
|
||||
return
|
||||
}
|
||||
target := userOpt.UserValue(s)
|
||||
if target == nil {
|
||||
respondEphemeral(s, i, "Invalid user.")
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.GuildMemberTimeout(i.GuildID, target.ID, nil); err != nil {
|
||||
respondEphemeral(s, i, "Failed to remove timeout. Check role hierarchy and permissions.")
|
||||
return
|
||||
}
|
||||
|
||||
if reason == "" {
|
||||
respondEphemeral(s, i, "Removed timeout from "+target.Mention()+".")
|
||||
sendModerationActionLog(s, i, "Timeout Removed", "Target: "+target.Mention(), "")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, "Removed timeout from "+target.Mention()+". Reason: "+reason)
|
||||
sendModerationActionLog(s, i, "Timeout Removed", "Target: "+target.Mention(), reason)
|
||||
}
|
||||
|
||||
func handleSlowmode(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
if !hasPerm(i, discordgo.PermissionManageChannels) {
|
||||
respondEphemeral(s, i, "You need **Manage Channels** for this command.")
|
||||
return
|
||||
}
|
||||
|
||||
opt := i.ApplicationCommandData().Options[0]
|
||||
var chOpt *discordgo.ApplicationCommandInteractionDataOption
|
||||
var seconds int64
|
||||
for _, o := range opt.Options {
|
||||
switch o.Name {
|
||||
case "channel":
|
||||
chOpt = o
|
||||
case "seconds":
|
||||
seconds = o.IntValue()
|
||||
}
|
||||
}
|
||||
if chOpt == nil {
|
||||
respondEphemeral(s, i, "Missing channel option.")
|
||||
return
|
||||
}
|
||||
if seconds < 0 || seconds > 21600 {
|
||||
respondEphemeral(s, i, "Seconds must be between 0 and 21600.")
|
||||
return
|
||||
}
|
||||
ch := chOpt.ChannelValue(s)
|
||||
if ch == nil {
|
||||
respondEphemeral(s, i, "Invalid channel.")
|
||||
return
|
||||
}
|
||||
|
||||
secondsInt := int(seconds)
|
||||
_, err := s.ChannelEditComplex(ch.ID, &discordgo.ChannelEdit{
|
||||
RateLimitPerUser: &secondsInt,
|
||||
})
|
||||
if err != nil {
|
||||
respondEphemeral(s, i, "Failed to update slowmode.")
|
||||
return
|
||||
}
|
||||
|
||||
if seconds == 0 {
|
||||
respondEphemeral(s, i, "Disabled slowmode in "+ch.Mention()+".")
|
||||
sendModerationActionLog(s, i, "Slowmode", "Channel: "+ch.Mention()+"\nSet to: `0s`", "")
|
||||
return
|
||||
}
|
||||
respondEphemeral(s, i, fmt.Sprintf("Set slowmode in %s to %d seconds.", ch.Mention(), seconds))
|
||||
sendModerationActionLog(s, i, "Slowmode", fmt.Sprintf("Channel: %s\nSet to: `%ds`", ch.Mention(), seconds), "")
|
||||
}
|
||||
|
||||
func hasPerm(i *discordgo.InteractionCreate, perm int64) bool {
|
||||
return i.Member != nil && (i.Member.Permissions&perm) == perm
|
||||
}
|
||||
|
||||
func respondEphemeral(s *discordgo.Session, i *discordgo.InteractionCreate, msg string) {
|
||||
_ = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
||||
Data: &discordgo.InteractionResponseData{
|
||||
Content: msg,
|
||||
Flags: discordgo.MessageFlagsEphemeral,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func sendModerationActionLog(s *discordgo.Session, i *discordgo.InteractionCreate, action, details, reason string) {
|
||||
if services.Global == nil {
|
||||
return
|
||||
}
|
||||
moderator := "Unknown"
|
||||
if i != nil && i.Member != nil && i.Member.User != nil {
|
||||
moderator = i.Member.User.Mention()
|
||||
}
|
||||
desc := "Moderator: " + moderator + "\n" + details
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
desc += "\nReason: " + reason
|
||||
}
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Moderation Action: " + action,
|
||||
Color: 0x5865F2,
|
||||
Description: desc,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
_ = modlog.Send(s, services.Global, i.GuildID, embed)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package moderation
|
||||
|
||||
import (
|
||||
"velox-bot/internal/commands/moderation/public"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
var (
|
||||
Moderation *discordgo.ApplicationCommand = public.Moderation
|
||||
)
|
||||
|
||||
func ModerationHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
||||
public.ModerationHandler(s, i)
|
||||
}
|
||||
|
||||
@@ -8,19 +8,18 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
BotToken string
|
||||
AppID string
|
||||
GuildID string
|
||||
DBHost string
|
||||
LavalinkHost string
|
||||
LavalinkPass string
|
||||
BotToken string
|
||||
AppID string
|
||||
GuildID string
|
||||
DBHost string
|
||||
LavalinkHost string
|
||||
LavalinkPass string
|
||||
TwitchClientID string
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
if err := godotenv.Load(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Load .env if present (e.g. local dev). Ignore if missing (e.g. Docker injects env vars).
|
||||
_ = godotenv.Load()
|
||||
|
||||
token := os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
@@ -33,9 +32,6 @@ func LoadConfig() (*Config, error) {
|
||||
}
|
||||
|
||||
guildID := os.Getenv("GUILD_ID")
|
||||
if guildID == "" {
|
||||
return nil, fmt.Errorf("GUILD_ID is not set")
|
||||
}
|
||||
|
||||
dbHost := os.Getenv("DB_HOST")
|
||||
if dbHost == "" {
|
||||
|
||||
@@ -15,6 +15,12 @@ type LevelReward struct {
|
||||
RoleID int64
|
||||
}
|
||||
|
||||
type LogSettings struct {
|
||||
GuildID int64
|
||||
ChannelID int64
|
||||
IsEnabled bool
|
||||
}
|
||||
|
||||
func NewRepo(db *sql.DB) *Repo {
|
||||
return &Repo{db: db}
|
||||
}
|
||||
@@ -240,6 +246,80 @@ func (r *Repo) SetLevelUpMessage(ctx context.Context, guildID int64, message str
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) SetLogChannel(ctx context.Context, guildID int64, channelID int64) error {
|
||||
const q = `
|
||||
INSERT INTO logsettings (guild_id, log_channel_id, is_enabled)
|
||||
VALUES ($1, $2, TRUE)
|
||||
ON CONFLICT (guild_id)
|
||||
DO UPDATE SET log_channel_id = EXCLUDED.log_channel_id
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, guildID, channelID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) SetLogEnabled(ctx context.Context, guildID int64, enabled bool) error {
|
||||
const q = `
|
||||
INSERT INTO logsettings (guild_id, is_enabled)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (guild_id)
|
||||
DO UPDATE SET is_enabled = EXCLUDED.is_enabled
|
||||
`
|
||||
_, err := r.db.ExecContext(ctx, q, guildID, enabled)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Repo) GetLogSettings(ctx context.Context, guildID int64) (*LogSettings, error) {
|
||||
const q = `
|
||||
SELECT log_channel_id, is_enabled
|
||||
FROM logsettings
|
||||
WHERE guild_id = $1
|
||||
`
|
||||
row := r.db.QueryRowContext(ctx, q, guildID)
|
||||
var channelID sql.NullInt64
|
||||
var enabled sql.NullBool
|
||||
switch err := row.Scan(&channelID, &enabled); err {
|
||||
case nil:
|
||||
out := &LogSettings{
|
||||
GuildID: guildID,
|
||||
ChannelID: 0,
|
||||
IsEnabled: false,
|
||||
}
|
||||
if channelID.Valid {
|
||||
out.ChannelID = channelID.Int64
|
||||
}
|
||||
if enabled.Valid {
|
||||
out.IsEnabled = enabled.Bool
|
||||
}
|
||||
return out, nil
|
||||
case sql.ErrNoRows:
|
||||
return &LogSettings{
|
||||
GuildID: guildID,
|
||||
ChannelID: 0,
|
||||
IsEnabled: false,
|
||||
}, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) HasLogSettings(ctx context.Context, guildID int64) (bool, error) {
|
||||
const q = `
|
||||
SELECT 1
|
||||
FROM logsettings
|
||||
WHERE guild_id = $1
|
||||
`
|
||||
row := r.db.QueryRowContext(ctx, q, guildID)
|
||||
var one int
|
||||
switch err := row.Scan(&one); err {
|
||||
case nil:
|
||||
return true, nil
|
||||
case sql.ErrNoRows:
|
||||
return false, nil
|
||||
default:
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) GetLevelUpMessage(ctx context.Context, guildID int64) (string, error) {
|
||||
const q = `
|
||||
SELECT message
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package logsettings
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"velox-bot/internal/db/repos/settingsrepo"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
settings *settingsrepo.Repo
|
||||
}
|
||||
|
||||
func New(settings *settingsrepo.Repo) *Service {
|
||||
return &Service{settings: settings}
|
||||
}
|
||||
|
||||
func (s *Service) SetChannel(ctx context.Context, guildID, channelID int64) error {
|
||||
return s.settings.SetLogChannel(ctx, guildID, channelID)
|
||||
}
|
||||
|
||||
func (s *Service) SetEnabled(ctx context.Context, guildID int64, enabled bool) error {
|
||||
return s.settings.SetLogEnabled(ctx, guildID, enabled)
|
||||
}
|
||||
|
||||
func (s *Service) GetSettings(ctx context.Context, guildID int64) (*settingsrepo.LogSettings, error) {
|
||||
return s.settings.GetLogSettings(ctx, guildID)
|
||||
}
|
||||
|
||||
func (s *Service) HasSettings(ctx context.Context, guildID int64) (bool, error) {
|
||||
return s.settings.HasLogSettings(ctx, guildID)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"velox-bot/internal/db/services/defaultrole"
|
||||
"velox-bot/internal/db/services/level"
|
||||
"velox-bot/internal/db/services/levelsettings"
|
||||
"velox-bot/internal/db/services/logsettings"
|
||||
"velox-bot/internal/db/services/meeting"
|
||||
"velox-bot/internal/db/services/projects"
|
||||
"velox-bot/internal/db/services/rps"
|
||||
@@ -10,7 +12,6 @@ import (
|
||||
"velox-bot/internal/db/services/twitch"
|
||||
"velox-bot/internal/db/services/usersettings"
|
||||
"velox-bot/internal/db/services/welcome"
|
||||
"velox-bot/internal/db/services/defaultrole"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
@@ -24,11 +25,12 @@ type Services struct {
|
||||
Twitch *twitch.Service
|
||||
Welcome *welcome.Service
|
||||
DefaultRole *defaultrole.Service
|
||||
LogSettings *logsettings.Service
|
||||
}
|
||||
|
||||
var Global *Services
|
||||
|
||||
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service) *Services {
|
||||
func NewServices(level *level.Service, levelSettings *levelsettings.Service, meeting *meeting.Service, schedule *schedule.Service, userSettings *usersettings.Service, projects *projects.Service, rps *rps.Service, twitchSvc *twitch.Service, welcomeSvc *welcome.Service, defaultRoleSvc *defaultrole.Service, logSettingsSvc *logsettings.Service) *Services {
|
||||
s := &Services{
|
||||
Level: level,
|
||||
LevelSettings: levelSettings,
|
||||
@@ -40,6 +42,7 @@ func NewServices(level *level.Service, levelSettings *levelsettings.Service, mee
|
||||
Twitch: twitchSvc,
|
||||
Welcome: welcomeSvc,
|
||||
DefaultRole: defaultRoleSvc,
|
||||
LogSettings: logSettingsSvc,
|
||||
}
|
||||
Global = s
|
||||
return s
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
)
|
||||
|
||||
func HandleMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate, services *services.Services) {
|
||||
CacheMessageFromCreate(m)
|
||||
|
||||
if m.Author.Bot || m.GuildID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"velox-bot/internal/db/services"
|
||||
"velox-bot/internal/modlog"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
type cachedMessage struct {
|
||||
Content string
|
||||
AuthorID string
|
||||
}
|
||||
|
||||
var messageCache sync.Map // map[messageID]cachedMessage
|
||||
|
||||
func HandleMessageUpdateLog(s *discordgo.Session, m *discordgo.MessageUpdate, svc *services.Services) {
|
||||
if s == nil || m == nil || m.Message == nil || m.GuildID == "" || svc == nil {
|
||||
return
|
||||
}
|
||||
if m.Author != nil && m.Author.Bot {
|
||||
return
|
||||
}
|
||||
|
||||
before := ""
|
||||
if m.BeforeUpdate != nil {
|
||||
before = strings.TrimSpace(m.BeforeUpdate.Content)
|
||||
}
|
||||
if before == "" {
|
||||
if prev, ok := getCachedMessage(m.ID); ok {
|
||||
before = strings.TrimSpace(prev.Content)
|
||||
}
|
||||
}
|
||||
after := strings.TrimSpace(m.Content)
|
||||
if before == after {
|
||||
return
|
||||
}
|
||||
|
||||
jumpURL := fmt.Sprintf("https://discord.com/channels/%s/%s/%s", m.GuildID, m.ChannelID, m.ID)
|
||||
authorMention := "Unknown"
|
||||
if m.Author != nil {
|
||||
authorMention = m.Author.Mention()
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Message Edited",
|
||||
Color: 0xF1C40F,
|
||||
Description: fmt.Sprintf("Author: %s\nChannel: <#%s>\n[Jump to message](%s)", authorMention, m.ChannelID, jumpURL),
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{Name: "Before", Value: truncateOrPlaceholder(before), Inline: false},
|
||||
{Name: "After", Value: truncateOrPlaceholder(after), Inline: false},
|
||||
},
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err := modlog.Send(s, svc, m.GuildID, embed); err != nil {
|
||||
log.Printf("log: failed to send message edit log: %v", err)
|
||||
}
|
||||
|
||||
cacheMessage(m.ID, after, authorIDOrEmpty(m.Author))
|
||||
}
|
||||
|
||||
func HandleMessageDeleteLog(s *discordgo.Session, m *discordgo.MessageDelete, svc *services.Services) {
|
||||
if s == nil || m == nil || m.Message == nil || m.GuildID == "" || svc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
authorID := ""
|
||||
authorLabel := "Unknown"
|
||||
content := ""
|
||||
if m.BeforeDelete != nil {
|
||||
if m.BeforeDelete.Author != nil {
|
||||
authorID = m.BeforeDelete.Author.ID
|
||||
authorLabel = m.BeforeDelete.Author.Mention()
|
||||
}
|
||||
content = strings.TrimSpace(m.BeforeDelete.Content)
|
||||
}
|
||||
if content == "" {
|
||||
if prev, ok := getCachedMessage(m.ID); ok {
|
||||
content = strings.TrimSpace(prev.Content)
|
||||
if authorID == "" && prev.AuthorID != "" {
|
||||
authorID = prev.AuthorID
|
||||
authorLabel = "<@" + prev.AuthorID + ">"
|
||||
}
|
||||
}
|
||||
}
|
||||
if authorID == "" && m.Author != nil {
|
||||
authorID = m.Author.ID
|
||||
authorLabel = m.Author.Mention()
|
||||
}
|
||||
if authorID == "" {
|
||||
authorLabel = "Unknown"
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Message Deleted",
|
||||
Color: 0xE74C3C,
|
||||
Description: fmt.Sprintf("Author: %s\nChannel: <#%s>\nMessage ID: `%s`", authorLabel, m.ChannelID, m.ID),
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{Name: "Content", Value: truncateOrPlaceholder(content), Inline: false},
|
||||
},
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err := modlog.Send(s, svc, m.GuildID, embed); err != nil {
|
||||
log.Printf("log: failed to send message delete log: %v", err)
|
||||
}
|
||||
deleteCachedMessage(m.ID)
|
||||
}
|
||||
|
||||
func HandleGuildMemberAddLog(s *discordgo.Session, m *discordgo.GuildMemberAdd, svc *services.Services) {
|
||||
if s == nil || m == nil || m.Member == nil || m.User == nil || m.GuildID == "" || svc == nil {
|
||||
return
|
||||
}
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Member Joined",
|
||||
Color: 0x2ECC71,
|
||||
Description: fmt.Sprintf("%s joined the server.\nUser ID: `%s`", m.User.Mention(), m.User.ID),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err := modlog.Send(s, svc, m.GuildID, embed); err != nil {
|
||||
log.Printf("log: failed to send member join log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGuildMemberRemoveLog(s *discordgo.Session, m *discordgo.GuildMemberRemove, svc *services.Services) {
|
||||
if s == nil || m == nil || m.Member == nil || m.User == nil || m.GuildID == "" || svc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to classify this leave as a kick via recent audit log.
|
||||
kickEntry, kickMod := findRecentAuditEntryForTarget(s, m.GuildID, m.User.ID, discordgo.AuditLogActionMemberKick)
|
||||
title := "Member Left"
|
||||
color := 0x95A5A6
|
||||
description := fmt.Sprintf("%s left the server.\nUser ID: `%s`", m.User.Mention(), m.User.ID)
|
||||
if kickEntry != nil {
|
||||
title = "Member Kicked"
|
||||
color = 0xE67E22
|
||||
description = fmt.Sprintf("%s was kicked.\nUser ID: `%s`", m.User.Mention(), m.User.ID)
|
||||
if kickMod != nil {
|
||||
description += "\nModerator: " + kickMod.Mention()
|
||||
}
|
||||
if strings.TrimSpace(kickEntry.Reason) != "" {
|
||||
description += "\nReason: " + kickEntry.Reason
|
||||
}
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: title,
|
||||
Color: color,
|
||||
Description: description,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err := modlog.Send(s, svc, m.GuildID, embed); err != nil {
|
||||
log.Printf("log: failed to send member remove log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGuildMemberUpdateLog(s *discordgo.Session, m *discordgo.GuildMemberUpdate, svc *services.Services) {
|
||||
if s == nil || m == nil || m.Member == nil || m.User == nil || m.GuildID == "" || svc == nil || m.BeforeUpdate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
added := diffRoleIDs(m.BeforeUpdate.Roles, m.Roles)
|
||||
removed := diffRoleIDs(m.Roles, m.BeforeUpdate.Roles)
|
||||
if len(added) == 0 && len(removed) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fields := make([]*discordgo.MessageEmbedField, 0, 2)
|
||||
if len(added) > 0 {
|
||||
fields = append(fields, &discordgo.MessageEmbedField{
|
||||
Name: "Roles Added",
|
||||
Value: joinRoleMentions(added),
|
||||
Inline: false,
|
||||
})
|
||||
}
|
||||
if len(removed) > 0 {
|
||||
fields = append(fields, &discordgo.MessageEmbedField{
|
||||
Name: "Roles Removed",
|
||||
Value: joinRoleMentions(removed),
|
||||
Inline: false,
|
||||
})
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Member Roles Updated",
|
||||
Color: 0x3498DB,
|
||||
Description: fmt.Sprintf("Member: %s (`%s`)", m.User.Mention(), m.User.ID),
|
||||
Fields: fields,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err := modlog.Send(s, svc, m.GuildID, embed); err != nil {
|
||||
log.Printf("log: failed to send member update log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGuildBanAddLog(s *discordgo.Session, m *discordgo.GuildBanAdd, svc *services.Services) {
|
||||
if s == nil || m == nil || m.User == nil || m.GuildID == "" || svc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry, moderator := findRecentAuditEntryForTarget(s, m.GuildID, m.User.ID, discordgo.AuditLogActionMemberBanAdd)
|
||||
desc := fmt.Sprintf("%s was banned.\nUser ID: `%s`", m.User.Mention(), m.User.ID)
|
||||
if moderator != nil {
|
||||
desc += "\nModerator: " + moderator.Mention()
|
||||
}
|
||||
if entry != nil && strings.TrimSpace(entry.Reason) != "" {
|
||||
desc += "\nReason: " + entry.Reason
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Member Banned",
|
||||
Color: 0xC0392B,
|
||||
Description: desc,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err := modlog.Send(s, svc, m.GuildID, embed); err != nil {
|
||||
log.Printf("log: failed to send ban add log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGuildBanRemoveLog(s *discordgo.Session, m *discordgo.GuildBanRemove, svc *services.Services) {
|
||||
if s == nil || m == nil || m.User == nil || m.GuildID == "" || svc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry, moderator := findRecentAuditEntryForTarget(s, m.GuildID, m.User.ID, discordgo.AuditLogActionMemberBanRemove)
|
||||
desc := fmt.Sprintf("%s was unbanned.\nUser ID: `%s`", m.User.Mention(), m.User.ID)
|
||||
if moderator != nil {
|
||||
desc += "\nModerator: " + moderator.Mention()
|
||||
}
|
||||
if entry != nil && strings.TrimSpace(entry.Reason) != "" {
|
||||
desc += "\nReason: " + entry.Reason
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Member Unbanned",
|
||||
Color: 0x27AE60,
|
||||
Description: desc,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
if err := modlog.Send(s, svc, m.GuildID, embed); err != nil {
|
||||
log.Printf("log: failed to send ban remove log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func findRecentAuditEntryForTarget(s *discordgo.Session, guildID, targetID string, action discordgo.AuditLogAction) (*discordgo.AuditLogEntry, *discordgo.User) {
|
||||
if s == nil || guildID == "" || targetID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
logs, err := s.GuildAuditLog(guildID, "", "", int(action), 10)
|
||||
if err != nil || logs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var chosen *discordgo.AuditLogEntry
|
||||
now := time.Now().UTC()
|
||||
for _, e := range logs.AuditLogEntries {
|
||||
if e == nil || e.TargetID != targetID || e.ActionType == nil || *e.ActionType != action {
|
||||
continue
|
||||
}
|
||||
ts, err := discordgo.SnowflakeTimestamp(e.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Keep only very recent entries to reduce false positives.
|
||||
if now.Sub(ts) > 2*time.Minute {
|
||||
continue
|
||||
}
|
||||
chosen = e
|
||||
break
|
||||
}
|
||||
if chosen == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for _, u := range logs.Users {
|
||||
if u != nil && u.ID == chosen.UserID {
|
||||
return chosen, u
|
||||
}
|
||||
}
|
||||
return chosen, nil
|
||||
}
|
||||
|
||||
func truncateOrPlaceholder(s string) string {
|
||||
v := strings.TrimSpace(s)
|
||||
if v == "" {
|
||||
return "*empty*"
|
||||
}
|
||||
if len(v) <= 1000 {
|
||||
return v
|
||||
}
|
||||
return v[:997] + "..."
|
||||
}
|
||||
|
||||
func diffRoleIDs(base, compare []string) []string {
|
||||
out := make([]string, 0)
|
||||
for _, id := range base {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(compare, id) {
|
||||
out = append(out, id)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func joinRoleMentions(roleIDs []string) string {
|
||||
if len(roleIDs) == 0 {
|
||||
return "*none*"
|
||||
}
|
||||
const max = 12
|
||||
parts := make([]string, 0, len(roleIDs))
|
||||
for idx, id := range roleIDs {
|
||||
if idx >= max {
|
||||
parts = append(parts, fmt.Sprintf("... and %d more", len(roleIDs)-max))
|
||||
break
|
||||
}
|
||||
parts = append(parts, "<@&"+id+">")
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func CacheMessageFromCreate(m *discordgo.MessageCreate) {
|
||||
if m == nil || m.Message == nil || m.ID == "" {
|
||||
return
|
||||
}
|
||||
cacheMessage(m.ID, strings.TrimSpace(m.Content), authorIDOrEmpty(m.Author))
|
||||
}
|
||||
|
||||
func cacheMessage(messageID, content, authorID string) {
|
||||
if messageID == "" {
|
||||
return
|
||||
}
|
||||
messageCache.Store(messageID, cachedMessage{
|
||||
Content: content,
|
||||
AuthorID: authorID,
|
||||
})
|
||||
}
|
||||
|
||||
func getCachedMessage(messageID string) (cachedMessage, bool) {
|
||||
if messageID == "" {
|
||||
return cachedMessage{}, false
|
||||
}
|
||||
raw, ok := messageCache.Load(messageID)
|
||||
if !ok {
|
||||
return cachedMessage{}, false
|
||||
}
|
||||
msg, ok := raw.(cachedMessage)
|
||||
if !ok {
|
||||
return cachedMessage{}, false
|
||||
}
|
||||
return msg, true
|
||||
}
|
||||
|
||||
func deleteCachedMessage(messageID string) {
|
||||
if messageID == "" {
|
||||
return
|
||||
}
|
||||
messageCache.Delete(messageID)
|
||||
}
|
||||
|
||||
func authorIDOrEmpty(u *discordgo.User) string {
|
||||
if u == nil {
|
||||
return ""
|
||||
}
|
||||
return u.ID
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"velox-bot/internal/db/services"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
// HandleBotAddedOnGuild applies safe defaults when this bot is added to a guild
|
||||
// and DMs setup instructions to the inviter (or guild owner fallback).
|
||||
func HandleBotAddedOnGuild(s *discordgo.Session, m *discordgo.GuildMemberAdd, svc *services.Services) {
|
||||
if s == nil || m == nil || m.Member == nil || m.User == nil || svc == nil {
|
||||
return
|
||||
}
|
||||
if s.State == nil || s.State.User == nil {
|
||||
return
|
||||
}
|
||||
if m.User.ID != s.State.User.ID {
|
||||
return
|
||||
}
|
||||
|
||||
guildID64, err := strconv.ParseInt(m.GuildID, 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Treat an existing logsettings row as "already onboarded once".
|
||||
alreadyOnboarded := false
|
||||
if svc.LogSettings != nil {
|
||||
exists, err := svc.LogSettings.HasSettings(context.Background(), guildID64)
|
||||
if err == nil {
|
||||
alreadyOnboarded = exists
|
||||
}
|
||||
}
|
||||
if alreadyOnboarded {
|
||||
return
|
||||
}
|
||||
|
||||
// Explicitly disable toggleable systems by default.
|
||||
if svc.LevelSettings != nil {
|
||||
if err := svc.LevelSettings.SetLevelSystemEnabled(context.Background(), guildID64, false); err != nil {
|
||||
log.Printf("onboarding: failed to set default level state for guild %s: %v", m.GuildID, err)
|
||||
}
|
||||
}
|
||||
if svc.LogSettings != nil {
|
||||
if err := svc.LogSettings.SetEnabled(context.Background(), guildID64, false); err != nil {
|
||||
log.Printf("onboarding: failed to set default log state for guild %s: %v", m.GuildID, err)
|
||||
}
|
||||
}
|
||||
|
||||
recipientID := findRecentBotAdderID(s, m.GuildID)
|
||||
if recipientID == "" {
|
||||
recipientID = guildOwnerID(s, m.GuildID)
|
||||
}
|
||||
if recipientID == "" {
|
||||
log.Printf("onboarding: unable to resolve DM recipient for guild %s", m.GuildID)
|
||||
return
|
||||
}
|
||||
|
||||
dmCh, err := s.UserChannelCreate(recipientID)
|
||||
if err != nil {
|
||||
log.Printf("onboarding: failed creating DM for user %s: %v", recipientID, err)
|
||||
return
|
||||
}
|
||||
|
||||
guildName := m.GuildID
|
||||
if g, err := s.State.Guild(m.GuildID); err == nil && g != nil && strings.TrimSpace(g.Name) != "" {
|
||||
guildName = g.Name
|
||||
} else if g, err := s.Guild(m.GuildID); err == nil && g != nil && strings.TrimSpace(g.Name) != "" {
|
||||
guildName = g.Name
|
||||
}
|
||||
|
||||
embed := &discordgo.MessageEmbed{
|
||||
Title: "Thanks for adding Velox Bot!",
|
||||
Color: 0x5865F2,
|
||||
Description: fmt.Sprintf(
|
||||
"Server: **%s**\n\nFor safety, toggleable systems start **disabled by default**.\nUse these commands in your server to enable features:",
|
||||
guildName,
|
||||
),
|
||||
Fields: []*discordgo.MessageEmbedField{
|
||||
{
|
||||
Name: "Level System",
|
||||
Value: "`/slvl toggle`",
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "Logging System",
|
||||
Value: "" +
|
||||
"`/config setlogchannel channel:#channel`\n" +
|
||||
"`/config enablelogging`",
|
||||
Inline: false,
|
||||
},
|
||||
{
|
||||
Name: "Welcome System",
|
||||
Value: "" +
|
||||
"`/config setwelcomechannel channel:#channel`\n" +
|
||||
"`/config setwelcomemessage message:\"...\"`\n" +
|
||||
"`/config setwelcomedm message:\"...\"`",
|
||||
Inline: false,
|
||||
},
|
||||
},
|
||||
Footer: &discordgo.MessageEmbedFooter{
|
||||
Text: "Tip: run /help in any server channel for the full command reference.",
|
||||
},
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if _, err := s.ChannelMessageSendEmbed(dmCh.ID, embed); err != nil {
|
||||
log.Printf("onboarding: failed sending setup DM to user %s: %v", recipientID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func findRecentBotAdderID(s *discordgo.Session, guildID string) string {
|
||||
if s == nil || s.State == nil || s.State.User == nil {
|
||||
return ""
|
||||
}
|
||||
logs, err := s.GuildAuditLog(guildID, "", "", int(discordgo.AuditLogActionBotAdd), 10)
|
||||
if err != nil || logs == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
botID := s.State.User.ID
|
||||
now := time.Now().UTC()
|
||||
for _, entry := range logs.AuditLogEntries {
|
||||
if entry == nil || entry.TargetID != botID || entry.ActionType == nil || *entry.ActionType != discordgo.AuditLogActionBotAdd {
|
||||
continue
|
||||
}
|
||||
ts, err := discordgo.SnowflakeTimestamp(entry.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if now.Sub(ts) > 10*time.Minute {
|
||||
continue
|
||||
}
|
||||
if entry.UserID != "" {
|
||||
return entry.UserID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func guildOwnerID(s *discordgo.Session, guildID string) string {
|
||||
if s == nil || guildID == "" {
|
||||
return ""
|
||||
}
|
||||
if s.State != nil {
|
||||
if g, err := s.State.Guild(guildID); err == nil && g != nil && g.OwnerID != "" {
|
||||
return g.OwnerID
|
||||
}
|
||||
}
|
||||
if g, err := s.Guild(guildID); err == nil && g != nil {
|
||||
return g.OwnerID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package modlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"velox-bot/internal/db/services"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
)
|
||||
|
||||
func Send(s *discordgo.Session, svc *services.Services, guildID string, embed *discordgo.MessageEmbed) error {
|
||||
if s == nil || svc == nil || svc.LogSettings == nil || embed == nil || guildID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
gid, err := strconv.ParseInt(guildID, 10, 64)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg, err := svc.LogSettings.GetSettings(context.Background(), gid)
|
||||
if err != nil || cfg == nil || !cfg.IsEnabled || cfg.ChannelID == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.ChannelMessageSendEmbed(strconv.FormatInt(cfg.ChannelID, 10), embed)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"velox-bot/internal/db/services"
|
||||
"velox-bot/internal/db/services/level"
|
||||
"velox-bot/internal/db/services/levelsettings"
|
||||
"velox-bot/internal/db/services/logsettings"
|
||||
"velox-bot/internal/db/services/meeting"
|
||||
"velox-bot/internal/db/services/projects"
|
||||
"velox-bot/internal/db/services/rps"
|
||||
@@ -57,6 +58,7 @@ func main() {
|
||||
defaultRoleRepo := defaultrolerepo.NewRepo(db)
|
||||
levelService := level.New(levelRepo, settingsRepo)
|
||||
levelSettingsService := levelsettings.New(settingsRepo)
|
||||
logSettingsService := logsettings.New(settingsRepo)
|
||||
meetingService := meeting.New(settingsRepo)
|
||||
scheduleService := schedule.New(scheduleRepo)
|
||||
userSettingsService := usersettings.New(userSettingsRepo)
|
||||
@@ -65,7 +67,7 @@ func main() {
|
||||
twitchService := twitch.New(twitchRepo, config.TwitchClientID)
|
||||
welcomeService := welcome.New(welcomeRepo)
|
||||
defaultRoleService := defaultrole.New(defaultRoleRepo)
|
||||
services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService, twitchService, welcomeService, defaultRoleService)
|
||||
services := services.NewServices(levelService, levelSettingsService, meetingService, scheduleService, userSettingsService, projectsService, rpsService, twitchService, welcomeService, defaultRoleService, logSettingsService)
|
||||
|
||||
bot, err := bot.NewBot(config.BotToken, config.AppID, config.GuildID, config.LavalinkHost, config.LavalinkPass, commands.AllCommands, services)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user