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