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