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