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