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