From 49fa964d3af220793f04e5833c793a71635df7d8 Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 26 Aug 2025 11:21:38 -0500 Subject: [PATCH] refactor: rearrange & rename files, fix meeting times response decode --- cmd/banner/main.go | 14 +++++----- internal/api/api.go | 26 +++++++++-------- internal/api/scrape.go | 10 +++---- internal/api/session.go | 4 +-- internal/{utils => api}/term.go | 7 +---- internal/bot/commands.go | 28 +++++++++---------- internal/bot/handlers.go | 16 +++++------ internal/bot/{bot.go => state.go} | 3 +- internal/{utils/logs.go => config/logging.go} | 2 +- internal/{utils => }/errors.go | 2 +- internal/{utils => }/helpers.go | 2 +- internal/{utils => }/meta.go | 2 +- internal/models/types.go | 14 ++++------ 13 files changed, 63 insertions(+), 67 deletions(-) rename internal/{utils => api}/term.go (95%) rename internal/bot/{bot.go => state.go} (91%) rename internal/{utils/logs.go => config/logging.go} (99%) rename internal/{utils => }/errors.go (93%) rename internal/{utils => }/helpers.go (99%) rename internal/{utils => }/meta.go (99%) diff --git a/cmd/banner/main.go b/cmd/banner/main.go index 51a543e..1a623c9 100644 --- a/cmd/banner/main.go +++ b/cmd/banner/main.go @@ -22,10 +22,10 @@ import ( "github.com/samber/lo" "resty.dev/v3" + "banner/internal" "banner/internal/api" "banner/internal/bot" "banner/internal/config" - "banner/internal/utils" ) var ( @@ -57,25 +57,25 @@ func init() { zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack // Use the custom console writer if we're in development - isDevelopment := utils.GetFirstEnv("ENVIRONMENT", "RAILWAY_ENVIRONMENT") + isDevelopment := internal.GetFirstEnv("ENVIRONMENT", "RAILWAY_ENVIRONMENT") if isDevelopment == "" { isDevelopment = "development" } if isDevelopment == "development" { - log.Logger = zerolog.New(utils.NewConsoleWriter()).With().Timestamp().Logger() + log.Logger = zerolog.New(config.NewConsoleWriter()).With().Timestamp().Logger() } else { - log.Logger = zerolog.New(utils.LogSplitter{Std: os.Stdout, Err: os.Stderr}).With().Timestamp().Logger() + log.Logger = zerolog.New(config.LogSplitter{Std: os.Stdout, Err: os.Stderr}).With().Timestamp().Logger() } log.Debug().Str("environment", isDevelopment).Msg("Loggers Setup") // Set discordgo's logger to use zerolog - discordgo.Logger = utils.DiscordGoLogger + discordgo.Logger = internal.DiscordGoLogger } func initRedis(cfg *config.Config) { // Setup redis - redisUrl := utils.GetFirstEnv("REDIS_URL", "REDIS_PRIVATE_URL") + redisUrl := internal.GetFirstEnv("REDIS_URL", "REDIS_PRIVATE_URL") if redisUrl == "" { log.Fatal().Stack().Msg("REDIS_URL/REDIS_PRIVATE_URL not set") } @@ -130,7 +130,7 @@ func main() { } // Try to grab the environment variable, or default to development - environment := utils.GetFirstEnv("ENVIRONMENT", "RAILWAY_ENVIRONMENT") + environment := internal.GetFirstEnv("ENVIRONMENT", "RAILWAY_ENVIRONMENT") if environment == "" { environment = "development" } diff --git a/internal/api/api.go b/internal/api/api.go index 523cb18..ff1f6c3 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,9 +1,9 @@ package api import ( + "banner/internal" "banner/internal/config" "banner/internal/models" - "banner/internal/utils" "context" "encoding/json" "errors" @@ -51,7 +51,7 @@ func SessionMiddleware(c *resty.Client, r *resty.Response) error { // GenerateSession generates a new session ID (nonce) for use with the Banner API. // Don't use this function directly, use GetSession instead. func GenerateSession() string { - return utils.RandomString(5) + utils.Nonce() + return internal.RandomString(5) + internal.Nonce() } var terms []BannerTerm @@ -131,7 +131,7 @@ func (a *API) GetTerms(search string, page int, maxResults int) ([]BannerTerm, e SetQueryParam("searchTerm", search). SetQueryParam("offset", strconv.Itoa(page)). SetQueryParam("max", strconv.Itoa(maxResults)). - SetQueryParam("_", utils.Nonce()). + SetQueryParam("_", internal.Nonce()). SetExpectResponseContentType("application/json"). SetResult(&[]BannerTerm{}) @@ -206,7 +206,7 @@ func (a *API) GetPartOfTerms(search string, term int, offset int, maxResults int SetQueryParam("offset", strconv.Itoa(offset)). SetQueryParam("max", strconv.Itoa(maxResults)). SetQueryParam("uniqueSessionId", a.EnsureSession()). - SetQueryParam("_", utils.Nonce()). + SetQueryParam("_", internal.Nonce()). SetExpectResponseContentType("application/json"). SetResult(&[]BannerTerm{}) @@ -239,7 +239,7 @@ func (a *API) GetInstructors(search string, term string, offset int, maxResults SetQueryParam("offset", strconv.Itoa(offset)). SetQueryParam("max", strconv.Itoa(maxResults)). SetQueryParam("uniqueSessionId", a.EnsureSession()). - SetQueryParam("_", utils.Nonce()). + SetQueryParam("_", internal.Nonce()). SetExpectResponseContentType("application/json"). SetResult(&[]Instructor{}) @@ -337,7 +337,7 @@ func (a *API) GetSubjects(search string, term string, offset int, maxResults int SetQueryParam("offset", strconv.Itoa(offset)). SetQueryParam("max", strconv.Itoa(maxResults)). SetQueryParam("uniqueSessionId", a.EnsureSession()). - SetQueryParam("_", utils.Nonce()). + SetQueryParam("_", internal.Nonce()). SetExpectResponseContentType("application/json"). SetResult(&[]Pair{}) @@ -370,7 +370,7 @@ func (a *API) GetCampuses(search string, term int, offset int, maxResults int) ( SetQueryParam("offset", strconv.Itoa(offset)). SetQueryParam("max", strconv.Itoa(maxResults)). SetQueryParam("uniqueSessionId", a.EnsureSession()). - SetQueryParam("_", utils.Nonce()). + SetQueryParam("_", internal.Nonce()). SetExpectResponseContentType("application/json"). SetResult(&[]Pair{}) @@ -403,7 +403,7 @@ func (a *API) GetInstructionalMethods(search string, term string, offset int, ma SetQueryParam("offset", strconv.Itoa(offset)). SetQueryParam("max", strconv.Itoa(maxResults)). SetQueryParam("uniqueSessionId", a.EnsureSession()). - SetQueryParam("_", utils.Nonce()). + SetQueryParam("_", internal.Nonce()). SetExpectResponseContentType("application/json"). SetResult(&[]Pair{}) @@ -423,23 +423,27 @@ func (a *API) GetInstructionalMethods(search string, term string, offset int, ma // It makes an HTTP GET request to the appropriate API endpoint and parses the response to extract the meeting time data. // The function returns a MeetingTimeResponse struct containing the extracted information. func (a *API) GetCourseMeetingTime(term int, crn int) ([]models.MeetingTimeResponse, error) { + type responseWrapper struct { + Fmt []models.MeetingTimeResponse `json:"fmt"` + } + req := a.config.Client.NewRequest(). SetQueryParam("term", strconv.Itoa(term)). SetQueryParam("courseReferenceNumber", strconv.Itoa(crn)). SetExpectResponseContentType("application/json"). - SetResult(&[]models.MeetingTimeResponse{}) + SetResult(&responseWrapper{}) res, err := req.Get("/searchResults/getFacultyMeetingTimes") if err != nil { return nil, fmt.Errorf("failed to get meeting time: %w", err) } - meetingTimes, ok := res.Result().(*[]models.MeetingTimeResponse) + result, ok := res.Result().(*responseWrapper) if !ok { return nil, fmt.Errorf("meeting times parsing failed to cast: %v", res.Result()) } - return *meetingTimes, nil + return result.Fmt, nil } // ResetDataForm makes a POST request that needs to be made upon before new search requests can be made. diff --git a/internal/api/scrape.go b/internal/api/scrape.go index be4810b..0f3a038 100644 --- a/internal/api/scrape.go +++ b/internal/api/scrape.go @@ -1,8 +1,8 @@ package api import ( + "banner/internal" "banner/internal/models" - "banner/internal/utils" "context" "fmt" "math/rand" @@ -70,7 +70,7 @@ func (a *API) Scrape() error { // GetExpiredSubjects returns a list of subjects that are expired and should be scraped. func (a *API) GetExpiredSubjects() ([]string, error) { - term := utils.Default(time.Now()).ToString() + term := Default(time.Now()).ToString() subjects := make([]string, 0) // Create a timeout context for Redis operations @@ -109,7 +109,7 @@ func (a *API) ScrapeMajor(subject string) error { for { // Build & execute the query query := NewQuery().Offset(offset).MaxResults(MaxPageSize * 2).Subject(subject) - term := utils.Default(time.Now()).ToString() + term := Default(time.Now()).ToString() result, err := a.Search(term, query, "subjectDescription", false) if err != nil { return fmt.Errorf("search failed: %w (%s)", err, query.String()) @@ -152,7 +152,7 @@ func (a *API) ScrapeMajor(subject string) error { break } - term := utils.Default(time.Now()).ToString() + term := Default(time.Now()).ToString() // Calculate the expiry time for the scrape (1 hour for every 200 classes, random +-15%) with a minimum of 1 hour var scrapeExpiry time.Duration @@ -190,7 +190,7 @@ func (a *API) CalculateExpiry(term string, count int, priority bool) time.Durati // Subjects with less than 50 classes have a reversed expiry (less classes, longer interval) // 1 class => 12 hours, 49 classes => 1 hour if count < 50 { - hours := utils.Slope(utils.Point{X: 1, Y: 12}, utils.Point{X: 49, Y: 1}, float64(count)).Y + hours := internal.Slope(internal.Point{X: 1, Y: 12}, internal.Point{X: 49, Y: 1}, float64(count)).Y baseExpiry = time.Duration(hours * float64(time.Hour)) } diff --git a/internal/api/session.go b/internal/api/session.go index 7de12f1..4ce8e6e 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -1,7 +1,7 @@ package api import ( - "banner/internal/utils" + "banner/internal" "net/url" log "github.com/rs/zerolog/log" @@ -18,7 +18,7 @@ func (a *API) Setup() { for _, path := range requestQueue { req := a.config.Client.NewRequest(). - SetQueryParam("_", utils.Nonce()). + SetQueryParam("_", internal.Nonce()). SetExpectResponseContentType("application/json") res, err := req.Get(path) diff --git a/internal/utils/term.go b/internal/api/term.go similarity index 95% rename from internal/utils/term.go rename to internal/api/term.go index 5fd79a8..ab863bb 100644 --- a/internal/utils/term.go +++ b/internal/api/term.go @@ -1,12 +1,10 @@ -package utils +package api import ( "banner/internal/config" "fmt" "strconv" "time" - - "github.com/rs/zerolog/log" ) // Term selection should yield smart results based on the current time, as well as the input provided. @@ -32,9 +30,6 @@ var ( func init() { loc, _ := time.LoadLocation(config.CentralTimezoneName) SpringRange, SummerRange, FallRange = GetYearDayRange(loc, uint16(time.Now().Year())) - - currentTerm, nextTerm := GetCurrentTerm(time.Now()) - log.Debug().Str("CurrentTerm", fmt.Sprintf("%+v", currentTerm)).Str("NextTerm", fmt.Sprintf("%+v", nextTerm)).Msg("GetCurrentTerm") } type YearDayRange struct { diff --git a/internal/bot/commands.go b/internal/bot/commands.go index 2206a99..8353967 100644 --- a/internal/bot/commands.go +++ b/internal/bot/commands.go @@ -1,9 +1,9 @@ package bot import ( + "banner/internal" "banner/internal/api" "banner/internal/models" - "banner/internal/utils" "fmt" "net/url" "regexp" @@ -40,7 +40,7 @@ var SearchCommandDefinition = &discordgo.ApplicationCommand{ Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, - MinLength: utils.GetIntPointer(0), + MinLength: internal.GetIntPointer(0), MaxLength: 48, Name: "title", Description: "Course Title (exact, use autocomplete)", @@ -50,7 +50,7 @@ var SearchCommandDefinition = &discordgo.ApplicationCommand{ { Type: discordgo.ApplicationCommandOptionString, Name: "code", - MinLength: utils.GetIntPointer(4), + MinLength: internal.GetIntPointer(4), Description: "Course Code (e.g. 3743, 3000-3999, 3xxx, 3000-)", Required: false, }, @@ -248,8 +248,8 @@ func SearchCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.Interaction Data: &discordgo.InteractionResponseData{ Embeds: []*discordgo.MessageEmbed{ { - Footer: utils.GetFetchedFooter(b.Config, fetch_time), - Description: fmt.Sprintf("%d Class%s", courses.TotalCount, utils.Plural(courses.TotalCount)), + Footer: internal.GetFetchedFooter(b.Config, fetch_time), + Description: fmt.Sprintf("%d Class%s", courses.TotalCount, internal.Plural(courses.TotalCount)), Fields: fields[:min(25, len(fields))], Color: color, }, @@ -267,7 +267,7 @@ var TermCommandDefinition = &discordgo.ApplicationCommand{ Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, - MinLength: utils.GetIntPointer(0), + MinLength: internal.GetIntPointer(0), MaxLength: 8, Name: "search", Description: "Term to search for", @@ -278,7 +278,7 @@ var TermCommandDefinition = &discordgo.ApplicationCommand{ Name: "page", Description: "Page Number", Required: false, - MinValue: utils.GetFloatPointer(1), + MinValue: internal.GetFloatPointer(1), }, }, } @@ -303,7 +303,7 @@ func TermCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCr termResult, err := b.API.GetTerms(searchTerm, pageNumber, 25) if err != nil { - utils.RespondError(s, i.Interaction, "Error while fetching terms", err) + internal.RespondError(s, i.Interaction, "Error while fetching terms", err) return err } @@ -328,8 +328,8 @@ func TermCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCr Data: &discordgo.InteractionResponseData{ Embeds: []*discordgo.MessageEmbed{ { - Footer: utils.GetFetchedFooter(b.Config, fetch_time), - Description: fmt.Sprintf("%d term%s (page %d)", len(termResult), utils.Plural(len(termResult)), pageNumber), + Footer: internal.GetFetchedFooter(b.Config, fetch_time), + Description: fmt.Sprintf("%d term%s (page %d)", len(termResult), internal.Plural(len(termResult)), pageNumber), Fields: fields[:min(25, len(fields))], }, }, @@ -387,7 +387,7 @@ func TimeCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCr Data: &discordgo.InteractionResponseData{ Embeds: []*discordgo.MessageEmbed{ { - Footer: utils.GetFetchedFooter(b.Config, fetch_time), + Footer: internal.GetFetchedFooter(b.Config, fetch_time), Description: "", Fields: []*discordgo.MessageEmbedField{ { @@ -404,7 +404,7 @@ func TimeCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCr }, { Name: "Days of Week", - Value: utils.WeekdaysToString(meetingTime.Days()), + Value: internal.WeekdaysToString(meetingTime.Days()), }, }, }, @@ -430,7 +430,7 @@ var IcsCommandDefinition = &discordgo.ApplicationCommand{ func IcsCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCreate) error { // Parse all options - options := utils.ParseOptions(i.ApplicationCommandData().Options) + options := internal.ParseOptions(i.ApplicationCommandData().Options) crn := options.GetInt("crn") course, err := b.API.GetCourse(strconv.Itoa(int(crn))) @@ -460,7 +460,7 @@ func IcsCommandHandler(b *Bot, s *discordgo.Session, i *discordgo.InteractionCre if !exists { log.Warn().Str("crn", course.CourseReferenceNumber).Msg("Non-meeting course requested for ICS file") - utils.RespondError(s, i.Interaction, "The course requested does not meet at a defined moment in time.", nil) + internal.RespondError(s, i.Interaction, "The course requested does not meet at a defined moment in time.", nil) return nil } diff --git a/internal/bot/handlers.go b/internal/bot/handlers.go index cc9b00d..f6dae07 100644 --- a/internal/bot/handlers.go +++ b/internal/bot/handlers.go @@ -1,7 +1,7 @@ package bot import ( - "banner/internal/utils" + "banner/internal" "fmt" "github.com/bwmarrin/discordgo" @@ -13,7 +13,7 @@ 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) + err := internal.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") } @@ -28,18 +28,18 @@ func (b *Bot) RegisterHandlers() { options.Str(option.Name, fmt.Sprintf("%v", option.Value)) } - event := log.Info().Str("name", name).Str("user", utils.GetUser(interaction).Username).Dict("options", options) + event := log.Info().Str("name", name).Str("user", internal.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)) + guild.Str("name", internal.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)) + guild.Str("name", internal.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 @@ -58,7 +58,7 @@ func (b *Bot) RegisterHandlers() { 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) + err := internal.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") } @@ -74,7 +74,7 @@ func (b *Bot) RegisterHandlers() { 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) + err = internal.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") } @@ -84,7 +84,7 @@ func (b *Bot) RegisterHandlers() { 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) + internal.RespondError(internalSession, interaction.Interaction, "Unexpected Error: interaction has no handler", nil) } }) } diff --git a/internal/bot/bot.go b/internal/bot/state.go similarity index 91% rename from internal/bot/bot.go rename to internal/bot/state.go index abd6b5e..4cb888f 100644 --- a/internal/bot/bot.go +++ b/internal/bot/state.go @@ -3,7 +3,6 @@ package bot import ( "banner/internal/api" "banner/internal/config" - "banner/internal/utils" "fmt" "time" @@ -28,7 +27,7 @@ func (b *Bot) SetClosing() { func (b *Bot) GetSession() (string, error) { sessionID := b.API.EnsureSession() - term := utils.Default(time.Now()).ToString() + term := api.Default(time.Now()).ToString() log.Info().Str("term", term).Str("sessionID", sessionID).Msg("Setting selected term") err := b.API.SelectTerm(term, sessionID) diff --git a/internal/utils/logs.go b/internal/config/logging.go similarity index 99% rename from internal/utils/logs.go rename to internal/config/logging.go index c4bc7f1..4440bed 100644 --- a/internal/utils/logs.go +++ b/internal/config/logging.go @@ -1,4 +1,4 @@ -package utils +package config import ( "io" diff --git a/internal/utils/errors.go b/internal/errors.go similarity index 93% rename from internal/utils/errors.go rename to internal/errors.go index a041996..3991c92 100644 --- a/internal/utils/errors.go +++ b/internal/errors.go @@ -1,4 +1,4 @@ -package utils +package internal import "fmt" diff --git a/internal/utils/helpers.go b/internal/helpers.go similarity index 99% rename from internal/utils/helpers.go rename to internal/helpers.go index 69c9815..45dac8e 100644 --- a/internal/utils/helpers.go +++ b/internal/helpers.go @@ -1,4 +1,4 @@ -package utils +package internal import ( "fmt" diff --git a/internal/utils/meta.go b/internal/meta.go similarity index 99% rename from internal/utils/meta.go rename to internal/meta.go index c6713f1..5959c2c 100644 --- a/internal/utils/meta.go +++ b/internal/meta.go @@ -1,4 +1,4 @@ -package utils +package internal import ( "banner/internal/config" diff --git a/internal/models/types.go b/internal/models/types.go index d7c5017..2dc542d 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -1,7 +1,7 @@ package models import ( - "banner/internal/utils" + "banner/internal" "encoding/json" "fmt" "strconv" @@ -11,8 +11,6 @@ import ( log "github.com/rs/zerolog/log" ) -const JsonContentType = "application/json" - type FacultyItem struct { BannerId string `json:"bannerId"` Category *string `json:"category"` @@ -114,7 +112,7 @@ func (m *MeetingTimeResponse) TimeString() string { return "???" } - return fmt.Sprintf("%s %s-%s", utils.WeekdaysToString(m.Days()), m.StartTime().String(), m.EndTime().String()) + return fmt.Sprintf("%s %s-%s", internal.WeekdaysToString(m.Days()), m.StartTime().String(), m.EndTime().String()) } // PlaceString returns a formatted string best representing the place of the meeting time @@ -195,7 +193,7 @@ func (m *MeetingTimeResponse) EndDay() time.Time { // StartTime returns the start time of the meeting time as a NaiveTime object // This is not cached and is parsed on each invocation. It may also panic without handling. -func (m *MeetingTimeResponse) StartTime() *utils.NaiveTime { +func (m *MeetingTimeResponse) StartTime() *internal.NaiveTime { raw := m.MeetingTime.BeginTime if raw == "" { log.Panic().Stack().Msg("Start time is empty") @@ -206,12 +204,12 @@ func (m *MeetingTimeResponse) StartTime() *utils.NaiveTime { log.Panic().Stack().Err(err).Str("raw", raw).Msg("Cannot parse start time integer") } - return utils.ParseNaiveTime(value) + return internal.ParseNaiveTime(value) } // EndTime returns the end time of the meeting time as a NaiveTime object // This is not cached and is parsed on each invocation. It may also panic without handling. -func (m *MeetingTimeResponse) EndTime() *utils.NaiveTime { +func (m *MeetingTimeResponse) EndTime() *internal.NaiveTime { raw := m.MeetingTime.EndTime if raw == "" { return nil @@ -222,7 +220,7 @@ func (m *MeetingTimeResponse) EndTime() *utils.NaiveTime { log.Panic().Stack().Err(err).Str("raw", raw).Msg("Cannot parse end time integer") } - return utils.ParseNaiveTime(value) + return internal.ParseNaiveTime(value) } type RRule struct {