]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Refactor dbus linked devices interface
[signal-cli] / src / main / java / org / asamk / signal / dbus / DbusManagerImpl.java
1 package org.asamk.signal.dbus;
2
3 import org.asamk.Signal;
4 import org.asamk.signal.DbusConfig;
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.StickerPackInvalidException;
9 import org.asamk.signal.manager.UntrustedIdentityException;
10 import org.asamk.signal.manager.api.Device;
11 import org.asamk.signal.manager.api.Group;
12 import org.asamk.signal.manager.api.Identity;
13 import org.asamk.signal.manager.api.Message;
14 import org.asamk.signal.manager.api.RecipientIdentifier;
15 import org.asamk.signal.manager.api.SendGroupMessageResults;
16 import org.asamk.signal.manager.api.SendMessageResults;
17 import org.asamk.signal.manager.api.TypingAction;
18 import org.asamk.signal.manager.groups.GroupId;
19 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
20 import org.asamk.signal.manager.groups.GroupLinkState;
21 import org.asamk.signal.manager.groups.GroupNotFoundException;
22 import org.asamk.signal.manager.groups.GroupPermission;
23 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
24 import org.asamk.signal.manager.groups.LastGroupAdminException;
25 import org.asamk.signal.manager.groups.NotAGroupMemberException;
26 import org.asamk.signal.manager.storage.recipients.Contact;
27 import org.asamk.signal.manager.storage.recipients.Profile;
28 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
29 import org.freedesktop.dbus.DBusPath;
30 import org.freedesktop.dbus.connections.impl.DBusConnection;
31 import org.freedesktop.dbus.exceptions.DBusException;
32 import org.freedesktop.dbus.interfaces.DBusInterface;
33 import org.whispersystems.libsignal.IdentityKey;
34 import org.whispersystems.libsignal.InvalidKeyException;
35 import org.whispersystems.libsignal.util.Pair;
36 import org.whispersystems.libsignal.util.guava.Optional;
37 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
38 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
39 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
40 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
41 import org.whispersystems.signalservice.api.util.UuidUtil;
42 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
43
44 import java.io.File;
45 import java.io.IOException;
46 import java.net.URI;
47 import java.net.URISyntaxException;
48 import java.util.ArrayList;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Set;
53 import java.util.UUID;
54 import java.util.concurrent.TimeUnit;
55 import java.util.function.Function;
56 import java.util.function.Supplier;
57 import java.util.stream.Collectors;
58
59 /**
60 * This class implements the Manager interface using the DBus Signal interface, where possible.
61 * It's used for the signal-cli dbus client mode (--dbus, --dbus-system)
62 */
63 public class DbusManagerImpl implements Manager {
64
65 private final Signal signal;
66 private final DBusConnection connection;
67
68 public DbusManagerImpl(final Signal signal, DBusConnection connection) {
69 this.signal = signal;
70 this.connection = connection;
71 }
72
73 @Override
74 public String getSelfNumber() {
75 return signal.getSelfNumber();
76 }
77
78 @Override
79 public void checkAccountState() throws IOException {
80 throw new UnsupportedOperationException();
81 }
82
83 @Override
84 public Map<String, Pair<String, UUID>> areUsersRegistered(final Set<String> numbers) throws IOException {
85 final var numbersList = new ArrayList<>(numbers);
86 final var registered = signal.isRegistered(numbersList);
87
88 final var result = new HashMap<String, Pair<String, UUID>>();
89 for (var i = 0; i < numbersList.size(); i++) {
90 result.put(numbersList.get(i),
91 new Pair<>(numbersList.get(i), registered.get(i) ? UuidUtil.UNKNOWN_UUID : null));
92 }
93 return result;
94 }
95
96 @Override
97 public void updateAccountAttributes(final String deviceName) throws IOException {
98 if (deviceName != null) {
99 final var devicePath = signal.getThisDevice();
100 getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName);
101 }
102 }
103
104 @Override
105 public void updateConfiguration(
106 final Boolean readReceipts,
107 final Boolean unidentifiedDeliveryIndicators,
108 final Boolean typingIndicators,
109 final Boolean linkPreviews
110 ) throws IOException {
111 throw new UnsupportedOperationException();
112 }
113
114 @Override
115 public void setProfile(
116 final String givenName,
117 final String familyName,
118 final String about,
119 final String aboutEmoji,
120 final Optional<File> avatar
121 ) throws IOException {
122 signal.updateProfile(emptyIfNull(givenName),
123 emptyIfNull(familyName),
124 emptyIfNull(about),
125 emptyIfNull(aboutEmoji),
126 avatar == null ? "" : avatar.transform(File::getPath).or(""),
127 avatar != null && !avatar.isPresent());
128 }
129
130 @Override
131 public void unregister() throws IOException {
132 throw new UnsupportedOperationException();
133 }
134
135 @Override
136 public void deleteAccount() throws IOException {
137 throw new UnsupportedOperationException();
138 }
139
140 @Override
141 public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
142 throw new UnsupportedOperationException();
143 }
144
145 @Override
146 public List<Device> getLinkedDevices() throws IOException {
147 final var thisDevice = signal.getThisDevice();
148 return signal.listDevices().stream().map(devicePath -> {
149 final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device");
150 return new Device((long) device.get("Id").getValue(),
151 (String) device.get("Name").getValue(),
152 (long) device.get("Created").getValue(),
153 (long) device.get("LastSeen").getValue(),
154 thisDevice.equals(devicePath));
155 }).collect(Collectors.toList());
156 }
157
158 @Override
159 public void removeLinkedDevices(final long deviceId) throws IOException {
160 final var devicePath = signal.getDevice(deviceId);
161 getRemoteObject(devicePath, Signal.Device.class).removeDevice();
162 }
163
164 @Override
165 public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException {
166 signal.addDevice(linkUri.toString());
167 }
168
169 @Override
170 public void setRegistrationLockPin(final Optional<String> pin) throws IOException, UnauthenticatedResponseException {
171 if (pin.isPresent()) {
172 signal.setPin(pin.get());
173 } else {
174 signal.removePin();
175 }
176 }
177
178 @Override
179 public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
180 throw new UnsupportedOperationException();
181 }
182
183 @Override
184 public List<Group> getGroups() {
185 final var groupIds = signal.getGroupIds();
186 return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList());
187 }
188
189 @Override
190 public SendGroupMessageResults quitGroup(
191 final GroupId groupId, final Set<RecipientIdentifier.Single> groupAdmins
192 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
193 if (groupAdmins.size() > 0) {
194 throw new UnsupportedOperationException();
195 }
196 signal.quitGroup(groupId.serialize());
197 return new SendGroupMessageResults(0, List.of());
198 }
199
200 @Override
201 public void deleteGroup(final GroupId groupId) throws IOException {
202 throw new UnsupportedOperationException();
203 }
204
205 @Override
206 public Pair<GroupId, SendGroupMessageResults> createGroup(
207 final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
208 ) throws IOException, AttachmentInvalidException {
209 final var newGroupId = signal.updateGroup(new byte[0],
210 emptyIfNull(name),
211 members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
212 avatarFile == null ? "" : avatarFile.getPath());
213 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
214 }
215
216 @Override
217 public SendGroupMessageResults updateGroup(
218 final GroupId groupId,
219 final String name,
220 final String description,
221 final Set<RecipientIdentifier.Single> members,
222 final Set<RecipientIdentifier.Single> removeMembers,
223 final Set<RecipientIdentifier.Single> admins,
224 final Set<RecipientIdentifier.Single> removeAdmins,
225 final boolean resetGroupLink,
226 final GroupLinkState groupLinkState,
227 final GroupPermission addMemberPermission,
228 final GroupPermission editDetailsPermission,
229 final File avatarFile,
230 final Integer expirationTimer,
231 final Boolean isAnnouncementGroup
232 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
233 signal.updateGroup(groupId.serialize(),
234 emptyIfNull(name),
235 members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
236 avatarFile == null ? "" : avatarFile.getPath());
237 return new SendGroupMessageResults(0, List.of());
238 }
239
240 @Override
241 public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException {
242 final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
243 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
244 }
245
246 @Override
247 public void sendTypingMessage(
248 final TypingAction action, final Set<RecipientIdentifier> recipients
249 ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
250 for (final var recipient : recipients) {
251 if (recipient instanceof RecipientIdentifier.Single) {
252 signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(),
253 action == TypingAction.STOP);
254 } else if (recipient instanceof RecipientIdentifier.Group) {
255 throw new UnsupportedOperationException();
256 }
257 }
258 }
259
260 @Override
261 public void sendReadReceipt(
262 final RecipientIdentifier.Single sender, final List<Long> messageIds
263 ) throws IOException, UntrustedIdentityException {
264 signal.sendReadReceipt(sender.getIdentifier(), messageIds);
265 }
266
267 @Override
268 public void sendViewedReceipt(
269 final RecipientIdentifier.Single sender, final List<Long> messageIds
270 ) throws IOException, UntrustedIdentityException {
271 throw new UnsupportedOperationException();
272 }
273
274 @Override
275 public SendMessageResults sendMessage(
276 final Message message, final Set<RecipientIdentifier> recipients
277 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
278 return handleMessage(recipients,
279 numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers),
280 () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()),
281 groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId));
282 }
283
284 @Override
285 public SendMessageResults sendRemoteDeleteMessage(
286 final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
287 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
288 return handleMessage(recipients,
289 numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
290 () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
291 groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
292 }
293
294 @Override
295 public SendMessageResults sendMessageReaction(
296 final String emoji,
297 final boolean remove,
298 final RecipientIdentifier.Single targetAuthor,
299 final long targetSentTimestamp,
300 final Set<RecipientIdentifier> recipients
301 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
302 return handleMessage(recipients,
303 numbers -> signal.sendMessageReaction(emoji,
304 remove,
305 targetAuthor.getIdentifier(),
306 targetSentTimestamp,
307 numbers),
308 () -> signal.sendMessageReaction(emoji,
309 remove,
310 targetAuthor.getIdentifier(),
311 targetSentTimestamp,
312 signal.getSelfNumber()),
313 groupId -> signal.sendGroupMessageReaction(emoji,
314 remove,
315 targetAuthor.getIdentifier(),
316 targetSentTimestamp,
317 groupId));
318 }
319
320 @Override
321 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
322 signal.sendEndSessionMessage(recipients.stream()
323 .map(RecipientIdentifier.Single::getIdentifier)
324 .collect(Collectors.toList()));
325 return new SendMessageResults(0, Map.of());
326 }
327
328 @Override
329 public void setContactName(
330 final RecipientIdentifier.Single recipient, final String name
331 ) throws NotMasterDeviceException, UnregisteredUserException {
332 signal.setContactName(recipient.getIdentifier(), name);
333 }
334
335 @Override
336 public void setContactBlocked(
337 final RecipientIdentifier.Single recipient, final boolean blocked
338 ) throws NotMasterDeviceException, IOException {
339 signal.setContactBlocked(recipient.getIdentifier(), blocked);
340 }
341
342 @Override
343 public void setGroupBlocked(
344 final GroupId groupId, final boolean blocked
345 ) throws GroupNotFoundException, IOException {
346 signal.setGroupBlocked(groupId.serialize(), blocked);
347 }
348
349 @Override
350 public void setExpirationTimer(
351 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
352 ) throws IOException {
353 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
354 }
355
356 @Override
357 public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
358 try {
359 return new URI(signal.uploadStickerPack(path.getPath()));
360 } catch (URISyntaxException e) {
361 throw new AssertionError(e);
362 }
363 }
364
365 @Override
366 public void requestAllSyncData() throws IOException {
367 signal.sendSyncRequest();
368 }
369
370 @Override
371 public void receiveMessages(
372 final long timeout,
373 final TimeUnit unit,
374 final boolean returnOnTimeout,
375 final boolean ignoreAttachments,
376 final ReceiveMessageHandler handler
377 ) throws IOException {
378 throw new UnsupportedOperationException();
379 }
380
381 @Override
382 public boolean hasCaughtUpWithOldMessages() {
383 throw new UnsupportedOperationException();
384 }
385
386 @Override
387 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
388 return signal.isContactBlocked(recipient.getIdentifier());
389 }
390
391 @Override
392 public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) {
393 throw new UnsupportedOperationException();
394 }
395
396 @Override
397 public void sendContacts() throws IOException {
398 signal.sendContacts();
399 }
400
401 @Override
402 public List<Pair<RecipientAddress, Contact>> getContacts() {
403 throw new UnsupportedOperationException();
404 }
405
406 @Override
407 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
408 return signal.getContactName(recipient.getIdentifier());
409 }
410
411 @Override
412 public Group getGroup(final GroupId groupId) {
413 final var id = groupId.serialize();
414 return new Group(groupId,
415 signal.getGroupName(id),
416 null,
417 null,
418 signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()),
419 Set.of(),
420 Set.of(),
421 Set.of(),
422 signal.isGroupBlocked(id),
423 0,
424 false,
425 signal.isMember(id));
426 }
427
428 @Override
429 public List<Identity> getIdentities() {
430 throw new UnsupportedOperationException();
431 }
432
433 @Override
434 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
435 throw new UnsupportedOperationException();
436 }
437
438 @Override
439 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
440 throw new UnsupportedOperationException();
441 }
442
443 @Override
444 public boolean trustIdentityVerifiedSafetyNumber(
445 final RecipientIdentifier.Single recipient, final String safetyNumber
446 ) {
447 throw new UnsupportedOperationException();
448 }
449
450 @Override
451 public boolean trustIdentityVerifiedSafetyNumber(
452 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
453 ) {
454 throw new UnsupportedOperationException();
455 }
456
457 @Override
458 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
459 throw new UnsupportedOperationException();
460 }
461
462 @Override
463 public String computeSafetyNumber(
464 final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
465 ) {
466 throw new UnsupportedOperationException();
467 }
468
469 @Override
470 public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
471 return address;
472 }
473
474 @Override
475 public void close() throws IOException {
476 }
477
478 private SendMessageResults handleMessage(
479 Set<RecipientIdentifier> recipients,
480 Function<List<String>, Long> recipientsHandler,
481 Supplier<Long> noteToSelfHandler,
482 Function<byte[], Long> groupHandler
483 ) {
484 long timestamp = 0;
485 final var singleRecipients = recipients.stream()
486 .filter(r -> r instanceof RecipientIdentifier.Single)
487 .map(RecipientIdentifier.Single.class::cast)
488 .map(RecipientIdentifier.Single::getIdentifier)
489 .collect(Collectors.toList());
490 if (singleRecipients.size() > 0) {
491 timestamp = recipientsHandler.apply(singleRecipients);
492 }
493
494 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
495 timestamp = noteToSelfHandler.get();
496 }
497 final var groupRecipients = recipients.stream()
498 .filter(r -> r instanceof RecipientIdentifier.Group)
499 .map(RecipientIdentifier.Group.class::cast)
500 .map(g -> g.groupId)
501 .collect(Collectors.toList());
502 for (final var groupId : groupRecipients) {
503 timestamp = groupHandler.apply(groupId.serialize());
504 }
505 return new SendMessageResults(timestamp, Map.of());
506 }
507
508 private String emptyIfNull(final String string) {
509 return string == null ? "" : string;
510 }
511
512 private <T extends DBusInterface> T getRemoteObject(final DBusPath devicePath, final Class<T> type) {
513 try {
514 return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type);
515 } catch (DBusException e) {
516 throw new AssertionError(e);
517 }
518 }
519 }