From dd30d7959b482e868af3ad08b221031c9191b449 Mon Sep 17 00:00:00 2001 From: Arnaud Delcasse Date: Thu, 21 Nov 2024 00:54:52 +0100 Subject: [PATCH] Add ICS calendars (global and organizations) --- go.mod | 3 +- go.sum | 9 +-- handlers/api/calendars.go | 121 +++++++++++++++++++++++++++++++++++ handlers/api/geo.go | 6 +- main.go | 12 ++-- services/agenda.go | 42 ++++++++++++ services/groupsmanagement.go | 8 ++- 7 files changed, 185 insertions(+), 16 deletions(-) create mode 100644 handlers/api/calendars.go diff --git a/go.mod b/go.mod index b07b05f..bd6560b 100755 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( git.coopgo.io/coopgo-platform/fleets v0.0.0-20230310144446-feb935f8bf4e git.coopgo.io/coopgo-platform/groups-management v0.0.0-20230310123255-5ef94ee0746c git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20230329105908-a76c0412a386 + github.com/arran4/golang-ical v0.3.1 github.com/coreos/go-oidc/v3 v3.11.0 github.com/gorilla/securecookie v1.1.1 github.com/minio/minio-go/v7 v7.0.43 @@ -114,9 +115,7 @@ require ( google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect - gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f07ccaa..aa0389d 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:o github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/arran4/golang-ical v0.3.1 h1:v13B3eQZ9VDHTAvT6M11vVzxYgcYmjyPBE2eAZl3VZk= +github.com/arran4/golang-ical v0.3.1/go.mod h1:LZWxF8ZIu/sjBVUCV0udiVPrQAgq3V0aa0RfbO99Qkk= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -320,6 +322,7 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6f github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= @@ -865,18 +868,15 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= -gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= -gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -890,6 +890,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/handlers/api/calendars.go b/handlers/api/calendars.go new file mode 100644 index 0000000..4e9367d --- /dev/null +++ b/handlers/api/calendars.go @@ -0,0 +1,121 @@ +package api + +import ( + "net/http" + "time" + + "git.coopgo.io/coopgo-apps/parcoursmob/services" + ics "github.com/arran4/golang-ical" + "github.com/gorilla/mux" + "github.com/rs/zerolog/log" +) + +func (h *APIHandler) icsCalendar(events []services.AgendaEvent) (*ics.Calendar, error) { + calendar := ics.NewCalendarFor(h.config.GetString("service_name")) + + for _, e := range events { + vevent := ics.NewEvent(e.ID) + vevent.SetSummary(e.Name) + vevent.SetDescription(e.Description) + if e.Allday { + vevent.SetAllDayStartAt(e.Startdate) + if e.Enddate.After(e.Startdate) { + vevent.SetAllDayEndAt(e.Enddate.Add(24 * time.Hour)) + } + } else { + timeloc, err := time.LoadLocation("Europe/Paris") + if err != nil { + log.Error().Err(err).Msg("Tried to load timezone location Europe/Paris. Error. Missing zones in container ?") + return nil, err + } + + starttime, err := time.ParseInLocation("15:04", e.Starttime, timeloc) + if err != nil { + return nil, err + } + startdatetime := time.Date(e.Startdate.Year(), e.Startdate.Month(), e.Startdate.Day(), starttime.Hour(), starttime.Minute(), 0, 0, timeloc) + + endtime, err := time.Parse("15:04", e.Endtime) + if err != nil { + return nil, err + } + enddatetime := time.Date(e.Enddate.Year(), e.Enddate.Month(), e.Enddate.Day(), endtime.Hour(), endtime.Minute(), 0, 0, timeloc) + + vevent.SetStartAt(startdatetime) + vevent.SetEndAt(enddatetime) + } + calendar.AddVEvent(vevent) + } + return calendar, nil +} + +func (h *APIHandler) CalendarGlobal(w http.ResponseWriter, r *http.Request) { + enabled := h.config.GetBool("modules.agenda.enabled") && h.config.GetBool("modules.agenda.calendars.global.enabled") + if !enabled { + log.Error().Msg("global calendar not activated in configuration") + w.WriteHeader(http.StatusForbidden) + return + } + + events, err := h.services.GetAgendaEvents() + if err != nil { + log.Error().Err(err).Msg("error retrieving agenda events") + w.WriteHeader(http.StatusInternalServerError) + return + } + + calendar, err := h.icsCalendar(events) + if err != nil { + log.Error().Err(err).Msg("error while creating ics calendar") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/calendar") + w.Write([]byte(calendar.Serialize())) +} + +func (h *APIHandler) CalendarOrganizations(w http.ResponseWriter, r *http.Request) { + enabled := h.config.GetBool("modules.agenda.enabled") && h.config.GetBool("modules.agenda.calendars.organizations.enabled") + if !enabled { + log.Error().Msg("organizations calendar not activated in configuration") + w.WriteHeader(http.StatusForbidden) + return + } + + // TODO set additional calendar rights in group configuration to prevent default behavior ? + + vars := mux.Vars(r) + groupid := vars["groupid"] + + events, err := h.services.GetAgendaEvents() + if err != nil { + log.Error().Err(err).Msg("error retrieving agenda events") + w.WriteHeader(http.StatusInternalServerError) + return + } + + filteredEvents := []services.AgendaEvent{} + + for _, e := range events { + for _, g := range e.Owners { + log.Debug().Str("groupid", groupid).Str("g", g).Msg("check identical") + if g == groupid { + filteredEvents = append(filteredEvents, e) + continue + } + } + } + + calendar, err := h.icsCalendar(filteredEvents) + if err != nil { + log.Error().Err(err).Msg("error while creating ics calendar") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/calendar") + w.Write([]byte(calendar.Serialize())) +} diff --git a/handlers/api/geo.go b/handlers/api/geo.go index ccdeca2..34f2297 100755 --- a/handlers/api/geo.go +++ b/handlers/api/geo.go @@ -3,13 +3,12 @@ package api import ( "encoding/json" "fmt" - "io/ioutil" + "io" "log" "net/http" ) func (h *APIHandler) GeoAutocomplete(w http.ResponseWriter, r *http.Request) { - pelias := h.config.GetString("geo.pelias.url") t, ok := r.URL.Query()["text"] @@ -29,8 +28,7 @@ func (h *APIHandler) GeoAutocomplete(w http.ResponseWriter, r *http.Request) { defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } diff --git a/main.go b/main.go index 401735b..f1b8dc8 100755 --- a/main.go +++ b/main.go @@ -20,10 +20,9 @@ import ( ) func main() { - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + // zerolog.TimeFieldFormat = zerolog.TimeFormatUnix cfg, err := ReadConfig() - if err != nil { panic(err) } @@ -81,6 +80,10 @@ func main() { r.Use(trackPage) } + calendars_router := r.PathPrefix("/api/calendars").Subrouter() + calendars_router.HandleFunc("/global.ics", apiHandler.CalendarGlobal) + calendars_router.HandleFunc("/organizations/{groupid}.ics", apiHandler.CalendarOrganizations) + api_router := r.PathPrefix("/api").Subrouter() api_router.HandleFunc("/", apiHandler.NotFound) api_router.HandleFunc("/geo/autocomplete", apiHandler.GeoAutocomplete) @@ -150,7 +153,7 @@ func main() { appGroup.HandleFunc("/groups", applicationHandler.CreateGroupModule) appGroup.HandleFunc("/groups/{groupid}", applicationHandler.DisplayGroupModule) - //TODO Subrouters with middlewares checking security for each module ? + // TODO Subrouters with middlewares checking security for each module ? application.Use(idp.Middleware) application.Use(idp.GroupsMiddleware) @@ -160,7 +163,7 @@ func main() { appAdmin.HandleFunc("/groups/{groupid}", applicationHandler.AdministrationGroupDisplay) appAdmin.HandleFunc("/groups/{groupid}/invite-admin", applicationHandler.AdministrationGroupInviteAdmin) appAdmin.HandleFunc("/groups/{groupid}/invite-member", applicationHandler.AdministrationGroupInviteMember) - //add statistiques + // add statistiques appAdmin.HandleFunc("/stats/vehicles", applicationHandler.AdminStatVehicles) appAdmin.HandleFunc("/stats/bookings", applicationHandler.AdminStatBookings) appAdmin.HandleFunc("/stats/beneficaires", applicationHandler.AdminStatBeneficaires) @@ -192,7 +195,6 @@ func main() { log.Info().Str("service_name", service_name).Str("address", address).Msg("Running HTTP server") log.Fatal().Err(srv.ListenAndServe()) - } func redirectApp(w http.ResponseWriter, r *http.Request) { diff --git a/services/agenda.go b/services/agenda.go index b27e30f..bf9ca40 100755 --- a/services/agenda.go +++ b/services/agenda.go @@ -1,8 +1,14 @@ package services import ( + "context" + "fmt" + "time" + agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi" + "git.coopgo.io/coopgo-platform/agenda/storage" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/timestamppb" ) type AgendaService struct { @@ -21,3 +27,39 @@ func NewAgendaService(dial string) (*AgendaService, error) { AgendaClient: client, }, nil } + +func (s *ServicesHandler) GetAgendaEvents() ([]AgendaEvent, error) { + resp, err := s.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{ + Namespaces: []string{"parcoursmob_dispositifs"}, + Mindate: timestamppb.New(time.Now().Add(-24 * time.Hour)), + }) + if err != nil { + return nil, err + } + + groups, err := s.GetGroupsMap() + if err != nil { + return nil, fmt.Errorf("error in groups request : %w", err) + } + + events := []AgendaEvent{} + + for _, e := range resp.Events { + newEvent := AgendaEvent{ + Event: e.ToStorageType(), + } + for _, o := range e.Owners { + newEvent.OwnersGroups = append(newEvent.OwnersGroups, GroupsManagementGroup{Group: groups[o]}) + } + events = append(events, newEvent) + } + + return events, nil +} + +// Enriched types + +type AgendaEvent struct { + OwnersGroups []GroupsManagementGroup + storage.Event +} diff --git a/services/groupsmanagement.go b/services/groupsmanagement.go index 08647b7..d51563e 100755 --- a/services/groupsmanagement.go +++ b/services/groupsmanagement.go @@ -41,7 +41,7 @@ func (s *ServicesHandler) GetGroupsMap() (groups map[string]storage.Group, err e return } -////////////////////////////////optimize the code////////////////////////////////////// +// //////////////////////////////optimize the code////////////////////////////////////// func (s *ServicesHandler) GetGroupsMemberMap(id string) (groups map[string]any, err error) { groups = map[string]any{} @@ -56,3 +56,9 @@ func (s *ServicesHandler) GetGroupsMemberMap(id string) (groups map[string]any, } return } + +// Enriched types + +type GroupsManagementGroup struct { + storage.Group +}