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