]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Implement reacting to stories
[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.Recipient;
19 import org.asamk.signal.manager.api.RecipientAddress;
20 import org.asamk.signal.manager.api.RecipientIdentifier;
21 import org.asamk.signal.manager.api.SendGroupMessageResults;
22 import org.asamk.signal.manager.api.SendMessageResults;
23 import org.asamk.signal.manager.api.StickerPack;
24 import org.asamk.signal.manager.api.StickerPackInvalidException;
25 import org.asamk.signal.manager.api.StickerPackUrl;
26 import org.asamk.signal.manager.api.TypingAction;
27 import org.asamk.signal.manager.api.UpdateGroup;
28 import org.asamk.signal.manager.api.UpdateProfile;
29 import org.asamk.signal.manager.api.UserStatus;
30 import org.asamk.signal.manager.groups.GroupId;
31 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
32 import org.asamk.signal.manager.groups.GroupNotFoundException;
33 import org.asamk.signal.manager.groups.GroupPermission;
34 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
35 import org.asamk.signal.manager.groups.LastGroupAdminException;
36 import org.asamk.signal.manager.groups.NotAGroupMemberException;
37 import org.asamk.signal.manager.storage.recipients.Contact;
38 import org.asamk.signal.manager.storage.recipients.Profile;
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 final boolean isStory
369 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
370 return handleMessage(recipients,
371 numbers -> signal.sendMessageReaction(emoji,
372 remove,
373 targetAuthor.getIdentifier(),
374 targetSentTimestamp,
375 numbers),
376 () -> signal.sendMessageReaction(emoji,
377 remove,
378 targetAuthor.getIdentifier(),
379 targetSentTimestamp,
380 signal.getSelfNumber()),
381 groupId -> signal.sendGroupMessageReaction(emoji,
382 remove,
383 targetAuthor.getIdentifier(),
384 targetSentTimestamp,
385 groupId));
386 }
387
388 @Override
389 public SendMessageResults sendPaymentNotificationMessage(
390 final byte[] receipt, final String note, final RecipientIdentifier.Single recipient
391 ) throws IOException {
392 final var timestamp = signal.sendPaymentNotification(receipt, note, recipient.getIdentifier());
393 return new SendMessageResults(timestamp, Map.of());
394 }
395
396 @Override
397 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
398 signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
399 return new SendMessageResults(0, Map.of());
400 }
401
402 @Override
403 public void deleteRecipient(final RecipientIdentifier.Single recipient) {
404 signal.deleteRecipient(recipient.getIdentifier());
405 }
406
407 @Override
408 public void deleteContact(final RecipientIdentifier.Single recipient) {
409 signal.deleteContact(recipient.getIdentifier());
410 }
411
412 @Override
413 public void setContactName(
414 final RecipientIdentifier.Single recipient, final String givenName, final String familyName
415 ) throws NotPrimaryDeviceException {
416 signal.setContactName(recipient.getIdentifier(), givenName);
417 }
418
419 @Override
420 public void setContactsBlocked(
421 final Collection<RecipientIdentifier.Single> recipients, final boolean blocked
422 ) throws NotPrimaryDeviceException, IOException {
423 for (final var recipient : recipients) {
424 signal.setContactBlocked(recipient.getIdentifier(), blocked);
425 }
426 }
427
428 @Override
429 public void setGroupsBlocked(
430 final Collection<GroupId> groupIds, final boolean blocked
431 ) throws GroupNotFoundException, IOException {
432 for (final var groupId : groupIds) {
433 setGroupProperty(groupId, "IsBlocked", blocked);
434 }
435 }
436
437 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
438 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
439 group.Set("org.asamk.Signal.Group", propertyName, blocked);
440 }
441
442 @Override
443 public void setExpirationTimer(
444 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
445 ) throws IOException {
446 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
447 }
448
449 @Override
450 public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
451 try {
452 return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath())));
453 } catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) {
454 throw new AssertionError(e);
455 }
456 }
457
458 @Override
459 public List<StickerPack> getStickerPacks() {
460 throw new UnsupportedOperationException();
461 }
462
463 @Override
464 public void requestAllSyncData() throws IOException {
465 signal.sendSyncRequest();
466 }
467
468 @Override
469 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
470 synchronized (messageHandlers) {
471 if (isWeakListener) {
472 weakHandlers.add(handler);
473 } else {
474 if (messageHandlers.size() == 0) {
475 installMessageHandlers();
476 }
477 messageHandlers.add(handler);
478 }
479 }
480 }
481
482 @Override
483 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
484 synchronized (messageHandlers) {
485 weakHandlers.remove(handler);
486 messageHandlers.remove(handler);
487 if (messageHandlers.size() == 0) {
488 uninstallMessageHandlers();
489 }
490 }
491 }
492
493 @Override
494 public boolean isReceiving() {
495 synchronized (messageHandlers) {
496 return messageHandlers.size() > 0;
497 }
498 }
499
500 @Override
501 public void receiveMessages(final ReceiveMessageHandler handler) throws IOException {
502 addReceiveHandler(handler);
503 try {
504 synchronized (this) {
505 this.wait();
506 }
507 } catch (InterruptedException ignored) {
508 }
509 removeReceiveHandler(handler);
510 }
511
512 @Override
513 public void receiveMessages(
514 final Duration timeout, final ReceiveMessageHandler handler
515 ) throws IOException {
516 final var lastMessage = new AtomicLong(System.currentTimeMillis());
517
518 final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
519 lastMessage.set(System.currentTimeMillis());
520 handler.handleMessage(envelope, e);
521 };
522 addReceiveHandler(receiveHandler);
523 while (true) {
524 try {
525 final var sleepTimeRemaining = timeout.toMillis() - (System.currentTimeMillis() - lastMessage.get());
526 if (sleepTimeRemaining < 0) {
527 break;
528 }
529 Thread.sleep(sleepTimeRemaining);
530 } catch (InterruptedException ignored) {
531 }
532 }
533 removeReceiveHandler(receiveHandler);
534 }
535
536 @Override
537 public void setReceiveConfig(final ReceiveConfig receiveConfig) {
538 }
539
540 @Override
541 public boolean hasCaughtUpWithOldMessages() {
542 return true;
543 }
544
545 @Override
546 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
547 return signal.isContactBlocked(recipient.getIdentifier());
548 }
549
550 @Override
551 public void sendContacts() throws IOException {
552 signal.sendContacts();
553 }
554
555 @Override
556 public List<Recipient> getRecipients(
557 final boolean onlyContacts,
558 final Optional<Boolean> blocked,
559 final Collection<RecipientIdentifier.Single> addresses,
560 final Optional<String> name
561 ) {
562 final var numbers = addresses.stream()
563 .filter(s -> s instanceof RecipientIdentifier.Number)
564 .map(s -> ((RecipientIdentifier.Number) s).number())
565 .collect(Collectors.toSet());
566 return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
567 final var contactBlocked = signal.isContactBlocked(n);
568 if (blocked.isPresent() && blocked.get() != contactBlocked) {
569 return null;
570 }
571 final var contactName = signal.getContactName(n);
572 if (onlyContacts && contactName.length() == 0) {
573 return null;
574 }
575 if (name.isPresent() && !name.get().equals(contactName)) {
576 return null;
577 }
578 return Recipient.newBuilder()
579 .withAddress(new RecipientAddress(null, n))
580 .withContact(new Contact(contactName, null, null, 0, contactBlocked, false, false))
581 .build();
582 }).filter(Objects::nonNull).toList();
583 }
584
585 @Override
586 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
587 return signal.getContactName(recipient.getIdentifier());
588 }
589
590 @Override
591 public Group getGroup(final GroupId groupId) {
592 final var groupPath = signal.getGroup(groupId.serialize());
593 return getGroup(groupPath);
594 }
595
596 @SuppressWarnings("unchecked")
597 private Group getGroup(final DBusPath groupPath) {
598 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
599 final var id = (byte[]) group.get("Id").getValue();
600 try {
601 return new Group(GroupId.unknownVersion(id),
602 (String) group.get("Name").getValue(),
603 (String) group.get("Description").getValue(),
604 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
605 ((List<String>) group.get("Members").getValue()).stream()
606 .map(m -> new RecipientAddress(null, m))
607 .collect(Collectors.toSet()),
608 ((List<String>) group.get("PendingMembers").getValue()).stream()
609 .map(m -> new RecipientAddress(null, m))
610 .collect(Collectors.toSet()),
611 ((List<String>) group.get("RequestingMembers").getValue()).stream()
612 .map(m -> new RecipientAddress(null, m))
613 .collect(Collectors.toSet()),
614 ((List<String>) group.get("Admins").getValue()).stream()
615 .map(m -> new RecipientAddress(null, m))
616 .collect(Collectors.toSet()),
617 ((List<String>) group.get("Banned").getValue()).stream()
618 .map(m -> new RecipientAddress(null, m))
619 .collect(Collectors.toSet()),
620 (boolean) group.get("IsBlocked").getValue(),
621 (int) group.get("MessageExpirationTimer").getValue(),
622 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
623 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
624 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
625 (boolean) group.get("IsMember").getValue(),
626 (boolean) group.get("IsAdmin").getValue());
627 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
628 throw new AssertionError(e);
629 }
630 }
631
632 @Override
633 public List<Identity> getIdentities() {
634 throw new UnsupportedOperationException();
635 }
636
637 @Override
638 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
639 throw new UnsupportedOperationException();
640 }
641
642 @Override
643 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
644 throw new UnsupportedOperationException();
645 }
646
647 @Override
648 public boolean trustIdentityVerifiedSafetyNumber(
649 final RecipientIdentifier.Single recipient, final String safetyNumber
650 ) {
651 throw new UnsupportedOperationException();
652 }
653
654 @Override
655 public boolean trustIdentityVerifiedSafetyNumber(
656 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
657 ) {
658 throw new UnsupportedOperationException();
659 }
660
661 @Override
662 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
663 throw new UnsupportedOperationException();
664 }
665
666 @Override
667 public void addAddressChangedListener(final Runnable listener) {
668 }
669
670 @Override
671 public void addClosedListener(final Runnable listener) {
672 synchronized (closedListeners) {
673 closedListeners.add(listener);
674 }
675 }
676
677 @Override
678 public void close() {
679 synchronized (this) {
680 this.notify();
681 }
682 synchronized (messageHandlers) {
683 if (messageHandlers.size() > 0) {
684 uninstallMessageHandlers();
685 }
686 weakHandlers.clear();
687 messageHandlers.clear();
688 }
689 synchronized (closedListeners) {
690 closedListeners.forEach(Runnable::run);
691 closedListeners.clear();
692 }
693 }
694
695 private SendMessageResults handleMessage(
696 Set<RecipientIdentifier> recipients,
697 Function<List<String>, Long> recipientsHandler,
698 Supplier<Long> noteToSelfHandler,
699 Function<byte[], Long> groupHandler
700 ) {
701 long timestamp = 0;
702 final var singleRecipients = recipients.stream()
703 .filter(r -> r instanceof RecipientIdentifier.Single)
704 .map(RecipientIdentifier.Single.class::cast)
705 .map(RecipientIdentifier.Single::getIdentifier)
706 .toList();
707 if (singleRecipients.size() > 0) {
708 timestamp = recipientsHandler.apply(singleRecipients);
709 }
710
711 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
712 timestamp = noteToSelfHandler.get();
713 }
714 final var groupRecipients = recipients.stream()
715 .filter(r -> r instanceof RecipientIdentifier.Group)
716 .map(RecipientIdentifier.Group.class::cast)
717 .map(RecipientIdentifier.Group::groupId)
718 .toList();
719 for (final var groupId : groupRecipients) {
720 timestamp = groupHandler.apply(groupId.serialize());
721 }
722 return new SendMessageResults(timestamp, Map.of());
723 }
724
725 private String emptyIfNull(final String string) {
726 return string == null ? "" : string;
727 }
728
729 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
730 try {
731 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
732 } catch (DBusException e) {
733 throw new AssertionError(e);
734 }
735 }
736
737 private void installMessageHandlers() {
738 try {
739 this.dbusMsgHandler = messageReceived -> {
740 final var extras = messageReceived.getExtras();
741 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
742 messageReceived.getSender())),
743 0,
744 messageReceived.getTimestamp(),
745 0,
746 0,
747 false,
748 Optional.empty(),
749 Optional.empty(),
750 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
751 messageReceived.getGroupId().length > 0
752 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
753 messageReceived.getGroupId()), false, 0))
754 : Optional.empty(),
755 Optional.empty(),
756 Optional.empty(),
757 Optional.of(messageReceived.getMessage()),
758 0,
759 false,
760 false,
761 false,
762 false,
763 false,
764 Optional.empty(),
765 Optional.empty(),
766 Optional.empty(),
767 getAttachments(extras),
768 Optional.empty(),
769 Optional.empty(),
770 List.of(),
771 List.of(),
772 List.of())),
773 Optional.empty(),
774 Optional.empty(),
775 Optional.empty());
776 notifyMessageHandlers(envelope);
777 };
778 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
779
780 this.dbusRcptHandler = receiptReceived -> {
781 final var type = switch (receiptReceived.getReceiptType()) {
782 case "read" -> MessageEnvelope.Receipt.Type.READ;
783 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
784 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
785 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
786 };
787 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
788 receiptReceived.getSender())),
789 0,
790 receiptReceived.getTimestamp(),
791 0,
792 0,
793 false,
794 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
795 type,
796 List.of(receiptReceived.getTimestamp()))),
797 Optional.empty(),
798 Optional.empty(),
799 Optional.empty(),
800 Optional.empty(),
801 Optional.empty());
802 notifyMessageHandlers(envelope);
803 };
804 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
805
806 this.dbusSyncHandler = syncReceived -> {
807 final var extras = syncReceived.getExtras();
808 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
809 syncReceived.getSource())),
810 0,
811 syncReceived.getTimestamp(),
812 0,
813 0,
814 false,
815 Optional.empty(),
816 Optional.empty(),
817 Optional.empty(),
818 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
819 syncReceived.getTimestamp(),
820 syncReceived.getDestination().isEmpty()
821 ? Optional.empty()
822 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
823 Set.of(),
824 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
825 syncReceived.getGroupId().length > 0
826 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
827 syncReceived.getGroupId()), false, 0))
828 : Optional.empty(),
829 Optional.empty(),
830 Optional.empty(),
831 Optional.of(syncReceived.getMessage()),
832 0,
833 false,
834 false,
835 false,
836 false,
837 false,
838 Optional.empty(),
839 Optional.empty(),
840 Optional.empty(),
841 getAttachments(extras),
842 Optional.empty(),
843 Optional.empty(),
844 List.of(),
845 List.of(),
846 List.of())),
847 Optional.empty())),
848 Optional.empty(),
849 List.of(),
850 List.of(),
851 Optional.empty(),
852 Optional.empty(),
853 Optional.empty(),
854 Optional.empty())),
855 Optional.empty(),
856 Optional.empty());
857 notifyMessageHandlers(envelope);
858 };
859 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
860 } catch (DBusException e) {
861 e.printStackTrace();
862 }
863 signal.subscribeReceive();
864 }
865
866 private void notifyMessageHandlers(final MessageEnvelope envelope) {
867 synchronized (messageHandlers) {
868 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
869 .forEach(h -> h.handleMessage(envelope, null));
870 }
871 }
872
873 private void uninstallMessageHandlers() {
874 try {
875 signal.unsubscribeReceive();
876 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
877 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
878 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
879 } catch (DBusException e) {
880 e.printStackTrace();
881 }
882 }
883
884 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
885 if (!extras.containsKey("attachments")) {
886 return List.of();
887 }
888
889 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
890 return attachments.stream().map(a -> {
891 final String file = a.containsKey("file") ? getValue(a, "file") : null;
892 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
893 ? Optional.of(getValue(a, "remoteId"))
894 : Optional.empty(),
895 file != null ? Optional.of(new File(file)) : Optional.empty(),
896 Optional.empty(),
897 getValue(a, "contentType"),
898 Optional.empty(),
899 Optional.empty(),
900 Optional.empty(),
901 Optional.empty(),
902 Optional.empty(),
903 Optional.empty(),
904 Optional.empty(),
905 getValue(a, "isVoiceNote"),
906 getValue(a, "isGif"),
907 getValue(a, "isBorderless"));
908 }).toList();
909 }
910
911 @SuppressWarnings("unchecked")
912 private <T> T getValue(
913 final Map<String, Variant<?>> stringVariantMap, final String field
914 ) {
915 return (T) stringVariantMap.get(field).getValue();
916 }
917 }