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