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