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