Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-contacts-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <json-glib/json-glib.h>
10 : : #include <valent.h>
11 : :
12 : : #include "valent-contacts-plugin.h"
13 : :
14 : :
15 : : struct _ValentContactsPlugin
16 : : {
17 : : ValentDevicePlugin parent_instance;
18 : :
19 : : GCancellable *cancellable;
20 : :
21 : : ValentContactStore *local_store;
22 : : ValentContactStore *remote_store;
23 : : };
24 : :
25 [ + + + - ]: 29 : G_DEFINE_FINAL_TYPE (ValentContactsPlugin, valent_contacts_plugin, VALENT_TYPE_DEVICE_PLUGIN)
26 : :
27 : :
28 : : /*
29 : : * Local Contacts
30 : : */
31 : : static void
32 : 1 : valent_contact_store_query_vcards_cb (ValentContactStore *store,
33 : : GAsyncResult *result,
34 : : ValentContactsPlugin *self)
35 : : {
36 : 1 : g_autoptr (JsonBuilder) builder = NULL;
37 [ - - - + ]: 1 : g_autoptr (JsonNode) response = NULL;
38 [ - - + - ]: 1 : g_autoslist (GObject) contacts = NULL;
39 : 1 : g_autoptr (GError) error = NULL;
40 : :
41 [ + - ]: 1 : g_assert (VALENT_IS_CONTACT_STORE (store));
42 : :
43 : 1 : contacts = valent_contact_store_query_finish (store, result, &error);
44 : :
45 [ - + ]: 1 : if (error != NULL)
46 : : {
47 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
48 [ # # ]: 0 : return;
49 : : }
50 : :
51 : 1 : valent_packet_init (&builder, "kdeconnect.contacts.response_vcards");
52 : :
53 : : /* Add UIDs */
54 : 1 : json_builder_set_member_name (builder, "uids");
55 : 1 : json_builder_begin_array (builder);
56 : :
57 [ + + ]: 3 : for (const GSList *iter = contacts; iter; iter = iter->next)
58 : : {
59 : 2 : const char *uid;
60 : :
61 : 2 : uid = e_contact_get_const (iter->data, E_CONTACT_UID);
62 : 2 : json_builder_add_string_value (builder, uid);
63 : : }
64 : 1 : json_builder_end_array (builder);
65 : :
66 : : /* Add vCard data */
67 [ + + ]: 3 : for (const GSList *iter = contacts; iter; iter = iter->next)
68 : : {
69 : 2 : const char *uid;
70 : 2 : g_autofree char *vcard_data = NULL;
71 : :
72 : 2 : uid = e_contact_get_const (iter->data, E_CONTACT_UID);
73 : 2 : json_builder_set_member_name (builder, uid);
74 : :
75 : 2 : vcard_data = e_vcard_to_string (iter->data, EVC_FORMAT_VCARD_21);
76 : 2 : json_builder_add_string_value (builder, vcard_data);
77 : : }
78 : :
79 : : /* Finish and send the response */
80 : 1 : response = valent_packet_end (&builder);
81 [ - + ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), response);
82 : : }
83 : :
84 : : static void
85 : 1 : valent_contact_plugin_handle_request_vcards_by_uid (ValentContactsPlugin *self,
86 : : JsonNode *packet)
87 : : {
88 : 1 : GSettings *settings;
89 : 1 : g_autofree EBookQuery **queries = NULL;
90 : 1 : g_autoptr (EBookQuery) query = NULL;
91 [ + - ]: 1 : g_autofree char *sexp = NULL;
92 : 1 : JsonArray *uids;
93 : 1 : unsigned int n_uids, n_queries;
94 : :
95 [ + - ]: 1 : g_assert (VALENT_IS_CONTACTS_PLUGIN (self));
96 : :
97 : : #ifndef __clang_analyzer__
98 : : /* Bail if exporting is disabled */
99 : 1 : settings = valent_extension_get_settings (VALENT_EXTENSION (self));
100 : :
101 [ + - - + ]: 1 : if (self->local_store == NULL || !g_settings_get_boolean (settings, "local-sync"))
102 : 0 : return;
103 : :
104 [ - + ]: 1 : if (!valent_packet_get_array (packet, "uids", &uids))
105 : : {
106 : 0 : g_debug ("%s(): expected \"uids\" field holding an array",
107 : : G_STRFUNC);
108 : 0 : return;
109 : : }
110 : :
111 : : /* Build a list of queries */
112 : 1 : n_uids = json_array_get_length (uids);
113 [ - + ]: 1 : queries = g_new (EBookQuery *, n_uids);
114 : 1 : n_queries = 0;
115 : :
116 [ + + ]: 3 : for (unsigned int i = 0; i < n_uids; i++)
117 : : {
118 : 2 : JsonNode *element = json_array_get_element (uids, i);
119 : 2 : const char *uid = NULL;
120 : :
121 [ + - ]: 2 : if G_LIKELY (json_node_get_value_type (element) == G_TYPE_STRING)
122 : 2 : uid = json_node_get_string (element);
123 : :
124 [ + - + - ]: 2 : if G_UNLIKELY (uid == NULL || *uid == '\0')
125 : : {
126 : 0 : g_debug ("%s(): expected \"uids\" element to contain a string",
127 : : G_STRFUNC);
128 : 0 : continue;
129 : : }
130 : :
131 : 2 : queries[n_queries++] = e_book_query_field_test (E_CONTACT_UID,
132 : : E_BOOK_QUERY_IS,
133 : : uid);
134 : : }
135 : :
136 [ + - ]: 1 : if (n_queries == 0)
137 : : return;
138 : :
139 : 1 : query = e_book_query_or (n_queries, queries, TRUE);
140 : 1 : sexp = e_book_query_to_string (query);
141 : :
142 : 1 : valent_contact_store_query (self->local_store,
143 : : sexp,
144 : : self->cancellable,
145 : : (GAsyncReadyCallback)valent_contact_store_query_vcards_cb,
146 : : self);
147 : : #endif /* __clang_analyzer__ */
148 : : }
149 : :
150 : : static void
151 : 1 : valent_contact_store_query_uids_cb (ValentContactStore *store,
152 : : GAsyncResult *result,
153 : : ValentContactsPlugin *self)
154 : : {
155 : 1 : g_autoptr (JsonBuilder) builder = NULL;
156 [ - - - + ]: 1 : g_autoptr (JsonNode) response = NULL;
157 [ - - + - ]: 1 : g_autoslist (GObject) contacts = NULL;
158 : 1 : g_autoptr (GError) error = NULL;
159 : :
160 [ + - ]: 1 : g_assert (VALENT_IS_CONTACT_STORE (store));
161 : :
162 : 1 : contacts = valent_contact_store_query_finish (store, result, &error);
163 : :
164 : : /* If the operation was cancelled, we're about to dispose. For any other
165 : : * error log a warning and send an empty list. */
166 [ - + ]: 1 : if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
167 [ # # ]: 0 : return;
168 : :
169 [ - + ]: 1 : if (error != NULL)
170 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
171 : :
172 : : /* Build response */
173 : 1 : valent_packet_init (&builder, "kdeconnect.contacts.response_uids_timestamps");
174 : :
175 [ + + ]: 3 : for (const GSList *iter = contacts; iter; iter = iter->next)
176 : : {
177 : 2 : const char *uid;
178 : 2 : int64_t timestamp = 0;
179 : :
180 : 2 : uid = e_contact_get_const (iter->data, E_CONTACT_UID);
181 : 2 : json_builder_set_member_name (builder, uid);
182 : :
183 : : // TODO: We probably need to convert between the custom field
184 : : // `X-KDECONNECT-TIMESTAMP` and `E_CONTACT_REV` to set a proper timestamp
185 : 2 : timestamp = 0;
186 : 2 : json_builder_add_int_value (builder, timestamp);
187 : : }
188 : :
189 : 1 : response = valent_packet_end (&builder);
190 [ - + ]: 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), response);
191 : : }
192 : :
193 : : static void
194 : 1 : valent_contact_plugin_handle_request_all_uids_timestamps (ValentContactsPlugin *self,
195 : : JsonNode *packet)
196 : : {
197 : 1 : GSettings *settings;
198 : 0 : g_autoptr (EBookQuery) query = NULL;
199 [ + - ]: 1 : g_autofree char *sexp = NULL;
200 : :
201 [ + - ]: 1 : g_assert (VALENT_IS_CONTACTS_PLUGIN (self));
202 : :
203 : : /* Bail if exporting is disabled */
204 : 1 : settings = valent_extension_get_settings (VALENT_EXTENSION (self));
205 : :
206 [ + - - + ]: 1 : if (self->local_store == NULL || !g_settings_get_boolean (settings, "local-sync"))
207 : 0 : return;
208 : :
209 : 1 : query = e_book_query_vcard_field_exists (EVC_UID);
210 : 1 : sexp = e_book_query_to_string (query);
211 : :
212 : 1 : valent_contact_store_query (self->local_store,
213 : : sexp,
214 : : self->cancellable,
215 : : (GAsyncReadyCallback)valent_contact_store_query_uids_cb,
216 : : self);
217 : : }
218 : :
219 : : /*
220 : : * Remote Contacts
221 : : */
222 : : static void
223 : 1 : valent_contact_plugin_handle_response_uids_timestamps (ValentContactsPlugin *self,
224 : : JsonNode *packet)
225 : : {
226 : 2 : g_autoptr (JsonNode) request = NULL;
227 [ + - ]: 1 : g_autoptr (JsonBuilder) builder = NULL;
228 : 1 : JsonObjectIter iter;
229 : 1 : const char *uid;
230 : 1 : JsonNode *node;
231 : 1 : unsigned int n_requested = 0;
232 : :
233 [ + - ]: 1 : g_assert (VALENT_IS_CONTACTS_PLUGIN (self));
234 : :
235 : : /* Start the packet */
236 : 1 : valent_packet_init (&builder, "kdeconnect.contacts.request_vcards_by_uid");
237 : 1 : json_builder_set_member_name (builder, "uids");
238 : 1 : json_builder_begin_array (builder);
239 : :
240 : 1 : json_object_iter_init (&iter, valent_packet_get_body (packet));
241 : :
242 [ + + ]: 4 : while (json_object_iter_next (&iter, &uid, &node))
243 : : {
244 : 3 : int64_t timestamp = 0;
245 : :
246 : : // skip the "uids" array
247 [ + + ]: 3 : if G_UNLIKELY (g_str_equal ("uids", uid))
248 : 1 : continue;
249 : :
250 [ + - ]: 2 : if G_LIKELY (json_node_get_value_type (node) == G_TYPE_INT64)
251 : 2 : timestamp = json_node_get_int (node);
252 : :
253 : : /* Check if the contact is new or updated */
254 [ - + ]: 2 : if (0 != timestamp)
255 : : //if (valent_contact_store_get_timestamp (self->store, uid) != timestamp)
256 : : {
257 : 2 : n_requested++;
258 : 2 : json_builder_add_string_value (builder, uid);
259 : : }
260 : : }
261 : :
262 : 1 : json_builder_end_array (builder);
263 : 1 : request = valent_packet_end (&builder);
264 : :
265 [ + - ]: 1 : if (n_requested > 0)
266 : 1 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), request);
267 : 1 : }
268 : :
269 : : static void
270 : 1 : valent_contact_store_add_contacts_cb (ValentContactStore *store,
271 : : GAsyncResult *result,
272 : : gpointer user_data)
273 : : {
274 : 2 : g_autoptr (GError) error = NULL;
275 : :
276 [ - + - - ]: 1 : if (!valent_contact_store_add_contacts_finish (store, result, &error) &&
277 : 0 : !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
278 : 0 : g_warning ("%s(): %s", G_STRFUNC, error->message);
279 : 1 : }
280 : :
281 : : static void
282 : 1 : valent_contact_plugin_handle_response_vcards (ValentContactsPlugin *self,
283 : : JsonNode *packet)
284 : : {
285 : 2 : g_autoslist (EContact) contacts = NULL;
286 : 1 : JsonObject *body;
287 : 1 : JsonObjectIter iter;
288 : 1 : const char *uid;
289 : 1 : JsonNode *node;
290 : :
291 [ + - ]: 1 : g_assert (VALENT_IS_CONTACTS_PLUGIN (self));
292 : :
293 : 1 : body = valent_packet_get_body (packet);
294 : 1 : json_object_iter_init (&iter, body);
295 : :
296 [ + + ]: 4 : while (json_object_iter_next (&iter, &uid, &node))
297 : : {
298 : 3 : EContact *contact;
299 : 3 : const char *vcard;
300 : :
301 : : /* NOTE: This has the side-effect of ignoring `uids` array, which is fine
302 : : * because the contact members are the ultimate source of truth. */
303 [ + + ]: 3 : if G_UNLIKELY (json_node_get_value_type (node) != G_TYPE_STRING)
304 : 1 : continue;
305 : :
306 : 2 : vcard = json_node_get_string (node);
307 : 2 : contact = e_contact_new_from_vcard_with_uid (vcard, uid);
308 : :
309 : 2 : contacts = g_slist_append (contacts, contact);
310 : : }
311 : :
312 [ + - ]: 1 : if (contacts != NULL)
313 : : {
314 : 1 : valent_contact_store_add_contacts (self->remote_store,
315 : : contacts,
316 : : self->cancellable,
317 : : (GAsyncReadyCallback)valent_contact_store_add_contacts_cb,
318 : : NULL);
319 : : }
320 : 1 : }
321 : :
322 : : static void
323 : 4 : valent_contacts_plugin_request_all_uids_timestamps (ValentContactsPlugin *self)
324 : : {
325 : 8 : g_autoptr (JsonNode) packet = NULL;
326 : :
327 [ + - ]: 4 : g_assert (VALENT_IS_CONTACTS_PLUGIN (self));
328 : :
329 : 4 : packet = valent_packet_new ("kdeconnect.contacts.request_all_uids_timestamps");
330 [ + - ]: 4 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
331 : 4 : }
332 : :
333 : : /*
334 : : * GActions
335 : : */
336 : : static void
337 : 1 : contacts_fetch_action (GSimpleAction *action,
338 : : GVariant *parameter,
339 : : gpointer user_data)
340 : : {
341 : 1 : ValentContactsPlugin *self = VALENT_CONTACTS_PLUGIN (user_data);
342 : :
343 [ + - ]: 1 : g_assert (VALENT_IS_CONTACTS_PLUGIN (self));
344 : :
345 : 1 : valent_contacts_plugin_request_all_uids_timestamps (self);
346 : 1 : }
347 : :
348 : : static const GActionEntry actions[] = {
349 : : {"fetch", contacts_fetch_action, NULL, NULL, NULL}
350 : : };
351 : :
352 : : /*
353 : : * ValentDevicePlugin
354 : : */
355 : : static void
356 : 10 : valent_contacts_plugin_update_state (ValentDevicePlugin *plugin,
357 : : ValentDeviceState state)
358 : : {
359 : 10 : ValentContactsPlugin *self = VALENT_CONTACTS_PLUGIN (plugin);
360 : 10 : gboolean available;
361 : :
362 [ + - ]: 10 : g_assert (VALENT_IS_CONTACTS_PLUGIN (self));
363 : :
364 : 10 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
365 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
366 : :
367 : 10 : valent_extension_toggle_actions (VALENT_EXTENSION (plugin), available);
368 : :
369 [ + + ]: 10 : if (available)
370 : 3 : valent_contacts_plugin_request_all_uids_timestamps (self);
371 : 10 : }
372 : :
373 : : static void
374 : 4 : valent_contacts_plugin_handle_packet (ValentDevicePlugin *plugin,
375 : : const char *type,
376 : : JsonNode *packet)
377 : : {
378 : 4 : ValentContactsPlugin *self = VALENT_CONTACTS_PLUGIN (plugin);
379 : :
380 [ + - ]: 4 : g_assert (VALENT_IS_CONTACTS_PLUGIN (plugin));
381 [ - + ]: 4 : g_assert (type != NULL);
382 [ - + ]: 4 : g_assert (VALENT_IS_PACKET (packet));
383 : :
384 : : /* A response to a request for a listing of contacts */
385 [ + + ]: 4 : if (g_str_equal (type, "kdeconnect.contacts.response_uids_timestamps"))
386 : 1 : valent_contact_plugin_handle_response_uids_timestamps (self, packet);
387 : :
388 : : /* A response to a request for contacts */
389 [ + + ]: 3 : else if (g_str_equal (type, "kdeconnect.contacts.response_vcards"))
390 : 1 : valent_contact_plugin_handle_response_vcards (self, packet);
391 : :
392 : : /* A request for a listing of contacts */
393 [ + + ]: 2 : else if (g_str_equal (type, "kdeconnect.contacts.request_all_uids_timestamps"))
394 : 1 : valent_contact_plugin_handle_request_all_uids_timestamps (self, packet);
395 : :
396 : : /* A request for contacts */
397 [ + - ]: 1 : else if (g_str_equal (type, "kdeconnect.contacts.request_vcards_by_uid"))
398 : 1 : valent_contact_plugin_handle_request_vcards_by_uid (self, packet);
399 : :
400 : : else
401 : 0 : g_assert_not_reached ();
402 : 4 : }
403 : :
404 : : /*
405 : : * ValentObject
406 : : */
407 : : static void
408 : 6 : valent_contacts_plugin_destroy (ValentObject *object)
409 : : {
410 : 6 : ValentContactsPlugin *self = VALENT_CONTACTS_PLUGIN (object);
411 : :
412 : : /* Cancel any pending operations and drop the address books */
413 : 6 : g_cancellable_cancel (self->cancellable);
414 [ + + ]: 6 : g_clear_object (&self->cancellable);
415 [ + + ]: 6 : g_clear_object (&self->remote_store);
416 [ + + ]: 6 : g_clear_object (&self->local_store);
417 : :
418 : 6 : VALENT_OBJECT_CLASS (valent_contacts_plugin_parent_class)->destroy (object);
419 : 6 : }
420 : :
421 : : /*
422 : : * GObject
423 : : */
424 : : static void
425 : 3 : valent_contacts_plugin_constructed (GObject *object)
426 : : {
427 : 3 : ValentContactsPlugin *self = VALENT_CONTACTS_PLUGIN (object);
428 : 3 : ValentDevicePlugin *plugin = VALENT_DEVICE_PLUGIN (object);
429 : 3 : ValentContacts *contacts = valent_contacts_get_default ();
430 : 3 : ValentContactStore *store = NULL;
431 : 6 : g_autofree char *local_uid = NULL;
432 : 3 : ValentDevice *device;
433 : 3 : GSettings *settings;
434 : :
435 : 3 : g_action_map_add_action_entries (G_ACTION_MAP (plugin),
436 : : actions,
437 : : G_N_ELEMENTS (actions),
438 : : plugin);
439 : :
440 : : /* Prepare Addressbooks */
441 : 3 : self->cancellable = g_cancellable_new ();
442 : 3 : device = valent_extension_get_object (VALENT_EXTENSION (plugin));
443 : 3 : settings = valent_extension_get_settings (VALENT_EXTENSION (self));
444 : :
445 : 3 : store = valent_contacts_ensure_store (valent_contacts_get_default (),
446 : : valent_device_get_id (device),
447 : : valent_device_get_name (device));
448 : 3 : g_set_object (&self->remote_store, store);
449 : :
450 : : /* Local address book, shared with remote device */
451 : 3 : local_uid = g_settings_get_string (settings, "local-uid");
452 : :
453 [ + + ]: 3 : if (*local_uid != '\0')
454 : : {
455 : 2 : unsigned int n_stores = g_list_model_get_n_items (G_LIST_MODEL (contacts));
456 : :
457 [ + - ]: 4 : for (unsigned int i = 0; i < n_stores; i++)
458 : : {
459 : 2 : g_autoptr (ValentContactStore) local = NULL;
460 : :
461 : 4 : local = g_list_model_get_item (G_LIST_MODEL (contacts), i);
462 : :
463 [ + + ]: 4 : if (g_strcmp0 (valent_contact_store_get_uid (local), local_uid) != 0)
464 [ + - ]: 2 : continue;
465 : :
466 : 2 : g_set_object (&self->local_store, local);
467 [ + - ]: 2 : break;
468 : : }
469 : : }
470 : :
471 : 3 : G_OBJECT_CLASS (valent_contacts_plugin_parent_class)->constructed (object);
472 : 3 : }
473 : :
474 : : static void
475 : 2 : valent_contacts_plugin_class_init (ValentContactsPluginClass *klass)
476 : : {
477 : 2 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
478 : 2 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
479 : 2 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
480 : :
481 : 2 : object_class->constructed = valent_contacts_plugin_constructed;
482 : :
483 : 2 : vobject_class->destroy = valent_contacts_plugin_destroy;
484 : :
485 : 2 : plugin_class->handle_packet = valent_contacts_plugin_handle_packet;
486 : 2 : plugin_class->update_state = valent_contacts_plugin_update_state;
487 : : }
488 : :
489 : : static void
490 : 3 : valent_contacts_plugin_init (ValentContactsPlugin *self)
491 : : {
492 : 3 : }
493 : :
|