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