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