From e44cf840c04c2de52888c0bd1e8e6b508b1cabe8 Mon Sep 17 00:00:00 2001 From: bean Date: Fri, 30 Aug 2024 13:23:29 +0700 Subject: [PATCH] feat: api get ogif stats (#744) Co-authored-by: baenv --- pkg/controller/discord/new.go | 49 +++++++++++++ pkg/handler/discord/discord.go | 27 ++++++++ pkg/handler/discord/interface.go | 1 + pkg/model/event.go | 18 ++--- pkg/routes/v1.go | 5 ++ pkg/routes/v1_test.go | 6 ++ pkg/store/eventspeaker/event_speaker.go | 92 +++++++++++++++++++++++++ pkg/store/eventspeaker/interface.go | 5 ++ pkg/view/event_speaker.go | 19 +++++ 9 files changed, 214 insertions(+), 8 deletions(-) create mode 100644 pkg/view/event_speaker.go diff --git a/pkg/controller/discord/new.go b/pkg/controller/discord/new.go index 5fd274088..46adb18c4 100644 --- a/pkg/controller/discord/new.go +++ b/pkg/controller/discord/new.go @@ -19,6 +19,7 @@ type IController interface { Log(in model.LogDiscordInput) error PublicAdvanceSalaryLog(in model.LogDiscordInput) error ListDiscordResearchTopics(ctx context.Context, days, limit, offset int) ([]model.DiscordResearchTopic, int64, error) + UserOgifStats(ctx context.Context, discordID string, after time.Time) (OgifStats, error) } type controller struct { @@ -272,3 +273,51 @@ func (c *controller) topicPopularity(topicID string, days int) (int64, []model.D return totalMessages, result, lastActiveTime, nil } + +// OgifStats contains list of ogif and some stats +type OgifStats struct { + OgifList []model.EventSpeaker `json:"ogifList"` + UserAllTimeSpeaksCount int64 `json:"userAllTimeSpeaksCount"` + UserAllTimeRank int64 `json:"userAllTimeRank"` + UserCurrentSpeaksCount int64 `json:"userCurrentSpeaksCount"` + UserCurrentRank int64 `json:"userCurrentRank"` + TotalSpeakCount int64 `json:"totalSpeakCount"` + CurrentSpeakCount int64 `json:"currentSpeakCount"` +} + +// UserOgifStats returns list ogif with some stats +func (c *controller) UserOgifStats(ctx context.Context, discordID string, after time.Time) (OgifStats, error) { + logger := c.logger.AddField("discordID", discordID).AddField("after", after) + + ogftList, err := c.store.EventSpeaker.List(c.repo.DB(), discordID, &after, "ogif") + if err != nil { + logger.Error(err, "error when retrieving list event speaker") + return OgifStats{}, err + } + + ogifStats, err := c.store.EventSpeaker.GetSpeakerStats(c.repo.DB(), discordID, &after, "ogif") + if err != nil { + logger.Error(err, "error when retrieving speaker stats") + } + + allTimeOgifStats, err := c.store.EventSpeaker.GetSpeakerStats(c.repo.DB(), discordID, nil, "ogif") + if err != nil { + logger.Error(err, "error when retrieving all time speaker stats") + return OgifStats{}, err + } + + allTimeTotalCount, err := c.store.EventSpeaker.Count(c.repo.DB(), "", nil, "ogif") + if err != nil { + logger.Error(err, "error when counting all time total speak") + } + + return OgifStats{ + OgifList: ogftList, + UserAllTimeSpeaksCount: allTimeOgifStats.TotalSpeakCount, + UserAllTimeRank: allTimeOgifStats.SpeakRank, + UserCurrentSpeaksCount: ogifStats.TotalSpeakCount, + UserCurrentRank: ogifStats.SpeakRank, + TotalSpeakCount: allTimeTotalCount, + CurrentSpeakCount: int64(len(ogftList)), + }, nil +} diff --git a/pkg/handler/discord/discord.go b/pkg/handler/discord/discord.go index 85b7191a1..674172185 100644 --- a/pkg/handler/discord/discord.go +++ b/pkg/handler/discord/discord.go @@ -703,3 +703,30 @@ func (h *handler) ListDiscordResearchTopics(c *gin.Context) { Total: total, }, nil, nil, "")) } + +func (h *handler) UserOgifStats(c *gin.Context) { + discordID := c.Query("discordID") + if discordID == "" { + c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, errors.New("discord_id is required"), nil, "")) + return + } + + var afterTime time.Time + after := c.Query("after") + if after != "" { + var err error + afterTime, err = time.Parse(time.RFC3339, after) + if err != nil { + c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, errors.New("invalid after time format"), nil, "")) + return + } + } + + stats, err := h.controller.Discord.UserOgifStats(c.Request.Context(), discordID, afterTime) + if err != nil { + c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, errors.New("discord_id is required"), nil, "")) + return + } + + c.JSON(http.StatusOK, view.CreateResponse(stats, nil, nil, nil, "")) +} diff --git a/pkg/handler/discord/interface.go b/pkg/handler/discord/interface.go index ab87b7446..ae815b143 100644 --- a/pkg/handler/discord/interface.go +++ b/pkg/handler/discord/interface.go @@ -14,4 +14,5 @@ type IHandler interface { ListScheduledEvent(c *gin.Context) SetScheduledEventSpeakers(c *gin.Context) ListDiscordResearchTopics(c *gin.Context) + UserOgifStats(c *gin.Context) } diff --git a/pkg/model/event.go b/pkg/model/event.go index f764f0206..c9e2f4ce4 100644 --- a/pkg/model/event.go +++ b/pkg/model/event.go @@ -11,20 +11,22 @@ type Event struct { Description string `json:"description"` Date time.Time `json:"date"` Image string `json:"image" gorm:"-"` - DiscordEventID string `json:"discord_event_id"` - DiscordChannelID string `json:"discord_channel_id"` - DiscordMessageID string `json:"discord_message_id"` - DiscordCreatorID string `json:"discord_creator_id"` + DiscordEventID string `json:"discordEventId"` + DiscordChannelID string `json:"discordChannelId"` + DiscordMessageID string `json:"discordMessageId"` + DiscordCreatorID string `json:"discordCreatorId"` EventType DiscordScheduledEventType `json:"type"` - EventSpeakers []EventSpeaker `json:"event_speakers"` - IsOver bool `json:"is_over" gorm:"-"` + EventSpeakers []EventSpeaker `json:"eventSpeakers"` + IsOver bool `json:"isOver" gorm:"-"` } // EventSpeaker struct type EventSpeaker struct { - EventID UUID `json:"event_id"` - DiscordAccountID UUID `json:"discord_account_id"` + EventID UUID `json:"eventId"` + DiscordAccountID UUID `json:"discordAccountId"` Topic string `json:"topic,omitempty"` + + Event *Event `json:"event"` } type DiscordScheduledEventType string diff --git a/pkg/routes/v1.go b/pkg/routes/v1.go index b1f6e571f..ecf6f1222 100644 --- a/pkg/routes/v1.go +++ b/pkg/routes/v1.go @@ -405,6 +405,11 @@ func loadV1Routes(r *gin.Engine, h *handler.Handler, repo store.DBRepo, s *store newsGroup.GET("", amw.WithAuth, h.News.Fetch) } + ogifGroup := v1.Group("/ogif") + { + ogifGroup.GET("", amw.WithAuth, pmw.WithPerm(model.PermissionEmployeesDiscordRead), h.Discord.UserOgifStats) + } + ///////////////// // PUBLIC API GROUP ///////////////// diff --git a/pkg/routes/v1_test.go b/pkg/routes/v1_test.go index cff019151..89521d5af 100644 --- a/pkg/routes/v1_test.go +++ b/pkg/routes/v1_test.go @@ -1066,6 +1066,12 @@ func Test_loadV1Routes(t *testing.T) { Handler: "github.com/dwarvesf/fortress-api/pkg/handler/youtube.IHandler.LatestBroadcast-fm", }, }, + "/api/v1/ogif": { + "GET": { + Method: "GET", + Handler: "github.com/dwarvesf/fortress-api/pkg/handler/discord.IHandler.UserOgifStats-fm", + }, + }, } l := logger.NewLogrusLogger() diff --git a/pkg/store/eventspeaker/event_speaker.go b/pkg/store/eventspeaker/event_speaker.go index 57bb03e88..0bc838d2f 100644 --- a/pkg/store/eventspeaker/event_speaker.go +++ b/pkg/store/eventspeaker/event_speaker.go @@ -1,6 +1,8 @@ package eventspeaker import ( + "time" + "gorm.io/gorm" "github.com/dwarvesf/fortress-api/pkg/model" @@ -21,3 +23,93 @@ func (s *store) Create(db *gorm.DB, e *model.EventSpeaker) (*model.EventSpeaker, func (s *store) DeleteAllByEventID(db *gorm.DB, eventID string) error { return db.Table("event_speakers").Where("event_id = ?", eventID).Delete(&model.EventSpeaker{}).Error } + +// List returns a lit of event speaker with loaded event info +func (s *store) List(db *gorm.DB, discordID string, after *time.Time, topic string) ([]model.EventSpeaker, error) { + var eventSpeakers []model.EventSpeaker + query := db.Table("event_speakers"). + Joins("JOIN discord_accounts ON event_speakers.discord_account_id = discord_accounts.id"). + Joins("JOIN events ON events.id = event_speakers.event_id"). + Order("events.date DESC") + + if after != nil { + query = query.Where("events.date > ?", after) + } + + if topic != "" { + query = query.Where("LOWER(event_speakers.topic) LIKE LOWER(?)", "%"+topic+"%") + } + + if discordID != "" { + query = query.Where("discord_accounts.discord_id = ?", discordID) + } + + err := query.Preload("Event"). + Find(&eventSpeakers).Error + if err != nil { + return nil, err + } + return eventSpeakers, nil +} + +// Count returns the total count of event speakers with the same filtering criteria as List +func (s *store) Count(db *gorm.DB, discordID string, after *time.Time, topic string) (int64, error) { + var count int64 + query := db.Table("event_speakers"). + Joins("JOIN discord_accounts ON event_speakers.discord_account_id = discord_accounts.id"). + Joins("JOIN events ON events.id = event_speakers.event_id") + + if after != nil { + query = query.Where("events.date > ?", after) + } + + if topic != "" { + query = query.Where("LOWER(event_speakers.topic) LIKE LOWER(?)", "%"+topic+"%") + } + + if discordID != "" { + query = query.Where("discord_accounts.discord_id = ?", discordID) + } + + err := query.Count(&count).Error + if err != nil { + return 0, err + } + return count, nil +} + +// SpeakerStats +type SpeakerStats struct { + TotalSpeakCount int64 `gorm:"column:total_speak_count"` + SpeakRank int64 `gorm:"column:speak_rank"` +} + +// GetSpeakerStats returns the total speak count and rank for a given discord_id +func (s *store) GetSpeakerStats(db *gorm.DB, discordID string, after *time.Time, topic string) (SpeakerStats, error) { + var stats SpeakerStats + + subQuery := db.Table("event_speakers"). + Select("discord_accounts.discord_id, COUNT(event_speakers.topic) as total_speak_count"). + Joins("JOIN discord_accounts ON event_speakers.discord_account_id = discord_accounts.id"). + Joins("JOIN events ON events.id = event_speakers.event_id") + + if after != nil { + subQuery = subQuery.Where("events.date > ?", after) + } + if topic != "" { + subQuery = subQuery.Where("LOWER(event_speakers.topic) LIKE LOWER(?)", "%"+topic+"%") + } + + subQuery = subQuery.Group("discord_accounts.discord_id") + + err := db.Table("(?) as subquery", subQuery). + Select("total_speak_count, RANK() OVER (ORDER BY total_speak_count DESC) as speak_rank"). + Where("discord_id = ?", discordID). + Scan(&stats).Error + + if err != nil { + return SpeakerStats{}, err + } + + return stats, nil +} diff --git a/pkg/store/eventspeaker/interface.go b/pkg/store/eventspeaker/interface.go index f25252f5e..3f1eadbe6 100644 --- a/pkg/store/eventspeaker/interface.go +++ b/pkg/store/eventspeaker/interface.go @@ -1,6 +1,8 @@ package eventspeaker import ( + "time" + "gorm.io/gorm" "github.com/dwarvesf/fortress-api/pkg/model" @@ -9,4 +11,7 @@ import ( type IStore interface { Create(db *gorm.DB, s *model.EventSpeaker) (ep *model.EventSpeaker, err error) DeleteAllByEventID(db *gorm.DB, eventID string) error + List(db *gorm.DB, discordID string, after *time.Time, topic string) ([]model.EventSpeaker, error) + GetSpeakerStats(db *gorm.DB, discordID string, after *time.Time, topic string) (SpeakerStats, error) + Count(db *gorm.DB, discordID string, after *time.Time, topic string) (int64, error) } diff --git a/pkg/view/event_speaker.go b/pkg/view/event_speaker.go new file mode 100644 index 000000000..0d9ad9a46 --- /dev/null +++ b/pkg/view/event_speaker.go @@ -0,0 +1,19 @@ +package view + +import "github.com/dwarvesf/fortress-api/pkg/model" + +// OgifStats contains list of ogif and some stats +type OgifStats struct { + OgifList []model.EventSpeaker `json:"ogifList"` + UserAllTimeSpeaksCount int64 `json:"userAllTimeSpeaksCount"` + UserAllTimeRank int64 `json:"userAllTimeRank"` + UserCurrentSpeaksCount int64 `json:"userCurrentSpeaksCount"` + UserCurrentRank int64 `json:"userCurrentRank"` + TotalSpeakCount int64 `json:"totalSpeakCount"` + CurrentSpeakCount int64 `json:"currentSpeakCount"` +} + +// OgifStatsResponse return ogif stats response +type OgifStatsResponse struct { + Data OgifStats `json:"data"` +} // @name OgifStatsResponse