Join Gate (account age, avatar, advertising and username filters, bot checks), raid detection with invite pausing, anti-nuke quarantine of admins who mass delete, manual and automatic lockdowns, a decaying heat score against spam, role and channel snapshots with restore, and alert DMs to the owner and trusted users. Everything that weakens protection is limited to the owner and the trusted list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
207 lines
6.5 KiB
Go
207 lines
6.5 KiB
Go
package bot
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"time"
|
|
"velox-bot/internal/commands"
|
|
"velox-bot/internal/db/repos/musicrepo"
|
|
"velox-bot/internal/db/services"
|
|
"velox-bot/internal/events"
|
|
"velox-bot/internal/music"
|
|
"velox-bot/internal/tickets"
|
|
"velox-bot/internal/security"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
)
|
|
|
|
type Bot struct {
|
|
Session *discordgo.Session
|
|
AppID string
|
|
GuildID string
|
|
Commands []*discordgo.ApplicationCommand
|
|
registeredCommands []*discordgo.ApplicationCommand
|
|
Services *services.Services
|
|
MusicRepo *musicrepo.Repo
|
|
DBHost string
|
|
LavalinkHost string
|
|
LavalinkPass string
|
|
}
|
|
|
|
func NewBot(token, appID, guildID, lavalinkHost, lavalinkPass string, cmds []*discordgo.ApplicationCommand, services *services.Services, musicRepo *musicrepo.Repo, dbHost string) (*Bot, error) {
|
|
session, err := discordgo.New("Bot " + token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Intents needed for:
|
|
// - InteractionCreate (slash commands): Guilds
|
|
// - MessageCreate (leveling): GuildMessages
|
|
// - GuildMemberAdd (welcome messages): GuildMembers
|
|
// - VoiceStateUpdate (meeting lobby): GuildVoiceStates
|
|
session.Identify.Intents = discordgo.IntentsGuilds |
|
|
discordgo.IntentsGuildBans |
|
|
discordgo.IntentsGuildMembers |
|
|
discordgo.IntentsGuildMessages |
|
|
discordgo.IntentsMessageContent |
|
|
discordgo.IntentsGuildVoiceStates |
|
|
discordgo.IntentsDirectMessages
|
|
|
|
return &Bot{
|
|
Session: session,
|
|
AppID: appID,
|
|
GuildID: guildID,
|
|
Commands: cmds,
|
|
Services: services,
|
|
MusicRepo: musicRepo,
|
|
DBHost: dbHost,
|
|
LavalinkHost: lavalinkHost,
|
|
LavalinkPass: lavalinkPass,
|
|
}, nil
|
|
}
|
|
|
|
func (b *Bot) Start() error {
|
|
if err := b.Session.Open(); err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = music.Init(b.Session, b.MusicRepo, b.AppID, b.LavalinkHost, b.LavalinkPass)
|
|
|
|
go func() {
|
|
if err := music.StartCommandListener(context.Background(), b.DBHost, b.MusicRepo); err != nil {
|
|
log.Printf("music: command listener stopped: %v", err)
|
|
}
|
|
}()
|
|
|
|
if st := tickets.Default(); st != nil {
|
|
go func() {
|
|
// The connection can drop (database restart, network blip):
|
|
// reconnect instead of silently ignoring the dashboard from then on.
|
|
for {
|
|
err := tickets.StartCommandListener(context.Background(), b.DBHost, b.Session, st)
|
|
log.Printf("tickets: command listener stopped: %v (retrying in 10s)", err)
|
|
time.Sleep(10 * time.Second)
|
|
}
|
|
}()
|
|
}
|
|
|
|
if eng := security.Start(b.Session, b.Session.State.User.ID); eng != nil {
|
|
// Moderators are exempt from heat; their permissions come from the
|
|
// gateway cache so a message never costs an API call.
|
|
eng.ChannelPerms = b.Session.State.UserChannelPermissions
|
|
eng.ResumeRaids(context.Background())
|
|
go eng.WatchRaids(context.Background(), 30*time.Second)
|
|
go eng.WatchSnapshots(context.Background())
|
|
go func() {
|
|
// Reconnect if the database connection drops, instead of
|
|
// silently ignoring the dashboard from then on.
|
|
for {
|
|
err := eng.StartCommandListener(context.Background(), b.DBHost)
|
|
log.Printf("security: command listener stopped: %v (retrying in 10s)", err)
|
|
time.Sleep(10 * time.Second)
|
|
}
|
|
}()
|
|
}
|
|
|
|
b.registeredCommands = make([]*discordgo.ApplicationCommand, len(b.Commands))
|
|
for _, cmd := range b.Commands {
|
|
created, err := b.Session.ApplicationCommandCreate(b.AppID, b.GuildID, cmd)
|
|
if err != nil {
|
|
log.Printf("Cannot create '%v' command: %v", cmd.Name, err)
|
|
continue
|
|
}
|
|
b.registeredCommands = append(b.registeredCommands, created)
|
|
}
|
|
|
|
b.Session.AddHandler(commands.HandleInteraction)
|
|
|
|
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
if eng := security.Default(); eng != nil {
|
|
eng.OnMessage(context.Background(), m)
|
|
}
|
|
events.HandleMessageCreate(s, m, b.Services)
|
|
})
|
|
|
|
b.Session.AddHandler(func(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
|
|
events.HandleBotAddedOnGuild(s, m, b.Services)
|
|
|
|
// Security runs first: someone it removes (join lock, Join Gate,
|
|
// raid) gets no welcome message or default role.
|
|
if eng := security.Default(); eng != nil && m != nil && m.Member != nil {
|
|
if eng.OnMemberJoin(context.Background(), m.GuildID, m.Member) {
|
|
events.HandleGuildMemberAddLog(s, m, b.Services)
|
|
return
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
|
|
// Anti-nuke and unauthorized bot additions read the audit log as it
|
|
// happens (needs the Guild Moderation intent and View Audit Log).
|
|
b.Session.AddHandler(func(s *discordgo.Session, ev *discordgo.GuildAuditLogEntryCreate) {
|
|
if eng := security.Default(); eng != nil && ev.AuditLogEntry != nil {
|
|
eng.OnAuditLogEntry(context.Background(), ev.GuildID, ev.AuditLogEntry)
|
|
}
|
|
})
|
|
|
|
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, t *discordgo.ThreadDelete) {
|
|
if st := tickets.Default(); st != nil && t.Channel != nil {
|
|
tickets.ThreadDeleted(context.Background(), st, t.ID)
|
|
}
|
|
})
|
|
|
|
b.Session.AddHandler(func(s *discordgo.Session, vs *discordgo.VoiceStateUpdate) {
|
|
events.HandleVoiceStateUpdate(s, vs, b.Services)
|
|
music.OnVoiceStateUpdate(vs)
|
|
})
|
|
|
|
b.Session.AddHandler(func(s *discordgo.Session, ev *discordgo.VoiceServerUpdate) {
|
|
music.OnVoiceServerUpdate(ev)
|
|
})
|
|
|
|
events.StartScheduleReminderLoop(b.Session, b.Services)
|
|
events.StartTwitchLiveLoop(b.Session, b.Services)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Bot) Close() error {
|
|
for _, cmd := range b.registeredCommands {
|
|
if err := b.Session.ApplicationCommandDelete(b.AppID, b.GuildID, cmd.ID); err != nil {
|
|
log.Printf("cannot delete '%s' command: %v", cmd.Name, err)
|
|
}
|
|
}
|
|
return b.Session.Close()
|
|
}
|