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