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