From 81acbd8eba64f6030fe76e4f22239f9b5b36fb48 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Wed, 5 Mar 2025 19:52:10 -0500 Subject: [PATCH] Add subtask model and repository, implement webhook notification handling fix Issue with Scheduler Support NotificationPlatformWebhook support Discord as notification target --- internal/chore/handler.go | 9 ++- internal/chore/scheduler.go | 3 +- internal/notifier/model/model.go | 1 + internal/notifier/notifier.go | 12 ++- internal/notifier/service/discord/discord.go | 84 ++++++++++++++++++++ internal/subtask/model/model.go | 1 + internal/subtask/repo/repository.go | 35 ++++++-- main.go | 2 + 8 files changed, 136 insertions(+), 11 deletions(-) create mode 100644 internal/notifier/service/discord/discord.go diff --git a/internal/chore/handler.go b/internal/chore/handler.go index cf62ece..9667385 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -243,7 +243,6 @@ func (h *Handler) createChore(c *gin.Context) { stringLabels = &labels } createdChore := &chModel.Chore{ - Name: choreReq.Name, FrequencyType: choreReq.FrequencyType, Frequency: choreReq.Frequency, @@ -264,6 +263,7 @@ func (h *Handler) createChore(c *gin.Context) { CompletionWindow: choreReq.CompletionWindow, Description: choreReq.Description, SubTasks: choreReq.SubTasks, + Priority: choreReq.Priority, } id, err := h.choreRepo.CreateChore(c, createdChore) createdChore.ID = id @@ -547,6 +547,7 @@ func (h *Handler) editChore(c *gin.Context) { if choreReq.SubTasks == nil { choreReq.SubTasks = &[]stModel.SubTask{} } + // check what subtask needed to be removed: for _, existedSubTask := range *oldChore.SubTasks { found := false for _, newSubTask := range *choreReq.SubTasks { @@ -559,14 +560,16 @@ func (h *Handler) editChore(c *gin.Context) { ToBeRemoved = append(ToBeRemoved, existedSubTask) } } - + // check what subtask needed to be added or updated: for _, newSubTask := range *choreReq.SubTasks { found := false newSubTask.ChoreID = oldChore.ID for _, existedSubTask := range *oldChore.SubTasks { if existedSubTask.ID == newSubTask.ID { - if existedSubTask.Name != newSubTask.Name || existedSubTask.OrderID != newSubTask.OrderID { + if existedSubTask.Name != newSubTask.Name || + existedSubTask.OrderID != newSubTask.OrderID || + existedSubTask.ParentId != newSubTask.ParentId { // there is a change in the subtask, update it break } diff --git a/internal/chore/scheduler.go b/internal/chore/scheduler.go index b936278..3341cc2 100644 --- a/internal/chore/scheduler.go +++ b/internal/chore/scheduler.go @@ -38,9 +38,8 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T if err != nil { return nil, fmt.Errorf("error parsing time in frequency metadata: %w", err) } - + t = t.UTC() baseDate = time.Date(baseDate.Year(), baseDate.Month(), baseDate.Day(), t.Hour(), t.Minute(), t.Second(), 0, time.UTC) - // If the time is in the past today, move it to tomorrow if baseDate.Before(completedDate) { baseDate = baseDate.AddDate(0, 0, 1) diff --git a/internal/notifier/model/model.go b/internal/notifier/model/model.go index a3696df..b8efb48 100644 --- a/internal/notifier/model/model.go +++ b/internal/notifier/model/model.go @@ -37,6 +37,7 @@ const ( NotificationPlatformTelegram NotificationPlatformPushover NotificationPlatformWebhook + NotificationPlatformDiscord ) type JSONB map[string]interface{} diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go index 1ee47d4..8ecd3ac 100644 --- a/internal/notifier/notifier.go +++ b/internal/notifier/notifier.go @@ -5,6 +5,7 @@ import ( "donetick.com/core/internal/events" nModel "donetick.com/core/internal/notifier/model" + "donetick.com/core/internal/notifier/service/discord" pushover "donetick.com/core/internal/notifier/service/pushover" telegram "donetick.com/core/internal/notifier/service/telegram" @@ -14,14 +15,16 @@ import ( type Notifier struct { Telegram *telegram.TelegramNotifier Pushover *pushover.Pushover + discord *discord.DiscordNotifier eventsProducer *events.EventsProducer } -func NewNotifier(t *telegram.TelegramNotifier, p *pushover.Pushover, ep *events.EventsProducer) *Notifier { +func NewNotifier(t *telegram.TelegramNotifier, p *pushover.Pushover, ep *events.EventsProducer, d *discord.DiscordNotifier) *Notifier { return &Notifier{ Telegram: t, Pushover: p, eventsProducer: ep, + discord: d, } } @@ -41,6 +44,13 @@ func (n *Notifier) SendNotification(c context.Context, notification *nModel.Noti return nil } err = n.Pushover.SendNotification(c, notification) + case nModel.NotificationPlatformDiscord: + if n.discord == nil { + log.Error("Discord is not initialized, Skipping sending message") + return nil + } + err = n.discord.SendNotification(c, notification) + case nModel.NotificationPlatformWebhook: // TODO: Implement webhook notification // currently we have eventProducer to send events always as a webhook diff --git a/internal/notifier/service/discord/discord.go b/internal/notifier/service/discord/discord.go new file mode 100644 index 0000000..30aed57 --- /dev/null +++ b/internal/notifier/service/discord/discord.go @@ -0,0 +1,84 @@ +package discord + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + + "donetick.com/core/config" + chModel "donetick.com/core/internal/chore/model" + nModel "donetick.com/core/internal/notifier/model" + uModel "donetick.com/core/internal/user/model" + "donetick.com/core/logging" +) + +type DiscordNotifier struct { +} + +func NewDiscordNotifier(config *config.Config) *DiscordNotifier { + return &DiscordNotifier{} +} + +func (dn *DiscordNotifier) SendChoreCompletion(c context.Context, chore *chModel.Chore, user *uModel.User) { + log := logging.FromContext(c) + if dn == nil { + log.Error("Discord notifier is not initialized, skipping message sending") + return + } + + var mt *chModel.NotificationMetadata + if err := json.Unmarshal([]byte(*chore.NotificationMetadata), &mt); err != nil { + log.Error("Error unmarshalling notification metadata", err) + } + + message := fmt.Sprintf("🎉 **%s** is completed! Great job, %s! 🌟", chore.Name, user.DisplayName) + err := dn.sendMessage(c, user.UserNotificationTargets.TargetID, message) + if err != nil { + log.Error("Error sending Discord message:", err) + } +} + +func (dn *DiscordNotifier) SendNotification(c context.Context, notification *nModel.NotificationDetails) error { + + if dn == nil { + return errors.New("Discord notifier is not initialized") + } + + if notification.Text == "" { + return errors.New("unable to send notification, text is empty") + } + + return dn.sendMessage(c, notification.TargetID, notification.Text) +} + +func (dn *DiscordNotifier) sendMessage(c context.Context, webhookURL string, message string) error { + log := logging.FromContext(c) + + if webhookURL == "" { + return errors.New("unable to send notification, webhook URL is empty") + } + + payload := map[string]string{"content": message} + jsonData, err := json.Marshal(payload) + if err != nil { + log.Error("Error marshalling JSON:", err) + return err + } + + resp, err := http.Post(webhookURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + log.Error("Error sending message to Discord:", err) + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { + log.Error("Discord webhook returned unexpected status:", resp.Status) + return errors.New("failed to send Discord message") + } + + return nil +} diff --git a/internal/subtask/model/model.go b/internal/subtask/model/model.go index 56e873d..e6dae3b 100644 --- a/internal/subtask/model/model.go +++ b/internal/subtask/model/model.go @@ -9,4 +9,5 @@ type SubTask struct { Name string `json:"name" gorm:"column:name"` CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"` CompletedBy int `json:"completedBy" gorm:"column:completed_by"` + ParentId *int `json:"parentId" gorm:"column:parent_id"` } diff --git a/internal/subtask/repo/repository.go b/internal/subtask/repo/repository.go index 9cd0e39..1f0d8e7 100644 --- a/internal/subtask/repo/repository.go +++ b/internal/subtask/repo/repository.go @@ -37,7 +37,9 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR var insertions []stModel.SubTask var updates []stModel.SubTask for _, subtask := range toBeAdd { - if subtask.ID == 0 { + if subtask.ID <= 0 { + // we interpret this as a new subtask + subtask.ID = 0 insertions = append(insertions, subtask) } else { updates = append(updates, subtask) @@ -51,7 +53,14 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR } if len(updates) > 0 { for _, subtask := range updates { - if err := tx.Model(&stModel.SubTask{}).Where("chore_id = ? AND id = ?", choreId, subtask.ID).Updates(subtask).Error; err != nil { + values := map[string]interface{}{ + "name": subtask.Name, + "order_id": subtask.OrderID, + "completed_at": subtask.CompletedAt, + "completed_by": subtask.CompletedBy, + "parent_id": subtask.ParentId, + } + if err := tx.Model(&stModel.SubTask{}).Where("chore_id = ? AND id = ?", choreId, subtask.ID).Updates(values).Error; err != nil { return err } } @@ -61,12 +70,28 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR return nil }) } - func (r *SubTasksRepository) DeleteSubtask(c context.Context, tx *gorm.DB, subtaskID int) error { if tx != nil { - return tx.Delete(&stModel.SubTask{}, subtaskID).Error + return r.deleteSubtaskWithChildren(c, tx, subtaskID) } - return r.db.WithContext(c).Delete(&stModel.SubTask{}, subtaskID).Error + return r.db.WithContext(c).Transaction(func(tx *gorm.DB) error { + return r.deleteSubtaskWithChildren(c, tx, subtaskID) + }) +} + +func (r *SubTasksRepository) deleteSubtaskWithChildren(c context.Context, tx *gorm.DB, subtaskID int) error { + var childSubtasks []stModel.SubTask + if err := tx.Where("parent_id = ?", subtaskID).Find(&childSubtasks).Error; err != nil { + return err + } + + for _, child := range childSubtasks { + if err := r.deleteSubtaskWithChildren(c, tx, child.ID); err != nil { + return err + } + } + + return tx.Delete(&stModel.SubTask{}, subtaskID).Error } func (r *SubTasksRepository) UpdateSubTaskStatus(c context.Context, userID int, subtaskID int, completedAt *time.Time) error { diff --git a/main.go b/main.go index bc1048b..f2c6c71 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( notifier "donetick.com/core/internal/notifier" nRepo "donetick.com/core/internal/notifier/repo" nps "donetick.com/core/internal/notifier/service" + discord "donetick.com/core/internal/notifier/service/discord" "donetick.com/core/internal/notifier/service/pushover" telegram "donetick.com/core/internal/notifier/service/telegram" pRepo "donetick.com/core/internal/points/repo" @@ -73,6 +74,7 @@ func main() { // add notifier fx.Provide(pushover.NewPushover), fx.Provide(telegram.NewTelegramNotifier), + fx.Provide(discord.NewDiscordNotifier), fx.Provide(notifier.NewNotifier), fx.Provide(events.NewEventsProducer),