- Assign default circle to user when leaving a circle

- Support Pushover
- Support Disable Signup
- Migrate chatID to TargetID
This commit is contained in:
Mo Tarbin 2024-12-14 02:15:51 -05:00
parent 850d472445
commit adf5c0c0cd
20 changed files with 362 additions and 151 deletions

View file

@ -3,13 +3,35 @@ 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 int `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"`
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"`
}
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
}
}
type NotificationType int8
const (
NotificationTypeNone NotificationType = iota
NotificationTypeTelegram
NotificationTypePushover
)

View file

@ -0,0 +1,41 @@
package notifier
import (
"context"
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
}
func NewNotifier(t *telegram.TelegramNotifier, p *pushover.Pushover) *Notifier {
return &Notifier{
Telegram: t,
Pushover: p,
}
}
func (n *Notifier) SendNotification(c context.Context, notification *nModel.Notification) error {
log := logging.FromContext(c)
switch notification.TypeID {
case nModel.NotificationTypeTelegram:
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:
if n.Pushover == nil {
log.Error("Pushover is not initialized, Skipping sending message")
return nil
}
return n.Pushover.SendNotification(c, notification)
}
return nil
}

View file

@ -41,3 +41,7 @@ func (r *NotificationRepository) GetPendingNotificaiton(c context.Context, lookb
}
return notifications, nil
}
func (r *NotificationRepository) DeleteSentNotifications(c context.Context, since time.Time) error {
return r.db.WithContext(c).Where("is_sent = ? AND scheduled_for < ?", true, since).Delete(&nModel.Notification{}).Error
}

View file

