fix: proper configuration handling across submodules

This commit is contained in:
2025-08-26 00:19:43 -05:00
parent 165e32bbf6
commit a37fbeb224
12 changed files with 408 additions and 327 deletions

40
internal/bot/bot.go Normal file
View File

@@ -0,0 +1,40 @@
package bot
import (
"banner/internal/api"
"banner/internal/config"
"banner/internal/utils"
"fmt"
"time"
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog/log"
)
type Bot struct {
Session *discordgo.Session
API *api.API
Config *config.Config
isClosing bool
}
func New(s *discordgo.Session, a *api.API, c *config.Config) *Bot {
return &Bot{Session: s, API: a, Config: c}
}
func (b *Bot) SetClosing() {
b.isClosing = true
}
func (b *Bot) GetSession() (string, error) {
sessionID := b.API.EnsureSession()
term := utils.Default(time.Now()).ToString()
log.Info().Str("term", term).Str("sessionID", sessionID).Msg("Setting selected term")
err := b.API.SelectTerm(term, sessionID)
if err != nil {
return "", fmt.Errorf("failed to select term while generating session ID: %w", err)
}
return sessionID, nil
}

View File

@@ -2,7 +2,6 @@ package bot
import (
"banner/internal/api"
"banner/internal/config"
"banner/internal/models"
"banner/internal/utils"
"fmt"
@@ -18,9 +17,16 @@ import (
"github.com/samber/lo"
)
const (
ICalTimestampFormatUtc = "20060102T150405Z"
ICalTimestampFormatLocal = "20060102T150405"
)
type CommandHandler func(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error
var (
CommandDefinitions = []*discordgo.ApplicationCommand{TermCommandDefinition, TimeCommandDefinition, SearchCommandDefinition, IcsCommandDefinition}
CommandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate) error{
CommandHandlers = map[string]CommandHandler{
TimeCommandDefinition.Name: TimeCommandHandler,
TermCommandDefinition.Name: TermCommandHandler,
SearchCommandDefinition.Name: SearchCommandHandler,
@@ -76,8 +82,8 @@ var SearchCommandDefinition = &discordgo.ApplicationCommand{
},
}
func SearchCommandHandler(session *discordgo.Session, interaction *discordgo.InteractionCreate) error {
data := interaction.ApplicationCommandData()
func SearchCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
query := api.NewQuery().Credits(3, 6)
for _, option := range data.Options {
@@ -177,9 +183,14 @@ func SearchCommandHandler(session *discordgo.Session, interaction *discordgo.Int
}
}
courses, err := api.Search(query, "", false)
term, err := b.GetSession()
if err != nil {
session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{
return err
}
courses, err := b.API.Search(term, query, "", false)
if err != nil {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Error searching for courses",
@@ -222,12 +233,12 @@ func SearchCommandHandler(session *discordgo.Session, interaction *discordgo.Int
color = 0xFF6500
}
err = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Footer: utils.GetFetchedFooter(fetch_time),
Footer: utils.GetFetchedFooter(b.Config, fetch_time),
Description: fmt.Sprintf("%d Class%s", courses.TotalCount, utils.Plural(courses.TotalCount)),
Fields: fields[:min(25, len(fields))],
Color: color,
@@ -262,8 +273,8 @@ var TermCommandDefinition = &discordgo.ApplicationCommand{
},
}
func TermCommandHandler(session *discordgo.Session, interaction *discordgo.InteractionCreate) error {
data := interaction.ApplicationCommandData()
func TermCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error {
data := i.ApplicationCommandData()
searchTerm := ""
pageNumber := 1
@@ -279,10 +290,10 @@ func TermCommandHandler(session *discordgo.Session, interaction *discordgo.Inter
}
}
termResult, err := api.GetTerms(searchTerm, pageNumber, 25)
termResult, err := b.API.GetTerms(searchTerm, pageNumber, 25)
if err != nil {
utils.RespondError(session, interaction.Interaction, "Error while fetching terms", err)
utils.RespondError(s, i.Interaction, "Error while fetching terms", err)
return err
}
@@ -302,12 +313,12 @@ func TermCommandHandler(session *discordgo.Session, interaction *discordgo.Inter
log.Warn().Int("count", len(fields)).Msg("Too many fields in term command (trimmed)")
}
err = session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Footer: utils.GetFetchedFooter(fetch_time),
Footer: utils.GetFetchedFooter(b.Config, fetch_time),
Description: fmt.Sprintf("%d term%s (page %d)", len(termResult), utils.Plural(len(termResult)), pageNumber),
Fields: fields[:min(25, len(fields))],
},
@@ -332,12 +343,12 @@ var TimeCommandDefinition = &discordgo.ApplicationCommand{
},
}
func TimeCommandHandler(s *discordgo.Session, i *discordgo.InteractionCreate) error {
func TimeCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error {
fetch_time := time.Now()
crn := i.ApplicationCommandData().Options[0].IntValue()
// Fix static term
meetingTimes, err := api.GetCourseMeetingTime(202510, int(crn))
meetingTimes, err := b.API.GetCourseMeetingTime(202510, int(crn))
if err != nil {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
@@ -356,7 +367,7 @@ func TimeCommandHandler(s *discordgo.Session, i *discordgo.InteractionCreate) er
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Footer: utils.GetFetchedFooter(fetch_time),
Footer: utils.GetFetchedFooter(b.Config, fetch_time),
Description: "",
Fields: []*discordgo.MessageEmbedField{
{
@@ -397,16 +408,18 @@ var IcsCommandDefinition = &discordgo.ApplicationCommand{
},
}
func IcsCommandHandler(s *discordgo.Session, i *discordgo.InteractionCreate) error {
crn := i.ApplicationCommandData().Options[0].IntValue()
func IcsCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error {
// Parse all options
options := utils.ParseOptions(i.ApplicationCommandData().Options)
crn := options.GetInt("crn")
course, err := api.GetCourse(strconv.Itoa(int(crn)))
course, err := b.API.GetCourse(strconv.Itoa(int(crn)))
if err != nil {
return fmt.Errorf("Error retrieving course data: %w", err)
}
// Fix static term
meetingTimes, err := api.GetCourseMeetingTime(202510, int(crn))
meetingTimes, err := b.API.GetCourseMeetingTime(202510, int(crn))
if err != nil {
return fmt.Errorf("Error requesting meeting time: %w", err)
}
@@ -433,22 +446,24 @@ func IcsCommandHandler(s *discordgo.Session, i *discordgo.InteractionCreate) err
events := []string{}
for _, meeting := range meetingTimes {
now := time.Now().In(config.CentralTimeLocation)
now := time.Now().In(b.Config.CentralTimeLocation)
uid := fmt.Sprintf("%d-%s@ical.banner.xevion.dev", now.Unix(), meeting.CourseReferenceNumber)
startDay := meeting.StartDay()
startTime := meeting.StartTime()
endTime := meeting.EndTime()
dtStart := time.Date(startDay.Year(), startDay.Month(), startDay.Day(), int(startTime.Hours), int(startTime.Minutes), 0, 0, config.CentralTimeLocation)
dtEnd := time.Date(startDay.Year(), startDay.Month(), startDay.Day(), int(endTime.Hours), int(endTime.Minutes), 0, 0, config.CentralTimeLocation)
dtStart := time.Date(startDay.Year(), startDay.Month(), startDay.Day(), int(startTime.Hours), int(startTime.Minutes), 0, 0, b.Config.CentralTimeLocation)
dtEnd := time.Date(startDay.Year(), startDay.Month(), startDay.Day(), int(endTime.Hours), int(endTime.Minutes), 0, 0, b.Config.CentralTimeLocation)
endDay := meeting.EndDay()
until := time.Date(endDay.Year(), endDay.Month(), endDay.Day(), 23, 59, 59, 0, config.CentralTimeLocation)
// endDay := meeting.EndDay()
// until := time.Date(endDay.Year(), endDay.Month(), endDay.Day(), 23, 59, 59, 0, b.Config.CentralTimeLocation)
summary := fmt.Sprintf("%s %s %s", course.Subject, course.CourseNumber, course.CourseTitle)
description := fmt.Sprintf("Instructor: %s\nSection: %s\nCRN: %s", course.Faculty[0].DisplayName, course.SequenceNumber, meeting.CourseReferenceNumber)
location := meeting.PlaceString()
rrule := meeting.RRule()
event := fmt.Sprintf(`BEGIN:VEVENT
DTSTAMP:%s
UID:%s
@@ -458,7 +473,7 @@ DTEND;TZID=America/Chicago:%s
SUMMARY:%s
DESCRIPTION:%s
LOCATION:%s
END:VEVENT`, now.Format(config.ICalTimestampFormatLocal), uid, dtStart.Format(config.ICalTimestampFormatLocal), meeting.ByDay(), until.Format(config.ICalTimestampFormatLocal), dtEnd.Format(config.ICalTimestampFormatLocal), summary, strings.Replace(description, "\n", `\n`, -1), location)
END:VEVENT`, now.Format(ICalTimestampFormatLocal), uid, dtStart.Format(ICalTimestampFormatLocal), rrule.ByDay, rrule.Until, dtEnd.Format(ICalTimestampFormatLocal), summary, strings.Replace(description, "\n", `\n`, -1), location)
events = append(events, event)
}

90
internal/bot/handlers.go Normal file
View File

@@ -0,0 +1,90 @@
package bot
import (
"banner/internal/utils"
"fmt"
"github.com/bwmarrin/discordgo"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func (b *Bot) RegisterHandlers() {
b.Session.AddHandler(func(internalSession *discordgo.Session, interaction *discordgo.InteractionCreate) {
// Handle commands during restart (highly unlikely, but just in case)
if b.isClosing {
err := utils.RespondError(internalSession, interaction.Interaction, "Bot is currently restarting, try again later.", nil)
if err != nil {
log.Error().Err(err).Msg("Failed to respond with restart error feedback")
}
return
}
name := interaction.ApplicationCommandData().Name
if handler, ok := CommandHandlers[name]; ok {
// Build dict of options for the log
options := zerolog.Dict()
for _, option := range interaction.ApplicationCommandData().Options {
options.Str(option.Name, fmt.Sprintf("%v", option.Value))
}
event := log.Info().Str("name", name).Str("user", utils.GetUser(interaction).Username).Dict("options", options)
// If the command was invoked in a guild, add guild & channel info to the log
if interaction.Member != nil {
guild := zerolog.Dict()
guild.Str("id", interaction.GuildID)
guild.Str("name", utils.GetGuildName(b.Config, internalSession, interaction.GuildID))
event.Dict("guild", guild)
channel := zerolog.Dict()
channel.Str("id", interaction.ChannelID)
guild.Str("name", utils.GetChannelName(b.Config, internalSession, interaction.ChannelID))
event.Dict("channel", channel)
} else {
// If the command was invoked in a DM, add the user info to the log
user := zerolog.Dict()
user.Str("id", interaction.User.ID)
user.Str("name", interaction.User.Username)
event.Dict("user", user)
}
// Log command invocation
event.Msg("Command Invoked")
// Prepare to recover
defer func() {
if err := recover(); err != nil {
log.Error().Stack().Str("commandName", name).Interface("detail", err).Msg("Command Handler Panic")
// Respond with error
err := utils.RespondError(internalSession, interaction.Interaction, "Unexpected Error: command handler panic", nil)
if err != nil {
log.Error().Stack().Str("commandName", name).Err(err).Msg("Failed to respond with panic error feedback")
}
}
}()
// Call handler
err := handler(b, internalSession, interaction)
// Log & respond error
if err != nil {
// TODO: Find a way to merge the response with the handler's error
log.Error().Str("commandName", name).Err(err).Msg("Command Handler Error")
// Respond with error
err = utils.RespondError(internalSession, interaction.Interaction, fmt.Sprintf("Unexpected Error: %s", err.Error()), nil)
if err != nil {
log.Error().Stack().Str("commandName", name).Err(err).Msg("Failed to respond with error feedback")
}
}
} else {
log.Error().Stack().Str("commandName", name).Msg("Command Interaction Has No Handler")
// Respond with error
utils.RespondError(internalSession, interaction.Interaction, "Unexpected Error: interaction has no handler", nil)
}
})
}