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