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-ui-utils"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <gio/gio.h>
9 : : #include <libebook-contacts/libebook-contacts.h>
10 : : #include <libtracker-sparql/tracker-sparql.h>
11 : :
12 : : #include "valent-ui-utils-private.h"
13 : :
14 : : /*< private>
15 : : *
16 : : * Cursor columns for `nco:Contact`.
17 : : */
18 : : #define CURSOR_CONTACT_IRI 0
19 : : #define CURSOR_CONTACT_UID 1
20 : : #define CURSOR_VCARD_DATA 2
21 : :
22 : : #define SEARCH_CONTACTS_RQ "/ca/andyholmes/Valent/sparql/search-contacts.rq"
23 : :
24 : : /*< private>
25 : : *
26 : : * Cursor columns for `vmo:PhoneMessage`.
27 : : */
28 : : #define CURSOR_MESSAGE_IRI 0
29 : : #define CURSOR_MESSAGE_BOX 1
30 : : #define CURSOR_MESSAGE_DATE 2
31 : : #define CURSOR_MESSAGE_ID 3
32 : : #define CURSOR_MESSAGE_READ 4
33 : : #define CURSOR_MESSAGE_RECIPIENTS 5
34 : : #define CURSOR_MESSAGE_SENDER 6
35 : : #define CURSOR_MESSAGE_SUBSCRIPTION_ID 7
36 : : #define CURSOR_MESSAGE_TEXT 8
37 : : #define CURSOR_MESSAGE_THREAD_ID 9
38 : : #define CURSOR_MESSAGE_ATTACHMENT_IRI 10
39 : : #define CURSOR_MESSAGE_ATTACHMENT_PREVIEW 11
40 : : #define CURSOR_MESSAGE_ATTACHMENT_FILE 12
41 : :
42 : : #define SEARCH_MESSAGES_RQ "/ca/andyholmes/Valent/sparql/search-messages.rq"
43 : :
44 [ + + ]: 7 : G_DEFINE_QUARK (VALENT_CONTACT_PAINTABLE, valent_contact_paintable)
45 : :
46 : :
47 : : // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
48 : : #define EMAIL_PATTERN "[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*"
49 : : // https://mathiasbynens.be/demo/url-regex, @stephenhay, relaxed scheme
50 : : #define URI_PATTERN "\\b([a-zA-Z0-9-]+:[\\/]{1,3}|www[.])[^\\s>]*"
51 : : #define COMPILE_FLAGS (G_REGEX_CASELESS | G_REGEX_MULTILINE | G_REGEX_NO_AUTO_CAPTURE | G_REGEX_OPTIMIZE)
52 : :
53 : : static GRegex *email_regex = NULL;
54 : : static GRegex *uri_regex = NULL;
55 : :
56 : : static inline gpointer
57 : 0 : _g_object_dup0 (gpointer object,
58 : : gpointer user_data)
59 : : {
60 [ # # ]: 0 : return object ? g_object_ref ((GObject *)object) : NULL;
61 : : }
62 : :
63 : : static gboolean
64 : 1 : valent_ui_replace_eval_uri (const GMatchInfo *info,
65 : : GString *result,
66 : : gpointer user_data)
67 : : {
68 : 2 : g_autofree char *uri = NULL;
69 : :
70 : 1 : uri = g_match_info_fetch (info, 0);
71 : :
72 [ + - ]: 1 : if (g_uri_is_valid (uri, G_URI_FLAGS_NONE, NULL))
73 : 1 : g_string_append_printf (result, "<a href=\"%s\">%s</a>", uri, uri);
74 [ # # ]: 0 : else if (g_regex_match (email_regex, uri, 0, NULL))
75 : 0 : g_string_append_printf (result, "<a href=\"mailto:%s\">%s</a>", uri, uri);
76 : : else
77 : 0 : g_string_append_printf (result, "<a href=\"https://%s\">%s</a>", uri, uri);
78 : :
79 : 1 : return FALSE;
80 : : }
81 : :
82 : : /**
83 : : * valent_string_to_markup:
84 : : * @text: (nullable): input text
85 : : *
86 : : * Add markup to text for recognized elements.
87 : : *
88 : : * This function currently scans for URLs and e-mail addresses, then amends each
89 : : * element with anchor tags (`<a>`).
90 : : *
91 : : * If @text is %NULL, this function will return %NULL.
92 : : *
93 : : * Returns: (transfer full) (nullable): a string of markup
94 : : *
95 : : * Since: 1.0
96 : : */
97 : : char *
98 : 1 : valent_string_to_markup (const char *text)
99 : : {
100 : 2 : g_autofree char *escaped = NULL;
101 : 1 : g_autofree char *markup = NULL;
102 : 1 : g_autoptr (GError) error = NULL;
103 : :
104 [ + - ]: 1 : if G_UNLIKELY (text == NULL)
105 : : return NULL;
106 : :
107 [ + - ]: 1 : if G_UNLIKELY (uri_regex == NULL)
108 : : {
109 : 1 : email_regex = g_regex_new (EMAIL_PATTERN, COMPILE_FLAGS, 0, NULL);
110 : 1 : uri_regex = g_regex_new (URI_PATTERN"|"EMAIL_PATTERN, COMPILE_FLAGS, 0, NULL);
111 : : }
112 : :
113 : 1 : escaped = g_markup_escape_text (text, -1);
114 : 1 : markup = g_regex_replace_eval (uri_regex,
115 : : escaped,
116 : 1 : strlen (escaped),
117 : : 0,
118 : : 0,
119 : : valent_ui_replace_eval_uri,
120 : : NULL,
121 : : &error);
122 : :
123 : 1 : return g_steal_pointer (&markup);
124 : : }
125 : :
126 : : static GdkPaintable *
127 : 4 : _e_contact_get_paintable (EContact *contact,
128 : : GError **error)
129 : : {
130 : 8 : g_autoptr (EContactPhoto) photo = NULL;
131 : 4 : GdkPaintable *paintable = NULL;
132 : 4 : GdkTexture *texture = NULL;
133 : 4 : const unsigned char *data;
134 : 4 : size_t len;
135 : 4 : const char *uri;
136 : :
137 [ + - + - : 4 : g_assert (E_IS_CONTACT (contact));
- + - - ]
138 : :
139 : 4 : paintable = g_object_get_qdata (G_OBJECT (contact),
140 : : valent_contact_paintable_quark ());
141 : :
142 [ + - ]: 4 : if (GDK_IS_PAINTABLE (paintable))
143 : : return paintable;
144 : :
145 : 4 : photo = e_contact_get (contact, E_CONTACT_PHOTO);
146 [ + + ]: 4 : if (photo == NULL)
147 : : return NULL;
148 : :
149 [ + - ]: 3 : if (photo->type == E_CONTACT_PHOTO_TYPE_INLINED &&
150 [ + - ]: 3 : (data = e_contact_photo_get_inlined (photo, &len)))
151 : : {
152 : 3 : g_autoptr (GBytes) bytes = NULL;
153 : :
154 : 3 : bytes = g_bytes_new (data, len);
155 [ + - ]: 3 : texture = gdk_texture_new_from_bytes (bytes, NULL);
156 : : }
157 [ # # ]: 0 : else if (photo->type == E_CONTACT_PHOTO_TYPE_URI &&
158 [ # # ]: 0 : (uri = e_contact_photo_get_uri (photo)))
159 : : {
160 : 3 : g_autoptr (GFile) file = NULL;
161 : :
162 : 0 : file = g_file_new_for_uri (uri);
163 [ # # ]: 0 : texture = gdk_texture_new_from_file (file, NULL);
164 : : }
165 : :
166 [ + - ]: 3 : if (GDK_IS_PAINTABLE (texture))
167 : : {
168 : 3 : g_object_set_qdata_full (G_OBJECT (contact),
169 : : valent_contact_paintable_quark (),
170 : : texture, /* owned */
171 : : g_object_unref);
172 : : }
173 : :
174 : 3 : return GDK_PAINTABLE (texture);
175 : : }
176 : :
177 : : GdkPaintable *
178 : 8 : valent_contact_to_paintable (gpointer user_data,
179 : : EContact *contact)
180 : : {
181 : 8 : GdkPaintable *paintable = NULL;
182 : :
183 [ + + ]: 8 : if (contact != NULL)
184 : 4 : paintable = _e_contact_get_paintable (contact, NULL);
185 : :
186 [ + + ]: 4 : return paintable ? g_object_ref (paintable) : NULL;
187 : : }
188 : :
189 : : static EContact *
190 : 0 : _e_contact_from_sparql_cursor (TrackerSparqlCursor *cursor)
191 : : {
192 : 0 : const char *uid = NULL;
193 : 0 : const char *vcard = NULL;
194 : :
195 [ # # ]: 0 : g_assert (TRACKER_IS_SPARQL_CURSOR (cursor));
196 : :
197 [ # # # # ]: 0 : if (!tracker_sparql_cursor_is_bound (cursor, CURSOR_CONTACT_UID) ||
198 : 0 : !tracker_sparql_cursor_is_bound (cursor, CURSOR_VCARD_DATA))
199 : 0 : g_return_val_if_reached (NULL);
200 : :
201 : 0 : uid = tracker_sparql_cursor_get_string (cursor, CURSOR_CONTACT_UID, NULL);
202 : 0 : vcard = tracker_sparql_cursor_get_string (cursor, CURSOR_VCARD_DATA, NULL);
203 : :
204 : 0 : return e_contact_new_from_vcard_with_uid (vcard, uid);
205 : : }
206 : :
207 : : static void
208 : 0 : cursor_lookup_medium_cb (TrackerSparqlCursor *cursor,
209 : : GAsyncResult *result,
210 : : gpointer user_data)
211 : : {
212 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
213 : 0 : const char *medium = g_task_get_task_data (task);
214 : 0 : EContact *contact = NULL;
215 [ # # ]: 0 : g_autoptr (GError) error = NULL;
216 : :
217 [ # # ]: 0 : if (tracker_sparql_cursor_next_finish (cursor, result, &error))
218 : 0 : contact = _e_contact_from_sparql_cursor (cursor);
219 [ # # ]: 0 : else if (error != NULL)
220 : 0 : g_debug ("%s(): %s", G_STRFUNC, error->message);
221 : :
222 [ # # ]: 0 : if (contact == NULL)
223 : : {
224 : 0 : g_autoptr (EPhoneNumber) number = NULL;
225 : :
226 : 0 : contact = e_contact_new ();
227 : 0 : number = e_phone_number_from_string (medium, NULL, NULL);
228 [ # # ]: 0 : if (number != NULL)
229 : : {
230 : 0 : g_autofree char *name = NULL;
231 : :
232 : 0 : name = e_phone_number_to_string (number,
233 : : E_PHONE_NUMBER_FORMAT_NATIONAL);
234 : 0 : e_contact_set (contact, E_CONTACT_FULL_NAME, name);
235 : 0 : e_contact_set (contact, E_CONTACT_PHONE_OTHER, medium);
236 : : }
237 : : else
238 : : {
239 : 0 : e_contact_set (contact, E_CONTACT_FULL_NAME, medium);
240 [ # # ]: 0 : if (g_strrstr (medium, "@") != NULL)
241 : 0 : e_contact_set (contact, E_CONTACT_EMAIL_1, medium);
242 : : else
243 : 0 : e_contact_set (contact, E_CONTACT_PHONE_OTHER, medium);
244 : : }
245 : : }
246 : :
247 : 0 : g_task_return_pointer (task, g_steal_pointer (&contact), g_object_unref);
248 [ # # ]: 0 : tracker_sparql_cursor_close (cursor);
249 : 0 : }
250 : :
251 : : static void
252 : 0 : execute_lookup_medium_cb (TrackerSparqlStatement *stmt,
253 : : GAsyncResult *result,
254 : : gpointer user_data)
255 : : {
256 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
257 [ # # # # ]: 0 : g_autoptr (TrackerSparqlCursor) cursor = NULL;
258 : 0 : GCancellable *cancellable = NULL;
259 : 0 : GError *error = NULL;
260 : :
261 : 0 : cursor = tracker_sparql_statement_execute_finish (stmt, result, &error);
262 [ # # ]: 0 : if (cursor == NULL)
263 : : {
264 : 0 : g_task_return_error (task, g_steal_pointer (&error));
265 [ # # ]: 0 : return;
266 : : }
267 : :
268 : 0 : cancellable = g_task_get_cancellable (G_TASK (result));
269 : 0 : tracker_sparql_cursor_next_async (cursor,
270 : : cancellable,
271 : : (GAsyncReadyCallback) cursor_lookup_medium_cb,
272 : : g_object_ref (task));
273 : : }
274 : :
275 : : #define LOOKUP_MEDIUM_FMT \
276 : : "SELECT ?contact ?uid ?vcardData \
277 : : WHERE { \
278 : : BIND(IRI(xsd:string(~medium)) AS ?contactMedium) \
279 : : ?contact nco:hasContactMedium ?contactMedium ; \
280 : : nco:contactUID ?uid ; \
281 : : nie:plainTextContent ?vcardData . \
282 : : } \
283 : : LIMIT 1"
284 : :
285 : : /**
286 : : * valent_contacts_adapter_reverse_lookup:
287 : : * @store: a `ValentContactsAdapter`
288 : : * @medium: a contact medium
289 : : * @cancellable: (nullable): `GCancellable`
290 : : * @callback: (scope async): a `GAsyncReadyCallback`
291 : : * @user_data: user supplied data
292 : : *
293 : : * A convenience wrapper for finding a contact by phone number or email address.
294 : : *
295 : : * Call [method@Valent.ContactsAdapter.reverse_lookup_finish] to get the result.
296 : : */
297 : : void
298 : 0 : valent_contacts_adapter_reverse_lookup (ValentContactsAdapter *adapter,
299 : : const char *medium,
300 : : GCancellable *cancellable,
301 : : GAsyncReadyCallback callback,
302 : : gpointer user_data)
303 : : {
304 : 0 : g_autoptr (TrackerSparqlConnection) connection = NULL;
305 [ # # ]: 0 : g_autoptr (TrackerSparqlStatement) stmt = NULL;
306 [ # # ]: 0 : g_autoptr (GTask) task = NULL;
307 : 0 : GError *error = NULL;
308 [ # # ]: 0 : g_autofree char *medium_iri = NULL;
309 : :
310 : 0 : VALENT_ENTRY;
311 : :
312 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONTACTS_ADAPTER (adapter));
313 [ # # # # ]: 0 : g_return_if_fail (medium != NULL && *medium != '\0');
314 [ # # # # : 0 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
# # # # ]
315 : :
316 : 0 : task = g_task_new (adapter, cancellable, callback, user_data);
317 [ # # ]: 0 : g_task_set_source_tag (task, valent_contacts_adapter_reverse_lookup);
318 [ # # ]: 0 : g_task_set_task_data (task, g_strdup (medium), g_free);
319 : :
320 [ # # ]: 0 : if (g_strrstr (medium, "@") != NULL)
321 : : {
322 : 0 : medium_iri = g_strdup_printf ("mailto:%s", medium);
323 : : }
324 : : else
325 : : {
326 : 0 : g_autoptr (EPhoneNumber) number = NULL;
327 : :
328 : 0 : number = e_phone_number_from_string (medium, NULL, NULL);
329 [ # # ]: 0 : if (number != NULL)
330 : 0 : medium_iri = e_phone_number_to_string (number, E_PHONE_NUMBER_FORMAT_RFC3966);
331 : : else
332 : 0 : medium_iri = g_strdup_printf ("tel:%s", medium);
333 : : }
334 : :
335 : 0 : g_object_get (adapter, "connection", &connection, NULL);
336 : 0 : stmt = tracker_sparql_connection_query_statement (connection,
337 : : LOOKUP_MEDIUM_FMT,
338 : : cancellable,
339 : : &error);
340 : :
341 [ # # ]: 0 : if (stmt == NULL)
342 : : {
343 : 0 : g_task_return_error (task, g_steal_pointer (&error));
344 : 0 : VALENT_EXIT;
345 : : }
346 : :
347 : 0 : tracker_sparql_statement_bind_string (stmt, "medium", medium_iri);
348 : 0 : tracker_sparql_statement_execute_async (stmt,
349 : : cancellable,
350 : : (GAsyncReadyCallback) execute_lookup_medium_cb,
351 : : g_object_ref (task));
352 : :
353 : 0 : VALENT_EXIT;
354 : : }
355 : :
356 : : /**
357 : : * valent_contacts_adapter_reverse_lookup_finish:
358 : : * @adapter: a `ValentContactsAdapter`
359 : : * @result: a `GAsyncResult`
360 : : * @error: (nullable): a `GError`
361 : : *
362 : : * Finish an operation started by [method@Valent.ContactsAdapter.reverse_lookup].
363 : : *
364 : : * Returns: (transfer full): an `EContact`
365 : : */
366 : : EContact *
367 : 0 : valent_contacts_adapter_reverse_lookup_finish (ValentContactsAdapter *adapter,
368 : : GAsyncResult *result,
369 : : GError **error)
370 : : {
371 [ # # ]: 0 : g_return_val_if_fail (VALENT_IS_CONTACTS_ADAPTER (adapter), NULL);
372 [ # # ]: 0 : g_return_val_if_fail (g_task_is_valid (result, adapter), NULL);
373 [ # # # # ]: 0 : g_return_val_if_fail (error == NULL || *error == NULL, NULL);
374 : :
375 : 0 : return g_task_propagate_pointer (G_TASK (result), error);
376 : : }
377 : :
378 : : static void
379 : 0 : cursor_search_contacts_cb (TrackerSparqlCursor *cursor,
380 : : GAsyncResult *result,
381 : : gpointer user_data)
382 : : {
383 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
384 : 0 : GCancellable *cancellable = g_task_get_cancellable (task);
385 : 0 : GListStore *contacts = g_task_get_task_data (task);
386 [ # # # # ]: 0 : g_autoptr (GError) error = NULL;
387 : :
388 [ # # ]: 0 : if (tracker_sparql_cursor_next_finish (cursor, result, &error))
389 : : {
390 [ # # ]: 0 : g_autoptr (EContact) contact = NULL;
391 : :
392 : 0 : contact = _e_contact_from_sparql_cursor (cursor);
393 [ # # ]: 0 : if (contact != NULL)
394 : 0 : g_list_store_append (contacts, contact);
395 : :
396 : 0 : tracker_sparql_cursor_next_async (cursor,
397 : : cancellable,
398 : : (GAsyncReadyCallback) cursor_search_contacts_cb,
399 : : g_object_ref (task));
400 [ # # ]: 0 : return;
401 : : }
402 : :
403 [ # # ]: 0 : if (error != NULL)
404 : 0 : g_task_return_error (task, g_steal_pointer (&error));
405 : : else
406 : 0 : g_task_return_pointer (task, g_object_ref (contacts), g_object_unref);
407 : :
408 [ # # ]: 0 : tracker_sparql_cursor_close (cursor);
409 : : }
410 : :
411 : : static void
412 : 0 : execute_search_contacts_cb (TrackerSparqlStatement *stmt,
413 : : GAsyncResult *result,
414 : : gpointer user_data)
415 : : {
416 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
417 : 0 : GCancellable *cancellable = g_task_get_cancellable (task);
418 [ # # # # ]: 0 : g_autoptr (TrackerSparqlCursor) cursor = NULL;
419 : 0 : GError *error = NULL;
420 : :
421 : 0 : cursor = tracker_sparql_statement_execute_finish (stmt, result, &error);
422 [ # # ]: 0 : if (cursor == NULL)
423 : : {
424 : 0 : g_task_return_error (task, g_steal_pointer (&error));
425 [ # # ]: 0 : return;
426 : : }
427 : :
428 : 0 : tracker_sparql_cursor_next_async (cursor,
429 : : cancellable,
430 : : (GAsyncReadyCallback) cursor_search_contacts_cb,
431 : : g_object_ref (task));
432 : : }
433 : :
434 : : /**
435 : : * valent_contacts_adapter_search:
436 : : * @adapter: a `ValentContactsAdapter`
437 : : * @query: a string to search for
438 : : * @cancellable: (nullable): a `GCancellable`
439 : : * @callback: (scope async): a `GAsyncReadyCallback`
440 : : * @user_data: user supplied data
441 : : *
442 : : * Search through all the contacts in @adapter and return the most recent message
443 : : * from each thread containing @query.
444 : : *
445 : : * Call [method@Valent.ContactsAdapter.search_contacts_finish] to get the result.
446 : : *
447 : : * Since: 1.0
448 : : */
449 : : void
450 : 0 : valent_contacts_adapter_search (ValentContactsAdapter *adapter,
451 : : const char *query,
452 : : GCancellable *cancellable,
453 : : GAsyncReadyCallback callback,
454 : : gpointer user_data)
455 : : {
456 : 0 : g_autoptr (TrackerSparqlStatement) stmt = NULL;
457 [ # # ]: 0 : g_autoptr (GTask) task = NULL;
458 [ # # ]: 0 : g_autofree char *query_sanitized = NULL;
459 : 0 : GError *error = NULL;
460 : :
461 : 0 : VALENT_ENTRY;
462 : :
463 [ # # ]: 0 : g_return_if_fail (VALENT_IS_CONTACTS_ADAPTER (adapter));
464 [ # # ]: 0 : g_return_if_fail (query != NULL);
465 [ # # # # : 0 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
# # # # ]
466 : :
467 : 0 : task = g_task_new (adapter, cancellable, callback, user_data);
468 [ # # ]: 0 : g_task_set_source_tag (task, valent_contacts_adapter_search);
469 : 0 : g_task_set_task_data (task, g_list_store_new (E_TYPE_CONTACT), g_object_unref);
470 : :
471 : 0 : stmt = g_object_dup_data (G_OBJECT (adapter),
472 : : "valent-contacts-adapter-search",
473 : : _g_object_dup0,
474 : : NULL);
475 : :
476 [ # # ]: 0 : if (stmt == NULL)
477 : : {
478 : 0 : g_autoptr (TrackerSparqlConnection) connection = NULL;
479 : :
480 : 0 : g_object_get (adapter, "connection", &connection, NULL);
481 : 0 : stmt = tracker_sparql_connection_load_statement_from_gresource (connection,
482 : : SEARCH_CONTACTS_RQ,
483 : : cancellable,
484 : : &error);
485 : :
486 [ # # ]: 0 : if (stmt == NULL)
487 : : {
488 : 0 : g_task_return_error (task, g_steal_pointer (&error));
489 [ # # ]: 0 : return;
490 : : }
491 : :
492 [ # # ]: 0 : g_object_set_data_full (G_OBJECT (adapter),
493 : : "valent-contacts-adapter-search",
494 : : g_object_ref (stmt),
495 : : g_object_unref);
496 : : }
497 : :
498 : 0 : query_sanitized = tracker_sparql_escape_string (query);
499 : 0 : tracker_sparql_statement_bind_string (stmt, "query", query_sanitized);
500 : 0 : tracker_sparql_statement_execute_async (stmt,
501 : : cancellable,
502 : : (GAsyncReadyCallback) execute_search_contacts_cb,
503 : : g_object_ref (task));
504 : :
505 : 0 : VALENT_EXIT;
506 : : }
507 : :
508 : : /**
509 : : * valent_contacts_adapter_search_finish:
510 : : * @adapter: a `ValentContactsAdapter`
511 : : * @result: a `GAsyncResult`
512 : : * @error: (nullable): a `GError`
513 : : *
514 : : * Finish an operation started by [method@Valent.ContactsAdapter.search].
515 : : *
516 : : * Returns: (transfer full) (element-type Valent.Message): a list of contacts
517 : : *
518 : : * Since: 1.0
519 : : */
520 : : GListModel *
521 : 0 : valent_contacts_adapter_search_finish (ValentContactsAdapter *adapter,
522 : : GAsyncResult *result,
523 : : GError **error)
524 : : {
525 : 0 : GListModel *ret;
526 : :
527 : 0 : VALENT_ENTRY;
528 : :
529 [ # # ]: 0 : g_return_val_if_fail (VALENT_IS_CONTACTS_ADAPTER (adapter), NULL);
530 [ # # ]: 0 : g_return_val_if_fail (g_task_is_valid (result, adapter), NULL);
531 [ # # # # ]: 0 : g_return_val_if_fail (error == NULL || *error == NULL, NULL);
532 : :
533 : 0 : ret = g_task_propagate_pointer (G_TASK (result), error);
534 : :
535 : 0 : VALENT_RETURN (ret);
536 : : }
537 : :
538 : : static void
539 : 0 : cursor_lookup_thread_cb (TrackerSparqlCursor *cursor,
540 : : GAsyncResult *result,
541 : : gpointer user_data)
542 : : {
543 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
544 [ # # ]: 0 : g_autoptr (GError) error = NULL;
545 : :
546 [ # # # # ]: 0 : if (tracker_sparql_cursor_next_finish (cursor, result, &error) &&
547 : 0 : tracker_sparql_cursor_is_bound (cursor, 0))
548 : 0 : {
549 : 0 : const char *iri = NULL;
550 : :
551 : 0 : iri = tracker_sparql_cursor_get_string (cursor, 0, NULL);
552 [ # # ]: 0 : g_task_return_pointer (task, g_strdup (iri), g_free);
553 : : }
554 : : else
555 : : {
556 [ # # ]: 0 : if (error == NULL)
557 : : {
558 : 0 : g_set_error_literal (&error,
559 : : G_IO_ERROR,
560 : : G_IO_ERROR_NOT_FOUND,
561 : : "Failed to find thread");
562 : : }
563 : :
564 : 0 : g_task_return_error (task, g_steal_pointer (&error));
565 : : }
566 : :
567 [ # # ]: 0 : tracker_sparql_cursor_close (cursor);
568 : 0 : }
569 : :
570 : : static void
571 : 0 : execute_lookup_thread_cb (TrackerSparqlStatement *stmt,
572 : : GAsyncResult *result,
573 : : gpointer user_data)
574 : : {
575 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
576 [ # # # # ]: 0 : g_autoptr (TrackerSparqlCursor) cursor = NULL;
577 : 0 : GCancellable *cancellable = NULL;
578 : 0 : GError *error = NULL;
579 : :
580 : 0 : cursor = tracker_sparql_statement_execute_finish (stmt, result, &error);
581 [ # # ]: 0 : if (cursor == NULL)
582 : : {
583 : 0 : g_task_return_error (task, g_steal_pointer (&error));
584 [ # # ]: 0 : return;
585 : : }
586 : :
587 : 0 : cancellable = g_task_get_cancellable (G_TASK (result));
588 : 0 : tracker_sparql_cursor_next_async (cursor,
589 : : cancellable,
590 : : (GAsyncReadyCallback) cursor_lookup_thread_cb,
591 : : g_object_ref (task));
592 : : }
593 : :
594 : : #define LOOKUP_THREAD_FMT \
595 : : "SELECT DISTINCT ?communicationChannel \
596 : : WHERE { \
597 : : VALUES ?specifiedIRIs { %s } \
598 : : ?communicationChannel vmo:hasParticipant ?participant . \
599 : : FILTER (?participant IN (%s)) \
600 : : FILTER NOT EXISTS { \
601 : : ?communicationChannel vmo:hasParticipant ?otherParticipant . \
602 : : FILTER (?otherParticipant NOT IN (%s)) \
603 : : } \
604 : : } \
605 : : GROUP BY ?communicationChannel \
606 : : HAVING (COUNT(DISTINCT ?participant) = %u)"
607 : :
608 : : /**
609 : : * valent_messages_adapter_lookup_thread:
610 : : * @adapter: a `ValentMessagesAdapter`
611 : : * @participants: a list of contact mediums
612 : : * @cancellable: (nullable): a `GCancellable`
613 : : * @callback: (scope async): a `GAsyncReadyCallback`
614 : : * @user_data: user supplied data
615 : : *
616 : : * Find the thread with @participants.
617 : : *
618 : : * Since: 1.0
619 : : */
620 : : void
621 : 0 : valent_messages_adapter_lookup_thread (ValentMessagesAdapter *adapter,
622 : : const char * const *participants,
623 : : GCancellable *cancellable,
624 : : GAsyncReadyCallback callback,
625 : : gpointer user_data)
626 : : {
627 : 0 : g_autoptr (TrackerSparqlConnection) connection = NULL;
628 [ # # ]: 0 : g_autoptr (TrackerSparqlStatement) stmt = NULL;
629 [ # # ]: 0 : g_autoptr (GTask) task = NULL;
630 [ # # ]: 0 : g_autoptr (GStrvBuilder) builder = NULL;
631 [ # # ]: 0 : g_auto (GStrv) iriv = NULL;
632 [ # # ]: 0 : g_autofree char *iris = NULL;
633 : 0 : g_autofree char *values = NULL;
634 : 0 : g_autofree char *sparql = NULL;
635 : 0 : GError *error = NULL;
636 : :
637 : 0 : VALENT_ENTRY;
638 : :
639 [ # # ]: 0 : g_return_if_fail (VALENT_IS_MESSAGES_ADAPTER (adapter));
640 [ # # # # ]: 0 : g_return_if_fail (participants != NULL && participants[0] != NULL);
641 [ # # # # : 0 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
# # # # ]
642 : :
643 : 0 : task = g_task_new (adapter, cancellable, callback, user_data);
644 [ # # ]: 0 : g_task_set_source_tag (task, valent_messages_adapter_lookup_thread);
645 : 0 : g_task_set_task_data (task,
646 : 0 : g_ptr_array_new_with_free_func (g_object_unref),
647 : : (GDestroyNotify)g_ptr_array_unref);
648 : :
649 : 0 : builder = g_strv_builder_new ();
650 [ # # ]: 0 : for (size_t i = 0; participants[i] != NULL; i++)
651 : : {
652 : 0 : g_autofree char *iri = NULL;
653 : :
654 [ # # ]: 0 : if (g_strrstr (participants[i], "@"))
655 : : {
656 : 0 : iri = g_strdup_printf ("<mailto:%s>", participants[i]);
657 : : }
658 : : else
659 : : {
660 : 0 : g_autoptr (EPhoneNumber) number = NULL;
661 : :
662 : 0 : number = e_phone_number_from_string (participants[i], NULL, NULL);
663 [ # # ]: 0 : if (number != NULL)
664 : : {
665 : 0 : g_autofree char *uri = NULL;
666 : :
667 : 0 : uri = e_phone_number_to_string (number, E_PHONE_NUMBER_FORMAT_RFC3966);
668 : 0 : iri = g_strdup_printf ("<%s>", uri);
669 : : }
670 : : }
671 : :
672 [ # # ]: 0 : if (iri != NULL)
673 : 0 : g_strv_builder_take (builder, g_steal_pointer (&iri));
674 : : }
675 : 0 : iriv = g_strv_builder_end (builder);
676 : :
677 : 0 : iris = g_strjoinv (", ", iriv);
678 : 0 : values = g_strjoinv (" ", iriv);
679 : 0 : sparql = g_strdup_printf (LOOKUP_THREAD_FMT,
680 : : values,
681 : : iris,
682 : : iris,
683 : : g_strv_length ((GStrv)iriv));
684 : :
685 : 0 : g_object_get (adapter, "connection", &connection, NULL);
686 : 0 : stmt = tracker_sparql_connection_query_statement (connection,
687 : : sparql,
688 : : cancellable,
689 : : &error);
690 : :
691 [ # # ]: 0 : if (stmt == NULL)
692 : : {
693 : 0 : g_task_return_error (task, g_steal_pointer (&error));
694 : 0 : VALENT_EXIT;
695 : : }
696 : :
697 : 0 : tracker_sparql_statement_execute_async (stmt,
698 : : cancellable,
699 : : (GAsyncReadyCallback) execute_lookup_thread_cb,
700 : : g_object_ref (task));
701 : :
702 : 0 : VALENT_EXIT;
703 : : }
704 : :
705 : : /**
706 : : * valent_messages_adapter_lookup_thread_finish:
707 : : * @adapter: a `ValentMessagesAdapter`
708 : : * @result: a `GAsyncResult`
709 : : * @error: (nullable): a `GError`
710 : : *
711 : : * Finish an operation started by valent_contact_adapter_lookup_contact().
712 : : *
713 : : * Returns: (transfer full): an `EContact`
714 : : */
715 : : GListModel *
716 : 0 : valent_messages_adapter_lookup_thread_finish (ValentMessagesAdapter *adapter,
717 : : GAsyncResult *result,
718 : : GError **error)
719 : : {
720 : 0 : GListModel *ret = NULL;
721 : 0 : g_autofree char *iri = NULL;
722 : :
723 : 0 : VALENT_ENTRY;
724 : :
725 [ # # ]: 0 : g_return_val_if_fail (VALENT_IS_MESSAGES_ADAPTER (adapter), NULL);
726 [ # # ]: 0 : g_return_val_if_fail (g_task_is_valid (result, adapter), NULL);
727 [ # # # # ]: 0 : g_return_val_if_fail (error == NULL || *error == NULL, NULL);
728 : :
729 : 0 : iri = g_task_propagate_pointer (G_TASK (result), error);
730 [ # # ]: 0 : if (iri != NULL)
731 : : {
732 : 0 : unsigned int n_threads = g_list_model_get_n_items (G_LIST_MODEL (adapter));
733 : :
734 [ # # ]: 0 : for (unsigned int i = 0; i < n_threads; i++)
735 : : {
736 : 0 : g_autoptr (GListModel) thread = NULL;
737 [ # # ]: 0 : g_autofree char *thread_iri = NULL;
738 : :
739 : 0 : thread = g_list_model_get_item (G_LIST_MODEL (adapter), i);
740 : 0 : g_object_get (thread, "iri", &thread_iri, NULL);
741 : :
742 [ # # ]: 0 : if (g_strcmp0 (iri, thread_iri) == 0)
743 : : {
744 : 0 : ret = g_steal_pointer (&thread);
745 : 0 : break;
746 : : }
747 : : }
748 : : }
749 : :
750 : 0 : VALENT_RETURN (ret);
751 : : }
752 : :
753 : : static ValentMessage *
754 : 0 : valent_message_from_sparql_cursor (TrackerSparqlCursor *cursor,
755 : : ValentMessage *current)
756 : : {
757 : 0 : ValentMessage *ret = NULL;
758 : 0 : int64_t message_id;
759 : :
760 [ # # ]: 0 : g_assert (TRACKER_IS_SPARQL_CURSOR (cursor));
761 [ # # # # ]: 0 : g_assert (current == NULL || VALENT_IS_MESSAGE (current));
762 : :
763 : 0 : message_id = tracker_sparql_cursor_get_integer (cursor, CURSOR_MESSAGE_ID);
764 [ # # # # ]: 0 : if (current != NULL && valent_message_get_id (current) == message_id)
765 : : {
766 : 0 : ret = g_object_ref (current);
767 : : }
768 : : else
769 : : {
770 : 0 : const char *iri = tracker_sparql_cursor_get_string (cursor, CURSOR_MESSAGE_IRI, NULL);
771 : 0 : g_autoptr (GListStore) attachments = NULL;
772 : 0 : ValentMessageBox box = VALENT_MESSAGE_BOX_ALL;
773 : 0 : int64_t date = 0;
774 [ # # ]: 0 : g_autoptr (GDateTime) datetime = NULL;
775 : 0 : gboolean read = FALSE;
776 : 0 : const char *recipients = NULL;
777 [ # # ]: 0 : g_auto (GStrv) recipientv = NULL;
778 : 0 : const char *sender = NULL;
779 : 0 : int64_t subscription_id = -1;
780 : 0 : const char *text = NULL;
781 : 0 : int64_t thread_id = -1;
782 : :
783 : 0 : attachments = g_list_store_new (VALENT_TYPE_MESSAGE_ATTACHMENT);
784 : 0 : box = tracker_sparql_cursor_get_integer (cursor, CURSOR_MESSAGE_BOX);
785 : :
786 : 0 : datetime = tracker_sparql_cursor_get_datetime (cursor, CURSOR_MESSAGE_DATE);
787 [ # # ]: 0 : if (datetime != NULL)
788 : 0 : date = g_date_time_to_unix_usec (datetime) / 1000;
789 : :
790 : 0 : read = tracker_sparql_cursor_get_boolean (cursor, CURSOR_MESSAGE_READ);
791 : :
792 : 0 : recipients = tracker_sparql_cursor_get_string (cursor, CURSOR_MESSAGE_RECIPIENTS, NULL);
793 [ # # ]: 0 : if (recipients != NULL)
794 : 0 : recipientv = g_strsplit (recipients, ",", -1);
795 : :
796 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, CURSOR_MESSAGE_SENDER))
797 : 0 : sender = tracker_sparql_cursor_get_string (cursor, CURSOR_MESSAGE_SENDER, NULL);
798 : :
799 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, CURSOR_MESSAGE_SUBSCRIPTION_ID))
800 : 0 : subscription_id = tracker_sparql_cursor_get_integer (cursor, CURSOR_MESSAGE_SUBSCRIPTION_ID);
801 : :
802 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, CURSOR_MESSAGE_TEXT))
803 : 0 : text = tracker_sparql_cursor_get_string (cursor, CURSOR_MESSAGE_TEXT, NULL);
804 : :
805 : 0 : thread_id = tracker_sparql_cursor_get_integer (cursor, CURSOR_MESSAGE_THREAD_ID);
806 : :
807 [ # # ]: 0 : ret = g_object_new (VALENT_TYPE_MESSAGE,
808 : : "iri", iri,
809 : : "box", box,
810 : : "date", date,
811 : : "id", message_id,
812 : : "read", read,
813 : : "recipients", recipientv,
814 : : "sender", sender,
815 : : "subscription-id", subscription_id,
816 : : "text", text,
817 : : "thread-id", thread_id,
818 : : "attachments", attachments,
819 : : NULL);
820 : : }
821 : :
822 : : /* Attachment
823 : : */
824 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, CURSOR_MESSAGE_ATTACHMENT_IRI))
825 : : {
826 : 0 : const char *iri = tracker_sparql_cursor_get_string (cursor, CURSOR_MESSAGE_ATTACHMENT_IRI, NULL);
827 : 0 : GListModel *attachments = valent_message_get_attachments (ret);
828 : 0 : g_autoptr (ValentMessageAttachment) attachment = NULL;
829 [ # # ]: 0 : g_autoptr (GIcon) preview = NULL;
830 [ # # ]: 0 : g_autoptr (GFile) file = NULL;
831 : :
832 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, CURSOR_MESSAGE_ATTACHMENT_PREVIEW))
833 : : {
834 : 0 : const char *base64_data;
835 : :
836 : 0 : base64_data = tracker_sparql_cursor_get_string (cursor, CURSOR_MESSAGE_ATTACHMENT_PREVIEW, NULL);
837 [ # # ]: 0 : if (base64_data != NULL)
838 : : {
839 : 0 : g_autoptr (GBytes) bytes = NULL;
840 : 0 : unsigned char *data;
841 : 0 : size_t len;
842 : :
843 : 0 : data = g_base64_decode (base64_data, &len);
844 : 0 : bytes = g_bytes_new_take (g_steal_pointer (&data), len);
845 [ # # ]: 0 : preview = g_bytes_icon_new (bytes);
846 : : }
847 : : }
848 : :
849 [ # # ]: 0 : if (tracker_sparql_cursor_is_bound (cursor, CURSOR_MESSAGE_ATTACHMENT_FILE))
850 : : {
851 : 0 : const char *file_uri;
852 : :
853 : 0 : file_uri = tracker_sparql_cursor_get_string (cursor, CURSOR_MESSAGE_ATTACHMENT_FILE, NULL);
854 [ # # ]: 0 : if (file_uri != NULL)
855 : 0 : file = g_file_new_for_uri (file_uri);
856 : : }
857 : :
858 : 0 : attachment = g_object_new (VALENT_TYPE_MESSAGE_ATTACHMENT,
859 : : "iri", iri,
860 : : "preview", preview,
861 : : "file", file,
862 : : NULL);
863 [ # # ]: 0 : g_list_store_append (G_LIST_STORE (attachments), attachment);
864 : : }
865 : :
866 : 0 : return g_steal_pointer (&ret);
867 : : }
868 : :
869 : : static void
870 : 0 : cursor_search_messages_cb (TrackerSparqlCursor *cursor,
871 : : GAsyncResult *result,
872 : : gpointer user_data)
873 : : {
874 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
875 : 0 : GListStore *messages = g_task_get_task_data (task);
876 [ # # # # ]: 0 : g_autoptr (GError) error = NULL;
877 : :
878 [ # # ]: 0 : if (tracker_sparql_cursor_next_finish (cursor, result, &error))
879 : : {
880 [ # # ]: 0 : g_autoptr (ValentMessage) current = NULL;
881 [ # # ]: 0 : g_autoptr (ValentMessage) message = NULL;
882 : 0 : unsigned int n_items = 0;
883 : :
884 : 0 : n_items = g_list_model_get_n_items (G_LIST_MODEL (messages));
885 [ # # ]: 0 : if (n_items > 0)
886 : 0 : current = g_list_model_get_item (G_LIST_MODEL (messages), n_items - 1);
887 : :
888 : 0 : message = valent_message_from_sparql_cursor (cursor, current);
889 [ # # ]: 0 : if (message != current)
890 : 0 : g_list_store_append (messages, message);
891 : :
892 : 0 : tracker_sparql_cursor_next_async (cursor,
893 : : g_task_get_cancellable (task),
894 : : (GAsyncReadyCallback) cursor_search_messages_cb,
895 : : g_object_ref (task));
896 [ # # ]: 0 : return;
897 : : }
898 : :
899 [ # # ]: 0 : if (error != NULL)
900 : 0 : g_task_return_error (task, g_steal_pointer (&error));
901 : : else
902 : 0 : g_task_return_pointer (task, g_object_ref (messages), g_object_unref);
903 : :
904 [ # # ]: 0 : tracker_sparql_cursor_close (cursor);
905 : : }
906 : :
907 : : static void
908 : 0 : execute_search_messages_cb (TrackerSparqlStatement *stmt,
909 : : GAsyncResult *result,
910 : : gpointer user_data)
911 : : {
912 : 0 : g_autoptr (GTask) task = G_TASK (g_steal_pointer (&user_data));
913 : 0 : GCancellable *cancellable = g_task_get_cancellable (task);
914 [ # # # # ]: 0 : g_autoptr (TrackerSparqlCursor) cursor = NULL;
915 : 0 : GError *error = NULL;
916 : :
917 : 0 : cursor = tracker_sparql_statement_execute_finish (stmt, result, &error);
918 [ # # ]: 0 : if (cursor == NULL)
919 : : {
920 : 0 : g_task_return_error (task, g_steal_pointer (&error));
921 [ # # ]: 0 : return;
922 : : }
923 : :
924 : 0 : tracker_sparql_cursor_next_async (cursor,
925 : : cancellable,
926 : : (GAsyncReadyCallback) cursor_search_messages_cb,
927 : : g_object_ref (task));
928 : : }
929 : :
930 : : /**
931 : : * valent_messages_adapter_search:
932 : : * @adapter: a `ValentMessagesAdapter`
933 : : * @query: a string to search for
934 : : * @cancellable: (nullable): a `GCancellable`
935 : : * @callback: (scope async): a `GAsyncReadyCallback`
936 : : * @user_data: user supplied data
937 : : *
938 : : * Search through all the messages in @adapter and return the most recent message
939 : : * from each thread containing @query.
940 : : *
941 : : * Call [method@Valent.MessagesAdapter.search_finish] to get the result.
942 : : *
943 : : * Since: 1.0
944 : : */
945 : : void
946 : 0 : valent_messages_adapter_search (ValentMessagesAdapter *adapter,
947 : : const char *query,
948 : : GCancellable *cancellable,
949 : : GAsyncReadyCallback callback,
950 : : gpointer user_data)
951 : : {
952 : 0 : g_autoptr (TrackerSparqlStatement) stmt = NULL;
953 [ # # ]: 0 : g_autoptr (GTask) task = NULL;
954 [ # # ]: 0 : g_autofree char *query_sanitized = NULL;
955 : 0 : GError *error = NULL;
956 : :
957 : 0 : VALENT_ENTRY;
958 : :
959 [ # # ]: 0 : g_return_if_fail (VALENT_IS_MESSAGES_ADAPTER (adapter));
960 [ # # ]: 0 : g_return_if_fail (query != NULL);
961 [ # # # # : 0 : g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
# # # # ]
962 : :
963 : 0 : task = g_task_new (adapter, cancellable, callback, user_data);
964 [ # # ]: 0 : g_task_set_source_tag (task, valent_messages_adapter_search);
965 : 0 : g_task_set_task_data (task, g_list_store_new (VALENT_TYPE_MESSAGE), g_object_unref);
966 : :
967 : 0 : stmt = g_object_dup_data (G_OBJECT (adapter),
968 : : "valent-message-adapter-search",
969 : : _g_object_dup0,
970 : : NULL);
971 : :
972 [ # # ]: 0 : if (stmt == NULL)
973 : : {
974 : 0 : g_autoptr (TrackerSparqlConnection) connection = NULL;
975 : :
976 : 0 : g_object_get (adapter, "connection", &connection, NULL);
977 : 0 : stmt = tracker_sparql_connection_load_statement_from_gresource (connection,
978 : : SEARCH_MESSAGES_RQ,
979 : : cancellable,
980 : : &error);
981 : :
982 [ # # ]: 0 : if (stmt == NULL)
983 : : {
984 : 0 : g_task_return_error (task, g_steal_pointer (&error));
985 [ # # ]: 0 : return;
986 : : }
987 : :
988 [ # # ]: 0 : g_object_set_data_full (G_OBJECT (adapter),
989 : : "valent-message-adapter-search",
990 : : g_object_ref (stmt),
991 : : g_object_unref);
992 : : }
993 : :
994 : 0 : query_sanitized = tracker_sparql_escape_string (query);
995 : 0 : tracker_sparql_statement_bind_string (stmt, "query", query_sanitized);
996 : 0 : tracker_sparql_statement_execute_async (stmt,
997 : : g_task_get_cancellable (task),
998 : : (GAsyncReadyCallback) execute_search_messages_cb,
999 : : g_object_ref (task));
1000 : :
1001 : 0 : VALENT_EXIT;
1002 : : }
1003 : :
1004 : : /**
1005 : : * valent_messages_adapter_search_finish:
1006 : : * @adapter: a `ValentMessagesAdapter`
1007 : : * @result: a `GAsyncResult`
1008 : : * @error: (nullable): a `GError`
1009 : : *
1010 : : * Finish an operation started by [method@Valent.MessagesAdapter.search].
1011 : : *
1012 : : * Returns: (transfer full) (element-type Valent.Message): a list of messages
1013 : : *
1014 : : * Since: 1.0
1015 : : */
1016 : : GListModel *
1017 : 0 : valent_messages_adapter_search_finish (ValentMessagesAdapter *adapter,
1018 : : GAsyncResult *result,
1019 : : GError **error)
1020 : : {
1021 : 0 : GListModel *ret;
1022 : :
1023 : 0 : VALENT_ENTRY;
1024 : :
1025 [ # # ]: 0 : g_return_val_if_fail (VALENT_IS_MESSAGES_ADAPTER (adapter), NULL);
1026 [ # # ]: 0 : g_return_val_if_fail (g_task_is_valid (result, adapter), NULL);
1027 [ # # # # ]: 0 : g_return_val_if_fail (error == NULL || *error == NULL, NULL);
1028 : :
1029 : 0 : ret = g_task_propagate_pointer (G_TASK (result), error);
1030 : :
1031 : 0 : VALENT_RETURN (ret);
1032 : : }
1033 : :
|