]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/ReceiveMessageHandler.java
Refactor RecipientAddress
[signal-cli] / src / main / java / org / asamk / signal / ReceiveMessageHandler.java
1 package org.asamk.signal;
2
3 import org.asamk.signal.manager.Manager;
4 import org.asamk.signal.manager.api.MessageEnvelope;
5 import org.asamk.signal.manager.api.RecipientAddress;
6 import org.asamk.signal.manager.api.RecipientIdentifier;
7 import org.asamk.signal.manager.api.UntrustedIdentityException;
8 import org.asamk.signal.manager.groups.GroupId;
9 import org.asamk.signal.output.PlainTextWriter;
10 import org.asamk.signal.util.DateUtils;
11 import org.asamk.signal.util.Hex;
12 import org.slf4j.helpers.MessageFormatter;
13
14 import java.util.ArrayList;
15 import java.util.stream.Collectors;
16
17 public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
18
19 final Manager m;
20 final PlainTextWriter writer;
21
22 public ReceiveMessageHandler(Manager m, final PlainTextWriter writer) {
23 this.m = m;
24 this.writer = writer;
25 }
26
27 @Override
28 public void handleMessage(MessageEnvelope envelope, Throwable exception) {
29 synchronized (writer) {
30 handleMessageInternal(envelope, exception);
31 }
32 }
33
34 private void handleMessageInternal(MessageEnvelope envelope, Throwable exception) {
35 var source = envelope.sourceAddress();
36 writer.println("Envelope from: {} (device: {})",
37 source.map(this::formatContact).orElse("unknown source"),
38 envelope.sourceDevice());
39 writer.println("Timestamp: {}", DateUtils.formatTimestamp(envelope.timestamp()));
40 writer.println("Server timestamps: received: {} delivered: {}",
41 DateUtils.formatTimestamp(envelope.serverReceivedTimestamp()),
42 DateUtils.formatTimestamp(envelope.serverDeliveredTimestamp()));
43 if (envelope.isUnidentifiedSender()) {
44 writer.println("Sent by unidentified/sealed sender");
45 }
46
47 if (exception != null) {
48 if (exception instanceof UntrustedIdentityException e) {
49 writer.println(
50 "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
51 final var recipientName = e.getSender().getLegacyIdentifier();
52 writer.println(
53 "Use 'signal-cli -a {} listIdentities -n {}', verify the key and run 'signal-cli -a {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted",
54 m.getSelfNumber(),
55 recipientName,
56 m.getSelfNumber(),
57 recipientName);
58 writer.println(
59 "If you don't care about security, use 'signal-cli -a {} trust -a {}' to trust it without verification",
60 m.getSelfNumber(),
61 recipientName);
62 } else {
63 writer.println("Exception: {} ({})", exception.getMessage(), exception.getClass().getSimpleName());
64 }
65 }
66
67 if (envelope.data().isPresent()) {
68 var message = envelope.data().get();
69 printDataMessage(writer, message);
70 }
71 if (envelope.story().isPresent()) {
72 var message = envelope.story().get();
73 printStoryMessage(writer.indentedWriter(), message);
74 }
75 if (envelope.sync().isPresent()) {
76 writer.println("Received a sync message");
77 var syncMessage = envelope.sync().get();
78 printSyncMessage(writer, syncMessage);
79 }
80 if (envelope.call().isPresent()) {
81 writer.println("Received a call message");
82 var callMessage = envelope.call().get();
83 printCallMessage(writer.indentedWriter(), callMessage);
84 }
85 if (envelope.receipt().isPresent()) {
86 writer.println("Received a receipt message");
87 var receiptMessage = envelope.receipt().get();
88 printReceiptMessage(writer.indentedWriter(), receiptMessage);
89 }
90 if (envelope.typing().isPresent()) {
91 writer.println("Received a typing message");
92 var typingMessage = envelope.typing().get();
93 printTypingMessage(writer.indentedWriter(), typingMessage);
94 }
95 writer.println();
96 }
97
98 private void printDataMessage(
99 PlainTextWriter writer, MessageEnvelope.Data message
100 ) {
101 writer.println("Message timestamp: {}", DateUtils.formatTimestamp(message.timestamp()));
102 if (message.isViewOnce()) {
103 writer.println("=VIEW ONCE=");
104 }
105
106 if (message.body().isPresent()) {
107 writer.println("Body: {}", message.body().get());
108 }
109 if (message.groupContext().isPresent()) {
110 writer.println("Group info:");
111 final var groupContext = message.groupContext().get();
112 printGroupContext(writer.indentedWriter(), groupContext);
113 }
114 if (message.storyContext().isPresent()) {
115 writer.println("Story reply:");
116 final var storyContext = message.storyContext().get();
117 printStoryContext(writer.indentedWriter(), storyContext);
118 }
119 if (message.groupCallUpdate().isPresent()) {
120 writer.println("Group call update:");
121 final var groupCallUpdate = message.groupCallUpdate().get();
122 writer.indentedWriter().println("Era id: {}", groupCallUpdate.eraId());
123 }
124 if (message.previews().size() > 0) {
125 writer.println("Previews:");
126 final var previews = message.previews();
127 for (var preview : previews) {
128 writer.println("- Preview");
129 printPreview(writer.indentedWriter(), preview);
130 }
131 }
132 if (message.sharedContacts().size() > 0) {
133 writer.println("Contacts:");
134 for (var contact : message.sharedContacts()) {
135 writer.println("- Contact:");
136 printSharedContact(writer.indentedWriter(), contact);
137 }
138 }
139 if (message.sticker().isPresent()) {
140 final var sticker = message.sticker().get();
141 writer.println("Sticker:");
142 printSticker(writer.indentedWriter(), sticker);
143 }
144 if (message.isEndSession()) {
145 writer.println("Is end session");
146 }
147 if (message.isExpirationUpdate()) {
148 writer.println("Is Expiration update: true");
149 }
150 if (message.expiresInSeconds() > 0) {
151 writer.println("Expires in: {} seconds", message.expiresInSeconds());
152 }
153 if (message.isProfileKeyUpdate()) {
154 writer.println("Profile key update");
155 }
156 if (message.hasProfileKey()) {
157 writer.println("With profile key");
158 }
159 if (message.reaction().isPresent()) {
160 writer.println("Reaction:");
161 final var reaction = message.reaction().get();
162 printReaction(writer.indentedWriter(), reaction);
163 }
164 if (message.quote().isPresent()) {
165 writer.println("Quote:");
166 var quote = message.quote().get();
167 printQuote(writer.indentedWriter(), quote);
168 }
169 if (message.remoteDeleteId().isPresent()) {
170 final var remoteDelete = message.remoteDeleteId().get();
171 writer.println("Remote delete message: timestamp = {}", remoteDelete);
172 }
173 if (message.mentions().size() > 0) {
174 writer.println("Mentions:");
175 for (var mention : message.mentions()) {
176 printMention(writer, mention);
177 }
178 }
179 if (message.attachments().size() > 0) {
180 writer.println("Attachments:");
181 for (var attachment : message.attachments()) {
182 writer.println("- Attachment:");
183 printAttachment(writer.indentedWriter(), attachment);
184 }
185 }
186 }
187
188 private void printStoryMessage(
189 PlainTextWriter writer, MessageEnvelope.Story message
190 ) {
191 writer.println("Story: with replies: {}", message.allowsReplies());
192 if (message.groupId().isPresent()) {
193 writer.println("Group info:");
194 printGroupInfo(writer.indentedWriter(), message.groupId().get());
195 }
196 if (message.textAttachment().isPresent()) {
197 writer.println("Body: {}", message.textAttachment().get().text().orElse(""));
198
199 if (message.textAttachment().get().preview().isPresent()) {
200 writer.println("Preview:");
201 printPreview(writer.indentedWriter(), message.textAttachment().get().preview().get());
202 }
203 }
204 if (message.fileAttachment().isPresent()) {
205 writer.println("Attachments:");
206 printAttachment(writer.indentedWriter(), message.fileAttachment().get());
207 }
208 }
209
210 private void printTypingMessage(
211 final PlainTextWriter writer, final MessageEnvelope.Typing typingMessage
212 ) {
213 writer.println("Action: {}", typingMessage.type());
214 writer.println("Timestamp: {}", DateUtils.formatTimestamp(typingMessage.timestamp()));
215 if (typingMessage.groupId().isPresent()) {
216 writer.println("Group Info:");
217 final var groupId = typingMessage.groupId().get();
218 printGroupInfo(writer.indentedWriter(), groupId);
219 }
220 }
221
222 private void printReceiptMessage(
223 final PlainTextWriter writer, final MessageEnvelope.Receipt receiptMessage
224 ) {
225 writer.println("When: {}", DateUtils.formatTimestamp(receiptMessage.when()));
226 if (receiptMessage.type() == MessageEnvelope.Receipt.Type.DELIVERY) {
227 writer.println("Is delivery receipt");
228 }
229 if (receiptMessage.type() == MessageEnvelope.Receipt.Type.READ) {
230 writer.println("Is read receipt");
231 }
232 if (receiptMessage.type() == MessageEnvelope.Receipt.Type.VIEWED) {
233 writer.println("Is viewed receipt");
234 }
235 writer.println("Timestamps:");
236 for (long timestamp : receiptMessage.timestamps()) {
237 writer.println("- {}", DateUtils.formatTimestamp(timestamp));
238 }
239 }
240
241 private void printCallMessage(
242 final PlainTextWriter writer, final MessageEnvelope.Call callMessage
243 ) {
244 if (callMessage.destinationDeviceId().isPresent()) {
245 final var deviceId = callMessage.destinationDeviceId().get();
246 writer.println("Destination device id: {}", deviceId);
247 }
248 if (callMessage.groupId().isPresent()) {
249 final var groupId = callMessage.groupId().get();
250 writer.println("Destination group id: {}", groupId);
251 }
252 if (callMessage.timestamp().isPresent()) {
253 writer.println("Timestamp: {}", DateUtils.formatTimestamp(callMessage.timestamp().get()));
254 }
255 if (callMessage.answer().isPresent()) {
256 var answerMessage = callMessage.answer().get();
257 writer.println("Answer message: {}, sdp: {})", answerMessage.id(), answerMessage.sdp());
258 }
259 if (callMessage.busy().isPresent()) {
260 var busyMessage = callMessage.busy().get();
261 writer.println("Busy message: {}", busyMessage.id());
262 }
263 if (callMessage.hangup().isPresent()) {
264 var hangupMessage = callMessage.hangup().get();
265 writer.println("Hangup message: {}", hangupMessage.id());
266 }
267 if (callMessage.iceUpdate().size() > 0) {
268 writer.println("Ice update messages:");
269 var iceUpdateMessages = callMessage.iceUpdate();
270 for (var iceUpdateMessage : iceUpdateMessages) {
271 writer.println("- {}, sdp: {}", iceUpdateMessage.id(), iceUpdateMessage.sdp());
272 }
273 }
274 if (callMessage.offer().isPresent()) {
275 var offerMessage = callMessage.offer().get();
276 writer.println("Offer message: {}, sdp: {}", offerMessage.id(), offerMessage.sdp());
277 }
278 if (callMessage.opaque().isPresent()) {
279 final var opaqueMessage = callMessage.opaque().get();
280 writer.println("Opaque message: size {}, urgency: {}",
281 opaqueMessage.opaque().length,
282 opaqueMessage.urgency().name());
283 }
284 }
285
286 private void printSyncMessage(
287 final PlainTextWriter writer, final MessageEnvelope.Sync syncMessage
288 ) {
289 if (syncMessage.contacts().isPresent()) {
290 final var contactsMessage = syncMessage.contacts().get();
291 var type = contactsMessage.isComplete() ? "complete" : "partial";
292 writer.println("Received {} sync contacts:", type);
293 }
294 if (syncMessage.groups().isPresent()) {
295 writer.println("Received sync groups.");
296 }
297 if (syncMessage.read().size() > 0) {
298 writer.println("Received sync read messages list");
299 for (var rm : syncMessage.read()) {
300 writer.println("- From: {} Message timestamp: {}",
301 formatContact(rm.sender()),
302 DateUtils.formatTimestamp(rm.timestamp()));
303 }
304 }
305 if (syncMessage.viewed().size() > 0) {
306 writer.println("Received sync viewed messages list");
307 for (var vm : syncMessage.viewed()) {
308 writer.println("- From: {} Message timestamp: {}",
309 formatContact(vm.sender()),
310 DateUtils.formatTimestamp(vm.timestamp()));
311 }
312 }
313 if (syncMessage.sent().isPresent()) {
314 writer.println("Received sync sent message");
315 final var sentTranscriptMessage = syncMessage.sent().get();
316 String to;
317 if (sentTranscriptMessage.destination().isPresent()) {
318 to = formatContact(sentTranscriptMessage.destination().get());
319 } else if (sentTranscriptMessage.recipients().size() > 0) {
320 to = sentTranscriptMessage.recipients()
321 .stream()
322 .map(this::formatContact)
323 .collect(Collectors.joining(", "));
324 } else {
325 to = "<unknown>";
326 }
327 writer.indentedWriter().println("To: {}", to);
328 writer.indentedWriter()
329 .println("Timestamp: {}", DateUtils.formatTimestamp(sentTranscriptMessage.timestamp()));
330 if (sentTranscriptMessage.expirationStartTimestamp() > 0) {
331 writer.indentedWriter()
332 .println("Expiration started at: {}",
333 DateUtils.formatTimestamp(sentTranscriptMessage.expirationStartTimestamp()));
334 }
335 if (sentTranscriptMessage.message().isPresent()) {
336 var message = sentTranscriptMessage.message().get();
337 printDataMessage(writer.indentedWriter(), message);
338 }
339 if (sentTranscriptMessage.story().isPresent()) {
340 var message = sentTranscriptMessage.story().get();
341 printStoryMessage(writer.indentedWriter(), message);
342 }
343 }
344 if (syncMessage.blocked().isPresent()) {
345 writer.println("Received sync message with block list");
346 writer.println("Blocked:");
347 final var blockedList = syncMessage.blocked().get();
348 for (var address : blockedList.recipients()) {
349 writer.println("- {}", address.getLegacyIdentifier());
350 }
351 for (var groupId : blockedList.groupIds()) {
352 writer.println("- {}", groupId.toBase64());
353 }
354 }
355 if (syncMessage.viewOnceOpen().isPresent()) {
356 final var viewOnceOpenMessage = syncMessage.viewOnceOpen().get();
357 writer.println("Received sync message with view once open message:");
358 writer.indentedWriter().println("Sender: {}", formatContact(viewOnceOpenMessage.sender()));
359 writer.indentedWriter()
360 .println("Timestamp: {}", DateUtils.formatTimestamp(viewOnceOpenMessage.timestamp()));
361 }
362 if (syncMessage.messageRequestResponse().isPresent()) {
363 final var requestResponseMessage = syncMessage.messageRequestResponse().get();
364 writer.println("Received message request response:");
365 writer.indentedWriter().println("Type: {}", requestResponseMessage.type());
366 if (requestResponseMessage.groupId().isPresent()) {
367 writer.println("For group:");
368 printGroupInfo(writer.indentedWriter(), requestResponseMessage.groupId().get());
369 }
370 if (requestResponseMessage.person().isPresent()) {
371 writer.indentedWriter().println("For Person: {}", formatContact(requestResponseMessage.person().get()));
372 }
373 }
374 }
375
376 private void printPreview(
377 final PlainTextWriter writer, final MessageEnvelope.Data.Preview preview
378 ) {
379 writer.println("Title: {}", preview.title());
380 writer.println("Description: {}", preview.description());
381 writer.println("Date: {}", DateUtils.formatTimestamp(preview.date()));
382 writer.println("Url: {}", preview.url());
383 if (preview.image().isPresent()) {
384 writer.println("Image:");
385 printAttachment(writer.indentedWriter(), preview.image().get());
386 }
387 }
388
389 private void printSticker(
390 final PlainTextWriter writer, final MessageEnvelope.Data.Sticker sticker
391 ) {
392 writer.println("Pack id: {}", Hex.toStringCondensed(sticker.packId().serialize()));
393 writer.println("Sticker id: {}", sticker.stickerId());
394 }
395
396 private void printReaction(
397 final PlainTextWriter writer, final MessageEnvelope.Data.Reaction reaction
398 ) {
399 writer.println("Emoji: {}", reaction.emoji());
400 writer.println("Target author: {}", formatContact(reaction.targetAuthor()));
401 writer.println("Target timestamp: {}", DateUtils.formatTimestamp(reaction.targetSentTimestamp()));
402 writer.println("Is remove: {}", reaction.isRemove());
403 }
404
405 private void printQuote(
406 final PlainTextWriter writer, final MessageEnvelope.Data.Quote quote
407 ) {
408 writer.println("Id: {}", quote.id());
409 writer.println("Author: {}", formatContact(quote.author()));
410 if (quote.text().isPresent()) {
411 writer.println("Text: {}", quote.text().get());
412 }
413 if (quote.mentions() != null && quote.mentions().size() > 0) {
414 writer.println("Mentions:");
415 for (var mention : quote.mentions()) {
416 printMention(writer, mention);
417 }
418 }
419 if (quote.attachments().size() > 0) {
420 writer.println("Attachments:");
421 for (var attachment : quote.attachments()) {
422 writer.println("- Attachment:");
423 printAttachment(writer.indentedWriter(), attachment);
424 }
425 }
426 }
427
428 private void printSharedContact(final PlainTextWriter writer, final MessageEnvelope.Data.SharedContact contact) {
429 writer.println("Name:");
430 var name = contact.name();
431 writer.indent(w -> {
432 if (name.display().isPresent() && !name.display().get().isBlank()) {
433 w.println("Display name: {}", name.display().get());
434 }
435 if (name.given().isPresent() && !name.given().get().isBlank()) {
436 w.println("First name: {}", name.given().get());
437 }
438 if (name.middle().isPresent() && !name.middle().get().isBlank()) {
439 w.println("Middle name: {}", name.middle().get());
440 }
441 if (name.family().isPresent() && !name.family().get().isBlank()) {
442 w.println("Family name: {}", name.family().get());
443 }
444 if (name.prefix().isPresent() && !name.prefix().get().isBlank()) {
445 w.println("Prefix name: {}", name.prefix().get());
446 }
447 if (name.suffix().isPresent() && !name.suffix().get().isBlank()) {
448 w.println("Suffix name: {}", name.suffix().get());
449 }
450 });
451
452 if (contact.avatar().isPresent()) {
453 var avatar = contact.avatar().get();
454 writer.println("Avatar: (profile: {})", avatar.isProfile());
455 printAttachment(writer.indentedWriter(), avatar.attachment());
456 }
457
458 if (contact.organization().isPresent()) {
459 writer.println("Organisation: {}", contact.organization().get());
460 }
461
462 if (contact.phone().size() > 0) {
463 writer.println("Phone details:");
464 for (var phone : contact.phone()) {
465 writer.println("- Phone:");
466 writer.indent(w -> {
467 w.println("Number: {}", phone.value());
468 w.println("Type: {}", phone.type());
469 if (phone.label().isPresent() && !phone.label().get().isBlank()) {
470 w.println("Label: {}", phone.label().get());
471 }
472 });
473 }
474 }
475
476 if (contact.email().size() > 0) {
477 writer.println("Email details:");
478 for (var email : contact.email()) {
479 writer.println("- Email:");
480 writer.indent(w -> {
481 w.println("Address: {}", email.value());
482 w.println("Type: {}", email.type());
483 if (email.label().isPresent() && !email.label().get().isBlank()) {
484 w.println("Label: {}", email.label().get());
485 }
486 });
487 }
488 }
489
490 if (contact.address().size() > 0) {
491 writer.println("Address details:");
492 for (var address : contact.address()) {
493 writer.println("- Address:");
494 writer.indent(w -> {
495 w.println("Type: {}", address.type());
496 if (address.label().isPresent() && !address.label().get().isBlank()) {
497 w.println("Label: {}", address.label().get());
498 }
499 if (address.street().isPresent() && !address.street().get().isBlank()) {
500 w.println("Street: {}", address.street().get());
501 }
502 if (address.pobox().isPresent() && !address.pobox().get().isBlank()) {
503 w.println("Pobox: {}", address.pobox().get());
504 }
505 if (address.neighborhood().isPresent() && !address.neighborhood().get().isBlank()) {
506 w.println("Neighbourhood: {}", address.neighborhood().get());
507 }
508 if (address.city().isPresent() && !address.city().get().isBlank()) {
509 w.println("City: {}", address.city().get());
510 }
511 if (address.region().isPresent() && !address.region().get().isBlank()) {
512 w.println("Region: {}", address.region().get());
513 }
514 if (address.postcode().isPresent() && !address.postcode().get().isBlank()) {
515 w.println("Postcode: {}", address.postcode().get());
516 }
517 if (address.country().isPresent() && !address.country().get().isBlank()) {
518 w.println("Country: {}", address.country().get());
519 }
520 });
521 }
522 }
523 }
524
525 private void printGroupContext(
526 final PlainTextWriter writer, final MessageEnvelope.Data.GroupContext groupContext
527 ) {
528 printGroupInfo(writer, groupContext.groupId());
529 writer.println("Revision: {}", groupContext.revision());
530 writer.println("Type: {}", groupContext.isGroupUpdate() ? "UPDATE" : "DELIVER");
531 }
532
533 private void printStoryContext(
534 final PlainTextWriter writer, final MessageEnvelope.Data.StoryContext storyContext
535 ) {
536 writer.println("Sender: {}", formatContact(storyContext.author()));
537 writer.println("Sent timestamp: {}", storyContext.sentTimestamp());
538 }
539
540 private void printGroupInfo(final PlainTextWriter writer, final GroupId groupId) {
541 writer.println("Id: {}", groupId.toBase64());
542
543 var group = m.getGroup(groupId);
544 if (group != null) {
545 writer.println("Name: {}", group.title());
546 } else {
547 writer.println("Name: <Unknown group>");
548 }
549 }
550
551 private void printMention(
552 PlainTextWriter writer, MessageEnvelope.Data.Mention mention
553 ) {
554 writer.println("- {}: {} (length: {})", formatContact(mention.recipient()), mention.start(), mention.length());
555 }
556
557 private void printAttachment(PlainTextWriter writer, MessageEnvelope.Data.Attachment attachment) {
558 writer.println("Content-Type: {}", attachment.contentType());
559 writer.println("Type: {}", attachment.id().isPresent() ? "Pointer" : "Stream");
560 if (attachment.id().isPresent()) {
561 writer.println("Id: {}", attachment.id().get());
562 }
563 if (attachment.uploadTimestamp().isPresent()) {
564 writer.println("Upload timestamp: {}", DateUtils.formatTimestamp(attachment.uploadTimestamp().get()));
565 }
566 if (attachment.caption().isPresent()) {
567 writer.println("Caption: {}", attachment.caption().get());
568 }
569 if (attachment.fileName().isPresent()) {
570 writer.println("Filename: {}", attachment.fileName().get());
571 }
572 if (attachment.size().isPresent() || attachment.preview().isPresent()) {
573 writer.println("Size: {}{}",
574 attachment.size().isPresent() ? attachment.size().get() + " bytes" : "<unavailable>",
575 attachment.preview().isPresent() ? " (Preview is available: "
576 + attachment.preview().get().length
577 + " bytes)" : "");
578 }
579 if (attachment.thumbnail().isPresent()) {
580 writer.println("Thumbnail:");
581 printAttachment(writer.indentedWriter(), attachment.thumbnail().get());
582 }
583 final var flags = new ArrayList<String>();
584 if (attachment.isVoiceNote()) {
585 flags.add("voice note");
586 }
587 if (attachment.isBorderless()) {
588 flags.add("borderless");
589 }
590 if (attachment.isGif()) {
591 flags.add("video gif");
592 }
593 if (flags.size() > 0) {
594 writer.println("Flags: {}", String.join(", ", flags));
595 }
596 if (attachment.width().isPresent() || attachment.height().isPresent()) {
597 writer.println("Dimensions: {}x{}", attachment.width().orElse(0), attachment.height().orElse(0));
598 }
599 if (attachment.file().isPresent()) {
600 var file = attachment.file().get();
601 if (file.exists()) {
602 writer.println("Stored plaintext in: {}", file);
603 }
604 }
605 }
606
607 private String formatContact(RecipientAddress address) {
608 final var number = address.getLegacyIdentifier();
609 final var name = m.getContactOrProfileName(RecipientIdentifier.Single.fromAddress(address));
610 if (name == null || name.isEmpty()) {
611 return number;
612 } else {
613 return MessageFormatter.arrayFormat("“{}” {}", new Object[]{name, number}).getMessage();
614 }
615 }
616 }