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