Compare commits

...

14 commits

Author SHA1 Message Date
Mohamad Tarbin
5b10f63e65
Merge pull request #149 from Alone2/fix/hardcode-email-address
Some checks are pending
Test and Build / test (push) Waiting to run
Test and Build / build (push) Blocked by required conditions
fix previously hardcoded email address
2025-03-17 01:18:57 -04:00
Mo Tarbin
cbd82c0e50 Merge branch 'dev' 2025-03-17 01:09:14 -04:00
Mo Tarbin
25d4fbfc1d Fix notification metadata field name in migration to ensure proper updates 2025-03-17 01:08:58 -04:00
Mo Tarbin
acb6bbe180 Merge branch 'dev'
Fix notification #163
2025-03-17 01:05:12 -04:00
Mo Tarbin
cdcde095dd Fix notification metadata handling and add migration for null values 2025-03-17 01:01:26 -04:00
Mo Tarbin
ffdf57322b Update due date calculations. as it now fix to pick from time from FrequencyMetadata 2025-03-05 20:07:13 -05:00
Mo Tarbin
da41061163 Update due date calculations. as it now fix to pick from time from FrequencyMetadata 2025-03-05 20:07:04 -05:00
Mo Tarbin
4f53a2eef2 Update database port configuration in local and selfhosted YAML files 2025-03-05 19:56:19 -05:00
Mo Tarbin
5d6a8b510b Merge branch 'main' of https://github.com/donetick/donetick 2025-03-05 19:54:27 -05:00
Mo Tarbin
5127383c59 Add subtask model and repository, implement webhook notification handling
Support NotificationPlatformWebhook
support Discord as notification target
fix Issue with Scheduler
Fix #154
Fix #153
2025-03-05 19:52:36 -05:00
Mo Tarbin
81acbd8eba Add subtask model and repository, implement webhook notification handling
fix Issue with Scheduler
Support NotificationPlatformWebhook
support Discord as notification target
2025-03-05 19:52:10 -05:00
Mohamad Tarbin
cff4c453ff
Merge pull request #142 from davidharrigan/fix/postgres-migration
fix: postgres migration
2025-02-27 23:55:09 -05:00
Alain Sinzig
b9eda3438a
fix previously hardcoded email address 2025-02-28 03:19:46 +01:00
David Harrigan
235cd0ad93 fix: postgres migration
Using postgres results in panic during migration due to "sqlite3" being
hard-coded as the db dialect for performing `ScriptMigration`.
This commit fixes this behavior by selecting the proper dialect from
config.
2025-02-22 20:13:35 -05:00
14 changed files with 226 additions and 31 deletions

View file

@ -8,6 +8,12 @@ pushover:
database: database:
type: "sqlite" type: "sqlite"
migration: true migration: true
# these are only required for postgres
host: "secret"
port: 5432
user: "secret"
password: "secret"
name: "secret"
jwt: jwt:
secret: "secret" secret: "secret"
session_time: 168h session_time: 168h

View file

@ -8,6 +8,12 @@ pushover:
database: database:
type: "sqlite" type: "sqlite"
migration: true migration: true
# these are only required for postgres
host: "secret"
port: 5432
user: "secret"
password: "secret"
name: "secret"
jwt: jwt:
secret: "secret" secret: "secret"
session_time: 168h session_time: 168h

View file

