]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Recreate recipient database with aci column
[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,
674 null,
675 null,
676 null,
677 0,
678 0,
679 false,
680 contactBlocked,
681 false,
682 false,
683 false,
684 null))
685 .build();
686 }).filter(Objects::nonNull).toList();
687 }
688
689 @Override
690 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
691 return signal.getContactName(recipient.getIdentifier());
692 }
693
694 @Override
695 public Group getGroup(final GroupId groupId) {
696 final var groupPath = signal.getGroup(groupId.serialize());
697 return getGroup(groupPath);
698 }
699
700 @SuppressWarnings("unchecked")
701 private Group getGroup(final DBusPath groupPath) {
702 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
703 final var id = (byte[]) group.get("Id").getValue();
704 try {
705 return new Group(GroupId.unknownVersion(id),
706 (String) group.get("Name").getValue(),
707 (String) group.get("Description").getValue(),
708 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
709 ((List<String>) group.get("Members").getValue()).stream()
710 .map(m -> new RecipientAddress(null, m))
711 .collect(Collectors.toSet()),
712 ((List<String>) group.get("PendingMembers").getValue()).stream()
713 .map(m -> new RecipientAddress(null, m))
714 .collect(Collectors.toSet()),
715 ((List<String>) group.get("RequestingMembers").getValue()).stream()
716 .map(m -> new RecipientAddress(null, m))
717 .collect(Collectors.toSet()),
718 ((List<String>) group.get("Admins").getValue()).stream()
719 .map(m -> new RecipientAddress(null, m))
720 .collect(Collectors.toSet()),
721 ((List<String>) group.get("Banned").getValue()).stream()
722 .map(m -> new RecipientAddress(null, m))
723 .collect(Collectors.toSet()),
724 (boolean) group.get("IsBlocked").getValue(),
725 (int) group.get("MessageExpirationTimer").getValue(),
726 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
727 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
728 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
729 (boolean) group.get("IsMember").getValue(),
730 (boolean) group.get("IsAdmin").getValue());
731 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
732 throw new AssertionError(e);
733 }
734 }
735
736 @Override
737 public List<Identity> getIdentities() {
738 throw new UnsupportedOperationException();
739 }
740
741 @Override
742 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
743 throw new UnsupportedOperationException();
744 }
745
746 @Override
747 public boolean trustIdentityVerified(
748 final RecipientIdentifier.Single recipient, final IdentityVerificationCode verificationCode
749 ) {
750 throw new UnsupportedOperationException();
751 }
752
753 @Override
754 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
755 throw new UnsupportedOperationException();
756 }
757
758 @Override
759 public void addAddressChangedListener(final Runnable listener) {
760 }
761
762 @Override
763 public void addClosedListener(final Runnable listener) {
764 synchronized (closedListeners) {
765 closedListeners.add(listener);
766 }
767 }
768
769 @Override
770 public void close() {
771 synchronized (this) {
772 this.notify();
773 }
774 synchronized (messageHandlers) {
775 if (!messageHandlers.isEmpty()) {
776 uninstallMessageHandlers();
777 }
778 weakHandlers.clear();
779 messageHandlers.clear();
780 }
781 synchronized (closedListeners) {
782 closedListeners.forEach(Runnable::run);
783 closedListeners.clear();
784 }
785 }
786
787 private SendMessageResults handleMessage(
788 Set<RecipientIdentifier> recipients,
789 Function<List<String>, Long> recipientsHandler,
790 Supplier<Long> noteToSelfHandler,
791 Function<byte[], Long> groupHandler
792 ) {
793 long timestamp = 0;
794 final var singleRecipients = recipients.stream()
795 .filter(r -> r instanceof RecipientIdentifier.Single)
796 .map(RecipientIdentifier.Single.class::cast)
797 .map(RecipientIdentifier.Single::getIdentifier)
798 .toList();
799 if (!singleRecipients.isEmpty()) {
800 timestamp = recipientsHandler.apply(singleRecipients);
801 }
802
803 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
804 timestamp = noteToSelfHandler.get();
805 }
806 final var groupRecipients = recipients.stream()
807 .filter(r -> r instanceof RecipientIdentifier.Group)
808 .map(RecipientIdentifier.Group.class::cast)
809 .map(RecipientIdentifier.Group::groupId)
810 .toList();
811 for (final var groupId : groupRecipients) {
812 timestamp = groupHandler.apply(groupId.serialize());
813 }
814 return new SendMessageResults(timestamp, Map.of());
815 }
816
817 private String emptyIfNull(final String string) {
818 return string == null ? "" : string;
819 }
820
821 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
822 try {
823 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
824 } catch (DBusException e) {
825 throw new AssertionError(e);
826 }
827 }
828
829 private void installMessageHandlers() {
830 try {
831 this.dbusMsgHandler = messageReceived -> {
832 final var extras = messageReceived.getExtras();
833 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
834 messageReceived.getSender())),
835 0,
836 messageReceived.getTimestamp(),
837 0,
838 0,
839 false,
840 Optional.empty(),
841 Optional.empty(),
842 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
843 messageReceived.getGroupId().length > 0
844 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
845 messageReceived.getGroupId()), false, 0))
846 : Optional.empty(),
847 Optional.empty(),
848 Optional.empty(),
849 Optional.of(messageReceived.getMessage()),
850 0,
851 false,
852 false,
853 false,
854 false,
855 false,
856 Optional.empty(),
857 Optional.empty(),
858 Optional.empty(),
859 getAttachments(extras),
860 Optional.empty(),
861 Optional.empty(),
862 List.of(),
863 getMentions(extras),
864 List.of(),
865 List.of())),
866 Optional.empty(),
867 Optional.empty(),
868 Optional.empty(),
869 Optional.empty());
870 notifyMessageHandlers(envelope);
871 };
872 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
873 this.dbusEditMsgHandler = messageReceived -> {
874 final var extras = messageReceived.getExtras();
875 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
876 messageReceived.getSender())),
877 0,
878 messageReceived.getTimestamp(),
879 0,
880 0,
881 false,
882 Optional.empty(),
883 Optional.empty(),
884 Optional.empty(),
885 Optional.of(new MessageEnvelope.Edit(messageReceived.getTargetSentTimestamp(),
886 new MessageEnvelope.Data(messageReceived.getTimestamp(),
887 messageReceived.getGroupId().length > 0
888 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
889 messageReceived.getGroupId()), false, 0))
890 : Optional.empty(),
891 Optional.empty(),
892 Optional.empty(),
893 Optional.of(messageReceived.getMessage()),
894 0,
895 false,
896 false,
897 false,
898 false,
899 false,
900 Optional.empty(),
901 Optional.empty(),
902 Optional.empty(),
903 getAttachments(extras),
904 Optional.empty(),
905 Optional.empty(),
906 List.of(),
907 getMentions(extras),
908 List.of(),
909 List.of()))),
910 Optional.empty(),
911 Optional.empty(),
912 Optional.empty());
913 notifyMessageHandlers(envelope);
914 };
915 connection.addSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler);
916
917 this.dbusRcptHandler = receiptReceived -> {
918 final var type = switch (receiptReceived.getReceiptType()) {
919 case "read" -> MessageEnvelope.Receipt.Type.READ;
920 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
921 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
922 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
923 };
924 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
925 receiptReceived.getSender())),
926 0,
927 receiptReceived.getTimestamp(),
928 0,
929 0,
930 false,
931 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
932 type,
933 List.of(receiptReceived.getTimestamp()))),
934 Optional.empty(),
935 Optional.empty(),
936 Optional.empty(),
937 Optional.empty(),
938 Optional.empty(),
939 Optional.empty());
940 notifyMessageHandlers(envelope);
941 };
942 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
943
944 this.dbusSyncHandler = syncReceived -> {
945 final var extras = syncReceived.getExtras();
946 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
947 syncReceived.getSource())),
948 0,
949 syncReceived.getTimestamp(),
950 0,
951 0,
952 false,
953 Optional.empty(),
954 Optional.empty(),
955 Optional.empty(),
956 Optional.empty(),
957 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
958 syncReceived.getTimestamp(),
959 syncReceived.getDestination().isEmpty()
960 ? Optional.empty()
961 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
962 Set.of(),
963 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
964 syncReceived.getGroupId().length > 0
965 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
966 syncReceived.getGroupId()), false, 0))
967 : Optional.empty(),
968 Optional.empty(),
969 Optional.empty(),
970 Optional.of(syncReceived.getMessage()),
971 0,
972 false,
973 false,
974 false,
975 false,
976 false,
977 Optional.empty(),
978 Optional.empty(),
979 Optional.empty(),
980 getAttachments(extras),
981 Optional.empty(),
982 Optional.empty(),
983 List.of(),
984 getMentions(extras),
985 List.of(),
986 List.of())),
987 Optional.empty(),
988 Optional.empty())),
989 Optional.empty(),
990 List.of(),
991 List.of(),
992 Optional.empty(),
993 Optional.empty(),
994 Optional.empty(),
995 Optional.empty())),
996 Optional.empty(),
997 Optional.empty());
998 notifyMessageHandlers(envelope);
999 };
1000 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
1001 } catch (DBusException e) {
1002 throw new RuntimeException(e);
1003 }
1004 signal.subscribeReceive();
1005 }
1006
1007 private void notifyMessageHandlers(final MessageEnvelope envelope) {
1008 synchronized (messageHandlers) {
1009 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
1010 .forEach(h -> h.handleMessage(envelope, null));
1011 }
1012 }
1013
1014 private void uninstallMessageHandlers() {
1015 try {
1016 signal.unsubscribeReceive();
1017 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
1018 connection.removeSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler);
1019 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
1020 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
1021 } catch (DBusException e) {
1022 throw new RuntimeException(e);
1023 }
1024 }
1025
1026 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
1027 if (!extras.containsKey("attachments")) {
1028 return List.of();
1029 }
1030
1031 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
1032 return attachments.stream().map(a -> {
1033 final String file = a.containsKey("file") ? getValue(a, "file") : null;
1034 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
1035 ? Optional.of(getValue(a, "remoteId"))
1036 : Optional.empty(),
1037 file != null ? Optional.of(new File(file)) : Optional.empty(),
1038 Optional.empty(),
1039 getValue(a, "contentType"),
1040 Optional.empty(),
1041 Optional.empty(),
1042 Optional.empty(),
1043 Optional.empty(),
1044 Optional.empty(),
1045 Optional.empty(),
1046 Optional.empty(),
1047 getValue(a, "isVoiceNote"),
1048 getValue(a, "isGif"),
1049 getValue(a, "isBorderless"));
1050 }).toList();
1051 }
1052
1053 private List<MessageEnvelope.Data.Mention> getMentions(final Map<String, Variant<?>> extras) {
1054 if (!extras.containsKey("mentions")) {
1055 return List.of();
1056 }
1057
1058 final List<DBusMap<String, Variant<?>>> mentions = getValue(extras, "mentions");
1059 return mentions.stream()
1060 .map(a -> new MessageEnvelope.Data.Mention(new RecipientAddress(null, getValue(a, "recipient")),
1061 getValue(a, "start"),
1062 getValue(a, "length")))
1063 .toList();
1064 }
1065
1066 @Override
1067 public InputStream retrieveAttachment(final String id) throws IOException {
1068 throw new UnsupportedOperationException();
1069 }
1070
1071 @SuppressWarnings("unchecked")
1072 private <T> T getValue(
1073 final Map<String, Variant<?>> stringVariantMap, final String field
1074 ) {
1075 return (T) stringVariantMap.get(field).getValue();
1076 }
1077 }