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