diff --git a/config/local.yaml b/config/local.yaml index 649abcd..f9c888f 100644 --- a/config/local.yaml +++ b/config/local.yaml @@ -8,12 +8,6 @@ pushover: database: type: "sqlite" migration: true - # these are only required for postgres - host: "secret" - port: 5432 - user: "secret" - password: "secret" - name: "secret" jwt: secret: "secret" session_time: 168h diff --git a/config/selfhosted.yaml b/config/selfhosted.yaml index 37e9d9b..26e2eb5 100644 --- a/config/selfhosted.yaml +++ b/config/selfhosted.yaml @@ -8,12 +8,6 @@ pushover: database: type: "sqlite" migration: true - # these are only required for postgres - host: "secret" - port: 5432 - user: "secret" - password: "secret" - name: "secret" jwt: secret: "secret" session_time: 168h diff --git a/internal/chore/handler.go b/internal/chore/handler.go index 4000d84..cf62ece 100644 --- a/internal/chore/handler.go +++ b/internal/chore/handler.go @@ -231,13 +231,7 @@ func (h *Handler) createChore(c *gin.Context) { return } 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 if len(choreReq.Labels) > 0 { var escapedLabels []string @@ -470,13 +464,6 @@ func (h *Handler) editChore(c *gin.Context) { return } 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 : var stringLabels *string diff --git a/internal/chore/scheduler.go b/internal/chore/scheduler.go index 3341cc2..b936278 100644 --- a/internal/chore/scheduler.go +++ b/internal/chore/scheduler.go @@ -38,8 +38,9 @@ func scheduleNextDueDate(chore *chModel.Chore, completedDate time.Time) (*time.T if err != nil { 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) + // If the time is in the past today, move it to tomorrow if baseDate.Before(completedDate) { baseDate = baseDate.AddDate(0, 0, 1) diff --git a/internal/chore/scheduler_test.go b/internal/chore/scheduler_test.go index 2091579..14c81d2 100644 --- a/internal/chore/scheduler_test.go +++ b/internal/chore/scheduler_test.go @@ -119,7 +119,7 @@ func TestScheduleNextDueDateInterval(t *testing.T) { FrequencyMetadata: jsonPtr(`{"unit": "days","time":"2024-07-07T14:30:00-04:00"}`), }, completedDate: now, - want: timePtr(truncateToDay(now.AddDate(0, 0, 2)).Add(18*time.Hour + 30*time.Minute)), + want: timePtr(truncateToDay(now.AddDate(0, 0, 2)).Add(14*time.Hour + 30*time.Minute)), }, { 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"}`), }, completedDate: now, - want: timePtr(truncateToDay(now.AddDate(0, 0, 4*7)).Add(18*time.Hour + 30*time.Minute)), + want: timePtr(truncateToDay(now.AddDate(0, 0, 4*7)).Add(14*time.Hour + 30*time.Minute)), }, { 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"}`), }, completedDate: now, - want: timePtr(truncateToDay(now.AddDate(0, 3, 0)).Add(18*time.Hour + 30*time.Minute)), + want: timePtr(truncateToDay(now.AddDate(0, 3, 0)).Add(14*time.Hour + 30*time.Minute)), }, { 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"}`), }, completedDate: now, - want: timePtr(truncateToDay(now.AddDate(2, 0, 0)).Add(18*time.Hour + 30*time.Minute)), + want: timePtr(truncateToDay(now.AddDate(2, 0, 0)).Add(14*time.Hour + 30*time.Minute)), }, } executeTestTable(t, tests) @@ -169,13 +169,13 @@ func TestScheduleNextDueDateDayOfWeek(t *testing.T) { chore: chModel.Chore{ FrequencyType: chModel.FrequencyTypeDayOfTheWeek, - FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T01:00:00-05:00"}`), + FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T18:00:00-05:00"}`), }, completedDate: now, want: func() *time.Time { // Calculate next Monday at 18:00 EST nextMonday := now.AddDate(0, 0, (int(time.Monday)-int(now.Weekday())+7)%7) - nextMonday = truncateToDay(nextMonday).Add(6*time.Hour + 0*time.Minute) + nextMonday = truncateToDay(nextMonday).Add(18*time.Hour + 0*time.Minute) return &nextMonday }(), }, @@ -184,7 +184,7 @@ func TestScheduleNextDueDateDayOfWeek(t *testing.T) { chore: chModel.Chore{ FrequencyType: chModel.FrequencyTypeDayOfTheWeek, IsRolling: true, - FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T01:00:00-05:00"}`), + FrequencyMetadata: jsonPtr(`{"days": ["monday"], "time": "2025-01-20T18:00:00-05:00"}`), }, completedDate: now.AddDate(0, 1, 0), @@ -192,7 +192,7 @@ func TestScheduleNextDueDateDayOfWeek(t *testing.T) { // Calculate next Thursday at 18:00 EST completedDate := now.AddDate(0, 1, 0) nextMonday := completedDate.AddDate(0, 0, (int(time.Monday)-int(completedDate.Weekday())+7)%7) - nextMonday = truncateToDay(nextMonday).Add(6*time.Hour + 0*time.Minute) + nextMonday = truncateToDay(nextMonday).Add(18*time.Hour + 0*time.Minute) return &nextMonday }(), }, @@ -214,10 +214,10 @@ func TestScheduleNextDueDateDayOfMonth(t *testing.T) { chore: chModel.Chore{ FrequencyType: chModel.FrequencyTypeDayOfTheMonth, Frequency: 15, - FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T14:00:00-05:00", "days": [], "months": [ "january" ] }`), + FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`), }, completedDate: now, - want: timePtr(time.Date(2025, 1, 15, 19, 0, 0, 0, location)), + want: timePtr(time.Date(2025, 1, 15, 18, 0, 0, 0, location)), }, { name: "Day of the month - 15th of January(isRolling)", @@ -225,10 +225,10 @@ func TestScheduleNextDueDateDayOfMonth(t *testing.T) { FrequencyType: chModel.FrequencyTypeDayOfTheMonth, Frequency: 15, IsRolling: true, - FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T02:00:00-05:00", "days": [], "months": [ "january" ] }`), + FrequencyMetadata: jsonPtr(`{ "unit": "days", "time": "2025-01-20T18:00:00-05:00", "days": [], "months": [ "january" ] }`), }, completedDate: now.AddDate(1, 1, 0), - want: timePtr(time.Date(2027, 1, 15, 7, 0, 0, 0, location)), + want: timePtr(time.Date(2027, 1, 15, 18, 0, 0, 0, location)), }, // test if completed before the 15th of the month: { diff --git a/internal/database/migration.go b/internal/database/migration.go index 54e7eb2..cfca864 100644 --- a/internal/database/migration.go +++ b/internal/database/migration.go @@ -3,9 +3,7 @@ package database import ( "embed" "fmt" - - migrate "github.com/rubenv/sql-migrate" - "gorm.io/gorm" + "os" "donetick.com/core/config" chModel "donetick.com/core/internal/chore/model" @@ -15,7 +13,9 @@ import ( stModel "donetick.com/core/internal/subtask/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 - "donetick.com/core/migrations" + migrations "donetick.com/core/migrations" + migrate "github.com/rubenv/sql-migrate" + "gorm.io/gorm" ) //go:embed migrations/*.sql @@ -52,14 +52,9 @@ func MigrationScripts(gormDB *gorm.DB, cfg *config.Config) error { Root: "migrations", } - var dialect string - switch cfg.Database.Type { - case "postgres": - dialect = "postgres" - case "sqlite": - dialect = "sqlite3" - default: - return fmt.Errorf("unsupported database type: %s", cfg.Database.Type) + path := os.Getenv("DT_SQLITE_PATH") + if path == "" { + path = "donetick.db" } db, err := gormDB.DB() @@ -67,7 +62,7 @@ func MigrationScripts(gormDB *gorm.DB, cfg *config.Config) error { return err } - n, err := migrate.Exec(db, dialect, migrations, migrate.Up) + n, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up) if err != nil { return err } diff --git a/internal/email/sender.go b/internal/email/sender.go index 6afc8e1..540e54a 100644 --- a/internal/email/sender.go +++ b/internal/email/sender.go @@ -32,7 +32,7 @@ func NewEmailSender(conf *config.Config) *EmailSender { 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 := gomail.NewMessage() - msg.SetHeader("From", es.client.Username) + msg.SetHeader("From", "no-reply@donetick.com") msg.SetHeader("To", to) msg.SetHeader("Subject", "Welcome to Donetick! Verifiy you 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 { msg := gomail.NewMessage() - msg.SetHeader("From", es.client.Username) + msg.SetHeader("From", "no-reply@donetick.com") msg.SetHeader("To", to) msg.SetHeader("Subject", "Donetick! Password Reset") htmlBody := ` diff --git a/internal/notifier/model/model.go b/internal/notifier/model/model.go index b8efb48..a3696df 100644 --- a/internal/notifier/model/model.go +++ b/internal/notifier/model/model.go @@ -37,7 +37,6 @@ const ( NotificationPlatformTelegram NotificationPlatformPushover NotificationPlatformWebhook - NotificationPlatformDiscord ) type JSONB map[string]interface{} diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go index 8ecd3ac..1ee47d4 100644 --- a/internal/notifier/notifier.go +++ b/internal/notifier/notifier.go @@ -5,7 +5,6 @@ import ( "donetick.com/core/internal/events" nModel "donetick.com/core/internal/notifier/model" - "donetick.com/core/internal/notifier/service/discord" pushover "donetick.com/core/internal/notifier/service/pushover" telegram "donetick.com/core/internal/notifier/service/telegram" @@ -15,16 +14,14 @@ import ( type Notifier struct { Telegram *telegram.TelegramNotifier Pushover *pushover.Pushover - discord *discord.DiscordNotifier eventsProducer *events.EventsProducer } -func NewNotifier(t *telegram.TelegramNotifier, p *pushover.Pushover, ep *events.EventsProducer, d *discord.DiscordNotifier) *Notifier { +func NewNotifier(t *telegram.TelegramNotifier, p *pushover.Pushover, ep *events.EventsProducer) *Notifier { return &Notifier{ Telegram: t, Pushover: p, eventsProducer: ep, - discord: d, } } @@ -44,13 +41,6 @@ func (n *Notifier) SendNotification(c context.Context, notification *nModel.Noti return nil } 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: // TODO: Implement webhook notification // currently we have eventProducer to send events always as a webhook diff --git a/internal/notifier/service/discord/discord.go b/internal/notifier/service/discord/discord.go deleted file mode 100644 index 30aed57..0000000 --- a/internal/notifier/service/discord/discord.go +++ /dev/null @@ -1,84 +0,0 @@ -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 -} diff --git a/internal/subtask/model/model.go b/internal/subtask/model/model.go index e6dae3b..56e873d 100644 --- a/internal/subtask/model/model.go +++ b/internal/subtask/model/model.go @@ -9,5 +9,4 @@ type SubTask struct { Name string `json:"name" gorm:"column:name"` CompletedAt *time.Time `json:"completedAt" gorm:"column:completed_at"` CompletedBy int `json:"completedBy" gorm:"column:completed_by"` - ParentId *int `json:"parentId" gorm:"column:parent_id"` } diff --git a/internal/subtask/repo/repository.go b/internal/subtask/repo/repository.go index 1f0d8e7..9cd0e39 100644 --- a/internal/subtask/repo/repository.go +++ b/internal/subtask/repo/repository.go @@ -37,9 +37,7 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR var insertions []stModel.SubTask var updates []stModel.SubTask for _, subtask := range toBeAdd { - if subtask.ID <= 0 { - // we interpret this as a new subtask - subtask.ID = 0 + if subtask.ID == 0 { insertions = append(insertions, subtask) } else { updates = append(updates, subtask) @@ -53,14 +51,7 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR } if len(updates) > 0 { for _, subtask := range updates { - 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 { + if err := tx.Model(&stModel.SubTask{}).Where("chore_id = ? AND id = ?", choreId, subtask.ID).Updates(subtask).Error; err != nil { return err } } @@ -70,28 +61,12 @@ func (r *SubTasksRepository) UpdateSubtask(c context.Context, choreId int, toBeR return nil }) } + func (r *SubTasksRepository) DeleteSubtask(c context.Context, tx *gorm.DB, subtaskID int) error { if tx != nil { - return r.deleteSubtaskWithChildren(c, tx, subtaskID) + return tx.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 + return r.db.WithContext(c).Delete(&stModel.SubTask{}, subtaskID).Error } func (r *SubTasksRepository) UpdateSubTaskStatus(c context.Context, userID int, subtaskID int, completedAt *time.Time) error { diff --git a/main.go b/main.go index f2c6c71..bc1048b 100644 --- a/main.go +++ b/main.go @@ -31,7 +31,6 @@ import ( notifier "donetick.com/core/internal/notifier" nRepo "donetick.com/core/internal/notifier/repo" nps "donetick.com/core/internal/notifier/service" - discord "donetick.com/core/internal/notifier/service/discord" "donetick.com/core/internal/notifier/service/pushover" telegram "donetick.com/core/internal/notifier/service/telegram" pRepo "donetick.com/core/internal/points/repo" @@ -74,7 +73,6 @@ func main() { // add notifier fx.Provide(pushover.NewPushover), fx.Provide(telegram.NewTelegramNotifier), - fx.Provide(discord.NewDiscordNotifier), fx.Provide(notifier.NewNotifier), fx.Provide(events.NewEventsProducer), diff --git a/migrations/20250314_fix_notification_metadata_experiment_modal.go b/migrations/20250314_fix_notification_metadata_experiment_modal.go deleted file mode 100644 index 3d3618c..0000000 --- a/migrations/20250314_fix_notification_metadata_experiment_modal.go +++ /dev/null @@ -1,43 +0,0 @@ -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{}) -}