]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Improve output for profile key update messages
[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 givenName, final String familyName
413 ) throws NotPrimaryDeviceException {
414 signal.setContactName(recipient.getIdentifier(), givenName);
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 false,
761 Optional.empty(),
762 Optional.empty(),
763 Optional.empty(),
764 getAttachments(extras),
765 Optional.empty(),
766 Optional.empty(),
767 List.of(),
768 List.of(),
769 List.of())),
770 Optional.empty(),
771 Optional.empty());
772 notifyMessageHandlers(envelope);
773 };
774 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
775
776 this.dbusRcptHandler = receiptReceived -> {
777 final var type = switch (receiptReceived.getReceiptType()) {
778 case "read" -> MessageEnvelope.Receipt.Type.READ;
779 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
780 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
781 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
782 };
783 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
784 receiptReceived.getSender())),
785 0,
786 receiptReceived.getTimestamp(),
787 0,
788 0,
789 false,
790 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
791 type,
792 List.of(receiptReceived.getTimestamp()))),
793 Optional.empty(),
794 Optional.empty(),
795 Optional.empty(),
796 Optional.empty());
797 notifyMessageHandlers(envelope);
798 };
799 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
800
801 this.dbusSyncHandler = syncReceived -> {
802 final var extras = syncReceived.getExtras();
803 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
804 syncReceived.getSource())),
805 0,
806 syncReceived.getTimestamp(),
807 0,
808 0,
809 false,
810 Optional.empty(),
811 Optional.empty(),
812 Optional.empty(),
813 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
814 syncReceived.getTimestamp(),
815 syncReceived.getDestination().isEmpty()
816 ? Optional.empty()
817 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
818 Set.of(),
819 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
820 syncReceived.getGroupId().length > 0
821 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
822 syncReceived.getGroupId()), false, 0))
823 : Optional.empty(),
824 Optional.empty(),
825 Optional.of(syncReceived.getMessage()),
826 0,
827 false,
828 false,
829 false,
830 false,
831 false,
832 Optional.empty(),
833 Optional.empty(),
834 Optional.empty(),
835 getAttachments(extras),
836 Optional.empty(),
837 Optional.empty(),
838 List.of(),
839 List.of(),
840 List.of())))),
841 Optional.empty(),
842 List.of(),
843 List.of(),
844 Optional.empty(),
845 Optional.empty(),
846 Optional.empty(),
847 Optional.empty())),
848 Optional.empty());
849 notifyMessageHandlers(envelope);
850 };
851 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
852 } catch (DBusException e) {
853 e.printStackTrace();
854 }
855 signal.subscribeReceive();
856 }
857
858 private void notifyMessageHandlers(final MessageEnvelope envelope) {
859 synchronized (messageHandlers) {
860 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
861 .forEach(h -> h.handleMessage(envelope, null));
862 }
863 }
864
865 private void uninstallMessageHandlers() {
866 try {
867 signal.unsubscribeReceive();
868 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
869 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
870 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
871 } catch (DBusException e) {
872 e.printStackTrace();
873 }
874 }
875
876 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
877 if (!extras.containsKey("attachments")) {
878 return List.of();
879 }
880
881 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
882 return attachments.stream().map(a -> {
883 final String file = a.containsKey("file") ? getValue(a, "file") : null;
884 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
885 ? Optional.of(getValue(a, "remoteId"))
886 : Optional.empty(),
887 file != null ? Optional.of(new File(file)) : Optional.empty(),
888 Optional.empty(),
889 getValue(a, "contentType"),
890 Optional.empty(),
891 Optional.empty(),
892 Optional.empty(),
893 Optional.empty(),
894 Optional.empty(),
895 Optional.empty(),
896 Optional.empty(),
897 getValue(a, "isVoiceNote"),
898 getValue(a, "isGif"),
899 getValue(a, "isBorderless"));
900 }).toList();
901 }
902
903 @SuppressWarnings("unchecked")
904 private <T> T getValue(
905 final Map<String, Variant<?>> stringVariantMap, final String field
906 ) {
907 return (T) stringVariantMap.get(field).getValue();
908 }
909 }