Add subtask model and repository, implement webhook notification handling

Support NotificationPlatformWebhook
support Discord as notification target
fix Issue with Scheduler
Fix #154
Fix #153
This commit is contained in:
Mo Tarbin 2025-03-05 19:52:36 -05:00
commit 5127383c59
8 changed files with 136 additions and 11 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -37,6 +37,7 @@ const (
NotificationPlatformTelegram
NotificationPlatformPushover
NotificationPlatformWebhook
NotificationPlatformDiscord
)
type JSONB map[string]interface{}

View file

@ -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

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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 {

View file

@ -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),