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