]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
bee87d7484b94ad3443bcc8e84ff699b238b99ef
[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 addClosedListener(final Runnable listener) {
626 synchronized (closedListeners) {
627 closedListeners.add(listener);
628 }
629 }
630
631 @Override
632 public void close() {
633 synchronized (this) {
634 this.notify();
635 }
636 synchronized (messageHandlers) {
637 if (messageHandlers.size() > 0) {
638 uninstallMessageHandlers();
639 }
640 weakHandlers.clear();
641 messageHandlers.clear();
642 }
643 synchronized (closedListeners) {
644 closedListeners.forEach(Runnable::run);
645 closedListeners.clear();
646 }
647 }
648
649 private SendMessageResults handleMessage(
650 Set<RecipientIdentifier> recipients,
651 Function<List<String>, Long> recipientsHandler,
652 Supplier<Long> noteToSelfHandler,
653 Function<byte[], Long> groupHandler
654 ) {
655 long timestamp = 0;
656 final var singleRecipients = recipients.stream()
657 .filter(r -> r instanceof RecipientIdentifier.Single)
658 .map(RecipientIdentifier.Single.class::cast)
659 .map(RecipientIdentifier.Single::getIdentifier)
660 .toList();
661 if (singleRecipients.size() > 0) {
662 timestamp = recipientsHandler.apply(singleRecipients);
663 }
664
665 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
666 timestamp = noteToSelfHandler.get();
667 }
668 final var groupRecipients = recipients.stream()
669 .filter(r -> r instanceof RecipientIdentifier.Group)
670 .map(RecipientIdentifier.Group.class::cast)
671 .map(RecipientIdentifier.Group::groupId)
672 .toList();
673 for (final var groupId : groupRecipients) {
674 timestamp = groupHandler.apply(groupId.serialize());
675 }
676 return new SendMessageResults(timestamp, Map.of());
677 }
678
679 private String emptyIfNull(final String string) {
680 return string == null ? "" : string;
681 }
682
683 private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
684 try {
685 return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
686 } catch (DBusException e) {
687 throw new AssertionError(e);
688 }
689 }
690
691 private void installMessageHandlers() {
692 try {
693 this.dbusMsgHandler = messageReceived -> {
694 final var extras = messageReceived.getExtras();
695 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
696 messageReceived.getSender())),
697 0,
698 messageReceived.getTimestamp(),
699 0,
700 0,
701 false,
702 Optional.empty(),
703 Optional.empty(),
704 Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
705 messageReceived.getGroupId().length > 0
706 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
707 messageReceived.getGroupId()), false, 0))
708 : Optional.empty(),
709 Optional.empty(),
710 Optional.of(messageReceived.getMessage()),
711 0,
712 false,
713 false,
714 false,
715 false,
716 Optional.empty(),
717 Optional.empty(),
718 Optional.empty(),
719 getAttachments(extras),
720 Optional.empty(),
721 Optional.empty(),
722 List.of(),
723 List.of(),
724 List.of())),
725 Optional.empty(),
726 Optional.empty());
727 notifyMessageHandlers(envelope);
728 };
729 connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
730
731 this.dbusRcptHandler = receiptReceived -> {
732 final var type = switch (receiptReceived.getReceiptType()) {
733 case "read" -> MessageEnvelope.Receipt.Type.READ;
734 case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
735 case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
736 default -> MessageEnvelope.Receipt.Type.UNKNOWN;
737 };
738 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
739 receiptReceived.getSender())),
740 0,
741 receiptReceived.getTimestamp(),
742 0,
743 0,
744 false,
745 Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
746 type,
747 List.of(receiptReceived.getTimestamp()))),
748 Optional.empty(),
749 Optional.empty(),
750 Optional.empty(),
751 Optional.empty());
752 notifyMessageHandlers(envelope);
753 };
754 connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
755
756 this.dbusSyncHandler = syncReceived -> {
757 final var extras = syncReceived.getExtras();
758 final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
759 syncReceived.getSource())),
760 0,
761 syncReceived.getTimestamp(),
762 0,
763 0,
764 false,
765 Optional.empty(),
766 Optional.empty(),
767 Optional.empty(),
768 Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
769 syncReceived.getTimestamp(),
770 syncReceived.getDestination().isEmpty()
771 ? Optional.empty()
772 : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
773 Set.of(),
774 new MessageEnvelope.Data(syncReceived.getTimestamp(),
775 syncReceived.getGroupId().length > 0
776 ? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
777 syncReceived.getGroupId()), false, 0))
778 : Optional.empty(),
779 Optional.empty(),
780 Optional.of(syncReceived.getMessage()),
781 0,
782 false,
783 false,
784 false,
785 false,
786 Optional.empty(),
787 Optional.empty(),
788 Optional.empty(),
789 getAttachments(extras),
790 Optional.empty(),
791 Optional.empty(),
792 List.of(),
793 List.of(),
794 List.of()))),
795 Optional.empty(),
796 List.of(),
797 List.of(),
798 Optional.empty(),
799 Optional.empty(),
800 Optional.empty(),
801 Optional.empty())),
802 Optional.empty());
803 notifyMessageHandlers(envelope);
804 };
805 connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
806 } catch (DBusException e) {
807 e.printStackTrace();
808 }
809 signal.subscribeReceive();
810 }
811
812 private void notifyMessageHandlers(final MessageEnvelope envelope) {
813 synchronized (messageHandlers) {
814 Stream.concat(messageHandlers.stream(), weakHandlers.stream())
815 .forEach(h -> h.handleMessage(envelope, null));
816 }
817 }
818
819 private void uninstallMessageHandlers() {
820 try {
821 signal.unsubscribeReceive();
822 connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
823 connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
824 connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
825 } catch (DBusException e) {
826 e.printStackTrace();
827 }
828 }
829
830 private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
831 if (!extras.containsKey("attachments")) {
832 return List.of();
833 }
834
835 final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
836 return attachments.stream().map(a -> {
837 final String file = a.containsKey("file") ? getValue(a, "file") : null;
838 return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
839 ? Optional.of(getValue(a, "remoteId"))
840 : Optional.empty(),
841 file != null ? Optional.of(new File(file)) : Optional.empty(),
842 Optional.empty(),
843 getValue(a, "contentType"),
844 Optional.empty(),
845 Optional.empty(),
846 Optional.empty(),
847 Optional.empty(),
848 Optional.empty(),
849 Optional.empty(),
850 Optional.empty(),
851 getValue(a, "isVoiceNote"),
852 getValue(a, "isGif"),
853 getValue(a, "isBorderless"));
854 }).toList();
855 }
856
857 @SuppressWarnings("unchecked")
858 private <T> T getValue(
859 final Map<String, Variant<?>> stringVariantMap, final String field
860 ) {
861 return (T) stringVariantMap.get(field).getValue();
862 }
863 }