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