]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Print text styles in plain text output
[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(),
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 String 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);
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());
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 List.of())),
783 Optional.empty(),
784 Optional.empty(),
785 Optional.empty());
786 notifyMessageHandlers(envelope);
787 };
788 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
789
790 this.dbusRcptHandler = receiptReceived -> {
791 final var type = switch (receiptReceived.getReceiptType()) {
792 case "read" -> MessageEnvelope.Receipt.Type.READ;
793 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
794 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
795 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
796 };
797 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
798 receiptReceived.getSender())),
799 0,
800 receiptReceived.getTimestamp(),
801 0,
802 0,
803 false,
804 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
805 type,
806 List.of(receiptReceived.getTimestamp()))),
807 Optional.empty(),
808 Optional.empty(),
809 Optional.empty(),
810 Optional.empty(),
811 Optional.empty());
812 notifyMessageHandlers(envelope);
813 };
814 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
815
816 this.dbusSyncHandler = syncReceived -> {
817 final var extras = syncReceived.getExtras();
818 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
819 syncReceived.getSource())),
820 0,
821 syncReceived.getTimestamp(),
822 0,
823 0,
824 false,
825 Optional.empty(),
826 Optional.empty(),
827 Optional.empty(),
828 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
829 syncReceived.getTimestamp(),
830 syncReceived.getDestination().isEmpty()
831 ? Optional.empty()
832 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
833 Set.of(),
834 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
835 syncReceived.getGroupId().length > 0
836 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
837 syncReceived.getGroupId()), false, 0))
838 : Optional.empty(),
839 Optional.empty(),
840 Optional.empty(),
841 Optional.of(syncReceived.getMessage()),
842 0,
843 false,
844 false,
845 false,
846 false,
847 false,
848 Optional.empty(),
849 Optional.empty(),
850 Optional.empty(),
851 getAttachments(extras),
852 Optional.empty(),
853 Optional.empty(),
854 List.of(),
855 List.of(),
856 List.of(),
857 List.of())),
858 Optional.empty())),
859 Optional.empty(),
860 List.of(),
861 List.of(),
862 Optional.empty(),
863 Optional.empty(),
864 Optional.empty(),
865 Optional.empty())),
866 Optional.empty(),
867 Optional.empty());
868 notifyMessageHandlers(envelope);
869 };
870 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
871 } catch (DBusException e) {
872 e.printStackTrace();
873 }
874 signal.subscribeReceive();
875 }
876
877 private void notifyMessageHandlers(final MessageEnvelope envelope) {
878 synchronized (messageHandlers) {
879 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
880 .forEach(h -> h.handleMessage(envelope, null));
881 }
882 }
883
884 private void uninstallMessageHandlers() {
885 try {
886 signal.unsubscribeReceive();
887 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
888 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
889 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
890 } catch (DBusException e) {
891 e.printStackTrace();
892 }
893 }
894
895 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
896 if (!extras.containsKey("attachments")) {
897 return List.of();
898 }
899
900 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
901 return attachments.stream().map(a -> {
902 final String file = a.containsKey("file") ? getValue(a, "file") : null;
903 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
904 ? Optional.of(getValue(a, "remoteId"))
905 : Optional.empty(),
906 file != null ? Optional.of(new File(file)) : Optional.empty(),
907 Optional.empty(),
908 getValue(a, "contentType"),
909 Optional.empty(),
910 Optional.empty(),
911 Optional.empty(),
912 Optional.empty(),
913 Optional.empty(),
914 Optional.empty(),
915 Optional.empty(),
916 getValue(a, "isVoiceNote"),
917 getValue(a, "isGif"),
918 getValue(a, "isBorderless"));
919 }).toList();
920 }
921
922 @Override
923 public InputStream retrieveAttachment(final String id) throws IOException {
924 throw new UnsupportedOperationException();
925 }
926
927 @SuppressWarnings("unchecked")
928 private <T> T getValue(
929 final Map<String, Variant<?>> stringVariantMap, final String field
930 ) {
931 return (T) stringVariantMap.get(field).getValue();
932 }
933 }