]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Update gradle
[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 final String nickGivenName,
521 final String nickFamilyName,
522 final String note
523 ) throws NotPrimaryDeviceException {
524 signal.setContactName(recipient.getIdentifier(), givenName);
525 }
526
527 @Override
528 public void setContactsBlocked(
529 final Collection<RecipientIdentifier.Single> recipients,
530 final boolean blocked
531 ) throws NotPrimaryDeviceException, IOException {
532 for (final var recipient : recipients) {
533 signal.setContactBlocked(recipient.getIdentifier(), blocked);
534 }
535 }
536
537 @Override
538 public void setGroupsBlocked(
539 final Collection<GroupId> groupIds,
540 final boolean blocked
541 ) throws GroupNotFoundException, IOException {
542 for (final var groupId : groupIds) {
543 setGroupProperty(groupId, "IsBlocked", blocked);
544 }
545 }
546
547 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
548 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
549 group.Set("org.asamk.Signal.Group", propertyName, blocked);
550 }
551
552 @Override
553 public void setExpirationTimer(
554 final RecipientIdentifier.Single recipient,
555 final int messageExpirationTimer
556 ) throws IOException {
557 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
558 }
559
560 @Override
561 public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
562 try {
563 return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath())));
564 } catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) {
565 throw new AssertionError(e);
566 }
567 }
568
569 @Override
570 public void installStickerPack(final StickerPackUrl url) throws IOException {
571 throw new UnsupportedOperationException();
572 }
573
574 @Override
575 public List<StickerPack> getStickerPacks() {
576 throw new UnsupportedOperationException();
577 }
578
579 @Override
580 public void requestAllSyncData() throws IOException {
581 signal.sendSyncRequest();
582 }
583
584 @Override
585 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
586 synchronized (messageHandlers) {
587 if (isWeakListener) {
588 weakHandlers.add(handler);
589 } else {
590 if (messageHandlers.isEmpty()) {
591 installMessageHandlers();
592 }
593 messageHandlers.add(handler);
594 }
595 }
596 }
597
598 @Override
599 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
600 synchronized (messageHandlers) {
601 weakHandlers.remove(handler);
602 messageHandlers.remove(handler);
603 if (messageHandlers.isEmpty()) {
604 uninstallMessageHandlers();
605 }
606 }
607 }
608
609 @Override
610 public boolean isReceiving() {
611 synchronized (messageHandlers) {
612 return !messageHandlers.isEmpty();
613 }
614 }
615
616 private Thread receiveThread;
617
618 @Override
619 public void receiveMessages(
620 Optional<Duration> timeout,
621 Optional<Integer> maxMessages,
622 ReceiveMessageHandler handler
623 ) throws IOException, AlreadyReceivingException {
624 if (receiveThread != null) {
625 throw new AlreadyReceivingException("Already receiving message.");
626 }
627 receiveThread = Thread.currentThread();
628
629 final var remainingMessages = new AtomicInteger(maxMessages.orElse(-1));
630 final var lastMessage = new AtomicLong(System.currentTimeMillis());
631 final var thread = Thread.currentThread();
632
633 final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
634 lastMessage.set(System.currentTimeMillis());
635 handler.handleMessage(envelope, e);
636 if (remainingMessages.get() > 0) {
637 if (remainingMessages.decrementAndGet() <= 0) {
638 remainingMessages.set(0);
639 thread.interrupt();
640 }
641 }
642 };
643 addReceiveHandler(receiveHandler);
644 if (timeout.isPresent()) {
645 while (remainingMessages.get() != 0) {
646 try {
647 final var passedTime = System.currentTimeMillis() - lastMessage.get();
648 final var sleepTimeRemaining = timeout.get().toMillis() - passedTime;
649 if (sleepTimeRemaining < 0) {
650 break;
651 }
652 Thread.sleep(sleepTimeRemaining);
653 } catch (InterruptedException ignored) {
654 break;
655 }
656 }
657 } else {
658 try {
659 synchronized (this) {
660 this.wait();
661 }
662 } catch (InterruptedException ignored) {
663 }
664 }
665
666 removeReceiveHandler(receiveHandler);
667 receiveThread = null;
668 }
669
670 @Override
671 public void stopReceiveMessages() {
672 if (receiveThread != null) {
673 receiveThread.interrupt();
674 }
675 }
676
677 @Override
678 public void setReceiveConfig(final ReceiveConfig receiveConfig) {
679 }
680
681 @Override
682 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
683 return signal.isContactBlocked(recipient.getIdentifier());
684 }
685
686 @Override
687 public void sendContacts() throws IOException {
688 signal.sendContacts();
689 }
690
691 @Override
692 public List<Recipient> getRecipients(
693 final boolean onlyContacts,
694 final Optional<Boolean> blocked,
695 final Collection<RecipientIdentifier.Single> addresses,
696 final Optional<String> name
697 ) {
698 final var numbers = addresses.stream()
699 .filter(s -> s instanceof RecipientIdentifier.Number)
700 .map(s -> ((RecipientIdentifier.Number) s).number())
701 .collect(Collectors.toSet());
702 return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
703 final var contactBlocked = signal.isContactBlocked(n);
704 if (blocked.isPresent() && blocked.get() != contactBlocked) {
705 return null;
706 }
707 final var contactName = signal.getContactName(n);
708 if (onlyContacts && contactName.isEmpty()) {
709 return null;
710 }
711 if (name.isPresent() && !name.get().equals(contactName)) {
712 return null;
713 }
714 return Recipient.newBuilder()
715 .withAddress(new RecipientAddress(n))
716 .withContact(new Contact(contactName,
717 null,
718 null,
719 null,
720 null,
721 null,
722 null,
723 0,
724 1,
725 0,
726 false,
727 contactBlocked,
728 false,
729 false,
730 false,
731 null))
732 .build();
733 }).filter(Objects::nonNull).toList();
734 }
735
736 @Override
737 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
738 return signal.getContactName(recipient.getIdentifier());
739 }
740
741 @Override
742 public Group getGroup(final GroupId groupId) {
743 final var groupPath = signal.getGroup(groupId.serialize());
744 return getGroup(groupPath);
745 }
746
747 @SuppressWarnings("unchecked")
748 private Group getGroup(final DBusPath groupPath) {
749 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
750 final var id = (byte[]) group.get("Id").getValue();
751 try {
752 return new Group(GroupId.unknownVersion(id),
753 (String) group.get("Name").getValue(),
754 (String) group.get("Description").getValue(),
755 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
756 ((List<String>) group.get("Members").getValue()).stream()
757 .map(m -> new RecipientAddress(m))
758 .collect(Collectors.toSet()),
759 ((List<String>) group.get("PendingMembers").getValue()).stream()
760 .map(m -> new RecipientAddress(m))
761 .collect(Collectors.toSet()),
762 ((List<String>) group.get("RequestingMembers").getValue()).stream()
763 .map(m -> new RecipientAddress(m))
764 .collect(Collectors.toSet()),
765 ((List<String>) group.get("Admins").getValue()).stream()
766 .map(m -> new RecipientAddress(m))
767 .collect(Collectors.toSet()),
768 ((List<String>) group.get("Banned").getValue()).stream()
769 .map(m -> new RecipientAddress(m))
770 .collect(Collectors.toSet()),
771 (boolean) group.get("IsBlocked").getValue(),
772 (int) group.get("MessageExpirationTimer").getValue(),
773 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
774 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
775 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
776 (boolean) group.get("IsMember").getValue(),
777 (boolean) group.get("IsAdmin").getValue());
778 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
779 throw new AssertionError(e);
780 }
781 }
782
783 @Override
784 public List<Identity> getIdentities() {
785 final var identities = signal.listIdentities();
786 return identities.stream().map(Signal.StructIdentity::getObjectPath).map(this::getIdentity).toList();
787 }
788
789 @Override
790 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
791 final var path = signal.getIdentity(recipient.getIdentifier());
792 return List.of(getIdentity(path));
793 }
794
795 private Identity getIdentity(final DBusPath identityPath) {
796 final var group = getRemoteObject(identityPath, Signal.Identity.class).GetAll("org.asamk.Signal.Identity");
797 final var aci = (String) group.get("Uuid").getValue();
798 final var number = (String) group.get("Number").getValue();
799 return new Identity(new RecipientAddress(aci, null, number, null),
800 (byte[]) group.get("Fingerprint").getValue(),
801 (String) group.get("SafetyNumber").getValue(),
802 (byte[]) group.get("ScannableSafetyNumber").getValue(),
803 TrustLevel.valueOf((String) group.get("TrustLevel").getValue()),
804 (Long) group.get("AddedDate").getValue());
805 }
806
807 @Override
808 public boolean trustIdentityVerified(
809 final RecipientIdentifier.Single recipient,
810 final IdentityVerificationCode verificationCode
811 ) {
812 throw new UnsupportedOperationException();
813 }
814
815 @Override
816 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
817 throw new UnsupportedOperationException();
818 }
819
820 @Override
821 public void addAddressChangedListener(final Runnable listener) {
822 }
823
824 @Override
825 public void addClosedListener(final Runnable listener) {
826 synchronized (closedListeners) {
827 closedListeners.add(listener);
828 }
829 }
830
831 @Override
832 public void close() {
833 synchronized (this) {
834 this.notify();
835 }
836 synchronized (messageHandlers) {
837 if (!messageHandlers.isEmpty()) {
838 uninstallMessageHandlers();
839 }
840 weakHandlers.clear();
841 messageHandlers.clear();
842 }
843 synchronized (closedListeners) {
844 closedListeners.forEach(Runnable::run);
845 closedListeners.clear();
846 }
847 }
848
849 private SendMessageResults handleMessage(
850 Set<RecipientIdentifier> recipients,
851 Function<List<String>, Long> recipientsHandler,
852 Supplier<Long> noteToSelfHandler,
853 Function<byte[], Long> groupHandler
854 ) {
855 long timestamp = 0;
856 final var singleRecipients = recipients.stream()
857 .filter(r -> r instanceof RecipientIdentifier.Single)
858 .map(RecipientIdentifier.Single.class::cast)
859 .map(RecipientIdentifier.Single::getIdentifier)
860 .toList();
861 if (!singleRecipients.isEmpty()) {
862 timestamp = recipientsHandler.apply(singleRecipients);
863 }
864
865 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
866 timestamp = noteToSelfHandler.get();
867 }
868 final var groupRecipients = recipients.stream()
869 .filter(r -> r instanceof RecipientIdentifier.Group)
870 .map(RecipientIdentifier.Group.class::cast)
871 .map(RecipientIdentifier.Group::groupId)
872 .toList();
873 for (final var groupId : groupRecipients) {
874 timestamp = groupHandler.apply(groupId.serialize());
875 }
876 return new SendMessageResults(timestamp, Map.of());
877 }
878
879 private String emptyIfNull(final String string) {
880 return string == null ? "" : string;
881 }
882
883 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
884 try {
885 return connection.getRemoteObject(busname, path.getPath(), type);
886 } catch (DBusException e) {
887 throw new AssertionError(e);
888 }
889 }
890
891 private void installMessageHandlers() {
892 try {
893 this.dbusMsgHandler = messageReceived -> {
894 final var extras = messageReceived.getExtras();
895 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(messageReceived.getSender())),
896 0,
897 messageReceived.getTimestamp(),
898 0,
899 0,
900 false,
901 Optional.empty(),
902 Optional.empty(),
903 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
904 messageReceived.getGroupId().length > 0
905 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
906 messageReceived.getGroupId()), false, 0))
907 : Optional.empty(),
908 Optional.empty(),
909 Optional.empty(),
910 Optional.of(messageReceived.getMessage()),
911 0,
912 false,
913 false,
914 false,
915 false,
916 false,
917 Optional.empty(),
918 Optional.empty(),
919 Optional.empty(),
920 getAttachments(extras),
921 Optional.empty(),
922 Optional.empty(),
923 List.of(),
924 getMentions(extras),
925 List.of(),
926 List.of())),
927 Optional.empty(),
928 Optional.empty(),
929 Optional.empty(),
930 Optional.empty());
931 notifyMessageHandlers(envelope);
932 };
933 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
934 this.dbusEditMsgHandler = messageReceived -> {
935 final var extras = messageReceived.getExtras();
936 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(messageReceived.getSender())),
937 0,
938 messageReceived.getTimestamp(),
939 0,
940 0,
941 false,
942 Optional.empty(),
943 Optional.empty(),
944 Optional.empty(),
945 Optional.of(new MessageEnvelope.Edit(messageReceived.getTargetSentTimestamp(),
946 new MessageEnvelope.Data(messageReceived.getTimestamp(),
947 messageReceived.getGroupId().length > 0
948 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
949 messageReceived.getGroupId()), false, 0))
950 : Optional.empty(),
951 Optional.empty(),
952 Optional.empty(),
953 Optional.of(messageReceived.getMessage()),
954 0,
955 false,
956 false,
957 false,
958 false,
959 false,
960 Optional.empty(),
961 Optional.empty(),
962 Optional.empty(),
963 getAttachments(extras),
964 Optional.empty(),
965 Optional.empty(),
966 List.of(),
967 getMentions(extras),
968 List.of(),
969 List.of()))),
970 Optional.empty(),
971 Optional.empty(),
972 Optional.empty());
973 notifyMessageHandlers(envelope);
974 };
975 connection.addSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler);
976
977 this.dbusRcptHandler = receiptReceived -> {
978 final var type = switch (receiptReceived.getReceiptType()) {
979 case "read" -> MessageEnvelope.Receipt.Type.READ;
980 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
981 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
982 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
983 };
984 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(receiptReceived.getSender())),
985 0,
986 receiptReceived.getTimestamp(),
987 0,
988 0,
989 false,
990 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
991 type,
992 List.of(receiptReceived.getTimestamp()))),
993 Optional.empty(),
994 Optional.empty(),
995 Optional.empty(),
996 Optional.empty(),
997 Optional.empty(),
998 Optional.empty());
999 notifyMessageHandlers(envelope);
1000 };
1001 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
1002
1003 this.dbusSyncHandler = syncReceived -> {
1004 final var extras = syncReceived.getExtras();
1005 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(syncReceived.getSource())),
1006 0,
1007 syncReceived.getTimestamp(),
1008 0,
1009 0,
1010 false,
1011 Optional.empty(),
1012 Optional.empty(),
1013 Optional.empty(),
1014 Optional.empty(),
1015 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
1016 syncReceived.getTimestamp(),
1017 syncReceived.getDestination().isEmpty()
1018 ? Optional.empty()
1019 : Optional.of(new RecipientAddress(syncReceived.getDestination())),
1020 Set.of(),
1021 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
1022 syncReceived.getGroupId().length > 0
1023 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
1024 syncReceived.getGroupId()), false, 0))
1025 : Optional.empty(),
1026 Optional.empty(),
1027 Optional.empty(),
1028 Optional.of(syncReceived.getMessage()),
1029 0,
1030 false,
1031 false,
1032 false,
1033 false,
1034 false,
1035 Optional.empty(),
1036 Optional.empty(),
1037 Optional.empty(),
1038 getAttachments(extras),
1039 Optional.empty(),
1040 Optional.empty(),
1041 List.of(),
1042 getMentions(extras),
1043 List.of(),
1044 List.of())),
1045 Optional.empty(),
1046 Optional.empty())),
1047 Optional.empty(),
1048 List.of(),
1049 List.of(),
1050 Optional.empty(),
1051 Optional.empty(),
1052 Optional.empty(),
1053 Optional.empty())),
1054 Optional.empty(),
1055 Optional.empty());
1056 notifyMessageHandlers(envelope);
1057 };
1058 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
1059 } catch (DBusException e) {
1060 throw new RuntimeException(e);
1061 }
1062 signal.subscribeReceive();
1063 }
1064
1065 private void notifyMessageHandlers(final MessageEnvelope envelope) {
1066 synchronized (messageHandlers) {
1067 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
1068 .forEach(h -> h.handleMessage(envelope, null));
1069 }
1070 }
1071
1072 private void uninstallMessageHandlers() {
1073 try {
1074 signal.unsubscribeReceive();
1075 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
1076 connection.removeSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler);
1077 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
1078 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
1079 } catch (DBusException e) {
1080 throw new RuntimeException(e);
1081 }
1082 }
1083
1084 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
1085 if (!extras.containsKey("attachments")) {
1086 return List.of();
1087 }
1088
1089 final List<Map<String, Variant<?>>> attachments = getValue(extras, "attachments");
1090 return attachments.stream().map(a -> {
1091 final String file = a.containsKey("file") ? getValue(a, "file") : null;
1092 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
1093 ? Optional.of(getValue(a, "remoteId"))
1094 : Optional.empty(),
1095 file != null ? Optional.of(new File(file)) : Optional.empty(),
1096 Optional.empty(),
1097 getValue(a, "contentType"),
1098 Optional.empty(),
1099 Optional.empty(),
1100 Optional.empty(),
1101 Optional.empty(),
1102 Optional.empty(),
1103 Optional.empty(),
1104 Optional.empty(),
1105 getValue(a, "isVoiceNote"),
1106 getValue(a, "isGif"),
1107 getValue(a, "isBorderless"));
1108 }).toList();
1109 }
1110
1111 private List<MessageEnvelope.Data.Mention> getMentions(final Map<String, Variant<?>> extras) {
1112 if (!extras.containsKey("mentions")) {
1113 return List.of();
1114 }
1115
1116 final List<Map<String, Variant<?>>> mentions = getValue(extras, "mentions");
1117 return mentions.stream()
1118 .map(a -> new MessageEnvelope.Data.Mention(new RecipientAddress(this.<String>getValue(a, "recipient")),
1119 getValue(a, "start"),
1120 getValue(a, "length")))
1121 .toList();
1122 }
1123
1124 @Override
1125 public InputStream retrieveAttachment(final String id) throws IOException {
1126 throw new UnsupportedOperationException();
1127 }
1128
1129 @Override
1130 public InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
1131 throw new UnsupportedOperationException();
1132 }
1133
1134 @Override
1135 public InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
1136 throw new UnsupportedOperationException();
1137 }
1138
1139 @Override
1140 public InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException {
1141 throw new UnsupportedOperationException();
1142 }
1143
1144 @Override
1145 public InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException {
1146 throw new UnsupportedOperationException();
1147 }
1148
1149 @SuppressWarnings("unchecked")
1150 private <T> T getValue(final Map<String, Variant<?>> stringVariantMap, final String field) {
1151 return (T) stringVariantMap.get(field).getValue();
1152 }
1153 }