]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
392b2df02a78f33a0d94803695e259e8cad3fbde
[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 }
257 }
258
259 @Override
260 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
261 try {
262 m.setGroupBlocked(getGroupId(groupId), blocked);
263 } catch (GroupNotFoundException e) {
264 throw new Error.GroupNotFound(e.getMessage());
265 }
266 }
267
268 @Override
269 public List<byte[]> getGroupIds() {
270 var groups = m.getGroups();
271 var ids = new ArrayList<byte[]>(groups.size());
272 for (var group : groups) {
273 ids.add(group.getGroupId().serialize());
274 }
275 return ids;
276 }
277
278 @Override
279 public String getGroupName(final byte[] groupId) {
280 var group = m.getGroup(getGroupId(groupId));
281 if (group == null) {
282 return "";
283 } else {
284 return group.getTitle();
285 }
286 }
287
288 @Override
289 public List<String> getGroupMembers(final byte[] groupId) {
290 var group = m.getGroup(getGroupId(groupId));
291 if (group == null) {
292 return List.of();
293 } else {
294 return group.getMembers()
295 .stream()
296 .map(m::resolveSignalServiceAddress)
297 .map(Util::getLegacyIdentifier)
298 .collect(Collectors.toList());
299 }
300 }
301
302 @Override
303 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
304 try {
305 if (groupId.length == 0) {
306 groupId = null;
307 }
308 if (name.isEmpty()) {
309 name = null;
310 }
311 if (avatar.isEmpty()) {
312 avatar = null;
313 }
314 final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername());
315 if (groupId == null) {
316 final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
317 checkSendMessageResults(results.second().getTimestamp(), results.second().getResults());
318 return results.first().serialize();
319 } else {
320 final var results = m.updateGroup(getGroupId(groupId),
321 name,
322 null,
323 memberIdentifiers,
324 null,
325 null,
326 null,
327 false,
328 null,
329 null,
330 null,
331 avatar == null ? null : new File(avatar),
332 null,
333 null);
334 if (results != null) {
335 checkSendMessageResults(results.getTimestamp(), results.getResults());
336 }
337 return groupId;
338 }
339 } catch (IOException e) {
340 throw new Error.Failure(e.getMessage());
341 } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) {
342 throw new Error.GroupNotFound(e.getMessage());
343 } catch (AttachmentInvalidException e) {
344 throw new Error.AttachmentInvalid(e.getMessage());
345 }
346 }
347
348 @Override
349 public boolean isRegistered() {
350 return true;
351 }
352
353 @Override
354 public void updateProfile(
355 final String name,
356 final String about,
357 final String aboutEmoji,
358 String avatarPath,
359 final boolean removeAvatar
360 ) {
361 try {
362 if (avatarPath.isEmpty()) {
363 avatarPath = null;
364 }
365 Optional<File> avatarFile = removeAvatar
366 ? Optional.absent()
367 : avatarPath == null ? null : Optional.of(new File(avatarPath));
368 m.setProfile(name, null, about, aboutEmoji, avatarFile);
369 } catch (IOException e) {
370 throw new Error.Failure(e.getMessage());
371 }
372 }
373
374 // Provide option to query a version string in order to react on potential
375 // future interface changes
376 @Override
377 public String version() {
378 return BaseConfig.PROJECT_VERSION;
379 }
380
381 // Create a unique list of Numbers from Identities and Contacts to really get
382 // all numbers the system knows
383 @Override
384 public List<String> listNumbers() {
385 return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
386 m.getContacts().stream().map(Pair::first))
387 .map(m::resolveSignalServiceAddress)
388 .map(a -> a.getNumber().orNull())
389 .filter(Objects::nonNull)
390 .distinct()
391 .collect(Collectors.toList());
392 }
393
394 @Override
395 public List<String> getContactNumber(final String name) {
396 // Contact names have precedence.
397 var numbers = new ArrayList<String>();
398 var contacts = m.getContacts();
399 for (var c : contacts) {
400 if (name.equals(c.second().getName())) {
401 numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
402 }
403 }
404 // Try profiles if no contact name was found
405 for (var identity : m.getIdentities()) {
406 final var recipientId = identity.getRecipientId();
407 final var address = m.resolveSignalServiceAddress(recipientId);
408 var number = address.getNumber().orNull();
409 if (number != null) {
410 var profile = m.getRecipientProfile(recipientId);
411 if (profile != null && profile.getDisplayName().equals(name)) {
412 numbers.add(number);
413 }
414 }
415 }
416 return numbers;
417 }
418
419 @Override
420 public void quitGroup(final byte[] groupId) {
421 var group = getGroupId(groupId);
422 try {
423 m.quitGroup(group, Set.of());
424 } catch (GroupNotFoundException | NotAGroupMemberException e) {
425 throw new Error.GroupNotFound(e.getMessage());
426 } catch (IOException | LastGroupAdminException e) {
427 throw new Error.Failure(e.getMessage());
428 }
429 }
430
431 @Override
432 public byte[] joinGroup(final String groupLink) {
433 try {
434 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
435 if (linkUrl == null) {
436 throw new Error.Failure("Group link is invalid:");
437 }
438 final var result = m.joinGroup(linkUrl);
439 return result.first().serialize();
440 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
441 throw new Error.Failure("Group link is invalid: " + e.getMessage());
442 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
443 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
444 } catch (IOException e) {
445 throw new Error.Failure(e.getMessage());
446 }
447 }
448
449 @Override
450 public boolean isContactBlocked(final String number) {
451 return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()));
452 }
453
454 @Override
455 public boolean isGroupBlocked(final byte[] groupId) {
456 var group = m.getGroup(getGroupId(groupId));
457 if (group == null) {
458 return false;
459 } else {
460 return group.isBlocked();
461 }
462 }
463
464 @Override
465 public boolean isMember(final byte[] groupId) {
466 var group = m.getGroup(getGroupId(groupId));
467 if (group == null) {
468 return false;
469 } else {
470 return group.isMember(m.getSelfRecipientId());
471 }
472 }
473
474 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
475 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
476
477 if (error == null) {
478 return;
479 }
480
481 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
482
483 if (result.getIdentityFailure() != null) {
484 throw new Error.UntrustedIdentity(message);
485 } else {
486 throw new Error.Failure(message);
487 }
488 }
489
490 private static void checkSendMessageResults(
491 long timestamp, Map<RecipientIdentifier, List<SendMessageResult>> results
492 ) throws DBusExecutionException {
493 final var sendMessageResults = results.values().stream().findFirst();
494 if (results.size() == 1 && sendMessageResults.get().size() == 1) {
495 checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get());
496 return;
497 }
498
499 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
500 if (errors.size() == 0) {
501 return;
502 }
503
504 var message = new StringBuilder();
505 message.append(timestamp).append('\n');
506 message.append("Failed to send (some) messages:\n");
507 for (var error : errors) {
508 message.append(error).append('\n');
509 }
510
511 throw new Error.Failure(message.toString());
512 }
513
514 private static void checkSendMessageResults(
515 long timestamp, Collection<SendMessageResult> results
516 ) throws DBusExecutionException {
517 if (results.size() == 1) {
518 checkSendMessageResult(timestamp, results.stream().findFirst().get());
519 return;
520 }
521
522 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
523 if (errors.size() == 0) {
524 return;
525 }
526
527 var message = new StringBuilder();
528 message.append(timestamp).append('\n');
529 message.append("Failed to send (some) messages:\n");
530 for (var error : errors) {
531 message.append(error).append('\n');
532 }
533
534 throw new Error.Failure(message.toString());
535 }
536
537 private static Set<RecipientIdentifier.Single> getSingleRecipientIdentifiers(
538 final Collection<String> recipientStrings, final String localNumber
539 ) throws DBusExecutionException {
540 final var identifiers = new HashSet<RecipientIdentifier.Single>();
541 for (var recipientString : recipientStrings) {
542 identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber));
543 }
544 return identifiers;
545 }
546
547 private static RecipientIdentifier.Single getSingleRecipientIdentifier(
548 final String recipientString, final String localNumber
549 ) throws DBusExecutionException {
550 try {
551 return RecipientIdentifier.Single.fromString(recipientString, localNumber);
552 } catch (InvalidNumberException e) {
553 throw new Error.InvalidNumber(e.getMessage());
554 }
555 }
556
557 private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException {
558 try {
559 return GroupId.unknownVersion(groupId);
560 } catch (Throwable e) {
561 throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
562 }
563 }
564 }