@ -8,7 +8,6 @@ import (
"donetick.com/core/config"
chRepo "donetick.com/core/internal/chore/repo"
nRepo "donetick.com/core/internal/notifier/repo"
notifier "donetick.com/core/internal/notifier/telegram"
uRepo "donetick.com/core/internal/user/repo"
"donetick.com/core/logging"
)
@ -23,12 +22,12 @@ type Scheduler struct {
choreRepo *chRepo.ChoreRepository
userRepo *uRepo.UserRepository
stopChan chan bool
notifier *notifier.TelegramNotifier
notifier *Notifier
notificationRepo *nRepo.NotificationRepository
SchedulerJobs config.SchedulerConfig
}
func NewScheduler(cfg *config.Config, ur *uRepo.UserRepository, cr *chRepo.ChoreRepository, n *notifier.TelegramNotifier, nr *nRepo.NotificationRepository) *Scheduler {
func NewScheduler(cfg *config.Config, ur *uRepo.UserRepository, cr *chRepo.ChoreRepository, n *Notifier, nr *nRepo.NotificationRepository) *Scheduler {
return &Scheduler{
choreRepo: cr,
userRepo: ur,
@ -43,12 +42,23 @@ func (s *Scheduler) Start(c context.Context) {
log := logging.FromContext(c)
log.Debug("Scheduler started")
go s.runScheduler(c, " NOTIFICATION_SCHEDULER ", s.loadAndSendNotificationJob, 3*time.Minute)
go s.runScheduler(c, " NOTIFICATION_CLEANUP ", s.cleanupSentNotifications, 24*time.Hour*30)
}
func (s *Scheduler) cleanupSentNotifications(c context.Context) (time.Duration, error) {
log := logging.FromContext(c)
deleteBefore := time.Now().UTC().Add(-time.Hour * 24 * 30)
err := s.notificationRepo.DeleteSentNotifications(c, deleteBefore)
if err != nil {
log.Error("Error deleting sent notifications", err)
return time.Duration(0), err
}
return time.Duration(0), nil
}
func (s *Scheduler) loadAndSendNotificationJob(c context.Context) (time.Duration, error) {
log := logging.FromContext(c)
startTime := time.Now()
getAllPendingNotifications, err := s.notificationRepo.GetPendingNotificaiton(c, time.Minute*15)
getAllPendingNotifications, err := s.notificationRepo.GetPendingNotificaiton(c, time.Minute*900)
log.Debug("Getting pending notifications", " count ", len(getAllPendingNotifications))
if err != nil {
@ -57,7 +67,11 @@ func (s *Scheduler) loadAndSendNotificationJob(c context.Context) (time.Duration
}
for _, notification := range getAllPendingNotifications {
s.notifier.SendNotification(c, notification)
err := s.notifier.SendNotification(c, notification)
if err != nil {
log.Error("Error sending notification", err)
continue
}
notification.IsSent = true
}
@ -78,7 +92,7 @@ func (s *Scheduler) runScheduler(c context.Context, jobName string, job func(c c
if err != nil {
logging.FromContext(c).Error("Error running scheduler job", err)
}
logging.FromContext(c).Debug("Scheduler job completed", jobName, " time", elapsedTime.String())
logging.FromContext(c).Debug("Scheduler job completed", jobName, " time: ", elapsedTime.String())
}
time.Sleep(interval)
}

View file

@ -80,18 +80,19 @@ func generateDueNotifications(chore *chModel.Chore, users []*cModel.UserCircleDe
}
}
for _, user := range users {
notification := &nModel.Notification{
ChoreID: chore.ID,
IsSent: false,
ScheduledFor: *chore.NextDueDate,
CreatedAt: time.Now().UTC(),
TypeID: 1,
TypeID: user.NotificationType,
UserID: user.ID,
TargetID: fmt.Sprint(user.ChatID),
TargetID: user.TargetID,
Text: fmt.Sprintf("📅 Reminder: *%s* is due today and assigned to %s.", chore.Name, assignee.DisplayName),
}
notifications = append(notifications, notification)
if notification.IsValid() {
notifications = append(notifications, notification)
}
}
return notifications
@ -112,12 +113,14 @@ func generatePreDueNotifications(chore *chModel.Chore, users []*cModel.UserCircl
IsSent: false,
ScheduledFor: *chore.NextDueDate,
CreatedAt: time.Now().UTC().Add(-time.Hour * 3),
TypeID: 3,
TypeID: user.NotificationType,
UserID: user.ID,
TargetID: fmt.Sprint(user.ChatID),
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),
}
notifications = append(notifications, notification)
if notification.IsValid() {
notifications = append(notifications, notification)
}
}
return notifications
@ -141,12 +144,14 @@ func generateOverdueNotifications(chore *chModel.Chore, users []*cModel.UserCirc
IsSent: false,
ScheduledFor: scheduleTime,
CreatedAt: time.Now().UTC(),
TypeID: 2,
TypeID: user.NotificationType,
UserID: user.ID,
TargetID: fmt.Sprint(user.ChatID),
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),
}
notifications = append(notifications, notification)
if notification.IsValid() {
notifications = append(notifications, notification)
}
}
}
@ -160,7 +165,7 @@ func generateCircleGroupNotifications(chore *chModel.Chore, mt *chModel.Notifica
return notifications
}
if mt.DueDate {
notifications = append(notifications, &nModel.Notification{
notification := &nModel.Notification{
ChoreID: chore.ID,
IsSent: false,
ScheduledFor: *chore.NextDueDate,
@ -168,10 +173,14 @@ 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),
})
}
if notification.IsValid() {
notifications = append(notifications, notification)
}
}
if mt.PreDue {
notifications = append(notifications, &nModel.Notification{
notification := &nModel.Notification{
ChoreID: chore.ID,
IsSent: false,
ScheduledFor: *chore.NextDueDate,
@ -179,12 +188,16 @@ 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")),
})
}
if notification.IsValid() {
notifications = append(notifications, notification)
}
}
if mt.Nagging {
for _, hours := range []int{24, 48, 72} {
scheduleTime := chore.NextDueDate.Add(time.Hour * time.Duration(hours))
notifications = append(notifications, &nModel.Notification{
notification := &nModel.Notification{
ChoreID: chore.ID,
IsSent: false,
ScheduledFor: scheduleTime,
@ -192,7 +205,10 @@ 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),
})
}
if notification.IsValid() {
notifications = append(notifications, notification)
}
}
}

View file

