From 64bf860eeaeae752a06be2cc4f7744547235d9de Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Tue, 28 Oct 2025 10:45:29 +0100
Subject: [PATCH] groupware: add ical blob parsing endpoint
---
pkg/jmap/jmap_api_calendar.go | 27 +++++++++++++++
pkg/jmap/jmap_model.go | 33 ++++++++++++++++++-
.../pkg/groupware/groupware_api_calendars.go | 23 +++++++++++++
.../pkg/groupware/groupware_route.go | 3 ++
4 files changed, 85 insertions(+), 1 deletion(-)
create mode 100644 pkg/jmap/jmap_api_calendar.go
diff --git a/pkg/jmap/jmap_api_calendar.go b/pkg/jmap/jmap_api_calendar.go
new file mode 100644
index 0000000000..f2fffc24a3
--- /dev/null
+++ b/pkg/jmap/jmap_api_calendar.go
@@ -0,0 +1,27 @@
+package jmap
+
+import (
+ "context"
+
+ "github.com/opencloud-eu/opencloud/pkg/log"
+)
+
+func (j *Client) ParseICalendarBlob(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, blobIds []string) (CalendarEventParseResponse, SessionState, Language, Error) {
+ logger = j.logger("ParseICalendarBlob", session, logger)
+
+ cmd, err := j.request(session, logger,
+ invocation(CommandCalendarEventParse, CalendarEventParseCommand{AccountId: accountId, BlobIDs: blobIds}, "0"),
+ )
+ if err != nil {
+ return CalendarEventParseResponse{}, "", "", err
+ }
+
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (CalendarEventParseResponse, Error) {
+ var response CalendarEventParseResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandCalendarEventParse, "0", &response)
+ if err != nil {
+ return CalendarEventParseResponse{}, err
+ }
+ return response, nil
+ })
+}
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 79bdce1cd7..34660f62fb 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -3790,7 +3790,7 @@ type CalendarEvent struct {
//
// The id uniquely identifies a JSCalendar Event with a particular `uid` and
// `recurrenceId` within a particular account.
- Id string `json:"id"`
+ Id string `json:"id,omitempty"`
// This is only defined if the `id` property is a synthetic id, generated by the
// server to represent a particular instance of a recurring event (immutable; server-set).
@@ -5204,6 +5204,35 @@ type ContactCardSetResponse struct {
NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"`
}
+type CalendarEventParseCommand struct {
+ // The id of the account to use.
+ AccountId string `json:"accountId"`
+
+ // The ids of the blobs to parse
+ BlobIDs []string `json:"blobIds,omitempty"`
+
+ // If supplied, only the properties listed in the array are returned for each CalendarEvent object.
+ //
+ // If omitted, defaults to all the properties.
+ Properties []string `json:"properties,omitempty"`
+}
+
+type CalendarEventParseResponse struct {
+ // The id of the account used for the call.
+ AccountId string `json:"accountId"`
+
+ // A map of blob ids to parsed CalendarEvent objects representations for each successfully
+ // parsed blob, or null if none.
+ Parsed map[string][]CalendarEvent `json:"parsed,omitempty"`
+
+ // A list of blob ids given that could not be found, or null if none.
+ NotFound []string `json:"notFound,omitempty"`
+
+ // A list of blob ids given that corresponded to blobs that could not be parsed as
+ // CalendarEvents, or null if none.
+ NotParsable []string `json:"notParsable,omitempty"`
+}
+
type ErrorResponse struct {
Type string `json:"type"`
Description string `json:"description,omitempty"`
@@ -5234,6 +5263,7 @@ const (
CommandContactCardQuery Command = "ContactCard/query"
CommandContactCardGet Command = "ContactCard/get"
CommandContactCardSet Command = "ContactCard/set"
+ CommandCalendarEventParse Command = "CalendarEvent/parse"
)
var CommandResponseTypeMap = map[Command]func() any{
@@ -5260,4 +5290,5 @@ var CommandResponseTypeMap = map[Command]func() any{
CommandContactCardQuery: func() any { return ContactCardQueryResponse{} },
CommandContactCardGet: func() any { return ContactCardGetResponse{} },
CommandContactCardSet: func() any { return ContactCardSetResponse{} },
+ CommandCalendarEventParse: func() any { return CalendarEventParseResponse{} },
}
diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go
index e7f4ba7ba1..ee8f5981a6 100644
--- a/services/groupware/pkg/groupware/groupware_api_calendars.go
+++ b/services/groupware/pkg/groupware/groupware_api_calendars.go
@@ -2,9 +2,11 @@ package groupware
import (
"net/http"
+ "strings"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/opencloud/pkg/jmap"
+ "github.com/opencloud-eu/opencloud/pkg/log"
)
// When the request succeeds.
@@ -105,3 +107,24 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
return response(events, req.session.State, "")
})
}
+
+func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ accountId, err := req.GetAccountIdForBlob()
+ if err != nil {
+ return errorResponse(err)
+ }
+
+ blobId := chi.URLParam(r, UriParamBlobId)
+
+ blobIds := strings.Split(blobId, ",")
+ l := req.logger.With().Array(UriParamBlobId, log.SafeStringArray(blobIds))
+ logger := log.From(l)
+
+ resp, sessionState, lang, jerr := g.jmap.ParseICalendarBlob(accountId, req.session, req.ctx, logger, req.language(), blobIds)
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+ return response(resp, sessionState, lang)
+ })
+}
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index b9b2c9d8cf..87ea007cb3 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -113,6 +113,9 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/{blobid}", g.GetBlobMeta)
r.Get("/{blobid}/{blobname}", g.DownloadBlob) // ?type=
})
+ r.Route("/ical", func(r chi.Router) {
+ r.Get("/{blobid}", g.ParseIcalBlob)
+ })
r.Route("/addressbooks", func(r chi.Router) {
r.Get("/", g.GetAddressbooks)
r.Get("/{addressbookid}", g.GetAddressbook)