Add subtask model and repository, implement webhook notification handling
fix Issue with Scheduler Support NotificationPlatformWebhook support Discord as notification target
This commit is contained in:
parent
8db572f1ec
commit
81acbd8eba
8 changed files with 136 additions and 11 deletions
|
@ -243,7 +243,6 @@ func (h *Handler) createChore(c *gin.Context) {
|
||||||
stringLabels = &labels
|
stringLabels = &labels
|
||||||
}
|
}
|
||||||
createdChore := &chModel.Chore{
|
createdChore := &chModel.Chore{
|
||||||
|
|
||||||
Name: choreReq.Name,
|
Name: choreReq.Name,
|
||||||
FrequencyType: choreReq.FrequencyType,
|
FrequencyType: choreReq.FrequencyType,
|
||||||
Frequency: choreReq.Frequency,
|
Frequency: choreReq.Frequency,
|
||||||
|
@ -264,6 +263,7 @@ func (h *Handler) createChore(c *gin.Context) {
|
||||||
CompletionWindow: choreReq.CompletionWindow,
|
CompletionWindow: choreReq.CompletionWindow,
|
||||||
Description: choreReq.Description,
|
Description: choreReq.Description,
|
||||||
SubTasks: choreReq.SubTasks,
|
SubTasks: choreReq.SubTasks,
|
||||||
|
Priority: choreReq.Priority,
|
||||||
}
|
}
|
||||||
id, err := h.choreRepo.CreateChore(c, createdChore)
|
id, err := h.choreRepo.CreateChore(c, createdChore)
|
||||||
createdChore.ID = id
|
createdChore.ID = id
|
||||||
|
@ -547,6 +547,7 @@ func (h *Handler) editChore(c *gin.Context) {
|
||||||
if choreReq.SubTasks == nil {
|
if choreReq.SubTasks == nil {
|
||||||
choreReq.SubTasks = &[]stModel.SubTask{}
|
choreReq.SubTasks = &[]stModel.SubTask{}
|
||||||
}
|
}
|
||||||
|
// check what subtask needed to be removed:
|
||||||
for _, existedSubTask := range *oldChore.SubTasks {
|
for _, existedSubTask := range *oldChore.SubTasks {
|
||||||
found := false
|
found := false
|
||||||
for _, newSubTask := range *choreReq.SubTasks {
|
for _, newSubTask := range *choreReq.SubTasks {
|
||||||
|
@ -559,14 +560,16 @@ func (h *Handler) editChore(c *gin.Context) {
|
||||||
ToBeRemoved = append(ToBeRemoved, existedSubTask)
|
ToBeRemoved = append(ToBeRemoved, existedSubTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// check what subtask needed to be added or updated:
|
||||||
for _, newSubTask := range *choreReq.SubTasks {
|
for _, newSubTask := range *choreReq.SubTasks {
|
||||||
found := false
|
found := false
|
||||||
newSubTask.ChoreID = oldChore.ID
|
newSubTask.ChoreID = oldChore.ID
|
||||||
|
|
||||||
for _, existedSubTask := range *oldChore.SubTasks {
|
for _, existedSubTask := range *oldChore.SubTasks {
|
||||||
if existedSubTask.ID == newSubTask.ID {
|
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
|
// there is a change in the subtask, update it
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,9 +38,8 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing time in frequency metadata: %w", err)
|
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)
|
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 the time is in the past today, move it to tomorrow
|
||||||
if baseDate.Before(completedDate) {
|
if baseDate.Before(completedDate) {
|
||||||
baseDate = baseDate.AddDate(0, 0, 1)
|
baseDate = baseDate.AddDate(0, 0, 1)
|
||||||
|
|
|
@ -37,6 +37,7 @@ const (
|
||||||
NotificationPlatformTelegram
|
NotificationPlatformTelegram
|
||||||
NotificationPlatformPushover
|
NotificationPlatformPushover
|
||||||
NotificationPlatformWebhook
|
NotificationPlatformWebhook
|
||||||
|
NotificationPlatformDiscord
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONB map[string]interface{}
|
type JSONB map[string]interface{}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
"donetick.com/core/internal/events"
|
"donetick.com/core/internal/events"
|
||||||
nModel "donetick.com/core/internal/notifier/model"
|
nModel "donetick.com/core/internal/notifier/model"
|
||||||
|
"donetick.com/core/internal/notifier/service/discord"
|
||||||
pushover "donetick.com/core/internal/notifier/service/pushover"
|
pushover "donetick.com/core/internal/notifier/service/pushover"
|
||||||
telegram "donetick.com/core/internal/notifier/service/telegram"
|
telegram "donetick.com/core/internal/notifier/service/telegram"
|
||||||
|
|
||||||
|
@ -14,14 +15,16 @@ import (
|
||||||
type Notifier struct {
|
type Notifier struct {
|
||||||
Telegram *telegram.TelegramNotifier
|
Telegram *telegram.TelegramNotifier
|
||||||
Pushover *pushover.Pushover
|
Pushover *pushover.Pushover
|
||||||
|
discord *discord.DiscordNotifier
|
||||||
eventsProducer *events.EventsProducer
|
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{
|
return &Notifier{
|
||||||
Telegram: t,
|
Telegram: t,
|
||||||
Pushover: p,
|
Pushover: p,
|
||||||
eventsProducer: ep,
|
eventsProducer: ep,
|
||||||
|
discord: d,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +44,13 @@ func (n *Notifier) SendNotification(c context.Context, notification *nModel.Noti
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err = n.Pushover.SendNotification(c, notification)
|
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:
|
case nModel.NotificationPlatformWebhook:
|
||||||
// TODO: Implement webhook notification
|
// TODO: Implement webhook notification
|
||||||
// currently we have eventProducer to send events always as a webhook
|
// currently we have eventProducer to send events always as a webhook
|
||||||
|
|
84
internal/notifier/service/discord/discord.go
Normal file
84
internal/notifier/service/discord/discord.go
Normal 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
|
||||||
|
}
|
|
@ -9,4 +9,5 @@ type SubTask struct {
|
||||||
Name string `json:"name" gorm:"column:name"`
|
Name string `json:"name" gorm:"column:name"`
|
||||||
CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"`
|
CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"`
|
||||||
CompletedBy int `json:"completedBy" gorm:"column:completed_by"`
|
CompletedBy int `json:"completedBy" gorm:"column:completed_by"`
|
||||||
|
ParentId *int `json:"parentId" gorm:"column:parent_id"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,9 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR
|
||||||
var insertions []stModel.SubTask
|
var insertions []stModel.SubTask
|
||||||
var updates []stModel.SubTask
|
var updates []stModel.SubTask
|
||||||
for _, subtask := range toBeAdd {
|
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)
|
insertions = append(insertions, subtask)
|
||||||
} else {
|
} else {
|
||||||
updates = append(updates, subtask)
|
updates = append(updates, subtask)
|
||||||
|
@ -51,7 +53,14 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR
|
||||||
}
|
}
|
||||||
if len(updates) > 0 {
|
if len(updates) > 0 {
|
||||||
for _, subtask := range updates {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +70,28 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SubTasksRepository) DeleteSubtask(c context.Context, tx *gorm.DB, subtaskID int) error {
|
func (r *SubTasksRepository) DeleteSubtask(c context.Context, tx *gorm.DB, subtaskID int) error {
|
||||||
if tx != nil {
|
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 {
|
func (r *SubTasksRepository) UpdateSubTaskStatus(c context.Context, userID int, subtaskID int, completedAt *time.Time) error {
|
||||||
|
|
2
main.go
2
main.go
|
@ -31,6 +31,7 @@ import (
|
||||||
notifier "donetick.com/core/internal/notifier"
|
notifier "donetick.com/core/internal/notifier"
|
||||||
nRepo "donetick.com/core/internal/notifier/repo"
|
nRepo "donetick.com/core/internal/notifier/repo"
|
||||||
nps "donetick.com/core/internal/notifier/service"
|
nps "donetick.com/core/internal/notifier/service"
|
||||||
|
discord "donetick.com/core/internal/notifier/service/discord"
|
||||||
"donetick.com/core/internal/notifier/service/pushover"
|
"donetick.com/core/internal/notifier/service/pushover"
|
||||||
telegram "donetick.com/core/internal/notifier/service/telegram"
|
telegram "donetick.com/core/internal/notifier/service/telegram"
|
||||||
pRepo "donetick.com/core/internal/points/repo"
|
pRepo "donetick.com/core/internal/points/repo"
|
||||||
|
@ -73,6 +74,7 @@ func main() {
|
||||||
// add notifier
|
// add notifier
|
||||||
fx.Provide(pushover.NewPushover),
|
fx.Provide(pushover.NewPushover),
|
||||||
fx.Provide(telegram.NewTelegramNotifier),
|
fx.Provide(telegram.NewTelegramNotifier),
|
||||||
|
fx.Provide(discord.NewDiscordNotifier),
|
||||||
fx.Provide(notifier.NewNotifier),
|
fx.Provide(notifier.NewNotifier),
|
||||||
fx.Provide(events.NewEventsProducer),
|
fx.Provide(events.NewEventsProducer),
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue