eintopf.info@v0.13.16/service/revent/notify_expired.go (about) 1 // Copyright (C) 2023 The Eintopf authors 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <https://www.gnu.org/licenses/>. 15 16 package revent 17 18 import ( 19 "context" 20 "fmt" 21 "time" 22 23 "eintopf.info/internal/crud" 24 "eintopf.info/service/dbmigration" 25 "eintopf.info/service/event" 26 "eintopf.info/service/notification" 27 "eintopf.info/service/oqueue" 28 "github.com/jmoiron/sqlx" 29 ) 30 31 type ExpiredState struct { 32 RepeatingEventID string `db:"repeating_event_id"` 33 } 34 35 func (e ExpiredState) Identifier() string { return e.RepeatingEventID } 36 37 type ExpiredStateFilter struct{} 38 39 type ExpiredStorer = crud.Storer[ExpiredState, ExpiredState, ExpiredStateFilter] 40 41 func newExpiredState(state *ExpiredState, id string) *ExpiredState { return state } 42 43 func NewExpiredMemoryStore() *crud.MemoryStore[ExpiredState, ExpiredState, ExpiredStateFilter] { 44 return crud.NewMemoryStore[ExpiredState, ExpiredState, ExpiredStateFilter](newExpiredState, nil) 45 } 46 47 func NewExpiredStateSqlStore(db *sqlx.DB, migrationService dbmigration.Service) (*ExpiredStateSqlStore, error) { 48 store := &ExpiredStateSqlStore{ 49 db, 50 migrationService, 51 crud.NewSqlStore(db, table, newExpiredState, nil), 52 } 53 if err := store.runMigrations(context.Background()); err != nil { 54 return nil, err 55 } 56 return store, nil 57 } 58 59 type ExpiredStateSqlStore struct { 60 db *sqlx.DB 61 migrationService dbmigration.Service 62 63 *crud.SqlStore[ExpiredState, ExpiredState, ExpiredStateFilter] 64 } 65 66 func (s *ExpiredStateSqlStore) runMigrations(ctx context.Context) error { 67 return s.migrationService.RunMigrations(ctx, []dbmigration.Migration{ 68 dbmigration.NewMigration("createActionTable3", s.createActionTable, nil), 69 }) 70 } 71 72 func (s *ExpiredStateSqlStore) createActionTable(ctx context.Context) error { 73 _, err := s.db.ExecContext(ctx, ` 74 CREATE TABLE IF NOT EXISTS repeating_event_expired_notification ( 75 repeating_event_id VARCHAR(64) NOT NULL PRIMARY KEY UNIQUE 76 ); 77 `) 78 return err 79 } 80 81 var table = crud.SqlTable[ExpiredStateFilter]{ 82 IDField: "repeating_event_id", 83 Table: "repeating_event_expired_notification", 84 Fields: []string{"repeating_event_id"}, 85 } 86 87 type ExpiredNotifier struct { 88 expiredStore ExpiredStorer 89 reventStore Storer 90 eventStore event.Storer 91 notifier notification.Notifier 92 } 93 94 func NewExpiredNotifier(expiredStore ExpiredStorer, reventStore Storer, eventStore event.Storer, notifier notification.Notifier) *ExpiredNotifier { 95 return &ExpiredNotifier{ 96 expiredStore: expiredStore, 97 reventStore: reventStore, 98 eventStore: eventStore, 99 notifier: notifier, 100 } 101 } 102 103 func (e *ExpiredNotifier) ConsumeOperation(op oqueue.Operation) error { 104 switch op := op.(type) { 105 case DeleteOperation: 106 _ = e.expiredStore.Delete(context.Background(), op.ID) 107 case event.CreateOperation: 108 repeatingEventID, ok := ParseID(op.Event.Parent) 109 if !ok { 110 return nil 111 } 112 _ = e.expiredStore.Delete(context.Background(), repeatingEventID) 113 } 114 return nil 115 } 116 117 func (e *ExpiredNotifier) NotifyAction(ctx context.Context) error { 118 deactivatedFilter := false 119 revents, _, err := e.reventStore.Find(ctx, &crud.FindParams[FindFilters]{ 120 Filters: &FindFilters{ 121 Deactivated: &deactivatedFilter, 122 }, 123 }) 124 if err != nil { 125 return err 126 } 127 128 for _, repeatingEvent := range revents { 129 publishedFilter := true 130 parentFilter := ID(repeatingEvent.ID) 131 events, _, err := e.eventStore.Find(ctx, &crud.FindParams[event.FindFilters]{ 132 Sort: "start", 133 Order: crud.OrderDesc, 134 Limit: 1, 135 Filters: &event.FindFilters{ 136 Deactivated: &deactivatedFilter, 137 Published: &publishedFilter, 138 Parent: &parentFilter, 139 }, 140 }) 141 if err != nil { 142 return err 143 } 144 if len(events) != 1 { 145 // No events were generated for this repeating event 146 continue 147 } 148 149 // Check if the last event is less than one day from now. 150 if events[0].Start.After(time.Now().AddDate(0, 0, 1)) { 151 continue 152 } 153 154 state, err := e.expiredStore.FindByID(ctx, repeatingEvent.ID) 155 if err != nil { 156 return err 157 } 158 if state != nil { 159 // The notification was already send for this repeating event. 160 continue 161 } 162 163 for _, userID := range repeatingEvent.OwnedBy { 164 // TODO: Check if user wants this notification 165 err := e.notifier.Notify( 166 ctx, 167 userID, 168 "Regelmäßige Veranstaltung abgelaufen", 169 fmt.Sprintf("Für die regelmäßige Veranstaltung '%s' wurden keine neuen Veranstaltungen mehr erstellt. Die letzte Veranstaltung war am %s", repeatingEvent.Name, events[0].Start.Format("02.01.2006")), 170 notification.Link{Title: "Zur Regelmäßigen Veranstaltung", URL: fmt.Sprintf("/backstage/repeatingevents/%s#generate", repeatingEvent.ID)}, 171 ) 172 if err != nil { 173 return err 174 } 175 } 176 177 _, err = e.expiredStore.Create(ctx, &ExpiredState{RepeatingEventID: repeatingEvent.ID}) 178 if err != nil { 179 return err 180 } 181 } 182 183 return nil 184 }