]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Refactor addDeviceLink method
[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.Contact;
9 import org.asamk.signal.manager.api.Device;
10 import org.asamk.signal.manager.api.DeviceLinkUrl;
11 import org.asamk.signal.manager.api.Group;
12 import org.asamk.signal.manager.api.GroupId;
13 import org.asamk.signal.manager.api.GroupInviteLinkUrl;
14 import org.asamk.signal.manager.api.GroupNotFoundException;
15 import org.asamk.signal.manager.api.GroupPermission;
16 import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
17 import org.asamk.signal.manager.api.Identity;
18 import org.asamk.signal.manager.api.InactiveGroupLinkException;
19 import org.asamk.signal.manager.api.InvalidDeviceLinkException;
20 import org.asamk.signal.manager.api.InvalidStickerException;
21 import org.asamk.signal.manager.api.InvalidUsernameException;
22 import org.asamk.signal.manager.api.LastGroupAdminException;
23 import org.asamk.signal.manager.api.Message;
24 import org.asamk.signal.manager.api.MessageEnvelope;
25 import org.asamk.signal.manager.api.NotAGroupMemberException;
26 import org.asamk.signal.manager.api.NotPrimaryDeviceException;
27 import org.asamk.signal.manager.api.Pair;
28 import org.asamk.signal.manager.api.ReceiveConfig;
29 import org.asamk.signal.manager.api.Recipient;
30 import org.asamk.signal.manager.api.RecipientAddress;
31 import org.asamk.signal.manager.api.RecipientIdentifier;
32 import org.asamk.signal.manager.api.SendGroupMessageResults;
33 import org.asamk.signal.manager.api.SendMessageResults;
34 import org.asamk.signal.manager.api.StickerPack;
35 import org.asamk.signal.manager.api.StickerPackInvalidException;
36 import org.asamk.signal.manager.api.StickerPackUrl;
37 import org.asamk.signal.manager.api.TypingAction;
38 import org.asamk.signal.manager.api.UnregisteredRecipientException;
39 import org.asamk.signal.manager.api.UpdateGroup;
40 import org.asamk.signal.manager.api.UpdateProfile;
41 import org.asamk.signal.manager.api.UserStatus;
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 DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException {
204 signal.addDevice(linkUri.createDeviceLinkUri().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 List<Group> getGroups() {
218 final var groups = signal.listGroups();
219 return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).toList();
220 }
221
222 @Override
223 public SendGroupMessageResults quitGroup(
224 final GroupId groupId, final Set<RecipientIdentifier.Single> groupAdmins
225 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
226 if (groupAdmins.size() > 0) {
227 throw new UnsupportedOperationException();
228 }
229 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
230 try {
231 group.quitGroup();
232 } catch (Signal.Error.GroupNotFound e) {
233 throw new GroupNotFoundException(groupId);
234 } catch (Signal.Error.NotAGroupMember e) {
235 throw new NotAGroupMemberException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
236 } catch (Signal.Error.LastGroupAdmin e) {
237 throw new LastGroupAdminException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
238 }
239 return new SendGroupMessageResults(0, List.of());
240 }
241
242 @Override
243 public void deleteGroup(final GroupId groupId) throws IOException {
244 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
245 group.deleteGroup();
246 }
247
248 @Override
249 public Pair<GroupId, SendGroupMessageResults> createGroup(
250 final String name, final Set<RecipientIdentifier.Single> members, final String avatarFile
251 ) throws IOException, AttachmentInvalidException {
252 final var newGroupId = signal.createGroup(emptyIfNull(name),
253 members.stream().map(RecipientIdentifier.Single::getIdentifier).toList(),
254 avatarFile == null ? "" : avatarFile);
255 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
256 }
257
258 @Override
259 public SendGroupMessageResults updateGroup(
260 final GroupId groupId, final UpdateGroup updateGroup
261 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
262 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
263 if (updateGroup.getName() != null) {
264 group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
265 }
266 if (updateGroup.getDescription() != null) {
267 group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
268 }
269 if (updateGroup.getAvatarFile() != null) {
270 group.Set("org.asamk.Signal.Group",
271 "Avatar",
272 updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile());
273 }
274 if (updateGroup.getExpirationTimer() != null) {
275 group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
276 }
277 if (updateGroup.getAddMemberPermission() != null) {
278 group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
279 }
280 if (updateGroup.getEditDetailsPermission() != null) {
281 group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
282 }
283 if (updateGroup.getIsAnnouncementGroup() != null) {
284 group.Set("org.asamk.Signal.Group",
285 "PermissionSendMessage",
286 updateGroup.getIsAnnouncementGroup()
287 ? GroupPermission.ONLY_ADMINS.name()
288 : GroupPermission.EVERY_MEMBER.name());
289 }
290 if (updateGroup.getMembers() != null) {
291 group.addMembers(updateGroup.getMembers().stream().map(RecipientIdentifier.Single::getIdentifier).toList());
292 }
293 if (updateGroup.getRemoveMembers() != null) {
294 group.removeMembers(updateGroup.getRemoveMembers()
295 .stream()
296 .map(RecipientIdentifier.Single::getIdentifier)
297 .toList());
298 }
299 if (updateGroup.getAdmins() != null) {
300 group.addAdmins(updateGroup.getAdmins().stream().map(RecipientIdentifier.Single::getIdentifier).toList());
301 }
302 if (updateGroup.getRemoveAdmins() != null) {
303 group.removeAdmins(updateGroup.getRemoveAdmins()
304 .stream()
305 .map(RecipientIdentifier.Single::getIdentifier)
306 .toList());
307 }
308 if (updateGroup.isResetGroupLink()) {
309 group.resetLink();
310 }
311 if (updateGroup.getGroupLinkState() != null) {
312 switch (updateGroup.getGroupLinkState()) {
313 case DISABLED -> group.disableLink();
314 case ENABLED -> group.enableLink(false);
315 case ENABLED_WITH_APPROVAL -> group.enableLink(true);
316 }
317 }
318 return new SendGroupMessageResults(0, List.of());
319 }
320
321 @Override
322 public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, InactiveGroupLinkException {
323 final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
324 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
325 }
326
327 @Override
328 public SendMessageResults sendTypingMessage(
329 final TypingAction action, final Set<RecipientIdentifier> recipients
330 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
331 return handleMessage(recipients, numbers -> {
332 numbers.forEach(n -> signal.sendTyping(n, action == TypingAction.STOP));
333 return 0L;
334 }, () -> {
335 signal.sendTyping(signal.getSelfNumber(), action == TypingAction.STOP);
336 return 0L;
337 }, groupId -> {
338 signal.sendGroupTyping(groupId, action == TypingAction.STOP);
339 return 0L;
340 });
341 }
342
343 @Override
344 public SendMessageResults sendReadReceipt(
345 final RecipientIdentifier.Single sender, final List<Long> messageIds
346 ) {
347 signal.sendReadReceipt(sender.getIdentifier(), messageIds);
348 return new SendMessageResults(0, Map.of());
349 }
350
351 @Override
352 public SendMessageResults sendViewedReceipt(
353 final RecipientIdentifier.Single sender, final List<Long> messageIds
354 ) {
355 signal.sendViewedReceipt(sender.getIdentifier(), messageIds);
356 return new SendMessageResults(0, Map.of());
357 }
358
359 @Override
360 public SendMessageResults sendMessage(
361 final Message message, final Set<RecipientIdentifier> recipients
362 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
363 return handleMessage(recipients,
364 numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
365 () -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
366 groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
367 }
368
369 @Override
370 public SendMessageResults sendEditMessage(
371 final Message message, final Set<RecipientIdentifier> recipients, final long editTargetTimestamp
372 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
373 throw new UnsupportedOperationException();
374 }
375
376 @Override
377 public SendMessageResults sendRemoteDeleteMessage(
378 final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
379 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
380 return handleMessage(recipients,
381 numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
382 () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
383 groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
384 }
385
386 @Override
387 public SendMessageResults sendMessageReaction(
388 final String emoji,
389 final boolean remove,
390 final RecipientIdentifier.Single targetAuthor,
391 final long targetSentTimestamp,
392 final Set<RecipientIdentifier> recipients,
393 final boolean isStory
394 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
395 return handleMessage(recipients,
396 numbers -> signal.sendMessageReaction(emoji,
397 remove,
398 targetAuthor.getIdentifier(),
399 targetSentTimestamp,
400 numbers),
401 () -> signal.sendMessageReaction(emoji,
402 remove,
403 targetAuthor.getIdentifier(),
404 targetSentTimestamp,
405 signal.getSelfNumber()),
406 groupId -> signal.sendGroupMessageReaction(emoji,
407 remove,
408 targetAuthor.getIdentifier(),
409 targetSentTimestamp,
410 groupId));
411 }
412
413 @Override
414 public SendMessageResults sendPaymentNotificationMessage(
415 final byte[] receipt, final String note, final RecipientIdentifier.Single recipient
416 ) throws IOException {
417 final var timestamp = signal.sendPaymentNotification(receipt, note, recipient.getIdentifier());
418 return new SendMessageResults(timestamp, Map.of());
419 }
420
421 @Override
422 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
423 signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
424 return new SendMessageResults(0, Map.of());
425 }
426
427 @Override
428 public void deleteRecipient(final RecipientIdentifier.Single recipient) {
429 signal.deleteRecipient(recipient.getIdentifier());
430 }
431
432 @Override
433 public void deleteContact(final RecipientIdentifier.Single recipient) {
434 signal.deleteContact(recipient.getIdentifier());
435 }
436
437 @Override
438 public void setContactName(
439 final RecipientIdentifier.Single recipient, final String givenName, final String familyName
440 ) throws NotPrimaryDeviceException {
441 signal.setContactName(recipient.getIdentifier(), givenName);
442 }
443
444 @Override
445 public void setContactsBlocked(
446 final Collection<RecipientIdentifier.Single> recipients, final boolean blocked
447 ) throws NotPrimaryDeviceException, IOException {
448 for (final var recipient : recipients) {
449 signal.setContactBlocked(recipient.getIdentifier(), blocked);
450 }
451 }
452
453 @Override
454 public void setGroupsBlocked(
455 final Collection<GroupId> groupIds, final boolean blocked
456 ) throws GroupNotFoundException, IOException {
457 for (final var groupId : groupIds) {
458 setGroupProperty(groupId, "IsBlocked", blocked);
459 }
460 }
461
462 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
463 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
464 group.Set("org.asamk.Signal.Group", propertyName, blocked);
465 }
466
467 @Override
468 public void setExpirationTimer(
469 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
470 ) throws IOException {
471 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
472 }
473
474 @Override
475 public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
476 try {
477 return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath())));
478 } catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) {
479 throw new AssertionError(e);
480 }
481 }
482
483 @Override
484 public List<StickerPack> getStickerPacks() {
485 throw new UnsupportedOperationException();
486 }
487
488 @Override
489 public void requestAllSyncData() throws IOException {
490 signal.sendSyncRequest();
491 }
492
493 @Override
494 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
495 synchronized (messageHandlers) {
496 if (isWeakListener) {
497 weakHandlers.add(handler);
498 } else {
499 if (messageHandlers.size() == 0) {
500 installMessageHandlers();
501 }
502 messageHandlers.add(handler);
503 }
504 }
505 }
506
507 @Override
508 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
509 synchronized (messageHandlers) {
510 weakHandlers.remove(handler);
511 messageHandlers.remove(handler);
512 if (messageHandlers.size() == 0) {
513 uninstallMessageHandlers();
514 }
515 }
516 }
517
518 @Override
519 public boolean isReceiving() {
520 synchronized (messageHandlers) {
521 return messageHandlers.size() > 0;
522 }
523 }
524
525 @Override
526 public void receiveMessages(
527 Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
528 ) throws IOException {
529 final var remainingMessages = new AtomicInteger(maxMessages.orElse(-1));
530 final var lastMessage = new AtomicLong(System.currentTimeMillis());
531 final var thread = Thread.currentThread();
532
533 final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
534 lastMessage.set(System.currentTimeMillis());
535 handler.handleMessage(envelope, e);
536 if (remainingMessages.get() > 0) {
537 if (remainingMessages.decrementAndGet() <= 0) {
538 remainingMessages.set(0);
539 thread.interrupt();
540 }
541 }
542 };
543 addReceiveHandler(receiveHandler);
544 if (timeout.isPresent()) {
545 while (remainingMessages.get() != 0) {
546 try {
547 final var passedTime = System.currentTimeMillis() - lastMessage.get();
548 final var sleepTimeRemaining = timeout.get().toMillis() - passedTime;
549 if (sleepTimeRemaining < 0) {
550 break;
551 }
552 Thread.sleep(sleepTimeRemaining);
553 } catch (InterruptedException ignored) {
554 }
555 }
556 } else {
557 try {
558 synchronized (this) {
559 this.wait();
560 }
561 } catch (InterruptedException ignored) {
562 }
563 }
564
565 removeReceiveHandler(receiveHandler);
566 }
567
568 @Override
569 public void setReceiveConfig(final ReceiveConfig receiveConfig) {
570 }
571
572 @Override
573 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
574 return signal.isContactBlocked(recipient.getIdentifier());
575 }
576
577 @Override
578 public void sendContacts() throws IOException {
579 signal.sendContacts();
580 }
581
582 @Override
583 public List<Recipient> getRecipients(
584 final boolean onlyContacts,
585 final Optional<Boolean> blocked,
586 final Collection<RecipientIdentifier.Single> addresses,
587 final Optional<String> name
588 ) {
589 final var numbers = addresses.stream()
590 .filter(s -> s instanceof RecipientIdentifier.Number)
591 .map(s -> ((RecipientIdentifier.Number) s).number())
592 .collect(Collectors.toSet());
593 return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
594 final var contactBlocked = signal.isContactBlocked(n);
595 if (blocked.isPresent() && blocked.get() != contactBlocked) {
596 return null;
597 }
598 final var contactName = signal.getContactName(n);
599 if (onlyContacts && contactName.length() == 0) {
600 return null;
601 }
602 if (name.isPresent() && !name.get().equals(contactName)) {
603 return null;
604 }
605 return Recipient.newBuilder()
606 .withAddress(new RecipientAddress(null, n))
607 .withContact(new Contact(contactName, null, null, 0, contactBlocked, false, false))
608 .build();
609 }).filter(Objects::nonNull).toList();
610 }
611
612 @Override
613 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
614 return signal.getContactName(recipient.getIdentifier());
615 }
616
617 @Override
618 public Group getGroup(final GroupId groupId) {
619 final var groupPath = signal.getGroup(groupId.serialize());
620 return getGroup(groupPath);
621 }
622
623 @SuppressWarnings("unchecked")
624 private Group getGroup(final DBusPath groupPath) {
625 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
626 final var id = (byte[]) group.get("Id").getValue();
627 try {
628 return new Group(GroupId.unknownVersion(id),
629 (String) group.get("Name").getValue(),
630 (String) group.get("Description").getValue(),
631 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
632 ((List<String>) group.get("Members").getValue()).stream()
633 .map(m -> new RecipientAddress(null, m))
634 .collect(Collectors.toSet()),
635 ((List<String>) group.get("PendingMembers").getValue()).stream()
636 .map(m -> new RecipientAddress(null, m))
637 .collect(Collectors.toSet()),
638 ((List<String>) group.get("RequestingMembers").getValue()).stream()
639 .map(m -> new RecipientAddress(null, m))
640 .collect(Collectors.toSet()),
641 ((List<String>) group.get("Admins").getValue()).stream()
642 .map(m -> new RecipientAddress(null, m))
643 .collect(Collectors.toSet()),
644 ((List<String>) group.get("Banned").getValue()).stream()
645 .map(m -> new RecipientAddress(null, m))
646 .collect(Collectors.toSet()),
647 (boolean) group.get("IsBlocked").getValue(),
648 (int) group.get("MessageExpirationTimer").getValue(),
649 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
650 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
651 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
652 (boolean) group.get("IsMember").getValue(),
653 (boolean) group.get("IsAdmin").getValue());
654 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
655 throw new AssertionError(e);
656 }
657 }
658
659 @Override
660 public List<Identity> getIdentities() {
661 throw new UnsupportedOperationException();
662 }
663
664 @Override
665 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
666 throw new UnsupportedOperationException();
667 }
668
669 @Override
670 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
671 throw new UnsupportedOperationException();
672 }
673
674 @Override
675 public boolean trustIdentityVerifiedSafetyNumber(
676 final RecipientIdentifier.Single recipient, final String safetyNumber
677 ) {
678 throw new UnsupportedOperationException();
679 }
680
681 @Override
682 public boolean trustIdentityVerifiedSafetyNumber(
683 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
684 ) {
685 throw new UnsupportedOperationException();
686 }
687
688 @Override
689 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
690 throw new UnsupportedOperationException();
691 }
692
693 @Override
694 public void addAddressChangedListener(final Runnable listener) {
695 }
696
697 @Override
698 public void addClosedListener(final Runnable listener) {
699 synchronized (closedListeners) {
700 closedListeners.add(listener);
701 }
702 }
703
704 @Override
705 public void close() {
706 synchronized (this) {
707 this.notify();
708 }
709 synchronized (messageHandlers) {
710 if (messageHandlers.size() > 0) {
711 uninstallMessageHandlers();
712 }
713 weakHandlers.clear();
714 messageHandlers.clear();
715 }
716 synchronized (closedListeners) {
717 closedListeners.forEach(Runnable::run);
718 closedListeners.clear();
719 }
720 }
721
722 private SendMessageResults handleMessage(
723 Set<RecipientIdentifier> recipients,
724 Function<List<String>, Long> recipientsHandler,
725 Supplier<Long> noteToSelfHandler,
726 Function<byte[], Long> groupHandler
727 ) {
728 long timestamp = 0;
729 final var singleRecipients = recipients.stream()
730 .filter(r -> r instanceof RecipientIdentifier.Single)
731 .map(RecipientIdentifier.Single.class::cast)
732 .map(RecipientIdentifier.Single::getIdentifier)
733 .toList();
734 if (singleRecipients.size() > 0) {
735 timestamp = recipientsHandler.apply(singleRecipients);
736 }
737
738 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
739 timestamp = noteToSelfHandler.get();
740 }
741 final var groupRecipients = recipients.stream()
742 .filter(r -> r instanceof RecipientIdentifier.Group)
743 .map(RecipientIdentifier.Group.class::cast)
744 .map(RecipientIdentifier.Group::groupId)
745 .toList();
746 for (final var groupId : groupRecipients) {
747 timestamp = groupHandler.apply(groupId.serialize());
748 }
749 return new SendMessageResults(timestamp, Map.of());
750 }
751
752 private String emptyIfNull(final String string) {
753 return string == null ? "" : string;
754 }
755
756 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
757 try {
758 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
759 } catch (DBusException e) {
760 throw new AssertionError(e);
761 }
762 }
763
764 private void installMessageHandlers() {
765 try {
766 this.dbusMsgHandler = messageReceived -> {
767 final var extras = messageReceived.getExtras();
768 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
769 messageReceived.getSender())),
770 0,
771 messageReceived.getTimestamp(),
772 0,
773 0,
774 false,
775 Optional.empty(),
776 Optional.empty(),
777 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
778 messageReceived.getGroupId().length > 0
779 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
780 messageReceived.getGroupId()), false, 0))
781 : Optional.empty(),
782 Optional.empty(),
783 Optional.empty(),
784 Optional.of(messageReceived.getMessage()),
785 0,
786 false,
787 false,
788 false,
789 false,
790 false,
791 Optional.empty(),
792 Optional.empty(),
793 Optional.empty(),
794 getAttachments(extras),
795 Optional.empty(),
796 Optional.empty(),
797 List.of(),
798 List.of(),
799 List.of(),
800 List.of())),
801 Optional.empty(),
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 Optional.empty());
832 notifyMessageHandlers(envelope);
833 };
834 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
835
836 this.dbusSyncHandler = syncReceived -> {
837 final var extras = syncReceived.getExtras();
838 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
839 syncReceived.getSource())),
840 0,
841 syncReceived.getTimestamp(),
842 0,
843 0,
844 false,
845 Optional.empty(),
846 Optional.empty(),
847 Optional.empty(),
848 Optional.empty(),
849 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
850 syncReceived.getTimestamp(),
851 syncReceived.getDestination().isEmpty()
852 ? Optional.empty()
853 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
854 Set.of(),
855 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
856 syncReceived.getGroupId().length > 0
857 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
858 syncReceived.getGroupId()), false, 0))
859 : Optional.empty(),
860 Optional.empty(),
861 Optional.empty(),
862 Optional.of(syncReceived.getMessage()),
863 0,
864 false,
865 false,
866 false,
867 false,
868 false,
869 Optional.empty(),
870 Optional.empty(),
871 Optional.empty(),
872 getAttachments(extras),
873 Optional.empty(),
874 Optional.empty(),
875 List.of(),
876 List.of(),
877 List.of(),
878 List.of())),
879 Optional.empty(),
880 Optional.empty())),
881 Optional.empty(),
882 List.of(),
883 List.of(),
884 Optional.empty(),
885 Optional.empty(),
886 Optional.empty(),
887 Optional.empty())),
888 Optional.empty(),
889 Optional.empty());
890 notifyMessageHandlers(envelope);
891 };
892 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
893 } catch (DBusException e) {
894 e.printStackTrace();
895 }
896 signal.subscribeReceive();
897 }
898
899 private void notifyMessageHandlers(final MessageEnvelope envelope) {
900 synchronized (messageHandlers) {
901 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
902 .forEach(h -> h.handleMessage(envelope, null));
903 }
904 }
905
906 private void uninstallMessageHandlers() {
907 try {
908 signal.unsubscribeReceive();
909 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
910 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
911 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
912 } catch (DBusException e) {
913 e.printStackTrace();
914 }
915 }
916
917 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
918 if (!extras.containsKey("attachments")) {
919 return List.of();
920 }
921
922 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
923 return attachments.stream().map(a -> {
924 final String file = a.containsKey("file") ? getValue(a, "file") : null;
925 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
926 ? Optional.of(getValue(a, "remoteId"))
927 : Optional.empty(),
928 file != null ? Optional.of(new File(file)) : Optional.empty(),
929 Optional.empty(),
930 getValue(a, "contentType"),
931 Optional.empty(),
932 Optional.empty(),
933 Optional.empty(),
934 Optional.empty(),
935 Optional.empty(),
936 Optional.empty(),
937 Optional.empty(),
938 getValue(a, "isVoiceNote"),
939 getValue(a, "isGif"),
940 getValue(a, "isBorderless"));
941 }).toList();
942 }
943
944 @Override
945 public InputStream retrieveAttachment(final String id) throws IOException {
946 throw new UnsupportedOperationException();
947 }
948
949 @SuppressWarnings("unchecked")
950 private <T> T getValue(
951 final Map<String, Variant<?>> stringVariantMap, final String field
952 ) {
953 return (T) stringVariantMap.get(field).getValue();
954 }
955 }