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