@ -231,7 +231,13 @@ func (h *Handler) createChore(c *gin.Context) {
return return
} }
stringNotificationMetadata := string(notificationMetadataBytes) stringNotificationMetadata := string(notificationMetadataBytes)
if stringNotificationMetadata == "null" {
// TODO: Clean this update after 0.1.38.. there is a bug in the frontend that sends null instead of empty object
// this is a temporary fix to avoid breaking changes
// once change we can change notificationMetadataBytes to var notificationMetadataMap map[string]interface{} to gernerate empty object
// and remove this check
stringNotificationMetadata = "{}"
}
var stringLabels *string var stringLabels *string
if len(choreReq.Labels) > 0 { if len(choreReq.Labels) > 0 {
var escapedLabels []string var escapedLabels []string
@ -464,6 +470,13 @@ func (h *Handler) editChore(c *gin.Context) {
return return
} }
stringNotificationMetadata := string(notificationMetadataBytes) stringNotificationMetadata := string(notificationMetadataBytes)
if stringNotificationMetadata == "null" {
// TODO: Clean this update after 0.1.38.. there is a bug in the frontend that sends null instead of empty object
// this is a temporary fix to avoid breaking changes
// once change we can change notificationMetadataBytes to var notificationMetadataMap map[string]interface{} to gernerate empty object
// and remove this check
stringNotificationMetadata = "{}"
}
// escape special characters in labels and store them as a string : // escape special characters in labels and store them as a string :
var stringLabels *string var stringLabels *string

View file

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

View file

@ -119,7 +119,7 @@ func TestScheduleNextDueDateInterval(t *testing.T) {
FrequencyMetadata: jsonPtr(`{"unit": "days","time":"2024-07-07T14:30:00-04:00"}`), FrequencyMetadata: jsonPtr(`{"unit": "days","time":"2024-07-07T14:30:00-04:00"}`),
}, },
completedDate: now, completedDate: now,
want: timePtr(truncateToDay(now.AddDate(0, 0, 2)).Add(14*time.Hour + 30*time.Minute)), want: timePtr(truncateToDay(now.AddDate(0, 0, 2)).Add(18*time.Hour + 30*time.Minute)),
}, },
{ {
name: "Interval - 4 Weeks", name: "Interval - 4 Weeks",
@ -129,7 +129,7 @@ func TestScheduleNextDueDateInterval(t *testing.T) {
FrequencyMetadata: jsonPtr(`{"unit": "weeks","time":"2024-07-07T14:30:00-04:00"}`), FrequencyMetadata: jsonPtr(`{"unit": "weeks","time":"2024-07-07T14:30:00-04:00"}`),
}, },
completedDate: now, completedDate: now,
want: timePtr(truncateToDay(now.AddDate(0, 0, 4*7)).Add(14*time.Hour + 30*time.Minute)), want: timePtr(truncateToDay(now.AddDate(0, 0, 4*7)).Add(18*time.Hour + 30*time.Minute)),
}, },
{ {
name: "Interval - 3 Months", name: "Interval - 3 Months",
@ -139,7 +139,7 @@ func TestScheduleNextDueDateInterval(t *testing.T) {
FrequencyMetadata: jsonPtr(`{"unit": "months","time":"2024-07-07T14:30:00-04:00"}`), FrequencyMetadata: jsonPtr(`{"unit": "months","time":"2024-07-07T14:30:00-04:00"}`),
}, },
completedDate: now, completedDate: now,
want: timePtr(truncateToDay(now.AddDate(0, 3, 0)).Add(14*time.Hour + 30*time.Minute)), want: timePtr(truncateToDay(now.AddDate(0, 3, 0)).Add(18*time.Hour + 30*time.Minute)),
}, },
{ {
name: "Interval - 2 Years", name: "Interval - 2 Years",
@ -149,7 +149,7 @@ func TestScheduleNextDueDateInterval(t *testing.T) {
FrequencyMetadata: jsonPtr(`{"unit": "years","time":"2024-07-07T14:30:00-04:00"}`), FrequencyMetadata: jsonPtr(`{"unit": "years","time":"2024-07-07T14:30:00-04:00"}`),
}, },
completedDate: now, completedDate: now,
want: timePtr(truncateToDay(now.AddDate(2, 0, 0)).Add(14*time.Hour + 30*time.Minute)), want: timePtr(truncateToDay(now.AddDate(2, 0, 0)).Add(18*time.Hour + 30*time.Minute)),
}, },
} }
executeTestTable(t, tests) executeTestTable(t, tests)
@ -169,13 +169,13 @@ func TestScheduleNextDueDateDayOfWeek(t *testing.T) {
chore: chModel.Chore{ chore: chModel.Chore{
FrequencyType: chModel.FrequencyTypeDayOfTheWeek, FrequencyType: chModel.FrequencyTypeDayOfTheWeek,
FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T18:00:00-05:00"}`), FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T01:00:00-05:00"}`),
}, },
completedDate: now, completedDate: now,
want: func() *time.Time { want: func() *time.Time {
// Calculate next Monday at 18:00 EST // Calculate next Monday at 18:00 EST
nextMonday := now.AddDate(0, 0, (int(time.Monday)-int(now.Weekday())+7)%7) nextMonday := now.AddDate(0, 0, (int(time.Monday)-int(now.Weekday())+7)%7)
nextMonday = truncateToDay(nextMonday).Add(18*time.Hour + 0*time.Minute) nextMonday = truncateToDay(nextMonday).Add(6*time.Hour + 0*time.Minute)
return &nextMonday return &nextMonday
}(), }(),
}, },
@ -184,7 +184,7 @@ func TestScheduleNextDueDateDayOfWeek(t *testing.T) {
chore: chModel.Chore{ chore: chModel.Chore{
FrequencyType: chModel.FrequencyTypeDayOfTheWeek, FrequencyType: chModel.FrequencyTypeDayOfTheWeek,
IsRolling: true, IsRolling: true,
FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T18:00:00-05:00"}`), FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T01:00:00-05:00"}`),
}, },
completedDate: now.AddDate(0, 1, 0), completedDate: now.AddDate(0, 1, 0),
@ -192,7 +192,7 @@ func TestScheduleNextDueDateDayOfWeek(t *testing.T) {
// Calculate next Thursday at 18:00 EST // Calculate next Thursday at 18:00 EST
completedDate := now.AddDate(0, 1, 0) completedDate := now.AddDate(0, 1, 0)
nextMonday := completedDate.AddDate(0, 0, (int(time.Monday)-int(completedDate.Weekday())+7)%7) nextMonday := completedDate.AddDate(0, 0, (int(time.Monday)-int(completedDate.Weekday())+7)%7)
nextMonday = truncateToDay(nextMonday).Add(18*time.Hour + 0*time.Minute) nextMonday = truncateToDay(nextMonday).Add(6*time.Hour + 0*time.Minute)
return &nextMonday return &nextMonday
}(), }(),
}, },
@ -214,10 +214,10 @@ func TestScheduleNextDueDateDayOfMonth(t *testing.T) {
chore: chModel.Chore{ chore: chModel.Chore{
FrequencyType: chModel.FrequencyTypeDayOfTheMonth, FrequencyType: chModel.FrequencyTypeDayOfTheMonth,
Frequency: 15, Frequency: 15,
FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`), FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T14:00:00-05:00", "days": [], "months": [ "january" ] }`),
}, },
completedDate: now, completedDate: now,
want: timePtr(time.Date(2025, 1, 15, 18, 0, 0, 0, location)), want: timePtr(time.Date(2025, 1, 15, 19, 0, 0, 0, location)),
}, },
{ {
name: "Day of the month - 15th of January(isRolling)", name: "Day of the month - 15th of January(isRolling)",
@ -225,10 +225,10 @@ func TestScheduleNextDueDateDayOfMonth(t *testing.T) {
FrequencyType: chModel.FrequencyTypeDayOfTheMonth, FrequencyType: chModel.FrequencyTypeDayOfTheMonth,
Frequency: 15, Frequency: 15,
IsRolling: true, IsRolling: true,
FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`), FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T02:00:00-05:00", "days": [], "months": [ "january" ] }`),
}, },
completedDate: now.AddDate(1, 1, 0), completedDate: now.AddDate(1, 1, 0),
want: timePtr(time.Date(2027, 1, 15, 18, 0, 0, 0, location)), want: timePtr(time.Date(2027, 1, 15, 7, 0, 0, 0, location)),
}, },
// test if completed before the 15th of the month: // test if completed before the 15th of the month:
{ {

View file

@ -3,7 +3,9 @@ package database
import ( import (
"embed" "embed"
"fmt" "fmt"
"os"
migrate "github.com/rubenv/sql-migrate"
"gorm.io/gorm"
"donetick.com/core/config" "donetick.com/core/config"
chModel "donetick.com/core/internal/chore/model" chModel "donetick.com/core/internal/chore/model"
@ -13,9 +15,7 @@ import (
stModel "donetick.com/core/internal/subtask/model" stModel "donetick.com/core/internal/subtask/model"
tModel "donetick.com/core/internal/thing/model" tModel "donetick.com/core/internal/thing/model"
uModel "donetick.com/core/internal/user/model" // Pure go SQLite driver, checkout https://github.com/glebarez/sqlite for details uModel "donetick.com/core/internal/user/model" // Pure go SQLite driver, checkout https://github.com/glebarez/sqlite for details
migrations "donetick.com/core/migrations" "donetick.com/core/migrations"
migrate "github.com/rubenv/sql-migrate"
"gorm.io/gorm"
) )
//go:embed migrations/*.sql //go:embed migrations/*.sql
@ -52,9 +52,14 @@ func MigrationScripts(gormDB *gorm.DB, cfg *config.Config) error {
Root: "migrations", Root: "migrations",
} }
path := os.Getenv("DT_SQLITE_PATH") var dialect string
if path == "" { switch cfg.Database.Type {
path = "donetick.db" case "postgres":
dialect = "postgres"
case "sqlite":
dialect = "sqlite3"
default:
return fmt.Errorf("unsupported database type: %s", cfg.Database.Type)
} }
db, err := gormDB.DB() db, err := gormDB.DB()
@ -62,7 +67,7 @@ func MigrationScripts(gormDB *gorm.DB, cfg *config.Config) error {
return err return err
} }
n, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up) n, err := migrate.Exec(db, dialect, migrations, migrate.Up)
if err != nil { if err != nil {
return err return err
} }

