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