Move to Donetick Org, first commit

This commit is contained in:
Mo Tarbin 2024-06-30 21:41:41 -04:00
commit c13dd9addb
42 changed files with 7463 additions and 0 deletions

View file

@ -0,0 +1,15 @@
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"`
}

View file

@ -0,0 +1,43 @@
package user
import (
"context"
"time"
nModel "donetick.com/core/internal/notifier/model"
"gorm.io/gorm"
)
type NotificationRepository struct {
db *gorm.DB
}
func NewNotificationRepository(db *gorm.DB) *NotificationRepository {
return &NotificationRepository{db}
}
func (r *NotificationRepository) DeleteAllChoreNotifications(choreID int) error {
return r.db.Where("chore_id = ?", choreID).Delete(&nModel.Notification{}).Error
}
func (r *NotificationRepository) BatchInsertNotifications(notifications []*nModel.Notification) error {
return r.db.Create(&notifications).Error
}
func (r *NotificationRepository) MarkNotificationsAsSent(notifications []*nModel.Notification) error {
// Extract IDs from notifications
var ids []int
for _, notification := range notifications {
ids = append(ids, notification.ID)
}
// 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
start := time.Now().UTC().Add(-lookback)
end := time.Now().UTC()
if err := r.db.Debug().Where("is_sent = ? AND scheduled_for < ? AND scheduled_for > ?", false, end, start).Find(&notifications).Error; err != nil {
return nil, err
}
return notifications, nil
}

View file

@ -0,0 +1,89 @@
package notifier
import (
"context"
"log"
"time"
"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"
)
type keyType string
const (
SchedulerKey keyType = "scheduler"
)
type Scheduler struct {
choreRepo *chRepo.ChoreRepository
userRepo *uRepo.UserRepository
stopChan chan bool
notifier *notifier.TelegramNotifier
notificationRepo *nRepo.NotificationRepository
SchedulerJobs config.SchedulerConfig
}
func NewScheduler(cfg *config.Config, ur *uRepo.UserRepository, cr *chRepo.ChoreRepository, n *notifier.TelegramNotifier, nr *nRepo.NotificationRepository) *Scheduler {
return &Scheduler{
choreRepo: cr,
userRepo: ur,
stopChan: make(chan bool),
notifier: n,
notificationRepo: nr,
SchedulerJobs: cfg.SchedulerJobs,
}
}
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)
}
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)
log.Debug("Getting pending notifications", " count ", len(getAllPendingNotifications))
if err != nil {
log.Error("Error getting pending notifications")
return time.Since(startTime), err
}
for _, notification := range getAllPendingNotifications {
s.notifier.SendNotification(c, notification)
notification.IsSent = true
}
s.notificationRepo.MarkNotificationsAsSent(getAllPendingNotifications)
return time.Since(startTime), nil
}
func (s *Scheduler) runScheduler(c context.Context, jobName string, job func(c context.Context) (time.Duration, error), interval time.Duration) {
for {
logging.FromContext(c).Debug("Scheduler running ", jobName, " time", time.Now().String())
select {
case <-s.stopChan:
log.Println("Scheduler stopped")
return
default:
elapsedTime, err := job(c)
if err != nil {
logging.FromContext(c).Error("Error running scheduler job", err)
}
logging.FromContext(c).Debug("Scheduler job completed", jobName, " time", elapsedTime.String())
}
time.Sleep(interval)
}
}
func (s *Scheduler) Stop() {
s.stopChan <- true
}

View file

