package application import ( "context" "fmt" "io" "sort" "time" "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification" "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting" "git.coopgo.io/coopgo-apps/parcoursmob/services" filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage" agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi" agendastorage "git.coopgo.io/coopgo-platform/agenda/storage" groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi" "git.coopgo.io/coopgo-platform/groups-management/storage" mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi" ics "github.com/arran4/golang-ical" "github.com/google/uuid" "github.com/rs/zerolog/log" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) type AgendaEventsResult struct { Events []agendastorage.Event Groups map[string]any } func (h *ApplicationHandler) GetAgendaEvents(ctx context.Context, minDate, maxDate *time.Time) (*AgendaEventsResult, error) { request := &agenda.GetEventsRequest{ Namespaces: []string{"parcoursmob_dispositifs"}, } if minDate != nil { request.Mindate = timestamppb.New(*minDate) } if maxDate != nil { request.Maxdate = timestamppb.New(*maxDate) } resp, err := h.services.GRPC.Agenda.GetEvents(ctx, request) if err != nil { return nil, err } responses := []agendastorage.Event{} groupids := []string{} for _, e := range resp.Events { groupids = append(groupids, e.Owners...) responses = append(responses, e.ToStorageType()) } sort.Sort(sorting.EventsByStartdate(responses)) groups := map[string]any{} if len(groupids) > 0 { groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, &groupsmanagement.GetGroupsBatchRequest{ Groupids: groupids, }) if err == nil { for _, g := range groupsresp.Groups { groups[g.Id] = g.ToStorageType() } } } return &AgendaEventsResult{ Events: responses, Groups: groups, }, nil } func (h *ApplicationHandler) CreateAgendaEvent(ctx context.Context, name, eventType, description string, address any, allday bool, startdate, enddate *time.Time, starttime, endtime string, maxSubscribers int, file io.Reader, filename string, fileSize int64, documentType, documentName string) (string, error) { // Get current group g := ctx.Value(identification.GroupKey) if g == nil { return "", fmt.Errorf("no group found in context") } group := g.(storage.Group) data, _ := structpb.NewStruct(map[string]any{ "address": address, }) request := &agenda.CreateEventRequest{ Event: &agenda.Event{ Namespace: "parcoursmob_dispositifs", Owners: []string{group.ID}, Type: eventType, Name: name, Description: description, Startdate: timestamppb.New(*startdate), Enddate: timestamppb.New(*enddate), Starttime: starttime, Endtime: endtime, Allday: allday, MaxSubscribers: int64(maxSubscribers), Data: data, Deleted: false, }, } resp, err := h.services.GRPC.Agenda.CreateEvent(ctx, request) if err != nil { return "", err } // Handle file upload if provided if file != nil && filename != "" { fileid := uuid.NewString() metadata := map[string]string{ "file_type": documentType, "file_name": documentName, } if err := h.filestorage.Put(file, filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s_%s", resp.Event.Id, fileid, filename), fileSize, metadata); err != nil { return "", err } } return resp.Event.Id, nil } type AgendaEventResult struct { Event agendastorage.Event Group storage.Group Documents []filestorage.FileInfo Subscribers map[string]any Accounts []any } func (h *ApplicationHandler) GetAgendaEvent(ctx context.Context, eventID string) (*AgendaEventResult, error) { request := &agenda.GetEventRequest{ Id: eventID, } resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request) if err != nil { return nil, err } grouprequest := &groupsmanagement.GetGroupRequest{ Id: resp.Event.Owners[0], } groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest) if err != nil { return nil, err } subscribers := map[string]any{} accids := []string{} for _, v := range resp.Event.Subscriptions { accids = append(accids, v.Subscriber) } if len(accids) > 0 { subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch( ctx, &mobilityaccounts.GetAccountsBatchRequest{ Accountids: accids, }, ) if err == nil { for _, sub := range subscriberresp.Accounts { subscribers[sub.Id] = sub.ToStorageType() } } } g := ctx.Value(identification.GroupKey) if g == nil { return nil, fmt.Errorf("no group found in context") } group := g.(storage.Group) accountids := []string{} for _, m := range group.Members { if !contains(resp.Event.Subscriptions, m) { accountids = append(accountids, m) } } accounts := []any{} if len(accountids) > 0 { accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch( ctx, &mobilityaccounts.GetAccountsBatchRequest{ Accountids: accountids, }, ) if err == nil { for _, acc := range accountresp.Accounts { accounts = append(accounts, acc) } } } documents := h.filestorage.List(filestorage.PREFIX_AGENDA + "/" + eventID) return &AgendaEventResult{ Event: resp.Event.ToStorageType(), Group: groupresp.Group.ToStorageType(), Documents: documents, Subscribers: subscribers, Accounts: accounts, }, nil } func (h *ApplicationHandler) SubscribeToAgendaEvent(ctx context.Context, eventID, subscriber string, subscriptionData map[string]any) error { datapb, err := structpb.NewStruct(subscriptionData) if err != nil { return err } request := &agenda.SubscribeEventRequest{ Eventid: eventID, Subscriber: subscriber, Data: datapb, } _, err = h.services.GRPC.Agenda.SubscribeEvent(ctx, request) return err } func (h *ApplicationHandler) UnsubscribeFromAgendaEvent(ctx context.Context, eventID, subscribeID, motif, currentUserID, currentUserDisplayName, currentUserEmail, currentGroupID, currentGroupName string) error { // Get the event first request := &agenda.GetEventRequest{ Id: eventID, } resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request) if err != nil { return err } // Find subscription data for the subscriber being removed var s_b_id, s_b_name, s_b_email, s_b_group_id, s_b_group_name string for i := range resp.Event.Subscriptions { if resp.Event.Subscriptions[i].Subscriber == subscribeID { s_b_id = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["id"].GetStringValue() s_b_name = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["display_name"].GetStringValue() s_b_email = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["email"].GetStringValue() s_b_group_id = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["id"].GetStringValue() s_b_group_name = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["name"].GetStringValue() break } } data := map[string]any{ "subscribed_by": map[string]any{ "user": map[string]any{ "id": s_b_id, "display_name": s_b_name, "email": s_b_email, }, "group": map[string]any{ "id": s_b_group_id, "name": s_b_group_name, }, }, "unsubscribed_by": map[string]any{ "user": map[string]any{ "id": currentUserID, "display_name": currentUserDisplayName, "email": currentUserEmail, }, "group": map[string]any{ "id": currentGroupID, "name": currentGroupName, }, }, "motif": motif, } datapb, err := structpb.NewStruct(data) if err != nil { return err } deleteRequest := &agenda.DeleteSubscriptionRequest{ Subscriber: subscribeID, Eventid: eventID, Data: datapb, } _, err = h.services.GRPC.Agenda.DeleteSubscription(ctx, deleteRequest) if err != nil { return err } // Send email notification emailData := map[string]any{ "motif": motif, "user": currentUserDisplayName, "subscriber": fmt.Sprintf("http://localhost:9000/app/beneficiaries/%s", subscribeID), "link": fmt.Sprintf("http://localhost:9000/app/agenda/%s", eventID), } if err := h.emailing.Send("delete_subscriber.request", s_b_email, emailData); err != nil { log.Error().Err(err).Msg("Cannot send email") // Don't return error for email failure } return nil } type AgendaEventHistoryResult struct { Event agendastorage.Event Group storage.Group Subscribers map[string]any Accounts []any } func (h *ApplicationHandler) GetAgendaEventHistory(ctx context.Context, eventID string) (*AgendaEventHistoryResult, error) { request := &agenda.GetEventRequest{ Id: eventID, } resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request) if err != nil { return nil, err } grouprequest := &groupsmanagement.GetGroupRequest{ Id: resp.Event.Owners[0], } groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest) if err != nil { return nil, err } subscribers := map[string]any{} accids := []string{} for _, v := range resp.Event.DeletedSubscription { accids = append(accids, v.Subscriber) } if len(accids) > 0 { subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch( ctx, &mobilityaccounts.GetAccountsBatchRequest{ Accountids: accids, }, ) if err == nil { for _, sub := range subscriberresp.Accounts { subscribers[sub.Id] = sub.ToStorageType() } } } g := ctx.Value(identification.GroupKey) if g == nil { return nil, fmt.Errorf("no group found in context") } group := g.(storage.Group) accountids := []string{} for _, m := range group.Members { if !contains(resp.Event.DeletedSubscription, m) { accountids = append(accountids, m) } } accounts := []any{} if len(accountids) > 0 { accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch( ctx, &mobilityaccounts.GetAccountsBatchRequest{ Accountids: accountids, }, ) if err == nil { for _, acc := range accountresp.Accounts { accounts = append(accounts, acc) } } } return &AgendaEventHistoryResult{ Event: resp.Event.ToStorageType(), Group: groupresp.Group.ToStorageType(), Subscribers: subscribers, Accounts: accounts, }, nil } func (h *ApplicationHandler) AddEventDocument(ctx context.Context, eventID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error { fileid := uuid.NewString() metadata := map[string]string{ "type": documentType, "name": documentName, } if err := h.filestorage.Put(file, filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s_%s", eventID, fileid, filename), fileSize, metadata); err != nil { return err } return nil } func (h *ApplicationHandler) GetEventDocument(ctx context.Context, eventID, document string) (io.Reader, *filestorage.FileInfo, error) { file, info, err := h.filestorage.Get(filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s", eventID, document)) if err != nil { return nil, nil, err } return file, info, nil } func contains(s []*agenda.Subscription, e string) bool { for _, a := range s { if a.Subscriber == e { return true } } return false } func (h *ApplicationHandler) UpdateAgendaEvent(ctx context.Context, eventID, name, eventType, description string, address any, allday bool, startdate, enddate *time.Time, starttime, endtime string, maxSubscribers int) (string, error) { // Get current group g := ctx.Value(identification.GroupKey) if g == nil { return "", fmt.Errorf("no group found in context") } group := g.(storage.Group) // Get existing event first getRequest := &agenda.GetEventRequest{ Id: eventID, } resp, err := h.services.GRPC.Agenda.GetEvent(ctx, getRequest) if err != nil { return "", err } data, _ := structpb.NewStruct(map[string]any{ "address": address, }) request := &agenda.UpdateEventRequest{ Event: &agenda.Event{ Namespace: "parcoursmob_dispositifs", Id: eventID, Owners: []string{group.ID}, Type: eventType, Name: name, Description: description, Startdate: timestamppb.New(*startdate), Enddate: timestamppb.New(*enddate), Starttime: starttime, Endtime: endtime, Allday: allday, MaxSubscribers: int64(maxSubscribers), Data: data, Subscriptions: resp.Event.Subscriptions, }, } updateResp, err := h.services.GRPC.Agenda.UpdateEvent(ctx, request) if err != nil { return "", err } return updateResp.Event.Id, nil } func (h *ApplicationHandler) DeleteAgendaEvent(ctx context.Context, eventID string) error { request := &agenda.GetEventRequest{ Id: eventID, } resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request) if err != nil { return err } updateRequest := &agenda.UpdateEventRequest{ Event: &agenda.Event{ Namespace: resp.Event.Namespace, Id: resp.Event.Id, Owners: resp.Event.Owners, Type: resp.Event.Type, Name: resp.Event.Name, Description: resp.Event.Description, Startdate: resp.Event.Startdate, Enddate: resp.Event.Enddate, Starttime: resp.Event.Starttime, Endtime: resp.Event.Endtime, Allday: resp.Event.Allday, MaxSubscribers: int64(resp.Event.MaxSubscribers), Data: resp.Event.Data, Subscriptions: resp.Event.Subscriptions, Deleted: true, }, } _, err = h.services.GRPC.Agenda.UpdateEvent(ctx, updateRequest) return err } type CalendarResult struct { CalendarData string } func (h *ApplicationHandler) GenerateGlobalCalendar(ctx context.Context) (*CalendarResult, error) { events, err := h.services.GetAgendaEvents() if err != nil { log.Error().Err(err).Msg("error retrieving agenda events") return nil, err } calendar, err := h.icsCalendar(events) if err != nil { return nil, err } return &CalendarResult{ CalendarData: calendar.Serialize(), }, nil } func (h *ApplicationHandler) GenerateOrganizationCalendar(ctx context.Context, groupID string) (*CalendarResult, error) { events, err := h.services.GetAgendaEvents() if err != nil { log.Error().Err(err).Msg("error retrieving agenda events") return nil, err } filteredEvents := []services.AgendaEvent{} for _, e := range events { for _, g := range e.Owners { if g == groupID { filteredEvents = append(filteredEvents, e) break } } } calendar, err := h.icsCalendar(filteredEvents) if err != nil { return nil, err } return &CalendarResult{ CalendarData: calendar.Serialize(), }, nil } func (h *ApplicationHandler) 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 } vevent.SetStartAt(e.Startdate.In(timeloc)) vevent.SetEndAt(e.Enddate.In(timeloc)) } calendar.AddVEvent(vevent) } return calendar, nil }