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