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