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