]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Fix deleting old group in dbus mode
[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 try {
223 group.quitGroup();
224 } catch (Signal.Error.GroupNotFound e) {
225 throw new GroupNotFoundException(groupId);
226 } catch (Signal.Error.NotAGroupMember e) {
227 throw new NotAGroupMemberException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
228 } catch (Signal.Error.LastGroupAdmin e) {
229 throw new LastGroupAdminException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
230 }
231 return new SendGroupMessageResults(0, List.of());
232 }
233
234 @Override
235 public void deleteGroup(final GroupId groupId) throws IOException {
236 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
237 group.deleteGroup();
238 }
239
240 @Override
241 public Pair<GroupId, SendGroupMessageResults> createGroup(
242 final String name, final Set<RecipientIdentifier.Single> members, final String avatarFile
243 ) throws IOException, AttachmentInvalidException {
244 final var newGroupId = signal.createGroup(emptyIfNull(name),
245 members.stream().map(RecipientIdentifier.Single::getIdentifier).toList(),
246 avatarFile == null ? "" : avatarFile);
247 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
248 }
249
250 @Override
251 public SendGroupMessageResults updateGroup(
252 final GroupId groupId, final UpdateGroup updateGroup
253 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
254 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
255 if (updateGroup.getName() != null) {
256 group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
257 }
258 if (updateGroup.getDescription() != null) {
259 group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
260 }
261 if (updateGroup.getAvatarFile() != null) {
262 group.Set("org.asamk.Signal.Group",
263 "Avatar",
264 updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile());
265 }
266 if (updateGroup.getExpirationTimer() != null) {
267 group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
268 }
269 if (updateGroup.getAddMemberPermission() != null) {
270 group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
271 }
272 if (updateGroup.getEditDetailsPermission() != null) {
273 group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
274 }
275 if (updateGroup.getIsAnnouncementGroup() != null) {
276 group.Set("org.asamk.Signal.Group",
277 "PermissionSendMessage",
278 updateGroup.getIsAnnouncementGroup()
279 ? GroupPermission.ONLY_ADMINS.name()
280 : GroupPermission.EVERY_MEMBER.name());
281 }
282 if (updateGroup.getMembers() != null) {
283 group.addMembers(updateGroup.getMembers().stream().map(RecipientIdentifier.Single::getIdentifier).toList());
284 }
285 if (updateGroup.getRemoveMembers() != null) {
286 group.removeMembers(updateGroup.getRemoveMembers()
287 .stream()
288 .map(RecipientIdentifier.Single::getIdentifier)
289 .toList());
290 }
291 if (updateGroup.getAdmins() != null) {
292 group.addAdmins(updateGroup.getAdmins().stream().map(RecipientIdentifier.Single::getIdentifier).toList());
293 }
294 if (updateGroup.getRemoveAdmins() != null) {
295 group.removeAdmins(updateGroup.getRemoveAdmins()
296 .stream()
297 .map(RecipientIdentifier.Single::getIdentifier)
298 .toList());
299 }
300 if (updateGroup.isResetGroupLink()) {
301 group.resetLink();
302 }
303 if (updateGroup.getGroupLinkState() != null) {
304 switch (updateGroup.getGroupLinkState()) {
305 case DISABLED -> group.disableLink();
306 case ENABLED -> group.enableLink(false);
307 case ENABLED_WITH_APPROVAL -> group.enableLink(true);
308 }
309 }
310 return new SendGroupMessageResults(0, List.of());
311 }
312
313 @Override
314 public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, InactiveGroupLinkException {
315 final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
316 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
317 }
318
319 @Override
320 public SendMessageResults sendTypingMessage(
321 final TypingAction action, final Set<RecipientIdentifier> recipients
322 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
323 return handleMessage(recipients, numbers -> {
324 numbers.forEach(n -> signal.sendTyping(n, action == TypingAction.STOP));
325 return 0L;
326 }, () -> {
327 signal.sendTyping(signal.getSelfNumber(), action == TypingAction.STOP);
328 return 0L;
329 }, groupId -> {
330 signal.sendGroupTyping(groupId, action == TypingAction.STOP);
331 return 0L;
332 });
333 }
334
335 @Override
336 public SendMessageResults sendReadReceipt(
337 final RecipientIdentifier.Single sender, final List<Long> messageIds
338 ) {
339 signal.sendReadReceipt(sender.getIdentifier(), messageIds);
340 return new SendMessageResults(0, Map.of());
341 }
342
343 @Override
344 public SendMessageResults sendViewedReceipt(
345 final RecipientIdentifier.Single sender, final List<Long> messageIds
346 ) {
347 signal.sendViewedReceipt(sender.getIdentifier(), messageIds);
348 return new SendMessageResults(0, Map.of());
349 }
350
351 @Override
352 public SendMessageResults sendMessage(
353 final Message message, final Set<RecipientIdentifier> recipients
354 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
355 return handleMessage(recipients,
356 numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
357 () -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
358 groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
359 }
360
361 @Override
362 public SendMessageResults sendRemoteDeleteMessage(
363 final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
364 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
365 return handleMessage(recipients,
366 numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
367 () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
368 groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
369 }
370
371 @Override
372 public SendMessageResults sendMessageReaction(
373 final String emoji,
374 final boolean remove,
375 final RecipientIdentifier.Single targetAuthor,
376 final long targetSentTimestamp,
377 final Set<RecipientIdentifier> recipients,
378 final boolean isStory
379 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
380 return handleMessage(recipients,
381 numbers -> signal.sendMessageReaction(emoji,
382 remove,
383 targetAuthor.getIdentifier(),
384 targetSentTimestamp,
385 numbers),
386 () -> signal.sendMessageReaction(emoji,
387 remove,
388 targetAuthor.getIdentifier(),
389 targetSentTimestamp,
390 signal.getSelfNumber()),
391 groupId -> signal.sendGroupMessageReaction(emoji,
392 remove,
393 targetAuthor.getIdentifier(),
394 targetSentTimestamp,
395 groupId));
396 }
397
398 @Override
399 public SendMessageResults sendPaymentNotificationMessage(
400 final byte[] receipt, final String note, final RecipientIdentifier.Single recipient
401 ) throws IOException {
402 final var timestamp = signal.sendPaymentNotification(receipt, note, recipient.getIdentifier());
403 return new SendMessageResults(timestamp, Map.of());
404 }
405
406 @Override
407 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
408 signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
409 return new SendMessageResults(0, Map.of());
410 }
411
412 @Override
413 public void deleteRecipient(final RecipientIdentifier.Single recipient) {
414 signal.deleteRecipient(recipient.getIdentifier());
415 }
416
417 @Override
418 public void deleteContact(final RecipientIdentifier.Single recipient) {
419 signal.deleteContact(recipient.getIdentifier());
420 }
421
422 @Override
423 public void setContactName(
424 final RecipientIdentifier.Single recipient, final String givenName, final String familyName
425 ) throws NotPrimaryDeviceException {
426 signal.setContactName(recipient.getIdentifier(), givenName);
427 }
428
429 @Override
430 public void setContactsBlocked(
431 final Collection<RecipientIdentifier.Single> recipients, final boolean blocked
432 ) throws NotPrimaryDeviceException, IOException {
433 for (final var recipient : recipients) {
434 signal.setContactBlocked(recipient.getIdentifier(), blocked);
435 }
436 }
437
438 @Override
439 public void setGroupsBlocked(
440 final Collection<GroupId> groupIds, final boolean blocked
441 ) throws GroupNotFoundException, IOException {
442 for (final var groupId : groupIds) {
443 setGroupProperty(groupId, "IsBlocked", blocked);
444 }
445 }
446
447 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
448 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
449 group.Set("org.asamk.Signal.Group", propertyName, blocked);
450 }
451
452 @Override
453 public void setExpirationTimer(
454 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
455 ) throws IOException {
456 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
457 }
458
459 @Override
460 public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
461 try {
462 return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath())));
463 } catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) {
464 throw new AssertionError(e);
465 }
466 }
467
468 @Override
469 public List<StickerPack> getStickerPacks() {
470 throw new UnsupportedOperationException();
471 }
472
473 @Override
474 public void requestAllSyncData() throws IOException {
475 signal.sendSyncRequest();
476 }
477
478 @Override
479 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
480 synchronized (messageHandlers) {
481 if (isWeakListener) {
482 weakHandlers.add(handler);
483 } else {
484 if (messageHandlers.size() == 0) {
485 installMessageHandlers();
486 }
487 messageHandlers.add(handler);
488 }
489 }
490 }
491
492 @Override
493 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
494 synchronized (messageHandlers) {
495 weakHandlers.remove(handler);
496 messageHandlers.remove(handler);
497 if (messageHandlers.size() == 0) {
498 uninstallMessageHandlers();
499 }
500 }
501 }
502
503 @Override
504 public boolean isReceiving() {
505 synchronized (messageHandlers) {
506 return messageHandlers.size() > 0;
507 }
508 }
509
510 @Override
511 public void receiveMessages(
512 Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
513 ) throws IOException {
514 final var remainingMessages = new AtomicInteger(maxMessages.orElse(-1));
515 final var lastMessage = new AtomicLong(System.currentTimeMillis());
516 final var thread = Thread.currentThread();
517
518 final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
519 lastMessage.set(System.currentTimeMillis());
520 handler.handleMessage(envelope, e);
521 if (remainingMessages.get() > 0) {
522 if (remainingMessages.decrementAndGet() <= 0) {
523 remainingMessages.set(0);
524 thread.interrupt();
525 }
526 }
527 };
528 addReceiveHandler(receiveHandler);
529 if (timeout.isPresent()) {
530 while (remainingMessages.get() != 0) {
531 try {
532 final var passedTime = System.currentTimeMillis() - lastMessage.get();
533 final var sleepTimeRemaining = timeout.get().toMillis() - passedTime;
534 if (sleepTimeRemaining < 0) {
535 break;
536 }
537 Thread.sleep(sleepTimeRemaining);
538 } catch (InterruptedException ignored) {
539 }
540 }
541 } else {
542 try {
543 synchronized (this) {
544 this.wait();
545 }
546 } catch (InterruptedException ignored) {
547 }
548 }
549
550 removeReceiveHandler(receiveHandler);
551 }
552
553 @Override
554 public void setReceiveConfig(final ReceiveConfig receiveConfig) {
555 }
556
557 @Override
558 public boolean hasCaughtUpWithOldMessages() {
559 return true;
560 }
561
562 @Override
563 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
564 return signal.isContactBlocked(recipient.getIdentifier());
565 }
566
567 @Override
568 public void sendContacts() throws IOException {
569 signal.sendContacts();
570 }
571
572 @Override
573 public List<Recipient> getRecipients(
574 final boolean onlyContacts,
575 final Optional<Boolean> blocked,
576 final Collection<RecipientIdentifier.Single> addresses,
577 final Optional<String> name
578 ) {
579 final var numbers = addresses.stream()
580 .filter(s -> s instanceof RecipientIdentifier.Number)
581 .map(s -> ((RecipientIdentifier.Number) s).number())
582 .collect(Collectors.toSet());
583 return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
584 final var contactBlocked = signal.isContactBlocked(n);
585 if (blocked.isPresent() && blocked.get() != contactBlocked) {
586 return null;
587 }
588 final var contactName = signal.getContactName(n);
589 if (onlyContacts && contactName.length() == 0) {
590 return null;
591 }
592 if (name.isPresent() && !name.get().equals(contactName)) {
593 return null;
594 }
595 return Recipient.newBuilder()
596 .withAddress(new RecipientAddress(null, n))
597 .withContact(new Contact(contactName, null, null, 0, contactBlocked, false, false))
598 .build();
599 }).filter(Objects::nonNull).toList();
600 }
601
602 @Override
603 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
604 return signal.getContactName(recipient.getIdentifier());
605 }
606
607 @Override
608 public Group getGroup(final GroupId groupId) {
609 final var groupPath = signal.getGroup(groupId.serialize());
610 return getGroup(groupPath);
611 }
612
613 @SuppressWarnings("unchecked")
614 private Group getGroup(final DBusPath groupPath) {
615 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
616 final var id = (byte[]) group.get("Id").getValue();
617 try {
618 return new Group(GroupId.unknownVersion(id),
619 (String) group.get("Name").getValue(),
620 (String) group.get("Description").getValue(),
621 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
622 ((List<String>) group.get("Members").getValue()).stream()
623 .map(m -> new RecipientAddress(null, m))
624 .collect(Collectors.toSet()),
625 ((List<String>) group.get("PendingMembers").getValue()).stream()
626 .map(m -> new RecipientAddress(null, m))
627 .collect(Collectors.toSet()),
628 ((List<String>) group.get("RequestingMembers").getValue()).stream()
629 .map(m -> new RecipientAddress(null, m))
630 .collect(Collectors.toSet()),
631 ((List<String>) group.get("Admins").getValue()).stream()
632 .map(m -> new RecipientAddress(null, m))
633 .collect(Collectors.toSet()),
634 ((List<String>) group.get("Banned").getValue()).stream()
635 .map(m -> new RecipientAddress(null, m))
636 .collect(Collectors.toSet()),
637 (boolean) group.get("IsBlocked").getValue(),
638 (int) group.get("MessageExpirationTimer").getValue(),
639 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
640 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
641 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
642 (boolean) group.get("IsMember").getValue(),
643 (boolean) group.get("IsAdmin").getValue());
644 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
645 throw new AssertionError(e);
646 }
647 }
648
649 @Override
650 public List<Identity> getIdentities() {
651 throw new UnsupportedOperationException();
652 }
653
654 @Override
655 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
656 throw new UnsupportedOperationException();
657 }
658
659 @Override
660 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
661 throw new UnsupportedOperationException();
662 }
663
664 @Override
665 public boolean trustIdentityVerifiedSafetyNumber(
666 final RecipientIdentifier.Single recipient, final String safetyNumber
667 ) {
668 throw new UnsupportedOperationException();
669 }
670
671 @Override
672 public boolean trustIdentityVerifiedSafetyNumber(
673 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
674 ) {
675 throw new UnsupportedOperationException();
676 }
677
678 @Override
679 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
680 throw new UnsupportedOperationException();
681 }
682
683 @Override
684 public void addAddressChangedListener(final Runnable listener) {
685 }
686
687 @Override
688 public void addClosedListener(final Runnable listener) {
689 synchronized (closedListeners) {
690 closedListeners.add(listener);
691 }
692 }
693
694 @Override
695 public void close() {
696 synchronized (this) {
697 this.notify();
698 }
699 synchronized (messageHandlers) {
700 if (messageHandlers.size() > 0) {
701 uninstallMessageHandlers();
702 }
703 weakHandlers.clear();
704 messageHandlers.clear();
705 }
706 synchronized (closedListeners) {
707 closedListeners.forEach(Runnable::run);
708 closedListeners.clear();
709 }
710 }
711
712 private SendMessageResults handleMessage(
713 Set<RecipientIdentifier> recipients,
714 Function<List<String>, Long> recipientsHandler,
715 Supplier<Long> noteToSelfHandler,
716 Function<byte[], Long> groupHandler
717 ) {
718 long timestamp = 0;
719 final var singleRecipients = recipients.stream()
720 .filter(r -> r instanceof RecipientIdentifier.Single)
721 .map(RecipientIdentifier.Single.class::cast)
722 .map(RecipientIdentifier.Single::getIdentifier)
723 .toList();
724 if (singleRecipients.size() > 0) {
725 timestamp = recipientsHandler.apply(singleRecipients);
726 }
727
728 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
729 timestamp = noteToSelfHandler.get();
730 }
731 final var groupRecipients = recipients.stream()
732 .filter(r -> r instanceof RecipientIdentifier.Group)
733 .map(RecipientIdentifier.Group.class::cast)
734 .map(RecipientIdentifier.Group::groupId)
735 .toList();
736 for (final var groupId : groupRecipients) {
737 timestamp = groupHandler.apply(groupId.serialize());
738 }
739 return new SendMessageResults(timestamp, Map.of());
740 }
741
742 private String emptyIfNull(final String string) {
743 return string == null ? "" : string;
744 }
745
746 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
747 try {
748 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
749 } catch (DBusException e) {
750 throw new AssertionError(e);
751 }
752 }
753
754 private void installMessageHandlers() {
755 try {
756 this.dbusMsgHandler = messageReceived -> {
757 final var extras = messageReceived.getExtras();
758 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
759 messageReceived.getSender())),
760 0,
761 messageReceived.getTimestamp(),
762 0,
763 0,
764 false,
765 Optional.empty(),
766 Optional.empty(),
767 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
768 messageReceived.getGroupId().length > 0
769 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
770 messageReceived.getGroupId()), false, 0))
771 : Optional.empty(),
772 Optional.empty(),
773 Optional.empty(),
774 Optional.of(messageReceived.getMessage()),
775 0,
776 false,
777 false,
778 false,
779 false,
780 false,
781 Optional.empty(),
782 Optional.empty(),
783 Optional.empty(),
784 getAttachments(extras),
785 Optional.empty(),
786 Optional.empty(),
787 List.of(),
788 List.of(),
789 List.of(),
790 List.of())),
791 Optional.empty(),
792 Optional.empty(),
793 Optional.empty());
794 notifyMessageHandlers(envelope);
795 };
796 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
797
798 this.dbusRcptHandler = receiptReceived -> {
799 final var type = switch (receiptReceived.getReceiptType()) {
800 case "read" -> MessageEnvelope.Receipt.Type.READ;
801 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
802 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
803 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
804 };
805 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
806 receiptReceived.getSender())),
807 0,
808 receiptReceived.getTimestamp(),
809 0,
810 0,
811 false,
812 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
813 type,
814 List.of(receiptReceived.getTimestamp()))),
815 Optional.empty(),
816 Optional.empty(),
817 Optional.empty(),
818 Optional.empty(),
819 Optional.empty());
820 notifyMessageHandlers(envelope);
821 };
822 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
823
824 this.dbusSyncHandler = syncReceived -> {
825 final var extras = syncReceived.getExtras();
826 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
827 syncReceived.getSource())),
828 0,
829 syncReceived.getTimestamp(),
830 0,
831 0,
832 false,
833 Optional.empty(),
834 Optional.empty(),
835 Optional.empty(),
836 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
837 syncReceived.getTimestamp(),
838 syncReceived.getDestination().isEmpty()
839 ? Optional.empty()
840 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
841 Set.of(),
842 Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
843 syncReceived.getGroupId().length > 0
844 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
845 syncReceived.getGroupId()), false, 0))
846 : Optional.empty(),
847 Optional.empty(),
848 Optional.empty(),
849 Optional.of(syncReceived.getMessage()),
850 0,
851 false,
852 false,
853 false,
854 false,
855 false,
856 Optional.empty(),
857 Optional.empty(),
858 Optional.empty(),
859 getAttachments(extras),
860 Optional.empty(),
861 Optional.empty(),
862 List.of(),
863 List.of(),
864 List.of(),
865 List.of())),
866 Optional.empty())),
867 Optional.empty(),
868 List.of(),
869 List.of(),
870 Optional.empty(),
871 Optional.empty(),
872 Optional.empty(),
873 Optional.empty())),
874 Optional.empty(),
875 Optional.empty());
876 notifyMessageHandlers(envelope);
877 };
878 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
879 } catch (DBusException e) {
880 e.printStackTrace();
881 }
882 signal.subscribeReceive();
883 }
884
885 private void notifyMessageHandlers(final MessageEnvelope envelope) {
886 synchronized (messageHandlers) {
887 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
888 .forEach(h -> h.handleMessage(envelope, null));
889 }
890 }
891
892 private void uninstallMessageHandlers() {
893 try {
894 signal.unsubscribeReceive();
895 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
896 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
897 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
898 } catch (DBusException e) {
899 e.printStackTrace();
900 }
901 }
902
903 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
904 if (!extras.containsKey("attachments")) {
905 return List.of();
906 }
907
908 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
909 return attachments.stream().map(a -> {
910 final String file = a.containsKey("file") ? getValue(a, "file") : null;
911 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
912 ? Optional.of(getValue(a, "remoteId"))
913 : Optional.empty(),
914 file != null ? Optional.of(new File(file)) : Optional.empty(),
915 Optional.empty(),
916 getValue(a, "contentType"),
917 Optional.empty(),
918 Optional.empty(),
919 Optional.empty(),
920 Optional.empty(),
921 Optional.empty(),
922 Optional.empty(),
923 Optional.empty(),
924 getValue(a, "isVoiceNote"),
925 getValue(a, "isGif"),
926 getValue(a, "isBorderless"));
927 }).toList();
928 }
929
930 @Override
931 public InputStream retrieveAttachment(final String id) throws IOException {
932 throw new UnsupportedOperationException();
933 }
934
935 @SuppressWarnings("unchecked")
936 private <T> T getValue(
937 final Map<String, Variant<?>> stringVariantMap, final String field
938 ) {
939 return (T) stringVariantMap.get(field).getValue();
940 }
941 }