]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
6bb93677f15e030c5a836aecdcd51821416015a4
[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 sendPaymentNotificationMessage(
393 final byte[] receipt, final String note, final RecipientIdentifier.Single recipient
394 ) throws IOException {
395 throw new UnsupportedOperationException();
396 }
397
398 @Override
399 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
400 signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
401 return new SendMessageResults(0, Map.of());
402 }
403
404 @Override
405 public void deleteRecipient(final RecipientIdentifier.Single recipient) {
406 signal.deleteRecipient(recipient.getIdentifier());
407 }
408
409 @Override
410 public void deleteContact(final RecipientIdentifier.Single recipient) {
411 signal.deleteContact(recipient.getIdentifier());
412 }
413
414 @Override
415 public void setContactName(
416 final RecipientIdentifier.Single recipient, final String name
417 ) throws NotMasterDeviceException {
418 signal.setContactName(recipient.getIdentifier(), name);
419 }
420
421 @Override
422 public void setContactsBlocked(
423 final Collection<RecipientIdentifier.Single> recipients, final boolean blocked
424 ) throws NotMasterDeviceException, IOException {
425 for (final var recipient : recipients) {
426 signal.setContactBlocked(recipient.getIdentifier(), blocked);
427 }
428 }
429
430 @Override
431 public void setGroupsBlocked(
432 final Collection<GroupId> groupIds, final boolean blocked
433 ) throws GroupNotFoundException, IOException {
434 for (final var groupId : groupIds) {
435 setGroupProperty(groupId, "IsBlocked", blocked);
436 }
437 }
438
439 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
440 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
441 group.Set("org.asamk.Signal.Group", propertyName, blocked);
442 }
443
444 @Override
445 public void setExpirationTimer(
446 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
447 ) throws IOException {
448 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
449 }
450
451 @Override
452 public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
453 try {
454 return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath())));
455 } catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) {
456 throw new AssertionError(e);
457 }
458 }
459
460 @Override
461 public List<StickerPack> getStickerPacks() {
462 throw new UnsupportedOperationException();
463 }
464
465 @Override
466 public void requestAllSyncData() throws IOException {
467 signal.sendSyncRequest();
468 }
469
470 @Override
471 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
472 synchronized (messageHandlers) {
473 if (isWeakListener) {
474 weakHandlers.add(handler);
475 } else {
476 if (messageHandlers.size() == 0) {
477 installMessageHandlers();
478 }
479 messageHandlers.add(handler);
480 }
481 }
482 }
483
484 @Override
485 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
486 synchronized (messageHandlers) {
487 weakHandlers.remove(handler);
488 messageHandlers.remove(handler);
489 if (messageHandlers.size() == 0) {
490 uninstallMessageHandlers();
491 }
492 }
493 }
494
495 @Override
496 public boolean isReceiving() {
497 synchronized (messageHandlers) {
498 return messageHandlers.size() > 0;
499 }
500 }
501
502 @Override
503 public void receiveMessages(final ReceiveMessageHandler handler) throws IOException {
504 addReceiveHandler(handler);
505 try {
506 synchronized (this) {
507 this.wait();
508 }
509 } catch (InterruptedException ignored) {
510 }
511 removeReceiveHandler(handler);
512 }
513
514 @Override
515 public void receiveMessages(
516 final Duration timeout, final ReceiveMessageHandler handler
517 ) throws IOException {
518 final var lastMessage = new AtomicLong(System.currentTimeMillis());
519
520 final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
521 lastMessage.set(System.currentTimeMillis());
522 handler.handleMessage(envelope, e);
523 };
524 addReceiveHandler(receiveHandler);
525 while (true) {
526 try {
527 final var sleepTimeRemaining = timeout.toMillis() - (System.currentTimeMillis() - lastMessage.get());
528 if (sleepTimeRemaining < 0) {
529 break;
530 }
531 Thread.sleep(sleepTimeRemaining);
532 } catch (InterruptedException ignored) {
533 }
534 }
535 removeReceiveHandler(receiveHandler);
536 }
537
538 @Override
539 public void setIgnoreAttachments(final boolean ignoreAttachments) {
540 }
541
542 @Override
543 public boolean hasCaughtUpWithOldMessages() {
544 return true;
545 }
546
547 @Override
548 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
549 return signal.isContactBlocked(recipient.getIdentifier());
550 }
551
552 @Override
553 public void sendContacts() throws IOException {
554 signal.sendContacts();
555 }
556
557 @Override
558 public List<Recipient> getRecipients(
559 final boolean onlyContacts,
560 final Optional<Boolean> blocked,
561 final Collection<RecipientIdentifier.Single> addresses,
562 final Optional<String> name
563 ) {
564 final var numbers = addresses.stream()
565 .filter(s -> s instanceof RecipientIdentifier.Number)
566 .map(s -> ((RecipientIdentifier.Number) s).number())
567 .collect(Collectors.toSet());
568 return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
569 final var contactBlocked = signal.isContactBlocked(n);
570 if (blocked.isPresent() && blocked.get() != contactBlocked) {
571 return null;
572 }
573 final var contactName = signal.getContactName(n);
574 if (onlyContacts && contactName.length() == 0) {
575 return null;
576 }
577 if (name.isPresent() && !name.get().equals(contactName)) {
578 return null;
579 }
580 return Recipient.newBuilder()
581 .withAddress(new RecipientAddress(null, n))
582 .withContact(new Contact(contactName, null, 0, contactBlocked, false, false))
583 .build();
584 }).filter(Objects::nonNull).toList();
585 }
586
587 @Override
588 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
589 return signal.getContactName(recipient.getIdentifier());
590 }
591
592 @Override
593 public Group getGroup(final GroupId groupId) {
594 final var groupPath = signal.getGroup(groupId.serialize());
595 return getGroup(groupPath);
596 }
597
598 @SuppressWarnings("unchecked")
599 private Group getGroup(final DBusPath groupPath) {
600 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
601 final var id = (byte[]) group.get("Id").getValue();
602 try {
603 return new Group(GroupId.unknownVersion(id),
604 (String) group.get("Name").getValue(),
605 (String) group.get("Description").getValue(),
606 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
607 ((List<String>) group.get("Members").getValue()).stream()
608 .map(m -> new RecipientAddress(null, m))
609 .collect(Collectors.toSet()),
610 ((List<String>) group.get("PendingMembers").getValue()).stream()
611 .map(m -> new RecipientAddress(null, m))
612 .collect(Collectors.toSet()),
613 ((List<String>) group.get("RequestingMembers").getValue()).stream()
614 .map(m -> new RecipientAddress(null, m))
615 .collect(Collectors.toSet()),
616 ((List<String>) group.get("Admins").getValue()).stream()
617 .map(m -> new RecipientAddress(null, m))
618 .collect(Collectors.toSet()),
619 ((List<String>) group.get("Banned").getValue()).stream()
620 .map(m -> new RecipientAddress(null, m))
621 .collect(Collectors.toSet()),
622 (boolean) group.get("IsBlocked").getValue(),
623 (int) group.get("MessageExpirationTimer").getValue(),
624 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
625 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
626 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
627 (boolean) group.get("IsMember").getValue(),
628 (boolean) group.get("IsAdmin").getValue());
629 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
630 throw new AssertionError(e);
631 }
632 }
633
634 @Override
635 public List<Identity> getIdentities() {
636 throw new UnsupportedOperationException();
637 }
638
639 @Override
640 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
641 throw new UnsupportedOperationException();
642 }
643
644 @Override
645 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
646 throw new UnsupportedOperationException();
647 }
648
649 @Override
650 public boolean trustIdentityVerifiedSafetyNumber(
651 final RecipientIdentifier.Single recipient, final String safetyNumber
652 ) {
653 throw new UnsupportedOperationException();
654 }
655
656 @Override
657 public boolean trustIdentityVerifiedSafetyNumber(
658 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
659 ) {
660 throw new UnsupportedOperationException();
661 }
662
663 @Override
664 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
665 throw new UnsupportedOperationException();
666 }
667
668 @Override
669 public void addAddressChangedListener(final Runnable listener) {
670 }
671
672 @Override
673 public void addClosedListener(final Runnable listener) {
674 synchronized (closedListeners) {
675 closedListeners.add(listener);
676 }
677 }
678
679 @Override
680 public void close() {
681 synchronized (this) {
682 this.notify();
683 }
684 synchronized (messageHandlers) {
685 if (messageHandlers.size() > 0) {
686 uninstallMessageHandlers();
687 }
688 weakHandlers.clear();
689 messageHandlers.clear();
690 }
691 synchronized (closedListeners) {
692 closedListeners.forEach(Runnable::run);
693 closedListeners.clear();
694 }
695 }
696
697 private SendMessageResults handleMessage(
698 Set<RecipientIdentifier> recipients,
699 Function<List<String>, Long> recipientsHandler,
700 Supplier<Long> noteToSelfHandler,
701 Function<byte[], Long> groupHandler
702 ) {
703 long timestamp = 0;
704 final var singleRecipients = recipients.stream()
705 .filter(r -> r instanceof RecipientIdentifier.Single)
706 .map(RecipientIdentifier.Single.class::cast)
707 .map(RecipientIdentifier.Single::getIdentifier)
708 .toList();
709 if (singleRecipients.size() > 0) {
710 timestamp = recipientsHandler.apply(singleRecipients);
711 }
712
713 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
714 timestamp = noteToSelfHandler.get();
715 }
716 final var groupRecipients = recipients.stream()
717 .filter(r -> r instanceof RecipientIdentifier.Group)
718 .map(RecipientIdentifier.Group.class::cast)
719 .map(RecipientIdentifier.Group::groupId)
720 .toList();
721 for (final var groupId : groupRecipients) {
722 timestamp = groupHandler.apply(groupId.serialize());
723 }
724 return new SendMessageResults(timestamp, Map.of());
725 }
726
727 private String emptyIfNull(final String string) {
728 return string == null ? "" : string;
729 }
730
731 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
732 try {
733 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
734 } catch (DBusException e) {
735 throw new AssertionError(e);
736 }
737 }
738
739 private void installMessageHandlers() {
740 try {
741 this.dbusMsgHandler = messageReceived -> {
742 final var extras = messageReceived.getExtras();
743 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
744 messageReceived.getSender())),
745 0,
746 messageReceived.getTimestamp(),
747 0,
748 0,
749 false,
750 Optional.empty(),
751 Optional.empty(),
752 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
753 messageReceived.getGroupId().length > 0
754 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
755 messageReceived.getGroupId()), false, 0))
756 : Optional.empty(),
757 Optional.empty(),
758 Optional.of(messageReceived.getMessage()),
759 0,
760 false,
761 false,
762 false,
763 false,
764 Optional.empty(),
765 Optional.empty(),
766 Optional.empty(),
767 getAttachments(extras),
768 Optional.empty(),
769 Optional.empty(),
770 List.of(),
771 List.of(),
772 List.of())),
773 Optional.empty(),
774 Optional.empty());
775 notifyMessageHandlers(envelope);
776 };
777 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
778
779 this.dbusRcptHandler = receiptReceived -> {
780 final var type = switch (receiptReceived.getReceiptType()) {
781 case "read" -> MessageEnvelope.Receipt.Type.READ;
782 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
783 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
784 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
785 };
786 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
787 receiptReceived.getSender())),
788 0,
789 receiptReceived.getTimestamp(),
790 0,
791 0,
792 false,
793 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
794 type,
795 List.of(receiptReceived.getTimestamp()))),
796 Optional.empty(),
797 Optional.empty(),
798 Optional.empty(),
799 Optional.empty());
800 notifyMessageHandlers(envelope);
801 };
802 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
803
804 this.dbusSyncHandler = syncReceived -> {
805 final var extras = syncReceived.getExtras();
806 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
807 syncReceived.getSource())),
808 0,
809 syncReceived.getTimestamp(),
810 0,
811 0,
812 false,
813 Optional.empty(),
814 Optional.empty(),
815 Optional.empty(),
816 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
817 syncReceived.getTimestamp(),
818 syncReceived.getDestination().isEmpty()
819 ? Optional.empty()
820 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
821 Set.of(),
822 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
823 syncReceived.getGroupId().length > 0
824 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
825 syncReceived.getGroupId()), false, 0))
826 : Optional.empty(),
827 Optional.empty(),
828 Optional.of(syncReceived.getMessage()),
829 0,
830 false,
831 false,
832 false,
833 false,
834 Optional.empty(),
835 Optional.empty(),
836 Optional.empty(),
837 getAttachments(extras),
838 Optional.empty(),
839 Optional.empty(),
840 List.of(),
841 List.of(),
842 List.of())))),
843 Optional.empty(),
844 List.of(),
845 List.of(),
846 Optional.empty(),
847 Optional.empty(),
848 Optional.empty(),
849 Optional.empty())),
850 Optional.empty());
851 notifyMessageHandlers(envelope);
852 };
853 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
854 } catch (DBusException e) {
855 e.printStackTrace();
856 }
857 signal.subscribeReceive();
858 }
859
860 private void notifyMessageHandlers(final MessageEnvelope envelope) {
861 synchronized (messageHandlers) {
862 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
863 .forEach(h -> h.handleMessage(envelope, null));
864 }
865 }
866
867 private void uninstallMessageHandlers() {
868 try {
869 signal.unsubscribeReceive();
870 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
871 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
872 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
873 } catch (DBusException e) {
874 e.printStackTrace();
875 }
876 }
877
878 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
879 if (!extras.containsKey("attachments")) {
880 return List.of();
881 }
882
883 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
884 return attachments.stream().map(a -> {
885 final String file = a.containsKey("file") ? getValue(a, "file") : null;
886 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
887 ? Optional.of(getValue(a, "remoteId"))
888 : Optional.empty(),
889 file != null ? Optional.of(new File(file)) : Optional.empty(),
890 Optional.empty(),
891 getValue(a, "contentType"),
892 Optional.empty(),
893 Optional.empty(),
894 Optional.empty(),
895 Optional.empty(),
896 Optional.empty(),
897 Optional.empty(),
898 Optional.empty(),
899 getValue(a, "isVoiceNote"),
900 getValue(a, "isGif"),
901 getValue(a, "isBorderless"));
902 }).toList();
903 }
904
905 @SuppressWarnings("unchecked")
906 private <T> T getValue(
907 final Map<String, Variant<?>> stringVariantMap, final String field
908 ) {
909 return (T) stringVariantMap.get(field).getValue();
910 }
911 }