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