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