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