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