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