diff --git a/backend/app/api/main.go b/backend/app/api/main.go index c92572a..5ea4373 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "net/http" "os" "path/filepath" @@ -168,6 +169,19 @@ func run(cfg *config.Config) error { Msg("failed to purge expired invitations") } }) + go app.startBgTask(time.Duration(1)*time.Hour, func() { + now := time.Now() + + if now.Hour() == 8 { + fmt.Println("run notifiers") + err := app.services.BackgroundService.SendNotifiersToday(context.Background()) + if err != nil { + log.Error(). + Err(err). + Msg("failed to send notifiers") + } + } + }) // TODO: Remove through external API that does setup if cfg.Demo { diff --git a/backend/internal/core/services/all.go b/backend/internal/core/services/all.go index dab59ef..8cbe896 100644 --- a/backend/internal/core/services/all.go +++ b/backend/internal/core/services/all.go @@ -5,9 +5,10 @@ import ( ) type AllServices struct { - User *UserService - Group *GroupService - Items *ItemService + User *UserService + Group *GroupService + Items *ItemService + BackgroundService *BackgroundService } type OptionsFunc func(*options) @@ -42,5 +43,6 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices { repo: repos, autoIncrementAssetID: options.autoIncrementAssetID, }, + BackgroundService: &BackgroundService{repos}, } } diff --git a/backend/internal/core/services/service_background.go b/backend/internal/core/services/service_background.go new file mode 100644 index 0000000..21ae4c3 --- /dev/null +++ b/backend/internal/core/services/service_background.go @@ -0,0 +1,81 @@ +package services + +import ( + "context" + "strings" + "time" + + "github.com/containrrr/shoutrrr" + "github.com/hay-kot/homebox/backend/internal/data/repo" + "github.com/hay-kot/homebox/backend/internal/data/types" + "github.com/rs/zerolog/log" +) + +type BackgroundService struct { + repos *repo.AllRepos +} + +func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error { + // Get All Groups + groups, err := svc.repos.Groups.GetAllGroups(ctx) + if err != nil { + return err + } + + today := types.DateFromTime(time.Now()) + + for i := range groups { + group := groups[i] + + entries, err := svc.repos.MaintEntry.GetScheduled(ctx, group.ID, today) + if err != nil { + return err + } + + if len(entries) == 0 { + log.Debug(). + Str("group_name", group.Name). + Str("group_id", group.ID.String()). + Msg("No scheduled maintenance for today") + continue + } + + notifiers, err := svc.repos.Notifiers.GetByGroup(ctx, group.ID) + if err != nil { + return err + } + + urls := make([]string, len(notifiers)) + for i := range notifiers { + urls[i] = notifiers[i].URL + } + + bldr := strings.Builder{} + + bldr.WriteString("Homebox Maintenance for (") + bldr.WriteString(today.String()) + bldr.WriteString("):\n") + + for i := range entries { + entry := entries[i] + bldr.WriteString(" - ") + bldr.WriteString(entry.Name) + bldr.WriteString("\n") + } + + var sendErrs []error + for i := range urls { + err := shoutrrr.Send(urls[i], bldr.String()) + + if err != nil { + sendErrs = append(sendErrs, err) + } + } + + if len(sendErrs) > 0 { + return sendErrs[0] + } + } + + return nil +} diff --git a/backend/internal/data/ent/schema/maintenance_entry.go b/backend/internal/data/ent/schema/maintenance_entry.go index 1c623cf..52b905b 100644 --- a/backend/internal/data/ent/schema/maintenance_entry.go +++ b/backend/internal/data/ent/schema/maintenance_entry.go @@ -33,6 +33,8 @@ func (MaintenanceEntry) Fields() []ent.Field { Optional(), field.Float("cost"). Default(0.0), + field.Bool("reminders_enabled"). + Default(false), } } diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 2b74071..fab543c 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -16,7 +16,36 @@ import ( ) type GroupRepository struct { - db *ent.Client + db *ent.Client + groupMapper MapFunc[*ent.Group, Group] + invitationMapper MapFunc[*ent.GroupInvitationToken, GroupInvitation] +} + +func NewGroupRepository(db *ent.Client) *GroupRepository { + gmap := func(g *ent.Group) Group { + return Group{ + ID: g.ID, + Name: g.Name, + CreatedAt: g.CreatedAt, + UpdatedAt: g.UpdatedAt, + Currency: strings.ToUpper(g.Currency.String()), + } + } + + imap := func(i *ent.GroupInvitationToken) GroupInvitation { + return GroupInvitation{ + ID: i.ID, + ExpiresAt: i.ExpiresAt, + Uses: i.Uses, + Group: gmap(i.Edges.Group), + } + } + + return &GroupRepository{ + db: db, + groupMapper: gmap, + invitationMapper: imap, + } } type ( @@ -76,27 +105,8 @@ type ( } ) -var mapToGroupErr = mapTErrFunc(mapToGroup) - -func mapToGroup(g *ent.Group) Group { - return Group{ - ID: g.ID, - Name: g.Name, - CreatedAt: g.CreatedAt, - UpdatedAt: g.UpdatedAt, - Currency: strings.ToUpper(g.Currency.String()), - } -} - -var mapToGroupInvitationErr = mapTErrFunc(mapToGroupInvitation) - -func mapToGroupInvitation(g *ent.GroupInvitationToken) GroupInvitation { - return GroupInvitation{ - ID: g.ID, - ExpiresAt: g.ExpiresAt, - Uses: g.Uses, - Group: mapToGroup(g.Edges.Group), - } +func (r *GroupRepository) GetAllGroups(ctx context.Context) ([]Group, error) { + return r.groupMapper.MapEachErr(r.db.Group.Query().All(ctx)) } func (r *GroupRepository) StatsLocationsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) { @@ -249,7 +259,7 @@ func (r *GroupRepository) StatsGroup(ctx context.Context, GID uuid.UUID) (GroupS } func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group, error) { - return mapToGroupErr(r.db.Group.Create(). + return r.groupMapper.MapErr(r.db.Group.Create(). SetName(name). Save(ctx)) } @@ -262,15 +272,15 @@ func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data Gr SetCurrency(currency). Save(ctx) - return mapToGroupErr(entity, err) + return r.groupMapper.MapErr(entity, err) } func (r *GroupRepository) GroupByID(ctx context.Context, id uuid.UUID) (Group, error) { - return mapToGroupErr(r.db.Group.Get(ctx, id)) + return r.groupMapper.MapErr(r.db.Group.Get(ctx, id)) } func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (GroupInvitation, error) { - return mapToGroupInvitationErr(r.db.GroupInvitationToken.Query(). + return r.invitationMapper.MapErr(r.db.GroupInvitationToken.Query(). Where(groupinvitationtoken.Token(token)). WithGroup(). Only(ctx)) diff --git a/backend/internal/data/repo/repo_maintenance_entry.go b/backend/internal/data/repo/repo_maintenance_entry.go index daf7887..b699d09 100644 --- a/backend/internal/data/repo/repo_maintenance_entry.go +++ b/backend/internal/data/repo/repo_maintenance_entry.go @@ -84,6 +84,27 @@ func mapMaintenanceEntry(entry *ent.MaintenanceEntry) MaintenanceEntry { } } +func (r *MaintenanceEntryRepository) GetScheduled(ctx context.Context, GID uuid.UUID, dt types.Date) ([]MaintenanceEntry, error) { + entries, err := r.db.MaintenanceEntry.Query(). + Where( + maintenanceentry.HasItemWith( + item.HasGroupWith(group.ID(GID)), + ), + maintenanceentry.ScheduledDate(dt.Time()), + maintenanceentry.Or( + maintenanceentry.DateIsNil(), + maintenanceentry.DateEQ(time.Time{}), + ), + ). + All(ctx) + + if err != nil { + return nil, err + } + + return mapEachMaintenanceEntry(entries), nil +} + func (r *MaintenanceEntryRepository) Create(ctx context.Context, itemID uuid.UUID, input MaintenanceEntryCreate) (MaintenanceEntry, error) { item, err := r.db.MaintenanceEntry.Create(). SetItemID(itemID). diff --git a/backend/internal/data/repo/repo_notifier.go b/backend/internal/data/repo/repo_notifier.go index c99cad2..2ea27eb 100644 --- a/backend/internal/data/repo/repo_notifier.go +++ b/backend/internal/data/repo/repo_notifier.go @@ -73,6 +73,16 @@ func (r *NotifierRepository) GetByGroup(ctx context.Context, groupID uuid.UUID) Where(notifier.GroupID(groupID)). Order(ent.Asc(notifier.FieldName)). All(ctx) + + return r.mapper.MapEachErr(notifier, err) +} + +func (r *NotifierRepository) GetActiveByGroup(ctx context.Context, groupID uuid.UUID) ([]NotifierOut, error) { + notifier, err := r.db.Notifier.Query(). + Where(notifier.GroupID(groupID), notifier.IsActive(true)). + Order(ent.Asc(notifier.FieldName)). + All(ctx) + return r.mapper.MapEachErr(notifier, err) } diff --git a/backend/internal/data/repo/repos_all.go b/backend/internal/data/repo/repos_all.go index 9a6d9c5..2a3cf27 100644 --- a/backend/internal/data/repo/repos_all.go +++ b/backend/internal/data/repo/repos_all.go @@ -20,7 +20,7 @@ func New(db *ent.Client, root string) *AllRepos { return &AllRepos{ Users: &UserRepository{db}, AuthTokens: &TokenRepository{db}, - Groups: &GroupRepository{db}, + Groups: NewGroupRepository(db), Locations: &LocationRepository{db}, Labels: &LabelRepository{db}, Items: &ItemsRepository{db},