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