]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Wrap ignoreAttachments option in a ReceiveConfig record
[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.NotPrimaryDeviceException;
16 import org.asamk.signal.manager.api.Pair;
17 import org.asamk.signal.manager.api.ReceiveConfig;
18 import org.asamk.signal.manager.api.RecipientIdentifier;
19 import org.asamk.signal.manager.api.SendGroupMessageResults;
20 import org.asamk.signal.manager.api.SendMessageResults;
21 import org.asamk.signal.manager.api.StickerPack;
22 import org.asamk.signal.manager.api.StickerPackInvalidException;
23 import org.asamk.signal.manager.api.StickerPackUrl;
24 import org.asamk.signal.manager.api.TypingAction;
25 import org.asamk.signal.manager.api.UpdateGroup;
26 import org.asamk.signal.manager.api.UpdateProfile;
27 import org.asamk.signal.manager.api.UserStatus;
28 import org.asamk.signal.manager.groups.GroupId;
29 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
30 import org.asamk.signal.manager.groups.GroupNotFoundException;
31 import org.asamk.signal.manager.groups.GroupPermission;
32 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
33 import org.asamk.signal.manager.groups.LastGroupAdminException;
34 import org.asamk.signal.manager.groups.NotAGroupMemberException;
35 import org.asamk.signal.manager.storage.recipients.Contact;
36 import org.asamk.signal.manager.storage.recipients.Profile;
37 import org.asamk.signal.manager.storage.recipients.Recipient;
38 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
39 import org.freedesktop.dbus.DBusMap;
40 import org.freedesktop.dbus.DBusPath;
41 import org.freedesktop.dbus.connections.impl.DBusConnection;
42 import org.freedesktop.dbus.exceptions.DBusException;
43 import org.freedesktop.dbus.interfaces.DBusInterface;
44 import org.freedesktop.dbus.interfaces.DBusSigHandler;
45 import org.freedesktop.dbus.types.Variant;
46
47 import java.io.File;
48 import java.io.IOException;
49 import java.net.URI;
50 import java.net.URISyntaxException;
51 import java.time.Duration;
52 import java.util.ArrayList;
53 import java.util.Collection;
54 import java.util.HashMap;
55 import java.util.HashSet;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Objects;
59 import java.util.Optional;
60 import java.util.Set;
61 import java.util.concurrent.atomic.AtomicLong;
62 import java.util.function.Function;
63 import java.util.function.Supplier;
64 import java.util.stream.Collectors;
65 import java.util.stream.Stream;
66
67 /**
68 * This class implements the Manager interface using the DBus Signal interface, where possible.
69 * It's used for the signal-cli dbus client mode (--dbus, --dbus-system)
70 */
71 public class DbusManagerImpl implements Manager {
72
73 private final Signal signal;
74 private final DBusConnection connection;
75
76 private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
77 private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
78 private final List<Runnable> closedListeners = new ArrayList<>();
79 private DBusSigHandler<Signal.MessageReceivedV2> dbusMsgHandler;
80 private DBusSigHandler<Signal.ReceiptReceivedV2> dbusRcptHandler;
81 private DBusSigHandler<Signal.SyncMessageReceivedV2> dbusSyncHandler;
82
83 public DbusManagerImpl(final Signal signal, DBusConnection connection) {
84 this.signal = signal;
85 this.connection = connection;
86 }
87
88 @Override
89 public String getSelfNumber() {
90 return signal.getSelfNumber();
91 }
92
93 @Override
94 public Map<String, UserStatus> getUserStatus(final Set<String> numbers) throws IOException {
95 final var numbersList = new ArrayList<>(numbers);
96 final var registered = signal.isRegistered(numbersList);
97
98 final var result = new HashMap<String, UserStatus>();
99 for (var i = 0; i < numbersList.size(); i++) {
100 result.put(numbersList.get(i),
101 new UserStatus(numbersList.get(i),
102 registered.get(i) ? RecipientAddress.UNKNOWN_UUID : null,
103 false));
104 }
105 return result;
106 }
107
108 @Override
109 public void updateAccountAttributes(final String deviceName) throws IOException {
110 if (deviceName != null) {
111 final var devicePath = signal.getThisDevice();
112 getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName);
113 }
114 }
115
116 @Override
117 public Configuration getConfiguration() {
118 final var configuration = getRemoteObject(new DBusPath(signal.getObjectPath() + "/Configuration"),
119 Signal.Configuration.class).GetAll("org.asamk.Signal.Configuration");
120 return new Configuration(Optional.of((Boolean) configuration.get("ReadReceipts").getValue()),
121 Optional.of((Boolean) configuration.get("UnidentifiedDeliveryIndicators").getValue()),
122 Optional.of((Boolean) configuration.get("TypingIndicators").getValue()),
123 Optional.of((Boolean) configuration.get("LinkPreviews").getValue()));
124 }
125
126 @Override
127 public void updateConfiguration(Configuration newConfiguration) throws IOException {
128 final var configuration = getRemoteObject(new DBusPath(signal.getObjectPath() + "/Configuration"),
129 Signal.Configuration.class);
130 newConfiguration.readReceipts()
131 .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "ReadReceipts", v));
132 newConfiguration.unidentifiedDeliveryIndicators()
133 .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration",
134 "UnidentifiedDeliveryIndicators",
135 v));
136 newConfiguration.typingIndicators()
137 .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "TypingIndicators", v));
138 newConfiguration.linkPreviews()
139 .ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "LinkPreviews", v));
140 }
141
142 @Override
143 public void updateProfile(UpdateProfile updateProfile) throws IOException {
144 signal.updateProfile(emptyIfNull(updateProfile.getGivenName()),
145 emptyIfNull(updateProfile.getFamilyName()),
146 emptyIfNull(updateProfile.getAbout()),
147 emptyIfNull(updateProfile.getAboutEmoji()),
148 updateProfile.getAvatar() == null ? "" : updateProfile.getAvatar().getPath(),
149 updateProfile.isDeleteAvatar());
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 sendPaymentNotificationMessage(
389 final byte[] receipt, final String note, final RecipientIdentifier.Single recipient
390 ) throws IOException {
391 throw new UnsupportedOperationException();
392 }
393
394 @Override
395 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
396 signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
397 return new SendMessageResults(0, Map.of());
398 }
399
400 @Override
401 public void deleteRecipient(final RecipientIdentifier.Single recipient) {
402 signal.deleteRecipient(recipient.getIdentifier());
403 }
404
405 @Override
406 public void deleteContact(final RecipientIdentifier.Single recipient) {
407 signal.deleteContact(recipient.getIdentifier());
408 }
409
410 @Override
411 public void setContactName(
412 final RecipientIdentifier.Single recipient, final String name
413 ) throws NotPrimaryDeviceException {
414 signal.setContactName(recipient.getIdentifier(), name);
415 }
416
417 @Override
418 public void setContactsBlocked(
419 final Collection<RecipientIdentifier.Single> recipients, final boolean blocked
420 ) throws NotPrimaryDeviceException, IOException {
421 for (final var recipient : recipients) {
422 signal.setContactBlocked(recipient.getIdentifier(), blocked);
423 }
424 }
425
426 @Override
427 public void setGroupsBlocked(
428 final Collection<GroupId> groupIds, final boolean blocked
429 ) throws GroupNotFoundException, IOException {
430 for (final var groupId : groupIds) {
431 setGroupProperty(groupId, "IsBlocked", blocked);
432 }
433 }
434
435 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
436 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
437 group.Set("org.asamk.Signal.Group", propertyName, blocked);
438 }
439
440 @Override
441 public void setExpirationTimer(
442 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
443 ) throws IOException {
444 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
445 }
446
447 @Override
448 public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
449 try {
450 return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath())));
451 } catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) {
452 throw new AssertionError(e);
453 }
454 }
455
456 @Override
457 public List<StickerPack> getStickerPacks() {
458 throw new UnsupportedOperationException();
459 }
460
461 @Override
462 public void requestAllSyncData() throws IOException {
463 signal.sendSyncRequest();
464 }
465
466 @Override
467 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
468 synchronized (messageHandlers) {
469 if (isWeakListener) {
470 weakHandlers.add(handler);
471 } else {
472 if (messageHandlers.size() == 0) {
473 installMessageHandlers();
474 }
475 messageHandlers.add(handler);
476 }
477 }
478 }
479
480 @Override
481 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
482 synchronized (messageHandlers) {
483 weakHandlers.remove(handler);
484 messageHandlers.remove(handler);
485 if (messageHandlers.size() == 0) {
486 uninstallMessageHandlers();
487 }
488 }
489 }
490
491 @Override
492 public boolean isReceiving() {
493 synchronized (messageHandlers) {
494 return messageHandlers.size() > 0;
495 }
496 }
497
498 @Override
499 public void receiveMessages(final ReceiveMessageHandler handler) throws IOException {
500 addReceiveHandler(handler);
501 try {
502 synchronized (this) {
503 this.wait();
504 }
505 } catch (InterruptedException ignored) {
506 }
507 removeReceiveHandler(handler);
508 }
509
510 @Override
511 public void receiveMessages(
512 final Duration timeout, final ReceiveMessageHandler handler
513 ) throws IOException {
514 final var lastMessage = new AtomicLong(System.currentTimeMillis());
515
516 final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
517 lastMessage.set(System.currentTimeMillis());
518 handler.handleMessage(envelope, e);
519 };
520 addReceiveHandler(receiveHandler);
521 while (true) {
522 try {
523 final var sleepTimeRemaining = timeout.toMillis() - (System.currentTimeMillis() - lastMessage.get());
524 if (sleepTimeRemaining < 0) {
525 break;
526 }
527 Thread.sleep(sleepTimeRemaining);
528 } catch (InterruptedException ignored) {
529 }
530 }
531 removeReceiveHandler(receiveHandler);
532 }
533
534 @Override
535 public void setReceiveConfig(final ReceiveConfig receiveConfig) {
536 }
537
538 @Override
539 public boolean hasCaughtUpWithOldMessages() {
540 return true;
541 }
542
543 @Override
544 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
545 return signal.isContactBlocked(recipient.getIdentifier());
546 }
547
548 @Override
549 public void sendContacts() throws IOException {
550 signal.sendContacts();
551 }
552
553 @Override
554 public List<Recipient> getRecipients(
555 final boolean onlyContacts,
556 final Optional<Boolean> blocked,
557 final Collection<RecipientIdentifier.Single> addresses,
558 final Optional<String> name
559 ) {
560 final var numbers = addresses.stream()
561 .filter(s -> s instanceof RecipientIdentifier.Number)
562 .map(s -> ((RecipientIdentifier.Number) s).number())
563 .collect(Collectors.toSet());
564 return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
565 final var contactBlocked = signal.isContactBlocked(n);
566 if (blocked.isPresent() && blocked.get() != contactBlocked) {
567 return null;
568 }
569 final var contactName = signal.getContactName(n);
570 if (onlyContacts && contactName.length() == 0) {
571 return null;
572 }
573 if (name.isPresent() && !name.get().equals(contactName)) {
574 return null;
575 }
576 return Recipient.newBuilder()
577 .withAddress(new RecipientAddress(null, n))
578 .withContact(new Contact(contactName, null, null, 0, contactBlocked, false, false))
579 .build();
580 }).filter(Objects::nonNull).toList();
581 }
582
583 @Override
584 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
585 return signal.getContactName(recipient.getIdentifier());
586 }
587
588 @Override
589 public Group getGroup(final GroupId groupId) {
590 final var groupPath = signal.getGroup(groupId.serialize());
591 return getGroup(groupPath);
592 }
593
594 @SuppressWarnings("unchecked")
595 private Group getGroup(final DBusPath groupPath) {
596 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
597 final var id = (byte[]) group.get("Id").getValue();
598 try {
599 return new Group(GroupId.unknownVersion(id),
600 (String) group.get("Name").getValue(),
601 (String) group.get("Description").getValue(),
602 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
603 ((List<String>) group.get("Members").getValue()).stream()
604 .map(m -> new RecipientAddress(null, m))
605 .collect(Collectors.toSet()),
606 ((List<String>) group.get("PendingMembers").getValue()).stream()
607 .map(m -> new RecipientAddress(null, m))
608 .collect(Collectors.toSet()),
609 ((List<String>) group.get("RequestingMembers").getValue()).stream()
610 .map(m -> new RecipientAddress(null, m))
611 .collect(Collectors.toSet()),
612 ((List<String>) group.get("Admins").getValue()).stream()
613 .map(m -> new RecipientAddress(null, m))
614 .collect(Collectors.toSet()),
615 ((List<String>) group.get("Banned").getValue()).stream()
616 .map(m -> new RecipientAddress(null, m))
617 .collect(Collectors.toSet()),
618 (boolean) group.get("IsBlocked").getValue(),
619 (int) group.get("MessageExpirationTimer").getValue(),
620 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
621 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
622 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
623 (boolean) group.get("IsMember").getValue(),
624 (boolean) group.get("IsAdmin").getValue());
625 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
626 throw new AssertionError(e);
627 }
628 }
629
630 @Override
631 public List<Identity> getIdentities() {
632 throw new UnsupportedOperationException();
633 }
634
635 @Override
636 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
637 throw new UnsupportedOperationException();
638 }
639
640 @Override
641 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
642 throw new UnsupportedOperationException();
643 }
644
645 @Override
646 public boolean trustIdentityVerifiedSafetyNumber(
647 final RecipientIdentifier.Single recipient, final String safetyNumber
648 ) {
649 throw new UnsupportedOperationException();
650 }
651
652 @Override
653 public boolean trustIdentityVerifiedSafetyNumber(
654 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
655 ) {
656 throw new UnsupportedOperationException();
657 }
658
659 @Override
660 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
661 throw new UnsupportedOperationException();
662 }
663
664 @Override
665 public void addAddressChangedListener(final Runnable listener) {
666 }
667
668 @Override
669 public void addClosedListener(final Runnable listener) {
670 synchronized (closedListeners) {
671 closedListeners.add(listener);
672 }
673 }
674
675 @Override
676 public void close() {
677 synchronized (this) {
678 this.notify();
679 }
680 synchronized (messageHandlers) {
681 if (messageHandlers.size() > 0) {
682 uninstallMessageHandlers();
683 }
684 weakHandlers.clear();
685 messageHandlers.clear();
686 }
687 synchronized (closedListeners) {
688 closedListeners.forEach(Runnable::run);
689 closedListeners.clear();
690 }
691 }
692
693 private SendMessageResults handleMessage(
694 Set<RecipientIdentifier> recipients,
695 Function<List<String>, Long> recipientsHandler,
696 Supplier<Long> noteToSelfHandler,
697 Function<byte[], Long> groupHandler
698 ) {
699 long timestamp = 0;
700 final var singleRecipients = recipients.stream()
701 .filter(r -> r instanceof RecipientIdentifier.Single)
702 .map(RecipientIdentifier.Single.class::cast)
703 .map(RecipientIdentifier.Single::getIdentifier)
704 .toList();
705 if (singleRecipients.size() > 0) {
706 timestamp = recipientsHandler.apply(singleRecipients);
707 }
708
709 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
710 timestamp = noteToSelfHandler.get();
711 }
712 final var groupRecipients = recipients.stream()
713 .filter(r -> r instanceof RecipientIdentifier.Group)
714 .map(RecipientIdentifier.Group.class::cast)
715 .map(RecipientIdentifier.Group::groupId)
716 .toList();
717 for (final var groupId : groupRecipients) {
718 timestamp = groupHandler.apply(groupId.serialize());
719 }
720 return new SendMessageResults(timestamp, Map.of());
721 }
722
723 private String emptyIfNull(final String string) {
724 return string == null ? "" : string;
725 }
726
727 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
728 try {
729 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
730 } catch (DBusException e) {
731 throw new AssertionError(e);
732 }
733 }
734
735 private void installMessageHandlers() {
736 try {
737 this.dbusMsgHandler = messageReceived -> {
738 final var extras = messageReceived.getExtras();
739 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
740 messageReceived.getSender())),
741 0,
742 messageReceived.getTimestamp(),
743 0,
744 0,
745 false,
746 Optional.empty(),
747 Optional.empty(),
748 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
749 messageReceived.getGroupId().length > 0
750 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
751 messageReceived.getGroupId()), false, 0))
752 : Optional.empty(),
753 Optional.empty(),
754 Optional.of(messageReceived.getMessage()),
755 0,
756 false,
757 false,
758 false,
759 false,
760 Optional.empty(),
761 Optional.empty(),
762 Optional.empty(),
763 getAttachments(extras),
764 Optional.empty(),
765 Optional.empty(),
766 List.of(),
767 List.of(),
768 List.of())),
769 Optional.empty(),
770 Optional.empty());
771 notifyMessageHandlers(envelope);
772 };
773 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
774
775 this.dbusRcptHandler = receiptReceived -> {
776 final var type = switch (receiptReceived.getReceiptType()) {
777 case "read" -> MessageEnvelope.Receipt.Type.READ;
778 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
779 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
780 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
781 };
782 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
783 receiptReceived.getSender())),
784 0,
785 receiptReceived.getTimestamp(),
786 0,
787 0,
788 false,
789 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
790 type,
791 List.of(receiptReceived.getTimestamp()))),
792 Optional.empty(),
793 Optional.empty(),
794 Optional.empty(),
795 Optional.empty());
796 notifyMessageHandlers(envelope);
797 };
798 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
799
800 this.dbusSyncHandler = syncReceived -> {
801 final var extras = syncReceived.getExtras();
802 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
803 syncReceived.getSource())),
804 0,
805 syncReceived.getTimestamp(),
806 0,
807 0,
808 false,
809 Optional.empty(),
810 Optional.empty(),
811 Optional.empty(),
812 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
813 syncReceived.getTimestamp(),
814 syncReceived.getDestination().isEmpty()
815 ? Optional.empty()
816 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
817 Set.of(),
818 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
819 syncReceived.getGroupId().length > 0
820 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
821 syncReceived.getGroupId()), false, 0))
822 : Optional.empty(),
823 Optional.empty(),
824 Optional.of(syncReceived.getMessage()),
825 0,
826 false,
827 false,
828 false,
829 false,
830 Optional.empty(),
831 Optional.empty(),
832 Optional.empty(),
833 getAttachments(extras),
834 Optional.empty(),
835 Optional.empty(),
836 List.of(),
837 List.of(),
838 List.of())))),
839 Optional.empty(),
840 List.of(),
841 List.of(),
842 Optional.empty(),
843 Optional.empty(),
844 Optional.empty(),
845 Optional.empty())),
846 Optional.empty());
847 notifyMessageHandlers(envelope);
848 };
849 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
850 } catch (DBusException e) {
851 e.printStackTrace();
852 }
853 signal.subscribeReceive();
854 }
855
856 private void notifyMessageHandlers(final MessageEnvelope envelope) {
857 synchronized (messageHandlers) {
858 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
859 .forEach(h -> h.handleMessage(envelope, null));
860 }
861 }
862
863 private void uninstallMessageHandlers() {
864 try {
865 signal.unsubscribeReceive();
866 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
867 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
868 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
869 } catch (DBusException e) {
870 e.printStackTrace();
871 }
872 }
873
874 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
875 if (!extras.containsKey("attachments")) {
876 return List.of();
877 }
878
879 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
880 return attachments.stream().map(a -> {
881 final String file = a.containsKey("file") ? getValue(a, "file") : null;
882 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
883 ? Optional.of(getValue(a, "remoteId"))
884 : Optional.empty(),
885 file != null ? Optional.of(new File(file)) : Optional.empty(),
886 Optional.empty(),
887 getValue(a, "contentType"),
888 Optional.empty(),
889 Optional.empty(),
890 Optional.empty(),
891 Optional.empty(),
892 Optional.empty(),
893 Optional.empty(),
894 Optional.empty(),
895 getValue(a, "isVoiceNote"),
896 getValue(a, "isGif"),
897 getValue(a, "isBorderless"));
898 }).toList();
899 }
900
901 @SuppressWarnings("unchecked")
902 private <T> T getValue(
903 final Map<String, Variant<?>> stringVariantMap, final String field
904 ) {
905 return (T) stringVariantMap.get(field).getValue();
906 }
907 }