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