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 }