]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Implement socket/tcp for daemon command
[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 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 void sendTypingMessage(
301 final TypingAction action, final Set<RecipientIdentifier> recipients
302 ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
303 for (final var recipient : recipients) {
304 if (recipient instanceof RecipientIdentifier.Single) {
305 signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(),
306 action == TypingAction.STOP);
307 } else if (recipient instanceof RecipientIdentifier.Group) {
308 throw new UnsupportedOperationException();
309 }
310 }
311 }
312
313 @Override
314 public void sendReadReceipt(
315 final RecipientIdentifier.Single sender, final List<Long> messageIds
316 ) throws IOException, UntrustedIdentityException {
317 signal.sendReadReceipt(sender.getIdentifier(), messageIds);
318 }
319
320 @Override
321 public void sendViewedReceipt(
322 final RecipientIdentifier.Single sender, final List<Long> messageIds
323 ) throws IOException, UntrustedIdentityException {
324 signal.sendViewedReceipt(sender.getIdentifier(), messageIds);
325 }
326
327 @Override
328 public SendMessageResults sendMessage(
329 final Message message, final Set<RecipientIdentifier> recipients
330 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
331 return handleMessage(recipients,
332 numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
333 () -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
334 groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
335 }
336
337 @Override
338 public SendMessageResults sendRemoteDeleteMessage(
339 final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
340 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
341 return handleMessage(recipients,
342 numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
343 () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
344 groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
345 }
346
347 @Override
348 public SendMessageResults sendMessageReaction(
349 final String emoji,
350 final boolean remove,
351 final RecipientIdentifier.Single targetAuthor,
352 final long targetSentTimestamp,
353 final Set<RecipientIdentifier> recipients
354 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
355 return handleMessage(recipients,
356 numbers -> signal.sendMessageReaction(emoji,
357 remove,
358 targetAuthor.getIdentifier(),
359 targetSentTimestamp,
360 numbers),
361 () -> signal.sendMessageReaction(emoji,
362 remove,
363 targetAuthor.getIdentifier(),
364 targetSentTimestamp,
365 signal.getSelfNumber()),
366 groupId -> signal.sendGroupMessageReaction(emoji,
367 remove,
368 targetAuthor.getIdentifier(),
369 targetSentTimestamp,
370 groupId));
371 }
372
373 @Override
374 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
375 signal.sendEndSessionMessage(recipients.stream()
376 .map(RecipientIdentifier.Single::getIdentifier)
377 .collect(Collectors.toList()));
378 return new SendMessageResults(0, Map.of());
379 }
380
381 @Override
382 public void setContactName(
383 final RecipientIdentifier.Single recipient, final String name
384 ) throws NotMasterDeviceException {
385 signal.setContactName(recipient.getIdentifier(), name);
386 }
387
388 @Override
389 public void setContactBlocked(
390 final RecipientIdentifier.Single recipient, final boolean blocked
391 ) throws NotMasterDeviceException, IOException {
392 signal.setContactBlocked(recipient.getIdentifier(), blocked);
393 }
394
395 @Override
396 public void setGroupBlocked(
397 final GroupId groupId, final boolean blocked
398 ) throws GroupNotFoundException, IOException {
399 setGroupProperty(groupId, "IsBlocked", blocked);
400 }
401
402 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
403 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
404 group.Set("org.asamk.Signal.Group", propertyName, blocked);
405 }
406
407 @Override
408 public void setExpirationTimer(
409 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
410 ) throws IOException {
411 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
412 }
413
414 @Override
415 public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
416 try {
417 return new URI(signal.uploadStickerPack(path.getPath()));
418 } catch (URISyntaxException e) {
419 throw new AssertionError(e);
420 }
421 }
422
423 @Override
424 public void requestAllSyncData() throws IOException {
425 signal.sendSyncRequest();
426 }
427
428 @Override
429 public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
430 synchronized (messageHandlers) {
431 if (isWeakListener) {
432 weakHandlers.add(handler);
433 } else {
434 if (messageHandlers.size() == 0) {
435 installMessageHandlers();
436 }
437 messageHandlers.add(handler);
438 }
439 }
440 }
441
442 @Override
443 public void removeReceiveHandler(final ReceiveMessageHandler handler) {
444 synchronized (messageHandlers) {
445 weakHandlers.remove(handler);
446 messageHandlers.remove(handler);
447 if (messageHandlers.size() == 0) {
448 uninstallMessageHandlers();
449 }
450 }
451 }
452
453 @Override
454 public boolean isReceiving() {
455 synchronized (messageHandlers) {
456 return messageHandlers.size() > 0;
457 }
458 }
459
460 @Override
461 public void receiveMessages(final ReceiveMessageHandler handler) throws IOException {
462 addReceiveHandler(handler);
463 try {
464 synchronized (this) {
465 this.wait();
466 }
467 } catch (InterruptedException ignored) {
468 }
469 removeReceiveHandler(handler);
470 }
471
472 @Override
473 public void receiveMessages(
474 final long timeout, final TimeUnit unit, final ReceiveMessageHandler handler
475 ) throws IOException {
476 addReceiveHandler(handler);
477 try {
478 Thread.sleep(unit.toMillis(timeout));
479 } catch (InterruptedException ignored) {
480 }
481 removeReceiveHandler(handler);
482 }
483
484 @Override
485 public void setIgnoreAttachments(final boolean ignoreAttachments) {
486 }
487
488 @Override
489 public boolean hasCaughtUpWithOldMessages() {
490 return true;
491 }
492
493 @Override
494 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
495 return signal.isContactBlocked(recipient.getIdentifier());
496 }
497
498 @Override
499 public void sendContacts() throws IOException {
500 signal.sendContacts();
501 }
502
503 @Override
504 public List<Pair<RecipientAddress, Contact>> getContacts() {
505 throw new UnsupportedOperationException();
506 }
507
508 @Override
509 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
510 return signal.getContactName(recipient.getIdentifier());
511 }
512
513 @Override
514 public Group getGroup(final GroupId groupId) {
515 final var groupPath = signal.getGroup(groupId.serialize());
516 return getGroup(groupPath);
517 }
518
519 @SuppressWarnings("unchecked")
520 private Group getGroup(final DBusPath groupPath) {
521 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
522 final var id = (byte[]) group.get("Id").getValue();
523 try {
524 return new Group(GroupId.unknownVersion(id),
525 (String) group.get("Name").getValue(),
526 (String) group.get("Description").getValue(),
527 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
528 ((List<String>) group.get("Members").getValue()).stream()
529 .map(m -> new RecipientAddress(null, m))
530 .collect(Collectors.toSet()),
531 ((List<String>) group.get("PendingMembers").getValue()).stream()
532 .map(m -> new RecipientAddress(null, m))
533 .collect(Collectors.toSet()),
534 ((List<String>) group.get("RequestingMembers").getValue()).stream()
535 .map(m -> new RecipientAddress(null, m))
536 .collect(Collectors.toSet()),
537 ((List<String>) group.get("Admins").getValue()).stream()
538 .map(m -> new RecipientAddress(null, m))
539 .collect(Collectors.toSet()),
540 (boolean) group.get("IsBlocked").getValue(),
541 (int) group.get("MessageExpirationTimer").getValue(),
542 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
543 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
544 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
545 (boolean) group.get("IsMember").getValue(),
546 (boolean) group.get("IsAdmin").getValue());
547 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
548 throw new AssertionError(e);
549 }
550 }
551
552 @Override
553 public List<Identity> getIdentities() {
554 throw new UnsupportedOperationException();
555 }
556
557 @Override
558 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
559 throw new UnsupportedOperationException();
560 }
561
562 @Override
563 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
564 throw new UnsupportedOperationException();
565 }
566
567 @Override
568 public boolean trustIdentityVerifiedSafetyNumber(
569 final RecipientIdentifier.Single recipient, final String safetyNumber
570 ) {
571 throw new UnsupportedOperationException();
572 }
573
574 @Override
575 public boolean trustIdentityVerifiedSafetyNumber(
576 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
577 ) {
578 throw new UnsupportedOperationException();
579 }
580
581 @Override
582 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
583 throw new UnsupportedOperationException();
584 }
585
586 @Override
587 public void close() throws IOException {
588 synchronized (this) {
589 this.notify();
590 }
591 synchronized (messageHandlers) {
592 if (messageHandlers.size() > 0) {
593 uninstallMessageHandlers();
594 }
595 weakHandlers.clear();
596 messageHandlers.clear();
597 }
598 }
599
600 private SendMessageResults handleMessage(
601 Set<RecipientIdentifier> recipients,
602 Function<List<String>, Long> recipientsHandler,
603 Supplier<Long> noteToSelfHandler,
604 Function<byte[], Long> groupHandler
605 ) {
606 long timestamp = 0;
607 final var singleRecipients = recipients.stream()
608 .filter(r -> r instanceof RecipientIdentifier.Single)
609 .map(RecipientIdentifier.Single.class::cast)
610 .map(RecipientIdentifier.Single::getIdentifier)
611 .collect(Collectors.toList());
612 if (singleRecipients.size() > 0) {
613 timestamp = recipientsHandler.apply(singleRecipients);
614 }
615
616 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
617 timestamp = noteToSelfHandler.get();
618 }
619 final var groupRecipients = recipients.stream()
620 .filter(r -> r instanceof RecipientIdentifier.Group)
621 .map(RecipientIdentifier.Group.class::cast)
622 .map(RecipientIdentifier.Group::groupId)
623 .collect(Collectors.toList());
624 for (final var groupId : groupRecipients) {
625 timestamp = groupHandler.apply(groupId.serialize());
626 }
627 return new SendMessageResults(timestamp, Map.of());
628 }
629
630 private String emptyIfNull(final String string) {
631 return string == null ? "" : string;
632 }
633
634 private <T extends DBusInterface> T getRemoteObject(final DBusPath devicePath, final Class<T> type) {
635 try {
636 return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type);
637 } catch (DBusException e) {
638 throw new AssertionError(e);
639 }
640 }
641
642 private void installMessageHandlers() {
643 try {
644 this.dbusMsgHandler = messageReceived -> {
645 final var extras = messageReceived.getExtras();
646 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
647 messageReceived.getSender())),
648 0,
649 messageReceived.getTimestamp(),
650 0,
651 0,
652 false,
653 Optional.empty(),
654 Optional.empty(),
655 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
656 messageReceived.getGroupId().length > 0
657 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
658 messageReceived.getGroupId()), false, 0))
659 : Optional.empty(),
660 Optional.empty(),
661 Optional.of(messageReceived.getMessage()),
662 0,
663 false,
664 false,
665 false,
666 false,
667 Optional.empty(),
668 Optional.empty(),
669 getAttachments(extras),
670 Optional.empty(),
671 Optional.empty(),
672 List.of(),
673 List.of(),
674 List.of())),
675 Optional.empty(),
676 Optional.empty());
677 notifyMessageHandlers(envelope);
678 };
679 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
680
681 this.dbusRcptHandler = receiptReceived -> {
682 final var type = switch (receiptReceived.getReceiptType()) {
683 case "read" -> MessageEnvelope.Receipt.Type.READ;
684 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
685 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
686 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
687 };
688 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
689 receiptReceived.getSender())),
690 0,
691 receiptReceived.getTimestamp(),
692 0,
693 0,
694 false,
695 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
696 type,
697 List.of(receiptReceived.getTimestamp()))),
698 Optional.empty(),
699 Optional.empty(),
700 Optional.empty(),
701 Optional.empty());
702 notifyMessageHandlers(envelope);
703 };
704 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
705
706 this.dbusSyncHandler = syncReceived -> {
707 final var extras = syncReceived.getExtras();
708 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
709 syncReceived.getSource())),
710 0,
711 syncReceived.getTimestamp(),
712 0,
713 0,
714 false,
715 Optional.empty(),
716 Optional.empty(),
717 Optional.empty(),
718 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
719 syncReceived.getTimestamp(),
720 syncReceived.getDestination().isEmpty()
721 ? Optional.empty()
722 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
723 Set.of(),
724 new MessageEnvelope.Data(syncReceived.getTimestamp(),
725 syncReceived.getGroupId().length > 0
726 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
727 syncReceived.getGroupId()), false, 0))
728 : Optional.empty(),
729 Optional.empty(),
730 Optional.of(syncReceived.getMessage()),
731 0,
732 false,
733 false,
734 false,
735 false,
736 Optional.empty(),
737 Optional.empty(),
738 getAttachments(extras),
739 Optional.empty(),
740 Optional.empty(),
741 List.of(),
742 List.of(),
743 List.of()))),
744 Optional.empty(),
745 List.of(),
746 List.of(),
747 Optional.empty(),
748 Optional.empty(),
749 Optional.empty(),
750 Optional.empty())),
751 Optional.empty());
752 notifyMessageHandlers(envelope);
753 };
754 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
755 } catch (DBusException e) {
756 e.printStackTrace();
757 }
758 signal.subscribeReceive();
759 }
760
761 private void notifyMessageHandlers(final MessageEnvelope envelope) {
762 synchronized (messageHandlers) {
763 Stream.concat(messageHandlers.stream(), weakHandlers.stream()).forEach(h -> {
764 h.handleMessage(envelope, null);
765 });
766 }
767 }
768
769 private void uninstallMessageHandlers() {
770 try {
771 signal.unsubscribeReceive();
772 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
773 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
774 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
775 } catch (DBusException e) {
776 e.printStackTrace();
777 }
778 }
779
780 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
781 if (!extras.containsKey("attachments")) {
782 return List.of();
783 }
784
785 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
786 return attachments.stream().map(a -> {
787 final String file = a.containsKey("file") ? getValue(a, "file") : null;
788 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
789 ? Optional.of(getValue(a, "remoteId"))
790 : Optional.empty(),
791 file != null ? Optional.of(new File(file)) : Optional.empty(),
792 Optional.empty(),
793 getValue(a, "contentType"),
794 Optional.empty(),
795 Optional.empty(),
796 Optional.empty(),
797 Optional.empty(),
798 Optional.empty(),
799 Optional.empty(),
800 Optional.empty(),
801 getValue(a, "isVoiceNote"),
802 getValue(a, "isGif"),
803 getValue(a, "isBorderless"));
804 }).collect(Collectors.toList());
805 }
806
807 @SuppressWarnings("unchecked")
808 private <T> T getValue(
809 final Map<String, Variant<?>> stringVariantMap, final String field
810 ) {
811 return (T) stringVariantMap.get(field).getValue();
812 }
813 }