]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Archive recipient's sessions after identity key switch
[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.groups.GroupId;
9 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
10 import org.asamk.signal.manager.groups.GroupNotFoundException;
11 import org.asamk.signal.manager.groups.NotAGroupMemberException;
12 import org.asamk.signal.manager.storage.identities.IdentityInfo;
13 import org.asamk.signal.util.ErrorUtils;
14 import org.freedesktop.dbus.exceptions.DBusExecutionException;
15 import org.whispersystems.libsignal.util.Pair;
16 import org.whispersystems.libsignal.util.guava.Optional;
17 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
18 import org.whispersystems.signalservice.api.messages.SendMessageResult;
19 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
20 import org.whispersystems.signalservice.api.util.InvalidNumberException;
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.Objects;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
29
30 public class DbusSignalImpl implements Signal {
31
32 private final Manager m;
33
34 public DbusSignalImpl(final Manager m) {
35 this.m = m;
36 }
37
38 @Override
39 public boolean isRemote() {
40 return false;
41 }
42
43 @Override
44 public String getObjectPath() {
45 return null;
46 }
47
48 @Override
49 public long sendMessage(final String message, final List<String> attachments, final String recipient) {
50 var recipients = new ArrayList<String>(1);
51 recipients.add(recipient);
52 return sendMessage(message, attachments, recipients);
53 }
54
55 private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException {
56 var error = ErrorUtils.getErrorMessageFromSendMessageResult(result);
57
58 if (error == null) {
59 return;
60 }
61
62 final var message = timestamp + "\nFailed to send message:\n" + error + '\n';
63
64 if (result.getIdentityFailure() != null) {
65 throw new Error.UntrustedIdentity(message);
66 } else {
67 throw new Error.Failure(message);
68 }
69 }
70
71 private static void checkSendMessageResults(
72 long timestamp, List<SendMessageResult> results
73 ) throws DBusExecutionException {
74 if (results.size() == 1) {
75 checkSendMessageResult(timestamp, results.get(0));
76 return;
77 }
78
79 var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
80 if (errors.size() == 0) {
81 return;
82 }
83
84 var message = new StringBuilder();
85 message.append(timestamp).append('\n');
86 message.append("Failed to send (some) messages:\n");
87 for (var error : errors) {
88 message.append(error).append('\n');
89 }
90
91 throw new Error.Failure(message.toString());
92 }
93
94 @Override
95 public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
96 try {
97 final var results = m.sendMessage(message, attachments, recipients);
98 checkSendMessageResults(results.first(), results.second());
99 return results.first();
100 } catch (InvalidNumberException e) {
101 throw new Error.InvalidNumber(e.getMessage());
102 } catch (AttachmentInvalidException e) {
103 throw new Error.AttachmentInvalid(e.getMessage());
104 } catch (IOException e) {
105 throw new Error.Failure(e.getMessage());
106 }
107 }
108
109 @Override
110 public long sendRemoteDeleteMessage(
111 final long targetSentTimestamp, final String recipient
112 ) {
113 var recipients = new ArrayList<String>(1);
114 recipients.add(recipient);
115 return sendRemoteDeleteMessage(targetSentTimestamp, recipients);
116 }
117
118 @Override
119 public long sendRemoteDeleteMessage(
120 final long targetSentTimestamp, final List<String> recipients
121 ) {
122 try {
123 final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, recipients);
124 checkSendMessageResults(results.first(), results.second());
125 return results.first();
126 } catch (IOException e) {
127 throw new Error.Failure(e.getMessage());
128 } catch (InvalidNumberException e) {
129 throw new Error.InvalidNumber(e.getMessage());
130 }
131 }
132
133 @Override
134 public long sendGroupRemoteDeleteMessage(
135 final long targetSentTimestamp, final byte[] groupId
136 ) {
137 try {
138 final var results = m.sendGroupRemoteDeleteMessage(targetSentTimestamp, GroupId.unknownVersion(groupId));
139 checkSendMessageResults(results.first(), results.second());
140 return results.first();
141 } catch (IOException e) {
142 throw new Error.Failure(e.getMessage());
143 } catch (GroupNotFoundException | NotAGroupMemberException e) {
144 throw new Error.GroupNotFound(e.getMessage());
145 }
146 }
147
148 @Override
149 public long sendMessageReaction(
150 final String emoji,
151 final boolean remove,
152 final String targetAuthor,
153 final long targetSentTimestamp,
154 final String recipient
155 ) {
156 var recipients = new ArrayList<String>(1);
157 recipients.add(recipient);
158 return sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
159 }
160
161 @Override
162 public long sendMessageReaction(
163 final String emoji,
164 final boolean remove,
165 final String targetAuthor,
166 final long targetSentTimestamp,
167 final List<String> recipients
168 ) {
169 try {
170 final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients);
171 checkSendMessageResults(results.first(), results.second());
172 return results.first();
173 } catch (InvalidNumberException e) {
174 throw new Error.InvalidNumber(e.getMessage());
175 } catch (IOException e) {
176 throw new Error.Failure(e.getMessage());
177 }
178 }
179
180 @Override
181 public long sendNoteToSelfMessage(
182 final String message, final List<String> attachments
183 ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
184 try {
185 final var results = m.sendSelfMessage(message, attachments);
186 checkSendMessageResult(results.first(), results.second());
187 return results.first();
188 } catch (AttachmentInvalidException e) {
189 throw new Error.AttachmentInvalid(e.getMessage());
190 } catch (IOException e) {
191 throw new Error.Failure(e.getMessage());
192 }
193 }
194
195 @Override
196 public void sendEndSessionMessage(final List<String> recipients) {
197 try {
198 final var results = m.sendEndSessionMessage(recipients);
199 checkSendMessageResults(results.first(), results.second());
200 } catch (IOException e) {
201 throw new Error.Failure(e.getMessage());
202 } catch (InvalidNumberException e) {
203 throw new Error.InvalidNumber(e.getMessage());
204 }
205 }
206
207 @Override
208 public long sendGroupMessage(final String message, final List<String> attachments, final byte[] groupId) {
209 try {
210 var results = m.sendGroupMessage(message, attachments, GroupId.unknownVersion(groupId));
211 checkSendMessageResults(results.first(), results.second());
212 return results.first();
213 } catch (IOException e) {
214 throw new Error.Failure(e.getMessage());
215 } catch (GroupNotFoundException | NotAGroupMemberException e) {
216 throw new Error.GroupNotFound(e.getMessage());
217 } catch (AttachmentInvalidException e) {
218 throw new Error.AttachmentInvalid(e.getMessage());
219 }
220 }
221
222 @Override
223 public long sendGroupMessageReaction(
224 final String emoji,
225 final boolean remove,
226 final String targetAuthor,
227 final long targetSentTimestamp,
228 final byte[] groupId
229 ) {
230 try {
231 final var results = m.sendGroupMessageReaction(emoji,
232 remove,
233 targetAuthor,
234 targetSentTimestamp,
235 GroupId.unknownVersion(groupId));
236 checkSendMessageResults(results.first(), results.second());
237 return results.first();
238 } catch (IOException e) {
239 throw new Error.Failure(e.getMessage());
240 } catch (InvalidNumberException e) {
241 throw new Error.InvalidNumber(e.getMessage());
242 } catch (GroupNotFoundException | NotAGroupMemberException e) {
243 throw new Error.GroupNotFound(e.getMessage());
244 }
245 }
246
247 // Since contact names might be empty if not defined, also potentially return
248 // the profile name
249 @Override
250 public String getContactName(final String number) {
251 try {
252 return m.getContactOrProfileName(number);
253 } catch (InvalidNumberException e) {
254 throw new Error.InvalidNumber(e.getMessage());
255 }
256 }
257
258 @Override
259 public void setContactName(final String number, final String name) {
260 try {
261 m.setContactName(number, name);
262 } catch (InvalidNumberException e) {
263 throw new Error.InvalidNumber(e.getMessage());
264 } catch (NotMasterDeviceException e) {
265 throw new Error.Failure("This command doesn't work on linked devices.");
266 }
267 }
268
269 @Override
270 public void setContactBlocked(final String number, final boolean blocked) {
271 try {
272 m.setContactBlocked(number, blocked);
273 } catch (InvalidNumberException e) {
274 throw new Error.InvalidNumber(e.getMessage());
275 } catch (NotMasterDeviceException e) {
276 throw new Error.Failure("This command doesn't work on linked devices.");
277 }
278 }
279
280 @Override
281 public void setGroupBlocked(final byte[] groupId, final boolean blocked) {
282 try {
283 m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked);
284 } catch (GroupNotFoundException e) {
285 throw new Error.GroupNotFound(e.getMessage());
286 }
287 }
288
289 @Override
290 public List<byte[]> getGroupIds() {
291 var groups = m.getGroups();
292 var ids = new ArrayList<byte[]>(groups.size());
293 for (var group : groups) {
294 ids.add(group.getGroupId().serialize());
295 }
296 return ids;
297 }
298
299 @Override
300 public String getGroupName(final byte[] groupId) {
301 var group = m.getGroup(GroupId.unknownVersion(groupId));
302 if (group == null) {
303 return "";
304 } else {
305 return group.getTitle();
306 }
307 }
308
309 @Override
310 public List<String> getGroupMembers(final byte[] groupId) {
311 var group = m.getGroup(GroupId.unknownVersion(groupId));
312 if (group == null) {
313 return List.of();
314 } else {
315 return group.getMembers()
316 .stream()
317 .map(m::resolveSignalServiceAddress)
318 .map(SignalServiceAddress::getLegacyIdentifier)
319 .collect(Collectors.toList());
320 }
321 }
322
323 @Override
324 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
325 try {
326 if (groupId.length == 0) {
327 groupId = null;
328 }
329 if (name.isEmpty()) {
330 name = null;
331 }
332 if (members.isEmpty()) {
333 members = null;
334 }
335 if (avatar.isEmpty()) {
336 avatar = null;
337 }
338 final var results = m.updateGroup(groupId == null ? null : GroupId.unknownVersion(groupId),
339 name,
340 members,
341 avatar == null ? null : new File(avatar));
342 checkSendMessageResults(0, results.second());
343 return results.first().serialize();
344 } catch (IOException e) {
345 throw new Error.Failure(e.getMessage());
346 } catch (GroupNotFoundException | NotAGroupMemberException e) {
347 throw new Error.GroupNotFound(e.getMessage());
348 } catch (InvalidNumberException e) {
349 throw new Error.InvalidNumber(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, 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(m.resolveSignalServiceAddress(c.first()).getLegacyIdentifier());
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(address);
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 = GroupId.unknownVersion(groupId);
429 try {
430 m.sendQuitGroupMessage(group);
431 } catch (GroupNotFoundException | NotAGroupMemberException e) {
432 throw new Error.GroupNotFound(e.getMessage());
433 } catch (IOException e) {
434 throw new Error.Failure(e.getMessage());
435 }
436 }
437
438 @Override
439 public void 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 m.joinGroup(linkUrl);
446 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
447 throw new Error.Failure("Group link is invalid: " + e.getMessage());
448 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
449 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
450 } catch (IOException e) {
451 throw new Error.Failure(e.getMessage());
452 }
453 }
454
455 @Override
456 public boolean isContactBlocked(final String number) {
457 try {
458 return m.isContactBlocked(number);
459 } catch (InvalidNumberException e) {
460 throw new Error.InvalidNumber(e.getMessage());
461 }
462 }
463
464 @Override
465 public boolean isGroupBlocked(final byte[] groupId) {
466 var group = m.getGroup(GroupId.unknownVersion(groupId));
467 if (group == null) {
468 return false;
469 } else {
470 return group.isBlocked();
471 }
472 }
473
474 @Override
475 public boolean isMember(final byte[] groupId) {
476 var group = m.getGroup(GroupId.unknownVersion(groupId));
477 if (group == null) {
478 return false;
479 } else {
480 return group.isMember(m.getSelfRecipientId());
481 }
482 }
483 }