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