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