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  }