diff --git a/pkg/controller/discord/new.go b/pkg/controller/discord/new.go index 6e7b1e468..916476fa4 100644 --- a/pkg/controller/discord/new.go +++ b/pkg/controller/discord/new.go @@ -21,6 +21,7 @@ type IController interface { ListDiscordResearchTopics(ctx context.Context, days, limit, offset int) ([]model.DiscordResearchTopic, int64, error) UserOgifStats(ctx context.Context, discordID string, after time.Time) (OgifStats, error) GetOgifLeaderboard(ctx context.Context, after time.Time, limit int) ([]model.OgifLeaderboardRecord, error) + ListDiscordChannelMessageLogs(ctx context.Context, channelID string, startTime *time.Time, endTime *time.Time) ([]model.DiscordTextMessageLog, error) } type controller struct { @@ -335,3 +336,60 @@ func (c *controller) GetOgifLeaderboard(ctx context.Context, after time.Time, li return leaderboard, nil } + +func (c *controller) ListDiscordChannelMessageLogs(ctx context.Context, channelID string, startTime *time.Time, endTime *time.Time) ([]model.DiscordTextMessageLog, error) { + threads, err := c.service.Discord.ListActiveThreadsByChannelID(c.config.Discord.IDs.DwarvesGuild, channelID) + if err != nil { + return nil, err + } + channelIDs := make([]string, 0) + channelIDs = append(channelIDs, channelID) + for _, thread := range threads { + channelIDs = append(channelIDs, thread.ID) + } + + members, err := c.service.Discord.GetMembers() + if err != nil { + return nil, err + } + + membersMap := make(map[string]string) + for _, mem := range members { + membersMap[fmt.Sprintf("<@%s>", mem.User.ID)] = fmt.Sprintf("@%s", mem.User.Username) + } + + messageLogs := make([]model.DiscordTextMessageLog, 0) + + for _, channnelID := range channelIDs { + messages, err := c.service.Discord.GetChannelMessagesInDateRange(channnelID, 100, startTime, endTime) + if err != nil { + return nil, err + } + + for _, msg := range messages { + messageLogs = append(messageLogs, model.DiscordTextMessageLog{ + ID: msg.ID, + Content: msg.Content, + AuthorName: msg.Author.Username, + AuthorID: msg.Author.ID, + ChannelID: msg.ChannelID, + GuildID: msg.GuildID, + Timestamp: msg.Timestamp, + }) + } + } + + for _, msg := range messageLogs { + content := msg.Content + for id, mem := range membersMap { + if strings.Contains(content, id) { + content = strings.ReplaceAll(content, id, mem) + } + } + } + + sort.Slice(messageLogs, func(i, j int) bool { + return messageLogs[i].Timestamp.Before(messageLogs[j].Timestamp) + }) + return messageLogs, nil +} diff --git a/pkg/handler/discord/discord.go b/pkg/handler/discord/discord.go index 9a7cc9581..97d9e9b23 100644 --- a/pkg/handler/discord/discord.go +++ b/pkg/handler/discord/discord.go @@ -776,3 +776,51 @@ func (h *handler) SweepOgifEvent(c *gin.Context) { c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, nil, nil, "events swept successfully")) } + +// ListChannelMessageLogs godoc +// @Summary Get list of messages in channel and its thread +// @Description Get list of messages in channel and its thread +// @id ListChannelMessageLogs +// @Tags Discord +// @Accept json +// @Produce json +// @Param discord_channel_id path string true "Channel Discord ID" +// @Param startDate query string true "Start Date" +// @Param endDate query string true "End Date" +// @Success 200 {object} ListDiscordTextMessageLog +// @Failure 400 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /discords/channels/{discord_channel_id}/message-logs [get] +func (h *handler) ListChannelMessageLogs(c *gin.Context) { + var input = request.GetChannelMessagesInput{ + DiscordChannelID: c.Param("discord_channel_id"), + } + + if err := c.ShouldBindQuery(&input); err != nil { + c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, err, input, "bind query failed")) + return + } + + if err := input.Validate(); err != nil { + c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, err, input, "")) + return + } + + startDate := input.GetStartDate() + endDate := input.GetEndDate() + + // maximum 1 month messages + oneMonth := time.Hour * 24 * 365 + if endDate.Sub(*startDate) > oneMonth { + newEndDate := startDate.Add(oneMonth) + endDate = &newEndDate + } + + messages, err := h.controller.Discord.ListDiscordChannelMessageLogs(c, input.DiscordChannelID, startDate, endDate) + if err != nil { + c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, "")) + return + } + c.JSON(http.StatusOK, view.CreateResponse(view.ToListDiscordTextMessageLog(messages), nil, nil, nil, "")) +} diff --git a/pkg/handler/discord/errs/errors.go b/pkg/handler/discord/errs/errors.go index c3ffaaead..66f2c8d35 100644 --- a/pkg/handler/discord/errs/errors.go +++ b/pkg/handler/discord/errs/errors.go @@ -3,12 +3,14 @@ package errs import "errors" var ( - ErrEmptyReportView = errors.New("view is empty") - ErrEmptyChannelID = errors.New("channelID is empty") - ErrEmptyGuildID = errors.New("guildID is empty") - ErrEmptyCreatorID = errors.New("creatorID is empty") - ErrEmptyName = errors.New("name is empty") - ErrEmptyDate = errors.New("date is nil") - ErrEmptyID = errors.New("discord user id is nil") - ErrEmptyTopic = errors.New("topic is nil") + ErrEmptyReportView = errors.New("view is empty") + ErrEmptyChannelID = errors.New("channelID is empty") + ErrEmptyGuildID = errors.New("guildID is empty") + ErrEmptyCreatorID = errors.New("creatorID is empty") + ErrEmptyName = errors.New("name is empty") + ErrEmptyDate = errors.New("date is nil") + ErrEmptyID = errors.New("discord user id is nil") + ErrEmptyTopic = errors.New("topic is nil") + ErrInvalidDate = errors.New("date is invalid") + ErrInvalidTimeRange = errors.New("start time must be before end time") ) diff --git a/pkg/handler/discord/interface.go b/pkg/handler/discord/interface.go index edd7200b6..421632932 100644 --- a/pkg/handler/discord/interface.go +++ b/pkg/handler/discord/interface.go @@ -18,4 +18,5 @@ type IHandler interface { UserOgifStats(c *gin.Context) OgifLeaderboard(c *gin.Context) SweepOgifEvent(c *gin.Context) + ListChannelMessageLogs(c *gin.Context) } diff --git a/pkg/handler/discord/request/request.go b/pkg/handler/discord/request/request.go index b624a5f68..47e62c714 100644 --- a/pkg/handler/discord/request/request.go +++ b/pkg/handler/discord/request/request.go @@ -97,3 +97,51 @@ func (input DiscordEventSpeakerInput) Validate() error { } return nil } + +type GetChannelMessagesInput struct { + DiscordChannelID string + StartDate string `json:"startDate" form:"startDate"` + EndDate string `json:"endDate" form:"endDate"` +} + +func (input GetChannelMessagesInput) Validate() error { + if len(input.DiscordChannelID) == 0 { + return errs.ErrEmptyChannelID + } + if (len(input.StartDate) == 0) || (len(input.EndDate) == 0) { + return errs.ErrEmptyDate + } + + sTime, err := time.Parse("2006-01-02", input.StartDate) + if err != nil { + return errs.ErrInvalidDate + } + eTime, err := time.Parse("2006-01-02", input.EndDate) + if err != nil { + return errs.ErrInvalidDate + } + + if sTime.After(eTime) { + return errs.ErrInvalidTimeRange + } + + return nil +} + +func (input GetChannelMessagesInput) GetStartDate() *time.Time { + date, err := time.Parse("2006-01-02", input.StartDate) + if err != nil { + return nil + } + + return &date +} + +func (input GetChannelMessagesInput) GetEndDate() *time.Time { + date, err := time.Parse("2006-01-02", input.EndDate) + if err != nil { + return nil + } + + return &date +} diff --git a/pkg/model/discord_message.go b/pkg/model/discord_message.go index e9a9caccb..7bda40740 100644 --- a/pkg/model/discord_message.go +++ b/pkg/model/discord_message.go @@ -1,6 +1,10 @@ package model -import "github.com/bwmarrin/discordgo" +import ( + "time" + + "github.com/bwmarrin/discordgo" +) type DiscordMessage struct { AvatarURL string `json:"avatar_url"` @@ -66,3 +70,13 @@ type OriginalDiscordMessage struct { Author *discordgo.User Roles []string } + +type DiscordTextMessageLog struct { + ID string + Content string + AuthorName string + AuthorID string + ChannelID string + GuildID string + Timestamp time.Time +} diff --git a/pkg/routes/v1.go b/pkg/routes/v1.go index 5fc8cef4b..5b70b8ba4 100644 --- a/pkg/routes/v1.go +++ b/pkg/routes/v1.go @@ -381,13 +381,14 @@ func loadV1Routes(r *gin.Engine, h *handler.Handler, repo store.DBRepo, s *store discordGroup.GET("/mma-scores", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Employee.ListWithMMAScore) discordGroup.POST("/advance-salary", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Employee.SalaryAdvance) discordGroup.POST("/check-advance-salary", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Employee.CheckSalaryAdvance) - discordGroup.GET("/salary-advance-report", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Employee.SalaryAdvanceReport) discordGroup.GET("/:discord_id/earns/transactions", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Employee.GetEmployeeEarnTransactions) discordGroup.GET("/:discord_id/earns/total", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Employee.GetEmployeeTotalEarn) discordGroup.GET("/earns/total", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Employee.GetTotalEarn) discordGroup.POST("/office-checkin", amw.WithAuth, pmw.WithPerm(model.PermissionTransferCheckinIcy), h.Employee.OfficeCheckIn) + discordGroup.GET("/channels/:discord_channel_id/message-logs", h.Discord.ListChannelMessageLogs) + discordGroup.GET("/icy-accounting", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Icy.Accounting) scheduledEventGroup := discordGroup.Group("/scheduled-events") diff --git a/pkg/routes/v1_test.go b/pkg/routes/v1_test.go index 5834588d0..3d0c58981 100644 --- a/pkg/routes/v1_test.go +++ b/pkg/routes/v1_test.go @@ -986,6 +986,12 @@ func Test_loadV1Routes(t *testing.T) { Handler: "github.com/dwarvesf/fortress-api/pkg/handler/employee.IHandler.GetEmployeeTotalEarn-fm", }, }, + "/api/v1/discords/channels/:discord_channel_id/message-logs": { + "GET": { + Method: "GET", + Handler: "github.com/dwarvesf/fortress-api/pkg/handler/discord.IHandler.ListChannelMessageLogs-fm", + }, + }, "/api/v1/discords/icy-accounting": { "GET": { Method: "GET", diff --git a/pkg/service/discord/discord.go b/pkg/service/discord/discord.go index 14502dd96..02a49473e 100644 --- a/pkg/service/discord/discord.go +++ b/pkg/service/discord/discord.go @@ -826,3 +826,31 @@ func (d *discordClient) ListActiveThreadsByChannelID(guildID, channelID string) return result, nil } + +func (d *discordClient) GetChannelMessagesInDateRange(channelID string, limit int, startDate, endDate *time.Time) ([]*discordgo.Message, error) { + before := "" + messages := make([]*discordgo.Message, 0) + for { + discordMessages, err := d.session.ChannelMessages(channelID, limit, before, "", "") + if err != nil { + return nil, err + } + + if len(discordMessages) == 0 { + break + } + + before = discordMessages[len(discordMessages)-1].ID + + for _, msg := range discordMessages { + markedTime := msg.Timestamp + if markedTime.Before(*startDate) { + return messages, nil + } + if markedTime.After(*startDate) && markedTime.Before(*endDate) { + messages = append(messages, msg) + } + } + } + return messages, nil +} diff --git a/pkg/service/discord/service.go b/pkg/service/discord/service.go index 1769c213c..0d50b2e68 100644 --- a/pkg/service/discord/service.go +++ b/pkg/service/discord/service.go @@ -1,6 +1,8 @@ package discord import ( + "time" + "github.com/bwmarrin/discordgo" "github.com/dwarvesf/fortress-api/pkg/model" @@ -42,4 +44,5 @@ type IService interface { SendDiscordMessageWithChannel(ses *discordgo.Session, msg *discordgo.Message, channelId string) error ListActiveThreadsByChannelID(guildID, channelID string) ([]discordgo.Channel, error) + GetChannelMessagesInDateRange(channelID string, limit int, startDate, endDate *time.Time) ([]*discordgo.Message, error) } diff --git a/pkg/view/discord_message.go b/pkg/view/discord_message.go new file mode 100644 index 000000000..8416f7b03 --- /dev/null +++ b/pkg/view/discord_message.go @@ -0,0 +1,43 @@ +package view + +import ( + "time" + + "github.com/dwarvesf/fortress-api/pkg/model" +) + +type DiscordTextMessageLog struct { + ID string `json:"id"` + Content string `json:"content"` + AuthorName string `json:"author_name"` + AuthorID string `json:"author_id"` + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` + Timestamp time.Time `json:"timestamp"` +} + +type ListDiscordTextMessageLog struct { + Data []DiscordTextMessageLog `json:"data"` +} // @name ListDiscordTextMessageLog + +func ToDiscordTextMessageLog(message model.DiscordTextMessageLog) DiscordTextMessageLog { + return DiscordTextMessageLog{ + ID: message.ID, + Content: message.Content, + AuthorName: message.AuthorName, + AuthorID: message.AuthorID, + ChannelID: message.ChannelID, + GuildID: message.GuildID, + Timestamp: message.Timestamp, + } +} + +func ToListDiscordTextMessageLog(messages []model.DiscordTextMessageLog) []DiscordTextMessageLog { + var results = make([]DiscordTextMessageLog, 0, len(messages)) + + for _, message := range messages { + results = append(results, ToDiscordTextMessageLog(message)) + } + + return results +}