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