]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Implement Dbus setPin and removePin (#733)
[signal-cli] / src / main / java / org / asamk / signal / dbus / DbusSignalImpl.java
1 package org.asamk.signal.dbus;
2
3 import org.asamk.Signal;
4 import org.asamk.signal.BaseConfig;
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.UntrustedIdentityException;
9 import org.asamk.signal.manager.api.Message;
10 import org.asamk.signal.manager.api.RecipientIdentifier;
11 import org.asamk.signal.manager.api.TypingAction;
12 import org.asamk.signal.manager.groups.GroupId;
13 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
14 import org.asamk.signal.manager.groups.GroupNotFoundException;
15 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
16 import org.asamk.signal.manager.groups.LastGroupAdminException;
17 import org.asamk.signal.manager.groups.NotAGroupMemberException;
18 import org.asamk.signal.manager.storage.identities.IdentityInfo;
19 import org.asamk.signal.util.ErrorUtils;
20 import org.asamk.signal.util.Util;
21
22 import org.freedesktop.dbus.exceptions.DBusExecutionException;
23 import org.whispersystems.libsignal.util.Pair;
24 import org.whispersystems.libsignal.util.guava.Optional;
25 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
26 import org.whispersystems.signalservice.api.messages.SendMessageResult;
27 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
28 import org.whispersystems.signalservice.api.util.InvalidNumberException;
29 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
30
31 import java.io.File;
32 import java.io.IOException;
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Objects;
39 import java.util.Set;
40 import java.util.stream.Collectors;
41 import java.util.stream.Stream;
42
43 import static org.asamk.signal.util.Util.getLegacyIdentifier;
44
45 public class DbusSignalImpl implements Signal {
46
47 private final Manager m;
48 private final String objectPath;
49
50 public DbusSignalImpl(final Manager m, final String objectPath) {
51 this.m = m;
52 this.objectPath = objectPath;
53 }
54
55 @Override
56 public boolean isRemote() {
57 return false;
58 }
59
60 @Override
61 public String getObjectPath() {
62 return objectPath;
63 }
64
65 @Override
66 public long sendMessage(final String message, final List<String> attachments, final String recipient) {
67 var recipients = new ArrayList<String>(1);
68 recipients.add(recipient);
69 return sendMessage(message, attachments, recipients);
70 }
71
72 @Override
73 public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
74 try {
75 final var results = m.sendMessage(new Message(message, attachments),
76 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
77 .map(RecipientIdentifier.class::cast)
78 .collect(Collectors.toSet()));
79
80 checkSendMessageResults(results.getTimestamp(), results.getResults());
81 return results.getTimestamp();
82 } catch (AttachmentInvalidException e) {
83 throw new Error.AttachmentInvalid(e.getMessage());
84 } catch (IOException e) {
85 throw new Error.Failure(e.getMessage());
86 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
87 throw new Error.GroupNotFound(e.getMessage());
88 }
89 }
90
91 @Override
92 public long sendRemoteDeleteMessage(
93 final long targetSentTimestamp, final String recipient
94 ) {
95 var recipients = new ArrayList<String>(1);
96 recipients.add(recipient);
97 return sendRemoteDeleteMessage(targetSentTimestamp, recipients);
98 }
99
100 @Override
101 public long sendRemoteDeleteMessage(
102 final long targetSentTimestamp, final List<String> recipients
103 ) {
104 try {
105 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
106 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
107 .map(RecipientIdentifier.class::cast)
108 .collect(Collectors.toSet()));
109 checkSendMessageResults(results.getTimestamp(), results.getResults());
110 return results.getTimestamp();
111 } catch (IOException e) {
112 throw new Error.Failure(e.getMessage());
113 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
114 throw new Error.GroupNotFound(e.getMessage());
115 }
116 }
117
118 @Override
119 public long sendGroupRemoteDeleteMessage(
120 final long targetSentTimestamp, final byte[] groupId
121 ) {
122 try {
123 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
124 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
125 checkSendMessageResults(results.getTimestamp(), results.getResults());
126 return results.getTimestamp();
127 } catch (IOException e) {
128 throw new Error.Failure(e.getMessage());
129 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
130 throw new Error.GroupNotFound(e.getMessage());
131 }
132 }
133
134 @Override
135 public long sendMessageReaction(
136 final String emoji,
137 final boolean remove,
138 final String targetAuthor,
139 final long targetSentTimestamp,
140 final String recipient
141 ) {
142 var recipients = new ArrayList<String>(1);
143 recipients.add(recipient);
144 return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
145 }
146
147 @Override
148 public long sendMessageReaction(
149 final String emoji,
150 final boolean remove,
151 final String targetAuthor,
152 final long targetSentTimestamp,
153 final List<String> recipients
154 ) {
155 try {
156 final var results = m.sendMessageReaction(emoji,
157 remove,
158 getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
159 targetSentTimestamp,
160 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
161 .map(RecipientIdentifier.class::cast)
162 .collect(Collectors.toSet()));
163 checkSendMessageResults(results.getTimestamp(), results.getResults());
164 return results.getTimestamp();
165 } catch (IOException e) {
166 throw new Error.Failure(e.getMessage());
167 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
168 throw new Error.GroupNotFound(e.getMessage());
169 }
170 }
171
172 @Override
173 public void sendTyping(
174 final String recipient, final boolean stop
175 ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity {
176 try {
177 var recipients = new ArrayList<String>(1);
178 recipients.add(recipient);
179 m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START,
180 getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
181 .map(RecipientIdentifier.class::cast)
182 .collect(Collectors.toSet()));
183 } catch (IOException e) {
184 throw new Error.Failure(e.getMessage());
185 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
186 throw new Error.GroupNotFound(e.getMessage());
187 } catch (UntrustedIdentityException e) {
188 throw new Error.UntrustedIdentity(e.getMessage());
189 }
190 }
191
192 @Override
193 public void sendReadReceipt(
194 final String recipient, final List<Long> timestamps
195 ) throws Error.Failure, Error.UntrustedIdentity {
196 try {
197 m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps);
198 } catch (IOException e) {
199 throw new Error.Failure(e.getMessage());
200 } catch (UntrustedIdentityException e) {
201 throw new Error.UntrustedIdentity(e.getMessage());
202 }
203 }
204
205 @Override
206 public long sendNoteToSelfMessage(
207 final String message, final List<String> attachments
208 ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
209 try {
210 final var results = m.sendMessage(new Message(message, attachments),
211 Set.of(new RecipientIdentifier.NoteToSelf()));
212 checkSendMessageResults(results.getTimestamp(), results.getResults());
213 return results.getTimestamp();
214 } catch (AttachmentInvalidException e) {
215 throw new Error.AttachmentInvalid(e.getMessage());
216 } catch (IOException e) {
217 throw new Error.Failure(e.getMessage());
218 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
219 throw new Error.GroupNotFound(e.getMessage());
220 }
221 }
222
223 @Override
224 public void sendEndSessionMessage(final List<String> recipients) {
225 try {
226 final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername()));
227 checkSendMessageResults(results.getTimestamp(), results.getResults());
228 } catch (IOException e) {
229 throw new Error.Failure(e.getMessage());
230 }
231 }
232
233 @Override
234 public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
235 try {
236 var results = m.sendMessage(new Message(message, attachments),
237 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
238 checkSendMessageResults(results.getTimestamp(), results.getResults());
239 return results.getTimestamp();
240 } catch (IOException e) {
241 throw new Error.Failure(e.getMessage());
242 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
243 throw new Error.GroupNotFound(e.getMessage());
244 } catch (AttachmentInvalidException e) {
245 throw new Error.AttachmentInvalid(e.getMessage());
246 }
247 }
248
249 @Override
250 public long sendGroupMessageReaction(
251 final String emoji,
252 final boolean remove,
253 final String targetAuthor,
254 final long targetSentTimestamp,
255 final byte[] groupId
256 ) {
257 try {
258 final var results = m.sendMessageReaction(emoji,
259 remove,
260 getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
261 targetSentTimestamp,
262 Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
263 checkSendMessageResults(results.getTimestamp(), results.getResults());
264 return results.getTimestamp();
265 } catch (IOException e) {
266 throw new Error.Failure(e.getMessage());
267 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
268 throw new Error.GroupNotFound(e.getMessage());
269 }
270 }
271
272 // Since contact names might be empty if not defined, also potentially return
273 // the profile name
274 @Override
275 public String getContactName(final String number) {
276 return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername()));
277 }
278
279 @Override
280 public void setContactName(final String number, final String name) {
281 try {
282 m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name);
283 } catch (NotMasterDeviceException e) {
284 throw new Error.Failure("This command doesn't work on linked devices.");
285 } catch (UnregisteredUserException e) {
286 throw new Error.Failure("Contact is not registered.");
287 }
288 }
289
290 @Override
291 public void setContactBlocked(final String number, final boolean blocked) {
292 try {
293 m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked);
294 } catch (NotMasterDeviceException e) {
295 throw new Error.Failure("This command doesn't work on linked devices.");
296 } catch (IOException e) {
297 throw new Error.Failure(e.getMessage());
298 }
299 }
300
301 @Override
302 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
303 try {
304 m.setGroupBlocked(getGroupId(groupId), blocked);
305 } catch (GroupNotFoundException e) {
306 throw new Error.GroupNotFound(e.getMessage());
307 } catch (IOException e) {
308 throw new Error.Failure(e.getMessage());
309 }
310 }
311
312 @Override
313 public List<byte[]> getGroupIds() {
314 var groups = m.getGroups();
315 var ids = new ArrayList<byte[]>(groups.size());
316 for (var group : groups) {
317 ids.add(group.getGroupId().serialize());
318 }
319 return ids;
320 }
321
322 @Override
323 public String getGroupName(final byte[] groupId) {
324 var group = m.getGroup(getGroupId(groupId));
325 if (group == null) {
326 return "";
327 } else {
328 return group.getTitle();
329 }
330 }
331
332 @Override
333 public List<String> getGroupMembers(final byte[] groupId) {
334 var group = m.getGroup(getGroupId(groupId));
335 if (group == null) {
336 return List.of();
337 } else {
338 return group.getMembers()
339 .stream()
340 .map(m::resolveSignalServiceAddress)
341 .map(Util::getLegacyIdentifier)
342 .collect(Collectors.toList());
343 }
344 }
345
346 @Override
347 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
348 try {
349 if (groupId.length == 0) {
350 groupId = null;
351 }
352 if (name.isEmpty()) {
353 name = null;
354 }
355 if (avatar.isEmpty()) {
356 avatar = null;
357 }
358 final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername());
359 if (groupId == null) {
360 final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
361 checkSendMessageResults(results.second().getTimestamp(), results.second().getResults());
362 return results.first().serialize();
363 } else {
364 final var results = m.updateGroup(getGroupId(groupId),
365 name,
366 null,
367 memberIdentifiers,
368 null,
369 null,
370 null,
371 false,
372 null,
373 null,
374 null,
375 avatar == null ? null : new File(avatar),
376 null,
377 null);
378 if (results != null) {
379 checkSendMessageResults(results.getTimestamp(), results.getResults());
380 }
381 return groupId;
382 }
383 } catch (IOException e) {
384 throw new Error.Failure(e.getMessage());
385 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
386 throw new Error.GroupNotFound(e.getMessage());
387 } catch (AttachmentInvalidException e) {
388 throw new Error.AttachmentInvalid(e.getMessage());
389 }
390 }
391
392 @Override
393 public boolean isRegistered() {
394 return true;
395 }
396
397 @Override
398 public void updateProfile(
399 final String name,
400 final String about,
401 final String aboutEmoji,
402 String avatarPath,
403 final boolean removeAvatar
404 ) {
405 try {
406 if (avatarPath.isEmpty()) {
407 avatarPath = null;
408 }
409 Optional<File> avatarFile = removeAvatar
410 ? Optional.absent()
411 : avatarPath == null ? null : Optional.of(new File(avatarPath));
412 m.setProfile(name, null, about, aboutEmoji, avatarFile);
413 } catch (IOException e) {
414 throw new Error.Failure(e.getMessage());
415 }
416 }
417
418 @Override
419 public void removePin() {
420 try {
421 m.setRegistrationLockPin(Optional.absent());
422 } catch (UnauthenticatedResponseException e) {
423 throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage());
424 } catch (IOException e) {
425 throw new Error.Failure("Remove pin error: " + e.getMessage());
426 }
427 }
428
429 @Override
430 public void setPin(String registrationLockPin) {
431 try {
432 m.setRegistrationLockPin(Optional.of(registrationLockPin));
433 } catch (UnauthenticatedResponseException e) {
434 throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage());
435 } catch (IOException e) {
436 throw new Error.Failure("Set pin error: " + e.getMessage());
437 }
438 }
439
440 // Provide option to query a version string in order to react on potential
441 // future interface changes
442 @Override
443 public String version() {
444 return BaseConfig.PROJECT_VERSION;
445 }
446
447 // Create a unique list of Numbers from Identities and Contacts to really get
448 // all numbers the system knows
449 @Override
450 public List<String> listNumbers() {
451 return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
452 m.getContacts().stream().map(Pair::first))
453 .map(m::resolveSignalServiceAddress)
454 .map(a -> a.getNumber().orNull())
455 .filter(Objects::nonNull)
456 .distinct()
457 .collect(Collectors.toList());
458 }
459
460 @Override
461 public List<String> getContactNumber(final String name) {
462 // Contact names have precedence.
463 var numbers = new ArrayList<String>();
464 var contacts = m.getContacts();
465 for (var c : contacts) {
466 if (name.equals(c.second().getName())) {
467 numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
468 }
469 }
470 // Try profiles if no contact name was found
471 for (var identity : m.getIdentities()) {
472 final var recipientId = identity.getRecipientId();
473 final var address = m.resolveSignalServiceAddress(recipientId);
474 var number = address.getNumber().orNull();
475 if (number != null) {
476 var profile = m.getRecipientProfile(recipientId);
477 if (profile != null && profile.getDisplayName().equals(name)) {
478 numbers.add(number);
479 }
480 }
481 }
482 return numbers;
483 }
484
485 @Override
486 public void quitGroup(final byte[] groupId) {
487 var group = getGroupId(groupId);
488 try {
489 m.quitGroup(group, Set.of());
490 } catch (GroupNotFoundException | NotAGroupMemberException e) {
491 throw new Error.GroupNotFound(e.getMessage());
492 } catch (IOException | LastGroupAdminException e) {
493 throw new Error.Failure(e.getMessage());
494 }
495 }
496
497 @Override
498 public byte[] joinGroup(final String groupLink) {
499 try {
500 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
501 if (linkUrl == null) {
502 throw new Error.Failure("Group link is invalid:");
503 }
504 final var result = m.joinGroup(linkUrl);
505 return result.first().serialize();
506 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
507 throw new Error.Failure("Group link is invalid: " + e.getMessage());
508 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
509 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
510 } catch (IOException e) {
511 throw new Error.Failure(e.getMessage());
512 }
513 }
514
515 @Override
516 public boolean isContactBlocked(final String number) {
517 return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()));
518 }
519
520 @Override
521 public boolean isGroupBlocked(final byte[] groupId) {
522 var group = m.getGroup(getGroupId(groupId));
523 if (group == null) {
524 return false;
525 } else {
526 return group.isBlocked();
527 }
528 }
529
530 @Override
531 public boolean isMember(final byte[] groupId) {
532 var group = m.getGroup(getGroupId(groupId));
533 if (group == null) {
534 return false;
535 } else {
536 return group.isMember(m.getSelfRecipientId());
537 }
538 }
539
540 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
541 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
542
543 if (error == null) {
544 return;
545 }
546
547 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
548
549 if (result.getIdentityFailure() != null) {
550 throw new Error.UntrustedIdentity(message);
551 } else {
552 throw new Error.Failure(message);
553 }
554 }
555
556 private static void checkSendMessageResults(
557 long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results
558 ) throws DBusExecutionException {
559 final var sendMessageResults = results.values().stream().findFirst();
560 if (results.size() == 1 && sendMessageResults.get().size() == 1) {
561 checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get());
562 return;
563 }
564
565 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
566 if (errors.size() == 0) {
567 return;
568 }
569
570 var message = new StringBuilder();
571 message.append(timestamp).append('\n');
572 message.append("Failed to send (some) messages:\n");
573 for (var error : errors) {
574 message.append(error).append('\n');
575 }
576
577 throw new Error.Failure(message.toString());
578 }
579
580 private static void checkSendMessageResults(
581 long timestamp, Collection<SendMessageResult> results
582 ) throws DBusExecutionException {
583 if (results.size() == 1) {
584 checkSendMessageResult(timestamp, results.stream().findFirst().get());
585 return;
586 }
587
588 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
589 if (errors.size() == 0) {
590 return;
591 }
592
593 var message = new StringBuilder();
594 message.append(timestamp).append('\n');
595 message.append("Failed to send (some) messages:\n");
596 for (var error : errors) {
597 message.append(error).append('\n');
598 }
599
600 throw new Error.Failure(message.toString());
601 }
602
603 private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
604 final Collection<String> recipientStrings, final String localNumber
605 ) throws DBusExecutionException {
606 final var identifiers = new HashSet<RecipientIdentifier.Single>();
607 for (var recipientString : recipientStrings) {
608 identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber));
609 }
610 return identifiers;
611 }
612
613 private static RecipientIdentifier.Single getSingleRecipientIdentifier(
614 final String recipientString, final String localNumber
615 ) throws DBusExecutionException {
616 try {
617 return RecipientIdentifier.Single.fromString(recipientString, localNumber);
618 } catch (InvalidNumberException e) {
619 throw new Error.InvalidNumber(e.getMessage());
620 }
621 }
622
623 private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException {
624 try {
625 return GroupId.unknownVersion(groupId);
626 } catch (Throwable e) {
627 throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
628 }
629 }
630 }