]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java
Configuration for Dbus and main
[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.Configuration;
11 import org.asamk.signal.manager.api.Device;
12 import org.asamk.signal.manager.api.Group;
13 import org.asamk.signal.manager.api.Identity;
14 import org.asamk.signal.manager.api.Message;
15 import org.asamk.signal.manager.api.RecipientIdentifier;
16 import org.asamk.signal.manager.api.SendGroupMessageResults;
17 import org.asamk.signal.manager.api.SendMessageResults;
18 import org.asamk.signal.manager.api.TypingAction;
19 import org.asamk.signal.manager.api.UpdateGroup;
20 import org.asamk.signal.manager.groups.GroupId;
21 import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
22 import org.asamk.signal.manager.groups.GroupNotFoundException;
23 import org.asamk.signal.manager.groups.GroupPermission;
24 import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
25 import org.asamk.signal.manager.groups.LastGroupAdminException;
26 import org.asamk.signal.manager.groups.NotAGroupMemberException;
27 import org.asamk.signal.manager.storage.recipients.Contact;
28 import org.asamk.signal.manager.storage.recipients.Profile;
29 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
30 import org.freedesktop.dbus.DBusPath;
31 import org.freedesktop.dbus.connections.impl.DBusConnection;
32 import org.freedesktop.dbus.exceptions.DBusException;
33 import org.freedesktop.dbus.interfaces.DBusInterface;
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
105 @Override
106 public List<Boolean> getConfiguration() {
107 throw new UnsupportedOperationException();
108 }
109
110 @Override
111 public void updateConfiguration(
112 final Boolean readReceipts,
113 final Boolean unidentifiedDeliveryIndicators,
114 final Boolean typingIndicators,
115 final Boolean linkPreviews
116 )
117 {
118 throw new UnsupportedOperationException();
119 }
120
121 @Override
122 public void setProfile(
123 final String givenName,
124 final String familyName,
125 final String about,
126 final String aboutEmoji,
127 final Optional<File> avatar
128 ) throws IOException {
129 signal.updateProfile(emptyIfNull(givenName),
130 emptyIfNull(familyName),
131 emptyIfNull(about),
132 emptyIfNull(aboutEmoji),
133 avatar == null ? "" : avatar.transform(File::getPath).or(""),
134 avatar != null && !avatar.isPresent());
135 }
136
137 @Override
138 public void unregister() throws IOException {
139 throw new UnsupportedOperationException();
140 }
141
142 @Override
143 public void deleteAccount() throws IOException {
144 throw new UnsupportedOperationException();
145 }
146
147 @Override
148 public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
149 throw new UnsupportedOperationException();
150 }
151
152 @Override
153 public List<Device> getLinkedDevices() throws IOException {
154 final var thisDevice = signal.getThisDevice();
155 return signal.listDevices().stream().map(d -> {
156 final var device = getRemoteObject(d.getObjectPath(),
157 Signal.Device.class).GetAll("org.asamk.Signal.Device");
158 return new Device((long) device.get("Id").getValue(),
159 (String) device.get("Name").getValue(),
160 (long) device.get("Created").getValue(),
161 (long) device.get("LastSeen").getValue(),
162 thisDevice.equals(d.getObjectPath()));
163 }).collect(Collectors.toList());
164 }
165
166 @Override
167 public void removeLinkedDevices(final long deviceId) throws IOException {
168 final var devicePath = signal.getDevice(deviceId);
169 getRemoteObject(devicePath, Signal.Device.class).removeDevice();
170 }
171
172 @Override
173 public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException {
174 signal.addDevice(linkUri.toString());
175 }
176
177 @Override
178 public void setRegistrationLockPin(final Optional<String> pin) throws IOException, UnauthenticatedResponseException {
179 if (pin.isPresent()) {
180 signal.setPin(pin.get());
181 } else {
182 signal.removePin();
183 }
184 }
185
186 @Override
187 public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
188 throw new UnsupportedOperationException();
189 }
190
191 @Override
192 public List<Group> getGroups() {
193 final var groups = signal.listGroups();
194 return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList());
195 }
196
197 @Override
198 public SendGroupMessageResults quitGroup(
199 final GroupId groupId, final Set<RecipientIdentifier.Single> groupAdmins
200 ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
201 if (groupAdmins.size() > 0) {
202 throw new UnsupportedOperationException();
203 }
204 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
205 group.quitGroup();
206 return new SendGroupMessageResults(0, List.of());
207 }
208
209 @Override
210 public void deleteGroup(final GroupId groupId) throws IOException {
211 throw new UnsupportedOperationException();
212 }
213
214 @Override
215 public Pair<GroupId, SendGroupMessageResults> createGroup(
216 final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
217 ) throws IOException, AttachmentInvalidException {
218 final var newGroupId = signal.createGroup(emptyIfNull(name),
219 members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
220 avatarFile == null ? "" : avatarFile.getPath());
221 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
222 }
223
224 @Override
225 public SendGroupMessageResults updateGroup(
226 final GroupId groupId, final UpdateGroup updateGroup
227 ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
228 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
229 if (updateGroup.getName() != null) {
230 group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
231 }
232 if (updateGroup.getDescription() != null) {
233 group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
234 }
235 if (updateGroup.getAvatarFile() != null) {
236 group.Set("org.asamk.Signal.Group",
237 "Avatar",
238 updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath());
239 }
240 if (updateGroup.getExpirationTimer() != null) {
241 group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
242 }
243 if (updateGroup.getAddMemberPermission() != null) {
244 group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
245 }
246 if (updateGroup.getEditDetailsPermission() != null) {
247 group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
248 }
249 if (updateGroup.getIsAnnouncementGroup() != null) {
250 group.Set("org.asamk.Signal.Group",
251 "PermissionSendMessage",
252 updateGroup.getIsAnnouncementGroup()
253 ? GroupPermission.ONLY_ADMINS.name()
254 : GroupPermission.EVERY_MEMBER.name());
255 }
256 if (updateGroup.getMembers() != null) {
257 group.addMembers(updateGroup.getMembers()
258 .stream()
259 .map(RecipientIdentifier.Single::getIdentifier)
260 .collect(Collectors.toList()));
261 }
262 if (updateGroup.getRemoveMembers() != null) {
263 group.removeMembers(updateGroup.getRemoveMembers()
264 .stream()
265 .map(RecipientIdentifier.Single::getIdentifier)
266 .collect(Collectors.toList()));
267 }
268 if (updateGroup.getAdmins() != null) {
269 group.addAdmins(updateGroup.getAdmins()
270 .stream()
271 .map(RecipientIdentifier.Single::getIdentifier)
272 .collect(Collectors.toList()));
273 }
274 if (updateGroup.getRemoveAdmins() != null) {
275 group.removeAdmins(updateGroup.getRemoveAdmins()
276 .stream()
277 .map(RecipientIdentifier.Single::getIdentifier)
278 .collect(Collectors.toList()));
279 }
280 if (updateGroup.isResetGroupLink()) {
281 group.resetLink();
282 }
283 if (updateGroup.getGroupLinkState() != null) {
284 switch (updateGroup.getGroupLinkState()) {
285 case DISABLED:
286 group.disableLink();
287 break;
288 case ENABLED:
289 group.enableLink(false);
290 break;
291 case ENABLED_WITH_APPROVAL:
292 group.enableLink(true);
293 break;
294 }
295 }
296 return new SendGroupMessageResults(0, List.of());
297 }
298
299 @Override
300 public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException {
301 final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
302 return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
303 }
304
305 @Override
306 public void sendTypingMessage(
307 final TypingAction action, final Set<RecipientIdentifier> recipients
308 ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
309 for (final var recipient : recipients) {
310 if (recipient instanceof RecipientIdentifier.Single) {
311 signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(),
312 action == TypingAction.STOP);
313 } else if (recipient instanceof RecipientIdentifier.Group) {
314 throw new UnsupportedOperationException();
315 }
316 }
317 }
318
319 @Override
320 public void sendReadReceipt(
321 final RecipientIdentifier.Single sender, final List<Long> messageIds
322 ) throws IOException, UntrustedIdentityException {
323 signal.sendReadReceipt(sender.getIdentifier(), messageIds);
324 }
325
326 @Override
327 public void sendViewedReceipt(
328 final RecipientIdentifier.Single sender, final List<Long> messageIds
329 ) throws IOException, UntrustedIdentityException {
330 throw new UnsupportedOperationException();
331 }
332
333 @Override
334 public SendMessageResults sendMessage(
335 final Message message, final Set<RecipientIdentifier> recipients
336 ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
337 return handleMessage(recipients,
338 numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers),
339 () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()),
340 groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId));
341 }
342
343 @Override
344 public SendMessageResults sendRemoteDeleteMessage(
345 final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
346 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
347 return handleMessage(recipients,
348 numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
349 () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
350 groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
351 }
352
353 @Override
354 public SendMessageResults sendMessageReaction(
355 final String emoji,
356 final boolean remove,
357 final RecipientIdentifier.Single targetAuthor,
358 final long targetSentTimestamp,
359 final Set<RecipientIdentifier> recipients
360 ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
361 return handleMessage(recipients,
362 numbers -> signal.sendMessageReaction(emoji,
363 remove,
364 targetAuthor.getIdentifier(),
365 targetSentTimestamp,
366 numbers),
367 () -> signal.sendMessageReaction(emoji,
368 remove,
369 targetAuthor.getIdentifier(),
370 targetSentTimestamp,
371 signal.getSelfNumber()),
372 groupId -> signal.sendGroupMessageReaction(emoji,
373 remove,
374 targetAuthor.getIdentifier(),
375 targetSentTimestamp,
376 groupId));
377 }
378
379 @Override
380 public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
381 signal.sendEndSessionMessage(recipients.stream()
382 .map(RecipientIdentifier.Single::getIdentifier)
383 .collect(Collectors.toList()));
384 return new SendMessageResults(0, Map.of());
385 }
386
387 @Override
388 public void setContactName(
389 final RecipientIdentifier.Single recipient, final String name
390 ) throws NotMasterDeviceException, UnregisteredUserException {
391 signal.setContactName(recipient.getIdentifier(), name);
392 }
393
394 @Override
395 public void setContactBlocked(
396 final RecipientIdentifier.Single recipient, final boolean blocked
397 ) throws NotMasterDeviceException, IOException {
398 signal.setContactBlocked(recipient.getIdentifier(), blocked);
399 }
400
401 @Override
402 public void setGroupBlocked(
403 final GroupId groupId, final boolean blocked
404 ) throws GroupNotFoundException, IOException {
405 setGroupProperty(groupId, "IsBlocked", blocked);
406 }
407
408 private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
409 final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
410 group.Set("org.asamk.Signal.Group", propertyName, blocked);
411 }
412
413 @Override
414 public void setExpirationTimer(
415 final RecipientIdentifier.Single recipient, final int messageExpirationTimer
416 ) throws IOException {
417 signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
418 }
419
420 @Override
421 public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
422 try {
423 return new URI(signal.uploadStickerPack(path.getPath()));
424 } catch (URISyntaxException e) {
425 throw new AssertionError(e);
426 }
427 }
428
429 @Override
430 public void requestAllSyncData() throws IOException {
431 signal.sendSyncRequest();
432 }
433
434 @Override
435 public void receiveMessages(
436 final long timeout,
437 final TimeUnit unit,
438 final boolean returnOnTimeout,
439 final boolean ignoreAttachments,
440 final ReceiveMessageHandler handler
441 ) throws IOException {
442 throw new UnsupportedOperationException();
443 }
444
445 @Override
446 public boolean hasCaughtUpWithOldMessages() {
447 throw new UnsupportedOperationException();
448 }
449
450 @Override
451 public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
452 return signal.isContactBlocked(recipient.getIdentifier());
453 }
454
455 @Override
456 public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) {
457 throw new UnsupportedOperationException();
458 }
459
460 @Override
461 public void sendContacts() throws IOException {
462 signal.sendContacts();
463 }
464
465 @Override
466 public List<Pair<RecipientAddress, Contact>> getContacts() {
467 throw new UnsupportedOperationException();
468 }
469
470 @Override
471 public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
472 return signal.getContactName(recipient.getIdentifier());
473 }
474
475 @Override
476 public Group getGroup(final GroupId groupId) {
477 final var groupPath = signal.getGroup(groupId.serialize());
478 return getGroup(groupPath);
479 }
480
481 @SuppressWarnings("unchecked")
482 private Group getGroup(final DBusPath groupPath) {
483 final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
484 final var id = (byte[]) group.get("Id").getValue();
485 try {
486 return new Group(GroupId.unknownVersion(id),
487 (String) group.get("Name").getValue(),
488 (String) group.get("Description").getValue(),
489 GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
490 ((List<String>) group.get("Members").getValue()).stream()
491 .map(m -> new RecipientAddress(null, m))
492 .collect(Collectors.toSet()),
493 ((List<String>) group.get("PendingMembers").getValue()).stream()
494 .map(m -> new RecipientAddress(null, m))
495 .collect(Collectors.toSet()),
496 ((List<String>) group.get("RequestingMembers").getValue()).stream()
497 .map(m -> new RecipientAddress(null, m))
498 .collect(Collectors.toSet()),
499 ((List<String>) group.get("Admins").getValue()).stream()
500 .map(m -> new RecipientAddress(null, m))
501 .collect(Collectors.toSet()),
502 (boolean) group.get("IsBlocked").getValue(),
503 (int) group.get("MessageExpirationTimer").getValue(),
504 GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
505 GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
506 GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
507 (boolean) group.get("IsMember").getValue(),
508 (boolean) group.get("IsAdmin").getValue());
509 } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
510 throw new AssertionError(e);
511 }
512 }
513
514 @Override
515 public List<Identity> getIdentities() {
516 throw new UnsupportedOperationException();
517 }
518
519 @Override
520 public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
521 throw new UnsupportedOperationException();
522 }
523
524 @Override
525 public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
526 throw new UnsupportedOperationException();
527 }
528
529 @Override
530 public boolean trustIdentityVerifiedSafetyNumber(
531 final RecipientIdentifier.Single recipient, final String safetyNumber
532 ) {
533 throw new UnsupportedOperationException();
534 }
535
536 @Override
537 public boolean trustIdentityVerifiedSafetyNumber(
538 final RecipientIdentifier.Single recipient, final byte[] safetyNumber
539 ) {
540 throw new UnsupportedOperationException();
541 }
542
543 @Override
544 public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
545 throw new UnsupportedOperationException();
546 }
547
548 @Override
549 public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
550 return address;
551 }
552
553 @Override
554 public void close() throws IOException {
555 }
556
557 private SendMessageResults handleMessage(
558 Set<RecipientIdentifier> recipients,
559 Function<List<String>, Long> recipientsHandler,
560 Supplier<Long> noteToSelfHandler,
561 Function<byte[], Long> groupHandler
562 ) {
563 long timestamp = 0;
564 final var singleRecipients = recipients.stream()
565 .filter(r -> r instanceof RecipientIdentifier.Single)
566 .map(RecipientIdentifier.Single.class::cast)
567 .map(RecipientIdentifier.Single::getIdentifier)
568 .collect(Collectors.toList());
569 if (singleRecipients.size() > 0) {
570 timestamp = recipientsHandler.apply(singleRecipients);
571 }
572
573 if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
574 timestamp = noteToSelfHandler.get();
575 }
576 final var groupRecipients = recipients.stream()
577 .filter(r -> r instanceof RecipientIdentifier.Group)
578 .map(RecipientIdentifier.Group.class::cast)
579 .map(g -> g.groupId)
580 .collect(Collectors.toList());
581 for (final var groupId : groupRecipients) {
582 timestamp = groupHandler.apply(groupId.serialize());
583 }
584 return new SendMessageResults(timestamp, Map.of());
585 }
586
587 private String emptyIfNull(final String string) {
588 return string == null ? "" : string;
589 }
590
591 private <T extends DBusInterface> T getRemoteObject(final DBusPath devicePath, final Class<T> type) {
592 try {
593 return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type);
594 } catch (DBusException e) {
595 throw new AssertionError(e);
596 }
597 }
598 }