Skip to content

Commit

Permalink
feat: migrate table memo log (#765)
Browse files Browse the repository at this point in the history
* chore: migrate table memo log

* chore: update test

* chore: add notify top author to discord

* chore: add param timeframe

* feat: add param timeframe and limit authors to api

* chore: add new route for notifying top memo authors in v1 routes

* chore: update codeowner
  • Loading branch information
lmquang authored Jan 21, 2025
1 parent 8dcb517 commit 258deb5
Show file tree
Hide file tree
Showing 16 changed files with 350 additions and 97 deletions.
12 changes: 6 additions & 6 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Default reviewers for every PR
* @huynguyenh @namnhce @lmquang
.github/ @huynguyenh @namnhce @lmquang
cmd/ @huynguyenh @namnhce @lmquang
docs/ @huynguyenh @namnhce @lmquang
migrations/ @huynguyenh @namnhce @lmquang
pkg/ @huynguyenh @namnhce @lmquang
* @huynguyenh @lmquang
.github/ @huynguyenh @lmquang
cmd/ @huynguyenh @lmquang
docs/ @huynguyenh @lmquang
migrations/ @huynguyenh @lmquang
pkg/ @huynguyenh @lmquang
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- +migrate Up
-- Add JSONB column to store discord account IDs
ALTER TABLE memo_logs
ADD COLUMN discord_account_ids JSONB DEFAULT '[]'::JSONB;

-- Migrate data from memo_authors to memo_logs
WITH author_data AS (
SELECT
memo_log_id,
json_agg(discord_account_id) AS discord_account_ids
FROM memo_authors
GROUP BY memo_log_id
)
UPDATE memo_logs ml
SET discord_account_ids = ad.discord_account_ids
FROM author_data ad
WHERE ml.id = ad.memo_log_id;

-- Drop the memo_authors table
DROP TABLE memo_authors;
DROP TABLE brainery_logs;

-- +migrate Down
CREATE TABLE "brainery_logs" (
"id" uuid NOT NULL DEFAULT uuid(),
"deleted_at" timestamp,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now(),
"title" text NOT NULL,
"url" text NOT NULL,
"github_id" text,
"discord_id" text NOT NULL,
"employee_id" uuid,
"tags" jsonb,
"published_at" timestamp NOT NULL,
"reward" numeric,
CONSTRAINT "brainery_logs_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employees"("id"),
PRIMARY KEY ("id")
);
CREATE TABLE IF NOT EXISTS memo_authors (
memo_log_id UUID NOT NULL REFERENCES memo_logs(id),
discord_account_id UUID NOT NULL REFERENCES discord_accounts(id),
created_at TIMESTAMP(6) DEFAULT (now()),
PRIMARY KEY (memo_log_id, discord_account_id)
);

INSERT INTO memo_authors (memo_log_id, discord_account_id)
SELECT
id AS memo_log_id,
jsonb_array_elements(discord_account_ids)::UUID AS discord_account_id
FROM memo_logs
WHERE jsonb_array_length(discord_account_ids) > 0;

-- Remove the JSONB column
ALTER TABLE memo_logs
DROP COLUMN discord_account_ids;
7 changes: 6 additions & 1 deletion pkg/controller/memologs/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,17 @@ func (c *controller) Sync() ([]model.MemoLog, error) {
continue
}

discordAccountIDs := make([]string, 0, len(authors))
for _, author := range authors {
discordAccountIDs = append(discordAccountIDs, author.ID.String())
}

newMemos = append(newMemos, model.MemoLog{
Title: item.Title,
URL: item.Link,
Description: item.Description,
PublishedAt: &pubDate,
Authors: authors,
DiscordAccountIDs: discordAccountIDs,
AuthorMemoUsernames: authorUsernames,
Category: extractMemoCategory(item.Link),
})
Expand Down
76 changes: 73 additions & 3 deletions pkg/handler/discord/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func New(controller *controller.Controller, store *store.Store, repo store.DBRep
const (
discordReadingChannel = "1225085624260759622"
discordRandomChannel = "788084358991970337"
discordPlayGroundReadingChannel = "1119171172198797393"
discordPlayGroundReadingChannel = "1064460652720160808" // quang's channel
)

func (h *handler) SyncDiscordInfo(c *gin.Context) {
Expand Down Expand Up @@ -375,7 +375,14 @@ func (h *handler) SyncMemo(c *gin.Context) {
return
}

_, err = h.service.Discord.SendNewMemoMessage(h.config.Discord.IDs.DwarvesGuild, memos, targetChannelID)
_, err = h.service.Discord.SendNewMemoMessage(
h.config.Discord.IDs.DwarvesGuild,
memos,
targetChannelID,
func(discordAccountID string) (*model.DiscordAccount, error) {
return h.store.DiscordAccount.One(h.repo.DB(), discordAccountID)
},
)
if err != nil {
h.logger.Error(err, "failed to send new memo message")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
Expand All @@ -396,6 +403,61 @@ func (h *handler) SweepMemo(c *gin.Context) {

c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, nil, nil, "ok"))
}
func (h *handler) NotifyTopMemoAuthors(c *gin.Context) {
in := request.TopMemoAuthorsInput{}
if err := c.ShouldBindJSON(&in); err != nil {
h.logger.Error(err, "failed to decode body")
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, err, in, ""))
return
}

if err := in.Validate(); err != nil {
h.logger.Error(err, "failed to validate input")
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, err, in, ""))
return
}

