From 884855ee0b97a1df0ed732673ae964c50c8fcf52 Mon Sep 17 00:00:00 2001 From: mindonwarp Date: Sun, 28 Dec 2025 21:11:20 +0100 Subject: [PATCH] core: Add a wrapper object for event organizer Add a wrapper for the ECalComponentOrganizer. An instance of this type is kept as part of the GcalEvent structure. The GcalEventOrganizer is just a "proxy" to the ECalComponentAttendee and its purpose is to unclutter the GcalEvent interface and provide a bindable base for a UI widget. Part-of: --- src/core/gcal-event-organizer.c | 220 ++++++++++++++++++++++++++++++++ src/core/gcal-event-organizer.h | 35 +++++ src/core/gcal-event.c | 79 +++++++++++- src/core/gcal-event.h | 5 + src/core/meson.build | 1 + tests/test-event.c | 54 ++++++++ 6 files changed, 390 insertions(+), 4 deletions(-) create mode 100644 src/core/gcal-event-organizer.c create mode 100644 src/core/gcal-event-organizer.h diff --git a/src/core/gcal-event-organizer.c b/src/core/gcal-event-organizer.c new file mode 100644 index 00000000..a7db468e --- /dev/null +++ b/src/core/gcal-event-organizer.c @@ -0,0 +1,220 @@ +/* gcal-event-organizer.c + * + * Copyright (C) 2025 Alen Galinec + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define G_LOG_DOMAIN "gcal_event_organizer" + +#include "gcal-event-organizer.h" +#include "glib-object.h" +#include "glib.h" +#include "libecal/libecal.h" + +struct _GcalEventOrganizer +{ + GObject parent_instance; + + ECalComponentOrganizer *ecal_organizer; +}; + +G_DEFINE_FINAL_TYPE (GcalEventOrganizer, gcal_event_organizer, G_TYPE_OBJECT) + +enum +{ + PROP_0, + PROP_NAME, + PROP_URI, + PROP_SENTBY, + PROP_LANGUAGE, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS]; + +static void +gcal_event_organizer_finalize (GObject *object) +{ + GcalEventOrganizer *self = (GcalEventOrganizer *) object; + + g_clear_pointer (&self->ecal_organizer, e_cal_component_organizer_free); + + G_OBJECT_CLASS (gcal_event_organizer_parent_class)->finalize (object); +} + +static void +gcal_event_organizer_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GcalEventOrganizer *self = GCAL_EVENT_ORGANIZER (object); + + switch (property_id) + { + case PROP_NAME: + g_value_set_string (value, gcal_event_organizer_get_name (self)); + break; + + case PROP_URI: + g_value_set_string (value, gcal_event_organizer_get_uri (self)); + break; + + case PROP_SENTBY: + g_value_set_string (value, gcal_event_organizer_get_sent_by (self)); + break; + + case PROP_LANGUAGE: + g_value_set_string (value, gcal_event_organizer_get_language (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gcal_event_organizer_class_init (GcalEventOrganizerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = gcal_event_organizer_finalize; + object_class->get_property = gcal_event_organizer_get_property; + + /** + * GcalEventOrganizer:name: + * + * The display (common name) of the organizer. + */ + properties[PROP_NAME] = + g_param_spec_string ("name", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GcalEventOrganizer:uri: + * + * This corresponds with the "value" from the #ECalComponentOrganizer. + * This is the email address or, more generally, the URI of the organizer. + */ + properties[PROP_URI] = + g_param_spec_string ("uri", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GcalEventOrganizer:sent-by: + * + * The mailto of the user acting on behalf of @self. + */ + properties[PROP_SENTBY] = + g_param_spec_string ("sent-by", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * GcalEventOrganizer:language: + * + * The language (used for text) of the organizer. + */ + properties[PROP_LANGUAGE] = + g_param_spec_string ("language", NULL, NULL, NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +gcal_event_organizer_init (GcalEventOrganizer *instance) +{ +} + +/** + * gcal_event_organizer_new: + * @ecal_organizer: #ECalComponentOrganizer from the parent event. + * + * Takes the ECalComponentOrganizer from its parent event instance + * and stores a copy of it internally. + * + * Returns: an instance of #GcalEventOrganizer. + */ +GcalEventOrganizer * +gcal_event_organizer_new (ECalComponentOrganizer *ecal_organizer) +{ + g_autoptr (GcalEventOrganizer) self = NULL; + + g_return_val_if_fail (ecal_organizer != NULL, NULL); + + self = g_object_new (GCAL_TYPE_EVENT_ORGANIZER, NULL); + self->ecal_organizer = e_cal_component_organizer_copy (ecal_organizer); + + return g_steal_pointer (&self); +} + +/** + * gcal_event_organizer_get_name: + * @self: an #GcalEventOrganizer instance. + * + * Returns: (transfer none) (nullable): the name (CN) of the organizer. + */ +const gchar * +gcal_event_organizer_get_name (GcalEventOrganizer *self) +{ + g_return_val_if_fail (GCAL_IS_EVENT_ORGANIZER (self), NULL); + + return e_cal_component_organizer_get_cn (self->ecal_organizer); +} + +/** + * gcal_event_organizer_get_uri: + * @self: an #GcalEventOrganizer instance. + * + * The URI aka ECalComponentOrganizer.value is normally + * the "mailto:user@email.org". + * + * Returns: (transfer none) (nullable): the URI (value) of the organizer. + */ +const gchar * +gcal_event_organizer_get_uri (GcalEventOrganizer *self) +{ + g_return_val_if_fail (GCAL_IS_EVENT_ORGANIZER (self), NULL); + + return e_cal_component_organizer_get_value (self->ecal_organizer); +} + +/** + * gcal_event_organizer_get_sent_by: + * @self: an #GcalEventOrganizer instance. + * + * Returns: (transfer none) (nullable): "mailto:" of the sender. + */ +const gchar * +gcal_event_organizer_get_sent_by (GcalEventOrganizer *self) +{ + g_return_val_if_fail (GCAL_IS_EVENT_ORGANIZER (self), NULL); + + return e_cal_component_organizer_get_sentby (self->ecal_organizer); +} + +/** + * gcal_event_organizer_get_language: + * @self: an #GcalEventOrganizer instance. + * + * Returns: (transfer none) (nullable): the language of the organizer. + */ +const gchar * +gcal_event_organizer_get_language (GcalEventOrganizer *self) +{ + g_return_val_if_fail (GCAL_IS_EVENT_ORGANIZER (self), NULL); + + return e_cal_component_organizer_get_language (self->ecal_organizer); +} diff --git a/src/core/gcal-event-organizer.h b/src/core/gcal-event-organizer.h new file mode 100644 index 00000000..fb4b1933 --- /dev/null +++ b/src/core/gcal-event-organizer.h @@ -0,0 +1,35 @@ +/* gcal-event-organizer.h + * + * Copyright (C) 2025 Alen Galinec + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "libecal/libecal.h" +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_EVENT_ORGANIZER (gcal_event_organizer_get_type ()) +G_DECLARE_FINAL_TYPE (GcalEventOrganizer, gcal_event_organizer, GCAL, EVENT_ORGANIZER, GObject) + +GcalEventOrganizer *gcal_event_organizer_new (ECalComponentOrganizer *ecal_organizer); +const gchar *gcal_event_organizer_get_name (GcalEventOrganizer *self); +const gchar *gcal_event_organizer_get_uri (GcalEventOrganizer *self); +const gchar *gcal_event_organizer_get_sent_by (GcalEventOrganizer *self); +const gchar *gcal_event_organizer_get_language (GcalEventOrganizer *self); + +G_END_DECLS diff --git a/src/core/gcal-event.c b/src/core/gcal-event.c index 7cec2766..1f37329e 100644 --- a/src/core/gcal-event.c +++ b/src/core/gcal-event.c @@ -18,13 +18,14 @@ #define G_LOG_DOMAIN "GcalEvent" -#include "gconstructor.h" +#include "gcal-event.h" #include "gcal-application.h" #include "gcal-context.h" #include "gcal-debug.h" -#include "gcal-event.h" -#include "gcal-utils.h" +#include "gcal-event-attendee.h" +#include "gcal-event-organizer.h" #include "gcal-recurrence.h" +#include "gcal-utils.h" #include @@ -98,6 +99,8 @@ struct _GcalEvent /* A map of GcalAlarmType */ GHashTable *alarms; + GSList *attendees; + GcalEventOrganizer *organizer; ECalComponent *component; GcalCalendar *calendar; @@ -303,6 +306,24 @@ load_alarms (GcalEvent *self) g_slist_free_full (alarm_uids, g_free); } +static void +load_attendees (GcalEvent *self) +{ + GSList *ecal_attendees = NULL, *c = NULL; + + ecal_attendees = e_cal_component_get_attendees (self->component); + + for (c = ecal_attendees; c != NULL; c = c->next) + { + GcalEventAttendee *attendee; + attendee = gcal_event_attendee_new (c->data); + /* we don't care about the order so no need to reverse */ + self->attendees = g_slist_prepend (self->attendees, attendee); + } + + g_slist_free_full (ecal_attendees, e_cal_component_attendee_free); +} + static gboolean setup_component (GcalEvent *self, GError **error) @@ -315,6 +336,7 @@ setup_component (GcalEvent *self, ICalTime *date; gboolean start_is_all_day, end_is_all_day; gchar *description, *location; + ECalComponentOrganizer *organizer = NULL; g_assert (self->component != NULL); @@ -416,6 +438,15 @@ setup_component (GcalEvent *self, /* Load and setup the alarms */ load_alarms (self); + /* Load and setup organizer */ + organizer = e_cal_component_get_organizer (self->component); + if (organizer) + self->organizer = gcal_event_organizer_new (organizer); + + /* load and setup attendees */ + if (e_cal_component_has_attendees (self->component)) + load_attendees (self); + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_RECURRENCE]); g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_RECURRENCE]); @@ -425,11 +456,11 @@ setup_component (GcalEvent *self, e_cal_component_text_free (text); e_cal_component_datetime_free (start); e_cal_component_datetime_free (end); + e_cal_component_organizer_free (organizer); return TRUE; } - static void gcal_event_set_component_internal (GcalEvent *self, ECalComponent *component) @@ -476,6 +507,8 @@ gcal_event_finalize (GObject *object) g_clear_object (&self->component); g_clear_object (&self->calendar); g_clear_pointer (&self->recurrence, gcal_recurrence_unref); + g_slist_free_full (self->attendees, g_object_unref); + g_clear_object (&self->organizer); G_OBJECT_CLASS (gcal_event_parent_class)->finalize (object); } @@ -1795,3 +1828,41 @@ gcal_event_overlaps (GcalEvent *self, overlap = gcal_range_calculate_overlap (gcal_event_get_range (self), range, NULL); return overlap != GCAL_RANGE_NO_OVERLAP; } + +/** + * gcal_event_get_attendees: + * @self: a #GcalEvent + * + * Returns: (transfer full) (nullable): a copy list of attendees. + */ +GSList * +gcal_event_get_attendees (GcalEvent *self) +{ + g_return_val_if_fail (GCAL_IS_EVENT (self), NULL); + + if (self->attendees == NULL) + return NULL; + + GSList *ret = NULL, *c = NULL; + for (c = self->attendees; c != NULL; c = c->next) + { + ret = g_slist_prepend (ret, g_object_ref (c->data)); + } + + /* reverse here so the returned list order matches the internal one */ + return g_slist_reverse (ret); +} + +/** + * gcal_event_get_organizer: + * @self: a #GcalEvent + * + * Returns: (transfer none) (nullable): the event organizer. + */ +GcalEventOrganizer * +gcal_event_get_organizer (GcalEvent *self) +{ + g_return_val_if_fail (GCAL_IS_EVENT (self), NULL); + + return self->organizer; +} diff --git a/src/core/gcal-event.h b/src/core/gcal-event.h index b9adbda6..47b0a076 100644 --- a/src/core/gcal-event.h +++ b/src/core/gcal-event.h @@ -20,6 +20,7 @@ #define GCAL_EVENT_H #include "gcal-calendar.h" +#include "gcal-event-organizer.h" #include "gcal-range.h" #include "gcal-recurrence.h" @@ -133,6 +134,10 @@ gchar* gcal_event_format_date (GcalEvent gboolean gcal_event_overlaps (GcalEvent *self, GcalRange *range); +GSList * gcal_event_get_attendees (GcalEvent *self); + +GcalEventOrganizer * gcal_event_get_organizer (GcalEvent *self); + G_END_DECLS #endif /* GCAL_EVENT_H */ diff --git a/src/core/meson.build b/src/core/meson.build index 06ee2c01..4b4f5671 100644 --- a/src/core/meson.build +++ b/src/core/meson.build @@ -18,4 +18,5 @@ sources += files( 'gcal-timer.c', 'gcal-time-zone-monitor.c', 'gcal-event-attendee.c', + 'gcal-event-organizer.c', ) diff --git a/tests/test-event.c b/tests/test-event.c index c92d665b..8e9307bc 100644 --- a/tests/test-event.c +++ b/tests/test-event.c @@ -18,6 +18,8 @@ #include +#include "gcal-event-attendee.h" +#include "gcal-event-organizer.h" #include "gcal-event.h" #include "gcal-stub-calendar.h" #include "gcal-utils.h" @@ -59,6 +61,17 @@ EVENT_STRING_FOR_DATE(":19970101T170000Z", dtend) +#define STUB_EVENT_WITH_ATTENDEES "BEGIN:VEVENT\n" \ + "UID:123456789@example.com\n" \ + "DTSTAMP:20240210T120000Z\n" \ + "DTSTART:20240215T150000Z\n" \ + "DTEND:20240215T160000Z\n" \ + "SUMMARY:Project Meeting\n" \ + "DESCRIPTION:Discuss project updates and next steps.\n" \ + "ORGANIZER;CN=Alice Smith;SENT-BY=\"MAILTO:jane_doe@host.com\":mailto:alice@example.com\n" \ + "ATTENDEE;CN=Bob Johnson;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:bob@example.com\n" \ + "ATTENDEE;CN=Carol Lee;RSVP=FALSE;ROLE=OPT-PARTICIPANT:mailto:carol@example.com\n" \ + "END:VEVENT\n" /* * Auxiliary methods @@ -391,6 +404,46 @@ event_date_check_tz (void) /*********************************************************************************************************************/ +static void +event_get_attendees (void) +{ + /* "ATTENDEE;CN=Bob Johnson;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:bob@example.com\n" \ */ + /* "ATTENDEE;CN=Carol Lee;RSVP=FALSE;ROLE=OPT-PARTICIPANT:mailto:carol@example.com\n" \ */ + g_autoptr (GcalEvent) event = NULL; + g_autoptr (GError) error = NULL; + GSList *attendees = NULL, *l = NULL; + + event = create_event_for_string (STUB_EVENT_WITH_ATTENDEES, &error); + + attendees = gcal_event_get_attendees (event); + + g_assert_nonnull (attendees); + + for (l = attendees; l != NULL; l = l->next) + { + GcalEventAttendee *at = (GcalEventAttendee *) l->data; + + g_assert_cmpstr (NULL, !=, gcal_event_attendee_get_name (at)); + g_assert_cmpstr (NULL, !=, gcal_event_attendee_get_uri (at)); + g_assert_cmpuint (GCAL_EVENT_ATTENDEE_TYPE_NONE, !=, gcal_event_attendee_get_attendee_type (at)); + } + + /* organizer should be set */ + /* "ORGANIZER;CN=Alice Smith;SENT-BY=\"MAILTO:jane_doe@host.com\":mailto:alice@example.com\n" \ */ + GcalEventOrganizer *organizer = gcal_event_get_organizer (event); + g_assert_nonnull (organizer); + g_assert_cmpstr ("Alice Smith", ==, gcal_event_organizer_get_name (organizer)); + g_assert_cmpstr ("MAILTO:jane_doe@host.com", ==, gcal_event_organizer_get_sent_by (organizer)); + g_assert_cmpstr ("mailto:alice@example.com", ==, gcal_event_organizer_get_uri (organizer)); + + g_autofree const gchar *uri_trimmed = gcal_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer)); + g_assert_cmpstr ("alice@example.com", ==, uri_trimmed); + + g_slist_free_full (attendees, g_object_unref); +} + +/*********************************************************************************************************************/ + gint main (gint argc, gchar *argv[]) @@ -411,6 +464,7 @@ main (gint argc, g_test_add_func ("/event/date/multiday", event_date_multiday); g_test_add_func ("/event/date/create-tzid", event_date_create_tzid); g_test_add_func ("/event/date/check-tz", event_date_check_tz); + g_test_add_func ("/event/date/get-attendees", event_get_attendees); return g_test_run (); }