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