now := time.Now()
end := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999999999, now.Location())
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -in.Days+1)

topAuthors, err := h.store.MemoLog.GetTopAuthors(h.repo.DB(), in.Limit, &start, &end)
if err != nil {
h.logger.Error(err, "failed to retrieve top memo authors")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
return
}

if len(topAuthors) == 0 {
c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, nil, nil, "no memo authors found"))
return
}

var topAuthorsStr string
for i, author := range topAuthors {
topAuthorsStr += fmt.Sprintf("%d. <@%s> (%d memos)\n", i+1, author.DiscordID, author.TotalMemos)
}

targetChannelID := discordPlayGroundReadingChannel
if h.config.Env == "prod" {
targetChannelID = discordRandomChannel
}

title := fmt.Sprintf("Top %d Memo Authors (Last %d Days)", in.Limit, in.Days)
msg := &discordgo.MessageEmbed{
Title: title,
Description: topAuthorsStr,
}

_, err = h.service.Discord.SendEmbeddedMessageWithChannel(nil, msg, targetChannelID)
if err != nil {
h.logger.Error(err, "failed to send top memo authors message")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
return
}

c.JSON(http.StatusOK, view.CreateResponse[any](nil, nil, nil, nil, "ok"))
}

func (h *handler) NotifyWeeklyMemos(c *gin.Context) {
// get last 7 days
Expand Down Expand Up @@ -430,7 +492,15 @@ func (h *handler) NotifyWeeklyMemos(c *gin.Context) {
targetChannelID = discordRandomChannel
}

_, err = h.service.Discord.SendWeeklyMemosMessage(h.config.Discord.IDs.DwarvesGuild, memos, weekRangeStr, targetChannelID)
_, err = h.service.Discord.SendWeeklyMemosMessage(
h.config.Discord.IDs.DwarvesGuild,
memos,
weekRangeStr,
targetChannelID,
func(discordAccountID string) (*model.DiscordAccount, error) {
return h.store.DiscordAccount.One(h.repo.DB(), discordAccountID)
},
)
if err != nil {
h.logger.Error(err, "failed to send weekly memos report")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
Expand Down
1 change: 1 addition & 0 deletions pkg/handler/discord/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ type IHandler interface {
UserOgifStats(c *gin.Context)
OgifLeaderboard(c *gin.Context)
SweepOgifEvent(c *gin.Context)
NotifyTopMemoAuthors(c *gin.Context)
}
18 changes: 18 additions & 0 deletions pkg/handler/discord/request/top_memo_authors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package request

import "errors"

type TopMemoAuthorsInput struct {
Limit int `json:"limit"`
Days int `json:"days"`
}

func (i *TopMemoAuthorsInput) Validate() error {
if i.Limit <= 0 {
return errors.New("limit must be greater than 0")
}
if i.Days <= 0 {
return errors.New("days must be greater than 0")
}
return nil
}
47 changes: 36 additions & 11 deletions pkg/handler/memologs/memo_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import (
"time"

"github.com/bwmarrin/discordgo"
"github.com/gin-gonic/gin"

"github.com/dwarvesf/fortress-api/pkg/config"
"github.com/dwarvesf/fortress-api/pkg/controller"
"github.com/dwarvesf/fortress-api/pkg/handler/memologs/request"
memologRequest "github.com/dwarvesf/fortress-api/pkg/handler/memologs/request"
"github.com/dwarvesf/fortress-api/pkg/logger"
"github.com/dwarvesf/fortress-api/pkg/model"
"github.com/dwarvesf/fortress-api/pkg/service"
"github.com/dwarvesf/fortress-api/pkg/service/mochiprofile"
"github.com/dwarvesf/fortress-api/pkg/store"
"github.com/dwarvesf/fortress-api/pkg/store/memolog"
"github.com/dwarvesf/fortress-api/pkg/view"
"github.com/gin-gonic/gin"
)

type handler struct {
Expand Down Expand Up @@ -51,7 +52,7 @@ func (h *handler) Create(c *gin.Context) {
},
)

body := request.CreateMemoLogsRequest{}
body := memologRequest.CreateMemoLogsRequest{}
if err := c.ShouldBindJSON(&body); err != nil {
l.Error(err, "[memologs.Create] failed to decode body")
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, err, body, ""))
Expand Down Expand Up @@ -132,14 +133,19 @@ func (h *handler) Create(c *gin.Context) {
authors = append(authors, newAuthor)
}

discordAccountIDs := make([]string, 0, len(authors))
for _, author := range authors {
discordAccountIDs = append(discordAccountIDs, author.ID.String())
}

b := model.MemoLog{
Title: b.Title,
URL: b.URL,
Authors: authors,
Tags: b.Tags,
PublishedAt: &publishedAt,
Description: b.Description,
Reward: b.Reward,
Title: b.Title,
URL: b.URL,
DiscordAccountIDs: discordAccountIDs,
Tags: b.Tags,
PublishedAt: &publishedAt,
Description: b.Description,
Reward: b.Reward,
}
memologs = append(memologs, b)
}
Expand Down Expand Up @@ -283,7 +289,26 @@ func (h *handler) GetTopAuthors(c *gin.Context) {
},
)

