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