package ultidraw import ( "bytes" "context" "fmt" "io" "strings" "time" "github.com/ultisuite/ulti-backend/internal/auth" "github.com/ultisuite/ulti-backend/internal/nextcloud" ) type Service struct { nc *nextcloud.Client Cfg Config hub fileChangePublisher } type fileChangePublisher interface { PublishFileChanged(platformUserID, path string) } func NewService(nc *nextcloud.Client, cfg Config, hub fileChangePublisher) *Service { return &Service{nc: nc, Cfg: cfg, hub: hub} } func (s *Service) EnsureNextcloudUser(ctx context.Context, claims *auth.Claims) (string, error) { return s.nc.EnsurePrincipal(ctx, claims.Email, claims.Sub, claims.Name) } type SessionResult struct { RoomID string `json:"roomId"` CanonicalPath string `json:"canonicalPath"` WsURL string `json:"wsUrl"` Token string `json:"token"` Mode string `json:"mode"` Collaboration bool `json:"collaboration"` } type PublicSessionResult struct { SessionResult DocumentURL string `json:"documentUrl,omitempty"` SaveURL string `json:"saveUrl,omitempty"` } func (s *Service) CreateSession(ctx context.Context, ncUser, filePath, mode, editorUserID, editorName string) (*SessionResult, error) { if !s.Cfg.Enabled { return nil, fmt.Errorf("ultidraw editor disabled") } filePath = normalizePath(filePath) if !isExcalidrawPath(filePath) { return nil, fmt.Errorf("not an excalidraw file: %s", filePath) } if _, err := s.nc.FileRevision(ctx, ncUser, filePath); err != nil { return nil, fmt.Errorf("file not found: %s", filePath) } if mode == "" { mode = "edit" } roomID, err := s.resolveCollabRoomID(ctx, ncUser, filePath) if err != nil { return nil, err } token, err := signRoomToken(roomTokenPayload{ Room: roomID, Path: filePath, User: ncUser, Sub: editorUserID, Name: editorName, Mode: mode, Expires: time.Now().Add(8 * time.Hour).Unix(), }, s.Cfg.HocuspocusSecret) if err != nil { return nil, err } wsURL := strings.TrimSpace(s.Cfg.HocuspocusPublicURL) collab := wsURL != "" && s.Cfg.HocuspocusSecret != "" return &SessionResult{ RoomID: roomID, CanonicalPath: filePath, WsURL: wsURL, Token: token, Mode: mode, Collaboration: collab, }, nil } func (s *Service) LoadDocument(ctx context.Context, ncUser, path string) ([]byte, error) { path = normalizePath(path) body, _, err := s.nc.Download(ctx, ncUser, path) if err != nil { return nil, err } defer body.Close() return io.ReadAll(body) } func (s *Service) LoadDocumentForUser(ctx context.Context, ncUser, path string) ([]byte, error) { path = normalizePath(path) if strings.HasPrefix(ncUser, "public:") { token := strings.TrimPrefix(ncUser, "public:") return s.LoadPublicDocumentLegacy(ctx, token, path, "") } return s.LoadDocument(ctx, ncUser, path) } func (s *Service) SaveDocument(ctx context.Context, ncUser, path string, raw []byte, platformUserID string) error { path = normalizePath(path) reader := bytes.NewReader(raw) if strings.HasPrefix(ncUser, "public:") { token := strings.TrimPrefix(ncUser, "public:") return s.SavePublicDocumentLegacy(ctx, token, path, "", raw) } if err := s.nc.Upload(ctx, ncUser, path, reader, "application/json"); err != nil { return err } if s.hub != nil && platformUserID != "" { s.hub.PublishFileChanged(platformUserID, path) } return nil }