- Added support for Faster Whisper transcription via Jigasi and Skynet. - Updated .env.example to include new environment variables for transcription settings. - Enhanced Jitsi Docker Compose configuration to include Skynet and Jigasi services. - Introduced new API endpoints for managing organizational folders in the drive service. - Updated Nextcloud initialization script to enable external file mounting. - Improved error handling and response structures in the drive API. - Added new properties for organization settings related to transcription and agenda management.
207 lines
6.3 KiB
Go
207 lines
6.3 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"
|
|
"github.com/ultisuite/ulti-backend/internal/orgpolicy"
|
|
)
|
|
|
|
type Service struct {
|
|
nc *nextcloud.Client
|
|
meetCfg *meetpkg.Config
|
|
policy *orgpolicy.Loader
|
|
}
|
|
|
|
var ErrMeetDisabled = errors.New("meet is disabled")
|
|
|
|
func NewService(nc *nextcloud.Client, meetCfg *meetpkg.Config, policy *orgpolicy.Loader) *Service {
|
|
return &Service{nc: nc, meetCfg: meetCfg, policy: policy}
|
|
}
|
|
|
|
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) {
|
|
existing, err := s.nc.GetEvent(ctx, userID, eventPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
merged := nextcloud.MergeEvent(existing, event)
|
|
if strings.TrimSpace(merged.Organizer) == "" {
|
|
merged.Organizer = userID
|
|
}
|
|
merged.Sequence = existing.Sequence + 1
|
|
if merged.Sequence < 1 {
|
|
merged.Sequence = 1
|
|
}
|
|
match := strings.TrimSpace(ifMatch)
|
|
if match == "" || match == "*" {
|
|
match = strings.TrimSpace(existing.ETag)
|
|
}
|
|
return s.nc.UpdateEvent(ctx, userID, eventPath, match, merged)
|
|
}
|
|
|
|
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())
|
|
}
|
|
tokenOpts := meetpkg.TokenOptions{}
|
|
if s.policy != nil {
|
|
if p, err := s.policy.MeetPolicy(ctx); err == nil && p.LiveTranscriptionJWT() {
|
|
tokenOpts.Transcription = true
|
|
}
|
|
}
|
|
token, err := s.meetCfg.GenerateToken(roomID, &meetpkg.UserInfo{
|
|
ID: userID,
|
|
Name: userName,
|
|
Email: userEmail,
|
|
IsMod: true,
|
|
}, 24*time.Hour, tokenOpts)
|
|
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
|
|
}
|