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