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