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