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)