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"`