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-sms-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <glib/gi18n.h>
9 : : #include <gio/gio.h>
10 : : #include <json-glib/json-glib.h>
11 : : #include <valent.h>
12 : :
13 : : #include "valent-message.h"
14 : : #include "valent-sms-plugin.h"
15 : : #include "valent-sms-store.h"
16 : : #include "valent-sms-window.h"
17 : :
18 : :
19 : : struct _ValentSmsPlugin
20 : : {
21 : : ValentDevicePlugin parent_instance;
22 : :
23 : : ValentSmsStore *store;
24 : : GtkWindow *window;
25 : : };
26 : :
27 : : static ValentMessage * valent_sms_plugin_deserialize_message (ValentSmsPlugin *self,
28 : : JsonNode *node);
29 : : static void valent_sms_plugin_request (ValentSmsPlugin *self,
30 : : ValentMessage *message);
31 : : static void valent_sms_plugin_request_conversation (ValentSmsPlugin *self,
32 : : int64_t thread_id,
33 : : int64_t start_date,
34 : : int64_t max_results);
35 : : static void valent_sms_plugin_request_conversations (ValentSmsPlugin *self);
36 : :
37 [ + + + - ]: 41 : G_DEFINE_FINAL_TYPE (ValentSmsPlugin, valent_sms_plugin, VALENT_TYPE_DEVICE_PLUGIN)
38 : :
39 : :
40 : : /**
41 : : * message_hash:
42 : : * @message_id: a message ID
43 : : * @message_text: (nullable): a message text
44 : : *
45 : : * Shift the lower 32-bits of @message_id to the upper 32-bits of a 64-bit
46 : : * integer, then set the lower 32-bits to a djb2 hash of @message_text.
47 : : *
48 : : * This hack is necessary because kdeconnect-android pulls SMS and MMS from
49 : : * separate tables so two messages (even in the same thread) may share an ID.
50 : : * The timestamp would be an ideal alternative except that it can change,
51 : : * possibly when moved between boxes (eg. outbox => sent).
52 : : *
53 : : * Returns: a unique ID
54 : : */
55 : : static inline int64_t
56 : 3 : message_hash (int64_t message_id,
57 : : const char *message_text)
58 : : {
59 : 3 : uint32_t hash = 5381;
60 : :
61 [ + - ]: 3 : if G_UNLIKELY (message_text == NULL)
62 : : message_text = "";
63 : :
64 : : // djb2
65 [ + + ]: 52 : for (unsigned int i = 0; message_text[i]; i++)
66 : 49 : hash = ((hash << 5L) + hash) + message_text[i]; /* hash * 33 + c */
67 : :
68 : 3 : return (((uint64_t) message_id) << 32) | hash;
69 : : }
70 : :
71 : : static ValentMessage *
72 : 3 : valent_sms_plugin_deserialize_message (ValentSmsPlugin *self,
73 : : JsonNode *node)
74 : : {
75 : 3 : JsonObject *object;
76 : 3 : JsonNode *addr_node;
77 : 3 : GVariant *addresses;
78 : 3 : GVariantDict dict;
79 : :
80 : 3 : ValentMessageBox box;
81 : 3 : int64_t date;
82 : 3 : int64_t id;
83 : 3 : GVariant *metadata;
84 : 3 : int64_t read;
85 : 3 : const char *sender = NULL;
86 : 3 : const char *text = NULL;
87 : 3 : int64_t thread_id;
88 : 3 : ValentMessageFlags event = VALENT_MESSAGE_FLAGS_UNKNOWN;
89 : 3 : int64_t sub_id = -1;
90 : :
91 [ + - ]: 3 : g_assert (VALENT_IS_SMS_PLUGIN (self));
92 [ - + ]: 3 : g_assert (JSON_NODE_HOLDS_OBJECT (node));
93 : :
94 : 3 : object = json_node_get_object (node);
95 : :
96 : : /* Check all the required fields exist */
97 [ + - + - : 3 : if G_UNLIKELY (!json_object_has_member (object, "thread_id") ||
+ - + - +
- + - +
- ]
98 : : !json_object_has_member (object, "_id") ||
99 : : !json_object_has_member (object, "body") ||
100 : : !json_object_has_member (object, "date") ||
101 : : !json_object_has_member (object, "read") ||
102 : : !json_object_has_member (object, "type") ||
103 : : !json_object_has_member (object, "addresses"))
104 : : {
105 : 0 : g_warning ("%s(): missing required message field", G_STRFUNC);
106 : 0 : return NULL;
107 : : }
108 : :
109 : : /* Basic fields */
110 : 3 : box = json_object_get_int_member (object, "type");
111 : 3 : date = json_object_get_int_member (object, "date");
112 : 3 : id = json_object_get_int_member (object, "_id");
113 : 3 : read = json_object_get_int_member (object, "read");
114 : 3 : text = json_object_get_string_member (object, "body");
115 : 3 : thread_id = json_object_get_int_member (object, "thread_id");
116 : :
117 : : /* Addresses */
118 : 3 : addr_node = json_object_get_member (object, "addresses");
119 : 3 : addresses = json_gvariant_deserialize (addr_node, "aa{sv}", NULL);
120 : :
121 : : /* If incoming, the first address will be the sender */
122 [ + + ]: 3 : if (box == VALENT_MESSAGE_BOX_INBOX)
123 : : {
124 : 1 : JsonObject *sender_obj;
125 : 1 : JsonArray *addr_array;
126 : :
127 : 1 : addr_array = json_node_get_array (addr_node);
128 : :
129 [ + - ]: 1 : if (json_array_get_length (addr_array) > 0)
130 : : {
131 : 1 : sender_obj = json_array_get_object_element (addr_array, 0);
132 : 1 : sender = json_object_get_string_member (sender_obj, "address");
133 : : }
134 : : else
135 : 0 : g_warning ("No address for message %"G_GINT64_FORMAT" in thread %"G_GINT64_FORMAT, id, thread_id);
136 : : }
137 : :
138 : : /* TODO: The `event` and `sub_id` fields are currently not implemented */
139 [ + - ]: 3 : if (json_object_has_member (object, "event"))
140 : 3 : event = json_object_get_int_member (object, "event");
141 : :
142 [ + - ]: 3 : if (json_object_has_member (object, "sub_id"))
143 : 3 : sub_id = json_object_get_int_member (object, "sub_id");
144 : :
145 : : /* HACK: try to create a truly unique ID from a potentially non-unique ID */
146 : 3 : id = message_hash (id, text);
147 : :
148 : : /* Build the metadata dictionary */
149 : 3 : g_variant_dict_init (&dict, NULL);
150 : 3 : g_variant_dict_insert_value (&dict, "addresses", addresses);
151 : 3 : g_variant_dict_insert (&dict, "event", "u", event);
152 : 3 : g_variant_dict_insert (&dict, "sub_id", "i", sub_id);
153 : 3 : metadata = g_variant_dict_end (&dict);
154 : :
155 : : /* Build and return the message object */
156 : 3 : return g_object_new (VALENT_TYPE_MESSAGE,
157 : : "box", box,
158 : : "date", date,
159 : : "id", id,
160 : : "metadata", metadata,
161 : : "read", read,
162 : : "sender", sender,
163 : : "text", text,
164 : : "thread-id", thread_id,
165 : : NULL);
166 : : }
167 : :
168 : : /**
169 : : * messages_is_thread:
170 : : * @messages: a `JsonArray`
171 : : *
172 : : * Check if @messages is a thread of messages, or a summary of threads.
173 : : *
174 : : * Returns: %TRUE if @messages is a conversation thread
175 : : */
176 : : static gboolean
177 : 3 : messages_is_thread (JsonArray *messages)
178 : : {
179 : 3 : JsonObject *message;
180 : 3 : int64_t first, second;
181 : :
182 : : /* TODO: A thread with a single message can't be distinguished from
183 : : * a summary with a single thread; in fact both could be true.
184 : : * If we assume the latter is true exclusively, we will get
185 : : * caught in a loop requesting the full thread. */
186 [ + + ]: 3 : if (json_array_get_length (messages) < 2)
187 : : return TRUE;
188 : :
189 : 2 : message = json_array_get_object_element (messages, 0);
190 : 2 : first = json_object_get_int_member (message, "thread_id");
191 : :
192 : 2 : message = json_array_get_object_element (messages, 1);
193 : 2 : second = json_object_get_int_member (message, "thread_id");
194 : :
195 : 2 : return first == second;
196 : : }
197 : :
198 : : static void
199 : 2 : valent_sms_plugin_handle_thread (ValentSmsPlugin *self,
200 : : JsonArray *messages)
201 : : {
202 : 4 : g_autoptr (GPtrArray) results = NULL;
203 : 2 : unsigned int n_messages;
204 : :
205 [ + - ]: 2 : g_assert (VALENT_IS_SMS_PLUGIN (self));
206 [ - + ]: 2 : g_assert (messages != NULL);
207 : :
208 : : /* Handle each message */
209 : 2 : n_messages = json_array_get_length (messages);
210 : 2 : results = g_ptr_array_new_with_free_func (g_object_unref);
211 : :
212 [ + + ]: 5 : for (unsigned int i = 0; i < n_messages; i++)
213 : : {
214 : 3 : JsonNode *message_node;
215 : 3 : ValentMessage *message;
216 : :
217 : 3 : message_node = json_array_get_element (messages, i);
218 : 3 : message = valent_sms_plugin_deserialize_message (self, message_node);
219 : 3 : g_ptr_array_add (results, message);
220 : : }
221 : :
222 [ + - ]: 2 : valent_sms_store_add_messages (self->store, results, NULL, NULL, NULL);
223 : 2 : }
224 : :
225 : : static void
226 : 3 : valent_sms_plugin_handle_messages (ValentSmsPlugin *self,
227 : : JsonNode *packet)
228 : : {
229 : 3 : JsonObject *body;
230 : 3 : JsonArray *messages;
231 : 3 : unsigned int n_messages;
232 : :
233 [ + - ]: 3 : g_assert (VALENT_IS_SMS_PLUGIN (self));
234 [ - + ]: 3 : g_assert (VALENT_IS_PACKET (packet));
235 : :
236 : 3 : body = valent_packet_get_body (packet);
237 : 3 : messages = json_object_get_array_member (body, "messages");
238 : 3 : n_messages = json_array_get_length (messages);
239 : :
240 : : /* This would typically mean "all threads have been deleted", but it's more
241 : : * reasonable to assume this was the result of an error. */
242 [ + - ]: 3 : if (n_messages == 0)
243 : : return;
244 : :
245 : : /* If this is a thread of messages we'll add them to the store */
246 [ + + ]: 3 : if (messages_is_thread (messages))
247 : : {
248 : 2 : valent_sms_plugin_handle_thread (self, messages);
249 : 2 : return;
250 : : }
251 : :
252 : : /* If this is a summary of threads we'll request each new thread */
253 [ + + ]: 3 : for (unsigned int i = 0; i < n_messages; i++)
254 : : {
255 : 2 : JsonObject *message;
256 : 2 : int64_t thread_id;
257 : 2 : int64_t thread_date;
258 : 2 : int64_t cache_date;
259 : :
260 : 2 : message = json_array_get_object_element (messages, i);
261 : 2 : thread_id = json_object_get_int_member (message, "thread_id");
262 : 2 : thread_date = json_object_get_int_member (message, "date");
263 : :
264 : : /* Get the last cached date and compare timestamps */
265 : 2 : cache_date = valent_sms_store_get_thread_date (self->store, thread_id);
266 : :
267 [ + - ]: 2 : if (cache_date < thread_date)
268 : 2 : valent_sms_plugin_request_conversation (self, thread_id, cache_date, 0);
269 : : }
270 : : }
271 : :
272 : : static void
273 : 2 : valent_sms_plugin_request_conversation (ValentSmsPlugin *self,
274 : : int64_t thread_id,
275 : : int64_t start_date,
276 : : int64_t max_results)
277 : : {
278 : 2 : g_autoptr (JsonBuilder) builder = NULL;
279 [ - + - - ]: 2 : g_autoptr (JsonNode) packet = NULL;
280 : :
281 [ + - ]: 2 : g_return_if_fail (VALENT_IS_SMS_PLUGIN (self));
282 [ - + ]: 2 : g_return_if_fail (thread_id >= 0);
283 : :
284 : 2 : valent_packet_init (&builder, "kdeconnect.sms.request_conversation");
285 : 2 : json_builder_set_member_name (builder, "threadID");
286 : 2 : json_builder_add_int_value (builder, thread_id);
287 : :
288 [ - + ]: 2 : if (start_date > 0)
289 : : {
290 : 0 : json_builder_set_member_name (builder, "rangeStartTimestamp");
291 : 0 : json_builder_add_int_value (builder, start_date);
292 : : }
293 : :
294 [ - + ]: 2 : if (max_results > 0)
295 : : {
296 : 0 : json_builder_set_member_name (builder, "numberToRequest");
297 : 0 : json_builder_add_int_value (builder, max_results);
298 : : }
299 : :
300 : 2 : packet = valent_packet_end (&builder);
301 : :
302 [ + - ]: 2 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
303 : : }
304 : :
305 : : static void
306 : 4 : valent_sms_plugin_request_conversations (ValentSmsPlugin *self)
307 : : {
308 : 4 : g_autoptr (JsonBuilder) builder = NULL;
309 [ - - - + ]: 4 : g_autoptr (JsonNode) packet = NULL;
310 : :
311 [ + - - - ]: 4 : g_return_if_fail (VALENT_IS_SMS_PLUGIN (self));
312 : :
313 : 4 : valent_packet_init (&builder, "kdeconnect.sms.request_conversations");
314 : 4 : packet = valent_packet_end (&builder);
315 : :
316 [ + - ]: 4 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
317 : : }
318 : :
319 : : static void
320 : 0 : valent_sms_plugin_request (ValentSmsPlugin *self,
321 : : ValentMessage *message)
322 : : {
323 : 0 : g_autoptr (JsonBuilder) builder = NULL;
324 [ # # # # ]: 0 : g_autoptr (JsonNode) packet = NULL;
325 : 0 : GVariant *metadata;
326 [ # # # # ]: 0 : g_autoptr (GVariant) addresses = NULL;
327 : 0 : JsonNode *addresses_node = NULL;
328 : 0 : int sub_id = -1;
329 : 0 : const char *text;
330 : :
331 [ # # ]: 0 : g_return_if_fail (VALENT_IS_SMS_PLUGIN (self));
332 [ # # ]: 0 : g_return_if_fail (VALENT_IS_MESSAGE (message));
333 : :
334 : : // Get the data
335 [ # # ]: 0 : if ((metadata = valent_message_get_metadata (message)) == NULL)
336 : 0 : g_return_if_reached ();
337 : :
338 [ # # ]: 0 : if ((addresses = g_variant_lookup_value (metadata, "addresses", NULL)) == NULL)
339 : 0 : g_return_if_reached ();
340 : :
341 [ # # ]: 0 : if (!g_variant_lookup (metadata, "sub_id", "i", &sub_id))
342 : 0 : sub_id = -1;
343 : :
344 : : // Build the packet
345 : 0 : valent_packet_init (&builder, "kdeconnect.sms.request");
346 : :
347 : 0 : json_builder_set_member_name (builder, "version");
348 : 0 : json_builder_add_int_value (builder, 2);
349 : :
350 : 0 : addresses_node = json_gvariant_serialize (addresses);
351 : 0 : json_builder_set_member_name (builder, "addresses");
352 : 0 : json_builder_add_value (builder, addresses_node);
353 : :
354 : 0 : text = valent_message_get_text (message);
355 : 0 : json_builder_set_member_name (builder, "messageBody");
356 : 0 : json_builder_add_string_value (builder, text);
357 : :
358 : 0 : json_builder_set_member_name (builder, "subID");
359 : 0 : json_builder_add_int_value (builder, sub_id);
360 : :
361 : 0 : packet = valent_packet_end (&builder);
362 : :
363 : 0 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
364 : : }
365 : :
366 : : /*
367 : : * GActions
368 : : */
369 : : static void
370 : 1 : fetch_action (GSimpleAction *action,
371 : : GVariant *parameter,
372 : : gpointer user_data)
373 : : {
374 : 1 : ValentSmsPlugin *self = VALENT_SMS_PLUGIN (user_data);
375 : :
376 [ + - ]: 1 : g_assert (VALENT_IS_SMS_PLUGIN (self));
377 : :
378 : 1 : valent_sms_plugin_request_conversations (self);
379 : 1 : }
380 : :
381 : : static gboolean
382 : 0 : on_send_message (ValentSmsWindow *window,
383 : : ValentMessage *message,
384 : : ValentSmsPlugin *self)
385 : : {
386 [ # # ]: 0 : g_assert (VALENT_IS_SMS_WINDOW (window));
387 [ # # ]: 0 : g_assert (VALENT_IS_MESSAGE (message));
388 : :
389 : 0 : valent_sms_plugin_request (self, message);
390 : :
391 : 0 : return TRUE;
392 : : }
393 : :
394 : : static void
395 : 1 : messaging_action (GSimpleAction *action,
396 : : GVariant *parameter,
397 : : gpointer user_data)
398 : : {
399 : 1 : ValentSmsPlugin *self = VALENT_SMS_PLUGIN (user_data);
400 : 1 : ValentDevice *device;
401 : :
402 [ + - ]: 1 : g_assert (VALENT_IS_SMS_PLUGIN (self));
403 : :
404 [ - + ]: 1 : if (!gtk_is_initialized ())
405 : : {
406 : 0 : g_warning ("%s: No display available", G_STRFUNC);
407 : 0 : return;
408 : : }
409 : :
410 [ + - ]: 1 : if (self->window == NULL)
411 : : {
412 : 1 : ValentContactStore *store;
413 : :
414 : 1 : device = valent_extension_get_object (VALENT_EXTENSION (self));
415 : 1 : store = valent_contacts_ensure_store (valent_contacts_get_default (),
416 : : valent_device_get_id (device),
417 : : valent_device_get_name (device));
418 : :
419 : 1 : self->window = g_object_new (VALENT_TYPE_SMS_WINDOW,
420 : : "contact-store", store,
421 : : "message-store", self->store,
422 : : NULL);
423 : 1 : g_object_add_weak_pointer (G_OBJECT (self->window),
424 : 1 : (gpointer) &self->window);
425 : :
426 : 1 : g_signal_connect_object (self->window,
427 : : "send-message",
428 : : G_CALLBACK (on_send_message),
429 : : self, 0);
430 : : }
431 : :
432 : 1 : gtk_window_present_with_time (GTK_WINDOW (self->window), GDK_CURRENT_TIME);
433 : : }
434 : :
435 : : static const GActionEntry actions[] = {
436 : : {"fetch", fetch_action, NULL, NULL, NULL},
437 : : {"messaging", messaging_action, NULL, NULL, NULL}
438 : : };
439 : :
440 : : /*
441 : : * ValentDevicePlugin
442 : : */
443 : : static void
444 : 10 : valent_sms_plugin_update_state (ValentDevicePlugin *plugin,
445 : : ValentDeviceState state)
446 : : {
447 : 10 : ValentSmsPlugin *self = VALENT_SMS_PLUGIN (plugin);
448 : 10 : gboolean available;
449 : :
450 [ + - ]: 10 : g_assert (VALENT_IS_SMS_PLUGIN (self));
451 : :
452 : 10 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
453 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
454 : :
455 : 10 : valent_extension_toggle_actions (VALENT_EXTENSION (plugin), available);
456 : :
457 : : /* Request summary of messages */
458 [ + + ]: 10 : if (available)
459 : 3 : valent_sms_plugin_request_conversations (self);
460 : 10 : }
461 : :
462 : : static void
463 : 3 : valent_sms_plugin_handle_packet (ValentDevicePlugin *plugin,
464 : : const char *type,
465 : : JsonNode *packet)
466 : : {
467 : 3 : ValentSmsPlugin *self = VALENT_SMS_PLUGIN (plugin);
468 : :
469 [ + - ]: 3 : g_assert (VALENT_IS_SMS_PLUGIN (plugin));
470 [ - + ]: 3 : g_assert (type != NULL);
471 [ - + ]: 3 : g_assert (VALENT_IS_PACKET (packet));
472 : :
473 [ + - ]: 3 : if (g_str_equal (type, "kdeconnect.sms.messages"))
474 : 3 : valent_sms_plugin_handle_messages (self, packet);
475 : : else
476 : 3 : g_assert_not_reached ();
477 : 3 : }
478 : :
479 : : /*
480 : : * ValentObject
481 : : */
482 : : static void
483 : 6 : valent_sms_plugin_destroy (ValentObject *object)
484 : : {
485 : 6 : ValentSmsPlugin *self = VALENT_SMS_PLUGIN (object);
486 : 6 : ValentDevicePlugin *plugin = VALENT_DEVICE_PLUGIN (object);
487 : :
488 : : /* Close message window and drop SMS Store */
489 [ + + ]: 6 : g_clear_pointer (&self->window, gtk_window_destroy);
490 [ + + ]: 6 : g_clear_object (&self->store);
491 : :
492 : 6 : valent_device_plugin_set_menu_item (plugin, "device.sms.messaging", NULL);
493 : :
494 : 6 : VALENT_OBJECT_CLASS (valent_sms_plugin_parent_class)->destroy (object);
495 : 6 : }
496 : :
497 : : /*
498 : : * GObject
499 : : */
500 : : static void
501 : 3 : valent_sms_plugin_constructed (GObject *object)
502 : : {
503 : 3 : ValentSmsPlugin *self = VALENT_SMS_PLUGIN (object);
504 : 3 : ValentDevicePlugin *plugin = VALENT_DEVICE_PLUGIN (object);
505 : 3 : ValentDevice *device;
506 : 3 : ValentContext *context = NULL;
507 : :
508 : : /* Load SMS Store */
509 : 3 : device = valent_extension_get_object (VALENT_EXTENSION (self));
510 : 3 : context = valent_device_get_context (device);
511 : 3 : self->store = g_object_new (VALENT_TYPE_SMS_STORE,
512 : : "domain", "plugin",
513 : : "id", "sms",
514 : : "parent", context,
515 : : NULL);
516 : :
517 : 3 : g_action_map_add_action_entries (G_ACTION_MAP (plugin),
518 : : actions,
519 : : G_N_ELEMENTS (actions),
520 : : plugin);
521 : 3 : valent_device_plugin_set_menu_action (plugin,
522 : : "device.sms.messaging",
523 : 3 : _("Messaging"),
524 : : "sms-symbolic");
525 : :
526 : 3 : G_OBJECT_CLASS (valent_sms_plugin_parent_class)->constructed (object);
527 : 3 : }
528 : :
529 : : static void
530 : 3 : valent_sms_plugin_finalize (GObject *object)
531 : : {
532 : 3 : ValentSmsPlugin *self = VALENT_SMS_PLUGIN (object);
533 : :
534 [ - + ]: 3 : if (self->window)
535 : 0 : g_clear_pointer (&self->window, gtk_window_destroy);
536 [ - + ]: 3 : g_clear_object (&self->store);
537 : :
538 : 3 : G_OBJECT_CLASS (valent_sms_plugin_parent_class)->finalize (object);
539 : 3 : }
540 : :
541 : : static void
542 : 4 : valent_sms_plugin_class_init (ValentSmsPluginClass *klass)
543 : : {
544 : 4 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
545 : 4 : ValentObjectClass *vobject_class = VALENT_OBJECT_CLASS (klass);
546 : 4 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
547 : :
548 : 4 : object_class->finalize = valent_sms_plugin_finalize;
549 : :
550 : 4 : object_class->constructed = valent_sms_plugin_constructed;
551 : 4 : plugin_class->handle_packet = valent_sms_plugin_handle_packet;
552 : 4 : plugin_class->update_state = valent_sms_plugin_update_state;
553 : :
554 : 4 : vobject_class->destroy = valent_sms_plugin_destroy;
555 : : }
556 : :
557 : : static void
558 : 3 : valent_sms_plugin_init (ValentSmsPlugin *self)
559 : : {
560 : 3 : }
561 : :
|