@ -0,0 +1,37 @@
package pushover
import (
"context"
"donetick.com/core/config"
nModel "donetick.com/core/internal/notifier/model"
"donetick.com/core/logging"
"github.com/gregdel/pushover"
)
type Pushover struct {
pushover *pushover.Pushover
}
func NewPushover(cfg *config.Config) *Pushover {
pushoverApp := pushover.New(cfg.Pushover.Token)
return &Pushover{
pushover: pushoverApp,
}
}
func (p *Pushover) SendNotification(c context.Context, notification *nModel.Notification) error {
log := logging.FromContext(c)
recipient := pushover.NewRecipient(notification.TargetID)
message := pushover.NewMessageWithTitle(notification.Text, "Donetick")
_, err := p.pushover.SendMessage(message, recipient)
if err != nil {
log.Debug("Error sending pushover notification", err)
return err
}
return nil
}

View file

@ -3,6 +3,7 @@ package telegram
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
@ -30,26 +31,6 @@ func NewTelegramNotifier(config *config.Config) *TelegramNotifier {
}
}
func (tn *TelegramNotifier) SendChoreReminder(c context.Context, chore *chModel.Chore, users []*uModel.User) {
for _, user := range users {
var assignee *uModel.User
if user.ID == chore.AssignedTo {
if user.ChatID == 0 {
continue
}
assignee = user
text := fmt.Sprintf("*%s* is due today and assigned to *%s*", chore.Name, assignee.DisplayName)
msg := tgbotapi.NewMessage(user.ChatID, text)
msg.ParseMode = "Markdown"
_, err := tn.bot.Send(msg)
if err != nil {
fmt.Println("Error sending message to user: ", err)
}
break
}
}
}
func (tn *TelegramNotifier) SendChoreCompletion(c context.Context, chore *chModel.Chore, user *uModel.User) {
log := logging.FromContext(c)
@ -89,54 +70,17 @@ func (tn *TelegramNotifier) SendChoreCompletion(c context.Context, chore *chMode
}
func (tn *TelegramNotifier) SendChoreOverdue(c context.Context, chore *chModel.Chore, users []*uModel.User) {
log := logging.FromContext(c)
for _, user := range users {
if user.ChatID == 0 {
continue
}
text := fmt.Sprintf("*%s* is overdue and assigned to *%s*", chore.Name, user.DisplayName)
msg := tgbotapi.NewMessage(user.ChatID, text)
msg.ParseMode = "Markdown"
_, err := tn.bot.Send(msg)
if err != nil {
log.Error("Error sending message to user: ", err)
log.Debug("Error sending message, chore: ", chore.Name, " user: ", user.DisplayName, " chatID: ", user.ChatID, " user id: ", user.ID)
}
}
}
func (tn *TelegramNotifier) SendChorePreDue(c context.Context, chore *chModel.Chore, users []*uModel.User) {
log := logging.FromContext(c)
for _, user := range users {
if user.ID != chore.AssignedTo {
continue
}
if user.ChatID == 0 {
continue
}
text := fmt.Sprintf("*%s* is due tomorrow and assigned to *%s*", chore.Name, user.DisplayName)
msg := tgbotapi.NewMessage(user.ChatID, text)
msg.ParseMode = "Markdown"
_, err := tn.bot.Send(msg)
if err != nil {
log.Error("Error sending message to user: ", err)
log.Debug("Error sending message, chore: ", chore.Name, " user: ", user.DisplayName, " chatID: ", user.ChatID, " user id: ", user.ID)
}
}
}
func (tn *TelegramNotifier) SendNotification(c context.Context, notification *nModel.Notification) {
func (tn *TelegramNotifier) SendNotification(c context.Context, notification *nModel.Notification) error {
log := logging.FromContext(c)
if notification.TargetID == "" {
log.Error("Notification target ID is empty")
return
return errors.New("Notification target ID is empty")
}
chatID, err := strconv.ParseInt(notification.TargetID, 10, 64)
if err != nil {
log.Error("Error parsing chatID: ", err)
return
return err
}
msg := tgbotapi.NewMessage(chatID, notification.Text)
@ -145,5 +89,7 @@ func (tn *TelegramNotifier) SendNotification(c context.Context, notification *nM
if err != nil {
log.Error("Error sending message to user: ", err)
log.Debug("Error sending message, notification: ", notification.Text, " chatID: ", chatID)
return err
}
return nil
}