View file

@ -32,7 +32,7 @@ func NewEmailSender(conf *config.Config) *EmailSender {
func (es *EmailSender) SendVerificationEmail(to, code string) error { func (es *EmailSender) SendVerificationEmail(to, code string) error {
// msg := []byte(fmt.Sprintf("To: %s\r\nSubject: %s\r\n\r\n%s\r\n", to, subject, body)) // msg := []byte(fmt.Sprintf("To: %s\r\nSubject: %s\r\n\r\n%s\r\n", to, subject, body))
msg := gomail.NewMessage() msg := gomail.NewMessage()
msg.SetHeader("From", "no-reply@donetick.com") msg.SetHeader("From", es.client.Username)
msg.SetHeader("To", to) msg.SetHeader("To", to)
msg.SetHeader("Subject", "Welcome to Donetick! Verifiy you email") msg.SetHeader("Subject", "Welcome to Donetick! Verifiy you email")
// text/html for a html email // text/html for a html email
@ -259,7 +259,7 @@ func (es *EmailSender) SendVerificationEmail(to, code string) error {
func (es *EmailSender) SendResetPasswordEmail(c context.Context, to, code string) error { func (es *EmailSender) SendResetPasswordEmail(c context.Context, to, code string) error {
msg := gomail.NewMessage() msg := gomail.NewMessage()
msg.SetHeader("From", "no-reply@donetick.com") msg.SetHeader("From", es.client.Username)
msg.SetHeader("To", to) msg.SetHeader("To", to)
msg.SetHeader("Subject", "Donetick! Password Reset") msg.SetHeader("Subject", "Donetick! Password Reset")
htmlBody := ` htmlBody := `

View file

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

View file

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

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

View file

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

View file

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

View file

@ -0,0 +1,43 @@
package migrations
import (
"context"
"donetick.com/core/logging"
"gorm.io/gorm"
)
type MigrateFixNotificationMetadataExperimentModal20241212 struct{}
func (m MigrateFixNotificationMetadataExperimentModal20241212) ID() string {
return "20250314_fix_notification_metadata_experiment_modal"
}
func (m MigrateFixNotificationMetadataExperimentModal20241212) Description() string {
return `Fix notification metadata for experiment modal, where notification metadata is a null string 'null' to empty json {}`
}
func (m MigrateFixNotificationMetadataExperimentModal20241212) Down(ctx context.Context, db *gorm.DB) error {
return nil
}
func (m MigrateFixNotificationMetadataExperimentModal20241212) Up(ctx context.Context, db *gorm.DB) error {
log := logging.FromContext(ctx)
// Start a transaction
return db.Transaction(func(tx *gorm.DB) error {
// Update all chore where notification metadata is a null stirng 'null' to empty json {}:
if err := tx.Table("chores").Where("notification_meta = ?", "null").Update("notification_meta", "{}").Error; err != nil {
log.Errorf("Failed to update chores with null notification metadata: %v", err)
return err
}
return nil
})
}
// Register this migration
func init() {
Register(MigrateFixNotificationMetadataExperimentModal20241212{})
}