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