package org.asamk.signal.dbus;
import org.asamk.Signal;
-import org.asamk.signal.DbusConfig;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.api.AlreadyReceivingException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.StickerPack;
+import org.asamk.signal.manager.api.StickerPackId;
import org.asamk.signal.manager.api.StickerPackInvalidException;
import org.asamk.signal.manager.api.StickerPackUrl;
+import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup;
import org.asamk.signal.manager.api.UpdateProfile;
import org.asamk.signal.manager.api.UserStatus;
-import org.freedesktop.dbus.DBusMap;
+import org.asamk.signal.manager.api.UsernameLinkUrl;
+import org.asamk.signal.manager.api.UsernameStatus;
import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
+import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.freedesktop.dbus.interfaces.DBusInterface;
import org.freedesktop.dbus.interfaces.DBusSigHandler;
import org.freedesktop.dbus.types.Variant;
private final Set<ReceiveMessageHandler> weakHandlers = new HashSet<>();
private final Set<ReceiveMessageHandler> messageHandlers = new HashSet<>();
private final List<Runnable> closedListeners = new ArrayList<>();
+ private final String busname;
private DBusSigHandler<Signal.MessageReceivedV2> dbusMsgHandler;
private DBusSigHandler<Signal.EditMessageReceived> dbusEditMsgHandler;
private DBusSigHandler<Signal.ReceiptReceivedV2> dbusRcptHandler;
private DBusSigHandler<Signal.SyncMessageReceivedV2> dbusSyncHandler;
- public DbusManagerImpl(final Signal signal, DBusConnection connection) {
+ public DbusManagerImpl(final Signal signal, DBusConnection connection, final String busname) {
this.signal = signal;
this.connection = connection;
+ this.busname = busname;
}
@Override
}
@Override
- public void updateAccountAttributes(final String deviceName) throws IOException {
+ public Map<String, UsernameStatus> getUsernameStatus(final Set<String> usernames) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void updateAccountAttributes(
+ final String deviceName,
+ final Boolean unrestrictedUnidentifiedSender,
+ final Boolean discoverableByNumber,
+ final Boolean numberSharing
+ ) throws IOException {
if (deviceName != null) {
final var devicePath = signal.getThisDevice();
getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName);
+ } else {
+ throw new UnsupportedOperationException();
}
}
}
@Override
- public String setUsername(final String username) throws IOException, InvalidUsernameException {
+ public String getUsername() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public UsernameLinkUrl getUsernameLink() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void setUsername(final String username) throws IOException, InvalidUsernameException {
throw new UnsupportedOperationException();
}
@Override
public void startChangeNumber(
- final String newNumber, final boolean voiceVerification, final String captcha
+ final String newNumber,
+ final boolean voiceVerification,
+ final String captcha
) throws RateLimitException, IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException {
throw new UnsupportedOperationException();
}
@Override
public void finishChangeNumber(
- final String newNumber, final String verificationCode, final String pin
+ final String newNumber,
+ final String verificationCode,
+ final String pin
) throws IncorrectPinException, PinLockedException, IOException {
throw new UnsupportedOperationException();
}
@Override
public SendGroupMessageResults quitGroup(
- final GroupId groupId, final Set<RecipientIdentifier.Single> groupAdmins
+ final GroupId groupId,
+ final Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
if (!groupAdmins.isEmpty()) {
throw new UnsupportedOperationException();
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(
- final String name, final Set<RecipientIdentifier.Single> members, final String avatarFile
+ final String name,
+ final Set<RecipientIdentifier.Single> members,
+ final String avatarFile
) throws IOException, AttachmentInvalidException {
final var newGroupId = signal.createGroup(emptyIfNull(name),
members.stream().map(RecipientIdentifier.Single::getIdentifier).toList(),
@Override
public SendGroupMessageResults updateGroup(
- final GroupId groupId, final UpdateGroup updateGroup
+ final GroupId groupId,
+ final UpdateGroup updateGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
if (updateGroup.getName() != null) {
@Override
public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, InactiveGroupLinkException {
- final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
- return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
+ try {
+ final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
+ return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
+ } catch (DBusExecutionException e) {
+ throw new IOException("Failed to join group: " + e.getMessage() + " (" + e.getClass().getSimpleName() + ")",
+ e);
+ }
}
@Override
public SendMessageResults sendTypingMessage(
- final TypingAction action, final Set<RecipientIdentifier> recipients
+ final TypingAction action,
+ final Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return handleMessage(recipients, numbers -> {
numbers.forEach(n -> signal.sendTyping(n, action == TypingAction.STOP));
}
@Override
- public SendMessageResults sendReadReceipt(
- final RecipientIdentifier.Single sender, final List<Long> messageIds
- ) {
+ public SendMessageResults sendReadReceipt(final RecipientIdentifier.Single sender, final List<Long> messageIds) {
signal.sendReadReceipt(sender.getIdentifier(), messageIds);
return new SendMessageResults(0, Map.of());
}
@Override
- public SendMessageResults sendViewedReceipt(
- final RecipientIdentifier.Single sender, final List<Long> messageIds
- ) {
+ public SendMessageResults sendViewedReceipt(final RecipientIdentifier.Single sender, final List<Long> messageIds) {
signal.sendViewedReceipt(sender.getIdentifier(), messageIds);
return new SendMessageResults(0, Map.of());
}
@Override
public SendMessageResults sendMessage(
- final Message message, final Set<RecipientIdentifier> recipients
+ final Message message,
+ final Set<RecipientIdentifier> recipients,
+ final boolean notifySelf
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return handleMessage(recipients,
numbers -> signal.sendMessage(message.messageText(), message.attachments(), numbers),
@Override
public SendMessageResults sendEditMessage(
- final Message message, final Set<RecipientIdentifier> recipients, final long editTargetTimestamp
+ final Message message,
+ final Set<RecipientIdentifier> recipients,
+ final long editTargetTimestamp
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException, InvalidStickerException {
throw new UnsupportedOperationException();
}
@Override
public SendMessageResults sendRemoteDeleteMessage(
- final long targetSentTimestamp, final Set<RecipientIdentifier> recipients
+ final long targetSentTimestamp,
+ final Set<RecipientIdentifier> recipients
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return handleMessage(recipients,
numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
@Override
public SendMessageResults sendPaymentNotificationMessage(
- final byte[] receipt, final String note, final RecipientIdentifier.Single recipient
+ final byte[] receipt,
+ final String note,
+ final RecipientIdentifier.Single recipient
) throws IOException {
final var timestamp = signal.sendPaymentNotification(receipt, note, recipient.getIdentifier());
return new SendMessageResults(timestamp, Map.of());
return new SendMessageResults(0, Map.of());
}
+ @Override
+ public SendMessageResults sendMessageRequestResponse(
+ final MessageEnvelope.Sync.MessageRequestResponse.Type type,
+ final Set<RecipientIdentifier> recipientIdentifiers
+ ) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void hideRecipient(final RecipientIdentifier.Single recipient) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public void deleteRecipient(final RecipientIdentifier.Single recipient) {
signal.deleteRecipient(recipient.getIdentifier());
@Override
public void setContactName(
- final RecipientIdentifier.Single recipient, final String givenName, final String familyName
+ final RecipientIdentifier.Single recipient,
+ final String givenName,
+ final String familyName,
+ final String nickGivenName,
+ final String nickFamilyName,
+ final String note
) throws NotPrimaryDeviceException {
signal.setContactName(recipient.getIdentifier(), givenName);
}
@Override
public void setContactsBlocked(
- final Collection<RecipientIdentifier.Single> recipients, final boolean blocked
+ final Collection<RecipientIdentifier.Single> recipients,
+ final boolean blocked
) throws NotPrimaryDeviceException, IOException {
for (final var recipient : recipients) {
signal.setContactBlocked(recipient.getIdentifier(), blocked);
@Override
public void setGroupsBlocked(
- final Collection<GroupId> groupIds, final boolean blocked
+ final Collection<GroupId> groupIds,
+ final boolean blocked
) throws GroupNotFoundException, IOException {
for (final var groupId : groupIds) {
setGroupProperty(groupId, "IsBlocked", blocked);
@Override
public void setExpirationTimer(
- final RecipientIdentifier.Single recipient, final int messageExpirationTimer
+ final RecipientIdentifier.Single recipient,
+ final int messageExpirationTimer
) throws IOException {
signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
}
}
}
+ private Thread receiveThread;
+
@Override
public void receiveMessages(
- Optional<Duration> timeout, Optional<Integer> maxMessages, ReceiveMessageHandler handler
- ) throws IOException {
+ Optional<Duration> timeout,
+ Optional<Integer> maxMessages,
+ ReceiveMessageHandler handler
+ ) throws IOException, AlreadyReceivingException {
+ if (receiveThread != null) {
+ throw new AlreadyReceivingException("Already receiving message.");
+ }
+ receiveThread = Thread.currentThread();
+
final var remainingMessages = new AtomicInteger(maxMessages.orElse(-1));
final var lastMessage = new AtomicLong(System.currentTimeMillis());
final var thread = Thread.currentThread();
}
Thread.sleep(sleepTimeRemaining);
} catch (InterruptedException ignored) {
+ break;
}
}
} else {
}
removeReceiveHandler(receiveHandler);
+ receiveThread = null;
+ }
+
+ @Override
+ public void stopReceiveMessages() {
+ if (receiveThread != null) {
+ receiveThread.interrupt();
+ }
}
@Override
return null;
}
return Recipient.newBuilder()
- .withAddress(new RecipientAddress(null, n))
- .withContact(new Contact(contactName, null, null, 0, contactBlocked, false, false))
+ .withAddress(new RecipientAddress(n))
+ .withContact(new Contact(contactName,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ 0,
+ 1,
+ 0,
+ false,
+ contactBlocked,
+ false,
+ false,
+ false,
+ null))
.build();
}).filter(Objects::nonNull).toList();
}
(String) group.get("Description").getValue(),
GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
((List<String>) group.get("Members").getValue()).stream()
- .map(m -> new RecipientAddress(null, m))
+ .map(m -> new RecipientAddress(m))
.collect(Collectors.toSet()),
((List<String>) group.get("PendingMembers").getValue()).stream()
- .map(m -> new RecipientAddress(null, m))
+ .map(m -> new RecipientAddress(m))
.collect(Collectors.toSet()),
((List<String>) group.get("RequestingMembers").getValue()).stream()
- .map(m -> new RecipientAddress(null, m))
+ .map(m -> new RecipientAddress(m))
.collect(Collectors.toSet()),
((List<String>) group.get("Admins").getValue()).stream()
- .map(m -> new RecipientAddress(null, m))
+ .map(m -> new RecipientAddress(m))
.collect(Collectors.toSet()),
((List<String>) group.get("Banned").getValue()).stream()
- .map(m -> new RecipientAddress(null, m))
+ .map(m -> new RecipientAddress(m))
.collect(Collectors.toSet()),
(boolean) group.get("IsBlocked").getValue(),
(int) group.get("MessageExpirationTimer").getValue(),
@Override
public List<Identity> getIdentities() {
- throw new UnsupportedOperationException();
+ final var identities = signal.listIdentities();
+ return identities.stream().map(Signal.StructIdentity::getObjectPath).map(this::getIdentity).toList();
}
@Override
public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
- throw new UnsupportedOperationException();
+ final var path = signal.getIdentity(recipient.getIdentifier());
+ return List.of(getIdentity(path));
+ }
+
+ private Identity getIdentity(final DBusPath identityPath) {
+ final var group = getRemoteObject(identityPath, Signal.Identity.class).GetAll("org.asamk.Signal.Identity");
+ final var aci = (String) group.get("Uuid").getValue();
+ final var number = (String) group.get("Number").getValue();
+ return new Identity(new RecipientAddress(aci, null, number, null),
+ (byte[]) group.get("Fingerprint").getValue(),
+ (String) group.get("SafetyNumber").getValue(),
+ (byte[]) group.get("ScannableSafetyNumber").getValue(),
+ TrustLevel.valueOf((String) group.get("TrustLevel").getValue()),
+ (Long) group.get("AddedDate").getValue());
}
@Override
public boolean trustIdentityVerified(
- final RecipientIdentifier.Single recipient, final IdentityVerificationCode verificationCode
+ final RecipientIdentifier.Single recipient,
+ final IdentityVerificationCode verificationCode
) {
throw new UnsupportedOperationException();
}
private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
try {
- return connection.getRemoteObject(DbusConfig.getBusname(), path.getPath(), type);
+ return connection.getRemoteObject(busname, path.getPath(), type);
} catch (DBusException e) {
throw new AssertionError(e);
}
try {
this.dbusMsgHandler = messageReceived -> {
final var extras = messageReceived.getExtras();
- final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
- messageReceived.getSender())),
+ final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(messageReceived.getSender())),
0,
messageReceived.getTimestamp(),
0,
Optional.empty(),
Optional.empty(),
List.of(),
- List.of(),
+ getMentions(extras),
List.of(),
List.of())),
Optional.empty(),
connection.addSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
this.dbusEditMsgHandler = messageReceived -> {
final var extras = messageReceived.getExtras();
- final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
- messageReceived.getSender())),
+ final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(messageReceived.getSender())),
0,
messageReceived.getTimestamp(),
0,
Optional.empty(),
Optional.empty(),
List.of(),
- List.of(),
+ getMentions(extras),
List.of(),
List.of()))),
Optional.empty(),
case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
default -> MessageEnvelope.Receipt.Type.UNKNOWN;
};
- final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
- receiptReceived.getSender())),
+ final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(receiptReceived.getSender())),
0,
receiptReceived.getTimestamp(),
0,
this.dbusSyncHandler = syncReceived -> {
final var extras = syncReceived.getExtras();
- final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(null,
- syncReceived.getSource())),
+ final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(syncReceived.getSource())),
0,
syncReceived.getTimestamp(),
0,
syncReceived.getTimestamp(),
syncReceived.getDestination().isEmpty()
? Optional.empty()
- : Optional.of(new RecipientAddress(null, syncReceived.getDestination())),
+ : Optional.of(new RecipientAddress(syncReceived.getDestination())),
Set.of(),
Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
syncReceived.getGroupId().length > 0
Optional.empty(),
Optional.empty(),
List.of(),
- List.of(),
+ getMentions(extras),
List.of(),
List.of())),
Optional.empty(),
};
connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
} catch (DBusException e) {
- e.printStackTrace();
+ throw new RuntimeException(e);
}
signal.subscribeReceive();
}
connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
} catch (DBusException e) {
- e.printStackTrace();
+ throw new RuntimeException(e);
}
}
return List.of();
}
- final List<DBusMap<String, Variant<?>>> attachments = getValue(extras, "attachments");
+ final List<Map<String, Variant<?>>> attachments = getValue(extras, "attachments");
return attachments.stream().map(a -> {
final String file = a.containsKey("file") ? getValue(a, "file") : null;
return new MessageEnvelope.Data.Attachment(a.containsKey("remoteId")
}).toList();
}
+ private List<MessageEnvelope.Data.Mention> getMentions(final Map<String, Variant<?>> extras) {
+ if (!extras.containsKey("mentions")) {
+ return List.of();
+ }
+
+ final List<Map<String, Variant<?>>> mentions = getValue(extras, "mentions");
+ return mentions.stream()
+ .map(a -> new MessageEnvelope.Data.Mention(new RecipientAddress(this.<String>getValue(a, "recipient")),
+ getValue(a, "start"),
+ getValue(a, "length")))
+ .toList();
+ }
+
@Override
public InputStream retrieveAttachment(final String id) throws IOException {
throw new UnsupportedOperationException();
}
+ @Override
+ public InputStream retrieveContactAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InputStream retrieveProfileAvatar(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InputStream retrieveGroupAvatar(final GroupId groupId) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public InputStream retrieveSticker(final StickerPackId stickerPackId, final int stickerId) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
@SuppressWarnings("unchecked")
- private <T> T getValue(
- final Map<String, Variant<?>> stringVariantMap, final String field
- ) {
+ private <T> T getValue(final Map<String, Variant<?>> stringVariantMap, final String field) {
return (T) stringVariantMap.get(field).getValue();
}
}