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