@ -0,0 +1,149 @@
package service
import (
"context"
"encoding/json"
"fmt"
"time"
chModel "donetick.com/core/internal/chore/model"
cModel "donetick.com/core/internal/circle/model"
cRepo "donetick.com/core/internal/circle/repo"
nModel "donetick.com/core/internal/notifier/model"
nRepo "donetick.com/core/internal/notifier/repo"
"donetick.com/core/logging"
)
type NotificationPlanner struct {
nRepo *nRepo.NotificationRepository
cRepo *cRepo.CircleRepository
}
func NewNotificationPlanner(nr *nRepo.NotificationRepository, cr *cRepo.CircleRepository) *NotificationPlanner {
return &NotificationPlanner{nRepo: nr,
cRepo: cr,
}
}
func (n *NotificationPlanner) GenerateNotifications(c context.Context, chore *chModel.Chore) bool {
log := logging.FromContext(c)
circleMembers, err := n.cRepo.GetCircleUsers(c, chore.CircleID)
assignees := make([]*cModel.UserCircleDetail, 0)
for _, member := range circleMembers {
if member.ID == chore.AssignedTo {
assignees = append(assignees, member)
}
}
if err != nil {
log.Error("Error getting circle members", err)
return false
}
n.nRepo.DeleteAllChoreNotifications(chore.ID)
notifications := make([]*nModel.Notification, 0)
if !chore.Notification || chore.FrequencyType == "trigger" {
return true
}
var mt *chModel.NotificationMetadata
if err := json.Unmarshal([]byte(*chore.NotificationMetadata), &mt); err != nil {
log.Error("Error unmarshalling notification metadata", err)
return true
}
if mt.DueDate {
notifications = append(notifications, generateDueNotifications(chore, assignees)...)
}
if mt.PreDue {
notifications = append(notifications, generatePreDueNotifications(chore, assignees)...)
}
if mt.Nagging {
notifications = append(notifications, generateOverdueNotifications(chore, assignees)...)
}
n.nRepo.BatchInsertNotifications(notifications)
return true
}
func generateDueNotifications(chore *chModel.Chore, users []*cModel.UserCircleDetail) []*nModel.Notification {
var assignee *cModel.UserCircleDetail
notifications := make([]*nModel.Notification, 0)
for _, user := range users {
if user.ID == chore.AssignedTo {
assignee = user
break
}
}
for _, user := range users {
notification := &nModel.Notification{
ChoreID: chore.ID,
IsSent: false,
ScheduledFor: *chore.NextDueDate,
CreatedAt: time.Now().UTC(),
TypeID: 1,
UserID: user.ID,
TargetID: fmt.Sprint(user.ChatID),
Text: fmt.Sprintf("📅 Reminder: '%s' is due today and assigned to %s.", chore.Name, assignee.DisplayName),
}
notifications = append(notifications, notification)
}
return notifications
}
func generatePreDueNotifications(chore *chModel.Chore, users []*cModel.UserCircleDetail) []*nModel.Notification {
var assignee *cModel.UserCircleDetail
for _, user := range users {
if user.ID == chore.AssignedTo {
assignee = user
break
}
}
notifications := make([]*nModel.Notification, 0)
for _, user := range users {
notification := &nModel.Notification{
ChoreID: chore.ID,
IsSent: false,
ScheduledFor: *chore.NextDueDate,
CreatedAt: time.Now().UTC().Add(-time.Hour * 3),
TypeID: 3,
UserID: user.ID,
TargetID: fmt.Sprint(user.ChatID),
Text: fmt.Sprintf("📢 Heads up! Chore '%s' is due soon (on %s) and assigned to %s.", chore.Name, chore.NextDueDate.Format("January 2nd"), assignee.DisplayName),
}
notifications = append(notifications, notification)
}
return notifications
}
func generateOverdueNotifications(chore *chModel.Chore, users []*cModel.UserCircleDetail) []*nModel.Notification {
var assignee *cModel.UserCircleDetail
for _, user := range users {
if user.ID == chore.AssignedTo {
assignee = user
break
}
}
notifications := make([]*nModel.Notification, 0)
for _, hours := range []int{24, 48, 72} {
scheduleTime := chore.NextDueDate.Add(time.Hour * time.Duration(hours))
for _, user := range users {
notification := &nModel.Notification{
ChoreID: chore.ID,
IsSent: false,
ScheduledFor: scheduleTime,
CreatedAt: time.Now().UTC(),
TypeID: 2,
UserID: user.ID,
TargetID: fmt.Sprint(user.ChatID),
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)
}
}
return notifications
}

View file

@ -0,0 +1,127 @@
package telegram
import (
"context"
"fmt"
"strconv"
"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"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
type TelegramNotifier struct {
bot *tgbotapi.BotAPI
}
func NewTelegramNotifier(config *config.Config) *TelegramNotifier {
bot, err := tgbotapi.NewBotAPI(config.Telegram.Token)
if err != nil {
fmt.Println("Error creating bot: ", err)
return nil
}
return &TelegramNotifier{
bot: bot,
}
}
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, users []*uModel.User) {
log := logging.FromContext(c)
for _, user := range users {
if user.ChatID == 0 {
continue
}
text := fmt.Sprintf("🎉 '%s' is completed! is off the list, %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) 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) {
log := logging.FromContext(c)
if notification.TargetID == "" {
log.Error("Notification target ID is empty")
return
}
chatID, err := strconv.ParseInt(notification.TargetID, 10, 64)
if err != nil {
log.Error("Error parsing chatID: ", err)
return
}
msg := tgbotapi.NewMessage(chatID, notification.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, notification: ", notification.Text, " chatID: ", chatID)
}
}