topAuthors, err := h.store.MemoLog.GetTopAuthors(h.repo.DB(), 10)
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
days, _ := strconv.Atoi(c.DefaultQuery("days", "30"))

if limit <= 0 {
l.Error(errors.New("limit must be greater than 0"), "invalid limit")
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, errors.New("limit must be greater than 0"), nil, ""))
return
}

if days <= 0 {
l.Error(errors.New("days must be greater than 0"), "invalid days")
c.JSON(http.StatusBadRequest, view.CreateResponse[any](nil, nil, errors.New("days must be greater than 0"), nil, ""))
return
}

now := time.Now()
end := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999999999, now.Location())
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -days+1)

topAuthors, err := h.store.MemoLog.GetTopAuthors(h.repo.DB(), limit, &start, &end)
if err != nil {
l.Error(err, "failed to get top authors")
c.JSON(http.StatusInternalServerError, view.CreateResponse[any](nil, nil, err, nil, ""))
Expand Down
19 changes: 10 additions & 9 deletions pkg/handler/survey/survey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ func TestHandler_GetSurveyReviewDetail(t *testing.T) {
}
}

// year in test case should be changed to the current year
func TestHandler_CreateSurvey(t *testing.T) {
cfg := config.LoadTestConfig()
loggerMock := logger.NewLogrusLogger()
Expand All @@ -844,10 +845,10 @@ func TestHandler_CreateSurvey(t *testing.T) {
wantResponsePath: "testdata/create_survey/200_work.json",
body: request.CreateSurveyFeedbackInput{
Quarter: "q3,q4",
Year: 2023,
Year: 2025,
Type: "peer-review",
FromDate: "2023-11-28",
ToDate: "2023-11-29",
FromDate: "2025-11-28",
ToDate: "2025-11-29",
},
},
{
Expand All @@ -856,10 +857,10 @@ func TestHandler_CreateSurvey(t *testing.T) {
wantResponsePath: "testdata/create_survey/400.json",
body: request.CreateSurveyFeedbackInput{
Quarter: "q3,q4",
Year: 2023,
Year: 2025,
Type: "work",
FromDate: "2023-11-30",
ToDate: "2023-11-29",
FromDate: "2025-11-30",
ToDate: "2025-11-29",
},
},
{
Expand All @@ -868,10 +869,10 @@ func TestHandler_CreateSurvey(t *testing.T) {
wantResponsePath: "testdata/create_survey/invalid_subtype.json",
body: request.CreateSurveyFeedbackInput{
Quarter: "q3,q4",
Year: 2023,
Year: 2025,
Type: "peer-revieww",
FromDate: "2023-11-28",
ToDate: "2023-11-29",
FromDate: "2025-11-28",
ToDate: "2025-11-29",
},
},
{
Expand Down
7 changes: 2 additions & 5 deletions pkg/model/memo_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (

"github.com/lib/pq"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)

type MemoLog struct {
Expand All @@ -19,7 +18,7 @@ type MemoLog struct {
Reward decimal.Decimal
Category pq.StringArray `json:"value" gorm:"type:text[]"`

Authors []DiscordAccount `json:"authors" gorm:"many2many:memo_authors;"`
DiscordAccountIDs JSONArrayString `json:"discord_account_ids" gorm:"type:jsonb;column:discord_account_ids"`

// This field is used to make sure response always contains authors
AuthorMemoUsernames []string `json:"-" gorm:"-"`
Expand All @@ -33,6 +32,4 @@ type DiscordAccountMemoRank struct {
Rank int
}

func (MemoLog) BeforeCreate(db *gorm.DB) error {
return db.SetupJoinTable(&MemoLog{}, "Authors", &MemoAuthor{})
}
// Remove BeforeCreate method as we no longer use many-to-many join table
1 change: 1 addition & 0 deletions pkg/routes/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func loadV1Routes(r *gin.Engine, h *handler.Handler, repo store.DBRepo, s *store
cronjob.POST("/sync-memo", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Discord.SyncMemo)
cronjob.POST("/sweep-memo", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Discord.SweepMemo)
cronjob.POST("/notify-weekly-memos", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Discord.NotifyWeeklyMemos)
cronjob.POST("/notify-top-memo-authors", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Discord.NotifyTopMemoAuthors)
cronjob.POST("/transcribe-youtube-broadcast", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Youtube.TranscribeBroadcast)
cronjob.POST("/sweep-ogif-event", amw.WithAuth, pmw.WithPerm(model.PermissionCronjobExecute), h.Discord.SweepOgifEvent)
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/routes/v1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,12 @@ func Test_loadV1Routes(t *testing.T) {
Handler: "github.com/dwarvesf/fortress-api/pkg/handler/discord.IHandler.NotifyWeeklyMemos-fm",
},
},
"/cronjobs/notify-top-memo-authors": {
"POST": {
Method: "POST",
Handler: "github.com/dwarvesf/fortress-api/pkg/handler/discord.IHandler.NotifyTopMemoAuthors-fm",
},
},
"/cronjobs/transcribe-youtube-broadcast": {
"POST": {
Method: "POST",
Expand Down
Loading

0 comments on commit 258deb5

Please sign in to comment.