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 }