]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
a8f071f7a6723c4510944a12b4490c1ab305cc62
[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.UntrustedIdentityException;
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.Message;
14 import org.asamk.signal.manager.api.RecipientIdentifier;
15 import org.asamk.signal.manager.api.SendGroupMessageResults;
16 import org.asamk.signal.manager.api.SendMessageResults;
17 import org.asamk.signal.manager.api.TypingAction;
18 import org.asamk.signal.manager.api.UpdateGroup;
19 import org.asamk.signal.manager.groups.GroupId;
20 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
21 import org.asamk.signal.manager.groups.GroupNotFoundException;
22 import org.asamk.signal.manager.groups.GroupPermission;
23 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
24 import org.asamk.signal.manager.groups.LastGroupAdminException;
25 import org.asamk.signal.manager.groups.NotAGroupMemberException;
26 import org.asamk.signal.manager.storage.recipients.Contact;
27 import org.asamk.signal.manager.storage.recipients.Profile;
28 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
29 import org.freedesktop.dbus.DBusPath;
30 import org.freedesktop.dbus.connections.impl.DBusConnection;
31 import org.freedesktop.dbus.exceptions.DBusException;
32 import org.freedesktop.dbus.interfaces.DBusInterface;
33 import org.whispersystems.libsignal.InvalidKeyException;
34 import org.whispersystems.libsignal.util.Pair;
35 import org.whispersystems.libsignal.util.guava.Optional;
36 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
37 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
38 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
39 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
40 import org.whispersystems.signalservice.api.util.UuidUtil;
41 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
42
43 import java.io.File;
44 import java.io.IOException;
45 import java.net.URI;
46 import java.net.URISyntaxException;
47 import java.util.ArrayList;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Set;
52 import java.util.UUID;
53 import java.util.concurrent.TimeUnit;
54 import java.util.function.Function;
55 import java.util.function.Supplier;
56 import java.util.stream.Collectors;
57
58 /**
59 * This class implements the Manager interface using the DBus Signal interface, where possible.
60 * It's used for the signal-cli dbus client mode (--dbus, --dbus-system)
61 */
62 public class DbusManagerImpl implements Manager {
63
64 private final Signal signal;
65 private final DBusConnection connection;
66
67 public DbusManagerImpl(final Signal signal, DBusConnection connection) {
68 this.signal = signal;
69 this.connection = connection;
70 }
71
72 @Override
73 public String getSelfNumber() {
74 return signal.getSelfNumber();
75 }
76
77 @Override
78 public void checkAccountState() throws IOException {
79 throw new UnsupportedOperationException();
80 }
81
82 @Override
83 public Map<String, Pair<String, UUID>> areUsersRegistered(final Set<String> numbers) throws IOException {
84 final var numbersList = new ArrayList<>(numbers);
85 final var registered = signal.isRegistered(numbersList);
86
87 final var result = new HashMap<String, Pair<String, UUID>>();
88 for (var i = 0; i < numbersList.size(); i++) {
89 result.put(numbersList.get(i),
90 new Pair<>(numbersList.get(i), registered.get(i) ? UuidUtil.UNKNOWN_UUID : null));
91 }
92 return result;
93 }
94
95 @Override
96 public void updateAccountAttributes(final String deviceName) throws IOException {
97 if (deviceName != null) {
98 final var devicePath = signal.getThisDevice();
99 getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName);
100 }
101 }
102
103 @Override
104 public void updateConfiguration(
105 final Boolean readReceipts,
106 final Boolean unidentifiedDeliveryIndicators,
107 final Boolean typingIndicators,
108 final Boolean linkPreviews
109 ) throws IOException {
110 signal.setConfiguration(
111 readReceipts,
112 unidentifiedDeliveryIndicators,
113 typingIndicators,
114 linkPreviews
115 );
116 }
117
118 @Override
119 public List<Boolean> getConfiguration() {
120 return signal.getConfiguration();
121 }
122
123 @Override
124 public void setProfile(
125 final String givenName,
126 final String familyName,
127 final String about,
128 final String aboutEmoji,
129 final Optional<File> avatar
130 ) throws IOException {
131 signal.updateProfile(emptyIfNull(givenName),
132 emptyIfNull(familyName),
133 emptyIfNull(about),
134 emptyIfNull(aboutEmoji),
135 avatar == null ? "" : avatar.transform(File::getPath).or(""),
136 avatar != null && !avatar.isPresent());
137 }
138
139 @Override
140 public void unregister() throws IOException {
141 throw new UnsupportedOperationException();
142 }
143
144 @Override
145 public void deleteAccount() throws IOException {
146 throw new UnsupportedOperationException();
147 }
148
149 @Override
150 public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
151 throw new UnsupportedOperationException();
152 }
153
154 @Override
155 public List<Device> getLinkedDevices() throws IOException {
156 final var thisDevice = signal.getThisDevice();
157 return signal.listDevices().stream().map(d -> {
158 final var device = getRemoteObject(d.getObjectPath(),
159 Signal.Device.class).GetAll("org.asamk.Signal.Device");
160 return new Device((long) device.get("Id").getValue(),
161 (String) device.get("Name").getValue(),
162 (long) device.get("Created").getValue(),
163 (long) device.get("LastSeen").getValue(),
164 thisDevice.equals(d.getObjectPath()));
165 }).collect(Collectors.toList());
166 }
167
168 @Override
169 public void removeLinkedDevices(final long deviceId) throws IOException {
170 final var devicePath = signal.getDevice(deviceId);
171 getRemoteObject(devicePath, Signal.Device.class).removeDevice();
172 }
173
174 @Override
175 public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException {
176 signal.addDevice(linkUri.toString());
177 }
178
179 @Override
180 public void setRegistrationLockPin(final Optional<String> pin) throws IOException, UnauthenticatedResponseException {
181 if (pin.isPresent()) {
182 signal.setPin(pin.get());
183 } else {
184 signal.removePin();
185 }
186 }
187
188 @Override
189 public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
190 throw new UnsupportedOperationException();
191 }
192
193 @Override
194 public List<Group> getGroups() {
195 final var groups = signal.listGroups();
196 return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList());
197 }
198
199 @Override
200 public SendGroupMessageResults quitGroup(
201 final GroupId groupId, final Set<RecipientIdentifier.Single> groupAdmins
202 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
203 if (groupAdmins.size() > 0) {
204 throw new UnsupportedOperationException();
205 }
206 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
207 group.quitGroup();
208 return new SendGroupMessageResults(0, List.of());
209 }
210
211 @Override
212 public void deleteGroup(final GroupId groupId) throws IOException {
213 throw new UnsupportedOperationException();
214 }
215
216 @Override
217 public Pair<GroupId, SendGroupMessageResults> createGroup(
218 final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
219 ) throws IOException, AttachmentInvalidException {
220 final var newGroupId = signal.createGroup(emptyIfNull(name),
221 members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
222 avatarFile == null ? "" : avatarFile.getPath());
223 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
224 }
225
226 @Override
227 public SendGroupMessageResults updateGroup(
228 final GroupId groupId, final UpdateGroup updateGroup
229 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
230 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
231 if (updateGroup.getName() != null) {
232 group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
233 }
234 if (updateGroup.getDescription() != null) {
235 group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
236 }
237 if (updateGroup.getAvatarFile() != null) {
238 group.Set("org.asamk.Signal.Group",
239 "Avatar",
240 updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath());
241 }
242 if (updateGroup.getExpirationTimer() != null) {
243 group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
244 }
245 if (updateGroup.getAddMemberPermission() != null) {
246 group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
247 }
248 if (updateGroup.getEditDetailsPermission() != null) {
249 group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
250 }
251 if (updateGroup.getIsAnnouncementGroup() != null) {
252 group.Set("org.asamk.Signal.Group",
253 "PermissionSendMessage",
254 updateGroup.getIsAnnouncementGroup()
255 ? GroupPermission.ONLY_ADMINS.name()
256 : GroupPermission.EVERY_MEMBER.name());
257 }
258 if (updateGroup.getMembers() != null) {
259 group.addMembers(updateGroup.getMembers()
260 .stream()
261 .map(RecipientIdentifier.Single::getIdentifier)
262 .collect(Collectors.toList()));
263 }
264 if (updateGroup.getRemoveMembers() != null) {
265 group.removeMembers(updateGroup.getRemoveMembers()
266 .stream()
267 .map(RecipientIdentifier.Single::getIdentifier)
268 .collect(Collectors.toList()));
269 }
270 if (updateGroup.getAdmins() != null) {
271 group.addAdmins(updateGroup.getAdmins()
272 .stream()
273 .map(RecipientIdentifier.Single::getIdentifier)
274 .collect(Collectors.toList()));
275 }
276 if (updateGroup.getRemoveAdmins() != null) {
277 group.removeAdmins(updateGroup.getRemoveAdmins()
278 .stream()
279 .map(RecipientIdentifier.Single::getIdentifier)
280 .collect(Collectors.toList()));
281 }
282 if (updateGroup.isResetGroupLink()) {
283 group.resetLink();
284 }
285 if (updateGroup.getGroupLinkState() != null) {
286 switch (updateGroup.getGroupLinkState()) {
287 case DISABLED:
288 group.disableLink();
289 break;
290 case ENABLED:
291 group.enableLink(false);
292 break;
293 case ENABLED_WITH_APPROVAL:
294 group.enableLink(true);
295 break;
296 }
297 }
298 return new SendGroupMessageResults(0, List.of());
299 }
300
301 @Override
302 public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException {
303 final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
304 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
305 }
306
307 @Override
308 public void sendTypingMessage(
309 final TypingAction action, final Set<RecipientIdentifier> recipients
310 ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
311 for (final var recipient : recipients) {
312 if (recipient instanceof RecipientIdentifier.Single) {
313 signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(),
314 action == TypingAction.STOP);
315 } else if (recipient instanceof RecipientIdentifier.Group) {
316 throw new UnsupportedOperationException();
317 }
318 }
319 }
320
321 @Override
322 public void sendReadReceipt(
323 final RecipientIdentifier.Single sender, final List<Long> messageIds
324 ) throws IOException, UntrustedIdentityException {
325 signal.sendReadReceipt(sender.getIdentifier(), messageIds);
326 }
327
328 @Override
329 public void sendViewedReceipt(
330 final RecipientIdentifier.Single sender, final List<Long> messageIds
331 ) throws IOException, UntrustedIdentityException {
332 throw new UnsupportedOperationException();
333 }
334
335 @Override
336 public SendMessageResults sendMessage(
337 final Message message, final Set<RecipientIdentifier> recipients
338 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
339 return handleMessage(recipients,
340 numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers),
341 () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()),
342 groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId));
343 }
344
345 @Override
346 public SendMessageResults sendRemoteDeleteMessage(
347 final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
348 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
349 return handleMessage(recipients,
350 numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
351 () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
352 groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
353 }
354
355 @Override
356 public SendMessageResults sendMessageReaction(
357 final String emoji,
358 final boolean remove,
359 final RecipientIdentifier.Single targetAuthor,
360 final long targetSentTimestamp,
361 final Set<RecipientIdentifier> recipients
362 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
363 return handleMessage(recipients,
364 numbers -> signal.sendMessageReaction(emoji,
365 remove,
366 targetAuthor.getIdentifier(),
367 targetSentTimestamp,
368 numbers),
369 () -> signal.sendMessageReaction(emoji,
370 remove,
371 targetAuthor.getIdentifier(),
372 targetSentTimestamp,
373 signal.getSelfNumber()),
374 groupId -> signal.sendGroupMessageReaction(emoji,
375 remove,
376 targetAuthor.getIdentifier(),
377 targetSentTimestamp,
378 groupId));
379 }
380
381 @Override
382 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
383 signal.sendEndSessionMessage(recipients.stream()
384 .map(RecipientIdentifier.Single::getIdentifier)
385 .collect(Collectors.toList()));
386 return new SendMessageResults(0, Map.of());
387 }
388
389 @Override
390 public void setContactName(
391 final RecipientIdentifier.Single recipient, final String name
392 ) throws NotMasterDeviceException, UnregisteredUserException {
393 signal.setContactName(recipient.getIdentifier(), name);
394 }
395
396 @Override
397 public void setContactBlocked(
398 final RecipientIdentifier.Single recipient, final boolean blocked
399 ) throws NotMasterDeviceException, IOException {
400 signal.setContactBlocked(recipient.getIdentifier(), blocked);
401 }
402
403 @Override
404 public void setGroupBlocked(
405 final GroupId groupId, final boolean blocked
406 ) throws GroupNotFoundException, IOException {
407 setGroupProperty(groupId, "IsBlocked", blocked);
408 }
409
410 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
411 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
412 group.Set("org.asamk.Signal.Group", propertyName, blocked);
413 }
414
415 @Override
416 public void setExpirationTimer(
417 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
418 ) throws IOException {
419 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
420 }
421
422 @Override
423 public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
424 try {
425 return new URI(signal.uploadStickerPack(path.getPath()));
426 } catch (URISyntaxException e) {
427 throw new AssertionError(e);
428 }
429 }
430
431 @Override
432 public void requestAllSyncData() throws IOException {
433 signal.sendSyncRequest();
434 }
435
436 @Override
437 public void receiveMessages(
438 final long timeout,
439 final TimeUnit unit,
440 final boolean returnOnTimeout,
441 final boolean ignoreAttachments,
442 final ReceiveMessageHandler handler
443 ) throws IOException {
444 throw new UnsupportedOperationException();
445 }
446
447 @Override
448 public boolean hasCaughtUpWithOldMessages() {
449 throw new UnsupportedOperationException();
450 }
451
452 @Override
453 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
454 return signal.isContactBlocked(recipient.getIdentifier());
455 }
456
457 @Override
458 public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) {
459 throw new UnsupportedOperationException();
460 }
461
462 @Override
463 public void sendContacts() throws IOException {
464 signal.sendContacts();
465 }
466
467 @Override
468 public List<Pair<RecipientAddress, Contact>> getContacts() {
469 throw new UnsupportedOperationException();
470 }
471
472 @Override
473 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
474 return signal.getContactName(recipient.getIdentifier());
475 }
476
477 @Override
478 public Group getGroup(final GroupId groupId) {
479 final var groupPath = signal.getGroup(groupId.serialize());
480 return getGroup(groupPath);
481 }
482
483 @SuppressWarnings("unchecked")
484 private Group getGroup(final DBusPath groupPath) {
485 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
486 final var id = (byte[]) group.get("Id").getValue();
487 try {
488 return new Group(GroupId.unknownVersion(id),
489 (String) group.get("Name").getValue(),
490 (String) group.get("Description").getValue(),
491 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
492 ((List<String>) group.get("Members").getValue()).stream()
493 .map(m -> new RecipientAddress(null, m))
494 .collect(Collectors.toSet()),
495 ((List<String>) group.get("PendingMembers").getValue()).stream()
496 .map(m -> new RecipientAddress(null, m))
497 .collect(Collectors.toSet()),
498 ((List<String>) group.get("RequestingMembers").getValue()).stream()
499 .map(m -> new RecipientAddress(null, m))
500 .collect(Collectors.toSet()),
501 ((List<String>) group.get("Admins").getValue()).stream()
502 .map(m -> new RecipientAddress(null, m))
503 .collect(Collectors.toSet()),
504 (boolean) group.get("IsBlocked").getValue(),
505 (int) group.get("MessageExpirationTimer").getValue(),
506 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
507 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
508 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
509 (boolean) group.get("IsMember").getValue(),
510 (boolean) group.get("IsAdmin").getValue());
511 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
512 throw new AssertionError(e);
513 }
514 }
515
516 @Override
517 public List<Identity> getIdentities() {
518 throw new UnsupportedOperationException();
519 }
520
521 @Override
522 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
523 throw new UnsupportedOperationException();
524 }
525
526 @Override
527 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
528 throw new UnsupportedOperationException();
529 }
530
531 @Override
532 public boolean trustIdentityVerifiedSafetyNumber(
533 final RecipientIdentifier.Single recipient, final String safetyNumber
534 ) {
535 throw new UnsupportedOperationException();
536 }
537
538 @Override
539 public boolean trustIdentityVerifiedSafetyNumber(
540 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
541 ) {
542 throw new UnsupportedOperationException();
543 }
544
545 @Override
546 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
547 throw new UnsupportedOperationException();
548 }
549
550 @Override
551 public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
552 return address;
553 }
554
555 @Override
556 public void close() throws IOException {
557 }
558
559 private SendMessageResults handleMessage(
560 Set<RecipientIdentifier> recipients,
561 Function<List<String>, Long> recipientsHandler,
562 Supplier<Long> noteToSelfHandler,
563 Function<byte[], Long> groupHandler
564 ) {
565 long timestamp = 0;
566 final var singleRecipients = recipients.stream()
567 .filter(r -> r instanceof RecipientIdentifier.Single)
568 .map(RecipientIdentifier.Single.class::cast)
569 .map(RecipientIdentifier.Single::getIdentifier)
570 .collect(Collectors.toList());
571 if (singleRecipients.size() > 0) {
572 timestamp = recipientsHandler.apply(singleRecipients);
573 }
574
575 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
576 timestamp = noteToSelfHandler.get();
577 }
578 final var groupRecipients = recipients.stream()
579 .filter(r -> r instanceof RecipientIdentifier.Group)
580 .map(RecipientIdentifier.Group.class::cast)
581 .map(g -> g.groupId)
582 .collect(Collectors.toList());
583 for (final var groupId : groupRecipients) {
584 timestamp = groupHandler.apply(groupId.serialize());
585 }
586 return new SendMessageResults(timestamp, Map.of());
587 }
588
589 private String emptyIfNull(final String string) {
590 return string == null ? "" : string;
591 }
592
593 private <T extends DBusInterface> T getRemoteObject(final DBusPath devicePath, final Class<T> type) {
594 try {
595 return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type);
596 } catch (DBusException e) {
597 throw new AssertionError(e);
598 }
599 }
600 }