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