]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java
Implement announcement groups
[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 null);
363 if (results != null) {
364 checkSendMessageResults(results.first(), results.second());
365 }
366 return groupId;
367 }
368 } catch (IOException e) {
369 throw new Error.Failure(e.getMessage());
370 } catch (GroupNotFoundException | NotAGroupMemberException e) {
371 throw new Error.GroupNotFound(e.getMessage());
372 } catch (InvalidNumberException e) {
373 throw new Error.InvalidNumber(e.getMessage());
374 } catch (AttachmentInvalidException e) {
375 throw new Error.AttachmentInvalid(e.getMessage());
376 }
377 }
378
379 @Override
380 public boolean isRegistered() {
381 return true;
382 }
383
384 @Override
385 public void updateProfile(
386 final String name,
387 final String about,
388 final String aboutEmoji,
389 String avatarPath,
390 final boolean removeAvatar
391 ) {
392 try {
393 if (avatarPath.isEmpty()) {
394 avatarPath = null;
395 }
396 Optional<File> avatarFile = removeAvatar
397 ? Optional.absent()
398 : avatarPath == null ? null : Optional.of(new File(avatarPath));
399 m.setProfile(name, null, about, aboutEmoji, avatarFile);
400 } catch (IOException e) {
401 throw new Error.Failure(e.getMessage());
402 }
403 }
404
405 // Provide option to query a version string in order to react on potential
406 // future interface changes
407 @Override
408 public String version() {
409 return BaseConfig.PROJECT_VERSION;
410 }
411
412 // Create a unique list of Numbers from Identities and Contacts to really get
413 // all numbers the system knows
414 @Override
415 public List<String> listNumbers() {
416 return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
417 m.getContacts().stream().map(Pair::first))
418 .map(m::resolveSignalServiceAddress)
419 .map(a -> a.getNumber().orNull())
420 .filter(Objects::nonNull)
421 .distinct()
422 .collect(Collectors.toList());
423 }
424
425 @Override
426 public List<String> getContactNumber(final String name) {
427 // Contact names have precedence.
428 var numbers = new ArrayList<String>();
429 var contacts = m.getContacts();
430 for (var c : contacts) {
431 if (name.equals(c.second().getName())) {
432 numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
433 }
434 }
435 // Try profiles if no contact name was found
436 for (var identity : m.getIdentities()) {
437 final var recipientId = identity.getRecipientId();
438 final var address = m.resolveSignalServiceAddress(recipientId);
439 var number = address.getNumber().orNull();
440 if (number != null) {
441 var profile = m.getRecipientProfile(recipientId);
442 if (profile != null && profile.getDisplayName().equals(name)) {
443 numbers.add(number);
444 }
445 }
446 }
447 return numbers;
448 }
449
450 @Override
451 public void quitGroup(final byte[] groupId) {
452 var group = GroupId.unknownVersion(groupId);
453 try {
454 m.sendQuitGroupMessage(group, Set.of());
455 } catch (GroupNotFoundException | NotAGroupMemberException e) {
456 throw new Error.GroupNotFound(e.getMessage());
457 } catch (IOException | LastGroupAdminException e) {
458 throw new Error.Failure(e.getMessage());
459 } catch (InvalidNumberException e) {
460 throw new Error.InvalidNumber(e.getMessage());
461 }
462 }
463
464 @Override
465 public void joinGroup(final String groupLink) {
466 try {
467 final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink);
468 if (linkUrl == null) {
469 throw new Error.Failure("Group link is invalid:");
470 }
471 m.joinGroup(linkUrl);
472 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) {
473 throw new Error.Failure("Group link is invalid: " + e.getMessage());
474 } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
475 throw new Error.Failure("Group link was created with an incompatible version: " + e.getMessage());
476 } catch (IOException e) {
477 throw new Error.Failure(e.getMessage());
478 }
479 }
480
481 @Override
482 public boolean isContactBlocked(final String number) {
483 try {
484 return m.isContactBlocked(number);
485 } catch (InvalidNumberException e) {
486 throw new Error.InvalidNumber(e.getMessage());
487 }
488 }
489
490 @Override
491 public boolean isGroupBlocked(final byte[] groupId) {
492 var group = m.getGroup(GroupId.unknownVersion(groupId));
493 if (group == null) {
494 return false;
495 } else {
496 return group.isBlocked();
497 }
498 }
499
500 @Override
501 public boolean isMember(final byte[] groupId) {
502 var group = m.getGroup(GroupId.unknownVersion(groupId));
503 if (group == null) {
504 return false;
505 } else {
506 return group.isMember(m.getSelfRecipientId());
507 }
508 }
509 }