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