eintopf.info@v0.13.16/service/eventsearch/index.go (about)

     1  // Copyright (C) 2022 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 eventsearch
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"eintopf.info/internal/crud"
    26  	"eintopf.info/internal/xtime"
    27  	"eintopf.info/service/event"
    28  	"eintopf.info/service/group"
    29  	"eintopf.info/service/place"
    30  	"eintopf.info/service/revent"
    31  	"eintopf.info/service/search"
    32  )
    33  
    34  func (s *ServiceImpl) Index(events ...*event.Event) error {
    35  	docs := []search.Indexable{}
    36  	for _, e := range events {
    37  		docs = append(docs, toIndexables(s.EventDocumentsFromEvent(e))...)
    38  	}
    39  
    40  	return s.searchService.Index(docs...)
    41  }
    42  
    43  func toIndexables(events []*EventDocument) []search.Indexable {
    44  	docs := []search.Indexable{}
    45  	for _, e := range events {
    46  		docs = append(docs, e)
    47  	}
    48  	return docs
    49  }
    50  
    51  func (s *ServiceImpl) EventDocumentsFromEvent(e *event.Event) []*EventDocument {
    52  	if e.End == nil || e.Start.Add(time.Hour*24).After(*e.End) {
    53  		return []*EventDocument{s.newEventDocument(e, e.ID, e.Start, -1, true)}
    54  	}
    55  	docs := []*EventDocument{}
    56  
    57  	docs = append(docs, s.newEventDocument(e, e.ID, time.Time{}, -2, true))
    58  	i := 1
    59  	for d := e.Start; !d.After(*e.End); d = d.Add(time.Hour * 24) {
    60  		docs = append(docs, s.newEventDocument(e, fmt.Sprintf("%s_%d", e.ID, i), d, i, true))
    61  		i++
    62  	}
    63  	return docs
    64  }
    65  
    66  func (s *ServiceImpl) newEventDocument(e *event.Event, indexID string, date time.Time, multiDay int, includeChildren bool) *EventDocument {
    67  	doc := &EventDocument{
    68  		indexID:     indexID,
    69  		ID:          e.ID,
    70  		Name:        e.Name,
    71  		Location2:   e.Location2,
    72  		Description: e.Description,
    73  		Involved:    e.Involved,
    74  		Date:        date,
    75  		MultiDay:    multiDay,
    76  		Start:       e.Start,
    77  		End:         e.End,
    78  		Tags:        e.Tags,
    79  		Image:       e.Image,
    80  		Published:   e.Published,
    81  		Canceled:    e.Canceled,
    82  		Listed:      e.Listable(),
    83  		ownedBy:     e.OwnedBy,
    84  	}
    85  
    86  	if includeChildren {
    87  		parentFilter := fmt.Sprintf("id:%s", e.ID)
    88  		deactivatedFilter := false
    89  		publishedFilter := true
    90  		childEvents, _, err := s.eventService.Find(context.Background(), &crud.FindParams[event.FindFilters]{
    91  			Filters: &event.FindFilters{
    92  				Parent:      &parentFilter,
    93  				Deactivated: &deactivatedFilter,
    94  				Published:   &publishedFilter,
    95  			},
    96  			Sort:  "start",
    97  			Order: crud.OrderAsc,
    98  		})
    99  		if err == nil && len(childEvents) > 0 {
   100  			doc.Children = []*EventDocument{}
   101  			for _, childEvent := range childEvents {
   102  				if multiDay < 0 || (childEvent.Start.After(xtime.StartOfDay(date)) && childEvent.Start.Before(xtime.EndOfDay(date))) {
   103  					doc.Children = append(doc.Children, s.newEventDocument(childEvent, childEvent.ID, childEvent.Start, -1, false))
   104  				}
   105  			}
   106  		}
   107  	}
   108  
   109  	doc.Organizers = []Organizer{}
   110  	for _, organizer := range e.Organizers {
   111  		if strings.HasPrefix(organizer, "id:") {
   112  			groupID := strings.TrimPrefix(organizer, "id:")
   113  			group, err := s.groupService.FindByID(context.Background(), groupID)
   114  			if err == nil && group != nil {
   115  				doc.Organizers = append(doc.Organizers, Organizer{Group: group})
   116  			}
   117  			doc.ownedBy = append(doc.ownedBy, group.OwnedBy...)
   118  		} else {
   119  			doc.Organizers = append(doc.Organizers, Organizer{Name: organizer})
   120  		}
   121  	}
   122  
   123  	if strings.HasPrefix(e.Location, "id:") {
   124  		placeID := strings.TrimPrefix(e.Location, "id:")
   125  		place, err := s.placeService.FindByID(context.Background(), placeID)
   126  		if err == nil && place != nil {
   127  			doc.Location = &Location{Place: place}
   128  		}
   129  	} else if e.Location != "" {
   130  		doc.Location = &Location{Name: e.Location}
   131  	}
   132  
   133  	if repeatingEventID, ok := revent.ParseID(e.Parent); ok {
   134  		repeatingEvent, err := s.repeatingEventService.FindByID(context.Background(), repeatingEventID)
   135  		if err == nil && repeatingEvent != nil {
   136  			doc.RepeatingEvent = repeatingEvent
   137  		}
   138  	}
   139  
   140  	return doc
   141  }
   142  
   143  type EventDocument struct {
   144  	// indexID is the ID with an optional prefix for multiday events:
   145  	//   $id_$day
   146  	// This is needed, to ensure a unique ID for multiday events, since seach
   147  	// day gets its own entry.
   148  	indexID     string
   149  	ID          string           `json:"id"`
   150  	Canceled    bool             `json:"canceled"`
   151  	Listed      bool             `json:"listed"`
   152  	Published   bool             `json:"published"`
   153  	Name        string           `json:"name"`
   154  	Organizers  []Organizer      `json:"organizers"`
   155  	Involved    []event.Involved `json:"involved"`
   156  	Location    *Location        `json:"location"`
   157  	Location2   string           `json:"location2"`
   158  	Description string           `json:"description"`
   159  	Date        time.Time        `json:"day"`
   160  	// MultiDay tracks the day of a multiday event
   161  	//   -2 for the raw multi day event
   162  	//   -1 for single day event
   163  	//   n for the nth day of a multiday event
   164  	MultiDay       int                    `json:"multiday"`
   165  	Start          time.Time              `json:"start"`
   166  	End            *time.Time             `json:"end"`
   167  	Tags           []string               `json:"tags"`
   168  	Image          string                 `json:"image"`
   169  	Children       []*EventDocument       `json:"children,omitempty"`
   170  	RepeatingEvent *revent.RepeatingEvent `json:"repeatingEvent,omitempty"`
   171  	ownedBy        []string
   172  }
   173  
   174  // Organizer can either be a group.Group or a string
   175  type Organizer struct {
   176  	Group *group.Group `json:"group,omitempty"`
   177  	Name  string       `json:"name,omitempty"`
   178  }
   179  
   180  // Location can either be a place.Place or a string
   181  type Location struct {
   182  	Place *place.Place `json:"place,omitempty"`
   183  	Name  string       `json:"name,omitempty"`
   184  }
   185  
   186  // Identifier returns the event id.
   187  func (e *EventDocument) Identifier() string {
   188  	return e.indexID
   189  }
   190  
   191  // Type returns "event" as the event type.
   192  func (e *EventDocument) Type() string {
   193  	return "event"
   194  }
   195  
   196  // QueryText consists of the following fields:
   197  //   - Name
   198  //   - Description
   199  //   - Organizers (Name or Group.Name)
   200  //   - Location (Name or Place.Name)
   201  //   - Tags
   202  //
   203  // The query text is used when making a text search.
   204  func (e *EventDocument) QueryText() string {
   205  	fields := []string{
   206  		e.Name,
   207  		e.Description,
   208  	}
   209  	for _, organizer := range e.Organizers {
   210  		if organizer.Group != nil {
   211  			fields = append(fields, organizer.Group.Name)
   212  		} else {
   213  			fields = append(fields, organizer.Name)
   214  		}
   215  	}
   216  	if e.Location != nil {
   217  		if e.Location.Place != nil {
   218  			fields = append(fields, e.Location.Place.Name)
   219  		} else if e.Location.Name != "" {
   220  			fields = append(fields, e.Location.Name)
   221  		}
   222  	}
   223  	for _, tag := range e.Tags {
   224  		fields = append(fields, tag)
   225  	}
   226  	return strings.Join(fields, " ")
   227  }
   228  
   229  const layoutISO = "2006-01-02"
   230  
   231  // SearchFields returns a map of fields to be index for searching.
   232  func (e *EventDocument) SearchFields() map[string]interface{} {
   233  	tags := []string{}
   234  	for _, tag := range e.Tags {
   235  		tags = append(tags, tag)
   236  	}
   237  	organizers := []string{}
   238  	groupIDs := []string{}
   239  	for _, organizer := range e.Organizers {
   240  		if organizer.Group != nil {
   241  			organizers = append(organizers, organizer.Group.Name)
   242  			groupIDs = append(groupIDs, organizer.Group.ID)
   243  		} else {
   244  			organizers = append(organizers, organizer.Name)
   245  		}
   246  	}
   247  	location := ""
   248  	placeID := ""
   249  	if e.Location != nil {
   250  		location = e.Location.Name
   251  		if e.Location.Place != nil {
   252  			location = e.Location.Place.Name
   253  			placeID = e.Location.Place.ID
   254  		}
   255  	}
   256  	involved := []string{}
   257  	for _, i := range e.Involved {
   258  		data, err := json.Marshal(i)
   259  		if err == nil {
   260  			involved = append(involved, string(data))
   261  		}
   262  	}
   263  
   264  	return map[string]interface{}{
   265  		"is":         append(tags, e.Type()),
   266  		"id":         e.ID,
   267  		"organizers": organizers,
   268  		"groupID":    groupIDs,
   269  		"location":   location,
   270  		"location2":  e.Location2,
   271  		"placeID":    placeID,
   272  		"tag":        tags,
   273  		"name":       e.Name,
   274  		"start":      e.Start,
   275  		"end":        e.End,
   276  		"date":       e.Date,
   277  		"multiday":   e.MultiDay,
   278  		"day":        time.Date(e.Date.Year(), e.Date.Month(), e.Date.Day(), 0, 0, 0, 0, time.UTC),
   279  		"listed":     e.Listed,
   280  		"published":  e.Published,
   281  		"involved":   involved,
   282  		"ownedBy":    e.ownedBy,
   283  		"children":   len(e.Children) > 0,
   284  	}
   285  }
   286  
   287  func tptr(t time.Time) *time.Time {
   288  	return &t
   289  }
   290  
   291  func sameDay(t1, t2 time.Time) bool {
   292  	return t1.Year() == t2.Year() && t1.Month() == t2.Month() && t1.Day() == t2.Day()
   293  }