ultisuite-backend/internal/api/calendar/service.go
R3D347HR4Y 1fda9e7bac
Some checks failed
CI / Go tests (push) Has been cancelled
CI / Integration tests (push) Has been cancelled
CI / DB migrations (push) Has been cancelled
Magnifique
2026-06-11 10:11:03 +02:00

186 lines
5.7 KiB
Go

package calendar
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/ultisuite/ulti-backend/internal/api/paginate"
"github.com/ultisuite/ulti-backend/internal/api/query"
"github.com/ultisuite/ulti-backend/internal/auth"
meetpkg "github.com/ultisuite/ulti-backend/internal/meet"
"github.com/ultisuite/ulti-backend/internal/nextcloud"
)
type Service struct {
nc *nextcloud.Client
meetCfg *meetpkg.Config
}
var ErrMeetDisabled = errors.New("meet is disabled")
func NewService(nc *nextcloud.Client, meetCfg *meetpkg.Config) *Service {
return &Service{nc: nc, meetCfg: meetCfg}
}
func (s *Service) EnsureNextcloudUser(ctx context.Context, claims *auth.Claims) (string, error) {
if s.nc == nil {
return "", fmt.Errorf("nextcloud unavailable")
}
return s.nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name)
}
// ReprovisionPrincipal drops cached CalDAV credentials and provisions a fresh app password.
func (s *Service) ReprovisionPrincipal(ctx context.Context, claims *auth.Claims) (string, error) {
if s.nc == nil {
return "", fmt.Errorf("nextcloud unavailable")
}
userID := nextcloud.UserIDFromClaims(claims.Email, claims.Sub)
if err := s.nc.InvalidatePrincipalCredentials(ctx, userID); err != nil {
return "", err
}
return s.nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name)
}
func calendarPath(userID, calID string) string {
return "/remote.php/dav/calendars/" + userID + "/" + calID + "/"
}
func (s *Service) ListCalendars(ctx context.Context, userID string) ([]nextcloud.Calendar, error) {
return s.nc.ListCalendars(ctx, userID)
}
func (s *Service) CreateCalendar(ctx context.Context, userID, calID, displayName, color string) error {
return s.nc.CreateCalendar(ctx, userID, calID, displayName, color)
}
func (s *Service) UpdateCalendar(ctx context.Context, userID, calID, displayName, color string) error {
return s.nc.UpdateCalendar(ctx, userID, calID, displayName, color)
}
func (s *Service) DeleteCalendar(ctx context.Context, userID, calID string) error {
return s.nc.DeleteCalendar(ctx, userID, calID)
}
type EventsList struct {
Events []nextcloud.Event `json:"events"`
Pagination query.PaginationMeta `json:"pagination,omitempty"`
}
func (s *Service) ListEvents(ctx context.Context, userID, calID string, params query.ListParams) (EventsList, error) {
from := time.Now().AddDate(0, -1, 0)
to := time.Now().AddDate(0, 1, 0)
if params.From != nil {
from = *params.From
}
if params.To != nil {
to = *params.To
}
events, err := s.nc.ListEvents(ctx, userID, calendarPath(userID, calID), from, to)
if err != nil {
return EventsList{}, err
}
filtered := filterEvents(events, params.Q)
page, total := paginate.Slice(filtered, params.Offset(), params.Limit())
return EventsList{
Events: page,
Pagination: params.Meta(&total),
}, nil
}
func (s *Service) CreateEvent(ctx context.Context, userID, calID string, event *nextcloud.Event) error {
if strings.TrimSpace(event.Organizer) == "" {
event.Organizer = userID
}
return s.nc.CreateEvent(ctx, userID, calendarPath(userID, calID), event)
}
func (s *Service) UpdateEvent(ctx context.Context, userID, eventPath, ifMatch string, event *nextcloud.Event) (string, error) {
if strings.TrimSpace(event.Organizer) == "" {
event.Organizer = userID
}
return s.nc.UpdateEvent(ctx, userID, eventPath, ifMatch, event)
}
func (s *Service) DeleteEvent(ctx context.Context, userID, eventPath string) error {
return s.nc.DeleteEvent(ctx, userID, eventPath)
}
func (s *Service) FreeBusy(ctx context.Context, userID string, req *nextcloud.FreeBusyRequest) (*nextcloud.FreeBusyResponse, error) {
return s.nc.FreeBusy(ctx, userID, req)
}
func (s *Service) RespondToInvitation(ctx context.Context, userID, eventPath, attendeeEmail, status, ifMatch string) (string, error) {
event, err := s.nc.GetEvent(ctx, userID, eventPath)
if err != nil {
return "", err
}
target := strings.ToLower(strings.TrimSpace(attendeeEmail))
for i := range event.Attendees {
if strings.EqualFold(strings.TrimSpace(event.Attendees[i].Email), target) {
event.Attendees[i].Status = strings.ToUpper(strings.TrimSpace(status))
match := strings.TrimSpace(ifMatch)
if match == "" {
match = strings.TrimSpace(event.ETag)
}
return s.nc.UpdateEvent(ctx, userID, eventPath, match, event)
}
}
return "", nextcloud.ErrAttendeeNotFound
}
func (s *Service) CreateMeetLink(ctx context.Context, userID, userName, userEmail, eventPath, ifMatch string) (string, string, error) {
if s.meetCfg == nil {
return "", "", ErrMeetDisabled
}
event, err := s.nc.GetEvent(ctx, userID, eventPath)
if err != nil {
return "", "", err
}
roomID := strings.TrimSpace(event.UID)
if roomID == "" {
roomID = fmt.Sprintf("event-%d", time.Now().Unix())
}
token, err := s.meetCfg.GenerateToken(roomID, &meetpkg.UserInfo{
ID: userID,
Name: userName,
Email: userEmail,
IsMod: true,
}, 24*time.Hour)
if err != nil {
return "", "", err
}
event.MeetURL = token.MeetURL
if strings.TrimSpace(event.Location) == "" {
event.Location = token.MeetURL
}
match := strings.TrimSpace(ifMatch)
if match == "" {
match = strings.TrimSpace(event.ETag)
}
etag, err := s.nc.UpdateEvent(ctx, userID, eventPath, match, event)
if err != nil {
return "", "", err
}
return token.MeetURL, etag, nil
}
func filterEvents(events []nextcloud.Event, q string) []nextcloud.Event {
q = strings.ToLower(strings.TrimSpace(q))
if q == "" {
return events
}
out := make([]nextcloud.Event, 0, len(events))
for _, e := range events {
if strings.Contains(strings.ToLower(e.Summary), q) ||
strings.Contains(strings.ToLower(e.Description), q) ||
strings.Contains(strings.ToLower(e.Location), q) {
out = append(out, e)
}
}
return out
}