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