]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Implement close for DbusManagerImpl
[signal-cli] / src / main / java / org / asamk / signal / dbus / DbusManagerImpl.java
1 package org.asamk.signal.dbus;
2
3 import org.asamk.Signal;
4 import org.asamk.signal.DbusConfig;
5 import org.asamk.signal.manager.AttachmentInvalidException;
6 import org.asamk.signal.manager.Manager;
7 import org.asamk.signal.manager.NotMasterDeviceException;
8 import org.asamk.signal.manager.StickerPackInvalidException;
9 import org.asamk.signal.manager.UntrustedIdentityException;
10 import org.asamk.signal.manager.api.Device;
11 import org.asamk.signal.manager.api.Group;
12 import org.asamk.signal.manager.api.Identity;
13 import org.asamk.signal.manager.api.InactiveGroupLinkException;
14 import org.asamk.signal.manager.api.InvalidDeviceLinkException;
15 import org.asamk.signal.manager.api.Message;
16 import org.asamk.signal.manager.api.MessageEnvelope;
17 import org.asamk.signal.manager.api.Pair;
18 import org.asamk.signal.manager.api.RecipientIdentifier;
19 import org.asamk.signal.manager.api.SendGroupMessageResults;
20 import org.asamk.signal.manager.api.SendMessageResults;
21 import org.asamk.signal.manager.api.TypingAction;
22 import org.asamk.signal.manager.api.UpdateGroup;
23 import org.asamk.signal.manager.groups.GroupId;
24 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
25 import org.asamk.signal.manager.groups.GroupNotFoundException;
26 import org.asamk.signal.manager.groups.GroupPermission;
27 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
28 import org.asamk.signal.manager.groups.LastGroupAdminException;
29 import org.asamk.signal.manager.groups.NotAGroupMemberException;
30 import org.asamk.signal.manager.storage.recipients.Contact;
31 import org.asamk.signal.manager.storage.recipients.Profile;
32 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
33 import org.freedesktop.dbus.DBusMap;
34 import org.freedesktop.dbus.DBusPath;
35 import org.freedesktop.dbus.connections.impl.DBusConnection;
36 import org.freedesktop.dbus.exceptions.DBusException;
37 import org.freedesktop.dbus.interfaces.DBusInterface;
38 import org.freedesktop.dbus.interfaces.DBusSigHandler;
39 import org.freedesktop.dbus.types.Variant;
40
41 import java.io.File;
42 import java.io.IOException;
43 import java.net.URI;
44 import java.net.URISyntaxException;
45 import java.util.ArrayList;
46 import java.util.HashMap;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Optional;
51 import java.util.Set;
52 import java.util.UUID;
53 import java.util.concurrent.TimeUnit;
54 import java.util.function.Function;
55 import java.util.function.Supplier;
56 import java.util.stream.Collectors;
57
58 /**
59 * This class implements the Manager interface using the DBus Signal interface, where possible.
60 * It's used for the signal-cli dbus client mode (--dbus, --dbus-system)
61 */
62 public class DbusManagerImpl implements Manager {
63
64 private final Signal signal;
65 private final DBusConnection connection;
66
67 private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
68 private DBusSigHandler<Signal.MessageReceivedV2> dbusMsgHandler;
69 private DBusSigHandler<Signal.ReceiptReceivedV2> dbusRcptHandler;
70 private DBusSigHandler<Signal.SyncMessageReceivedV2> dbusSyncHandler;
71
72 public DbusManagerImpl(final Signal signal, DBusConnection connection) {
73 this.signal = signal;
74 this.connection = connection;
75 }
76
77 @Override
78 public String getSelfNumber() {
79 return signal.getSelfNumber();
80 }
81
82 @Override
83 public void checkAccountState() throws IOException {
84 throw new UnsupportedOperationException();
85 }
86
87 @Override
88 public Map<String, Pair<String, UUID>> areUsersRegistered(final Set<String> numbers) throws IOException {
89 final var numbersList = new ArrayList<>(numbers);
90 final var registered = signal.isRegistered(numbersList);
91
92 final var result = new HashMap<String, Pair<String, UUID>>();
93 for (var i = 0; i < numbersList.size(); i++) {
94 result.put(numbersList.get(i),
95 new Pair<>(numbersList.get(i), registered.get(i) ? RecipientAddress.UNKNOWN_UUID : null));
96 }
97 return result;
98 }
99
100 @Override
101 public void updateAccountAttributes(final String deviceName) throws IOException {
102 if (deviceName != null) {
103 final var devicePath = signal.getThisDevice();
104 getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName);
105 }
106 }
107
108 @Override
109 public void updateConfiguration(
110 final Boolean readReceipts,
111 final Boolean unidentifiedDeliveryIndicators,
112 final Boolean typingIndicators,
113 final Boolean linkPreviews
114 ) throws IOException {
115 throw new UnsupportedOperationException();
116 }
117
118 @Override
119 public void setProfile(
120 final String givenName,
121 final String familyName,
122 final String about,
123 final String aboutEmoji,
124 final Optional<File> avatar
125 ) throws IOException {
126 signal.updateProfile(emptyIfNull(givenName),
127 emptyIfNull(familyName),
128 emptyIfNull(about),
129 emptyIfNull(aboutEmoji),
130 avatar == null ? "" : avatar.map(File::getPath).orElse(""),
131 avatar != null && !avatar.isPresent());
132 }
133
134 @Override
135 public void unregister() throws IOException {
136 throw new UnsupportedOperationException();
137 }
138
139 @Override
140 public void deleteAccount() throws IOException {
141 throw new UnsupportedOperationException();
142 }
143
144 @Override
145 public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
146 signal.submitRateLimitChallenge(challenge, captcha);
147 }
148
149 @Override
150 public List<Device> getLinkedDevices() throws IOException {
151 final var thisDevice = signal.getThisDevice();
152 return signal.listDevices().stream().map(d -> {
153 final var device = getRemoteObject(d.getObjectPath(),
154 Signal.Device.class).GetAll("org.asamk.Signal.Device");
155 return new Device((long) device.get("Id").getValue(),
156 (String) device.get("Name").getValue(),
157 (long) device.get("Created").getValue(),
158 (long) device.get("LastSeen").getValue(),
159 thisDevice.equals(d.getObjectPath()));
160 }).collect(Collectors.toList());
161 }
162
163 @Override
164 public void removeLinkedDevices(final long deviceId) throws IOException {
165 final var devicePath = signal.getDevice(deviceId);
166 getRemoteObject(devicePath, Signal.Device.class).removeDevice();
167 }
168
169 @Override
170 public void addDeviceLink(final URI linkUri) throws IOException, InvalidDeviceLinkException {
171 signal.addDevice(linkUri.toString());
172 }
173
174 @Override
175 public void setRegistrationLockPin(final Optional<String> pin) throws IOException {
176 if (pin.isPresent()) {
177 signal.setPin(pin.get());
178 } else {
179 signal.removePin();
180 }
181 }
182
183 @Override
184 public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) {
185 throw new UnsupportedOperationException();
186 }
187
188 @Override
189 public List<Group> getGroups() {
190 final var groups = signal.listGroups();
191 return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList());
192 }
193
194 @Override
195 public SendGroupMessageResults quitGroup(
196 final GroupId groupId, final Set<RecipientIdentifier.Single> groupAdmins
197 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
198 if (groupAdmins.size() > 0) {
199 throw new UnsupportedOperationException();
200 }
201 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
202 group.quitGroup();
203 return new SendGroupMessageResults(0, List.of());
204 }
205
206 @Override
207 public void deleteGroup(final GroupId groupId) throws IOException {
208 throw new UnsupportedOperationException();
209 }
210
211 @Override
212 public Pair<GroupId, SendGroupMessageResults> createGroup(
213 final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
214 ) throws IOException, AttachmentInvalidException {
215 final var newGroupId = signal.createGroup(emptyIfNull(name),
216 members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
217 avatarFile == null ? "" : avatarFile.getPath());
218 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
219 }
220
221 @Override
222 public SendGroupMessageResults updateGroup(
223 final GroupId groupId, final UpdateGroup updateGroup
224 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
225 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
226 if (updateGroup.getName() != null) {
227 group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
228 }
229 if (updateGroup.getDescription() != null) {
230 group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
231 }
232 if (updateGroup.getAvatarFile() != null) {
233 group.Set("org.asamk.Signal.Group",
234 "Avatar",
235 updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath());
236 }
237 if (updateGroup.getExpirationTimer() != null) {
238 group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
239 }
240 if (updateGroup.getAddMemberPermission() != null) {
241 group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
242 }
243 if (updateGroup.getEditDetailsPermission() != null) {
244 group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
245 }
246 if (updateGroup.getIsAnnouncementGroup() != null) {
247 group.Set("org.asamk.Signal.Group",
248 "PermissionSendMessage",
249 updateGroup.getIsAnnouncementGroup()
250 ? GroupPermission.ONLY_ADMINS.name()
251 : GroupPermission.EVERY_MEMBER.name());
252 }
253 if (updateGroup.getMembers() != null) {
254 group.addMembers(updateGroup.getMembers()
255 .stream()
256 .map(RecipientIdentifier.Single::getIdentifier)
257 .collect(Collectors.toList()));
258 }
259 if (updateGroup.getRemoveMembers() != null) {
260 group.removeMembers(updateGroup.getRemoveMembers()
261 .stream()
262 .map(RecipientIdentifier.Single::getIdentifier)
263 .collect(Collectors.toList()));
264 }
265 if (updateGroup.getAdmins() != null) {
266 group.addAdmins(updateGroup.getAdmins()
267 .stream()
268 .map(RecipientIdentifier.Single::getIdentifier)
269 .collect(Collectors.toList()));
270 }
271 if (updateGroup.getRemoveAdmins() != null) {
272 group.removeAdmins(updateGroup.getRemoveAdmins()
273 .stream()
274 .map(RecipientIdentifier.Single::getIdentifier)
275 .collect(Collectors.toList()));
276 }
277 if (updateGroup.isResetGroupLink()) {
278 group.resetLink();
279 }
280 if (updateGroup.getGroupLinkState() != null) {
281 switch (updateGroup.getGroupLinkState()) {
282 case DISABLED -> group.disableLink();
283 case ENABLED -> group.enableLink(false);
284 case ENABLED_WITH_APPROVAL -> group.enableLink(true);
285 }
286 }
287 return new SendGroupMessageResults(0, List.of());
288 }
289
290 @Override
291 public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, InactiveGroupLinkException {
292 final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
293 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
294 }
295
296 @Override
297 public void sendTypingMessage(
298 final TypingAction action, final Set<RecipientIdentifier> recipients
299 ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
300 for (final var recipient : recipients) {
301 if (recipient instanceof RecipientIdentifier.Single) {
302 signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(),
303 action == TypingAction.STOP);
304 } else if (recipient instanceof RecipientIdentifier.Group) {
305 throw new UnsupportedOperationException();
306 }
307 }
308 }
309
310 @Override
311 public void sendReadReceipt(
312 final RecipientIdentifier.Single sender, final List<Long> messageIds
313 ) throws IOException, UntrustedIdentityException {
314 signal.sendReadReceipt(sender.getIdentifier(), messageIds);
315 }
316
317 @Override
318 public void sendViewedReceipt(
319 final RecipientIdentifier.Single sender, final List<Long> messageIds
320 ) throws IOException, UntrustedIdentityException {
321 signal.sendViewedReceipt(sender.getIdentifier(), messageIds);
322 }
323
324 @Override
325 public SendMessageResults sendMessage(
326 final Message message, final Set<RecipientIdentifier> recipients
327 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
328 return handleMessage(recipients,
329 numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
330 () -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
331 groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
332 }
333
334 @Override
335 public SendMessageResults sendRemoteDeleteMessage(
336 final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
337 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
338 return handleMessage(recipients,
339 numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
340 () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
341 groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
342 }
343
344 @Override
345 public SendMessageResults sendMessageReaction(
346 final String emoji,
347 final boolean remove,
348 final RecipientIdentifier.Single targetAuthor,
349 final long targetSentTimestamp,
350 final Set<RecipientIdentifier> recipients
351 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
352 return handleMessage(recipients,
353 numbers -> signal.sendMessageReaction(emoji,
354 remove,
355 targetAuthor.getIdentifier(),
356 targetSentTimestamp,
357 numbers),
358 () -> signal.sendMessageReaction(emoji,
359 remove,
360 targetAuthor.getIdentifier(),
361 targetSentTimestamp,
362 signal.getSelfNumber()),
363 groupId -> signal.sendGroupMessageReaction(emoji,
364 remove,
365 targetAuthor.getIdentifier(),
366 targetSentTimestamp,
367 groupId));
368 }
369
370 @Override
371 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
372 signal.sendEndSessionMessage(recipients.stream()
373 .map(RecipientIdentifier.Single::getIdentifier)
374 .collect(Collectors.toList()));
375 return new SendMessageResults(0, Map.of());
376 }
377
378 @Override
379 public void setContactName(
380 final RecipientIdentifier.Single recipient, final String name
381 ) throws NotMasterDeviceException {
382 signal.setContactName(recipient.getIdentifier(), name);
383 }
384
385 @Override
386 public void setContactBlocked(
387 final RecipientIdentifier.Single recipient, final boolean blocked
388 ) throws NotMasterDeviceException, IOException {
389 signal.setContactBlocked(recipient.getIdentifier(), blocked);
390 }
391
392 @Override
393 public void setGroupBlocked(
394 final GroupId groupId, final boolean blocked
395 ) throws GroupNotFoundException, IOException {
396 setGroupProperty(groupId, "IsBlocked", blocked);
397 }
398
399 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
400 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
401 group.Set("org.asamk.Signal.Group", propertyName, blocked);
402 }
403
404 @Override
405 public void setExpirationTimer(
406 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
407 ) throws IOException {
408 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
409 }
410
411 @Override
412 public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
413 try {
414 return new URI(signal.uploadStickerPack(path.getPath()));
415 } catch (URISyntaxException e) {
416 throw new AssertionError(e);
417 }
418 }
419
420 @Override
421 public void requestAllSyncData() throws IOException {
422 signal.sendSyncRequest();
423 }
424
425 @Override
426 public void addReceiveHandler(final ReceiveMessageHandler handler) {
427 synchronized (messageHandlers) {
428 if (messageHandlers.size() == 0) {
429 installMessageHandlers();
430 }
431 messageHandlers.add(handler);
432 }
433 }
434
435 @Override
436 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
437 synchronized (messageHandlers) {
438 messageHandlers.remove(handler);
439 if (messageHandlers.size() == 0) {
440 uninstallMessageHandlers();
441 }
442 }
443 }
444
445 @Override
446 public boolean isReceiving() {
447 synchronized (messageHandlers) {
448 return messageHandlers.size() > 0;
449 }
450 }
451
452 @Override
453 public void receiveMessages(final ReceiveMessageHandler handler) throws IOException {
454 addReceiveHandler(handler);
455 try {
456 synchronized (this) {
457 this.wait();
458 }
459 } catch (InterruptedException ignored) {
460 }
461 removeReceiveHandler(handler);
462 }
463
464 @Override
465 public void receiveMessages(
466 final long timeout, final TimeUnit unit, final ReceiveMessageHandler handler
467 ) throws IOException {
468 addReceiveHandler(handler);
469 try {
470 Thread.sleep(unit.toMillis(timeout));
471 } catch (InterruptedException ignored) {
472 }
473 removeReceiveHandler(handler);
474 }
475
476 @Override
477 public void setIgnoreAttachments(final boolean ignoreAttachments) {
478 }
479
480 @Override
481 public boolean hasCaughtUpWithOldMessages() {
482 return true;
483 }
484
485 @Override
486 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
487 return signal.isContactBlocked(recipient.getIdentifier());
488 }
489
490 @Override
491 public void sendContacts() throws IOException {
492 signal.sendContacts();
493 }
494
495 @Override
496 public List<Pair<RecipientAddress, Contact>> getContacts() {
497 throw new UnsupportedOperationException();
498 }
499
500 @Override
501 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
502 return signal.getContactName(recipient.getIdentifier());
503 }
504
505 @Override
506 public Group getGroup(final GroupId groupId) {
507 final var groupPath = signal.getGroup(groupId.serialize());
508 return getGroup(groupPath);
509 }
510
511 @SuppressWarnings("unchecked")
512 private Group getGroup(final DBusPath groupPath) {
513 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
514 final var id = (byte[]) group.get("Id").getValue();
515 try {
516 return new Group(GroupId.unknownVersion(id),
517 (String) group.get("Name").getValue(),
518 (String) group.get("Description").getValue(),
519 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
520 ((List<String>) group.get("Members").getValue()).stream()
521 .map(m -> new RecipientAddress(null, m))
522 .collect(Collectors.toSet()),
523 ((List<String>) group.get("PendingMembers").getValue()).stream()
524 .map(m -> new RecipientAddress(null, m))
525 .collect(Collectors.toSet()),
526 ((List<String>) group.get("RequestingMembers").getValue()).stream()
527 .map(m -> new RecipientAddress(null, m))
528 .collect(Collectors.toSet()),
529 ((List<String>) group.get("Admins").getValue()).stream()
530 .map(m -> new RecipientAddress(null, m))
531 .collect(Collectors.toSet()),
532 (boolean) group.get("IsBlocked").getValue(),
533 (int) group.get("MessageExpirationTimer").getValue(),
534 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
535 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
536 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
537 (boolean) group.get("IsMember").getValue(),
538 (boolean) group.get("IsAdmin").getValue());
539 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
540 throw new AssertionError(e);
541 }
542 }
543
544 @Override
545 public List<Identity> getIdentities() {
546 throw new UnsupportedOperationException();
547 }
548
549 @Override
550 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
551 throw new UnsupportedOperationException();
552 }
553
554 @Override
555 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
556 throw new UnsupportedOperationException();
557 }
558
559 @Override
560 public boolean trustIdentityVerifiedSafetyNumber(
561 final RecipientIdentifier.Single recipient, final String safetyNumber
562 ) {
563 throw new UnsupportedOperationException();
564 }
565
566 @Override
567 public boolean trustIdentityVerifiedSafetyNumber(
568 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
569 ) {
570 throw new UnsupportedOperationException();
571 }
572
573 @Override
574 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
575 throw new UnsupportedOperationException();
576 }
577
578 @Override
579 public void close() throws IOException {
580 synchronized (this) {
581 this.notify();
582 }
583 synchronized (messageHandlers) {
584 messageHandlers.clear();
585 uninstallMessageHandlers();
586 }
587 }
588
589 private SendMessageResults handleMessage(
590 Set<RecipientIdentifier> recipients,
591 Function<List<String>, Long> recipientsHandler,
592 Supplier<Long> noteToSelfHandler,
593 Function<byte[], Long> groupHandler
594 ) {
595 long timestamp = 0;
596 final var singleRecipients = recipients.stream()
597 .filter(r -> r instanceof RecipientIdentifier.Single)
598 .map(RecipientIdentifier.Single.class::cast)
599 .map(RecipientIdentifier.Single::getIdentifier)
600 .collect(Collectors.toList());
601 if (singleRecipients.size() > 0) {
602 timestamp = recipientsHandler.apply(singleRecipients);
603 }
604
605 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
606 timestamp = noteToSelfHandler.get();
607 }
608 final var groupRecipients = recipients.stream()
609 .filter(r -> r instanceof RecipientIdentifier.Group)
610 .map(RecipientIdentifier.Group.class::cast)
611 .map(RecipientIdentifier.Group::groupId)
612 .collect(Collectors.toList());
613 for (final var groupId : groupRecipients) {
614 timestamp = groupHandler.apply(groupId.serialize());
615 }
616 return new SendMessageResults(timestamp, Map.of());
617 }
618
619 private String emptyIfNull(final String string) {
620 return string == null ? "" : string;
621 }
622
623 private <T extends DBusInterface> T getRemoteObject(final DBusPath devicePath, final Class<T> type) {
624 try {
625 return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type);
626 } catch (DBusException e) {
627 throw new AssertionError(e);
628 }
629 }
630
631 private void installMessageHandlers() {
632 try {
633 this.dbusMsgHandler = messageReceived -> {
634 final var extras = messageReceived.getExtras();
635 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
636 messageReceived.getSender())),
637 0,
638 messageReceived.getTimestamp(),
639 0,
640 0,
641 false,
642 Optional.empty(),
643 Optional.empty(),
644 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
645 messageReceived.getGroupId().length > 0
646 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
647 messageReceived.getGroupId()), false, 0))
648 : Optional.empty(),
649 Optional.empty(),
650 Optional.of(messageReceived.getMessage()),
651 0,
652 false,
653 false,
654 false,
655 false,
656 Optional.empty(),
657 Optional.empty(),
658 getAttachments(extras),
659 Optional.empty(),
660 Optional.empty(),
661 List.of(),
662 List.of(),
663 List.of())),
664 Optional.empty(),
665 Optional.empty());
666 synchronized (messageHandlers) {
667 for (final var messageHandler : messageHandlers) {
668 messageHandler.handleMessage(envelope, null);
669 }
670 }
671 };
672 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
673
674 this.dbusRcptHandler = receiptReceived -> {
675 final var type = switch (receiptReceived.getReceiptType()) {
676 case "read" -> MessageEnvelope.Receipt.Type.READ;
677 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
678 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
679 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
680 };
681 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
682 receiptReceived.getSender())),
683 0,
684 receiptReceived.getTimestamp(),
685 0,
686 0,
687 false,
688 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
689 type,
690 List.of(receiptReceived.getTimestamp()))),
691 Optional.empty(),
692 Optional.empty(),
693 Optional.empty(),
694 Optional.empty());
695 synchronized (messageHandlers) {
696 for (final var messageHandler : messageHandlers) {
697 messageHandler.handleMessage(envelope, null);
698 }
699 }
700 };
701 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
702
703 this.dbusSyncHandler = syncReceived -> {
704 final var extras = syncReceived.getExtras();
705 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
706 syncReceived.getSource())),
707 0,
708 syncReceived.getTimestamp(),
709 0,
710 0,
711 false,
712 Optional.empty(),
713 Optional.empty(),
714 Optional.empty(),
715 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
716 syncReceived.getTimestamp(),
717 syncReceived.getDestination().isEmpty()
718 ? Optional.empty()
719 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
720 Set.of(),
721 new MessageEnvelope.Data(syncReceived.getTimestamp(),
722 syncReceived.getGroupId().length > 0
723 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
724 syncReceived.getGroupId()), false, 0))
725 : Optional.empty(),
726 Optional.empty(),
727 Optional.of(syncReceived.getMessage()),
728 0,
729 false,
730 false,
731 false,
732 false,
733 Optional.empty(),
734 Optional.empty(),
735 getAttachments(extras),
736 Optional.empty(),
737 Optional.empty(),
738 List.of(),
739 List.of(),
740 List.of()))),
741 Optional.empty(),
742 List.of(),
743 List.of(),
744 Optional.empty(),
745 Optional.empty(),
746 Optional.empty(),
747 Optional.empty())),
748 Optional.empty());
749 synchronized (messageHandlers) {
750 for (final var messageHandler : messageHandlers) {
751 messageHandler.handleMessage(envelope, null);
752 }
753 }
754 };
755 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
756 } catch (DBusException e) {
757 e.printStackTrace();
758 }
759 }
760
761 private void uninstallMessageHandlers() {
762 try {
763 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
764 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
765 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
766 } catch (DBusException e) {
767 e.printStackTrace();
768 }
769 }
770
771 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
772 if (!extras.containsKey("attachments")) {
773 return List.of();
774 }
775
776 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
777 return attachments.stream().map(a -> {
778 final String file = a.containsKey("file") ? getValue(a, "file") : null;
779 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
780 ? Optional.of(getValue(a, "remoteId"))
781 : Optional.empty(),
782 file != null ? Optional.of(new File(file)) : Optional.empty(),
783 Optional.empty(),
784 getValue(a, "contentType"),
785 Optional.empty(),
786 Optional.empty(),
787 Optional.empty(),
788 Optional.empty(),
789 Optional.empty(),
790 Optional.empty(),
791 Optional.empty(),
792 getValue(a, "isVoiceNote"),
793 getValue(a, "isGif"),
794 getValue(a, "isBorderless"));
795 }).collect(Collectors.toList());
796 }
797
798 @SuppressWarnings("unchecked")
799 private <T> T getValue(
800 final Map<String, Variant<?>> stringVariantMap, final String field
801 ) {
802 return (T) stringVariantMap.get(field).getValue();
803 }
804 }