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