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