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