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