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