package discord import ( "context" "fmt" "time" "gitea.dwysokinski.me/twhelp/dcbot/internal/discord/internal/discordi18n" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "github.com/bwmarrin/discordgo" "github.com/robfig/cron/v3" "go.uber.org/zap" "golang.org/x/text/language" ) type GroupService interface { Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error) AddTribe(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error) RemoveTribe(ctx context.Context, id, serverID, tribeTag string) (domain.GroupWithMonitors, error) SetLanguageTag(ctx context.Context, id, serverID, languageTag string) (domain.GroupWithMonitors, error) SetChannelGains(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error) SetChannelLosses(ctx context.Context, id, serverID, channel string) (domain.GroupWithMonitors, error) SetInternals(ctx context.Context, id, serverID string, internals bool) (domain.GroupWithMonitors, error) SetBarbarians(ctx context.Context, id, serverID string, barbarians bool) (domain.GroupWithMonitors, error) Execute(ctx context.Context) ([]domain.EnnoblementNotification, error) CleanUp(ctx context.Context) error ListServer(ctx context.Context, serverID string) ([]domain.GroupWithMonitors, error) GetWithTribes(ctx context.Context, id, serverID string) (domain.GroupWithMonitorsAndTribes, error) Delete(ctx context.Context, id, serverID string) error } type ChoiceService interface { Versions(ctx context.Context) ([]domain.Choice, error) } type VillageService interface { TranslateCoords( ctx context.Context, params domain.TranslateVillageCoordsParams, page int32, ) (domain.TranslateVillageCoordsResult, error) TranslateCoordsFromHash(ctx context.Context, paramsSHA256Hash string, page int32) (domain.TranslateVillageCoordsResult, error) } type Bot struct { session *discordgo.Session cron *cron.Cron groupSvc GroupService choiceSvc *choiceService villageSvc VillageService logger *zap.Logger localizer *discordi18n.Localizer } func NewBot( token string, groupSvc GroupService, choiceSvc ChoiceService, villageSvc VillageService, logger *zap.Logger, ) (*Bot, error) { localizer, err := discordi18n.NewLocalizer(language.English) if err != nil { return nil, err } s, err := discordgo.New("Bot " + token) if err != nil { return nil, err } s.Identify.Intents = discordgo.IntentsNone b := &Bot{ session: s, cron: cron.New( cron.WithLocation(time.UTC), cron.WithChain( cron.SkipIfStillRunning(cron.DiscardLogger), ), ), groupSvc: groupSvc, choiceSvc: &choiceService{ choiceSvc: choiceSvc, localizer: localizer, }, villageSvc: villageSvc, logger: logger, localizer: localizer, } b.session.AddHandler(b.handleSessionReady) b.session.AddHandler(b.logCommands) return b, nil } func (b *Bot) Run() error { if err := b.registerCronJobs(); err != nil { return err } if err := b.session.Open(); err != nil { return err } if err := b.registerCommands(); err != nil { _ = b.session.Close() return err } b.cron.Run() return nil } type command interface { name() string register(s *discordgo.Session) error } func (b *Bot) registerCommands() error { commands := []command{ &groupCommand{localizer: b.localizer, logger: b.logger, groupSvc: b.groupSvc, choiceSvc: b.choiceSvc}, &coordsCommand{localizer: b.localizer, logger: b.logger, villageSvc: b.villageSvc, choiceSvc: b.choiceSvc}, } for _, cmd := range commands { if err := cmd.register(b.session); err != nil { return fmt.Errorf("couldn't register command '%s': %w", cmd.name(), err) } } return nil } func (b *Bot) registerCronJobs() error { jobs := []struct { spec string job cron.Job }{ { spec: "@every 1m", job: &executeMonitorsJob{svc: b.groupSvc, s: b.session, localizer: b.localizer, logger: b.logger}, }, { spec: "0 */8 * * *", job: &cleanUpGroupsJob{svc: b.groupSvc, logger: b.logger}, }, } for _, j := range jobs { if _, err := b.cron.AddJob(j.spec, j.job); err != nil { return err } } return nil } func (b *Bot) handleSessionReady(s *discordgo.Session, _ *discordgo.Ready) { _ = s.UpdateGameStatus(0, "Tribal Wars") b.logger.Info("Bot is up and running") } 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 || options[0].Type == discordgo.ApplicationCommandOptionSubCommandGroup) { 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.cron.Stop().Done() return b.session.Close() }