From c1cd614abc8ad066918b945e207ef7fe2ab5ddd2 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Wed, 5 Nov 2025 16:21:47 +0100
Subject: [PATCH] groupware: fix deserialization of Event Alert Trigger types
using mapstructure
---
pkg/jmap/jmap_test.go | 48 ++++++++++++++++++++
pkg/jmap/jmap_tools.go | 11 +++--
pkg/jscalendar/jscalendar_model.go | 72 ++++++++++++++++++++++++++++++
3 files changed, 128 insertions(+), 3 deletions(-)
diff --git a/pkg/jmap/jmap_test.go b/pkg/jmap/jmap_test.go
index 195eb1f883..80fe89a61d 100644
--- a/pkg/jmap/jmap_test.go
+++ b/pkg/jmap/jmap_test.go
@@ -725,3 +725,51 @@ func TestUnmarshallingCalendarEventGetResponse(t *testing.T) {
require.Equal("9a7ab91a-edca-4988-886f-25e00743430d", result.Uid)
require.Equal(jscalendar.PrivacyPublic, result.Privacy)
}
+
+func TestAlertWithOffsetTriggerInResponse(t *testing.T) {
+ require := require.New(t)
+
+ text := `{
+ "methodResponses":[
+ ["CalendarEvent/get",{
+ "accountId":"b",
+ "state":"ssecq",
+ "list":[{
+ "@type": "Event",
+ "start":"2025-11-01T14:30:00",
+ "alerts": {
+ "M87fT82": {
+ "@type": "Alert",
+ "trigger": {
+ "@type": "OffsetTrigger",
+ "offset": "-PT15M",
+ "relativeTo": "start"
+ }
+ }
+ }
+ }],
+ "notFound":[]
+ },"1"]
+ ],"sessionState":"7d3cae5b"
+}`
+
+ var response Response
+ err := json.Unmarshal([]byte(text), &response)
+ require.NoError(err)
+
+ resp := response.MethodResponses[0]
+ require.Equal(CommandCalendarEventGet, resp.Command)
+ require.IsType(CalendarEventGetResponse{}, resp.Parameters)
+ params := resp.Parameters.(CalendarEventGetResponse)
+ require.Len(params.List, 1)
+ event := params.List[0]
+ require.Contains(event.Alerts, "M87fT82")
+ alert := event.Alerts["M87fT82"]
+ require.NotNil(alert)
+ trigger := alert.Trigger
+ require.NotNil(trigger)
+ require.IsType(jscalendar.OffsetTrigger{}, trigger)
+ offsetTrigger := trigger.(jscalendar.OffsetTrigger)
+ require.Equal(jscalendar.SignedDuration("-PT15M"), offsetTrigger.Offset)
+ require.Equal(jscalendar.RelativeToStart, offsetTrigger.RelativeTo)
+}
diff --git a/pkg/jmap/jmap_tools.go b/pkg/jmap/jmap_tools.go
index 538f8bb0b3..6dd2f9c50b 100644
--- a/pkg/jmap/jmap_tools.go
+++ b/pkg/jmap/jmap_tools.go
@@ -12,6 +12,7 @@ import (
"time"
"github.com/mitchellh/mapstructure"
+ "github.com/opencloud-eu/opencloud/pkg/jscalendar"
"github.com/opencloud-eu/opencloud/pkg/log"
)
@@ -139,8 +140,9 @@ func mapstructStringToTimeHook() mapstructure.DecodeHookFunc {
// mapstruct isn't able to properly map RFC3339 date strings into Time
// objects, which is why we require this custom hook,
// see https://github.com/mitchellh/mapstructure/issues/41
+ wanted := reflect.TypeOf(time.Time{})
return func(from reflect.Type, to reflect.Type, data any) (any, error) {
- if to != reflect.TypeOf(time.Time{}) {
+ if to != wanted {
return data, nil
}
switch from.Kind() {
@@ -159,8 +161,11 @@ func mapstructStringToTimeHook() mapstructure.DecodeHookFunc {
func decodeMap(input map[string]any, target any) error {
// https://github.com/mitchellh/mapstructure/issues/41
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
- Metadata: nil,
- DecodeHook: mapstructure.ComposeDecodeHookFunc(mapstructStringToTimeHook()),
+ Metadata: nil,
+ DecodeHook: mapstructure.ComposeDecodeHookFunc(
+ mapstructStringToTimeHook(),
+ jscalendar.MapstructTriggerHook(),
+ ),
Result: &target,
ErrorUnused: false,
ErrorUnset: false,
diff --git a/pkg/jscalendar/jscalendar_model.go b/pkg/jscalendar/jscalendar_model.go
index 139fe89c16..3d21f23326 100644
--- a/pkg/jscalendar/jscalendar_model.go
+++ b/pkg/jscalendar/jscalendar_model.go
@@ -3,7 +3,11 @@ package jscalendar
import (
"encoding/json"
"fmt"
+ "reflect"
+ "slices"
"time"
+
+ "github.com/mitchellh/mapstructure"
)
// This is a date-time string with no time zone/offset information.
@@ -1407,6 +1411,74 @@ var _ Trigger = UnknownTrigger{}
func (o UnknownTrigger) trigger() {}
+func MapstructTriggerHook() mapstructure.DecodeHookFunc {
+ fn := func(Trigger) {}
+ wanted := reflect.TypeOf(fn).In(0)
+ return func(from reflect.Type, to reflect.Type, data any) (any, error) {
+ if to != wanted {
+ return data, nil
+ }
+ m := data.(map[string]any)
+ if typ, ok := m["@type"]; ok {
+ switch typ {
+ case string(OffsetTriggerType):
+ return mapOffsetTrigger(m)
+ case string(AbsoluteTriggerType):
+ return mapAbsoluteTrigger(m)
+ default:
+ return UnknownTrigger(m), nil
+ }
+ } else {
+ if _, ok := m["offset"]; ok {
+ return mapOffsetTrigger(m)
+ }
+ if _, ok := m["when"]; ok {
+ return mapAbsoluteTrigger(m)
+ } else {
+ return UnknownTrigger(m), nil
+ }
+ }
+ }
+}
+
+func mapOffsetTrigger(m map[string]any) (OffsetTrigger, error) {
+ trigger := OffsetTrigger{
+ Type: OffsetTriggerType,
+ }
+ if value, ok := m["offset"]; ok {
+ if str, ok := value.(string); ok {
+ trigger.Offset = SignedDuration(str)
+ }
+ }
+ if value, ok := m["relativeTo"]; ok {
+ if str, ok := value.(string); ok {
+ t := RelativeTo(str)
+ if slices.Contains(RelativeTos, t) {
+ trigger.RelativeTo = t
+ } else {
+ return trigger, fmt.Errorf("unsupported Trigger.relativeTo value: '%v'", value)
+ }
+ }
+ }
+ return trigger, nil
+}
+
+func mapAbsoluteTrigger(m map[string]any) (AbsoluteTrigger, error) {
+ trigger := AbsoluteTrigger{
+ Type: AbsoluteTriggerType,
+ }
+ if value, ok := m["when"]; ok {
+ if str, ok := value.(string); ok {
+ if w, err := time.Parse(time.RFC3339, str); err != nil {
+ trigger.When = w
+ } else {
+ return trigger, err
+ }
+ }
+ }
+ return trigger, nil
+}
+
type Alert struct {
// This specifies the type of this object. This MUST be `Alert`.
Type TypeOfAlert `json:"@type,omitempty"`