package discord import ( "context" "fmt" "time" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "github.com/bwmarrin/discordgo" "github.com/robfig/cron/v3" "go.uber.org/zap" ) type GroupService interface { Create(ctx context.Context, params domain.CreateGroupParams) (domain.Group, error) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.Group, error) SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.Group, error) SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.Group, error) SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.Group, error) CleanUp(ctx context.Context) error List(ctx context.Context, params domain.ListGroupsParams) ([]domain.Group, error) Delete(ctx context.Context, id, serverID string) error } type MonitorService interface { Create(ctx context.Context, groupID, serverID, tribeTag string) (domain.Monitor, error) List(ctx context.Context, groupID, serverID string) ([]domain.MonitorWithTribe, error) Execute(ctx context.Context) ([]domain.EnnoblementNotification, error) Delete(ctx context.Context, id, serverID string) error } type ChoiceService interface { Versions(ctx context.Context) ([]domain.Choice, error) } type Bot struct { s *discordgo.Session c *cron.Cron groupSvc GroupService monitorSvc MonitorService choiceSvc ChoiceService logger *zap.Logger } func NewBot( token string, groupSvc GroupService, monitorSvc MonitorService, client ChoiceService, logger *zap.Logger, ) (*Bot, error) { s, err := discordgo.New("Bot " + token) if err != nil { return nil, fmt.Errorf("discordgo.New: %w", err) } s.Identify.Intents = discordgo.IntentsNone b := &Bot{ s: s, c: cron.New( cron.WithLocation(time.UTC), cron.WithChain( cron.SkipIfStillRunning(cron.DiscardLogger), ), ), groupSvc: groupSvc, monitorSvc: monitorSvc, choiceSvc: client, logger: logger, } b.s.AddHandler(b.handleSessionReady) b.s.AddHandler(b.logCommands) return b, nil } func (b *Bot) Run() error { if err := b.initCron(); err != nil { return fmt.Errorf("initCron: %w", err) } if err := b.s.Open(); err != nil { return fmt.Errorf("s.Open: %w", err) } if err := b.registerCommands(); err != nil { _ = b.s.Close() return fmt.Errorf("couldn't register commands: %w", err) } b.c.Run() return nil } type command interface { name() string register(s *discordgo.Session) error } func (b *Bot) registerCommands() error { commands := []command{ &groupCommand{groupSvc: b.groupSvc, choiceSvc: b.choiceSvc}, &monitorCommand{svc: b.monitorSvc}, } for _, c := range commands { if err := b.registerCommand(c); err != nil { return err } } return nil } func (b *Bot) registerCommand(cmd command) error { if err := cmd.register(b.s); err != nil { return fmt.Errorf("couldn't register command '%s': %w", cmd.name(), err) } return nil } func (b *Bot) initCron() error { jobs := []struct { spec string job cron.Job }{ { spec: "@every 1m", job: &executeMonitorsJob{svc: b.monitorSvc, s: b.s, logger: b.logger}, }, { spec: "0 */8 * * *", job: &cleanUpGroupsJob{svc: b.groupSvc, logger: b.logger}, }, } for _, j := range jobs { _, err := b.c.AddJob(j.spec, j.job) if err != nil { return err } } return nil } func (b *Bot) handleSessionReady(s *discordgo.Session, _ *discordgo.Ready) { _ = s.UpdateGameStatus(0, "Tribal Wars") } func (b *Bot) logCommands(_ *discordgo.Session, i *discordgo.InteractionCreate) { if i.Type != discordgo.InteractionApplicationCommand { return } cmdData := i.ApplicationCommandData() cmd := cmdData.Name options := cmdData.Options for len(options) > 0 && options[0].Type == discordgo.ApplicationCommandOptionSubCommand { cmd += " " + options[0].Name options = options[0].Options } userID := "" if i.User != nil { userID = i.User.ID } else if i.Member != nil { userID = i.Member.User.ID } b.logger.Info( "executing command", zap.String("command", cmd), zap.String("user", userID), zap.String("serverID", i.GuildID), zap.String("channelID", i.ChannelID), ) } func (b *Bot) Close() error { <-b.c.Stop().Done() return b.s.Close() }