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