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