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