eintopf.info@v0.13.16/service/ical/ical.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 ical 17 18 import ( 19 "context" 20 "fmt" 21 "path" 22 "strings" 23 "time" 24 25 ics "github.com/arran4/golang-ical" 26 27 "eintopf.info/service/event" 28 "eintopf.info/service/eventsearch" 29 ) 30 31 type Service interface { 32 Feed(ctx context.Context, opts eventsearch.Options) (string, error) 33 GroupFeed(ctx context.Context, groupID string) (string, error) 34 PlaceFeed(ctx context.Context, placeID string) (string, error) 35 EventIcal(ctx context.Context, eventID string) (string, error) 36 } 37 38 type ServiceImpl struct { 39 eventSearchService eventsearch.Service 40 41 baseURL string 42 } 43 44 func NewService(eventSearchService eventsearch.Service, baseURL string) *ServiceImpl { 45 return &ServiceImpl{ 46 eventSearchService: eventSearchService, 47 48 baseURL: baseURL, 49 } 50 } 51 52 func (s *ServiceImpl) Feed(ctx context.Context, opts eventsearch.Options) (string, error) { 53 if opts.Sort == "" { 54 opts.Sort = "start" 55 } 56 if opts.Filters == nil { 57 opts.Filters = []eventsearch.Filter{eventsearch.ListedFilter{Listed: true}} 58 } else { 59 foundListedFilter := false 60 for _, f := range opts.Filters { 61 if _, ok := f.(eventsearch.ListedFilter); ok { 62 foundListedFilter = true 63 break 64 } 65 } 66 if !foundListedFilter { 67 opts.Filters = append(opts.Filters, eventsearch.ListedFilter{Listed: true}) 68 } 69 } 70 // Do not include every day of a multiday event. 71 opts.Filters = append(opts.Filters, eventsearch.MultidayRangeFilter{ 72 MultidayMin: intptr(-1), 73 MultidayMax: intptr(2), 74 }) 75 result, err := s.eventSearchService.Search(ctx, opts) 76 if err != nil { 77 return "", err 78 } 79 80 return s.icsFromEventDocuments(ctx, result.Events) 81 } 82 83 func (s *ServiceImpl) GroupFeed(ctx context.Context, groupID string) (string, error) { 84 opts := eventsearch.Options{ 85 Filters: []eventsearch.Filter{ 86 eventsearch.GroupIDFilter{GroupID: groupID}, 87 eventsearch.DateRangeFilter{DateMin: time.Now()}, 88 }, 89 } 90 return s.Feed(ctx, opts) 91 } 92 93 func (s *ServiceImpl) PlaceFeed(ctx context.Context, placeID string) (string, error) { 94 opts := eventsearch.Options{ 95 Filters: []eventsearch.Filter{ 96 eventsearch.PlaceIDFilter{PlaceID: placeID}, 97 eventsearch.DateRangeFilter{DateMin: time.Now()}, 98 }, 99 } 100 return s.Feed(ctx, opts) 101 } 102 103 func (s *ServiceImpl) EventIcal(ctx context.Context, eventID string) (string, error) { 104 opts := eventsearch.Options{ 105 Filters: []eventsearch.Filter{eventsearch.IDFilter{ID: eventID}}, 106 } 107 return s.Feed(ctx, opts) 108 } 109 110 func (s *ServiceImpl) icsFromEventDocuments(ctx context.Context, events []*eventsearch.EventDocument) (string, error) { 111 cal := ics.NewCalendar() 112 cal.SetProductId("eintopf.info") 113 cal.SetMethod(ics.MethodRequest) 114 115 for _, e := range events { 116 calEvent := cal.AddEvent(e.ID) 117 calEvent.SetURL(path.Join(s.baseURL, "event", e.ID)) 118 calEvent.SetSummary(e.Name) 119 calEvent.SetStartAt(e.Start) 120 eventURL, err := event.URLFromID(s.baseURL, e.ID) 121 if err != nil { 122 return "", fmt.Errorf("event url: %s", err) 123 } 124 calEvent.SetURL(eventURL) 125 if e.End != nil { 126 calEvent.SetEndAt(*e.End) 127 } 128 calEvent.SetDescription(e.Description) 129 if e.Location != nil { 130 if e.Location.Name != "" { 131 calEvent.SetLocation(e.Location.Name) 132 } 133 if e.Location.Place != nil { 134 place := e.Location.Place 135 if place.Name != "" && place.Address != "" { 136 calEvent.SetLocation(fmt.Sprintf("%s, %s", place.Name, place.Address)) 137 } else if place.Address != "" { 138 calEvent.SetLocation(place.Address) 139 } else if place.Name != "" { 140 calEvent.SetLocation(place.Name) 141 } 142 143 if place.Lat != 0 && place.Lng != 0 { 144 calEvent.SetGeo(place.Lat, place.Lng) 145 } 146 } 147 } 148 149 organizers := []string{} 150 for _, organizer := range e.Organizers { 151 if organizer.Name != "" { 152 organizers = append(organizers, organizer.Name) 153 } 154 if organizer.Group != nil { 155 organizers = append(organizers, organizer.Group.Name) 156 } 157 } 158 if len(organizers) > 0 { 159 calEvent.SetOrganizer(strings.Join(organizers, ",")) 160 } 161 } 162 163 return cal.Serialize(), nil 164 } 165 166 func intptr(i int) *int { return &i }