Invite people to events on Federated Core calendars
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
"finish-args" : [
|
||||
"--env=G_DEBUG=fatal-criticals",
|
||||
"--device=dri",
|
||||
"--filesystem=xdg-config/coredesktop:ro",
|
||||
"--share=ipc",
|
||||
"--share=network",
|
||||
"--socket=fallback-x11",
|
||||
|
||||
@@ -96,6 +96,7 @@ assert(cc.has_function('strerror'), '"strerror" not found')
|
||||
|
||||
# options
|
||||
config_h.set('ENABLE_TRACING', enable_tracing)
|
||||
config_h.set('ENABLE_FEDERATED_CORE', get_option('federated_core'))
|
||||
config_h.set('HANDY_USE_UNSTABLE_API', true)
|
||||
|
||||
# _NL_TIME_FIRST_WEEKDAY is an enum and not a define
|
||||
@@ -210,6 +211,9 @@ summary({
|
||||
'Source': meson.global_source_root(),
|
||||
'Prefix': calendar_prefix,
|
||||
})
|
||||
summary({
|
||||
'Federated Core': get_option('federated_core'),
|
||||
}, section: 'Features')
|
||||
summary({
|
||||
'Debug': calendar_debug,
|
||||
'Tracing': enable_tracing,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
option('tracing', type: 'boolean', value: false, description: 'add extra debugging information')
|
||||
option('profile', type: 'combo', choices: ['default', 'development'], value: 'default')
|
||||
option('federated_core', type: 'boolean', value: true, description: 'invite people to events on the calendars of a Federated Computer Core')
|
||||
|
||||
@@ -2023,3 +2023,250 @@ gcal_event_get_organizer (GcalEvent *self)
|
||||
|
||||
return self->organizer;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
mailto_equal (const gchar *uri1,
|
||||
const gchar *uri2)
|
||||
{
|
||||
if (!uri1 || !uri2)
|
||||
return FALSE;
|
||||
|
||||
if (g_ascii_strncasecmp (uri1, "mailto:", 7) == 0)
|
||||
uri1 += 7;
|
||||
|
||||
if (g_ascii_strncasecmp (uri2, "mailto:", 7) == 0)
|
||||
uri2 += 7;
|
||||
|
||||
return g_ascii_strcasecmp (uri1, uri2) == 0;
|
||||
}
|
||||
|
||||
static void
|
||||
set_component_attendees (GcalEvent *self,
|
||||
GSList *ecal_attendees)
|
||||
{
|
||||
e_cal_component_set_attendees (self->component, ecal_attendees);
|
||||
e_cal_component_commit_sequence (self->component);
|
||||
|
||||
g_list_store_remove_all (self->attendees);
|
||||
load_attendees (self);
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_event_set_organizer:
|
||||
* @self: a #GcalEvent
|
||||
* @email: (nullable): the email address of the organizer
|
||||
* @name: (nullable): the common name of the organizer
|
||||
*
|
||||
* Sets the organizer of @self. Passing %NULL as @email removes the
|
||||
* organizer.
|
||||
*/
|
||||
void
|
||||
gcal_event_set_organizer (GcalEvent *self,
|
||||
const gchar *email,
|
||||
const gchar *name)
|
||||
{
|
||||
ECalComponentOrganizer *organizer = NULL;
|
||||
|
||||
g_return_if_fail (GCAL_IS_EVENT (self));
|
||||
|
||||
g_clear_object (&self->organizer);
|
||||
|
||||
if (email && *email)
|
||||
{
|
||||
g_autofree gchar *uri = g_strdup_printf ("mailto:%s", email);
|
||||
|
||||
organizer = e_cal_component_organizer_new_full (uri, NULL, (name && *name) ? name : NULL, NULL);
|
||||
self->organizer = gcal_event_organizer_new (organizer);
|
||||
}
|
||||
|
||||
e_cal_component_set_organizer (self->component, organizer);
|
||||
e_cal_component_commit_sequence (self->component);
|
||||
|
||||
e_cal_component_organizer_free (organizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_event_has_attendee:
|
||||
* @self: a #GcalEvent
|
||||
* @email: an email address, with or without the "mailto:" prefix
|
||||
*
|
||||
* Returns: %TRUE if @email is one of the attendees of @self
|
||||
*/
|
||||
gboolean
|
||||
gcal_event_has_attendee (GcalEvent *self,
|
||||
const gchar *email)
|
||||
{
|
||||
guint n_items;
|
||||
|
||||
g_return_val_if_fail (GCAL_IS_EVENT (self), FALSE);
|
||||
|
||||
n_items = g_list_model_get_n_items (G_LIST_MODEL (self->attendees));
|
||||
|
||||
for (guint i = 0; i < n_items; i++)
|
||||
{
|
||||
g_autoptr (GcalEventAttendee) attendee = g_list_model_get_item (G_LIST_MODEL (self->attendees), i);
|
||||
|
||||
if (mailto_equal (gcal_event_attendee_get_uri (attendee), email))
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_event_add_attendee:
|
||||
* @self: a #GcalEvent
|
||||
* @email: the email address of the attendee
|
||||
* @name: (nullable): the common name of the attendee
|
||||
*
|
||||
* Adds a person to the attendees of @self, who is asked to respond.
|
||||
*
|
||||
* Returns: %FALSE if @email already was an attendee
|
||||
*/
|
||||
gboolean
|
||||
gcal_event_add_attendee (GcalEvent *self,
|
||||
const gchar *email,
|
||||
const gchar *name)
|
||||
{
|
||||
g_autofree gchar *uri = NULL;
|
||||
GSList *ecal_attendees;
|
||||
|
||||
g_return_val_if_fail (GCAL_IS_EVENT (self), FALSE);
|
||||
g_return_val_if_fail (email && *email, FALSE);
|
||||
|
||||
if (gcal_event_has_attendee (self, email))
|
||||
return FALSE;
|
||||
|
||||
uri = g_strdup_printf ("mailto:%s", email);
|
||||
|
||||
ecal_attendees = e_cal_component_get_attendees (self->component);
|
||||
ecal_attendees = g_slist_append (ecal_attendees,
|
||||
e_cal_component_attendee_new_full (uri,
|
||||
NULL,
|
||||
I_CAL_CUTYPE_INDIVIDUAL,
|
||||
I_CAL_ROLE_REQPARTICIPANT,
|
||||
I_CAL_PARTSTAT_NEEDSACTION,
|
||||
TRUE,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
(name && *name) ? name : NULL,
|
||||
NULL));
|
||||
|
||||
set_component_attendees (self, ecal_attendees);
|
||||
|
||||
g_slist_free_full (ecal_attendees, e_cal_component_attendee_free);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_event_remove_attendee:
|
||||
* @self: a #GcalEvent
|
||||
* @email: an email address, with or without the "mailto:" prefix
|
||||
*
|
||||
* Removes @email from the attendees of @self.
|
||||
*/
|
||||
void
|
||||
gcal_event_remove_attendee (GcalEvent *self,
|
||||
const gchar *email)
|
||||
{
|
||||
GSList *ecal_attendees;
|
||||
GSList *l;
|
||||
|
||||
g_return_if_fail (GCAL_IS_EVENT (self));
|
||||
|
||||
ecal_attendees = e_cal_component_get_attendees (self->component);
|
||||
|
||||
for (l = ecal_attendees; l != NULL; l = l->next)
|
||||
{
|
||||
if (mailto_equal (e_cal_component_attendee_get_value (l->data), email))
|
||||
break;
|
||||
}
|
||||
|
||||
if (l)
|
||||
{
|
||||
e_cal_component_attendee_free (l->data);
|
||||
ecal_attendees = g_slist_delete_link (ecal_attendees, l);
|
||||
|
||||
set_component_attendees (self, ecal_attendees);
|
||||
}
|
||||
|
||||
g_slist_free_full (ecal_attendees, e_cal_component_attendee_free);
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_event_copy_attendees:
|
||||
* @self: a #GcalEvent
|
||||
* @event: the #GcalEvent to copy from
|
||||
*
|
||||
* Replaces the organizer and attendees of @self with those of @event.
|
||||
*/
|
||||
void
|
||||
gcal_event_copy_attendees (GcalEvent *self,
|
||||
GcalEvent *event)
|
||||
{
|
||||
ECalComponentOrganizer *organizer;
|
||||
GSList *ecal_attendees;
|
||||
|
||||
g_return_if_fail (GCAL_IS_EVENT (self));
|
||||
g_return_if_fail (GCAL_IS_EVENT (event));
|
||||
|
||||
organizer = e_cal_component_get_organizer (event->component);
|
||||
|
||||
g_clear_object (&self->organizer);
|
||||
if (organizer)
|
||||
self->organizer = gcal_event_organizer_new (organizer);
|
||||
|
||||
e_cal_component_set_organizer (self->component, organizer);
|
||||
|
||||
ecal_attendees = e_cal_component_get_attendees (event->component);
|
||||
set_component_attendees (self, ecal_attendees);
|
||||
|
||||
g_slist_free_full (ecal_attendees, e_cal_component_attendee_free);
|
||||
e_cal_component_organizer_free (organizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_event_attendees_equal:
|
||||
* @event1: a #GcalEvent
|
||||
* @event2: a #GcalEvent
|
||||
*
|
||||
* Checks if @event1 and @event2 have the same organizer and attendees.
|
||||
* Only the email addresses are compared.
|
||||
*
|
||||
* Returns: %TRUE if the events' organizer and attendees are identical
|
||||
*/
|
||||
gboolean
|
||||
gcal_event_attendees_equal (GcalEvent *event1,
|
||||
GcalEvent *event2)
|
||||
{
|
||||
const gchar *organizer1 = NULL;
|
||||
const gchar *organizer2 = NULL;
|
||||
guint n_items;
|
||||
|
||||
g_assert (GCAL_IS_EVENT (event1) && GCAL_IS_EVENT (event2));
|
||||
|
||||
if (event1->organizer)
|
||||
organizer1 = gcal_event_organizer_get_uri (event1->organizer);
|
||||
if (event2->organizer)
|
||||
organizer2 = gcal_event_organizer_get_uri (event2->organizer);
|
||||
|
||||
if ((organizer1 || organizer2) && !mailto_equal (organizer1, organizer2))
|
||||
return FALSE;
|
||||
|
||||
n_items = g_list_model_get_n_items (G_LIST_MODEL (event1->attendees));
|
||||
|
||||
if (n_items != g_list_model_get_n_items (G_LIST_MODEL (event2->attendees)))
|
||||
return FALSE;
|
||||
|
||||
for (guint i = 0; i < n_items; i++)
|
||||
{
|
||||
g_autoptr (GcalEventAttendee) attendee = g_list_model_get_item (G_LIST_MODEL (event1->attendees), i);
|
||||
|
||||
if (!gcal_event_has_attendee (event2, gcal_event_attendee_get_uri (attendee)))
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,26 @@ GListModel * gcal_event_get_attendees (GcalEvent
|
||||
|
||||
GcalEventOrganizer * gcal_event_get_organizer (GcalEvent *self);
|
||||
|
||||
void gcal_event_set_organizer (GcalEvent *self,
|
||||
const gchar *email,
|
||||
const gchar *name);
|
||||
|
||||
gboolean gcal_event_has_attendee (GcalEvent *self,
|
||||
const gchar *email);
|
||||
|
||||
gboolean gcal_event_add_attendee (GcalEvent *self,
|
||||
const gchar *email,
|
||||
const gchar *name);
|
||||
|
||||
void gcal_event_remove_attendee (GcalEvent *self,
|
||||
const gchar *email);
|
||||
|
||||
void gcal_event_copy_attendees (GcalEvent *self,
|
||||
GcalEvent *event);
|
||||
|
||||
gboolean gcal_event_attendees_equal (GcalEvent *event1,
|
||||
GcalEvent *event2);
|
||||
|
||||
gboolean gcal_event_alarms_equal (GcalEvent *event1,
|
||||
GcalEvent *event2);
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
/* gcal-federated-core.c
|
||||
*
|
||||
* Copyright (C) 2026 David Young <david@federated.computer>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#define G_LOG_DOMAIN "GcalFederatedCore"
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "gcal-federated-core.h"
|
||||
|
||||
#include <libedataserver/libedataserver.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* SECTION:gcal-federated-core
|
||||
* @short_description: Federated Computer Core integration
|
||||
* @title:GcalFederatedCore
|
||||
*
|
||||
* When CoreDesktop's `configure` has connected this computer to a Federated
|
||||
* Computer Core, each Core is described in ~/.config/coredesktop/cores/<domain>.conf
|
||||
* and its Nextcloud calendars are added through GNOME Online Accounts.
|
||||
*
|
||||
* Nextcloud schedules events on the server: when an event whose organizer
|
||||
* is the calendar's owner is saved with attendees, Nextcloud emails the
|
||||
* invitations, updates and cancellations. So inviting people to an event on
|
||||
* a Core calendar only takes setting its organizer and attendees.
|
||||
*/
|
||||
|
||||
typedef struct
|
||||
{
|
||||
gchar *domain;
|
||||
gchar *email;
|
||||
gchar *name;
|
||||
} CoreConf;
|
||||
|
||||
static void
|
||||
core_conf_free (CoreConf *conf)
|
||||
{
|
||||
g_free (conf->domain);
|
||||
g_free (conf->email);
|
||||
g_free (conf->name);
|
||||
g_free (conf);
|
||||
}
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (CoreConf, core_conf_free)
|
||||
|
||||
/*
|
||||
* The files are written by bash with printf %q, e.g. CORE_NAME=David\ Young,
|
||||
* which g_shell_unquote() reads.
|
||||
*/
|
||||
static CoreConf *
|
||||
load_core_conf (const gchar *path)
|
||||
{
|
||||
g_autoptr (CoreConf) conf = NULL;
|
||||
g_autofree gchar *contents = NULL;
|
||||
g_auto (GStrv) lines = NULL;
|
||||
|
||||
if (!g_file_get_contents (path, &contents, NULL, NULL))
|
||||
return NULL;
|
||||
|
||||
conf = g_new0 (CoreConf, 1);
|
||||
lines = g_strsplit (contents, "\n", -1);
|
||||
|
||||
for (gint i = 0; lines[i]; i++)
|
||||
{
|
||||
g_autofree gchar *value = NULL;
|
||||
gchar *equals;
|
||||
gchar **field;
|
||||
|
||||
equals = strchr (lines[i], '=');
|
||||
if (!equals)
|
||||
continue;
|
||||
|
||||
*equals = '\0';
|
||||
|
||||
if (g_str_equal (lines[i], "CORE_DOMAIN"))
|
||||
field = &conf->domain;
|
||||
else if (g_str_equal (lines[i], "CORE_EMAIL"))
|
||||
field = &conf->email;
|
||||
else if (g_str_equal (lines[i], "CORE_NAME"))
|
||||
field = &conf->name;
|
||||
else
|
||||
continue;
|
||||
|
||||
value = g_shell_unquote (equals + 1, NULL);
|
||||
if (value)
|
||||
g_set_str (field, value);
|
||||
}
|
||||
|
||||
if (!conf->domain || !*conf->domain || !conf->email || !strchr (conf->email, '@'))
|
||||
return NULL;
|
||||
|
||||
return g_steal_pointer (&conf);
|
||||
}
|
||||
|
||||
static GPtrArray *
|
||||
load_cores (const gchar *config_dir)
|
||||
{
|
||||
g_autofree gchar *cores_dir = NULL;
|
||||
g_autoptr (GPtrArray) cores = NULL;
|
||||
g_autoptr (GDir) dir = NULL;
|
||||
const gchar *filename;
|
||||
|
||||
cores = g_ptr_array_new_with_free_func ((GDestroyNotify) core_conf_free);
|
||||
cores_dir = g_build_filename (config_dir, "cores", NULL);
|
||||
dir = g_dir_open (cores_dir, 0, NULL);
|
||||
|
||||
while (dir && (filename = g_dir_read_name (dir)) != NULL)
|
||||
{
|
||||
g_autofree gchar *path = NULL;
|
||||
CoreConf *conf;
|
||||
|
||||
if (!g_str_has_suffix (filename, ".conf"))
|
||||
continue;
|
||||
|
||||
path = g_build_filename (cores_dir, filename, NULL);
|
||||
conf = load_core_conf (path);
|
||||
|
||||
if (conf)
|
||||
g_ptr_array_add (cores, conf);
|
||||
}
|
||||
|
||||
/* Before CoreDesktop supported several Cores, there was only core.conf */
|
||||
if (cores->len == 0)
|
||||
{
|
||||
g_autofree gchar *path = g_build_filename (config_dir, "core.conf", NULL);
|
||||
CoreConf *conf = load_core_conf (path);
|
||||
|
||||
if (conf)
|
||||
g_ptr_array_add (cores, conf);
|
||||
}
|
||||
|
||||
return g_steal_pointer (&cores);
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_federated_core_find_core:
|
||||
* @config_dir: CoreDesktop's configuration directory
|
||||
* @host: the host name of a CalDAV server
|
||||
* @out_email: (out) (optional): return location for the Core email
|
||||
* @out_name: (out) (optional): return location for the name
|
||||
*
|
||||
* Looks for the Core whose Nextcloud is at @host.
|
||||
*
|
||||
* Returns: %TRUE if @host is the Nextcloud of a Core set up on this computer
|
||||
*/
|
||||
gboolean
|
||||
gcal_federated_core_find_core (const gchar *config_dir,
|
||||
const gchar *host,
|
||||
gchar **out_email,
|
||||
gchar **out_name)
|
||||
{
|
||||
g_autoptr (GPtrArray) cores = NULL;
|
||||
|
||||
g_return_val_if_fail (config_dir != NULL, FALSE);
|
||||
|
||||
if (!host)
|
||||
return FALSE;
|
||||
|
||||
cores = load_cores (config_dir);
|
||||
|
||||
for (guint i = 0; i < cores->len; i++)
|
||||
{
|
||||
CoreConf *conf = g_ptr_array_index (cores, i);
|
||||
g_autofree gchar *nextcloud_host = g_strconcat ("nextcloud.", conf->domain, NULL);
|
||||
|
||||
if (g_ascii_strcasecmp (host, nextcloud_host) != 0)
|
||||
continue;
|
||||
|
||||
if (out_email)
|
||||
*out_email = g_strdup (conf->email);
|
||||
if (out_name)
|
||||
*out_name = g_strdup (conf->name);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_federated_core_get_organizer:
|
||||
* @calendar: a #GcalCalendar
|
||||
* @out_email: (out) (optional): return location for the organizer's email
|
||||
* @out_name: (out) (optional): return location for the organizer's name
|
||||
*
|
||||
* Checks whether @calendar is on the Nextcloud of a Federated Computer Core
|
||||
* set up on this computer, where people can be invited to events, and who
|
||||
* the organizer of those events is.
|
||||
*
|
||||
* Returns: %TRUE if people can be invited to events in @calendar
|
||||
*/
|
||||
gboolean
|
||||
gcal_federated_core_get_organizer (GcalCalendar *calendar,
|
||||
gchar **out_email,
|
||||
gchar **out_name)
|
||||
{
|
||||
#ifdef ENABLE_FEDERATED_CORE
|
||||
g_autofree gchar *config_dir = NULL;
|
||||
g_autofree gchar *server_email = NULL;
|
||||
g_autofree gchar *core_email = NULL;
|
||||
g_autofree gchar *core_name = NULL;
|
||||
g_autoptr (GUri) uri = NULL;
|
||||
ESourceWebdav *webdav;
|
||||
ESource *source;
|
||||
|
||||
g_return_val_if_fail (GCAL_IS_CALENDAR (calendar), FALSE);
|
||||
|
||||
if (gcal_calendar_is_read_only (calendar))
|
||||
return FALSE;
|
||||
|
||||
source = gcal_calendar_get_source (calendar);
|
||||
if (!source || !e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND))
|
||||
return FALSE;
|
||||
|
||||
webdav = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
|
||||
uri = e_source_webdav_dup_uri (webdav);
|
||||
|
||||
/* CoreDesktop writes to ~/.config, even where XDG_CONFIG_HOME is elsewhere (e.g. Flatpak) */
|
||||
config_dir = g_build_filename (g_get_home_dir (), ".config", "coredesktop", NULL);
|
||||
|
||||
if (!uri || !gcal_federated_core_find_core (config_dir, g_uri_get_host (uri), &core_email, &core_name))
|
||||
return FALSE;
|
||||
|
||||
/*
|
||||
* Nextcloud only sends invitations when the organizer is one of the
|
||||
* owner's addresses. The CalDAV server says which address that is, so
|
||||
* prefer it to the Core email.
|
||||
*/
|
||||
server_email = e_source_webdav_dup_email_address (webdav);
|
||||
|
||||
if (out_email)
|
||||
*out_email = (server_email && *server_email) ? g_steal_pointer (&server_email) : g_steal_pointer (&core_email);
|
||||
if (out_name)
|
||||
*out_name = g_steal_pointer (&core_name);
|
||||
|
||||
return TRUE;
|
||||
#else
|
||||
return FALSE;
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/* gcal-federated-core.h
|
||||
*
|
||||
* Copyright (C) 2026 David Young <david@federated.computer>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "gcal-calendar.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
gboolean gcal_federated_core_get_organizer (GcalCalendar *calendar,
|
||||
gchar **out_email,
|
||||
gchar **out_name);
|
||||
|
||||
gboolean gcal_federated_core_find_core (const gchar *config_dir,
|
||||
const gchar *host,
|
||||
gchar **out_email,
|
||||
gchar **out_name);
|
||||
|
||||
G_END_DECLS
|
||||
@@ -0,0 +1,5 @@
|
||||
calendar_incs += include_directories('.')
|
||||
|
||||
sources += files(
|
||||
'gcal-federated-core.c',
|
||||
)
|
||||
@@ -54,6 +54,8 @@ struct _GcalAttendeeDetailsPage
|
||||
GtkWidget *group_not_responded;
|
||||
|
||||
GcalEventAttendeeTypeFilterFlags attendee_type_filter_flags;
|
||||
|
||||
gboolean removable;
|
||||
};
|
||||
|
||||
enum
|
||||
@@ -64,6 +66,7 @@ enum
|
||||
PROP_MODEL_DELEGATED,
|
||||
PROP_MODEL_NOT_RESPONDED,
|
||||
PROP_ATTENDEE_TYPE,
|
||||
PROP_REMOVABLE,
|
||||
N_PROPS
|
||||
};
|
||||
|
||||
@@ -83,7 +86,11 @@ static GtkWidget *
|
||||
create_attendee_row_widget (void *attendee,
|
||||
void *user_data)
|
||||
{
|
||||
return gcal_attendee_row_new (attendee);
|
||||
GtkWidget *row = gcal_attendee_row_new (attendee);
|
||||
|
||||
g_object_bind_property (user_data, "removable", row, "removable", G_BINDING_SYNC_CREATE);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -237,6 +244,9 @@ gcal_attendee_details_page_get_property (GObject *object,
|
||||
case PROP_ATTENDEE_TYPE:
|
||||
g_value_set_flags (value, self->attendee_type_filter_flags);
|
||||
break;
|
||||
case PROP_REMOVABLE:
|
||||
g_value_set_boolean (value, self->removable);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
}
|
||||
@@ -255,6 +265,9 @@ gcal_attendee_details_page_set_property (GObject *object,
|
||||
case PROP_ATTENDEE_TYPE:
|
||||
set_type_filter_flags (self, g_value_get_flags (value));
|
||||
break;
|
||||
case PROP_REMOVABLE:
|
||||
gcal_attendee_details_page_set_removable (self, g_value_get_boolean (value));
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
}
|
||||
@@ -275,27 +288,27 @@ gcal_attendee_details_page_init (GcalAttendeeDetailsPage *instance)
|
||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_accepted),
|
||||
G_LIST_MODEL (instance->filter_list_accepted),
|
||||
create_attendee_row_widget,
|
||||
NULL, NULL);
|
||||
instance, NULL);
|
||||
|
||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_tentative),
|
||||
G_LIST_MODEL (instance->filter_list_tentative),
|
||||
create_attendee_row_widget,
|
||||
NULL, NULL);
|
||||
instance, NULL);
|
||||
|
||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_declined),
|
||||
G_LIST_MODEL (instance->filter_list_declined),
|
||||
create_attendee_row_widget,
|
||||
NULL, NULL);
|
||||
instance, NULL);
|
||||
|
||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_delegated),
|
||||
G_LIST_MODEL (instance->filter_list_delegated),
|
||||
create_attendee_row_widget,
|
||||
NULL, NULL);
|
||||
instance, NULL);
|
||||
|
||||
adw_preferences_group_bind_model (ADW_PREFERENCES_GROUP (instance->group_not_responded),
|
||||
G_LIST_MODEL (instance->filter_list_not_responded),
|
||||
create_attendee_row_widget,
|
||||
NULL, NULL);
|
||||
instance, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -370,6 +383,16 @@ gcal_attendee_details_page_class_init (GcalAttendeeDetailsPageClass *klass)
|
||||
GCAL_EVENT_ATTENDEE_TYPE_FILTER_NONE,
|
||||
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
|
||||
|
||||
/**
|
||||
* GcalAttendeeDetailsPage:removable:
|
||||
*
|
||||
* Whether attendees can be removed from the event.
|
||||
*/
|
||||
properties[PROP_REMOVABLE] =
|
||||
g_param_spec_boolean ("removable", NULL, NULL,
|
||||
FALSE,
|
||||
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
|
||||
|
||||
g_object_class_install_properties (object_class, N_PROPS, properties);
|
||||
|
||||
/* widget */
|
||||
@@ -420,3 +443,24 @@ gcal_attendee_details_page_set_type_filter (GcalAttendeeDetailsPage *sel
|
||||
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
|
||||
set_type_filter_flags (self, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_attendee_details_page_set_removable:
|
||||
* @self: a #GcalAttendeeDetailsPage instance.
|
||||
* @removable: whether attendees can be removed
|
||||
*
|
||||
* Sets whether each attendee is shown with a button that removes them
|
||||
* from the event.
|
||||
*/
|
||||
void
|
||||
gcal_attendee_details_page_set_removable (GcalAttendeeDetailsPage *self,
|
||||
gboolean removable)
|
||||
{
|
||||
g_assert (GCAL_IS_ATTENDEE_DETAILS_PAGE (self));
|
||||
|
||||
if (self->removable == removable)
|
||||
return;
|
||||
|
||||
self->removable = removable;
|
||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_REMOVABLE]);
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ void gcal_attendee_details_page_set_attendees (GcalAtte
|
||||
void gcal_attendee_details_page_set_type_filter (GcalAttendeeDetailsPage *self,
|
||||
GcalEventAttendeeTypeFilterFlags flags);
|
||||
|
||||
void gcal_attendee_details_page_set_removable (GcalAttendeeDetailsPage *self,
|
||||
gboolean removable);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,6 +12,18 @@ template $GcalAttendeeRow: Adw.ActionRow {
|
||||
"dimmed",
|
||||
]
|
||||
}
|
||||
|
||||
[suffix]
|
||||
Button remove_button {
|
||||
visible: bind template.removable;
|
||||
valign: center;
|
||||
icon-name: "list-remove-symbolic";
|
||||
tooltip-text: _("Remove Participant");
|
||||
|
||||
styles [
|
||||
"flat",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
PopoverMenu context_menu {
|
||||
|
||||
@@ -34,10 +34,13 @@ struct _GcalAttendeeRow
|
||||
GcalEventAttendee *attendee;
|
||||
|
||||
GtkWidget *role;
|
||||
GtkWidget *remove_button;
|
||||
GtkWidget *context_menu;
|
||||
GtkGestureClick *gesture_click;
|
||||
|
||||
GSimpleActionGroup *action_group;
|
||||
|
||||
gboolean removable;
|
||||
};
|
||||
|
||||
G_DEFINE_FINAL_TYPE (GcalAttendeeRow, gcal_attendee_row, ADW_TYPE_ACTION_ROW);
|
||||
@@ -46,6 +49,7 @@ enum
|
||||
{
|
||||
PROP_0,
|
||||
PROP_ATTENDEE,
|
||||
PROP_REMOVABLE,
|
||||
N_PROPS,
|
||||
};
|
||||
|
||||
@@ -164,6 +168,16 @@ gcal_attendee_row_set_property (GObject *object,
|
||||
g_assert (self->attendee == NULL);
|
||||
self->attendee = g_value_get_object (value);
|
||||
set_up_row_content (self);
|
||||
gtk_actionable_set_action_target (GTK_ACTIONABLE (self->remove_button), "s",
|
||||
gcal_event_attendee_get_uri (self->attendee) ? gcal_event_attendee_get_uri (self->attendee) : "");
|
||||
gtk_actionable_set_action_name (GTK_ACTIONABLE (self->remove_button), "event-editor.remove-attendee");
|
||||
break;
|
||||
case PROP_REMOVABLE:
|
||||
if (self->removable != g_value_get_boolean (value))
|
||||
{
|
||||
self->removable = g_value_get_boolean (value);
|
||||
g_object_notify_by_pspec (object, pspec);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
@@ -183,6 +197,9 @@ gcal_attendee_row_get_property (GObject *object,
|
||||
case PROP_ATTENDEE:
|
||||
g_value_set_object (value, self->attendee);
|
||||
break;
|
||||
case PROP_REMOVABLE:
|
||||
g_value_set_boolean (value, self->removable);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
||||
}
|
||||
@@ -209,12 +226,23 @@ gcal_attendee_row_class_init (GcalAttendeeRowClass *klass)
|
||||
GCAL_TYPE_EVENT_ATTENDEE,
|
||||
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
|
||||
|
||||
/**
|
||||
* GcalAttendeeRow:removable:
|
||||
*
|
||||
* Whether the row offers to remove the attendee from the event.
|
||||
*/
|
||||
properties[PROP_REMOVABLE] =
|
||||
g_param_spec_boolean ("removable", NULL, NULL,
|
||||
FALSE,
|
||||
G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
|
||||
|
||||
g_object_class_install_properties (object_class, N_PROPS, properties);
|
||||
|
||||
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendee-row.ui");
|
||||
|
||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, context_menu);
|
||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, gesture_click);
|
||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, remove_button);
|
||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeeRow, role);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,16 @@ template $GcalAttendeesSection: $GcalEventEditorSection {
|
||||
filter-flags: GCAL_EVENT_ATTENDEE_TYPE_FILTER_PERSON;
|
||||
}
|
||||
|
||||
Adw.EntryRow invite_row {
|
||||
title: _("Invite by Email");
|
||||
visible: bind template.can-invite;
|
||||
input-purpose: email;
|
||||
show-apply-button: true;
|
||||
apply => $on_invite_row_apply_cb(template) not-swapped;
|
||||
entry-activated => $on_invite_row_apply_cb(template) not-swapped;
|
||||
changed => $on_invite_row_changed_cb(template) not-swapped;
|
||||
}
|
||||
|
||||
$GcalAttendeeSummaryRow {
|
||||
title: _("Resources");
|
||||
attendees: bind template.attendees;
|
||||
|
||||
@@ -27,17 +27,25 @@
|
||||
#include "gcal-event-organizer.h"
|
||||
#include "gcal-event.h"
|
||||
#include "gcal-organizer-row.h"
|
||||
#include "gcal-utils.h"
|
||||
#include "glib-object.h"
|
||||
|
||||
struct _GcalAttendeesSection
|
||||
{
|
||||
GcalEventEditorSection parent_instance;
|
||||
|
||||
GcalEvent *event;
|
||||
GcalEventOrganizer *organizer;
|
||||
|
||||
GtkWidget *invite_row;
|
||||
GtkWidget *organizer_row;
|
||||
AdwActionRow *summary_row;
|
||||
|
||||
GListModel *attendees;
|
||||
|
||||
/* Who invitations come from; NULL when people can't be invited */
|
||||
gchar *inviter_email;
|
||||
gchar *inviter_name;
|
||||
};
|
||||
|
||||
G_DEFINE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL_TYPE_EVENT_EDITOR_SECTION)
|
||||
@@ -46,6 +54,7 @@ enum
|
||||
{
|
||||
PROP_0,
|
||||
PROP_ATTENDEES,
|
||||
PROP_CAN_INVITE,
|
||||
PROP_ORGANIZER,
|
||||
N_PROPS
|
||||
};
|
||||
@@ -53,6 +62,97 @@ enum
|
||||
static GParamSpec *properties[N_PROPS] = { NULL, };
|
||||
|
||||
|
||||
/*
|
||||
* Auxiliary methods
|
||||
*/
|
||||
|
||||
static void
|
||||
update_from_event (GcalAttendeesSection *self)
|
||||
{
|
||||
self->organizer = self->event ? gcal_event_get_organizer (self->event) : NULL;
|
||||
self->attendees = self->event ? gcal_event_get_attendees (self->event) : NULL;
|
||||
|
||||
gtk_widget_set_visible (self->organizer_row, self->organizer != NULL);
|
||||
|
||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ATTENDEES]);
|
||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ORGANIZER]);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
invite (GcalAttendeesSection *self,
|
||||
const gchar *text)
|
||||
{
|
||||
g_autofree gchar *email = NULL;
|
||||
g_autofree gchar *name = NULL;
|
||||
|
||||
if (!gcal_util_parse_email_address (text, &email, &name))
|
||||
return FALSE;
|
||||
|
||||
/* The organizer doesn't invite themselves */
|
||||
if (g_ascii_strcasecmp (email, self->inviter_email) == 0)
|
||||
return TRUE;
|
||||
|
||||
if (!gcal_event_get_organizer (self->event))
|
||||
gcal_event_set_organizer (self->event, self->inviter_email, self->inviter_name);
|
||||
|
||||
gcal_event_add_attendee (self->event, email, name);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Callbacks
|
||||
*/
|
||||
|
||||
static void
|
||||
on_invite_row_apply_cb (AdwEntryRow *row,
|
||||
GcalAttendeesSection *self)
|
||||
{
|
||||
g_autoptr (GStrvBuilder) not_invited = NULL;
|
||||
g_auto (GStrv) addresses = NULL;
|
||||
g_auto (GStrv) remaining = NULL;
|
||||
g_autofree gchar *remaining_text = NULL;
|
||||
|
||||
if (!self->event || !self->inviter_email)
|
||||
return;
|
||||
|
||||
/* Several addresses can be pasted at once */
|
||||
addresses = g_strsplit_set (gtk_editable_get_text (GTK_EDITABLE (row)), ",;\n", -1);
|
||||
not_invited = g_strv_builder_new ();
|
||||
|
||||
for (gint i = 0; addresses[i]; i++)
|
||||
{
|
||||
g_strstrip (addresses[i]);
|
||||
|
||||
if (*addresses[i] && !invite (self, addresses[i]))
|
||||
g_strv_builder_add (not_invited, addresses[i]);
|
||||
}
|
||||
|
||||
/* Leave what isn't an email address, so it can be corrected */
|
||||
remaining = g_strv_builder_end (not_invited);
|
||||
remaining_text = g_strjoinv (", ", remaining);
|
||||
|
||||
gtk_editable_set_text (GTK_EDITABLE (row), remaining_text);
|
||||
|
||||
if (*remaining_text)
|
||||
gtk_widget_add_css_class (GTK_WIDGET (row), "error");
|
||||
}
|
||||
|
||||
static void
|
||||
on_invite_row_changed_cb (AdwEntryRow *row,
|
||||
GcalAttendeesSection *self)
|
||||
{
|
||||
gtk_widget_remove_css_class (GTK_WIDGET (row), "error");
|
||||
}
|
||||
|
||||
static void
|
||||
on_attendees_changed_cb (GcalAttendeesSection *self)
|
||||
{
|
||||
update_from_event (self);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* GcalEventEditorSection overrides
|
||||
*/
|
||||
@@ -63,19 +163,23 @@ gcal_attendees_section_event_set_cb (GcalEventEditorSection *section,
|
||||
{
|
||||
GcalAttendeesSection *self = GCAL_ATTENDEES_SECTION (section);
|
||||
|
||||
gtk_editable_set_text (GTK_EDITABLE (self->invite_row), "");
|
||||
|
||||
if (self->event)
|
||||
g_signal_handlers_disconnect_by_func (gcal_event_get_attendees (self->event), on_attendees_changed_cb, self);
|
||||
|
||||
g_set_weak_pointer (&self->event, event);
|
||||
|
||||
if (event)
|
||||
{
|
||||
self->organizer = gcal_event_get_organizer (event);
|
||||
self->attendees = gcal_event_get_attendees (event);
|
||||
}
|
||||
else
|
||||
{
|
||||
self->organizer = NULL;
|
||||
self->attendees = NULL;
|
||||
g_signal_connect_object (gcal_event_get_attendees (event),
|
||||
"items-changed",
|
||||
G_CALLBACK (on_attendees_changed_cb),
|
||||
self,
|
||||
G_CONNECT_SWAPPED);
|
||||
}
|
||||
|
||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ATTENDEES]);
|
||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ORGANIZER]);
|
||||
update_from_event (self);
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +201,10 @@ gcal_attendees_section_get_property (GObject *object,
|
||||
g_value_set_object (value, self->attendees);
|
||||
break;
|
||||
|
||||
case PROP_CAN_INVITE:
|
||||
g_value_set_boolean (value, self->inviter_email != NULL);
|
||||
break;
|
||||
|
||||
case PROP_ORGANIZER:
|
||||
g_value_set_object (value, self->organizer);
|
||||
break;
|
||||
@@ -132,6 +240,13 @@ gcal_attendees_section_finalize (GObject *object)
|
||||
|
||||
self->attendees = NULL; /* not owned */
|
||||
|
||||
if (self->event)
|
||||
g_signal_handlers_disconnect_by_func (gcal_event_get_attendees (self->event), on_attendees_changed_cb, self);
|
||||
g_clear_weak_pointer (&self->event);
|
||||
|
||||
g_clear_pointer (&self->inviter_email, g_free);
|
||||
g_clear_pointer (&self->inviter_name, g_free);
|
||||
|
||||
G_OBJECT_CLASS (gcal_attendees_section_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
@@ -168,6 +283,17 @@ gcal_attendees_section_class_init (GcalAttendeesSectionClass *klass)
|
||||
G_TYPE_LIST_MODEL,
|
||||
G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
|
||||
|
||||
/**
|
||||
* GcalAttendeesSection:can-invite:
|
||||
*
|
||||
* Whether people can be invited to the event.
|
||||
* See gcal_attendees_section_set_inviter().
|
||||
*/
|
||||
properties[PROP_CAN_INVITE] =
|
||||
g_param_spec_boolean ("can-invite", NULL, NULL,
|
||||
FALSE,
|
||||
G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
|
||||
|
||||
/**
|
||||
* GcalAttendeesSection:organizer:
|
||||
*
|
||||
@@ -183,5 +309,37 @@ gcal_attendees_section_class_init (GcalAttendeesSectionClass *klass)
|
||||
|
||||
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-attendees-section.ui");
|
||||
|
||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeesSection, invite_row);
|
||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeesSection, organizer_row);
|
||||
gtk_widget_class_bind_template_child (widget_class, GcalAttendeesSection, summary_row);
|
||||
|
||||
gtk_widget_class_bind_template_callback (widget_class, on_invite_row_apply_cb);
|
||||
gtk_widget_class_bind_template_callback (widget_class, on_invite_row_changed_cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_attendees_section_set_inviter:
|
||||
* @self: a #GcalAttendeesSection
|
||||
* @email: (nullable): the email address invitations come from
|
||||
* @name: (nullable): the name invitations come from
|
||||
*
|
||||
* Lets people be invited to the event, on behalf of @email, who becomes
|
||||
* its organizer. Passing %NULL as @email stops people from being invited.
|
||||
*/
|
||||
void
|
||||
gcal_attendees_section_set_inviter (GcalAttendeesSection *self,
|
||||
const gchar *email,
|
||||
const gchar *name)
|
||||
{
|
||||
gboolean could_invite;
|
||||
|
||||
g_return_if_fail (GCAL_IS_ATTENDEES_SECTION (self));
|
||||
|
||||
could_invite = self->inviter_email != NULL;
|
||||
|
||||
g_set_str (&self->inviter_email, email);
|
||||
g_set_str (&self->inviter_name, email ? name : NULL);
|
||||
|
||||
if (could_invite != (self->inviter_email != NULL))
|
||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CAN_INVITE]);
|
||||
}
|
||||
|
||||
@@ -27,4 +27,8 @@ G_BEGIN_DECLS
|
||||
#define GCAL_TYPE_ATTENDEES_SECTION (gcal_attendees_section_get_type ())
|
||||
G_DECLARE_FINAL_TYPE (GcalAttendeesSection, gcal_attendees_section, GCAL, ATTENDEES_SECTION, GcalEventEditorSection)
|
||||
|
||||
void gcal_attendees_section_set_inviter (GcalAttendeesSection *self,
|
||||
const gchar *email,
|
||||
const gchar *name);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "gcal-event-editor-dialog.h"
|
||||
#include "gcal-event-editor-section.h"
|
||||
#include "gcal-event.h"
|
||||
#include "gcal-federated-core.h"
|
||||
#include "gcal-notes-section.h"
|
||||
#include "gcal-reminders-section.h"
|
||||
#include "gcal-schedule-section.h"
|
||||
@@ -128,6 +129,48 @@ set_writable (GcalEventEditorDialog *self,
|
||||
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WRITABLE]);
|
||||
}
|
||||
|
||||
/*
|
||||
* People can be invited to events on the calendars of a Federated Computer
|
||||
* Core, which sends the invitations, unless someone else organizes the event.
|
||||
*/
|
||||
static void
|
||||
update_invite_state (GcalEventEditorDialog *self)
|
||||
{
|
||||
g_autofree gchar *email = NULL;
|
||||
g_autofree gchar *name = NULL;
|
||||
GcalEventOrganizer *organizer;
|
||||
GcalCalendar *calendar;
|
||||
gboolean can_invite = FALSE;
|
||||
|
||||
if (!self->edited_event)
|
||||
return;
|
||||
|
||||
calendar = gcal_calendar_combo_row_get_calendar (self->calendar_combo_row);
|
||||
if (!calendar)
|
||||
calendar = gcal_event_get_calendar (self->edited_event);
|
||||
|
||||
organizer = gcal_event_get_organizer (self->edited_event);
|
||||
|
||||
if (self->writable && calendar && gcal_federated_core_get_organizer (calendar, &email, &name))
|
||||
{
|
||||
g_autofree const gchar *organizer_email = NULL;
|
||||
|
||||
if (organizer)
|
||||
organizer_email = gcal_util_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer));
|
||||
|
||||
can_invite = !organizer_email || g_ascii_strcasecmp (organizer_email, email) == 0;
|
||||
}
|
||||
|
||||
gcal_attendees_section_set_inviter (GCAL_ATTENDEES_SECTION (self->attendees_section),
|
||||
can_invite ? email : NULL,
|
||||
can_invite ? name : NULL);
|
||||
|
||||
gcal_attendee_details_page_set_removable (GCAL_ATTENDEE_DETAILS_PAGE (self->attendee_details_page), can_invite);
|
||||
gtk_widget_action_set_enabled (GTK_WIDGET (self), "event-editor.remove-attendee", can_invite);
|
||||
|
||||
gtk_widget_set_visible (GTK_WIDGET (self->attendees_section), organizer != NULL || can_invite);
|
||||
}
|
||||
|
||||
static void
|
||||
set_event (GcalEventEditorDialog *self,
|
||||
GcalEvent *event,
|
||||
@@ -165,8 +208,7 @@ set_event (GcalEventEditorDialog *self,
|
||||
gcal_calendar_combo_row_set_calendar (self->calendar_combo_row, calendar);
|
||||
|
||||
set_writable (self, !gcal_calendar_is_read_only (calendar));
|
||||
gboolean has_participants = gcal_event_get_organizer (self->edited_event) != NULL;
|
||||
gtk_widget_set_visible (GTK_WIDGET (self->attendees_section), has_participants);
|
||||
update_invite_state (self);
|
||||
|
||||
gtk_widget_set_visible (self->cancel_button, self->writable);
|
||||
|
||||
@@ -235,6 +277,9 @@ apply_event_properties_to_template_event (GcalEvent *template_event,
|
||||
if (alarm)
|
||||
gcal_event_add_alarm (template_event, alarm);
|
||||
}
|
||||
|
||||
if (!gcal_event_attendees_equal (template_event, event))
|
||||
gcal_event_copy_attendees (template_event, event);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -275,7 +320,7 @@ save_event (GcalEventEditorDialog *self,
|
||||
GcalContext *context;
|
||||
GcalManager *manager;
|
||||
|
||||
gboolean calendar_equal, alarms_equal, schedule_equal, strings_equal, event_equal;
|
||||
gboolean calendar_equal, alarms_equal, schedule_equal, strings_equal, attendees_equal, event_equal;
|
||||
|
||||
GCAL_ENTRY;
|
||||
|
||||
@@ -284,11 +329,20 @@ save_event (GcalEventEditorDialog *self,
|
||||
|
||||
selected_calendar = gcal_calendar_combo_row_get_calendar (self->calendar_combo_row);
|
||||
|
||||
/* Everyone invited here was removed again, so there's nothing to organize */
|
||||
if (!gcal_event_get_organizer (self->event) &&
|
||||
gcal_event_get_organizer (self->edited_event) &&
|
||||
g_list_model_get_n_items (gcal_event_get_attendees (self->edited_event)) == 0)
|
||||
{
|
||||
gcal_event_set_organizer (self->edited_event, NULL, NULL);
|
||||
}
|
||||
|
||||
calendar_equal = selected_calendar && calendar == selected_calendar;
|
||||
alarms_equal = gcal_event_alarms_equal (self->event, self->edited_event);
|
||||
schedule_equal = gcal_event_schedule_equal (self->event, self->edited_event);
|
||||
strings_equal = gcal_event_strings_equal (self->event, self->edited_event);
|
||||
event_equal = calendar_equal && alarms_equal && schedule_equal && strings_equal;
|
||||
attendees_equal = gcal_event_attendees_equal (self->event, self->edited_event);
|
||||
event_equal = calendar_equal && alarms_equal && schedule_equal && strings_equal && attendees_equal;
|
||||
|
||||
if (self->event_is_new)
|
||||
{
|
||||
@@ -357,6 +411,25 @@ on_show_attendees_detail_page_action_activated_cb (GtkWidget *widget,
|
||||
ADW_NAVIGATION_PAGE (self->attendee_details_page));
|
||||
}
|
||||
|
||||
static void
|
||||
on_remove_attendee_action_activated_cb (GtkWidget *widget,
|
||||
const char *action_name,
|
||||
GVariant *param)
|
||||
{
|
||||
GcalEventEditorDialog *self = GCAL_EVENT_EDITOR_DIALOG (widget);
|
||||
|
||||
gcal_event_remove_attendee (self->edited_event, g_variant_get_string (param, NULL));
|
||||
|
||||
if (g_list_model_get_n_items (self->attendees_model) == 0)
|
||||
adw_navigation_view_pop (ADW_NAVIGATION_VIEW (self->nav_view));
|
||||
}
|
||||
|
||||
static void
|
||||
on_calendar_selected_cb (GcalEventEditorDialog *self)
|
||||
{
|
||||
update_invite_state (self);
|
||||
}
|
||||
|
||||
static void
|
||||
on_cancel_button_clicked_cb (GtkButton *button,
|
||||
GcalEventEditorDialog *self)
|
||||
@@ -638,6 +711,8 @@ gcal_event_editor_dialog_class_init (GcalEventEditorDialogClass *klass)
|
||||
gtk_widget_class_install_action (widget_class, "event-editor.save", NULL, on_event_editor_save_action_activated_cb);
|
||||
gtk_widget_class_install_action (widget_class, "event-editor.show-attendees-detail-page", "u",
|
||||
on_show_attendees_detail_page_action_activated_cb);
|
||||
gtk_widget_class_install_action (widget_class, "event-editor.remove-attendee", "s",
|
||||
on_remove_attendee_action_activated_cb);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -648,6 +723,7 @@ gcal_event_editor_dialog_init (GcalEventEditorDialog *self)
|
||||
set_up_context (self);
|
||||
|
||||
g_signal_connect_swapped (self, "validation-requested", G_CALLBACK (on_validation_requested_cb), self);
|
||||
g_signal_connect_swapped (self->calendar_combo_row, "notify::selected-item", G_CALLBACK (on_calendar_selected_cb), self);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -89,6 +89,10 @@ generate_subtitle (GcalOrganizerRow *self,
|
||||
return NULL;
|
||||
|
||||
email = gcal_util_get_email_from_mailto_uri (gcal_event_organizer_get_uri (organizer));
|
||||
|
||||
if (!gcal_event_organizer_get_name (organizer))
|
||||
return g_strdup (email);
|
||||
|
||||
name_with_email = g_strdup_printf ("%s (%s)", gcal_event_organizer_get_name (organizer), email);
|
||||
|
||||
return g_strdup (name_with_email);
|
||||
|
||||
@@ -75,6 +75,7 @@ sources = files()
|
||||
subdir('appdata')
|
||||
subdir('contrib')
|
||||
subdir('core')
|
||||
subdir('federated')
|
||||
subdir('gui')
|
||||
subdir('search')
|
||||
subdir('theme')
|
||||
|
||||
@@ -1081,6 +1081,93 @@ gcal_util_get_email_from_mailto_uri (const gchar *mailto_uri)
|
||||
return address;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
is_valid_email (const gchar *email)
|
||||
{
|
||||
const gchar *at;
|
||||
const gchar *domain;
|
||||
|
||||
at = strchr (email, '@');
|
||||
if (!at || at == email || strchr (at + 1, '@'))
|
||||
return FALSE;
|
||||
|
||||
domain = at + 1;
|
||||
if (!strchr (domain, '.') || *domain == '.' || g_str_has_suffix (domain, "."))
|
||||
return FALSE;
|
||||
|
||||
for (const gchar *c = email; *c; c++)
|
||||
{
|
||||
if (g_ascii_isspace (*c) || strchr ("<>()[],;:\\\"", *c))
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_util_parse_email_address:
|
||||
* @text: an address as typed, e.g. "ana@example.com" or "Ana Lima <ana@example.com>"
|
||||
* @out_email: (out) (optional): return location for the email address
|
||||
* @out_name: (out) (optional) (nullable): return location for the name, if any
|
||||
*
|
||||
* Reads an email address, optionally with a name in front of it.
|
||||
*
|
||||
* Returns: %TRUE if @text is an email address
|
||||
*/
|
||||
gboolean
|
||||
gcal_util_parse_email_address (const gchar *text,
|
||||
gchar **out_email,
|
||||
gchar **out_name)
|
||||
{
|
||||
g_autofree gchar *stripped = NULL;
|
||||
g_autofree gchar *email = NULL;
|
||||
g_autofree gchar *name = NULL;
|
||||
gchar *open_bracket;
|
||||
|
||||
if (!text)
|
||||
return FALSE;
|
||||
|
||||
stripped = g_strstrip (g_strdup (text));
|
||||
open_bracket = strrchr (stripped, '<');
|
||||
|
||||
if (open_bracket && g_str_has_suffix (stripped, ">"))
|
||||
{
|
||||
stripped[strlen (stripped) - 1] = '\0';
|
||||
email = g_strstrip (g_strdup (open_bracket + 1));
|
||||
|
||||
*open_bracket = '\0';
|
||||
name = g_strstrip (g_strdup (stripped));
|
||||
|
||||
/* "Ana Lima" <ana@example.com> */
|
||||
if (g_str_has_prefix (name, "\"") && g_str_has_suffix (name, "\"") && strlen (name) >= 2)
|
||||
{
|
||||
name[strlen (name) - 1] = '\0';
|
||||
memmove (name, name + 1, strlen (name));
|
||||
g_strstrip (name);
|
||||
}
|
||||
|
||||
if (!*name)
|
||||
g_clear_pointer (&name, g_free);
|
||||
}
|
||||
else
|
||||
{
|
||||
email = g_strdup (stripped);
|
||||
}
|
||||
|
||||
if (g_ascii_strncasecmp (email, "mailto:", 7) == 0)
|
||||
memmove (email, email + 7, strlen (email + 7) + 1);
|
||||
|
||||
if (!is_valid_email (email))
|
||||
return FALSE;
|
||||
|
||||
if (out_email)
|
||||
*out_email = g_steal_pointer (&email);
|
||||
if (out_name)
|
||||
*out_name = g_steal_pointer (&name);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* gcal_create_activate_signal_shortcuts:
|
||||
* @widget_class: the class to add the bindings to
|
||||
|
||||
@@ -118,6 +118,10 @@ GListModel * gcal_util_create_writable_calendars_model (GcalManager
|
||||
|
||||
const gchar * gcal_util_get_email_from_mailto_uri (const gchar *mailto_uri);
|
||||
|
||||
gboolean gcal_util_parse_email_address (const gchar *text,
|
||||
gchar **out_email,
|
||||
gchar **out_name);
|
||||
|
||||
guint gcal_util_create_activate_signal_and_shortcuts (GtkWidgetClass *widget_class,
|
||||
GType widget_type);
|
||||
#endif /* __GCAL_UTILS_H__ */
|
||||
|
||||
@@ -43,6 +43,7 @@ tests = [
|
||||
'server',
|
||||
'utils',
|
||||
'event-attendee',
|
||||
'federated-core',
|
||||
]
|
||||
|
||||
foreach test : tests
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
#include <glib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "gcal-event-attendee.h"
|
||||
#include "gcal-event-organizer.h"
|
||||
@@ -454,6 +455,97 @@ event_get_attendees (void)
|
||||
|
||||
/*********************************************************************************************************************/
|
||||
|
||||
static void
|
||||
event_invite_attendees (void)
|
||||
{
|
||||
g_autoptr (GcalEvent) event = NULL;
|
||||
g_autoptr (GcalEvent) clone = NULL;
|
||||
g_autoptr (GError) error = NULL;
|
||||
g_autofree gchar *ical = NULL;
|
||||
GcalEventOrganizer *organizer;
|
||||
GListModel *attendees;
|
||||
|
||||
event = create_event_for_string (STUB_EVENT, &error);
|
||||
g_assert_no_error (error);
|
||||
|
||||
attendees = gcal_event_get_attendees (event);
|
||||
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 0);
|
||||
g_assert_null (gcal_event_get_organizer (event));
|
||||
|
||||
clone = gcal_event_new_from_event (event);
|
||||
g_assert_true (gcal_event_attendees_equal (event, clone));
|
||||
|
||||
gcal_event_set_organizer (clone, "me@example.com", "Me Myself");
|
||||
organizer = gcal_event_get_organizer (clone);
|
||||
g_assert_nonnull (organizer);
|
||||
g_assert_cmpstr (gcal_event_organizer_get_uri (organizer), ==, "mailto:me@example.com");
|
||||
g_assert_cmpstr (gcal_event_organizer_get_name (organizer), ==, "Me Myself");
|
||||
g_assert_false (gcal_event_attendees_equal (event, clone));
|
||||
|
||||
g_assert_true (gcal_event_add_attendee (clone, "ana@example.com", "Ana Lima"));
|
||||
g_assert_true (gcal_event_add_attendee (clone, "bo@example.com", NULL));
|
||||
g_assert_false (gcal_event_add_attendee (clone, "ANA@example.com", NULL));
|
||||
|
||||
attendees = gcal_event_get_attendees (clone);
|
||||
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 2);
|
||||
g_assert_true (gcal_event_has_attendee (clone, "mailto:Ana@Example.com"));
|
||||
g_assert_false (gcal_event_has_attendee (clone, "carl@example.com"));
|
||||
|
||||
{
|
||||
g_autoptr (GcalEventAttendee) attendee = g_list_model_get_item (attendees, 0);
|
||||
|
||||
g_assert_cmpstr (gcal_event_attendee_get_uri (attendee), ==, "mailto:ana@example.com");
|
||||
g_assert_cmpstr (gcal_event_attendee_get_name (attendee), ==, "Ana Lima");
|
||||
g_assert_cmpint (gcal_event_attendee_get_attendee_type (attendee), ==, GCAL_EVENT_ATTENDEE_TYPE_INDIVIDUAL);
|
||||
g_assert_cmpint (gcal_event_attendee_get_role (attendee), ==, GCAL_EVENT_ATTENDEE_ROLE_REQUIRED);
|
||||
g_assert_cmpint (gcal_event_attendee_get_part_status (attendee), ==, GCAL_EVENT_ATTENDEE_PART_NEEDS_ACTION);
|
||||
g_assert_true (gcal_event_attendee_get_requires_rsvp (attendee));
|
||||
}
|
||||
|
||||
/* What the server sees */
|
||||
ical = e_cal_component_get_as_string (gcal_event_get_component (clone));
|
||||
g_assert_nonnull (strstr (ical, "ORGANIZER;CN=Me Myself:mailto:me@example.com"));
|
||||
g_assert_nonnull (strstr (ical, "mailto:ana@example.com"));
|
||||
g_assert_nonnull (strstr (ical, "mailto:bo@example.com"));
|
||||
|
||||
gcal_event_remove_attendee (clone, "mailto:ANA@example.com");
|
||||
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 1);
|
||||
g_assert_false (gcal_event_has_attendee (clone, "ana@example.com"));
|
||||
|
||||
/* Removing someone who isn't invited does nothing */
|
||||
gcal_event_remove_attendee (clone, "carl@example.com");
|
||||
g_assert_cmpuint (g_list_model_get_n_items (attendees), ==, 1);
|
||||
|
||||
gcal_event_remove_attendee (clone, "bo@example.com");
|
||||
gcal_event_set_organizer (clone, NULL, NULL);
|
||||
g_assert_null (gcal_event_get_organizer (clone));
|
||||
g_assert_true (gcal_event_attendees_equal (event, clone));
|
||||
}
|
||||
|
||||
static void
|
||||
event_copy_attendees (void)
|
||||
{
|
||||
g_autoptr (GcalEvent) with_attendees = NULL;
|
||||
g_autoptr (GcalEvent) event = NULL;
|
||||
g_autoptr (GError) error = NULL;
|
||||
|
||||
with_attendees = create_event_for_string (STUB_EVENT_WITH_ATTENDEES, &error);
|
||||
g_assert_no_error (error);
|
||||
|
||||
event = create_event_for_string (STUB_EVENT, &error);
|
||||
g_assert_no_error (error);
|
||||
|
||||
g_assert_false (gcal_event_attendees_equal (event, with_attendees));
|
||||
|
||||
gcal_event_copy_attendees (event, with_attendees);
|
||||
|
||||
g_assert_true (gcal_event_attendees_equal (event, with_attendees));
|
||||
g_assert_cmpuint (g_list_model_get_n_items (gcal_event_get_attendees (event)), ==, 2);
|
||||
g_assert_cmpstr (gcal_event_organizer_get_uri (gcal_event_get_organizer (event)), ==, "mailto:alice@example.com");
|
||||
}
|
||||
|
||||
/*********************************************************************************************************************/
|
||||
|
||||
static GDateTime *
|
||||
dt_add_second_random (GDateTime *dt)
|
||||
{
|
||||
@@ -595,6 +687,8 @@ main (gint argc,
|
||||
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);
|
||||
g_test_add_func ("/event/attendees/invite", event_invite_attendees);
|
||||
g_test_add_func ("/event/attendees/copy", event_copy_attendees);
|
||||
g_test_add_func ("/event/equal/schedule", event_schedule_equal);
|
||||
|
||||
return g_test_run ();
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/* test-federated-core.c
|
||||
*
|
||||
* Copyright (C) 2026 David Young <david@federated.computer>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
|
||||
#include "gcal-federated-core.h"
|
||||
|
||||
/*
|
||||
* Written by CoreDesktop's configure with bash's printf %q
|
||||
*/
|
||||
#define CORE_CONF(domain, email, name) \
|
||||
"CORE_DOMAIN=" domain "\n" \
|
||||
"CORE_EMAIL=" email "\n" \
|
||||
"CORE_NAME=" name "\n"
|
||||
|
||||
static gchar *
|
||||
create_config_dir (void)
|
||||
{
|
||||
g_autoptr (GError) error = NULL;
|
||||
gchar *config_dir;
|
||||
|
||||
config_dir = g_dir_make_tmp ("gcal-federated-core-XXXXXX", &error);
|
||||
g_assert_no_error (error);
|
||||
|
||||
return config_dir;
|
||||
}
|
||||
|
||||
static void
|
||||
write_file (const gchar *config_dir,
|
||||
const gchar *filename,
|
||||
const gchar *contents)
|
||||
{
|
||||
g_autoptr (GError) error = NULL;
|
||||
g_autofree gchar *path = g_build_filename (config_dir, filename, NULL);
|
||||
g_autofree gchar *dir = g_path_get_dirname (path);
|
||||
|
||||
g_mkdir_with_parents (dir, 0700);
|
||||
g_file_set_contents (path, contents, -1, &error);
|
||||
g_assert_no_error (error);
|
||||
}
|
||||
|
||||
static void
|
||||
remove_config_dir (const gchar *config_dir)
|
||||
{
|
||||
g_autoptr (GFile) dir = g_file_new_for_path (config_dir);
|
||||
g_autoptr (GFileEnumerator) children = NULL;
|
||||
GFileInfo *info;
|
||||
GFile *child;
|
||||
|
||||
children = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
|
||||
|
||||
while (children && g_file_enumerator_iterate (children, &info, &child, NULL, NULL) && info)
|
||||
{
|
||||
if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
|
||||
{
|
||||
g_autofree gchar *path = g_file_get_path (child);
|
||||
remove_config_dir (path);
|
||||
}
|
||||
else
|
||||
{
|
||||
g_file_delete (child, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
g_file_delete (dir, NULL, NULL);
|
||||
}
|
||||
|
||||
/*********************************************************************************************************************/
|
||||
|
||||
static void
|
||||
federated_core_find_core (void)
|
||||
{
|
||||
g_autofree gchar *config_dir = create_config_dir ();
|
||||
g_autofree gchar *email = NULL;
|
||||
g_autofree gchar *name = NULL;
|
||||
|
||||
/* Nothing set up */
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", NULL, NULL));
|
||||
|
||||
write_file (config_dir, "cores/example.com.conf", CORE_CONF ("example.com", "ana@example.com", "Ana\\ Lima"));
|
||||
write_file (config_dir, "cores/example.org.conf", CORE_CONF ("example.org", "bo@example.org", "Bo"));
|
||||
|
||||
g_assert_true (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", &email, &name));
|
||||
g_assert_cmpstr (email, ==, "ana@example.com");
|
||||
g_assert_cmpstr (name, ==, "Ana Lima");
|
||||
|
||||
g_clear_pointer (&email, g_free);
|
||||
g_clear_pointer (&name, g_free);
|
||||
|
||||
g_assert_true (gcal_federated_core_find_core (config_dir, "NextCloud.Example.Org", &email, &name));
|
||||
g_assert_cmpstr (email, ==, "bo@example.org");
|
||||
g_assert_cmpstr (name, ==, "Bo");
|
||||
|
||||
/* Only the Core's own Nextcloud */
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, "example.com", NULL, NULL));
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, "caldav.example.com", NULL, NULL));
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.com.evil.net", NULL, NULL));
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, NULL, NULL, NULL));
|
||||
|
||||
remove_config_dir (config_dir);
|
||||
}
|
||||
|
||||
static void
|
||||
federated_core_single_core_conf (void)
|
||||
{
|
||||
g_autofree gchar *config_dir = create_config_dir ();
|
||||
g_autofree gchar *email = NULL;
|
||||
g_autofree gchar *name = NULL;
|
||||
|
||||
/* Before CoreDesktop supported several Cores */
|
||||
write_file (config_dir, "core.conf", CORE_CONF ("example.com", "ana@example.com", "\\'Ana\\'"));
|
||||
|
||||
g_assert_true (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", &email, &name));
|
||||
g_assert_cmpstr (email, ==, "ana@example.com");
|
||||
g_assert_cmpstr (name, ==, "'Ana'");
|
||||
|
||||
remove_config_dir (config_dir);
|
||||
}
|
||||
|
||||
static void
|
||||
federated_core_incomplete_conf (void)
|
||||
{
|
||||
g_autofree gchar *config_dir = create_config_dir ();
|
||||
|
||||
write_file (config_dir, "cores/example.com.conf", "CORE_DOMAIN=example.com\n");
|
||||
write_file (config_dir, "cores/example.org.conf", CORE_CONF ("example.org", "not-an-email", "Bo"));
|
||||
write_file (config_dir, "cores/example.net.conf.bak", CORE_CONF ("example.net", "cy@example.net", "Cy"));
|
||||
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.com", NULL, NULL));
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.org", NULL, NULL));
|
||||
g_assert_false (gcal_federated_core_find_core (config_dir, "nextcloud.example.net", NULL, NULL));
|
||||
|
||||
remove_config_dir (config_dir);
|
||||
}
|
||||
|
||||
/*********************************************************************************************************************/
|
||||
|
||||
gint
|
||||
main (gint argc,
|
||||
gchar *argv[])
|
||||
{
|
||||
g_test_init (&argc, &argv, NULL);
|
||||
|
||||
g_test_add_func ("/federated-core/find-core", federated_core_find_core);
|
||||
g_test_add_func ("/federated-core/single-core-conf", federated_core_single_core_conf);
|
||||
g_test_add_func ("/federated-core/incomplete-conf", federated_core_incomplete_conf);
|
||||
|
||||
return g_test_run ();
|
||||
}
|
||||
@@ -233,6 +233,52 @@ test_first_weekday (void)
|
||||
|
||||
/*********************************************************************************************************************/
|
||||
|
||||
static void
|
||||
parse_email_address (void)
|
||||
{
|
||||
struct {
|
||||
const gchar *text;
|
||||
const gchar *email;
|
||||
const gchar *name;
|
||||
} valid[] = {
|
||||
{ "ana@example.com", "ana@example.com", NULL },
|
||||
{ " ana@example.com ", "ana@example.com", NULL },
|
||||
{ "mailto:ana@example.com", "ana@example.com", NULL },
|
||||
{ "Ana Lima <ana@example.com>", "ana@example.com", "Ana Lima" },
|
||||
{ "\"Lima, Ana\" <ana@example.com>", "ana@example.com", "Lima, Ana" },
|
||||
{ "<ana@example.com>", "ana@example.com", NULL },
|
||||
{ "ana.lima+work@mail.example.co.uk", "ana.lima+work@mail.example.co.uk", NULL },
|
||||
};
|
||||
const gchar *invalid[] = {
|
||||
NULL,
|
||||
"",
|
||||
"ana",
|
||||
"ana@",
|
||||
"@example.com",
|
||||
"ana@example",
|
||||
"ana@example.",
|
||||
"ana@.example.com",
|
||||
"ana@@example.com",
|
||||
"ana lima@example.com",
|
||||
"Ana Lima <ana>",
|
||||
};
|
||||
|
||||
for (gsize i = 0; i < G_N_ELEMENTS (valid); i++)
|
||||
{
|
||||
g_autofree gchar *email = NULL;
|
||||
g_autofree gchar *name = NULL;
|
||||
|
||||
g_assert_true (gcal_util_parse_email_address (valid[i].text, &email, &name));
|
||||
g_assert_cmpstr (email, ==, valid[i].email);
|
||||
g_assert_cmpstr (name, ==, valid[i].name);
|
||||
}
|
||||
|
||||
for (gsize i = 0; i < G_N_ELEMENTS (invalid); i++)
|
||||
g_assert_false (gcal_util_parse_email_address (invalid[i], NULL, NULL));
|
||||
}
|
||||
|
||||
/*********************************************************************************************************************/
|
||||
|
||||
gint
|
||||
main (gint argc,
|
||||
gchar *argv[])
|
||||
@@ -245,6 +291,7 @@ main (gint argc,
|
||||
g_test_add_func ("/utils/date-time/date_time_from_icaltime", date_time_from_icaltime);
|
||||
g_test_add_func ("/utils/misc/extract_meeting_url", extract_meeting_url);
|
||||
g_test_add_func ("/utils/misc/get_first_weekday", test_first_weekday);
|
||||
g_test_add_func ("/utils/misc/parse_email_address", parse_email_address);
|
||||
|
||||
return g_test_run ();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user