]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Add addStickerPack command
[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 void installStickerPack(final StickerPackUrl url) throws IOException {
486 throw new UnsupportedOperationException();
487 }
488
489 @Override
490 public List<StickerPack> getStickerPacks() {
491 throw new UnsupportedOperationException();
492 }
493
494 @Override
495 public void requestAllSyncData() throws IOException {
496 signal.sendSyncRequest();
497 }
498
499 @Override
500 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
501 synchronized (messageHandlers) {
502 if (isWeakListener) {
503 weakHandlers.add(handler);
504 } else {
505 if (messageHandlers.size() == 0) {
506 installMessageHandlers();
507 }
508 messageHandlers.add(handler);
509 }
510 }
511 }
512
513 @Override
514 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
515 synchronized (messageHandlers) {
516 weakHandlers.remove(handler);
517 messageHandlers.remove(handler);
518 if (messageHandlers.size() == 0) {
519 uninstallMessageHandlers();
520 }
521 }
522 }
523
524 @Override
525 public boolean isReceiving() {
526 synchronized (messageHandlers) {
527 return messageHandlers.size() > 0;
528 }
529 }
530
531 @Override
532 public void receiveMessages(
533 Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
534 ) throws IOException {
535 final var remainingMessages = new AtomicInteger(maxMessages.orElse(-1));
536 final var lastMessage = new AtomicLong(System.currentTimeMillis());
537 final var thread = Thread.currentThread();
538
539 final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
540 lastMessage.set(System.currentTimeMillis());
541 handler.handleMessage(envelope, e);
542 if (remainingMessages.get() > 0) {
543 if (remainingMessages.decrementAndGet() <= 0) {
544 remainingMessages.set(0);
545 thread.interrupt();
546 }
547 }
548 };
549 addReceiveHandler(receiveHandler);
550 if (timeout.isPresent()) {
551 while (remainingMessages.get() != 0) {
552 try {
553 final var passedTime = System.currentTimeMillis() - lastMessage.get();
554 final var sleepTimeRemaining = timeout.get().toMillis() - passedTime;
555 if (sleepTimeRemaining < 0) {
556 break;
557 }
558 Thread.sleep(sleepTimeRemaining);
559 } catch (InterruptedException ignored) {
560 }
561 }
562 } else {
563 try {
564 synchronized (this) {
565 this.wait();
566 }
567 } catch (InterruptedException ignored) {
568 }
569 }
570
571 removeReceiveHandler(receiveHandler);
572 }
573
574 @Override
575 public void setReceiveConfig(final ReceiveConfig receiveConfig) {
576 }
577
578 @Override
579 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
580 return signal.isContactBlocked(recipient.getIdentifier());
581 }
582
583 @Override
584 public void sendContacts() throws IOException {
585 signal.sendContacts();
586 }
587
588 @Override
589 public List<Recipient> getRecipients(
590 final boolean onlyContacts,
591 final Optional<Boolean> blocked,
592 final Collection<RecipientIdentifier.Single> addresses,
593 final Optional<String> name
594 ) {
595 final var numbers = addresses.stream()
596 .filter(s -> s instanceof RecipientIdentifier.Number)
597 .map(s -> ((RecipientIdentifier.Number) s).number())
598 .collect(Collectors.toSet());
599 return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
600 final var contactBlocked = signal.isContactBlocked(n);
601 if (blocked.isPresent() && blocked.get() != contactBlocked) {
602 return null;
603 }
604 final var contactName = signal.getContactName(n);
605 if (onlyContacts && contactName.length() == 0) {
606 return null;
607 }
608 if (name.isPresent() && !name.get().equals(contactName)) {
609 return null;
610 }
611 return Recipient.newBuilder()
612 .withAddress(new RecipientAddress(null, n))
613 .withContact(new Contact(contactName, null, null, 0, contactBlocked, false, false))
614 .build();
615 }).filter(Objects::nonNull).toList();
616 }
617
618 @Override
619 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
620 return signal.getContactName(recipient.getIdentifier());
621 }
622
623 @Override
624 public Group getGroup(final GroupId groupId) {
625 final var groupPath = signal.getGroup(groupId.serialize());
626 return getGroup(groupPath);
627 }
628
629 @SuppressWarnings("unchecked")
630 private Group getGroup(final DBusPath groupPath) {
631 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
632 final var id = (byte[]) group.get("Id").getValue();
633 try {
634 return new Group(GroupId.unknownVersion(id),
635 (String) group.get("Name").getValue(),
636 (String) group.get("Description").getValue(),
637 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
638 ((List<String>) group.get("Members").getValue()).stream()
639 .map(m -> new RecipientAddress(null, m))
640 .collect(Collectors.toSet()),
641 ((List<String>) group.get("PendingMembers").getValue()).stream()
642 .map(m -> new RecipientAddress(null, m))
643 .collect(Collectors.toSet()),
644 ((List<String>) group.get("RequestingMembers").getValue()).stream()
645 .map(m -> new RecipientAddress(null, m))
646 .collect(Collectors.toSet()),
647 ((List<String>) group.get("Admins").getValue()).stream()
648 .map(m -> new RecipientAddress(null, m))
649 .collect(Collectors.toSet()),
650 ((List<String>) group.get("Banned").getValue()).stream()
651 .map(m -> new RecipientAddress(null, m))
652 .collect(Collectors.toSet()),
653 (boolean) group.get("IsBlocked").getValue(),
654 (int) group.get("MessageExpirationTimer").getValue(),
655 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
656 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
657 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
658 (boolean) group.get("IsMember").getValue(),
659 (boolean) group.get("IsAdmin").getValue());
660 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
661 throw new AssertionError(e);
662 }
663 }
664
665 @Override
666 public List<Identity> getIdentities() {
667 throw new UnsupportedOperationException();
668 }
669
670 @Override
671 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
672 throw new UnsupportedOperationException();
673 }
674
675 @Override
676 public boolean trustIdentityVerified(
677 final RecipientIdentifier.Single recipient, final IdentityVerificationCode verificationCode
678 ) {
679 throw new UnsupportedOperationException();
680 }
681
682 @Override
683 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
684 throw new UnsupportedOperationException();
685 }
686
687 @Override
688 public void addAddressChangedListener(final Runnable listener) {
689 }
690
691 @Override
692 public void addClosedListener(final Runnable listener) {
693 synchronized (closedListeners) {
694 closedListeners.add(listener);
695 }
696 }
697
698 @Override
699 public void close() {
700 synchronized (this) {
701 this.notify();
702 }
703 synchronized (messageHandlers) {
704 if (messageHandlers.size() > 0) {
705 uninstallMessageHandlers();
706 }
707 weakHandlers.clear();
708 messageHandlers.clear();
709 }
710 synchronized (closedListeners) {
711 closedListeners.forEach(Runnable::run);
712 closedListeners.clear();
713 }
714 }
715
716 private SendMessageResults handleMessage(
717 Set<RecipientIdentifier> recipients,
718 Function<List<String>, Long> recipientsHandler,
719 Supplier<Long> noteToSelfHandler,
720 Function<byte[], Long> groupHandler
721 ) {
722 long timestamp = 0;
723 final var singleRecipients = recipients.stream()
724 .filter(r -> r instanceof RecipientIdentifier.Single)
725 .map(RecipientIdentifier.Single.class::cast)
726 .map(RecipientIdentifier.Single::getIdentifier)
727 .toList();
728 if (singleRecipients.size() > 0) {
729 timestamp = recipientsHandler.apply(singleRecipients);
730 }
731
732 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
733 timestamp = noteToSelfHandler.get();
734 }
735 final var groupRecipients = recipients.stream()
736 .filter(r -> r instanceof RecipientIdentifier.Group)
737 .map(RecipientIdentifier.Group.class::cast)
738 .map(RecipientIdentifier.Group::groupId)
739 .toList();
740 for (final var groupId : groupRecipients) {
741 timestamp = groupHandler.apply(groupId.serialize());
742 }
743 return new SendMessageResults(timestamp, Map.of());
744 }
745
746 private String emptyIfNull(final String string) {
747 return string == null ? "" : string;
748 }
749
750 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
751 try {
752 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
753 } catch (DBusException e) {
754 throw new AssertionError(e);
755 }
756 }
757
758 private void installMessageHandlers() {
759 try {
760 this.dbusMsgHandler = messageReceived -> {
761 final var extras = messageReceived.getExtras();
762 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
763 messageReceived.getSender())),
764 0,
765 messageReceived.getTimestamp(),
766 0,
767 0,
768 false,
769 Optional.empty(),
770 Optional.empty(),
771 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
772 messageReceived.getGroupId().length > 0
773 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
774 messageReceived.getGroupId()), false, 0))
775 : Optional.empty(),
776 Optional.empty(),
777 Optional.empty(),
778 Optional.of(messageReceived.getMessage()),
779 0,
780 false,
781 false,
782 false,
783 false,
784 false,
785 Optional.empty(),
786 Optional.empty(),
787 Optional.empty(),
788 getAttachments(extras),
789 Optional.empty(),
790 Optional.empty(),
791 List.of(),
792 List.of(),
793 List.of(),
794 List.of())),
795 Optional.empty(),
796 Optional.empty(),
797 Optional.empty(),
798 Optional.empty());
799 notifyMessageHandlers(envelope);
800 };
801 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
802
803 this.dbusRcptHandler = receiptReceived -> {
804 final var type = switch (receiptReceived.getReceiptType()) {
805 case "read" -> MessageEnvelope.Receipt.Type.READ;
806 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
807 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
808 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
809 };
810 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
811 receiptReceived.getSender())),
812 0,
813 receiptReceived.getTimestamp(),
814 0,
815 0,
816 false,
817 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
818 type,
819 List.of(receiptReceived.getTimestamp()))),
820 Optional.empty(),
821 Optional.empty(),
822 Optional.empty(),
823 Optional.empty(),
824 Optional.empty(),
825 Optional.empty());
826 notifyMessageHandlers(envelope);
827 };
828 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
829
830 this.dbusSyncHandler = syncReceived -> {
831 final var extras = syncReceived.getExtras();
832 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
833 syncReceived.getSource())),
834 0,
835 syncReceived.getTimestamp(),
836 0,
837 0,
838 false,
839 Optional.empty(),
840 Optional.empty(),
841 Optional.empty(),
842 Optional.empty(),
843 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
844 syncReceived.getTimestamp(),
845 syncReceived.getDestination().isEmpty()
846 ? Optional.empty()
847 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
848 Set.of(),
849 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
850 syncReceived.getGroupId().length > 0
851 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
852 syncReceived.getGroupId()), false, 0))
853 : Optional.empty(),
854 Optional.empty(),
855 Optional.empty(),
856 Optional.of(syncReceived.getMessage()),
857 0,
858 false,
859 false,
860 false,
861 false,
862 false,
863 Optional.empty(),
864 Optional.empty(),
865 Optional.empty(),
866 getAttachments(extras),
867 Optional.empty(),
868 Optional.empty(),
869 List.of(),
870 List.of(),
871 List.of(),
872 List.of())),
873 Optional.empty(),
874 Optional.empty())),
875 Optional.empty(),
876 List.of(),
877 List.of(),
878 Optional.empty(),
879 Optional.empty(),
880 Optional.empty(),
881 Optional.empty())),
882 Optional.empty(),
883 Optional.empty());
884 notifyMessageHandlers(envelope);
885 };
886 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
887 } catch (DBusException e) {
888 e.printStackTrace();
889 }
890 signal.subscribeReceive();
891 }
892
893 private void notifyMessageHandlers(final MessageEnvelope envelope) {
894 synchronized (messageHandlers) {
895 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
896 .forEach(h -> h.handleMessage(envelope, null));
897 }
898 }
899
900 private void uninstallMessageHandlers() {
901 try {
902 signal.unsubscribeReceive();
903 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
904 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
905 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
906 } catch (DBusException e) {
907 e.printStackTrace();
908 }
909 }
910
911 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
912 if (!extras.containsKey("attachments")) {
913 return List.of();
914 }
915
916 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
917 return attachments.stream().map(a -> {
918 final String file = a.containsKey("file") ? getValue(a, "file") : null;
919 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
920 ? Optional.of(getValue(a, "remoteId"))
921 : Optional.empty(),
922 file != null ? Optional.of(new File(file)) : Optional.empty(),
923 Optional.empty(),
924 getValue(a, "contentType"),
925 Optional.empty(),
926 Optional.empty(),
927 Optional.empty(),
928 Optional.empty(),
929 Optional.empty(),
930 Optional.empty(),
931 Optional.empty(),
932 getValue(a, "isVoiceNote"),
933 getValue(a, "isGif"),
934 getValue(a, "isBorderless"));
935 }).toList();
936 }
937
938 @Override
939 public InputStream retrieveAttachment(final String id) throws IOException {
940 throw new UnsupportedOperationException();
941 }
942
943 @SuppressWarnings("unchecked")
944 private <T> T getValue(
945 final Map<String, Variant<?>> stringVariantMap, final String field
946 ) {
947 return (T) stringVariantMap.get(field).getValue();
948 }
949 }