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