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