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