Add Event Producer
Update User to carry webhook from circle if assigned Refactor notification handling and update models for webhook support
This commit is contained in:
parent
44cb5501dd
commit
04d1894aea
17 changed files with 351 additions and 101 deletions
|
@ -3,35 +3,32 @@ package model
|
|||
import "time"
|
||||
|
||||
type Notification struct {
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
ChoreID int `json:"chore_id" gorm:"column:chore_id"`
|
||||
UserID int `json:"user_id" gorm:"column:user_id"`
|
||||
TargetID string `json:"target_id" gorm:"column:target_id"`
|
||||
Text string `json:"text" gorm:"column:text"`
|
||||
IsSent bool `json:"is_sent" gorm:"column:is_sent;index;default:false"`
|
||||
TypeID NotificationType `json:"type" gorm:"column:type"`
|
||||
ScheduledFor time.Time `json:"scheduled_for" gorm:"column:scheduled_for;index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
ID int `json:"id" gorm:"primaryKey"`
|
||||
ChoreID int `json:"chore_id" gorm:"column:chore_id"`
|
||||
CircleID int `json:"circle_id" gorm:"column:circle_id"`
|
||||
UserID int `json:"user_id" gorm:"column:user_id"`
|
||||
TargetID string `json:"target_id" gorm:"column:target_id"`
|
||||
Text string `json:"text" gorm:"column:text"`
|
||||
IsSent bool `json:"is_sent" gorm:"column:is_sent;index;default:false"`
|
||||
TypeID NotificationPlatform `json:"type" gorm:"column:type"`
|
||||
ScheduledFor time.Time `json:"scheduled_for" gorm:"column:scheduled_for;index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
RawEvent interface{} `json:"raw_event" gorm:"column:raw_event;type:jsonb"`
|
||||
}
|
||||
type NotificationDetails struct {
|
||||
Notification
|
||||
WebhookURL *string `json:"webhook_url" gorm:"column:webhook_url;<-:null"` // read-only, will only be used if webhook enabled
|
||||
|
||||
}
|
||||
|
||||
func (n *Notification) IsValid() bool {
|
||||
switch n.TypeID {
|
||||
case NotificationTypeTelegram, NotificationTypePushover:
|
||||
if n.TargetID == "" {
|
||||
return false
|
||||
} else if n.Text == "0" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type NotificationType int8
|
||||
type NotificationPlatform int8
|
||||
|
||||
const (
|
||||
NotificationTypeNone NotificationType = iota
|
||||
NotificationTypeTelegram
|
||||
NotificationTypePushover
|
||||
NotificationPlatformNone NotificationPlatform = iota
|
||||
NotificationPlatformTelegram
|
||||
NotificationPlatformPushover
|
||||
)
|
||||
|
|
|
@ -3,39 +3,52 @@ package notifier
|
|||
import (
|
||||
"context"
|
||||
|
||||
"donetick.com/core/internal/events"
|
||||
nModel "donetick.com/core/internal/notifier/model"
|
||||
pushover "donetick.com/core/internal/notifier/service/pushover"
|
||||
telegram "donetick.com/core/internal/notifier/service/telegram"
|
||||
|
||||
"donetick.com/core/logging"
|
||||
)
|
||||
|
||||
type Notifier struct {
|
||||
Telegram *telegram.TelegramNotifier
|
||||
Pushover *pushover.Pushover
|
||||
Telegram *telegram.TelegramNotifier
|
||||
Pushover *pushover.Pushover
|
||||
eventsProducer *events.EventsProducer
|
||||
}
|
||||
|
||||
func NewNotifier(t *telegram.TelegramNotifier, p *pushover.Pushover) *Notifier {
|
||||
func NewNotifier(t *telegram.TelegramNotifier, p *pushover.Pushover, ep *events.EventsProducer) *Notifier {
|
||||
return &Notifier{
|
||||
Telegram: t,
|
||||
Pushover: p,
|
||||
Telegram: t,
|
||||
Pushover: p,
|
||||
eventsProducer: ep,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifier) SendNotification(c context.Context, notification *nModel.Notification) error {
|
||||
func (n *Notifier) SendNotification(c context.Context, notification *nModel.NotificationDetails) error {
|
||||
log := logging.FromContext(c)
|
||||
var err error
|
||||
switch notification.TypeID {
|
||||
case nModel.NotificationTypeTelegram:
|
||||
case nModel.NotificationPlatformTelegram:
|
||||
if n.Telegram == nil {
|
||||
log.Error("Telegram bot is not initialized, Skipping sending message")
|
||||
return nil
|
||||
}
|
||||
return n.Telegram.SendNotification(c, notification)
|
||||
case nModel.NotificationTypePushover:
|
||||
err = n.Telegram.SendNotification(c, notification)
|
||||
case nModel.NotificationPlatformPushover:
|
||||
if n.Pushover == nil {
|
||||
log.Error("Pushover is not initialized, Skipping sending message")
|
||||
return nil
|
||||
}
|
||||
return n.Pushover.SendNotification(c, notification)
|
||||
err = n.Pushover.SendNotification(c, notification)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to send notification", "err", err)
|
||||
}
|
||||
if notification.RawEvent != nil && notification.WebhookURL != nil {
|
||||
// if we have a webhook url, we should send the event to the webhook
|
||||
n.eventsProducer.NotificaitonEvent(c, *notification.WebhookURL, notification.RawEvent)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ func (r *NotificationRepository) DeleteAllChoreNotifications(choreID int) error
|
|||
func (r *NotificationRepository) BatchInsertNotifications(notifications []*nModel.Notification) error {
|
||||
return r.db.Create(¬ifications).Error
|
||||
}
|
||||
func (r *NotificationRepository) MarkNotificationsAsSent(notifications []*nModel.Notification) error {
|
||||
func (r *NotificationRepository) MarkNotificationsAsSent(notifications []*nModel.NotificationDetails) error {
|
||||
// Extract IDs from notifications
|
||||
var ids []int
|
||||
for _, notification := range notifications {
|
||||
|
@ -32,11 +32,15 @@ func (r *NotificationRepository) MarkNotificationsAsSent(notifications []*nModel
|
|||
// Use the extracted IDs in the Where clause
|
||||
return r.db.Model(&nModel.Notification{}).Where("id IN (?)", ids).Update("is_sent", true).Error
|
||||
}
|
||||
func (r *NotificationRepository) GetPendingNotificaiton(c context.Context, lookback time.Duration) ([]*nModel.Notification, error) {
|
||||
var notifications []*nModel.Notification
|
||||
func (r *NotificationRepository) GetPendingNotificaiton(c context.Context, lookback time.Duration) ([]*nModel.NotificationDetails, error) {
|
||||
var notifications []*nModel.NotificationDetails
|
||||
start := time.Now().UTC().Add(-lookback)
|
||||
end := time.Now().UTC()
|
||||
if err := r.db.Where("is_sent = ? AND scheduled_for < ? AND scheduled_for > ?", false, end, start).Find(¬ifications).Error; err != nil {
|
||||
if err := r.db.Table("notifications").
|
||||
Select("notifications.*, circles.webhook_url as webhook_url").
|
||||
Joins("left join circles on circles.id = notifications.circle_id").
|
||||
Where("notifications.is_sent = ? AND notifications.scheduled_for < ? AND notifications.scheduled_for > ?", false, end, start).
|
||||
Find(¬ifications).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return notifications, nil
|
||||
|
|
|
@ -65,7 +65,7 @@ func (n *NotificationPlanner) GenerateNotifications(c context.Context, chore *ch
|
|||
if mt.CircleGroup {
|
||||
notifications = append(notifications, generateCircleGroupNotifications(chore, mt)...)
|
||||
}
|
||||
|
||||
log.Debug("Generated notifications", "count", len(notifications))
|
||||
n.nRepo.BatchInsertNotifications(notifications)
|
||||
return true
|
||||
}
|
||||
|
@ -89,6 +89,13 @@ func generateDueNotifications(chore *chModel.Chore, users []*cModel.UserCircleDe
|
|||
UserID: user.ID,
|
||||
TargetID: user.TargetID,
|
||||
Text: fmt.Sprintf("📅 Reminder: *%s* is due today and assigned to %s.", chore.Name, assignee.DisplayName),
|
||||
RawEvent: map[string]interface{}{
|
||||
"id": chore.ID,
|
||||
"name": chore.Name,
|
||||
"due_date": chore.NextDueDate.Format("January 2nd"),
|
||||
"assignee": assignee.DisplayName,
|
||||
"assignee_username": assignee.Username,
|
||||
},
|
||||
}
|
||||
if notification.IsValid() {
|
||||
notifications = append(notifications, notification)
|
||||
|
@ -117,6 +124,13 @@ func generatePreDueNotifications(chore *chModel.Chore, users []*cModel.UserCircl
|
|||
UserID: user.ID,
|
||||
TargetID: user.TargetID,
|
||||
Text: fmt.Sprintf("📢 Heads up! *%s* is due soon (on %s) and assigned to %s.", chore.Name, chore.NextDueDate.Format("January 2nd"), assignee.DisplayName),
|
||||
RawEvent: map[string]interface{}{
|
||||
"id": chore.ID,
|
||||
"name": chore.Name,
|
||||
"due_date": chore.NextDueDate.Format("January 2nd"),
|
||||
"assignee": assignee.DisplayName,
|
||||
"assignee_username": assignee.Username,
|
||||
},
|
||||
}
|
||||
if notification.IsValid() {
|
||||
notifications = append(notifications, notification)
|
||||
|
@ -148,6 +162,14 @@ func generateOverdueNotifications(chore *chModel.Chore, users []*cModel.UserCirc
|
|||
UserID: user.ID,
|
||||
TargetID: fmt.Sprint(user.TargetID),
|
||||
Text: fmt.Sprintf("🚨 *%s* is now %d hours overdue. Please complete it as soon as possible. (Assigned to %s)", chore.Name, hours, assignee.DisplayName),
|
||||
RawEvent: map[string]interface{}{
|
||||
"id": chore.ID,
|
||||
"type": EventTypeOverdue,
|
||||
"name": chore.Name,
|
||||
"due_date": chore.NextDueDate.Format("January 2nd"),
|
||||
"assignee": assignee.DisplayName,
|
||||
"assignee_username": assignee.Username,
|
||||
},
|
||||
}
|
||||
if notification.IsValid() {
|
||||
notifications = append(notifications, notification)
|
||||
|
@ -173,6 +195,12 @@ func generateCircleGroupNotifications(chore *chModel.Chore, mt *chModel.Notifica
|
|||
TypeID: 1,
|
||||
TargetID: fmt.Sprint(*mt.CircleGroupID),
|
||||
Text: fmt.Sprintf("📅 Reminder: *%s* is due today.", chore.Name),
|
||||
RawEvent: map[string]interface{}{
|
||||
"id": chore.ID,
|
||||
"type": EventTypeDue,
|
||||
"name": chore.Name,
|
||||
"due_date": chore.NextDueDate.Format("January 2nd"),
|
||||
},
|
||||
}
|
||||
if notification.IsValid() {
|
||||
notifications = append(notifications, notification)
|
||||
|
@ -188,6 +216,12 @@ func generateCircleGroupNotifications(chore *chModel.Chore, mt *chModel.Notifica
|
|||
TypeID: 3,
|
||||
TargetID: fmt.Sprint(*mt.CircleGroupID),
|
||||
Text: fmt.Sprintf("📢 Heads up! *%s* is due soon (on %s).", chore.Name, chore.NextDueDate.Format("January 2nd")),
|
||||
RawEvent: map[string]interface{}{
|
||||
"id": chore.ID,
|
||||
"type": EventTypePreDue,
|
||||
"name": chore.Name,
|
||||
"due_date": chore.NextDueDate.Format("January 2nd"),
|
||||
},
|
||||
}
|
||||
if notification.IsValid() {
|
||||
notifications = append(notifications, notification)
|
||||
|
@ -205,6 +239,12 @@ func generateCircleGroupNotifications(chore *chModel.Chore, mt *chModel.Notifica
|
|||
TypeID: 2,
|
||||
TargetID: fmt.Sprint(*mt.CircleGroupID),
|
||||
Text: fmt.Sprintf("🚨 *%s* is now %d hours overdue. Please complete it as soon as possible.", chore.Name, hours),
|
||||
RawEvent: map[string]interface{}{
|
||||
"id": chore.ID,
|
||||
"type": EventTypeOverdue,
|
||||
"name": chore.Name,
|
||||
"due_date": chore.NextDueDate.Format("January 2nd"),
|
||||
},
|
||||
}
|
||||
if notification.IsValid() {
|
||||
notifications = append(notifications, notification)
|
||||
|
@ -214,3 +254,12 @@ func generateCircleGroupNotifications(chore *chModel.Chore, mt *chModel.Notifica
|
|||
|
||||
return notifications
|
||||
}
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeUnknown EventType = "unknown"
|
||||
EventTypeDue EventType = "due"
|
||||
EventTypePreDue EventType = "pre_due"
|
||||
EventTypeOverdue EventType = "overdue"
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@ package pushover
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"donetick.com/core/config"
|
||||
nModel "donetick.com/core/internal/notifier/model"
|
||||
|
@ -22,7 +23,10 @@ func NewPushover(cfg *config.Config) *Pushover {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Pushover) SendNotification(c context.Context, notification *nModel.Notification) error {
|
||||
func (p *Pushover) SendNotification(c context.Context, notification *nModel.NotificationDetails) error {
|
||||
if notification.TargetID == "" {
|
||||
return errors.New("unable to send notification, targetID is empty")
|
||||
}
|
||||
log := logging.FromContext(c)
|
||||
recipient := pushover.NewRecipient(notification.TargetID)
|
||||
message := pushover.NewMessageWithTitle(notification.Text, "Donetick")
|
||||
|
|
|
@ -70,12 +70,10 @@ func (tn *TelegramNotifier) SendChoreCompletion(c context.Context, chore *chMode
|
|||
|
||||
}
|
||||
|
||||
func (tn *TelegramNotifier) SendNotification(c context.Context, notification *nModel.Notification) error {
|
||||
|
||||
func (tn *TelegramNotifier) SendNotification(c context.Context, notification *nModel.NotificationDetails) error {
|
||||
log := logging.FromContext(c)
|
||||
if notification.TargetID == "" {
|
||||
log.Error("Notification target ID is empty")
|
||||
return errors.New("Notification target ID is empty")
|
||||
return errors.New("unable to send notification, targetID is empty")
|
||||
}
|
||||
chatID, err := strconv.ParseInt(notification.TargetID, 10, 64)
|
||||
if err != nil {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue