mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-05-29 15:04:16 +00:00
Keep the unidentified socket alive while a JSON-RPC connection is open (including stdio mode) and while a D-Bus object is exported, instead of for the lifetime of the receive loop. The receive loop does not use the unauthenticated socket, so keeping it alive there was semantically wrong. This also covers --receive-mode=manual, where no receive loop runs butclients still send messages.
1347 lines
56 KiB
Java
1347 lines
56 KiB
Java
package org.asamk.signal.dbus;
|
|
|
|
import org.asamk.Signal;
|
|
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.CallInfo;
|
|
import org.asamk.signal.manager.api.CallOffer;
|
|
import org.asamk.signal.manager.api.CaptchaRequiredException;
|
|
import org.asamk.signal.manager.api.Configuration;
|
|
import org.asamk.signal.manager.api.Contact;
|
|
import org.asamk.signal.manager.api.Device;
|
|
import org.asamk.signal.manager.api.DeviceLinkUrl;
|
|
import org.asamk.signal.manager.api.Group;
|
|
import org.asamk.signal.manager.api.GroupId;
|
|
import org.asamk.signal.manager.api.GroupInviteLinkUrl;
|
|
import org.asamk.signal.manager.api.GroupMember;
|
|
import org.asamk.signal.manager.api.GroupNotFoundException;
|
|
import org.asamk.signal.manager.api.GroupPermission;
|
|
import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
|
import org.asamk.signal.manager.api.Identity;
|
|
import org.asamk.signal.manager.api.IdentityVerificationCode;
|
|
import org.asamk.signal.manager.api.InactiveGroupLinkException;
|
|
import org.asamk.signal.manager.api.IncorrectPinException;
|
|
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
|
|
import org.asamk.signal.manager.api.InvalidStickerException;
|
|
import org.asamk.signal.manager.api.InvalidUsernameException;
|
|
import org.asamk.signal.manager.api.LastGroupAdminException;
|
|
import org.asamk.signal.manager.api.Message;
|
|
import org.asamk.signal.manager.api.MessageEnvelope;
|
|
import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
|
|
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
|
import org.asamk.signal.manager.api.NotPrimaryDeviceException;
|
|
import org.asamk.signal.manager.api.Pair;
|
|
import org.asamk.signal.manager.api.PinLockedException;
|
|
import org.asamk.signal.manager.api.RateLimitException;
|
|
import org.asamk.signal.manager.api.ReceiveConfig;
|
|
import org.asamk.signal.manager.api.Recipient;
|
|
import org.asamk.signal.manager.api.RecipientAddress;
|
|
import org.asamk.signal.manager.api.RecipientIdentifier;
|
|
import org.asamk.signal.manager.api.SendGroupMessageResults;
|
|
import org.asamk.signal.manager.api.SendMessageResult;
|
|
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.TurnServer;
|
|
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.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;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.URI;
|
|
import java.net.URISyntaxException;
|
|
import java.time.Duration;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
import java.util.function.Function;
|
|
import java.util.function.Supplier;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
|
|
/**
|
|
* This class implements the Manager interface using the DBus Signal interface, where possible.
|
|
* It's used for the signal-cli dbus client mode (--dbus, --dbus-system)
|
|
*/
|
|
public class DbusManagerImpl implements Manager {
|
|
|
|
private final Signal signal;
|
|
private final DBusConnection connection;
|
|
|
|
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, final String busname) {
|
|
this.signal = signal;
|
|
this.connection = connection;
|
|
this.busname = busname;
|
|
}
|
|
|
|
@Override
|
|
public String getSelfNumber() {
|
|
return signal.getSelfNumber();
|
|
}
|
|
|
|
@Override
|
|
public Map<String, UserStatus> getUserStatus(final Set<String> numbers) throws IOException {
|
|
final var numbersList = new ArrayList<>(numbers);
|
|
final var registered = signal.isRegistered(numbersList);
|
|
|
|
final var result = new HashMap<String, UserStatus>();
|
|
for (var i = 0; i < numbersList.size(); i++) {
|
|
result.put(numbersList.get(i),
|
|
new UserStatus(numbersList.get(i),
|
|
registered.get(i) ? RecipientAddress.UNKNOWN_UUID : null,
|
|
false));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
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 Configuration getConfiguration() {
|
|
final var configuration = getRemoteObject(new DBusPath(signal.getObjectPath() + "/Configuration"),
|
|
Signal.Configuration.class).GetAll("org.asamk.Signal.Configuration");
|
|
return new Configuration(Optional.of((Boolean) configuration.get("ReadReceipts").getValue()),
|
|
Optional.of((Boolean) configuration.get("UnidentifiedDeliveryIndicators").getValue()),
|
|
Optional.of((Boolean) configuration.get("TypingIndicators").getValue()),
|
|
Optional.of((Boolean) configuration.get("LinkPreviews").getValue()));
|
|
}
|
|
|
|
@Override
|
|
public void updateConfiguration(Configuration newConfiguration) {
|
|
final var configuration = getRemoteObject(new DBusPath(signal.getObjectPath() + "/Configuration"),
|
|
Signal.Configuration.class);
|
|
newConfiguration.readReceipts()
|
|
.ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "ReadReceipts", v));
|
|
newConfiguration.unidentifiedDeliveryIndicators()
|
|
.ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration",
|
|
"UnidentifiedDeliveryIndicators",
|
|
v));
|
|
newConfiguration.typingIndicators()
|
|
.ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "TypingIndicators", v));
|
|
newConfiguration.linkPreviews()
|
|
.ifPresent(v -> configuration.Set("org.asamk.Signal.Configuration", "LinkPreviews", v));
|
|
}
|
|
|
|
@Override
|
|
public void updateProfile(UpdateProfile updateProfile) throws IOException {
|
|
signal.updateProfile(emptyIfNull(updateProfile.getGivenName()),
|
|
emptyIfNull(updateProfile.getFamilyName()),
|
|
emptyIfNull(updateProfile.getAbout()),
|
|
emptyIfNull(updateProfile.getAboutEmoji()),
|
|
updateProfile.getAvatar() == null ? "" : updateProfile.getAvatar(),
|
|
updateProfile.isDeleteAvatar());
|
|
}
|
|
|
|
@Override
|
|
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 deleteUsername() throws IOException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public void startChangeNumber(
|
|
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
|
|
) throws IncorrectPinException, PinLockedException, IOException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public void unregister() throws IOException {
|
|
signal.unregister();
|
|
}
|
|
|
|
@Override
|
|
public void deleteAccount() throws IOException {
|
|
signal.deleteAccount();
|
|
}
|
|
|
|
@Override
|
|
public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
|
|
signal.submitRateLimitChallenge(challenge, captcha);
|
|
}
|
|
|
|
@Override
|
|
public List<Device> getLinkedDevices() throws IOException {
|
|
final var thisDevice = signal.getThisDevice();
|
|
return signal.listDevices().stream().map(d -> {
|
|
final var device = getRemoteObject(d.getObjectPath(),
|
|
Signal.Device.class).GetAll("org.asamk.Signal.Device");
|
|
return new Device((Integer) device.get("Id").getValue(),
|
|
(String) device.get("Name").getValue(),
|
|
(long) device.get("Created").getValue(),
|
|
(long) device.get("LastSeen").getValue(),
|
|
thisDevice.equals(d.getObjectPath()));
|
|
}).toList();
|
|
}
|
|
|
|
@Override
|
|
public void updateLinkedDevice(
|
|
final int deviceId,
|
|
final String name
|
|
) throws IOException, NotPrimaryDeviceException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public void removeLinkedDevices(final int deviceId) throws IOException {
|
|
final var devicePath = signal.getDevice(deviceId);
|
|
getRemoteObject(devicePath, Signal.Device.class).removeDevice();
|
|
}
|
|
|
|
@Override
|
|
public void addDeviceLink(final DeviceLinkUrl linkUri) throws IOException, InvalidDeviceLinkException {
|
|
signal.addDevice(linkUri.createDeviceLinkUri().toString());
|
|
}
|
|
|
|
@Override
|
|
public void setRegistrationLockPin(final Optional<String> pin) throws IOException {
|
|
if (pin.isPresent()) {
|
|
signal.setPin(pin.get());
|
|
} else {
|
|
signal.removePin();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public List<Group> getGroups() {
|
|
final var groups = signal.listGroups();
|
|
return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).toList();
|
|
}
|
|
|
|
@Override
|
|
public List<Group> getGroups(final Collection<GroupId> groupIds) {
|
|
final var groups = signal.listGroups();
|
|
return groups.stream()
|
|
.map(Signal.StructGroup::getObjectPath)
|
|
.map(this::getGroup)
|
|
.filter(g -> groupIds.contains(g.groupId()))
|
|
.toList();
|
|
}
|
|
|
|
@Override
|
|
public SendGroupMessageResults quitGroup(
|
|
final GroupId groupId,
|
|
final Set<RecipientIdentifier.Single> groupAdmins
|
|
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
|
|
if (!groupAdmins.isEmpty()) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
|
|
try {
|
|
group.quitGroup();
|
|
} catch (Signal.Error.GroupNotFound e) {
|
|
throw new GroupNotFoundException(groupId);
|
|
} catch (Signal.Error.NotAGroupMember e) {
|
|
throw new NotAGroupMemberException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
|
|
} catch (Signal.Error.LastGroupAdmin e) {
|
|
throw new LastGroupAdminException(groupId, group.Get("org.asamk.Signal.Group", "Name"));
|
|
}
|
|
return new SendGroupMessageResults(0, List.of());
|
|
}
|
|
|
|
@Override
|
|
public void deleteGroup(final GroupId groupId) throws IOException {
|
|
final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
|
|
group.deleteGroup();
|
|
}
|
|
|
|
@Override
|
|
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
|
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(),
|
|
avatarFile == null ? "" : avatarFile);
|
|
return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
|
|
}
|
|
|
|
@Override
|
|
public SendGroupMessageResults 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) {
|
|
group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName());
|
|
}
|
|
if (updateGroup.getDescription() != null) {
|
|
group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription());
|
|
}
|
|
if (updateGroup.getAvatarFile() != null) {
|
|
group.Set("org.asamk.Signal.Group",
|
|
"Avatar",
|
|
updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile());
|
|
}
|
|
if (updateGroup.getExpirationTimer() != null) {
|
|
group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer());
|
|
}
|
|
if (updateGroup.getAddMemberPermission() != null) {
|
|
group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name());
|
|
}
|
|
if (updateGroup.getEditDetailsPermission() != null) {
|
|
group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name());
|
|
}
|
|
if (updateGroup.getIsAnnouncementGroup() != null) {
|
|
group.Set("org.asamk.Signal.Group",
|
|
"PermissionSendMessage",
|
|
updateGroup.getIsAnnouncementGroup()
|
|
? GroupPermission.ONLY_ADMINS.name()
|
|
: GroupPermission.EVERY_MEMBER.name());
|
|
}
|
|
if (updateGroup.getMembers() != null) {
|
|
group.addMembers(updateGroup.getMembers().stream().map(RecipientIdentifier.Single::getIdentifier).toList());
|
|
}
|
|
if (updateGroup.getRemoveMembers() != null) {
|
|
group.removeMembers(updateGroup.getRemoveMembers()
|
|
.stream()
|
|
.map(RecipientIdentifier.Single::getIdentifier)
|
|
.toList());
|
|
}
|
|
if (updateGroup.getAdmins() != null) {
|
|
group.addAdmins(updateGroup.getAdmins().stream().map(RecipientIdentifier.Single::getIdentifier).toList());
|
|
}
|
|
if (updateGroup.getRemoveAdmins() != null) {
|
|
group.removeAdmins(updateGroup.getRemoveAdmins()
|
|
.stream()
|
|
.map(RecipientIdentifier.Single::getIdentifier)
|
|
.toList());
|
|
}
|
|
if (updateGroup.isResetGroupLink()) {
|
|
group.resetLink();
|
|
}
|
|
if (updateGroup.getGroupLinkState() != null) {
|
|
switch (updateGroup.getGroupLinkState()) {
|
|
case DISABLED -> group.disableLink();
|
|
case ENABLED -> group.enableLink(false);
|
|
case ENABLED_WITH_APPROVAL -> group.enableLink(true);
|
|
}
|
|
}
|
|
return new SendGroupMessageResults(0, List.of());
|
|
}
|
|
|
|
@Override
|
|
public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, InactiveGroupLinkException {
|
|
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
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
|
return handleMessage(recipients, numbers -> {
|
|
numbers.forEach(n -> signal.sendTyping(n, action == TypingAction.STOP));
|
|
return 0L;
|
|
}, () -> {
|
|
signal.sendTyping(signal.getSelfNumber(), action == TypingAction.STOP);
|
|
return 0L;
|
|
}, groupId -> {
|
|
signal.sendGroupTyping(groupId, action == TypingAction.STOP);
|
|
return 0L;
|
|
});
|
|
}
|
|
|
|
@Override
|
|
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) {
|
|
signal.sendViewedReceipt(sender.getIdentifier(), messageIds);
|
|
return new SendMessageResults(0, Map.of());
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendMessage(
|
|
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),
|
|
() -> signal.sendNoteToSelfMessage(message.messageText(), message.attachments()),
|
|
groupId -> signal.sendGroupMessage(message.messageText(), message.attachments(), groupId));
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendEditMessage(
|
|
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
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
|
return handleMessage(recipients,
|
|
numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers),
|
|
() -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()),
|
|
groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId));
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendMessageReaction(
|
|
final String emoji,
|
|
final boolean remove,
|
|
final RecipientIdentifier.Single targetAuthor,
|
|
final long targetSentTimestamp,
|
|
final Set<RecipientIdentifier> recipients,
|
|
final boolean notifySelf,
|
|
final boolean isStory
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
|
|
return handleMessage(recipients,
|
|
numbers -> signal.sendMessageReaction(emoji,
|
|
remove,
|
|
targetAuthor.getIdentifier(),
|
|
targetSentTimestamp,
|
|
numbers),
|
|
() -> signal.sendMessageReaction(emoji,
|
|
remove,
|
|
targetAuthor.getIdentifier(),
|
|
targetSentTimestamp,
|
|
signal.getSelfNumber()),
|
|
groupId -> signal.sendGroupMessageReaction(emoji,
|
|
remove,
|
|
targetAuthor.getIdentifier(),
|
|
targetSentTimestamp,
|
|
groupId));
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendAdminDelete(
|
|
final RecipientIdentifier.Single targetAuthor,
|
|
final long targetSentTimestamp,
|
|
final Set<RecipientIdentifier.Group> recipients,
|
|
final boolean notifySelf,
|
|
final boolean isStory
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendPinMessage(
|
|
final int pinDuration,
|
|
final RecipientIdentifier.Single targetAuthor,
|
|
final long targetSentTimestamp,
|
|
final Set<RecipientIdentifier> recipients,
|
|
final boolean notifySelf,
|
|
final boolean isStory
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendUnpinMessage(
|
|
final RecipientIdentifier.Single targetAuthor,
|
|
final long targetSentTimestamp,
|
|
final Set<RecipientIdentifier> recipients,
|
|
final boolean notifySelf,
|
|
final boolean isStory
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendPaymentNotificationMessage(
|
|
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());
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
|
|
signal.sendEndSessionMessage(recipients.stream().map(RecipientIdentifier.Single::getIdentifier).toList());
|
|
return new SendMessageResults(0, Map.of());
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendMessageRequestResponse(
|
|
final MessageEnvelope.Sync.MessageRequestResponse.Type type,
|
|
final Set<RecipientIdentifier> recipientIdentifiers
|
|
) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendPollCreateMessage(
|
|
final String question,
|
|
final boolean allowMultiple,
|
|
final List<String> options,
|
|
final Set<RecipientIdentifier> recipients,
|
|
final boolean notifySelf
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendPollVoteMessage(
|
|
final RecipientIdentifier.Single targetAuthor,
|
|
final long targetSentTimestamp,
|
|
final List<Integer> optionIndexes,
|
|
final int voteCount,
|
|
final Set<RecipientIdentifier> recipients,
|
|
final boolean notifySelf
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResults sendPollTerminateMessage(
|
|
final long targetSentTimestamp,
|
|
final Set<RecipientIdentifier> recipients,
|
|
final boolean notifySelf
|
|
) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException, UnregisteredRecipientException {
|
|
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 deleteContact(final RecipientIdentifier.Single recipient) {
|
|
signal.deleteContact(recipient.getIdentifier());
|
|
}
|
|
|
|
@Override
|
|
public void setContactName(
|
|
final RecipientIdentifier.Single recipient,
|
|
final String givenName,
|
|
final String familyName,
|
|
final String nickGivenName,
|
|
final String nickFamilyName,
|
|
final String note
|
|
) {
|
|
signal.setContactName(recipient.getIdentifier(), givenName);
|
|
}
|
|
|
|
@Override
|
|
public void setContactsBlocked(
|
|
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
|
|
) throws GroupNotFoundException, IOException {
|
|
for (final var groupId : groupIds) {
|
|
setGroupProperty(groupId, "IsBlocked", blocked);
|
|
}
|
|
}
|
|
|
|
private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) {
|
|
final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class);
|
|
group.Set("org.asamk.Signal.Group", propertyName, blocked);
|
|
}
|
|
|
|
@Override
|
|
public void setExpirationTimer(
|
|
final RecipientIdentifier.Single recipient,
|
|
final int messageExpirationTimer
|
|
) throws IOException {
|
|
signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
|
|
}
|
|
|
|
@Override
|
|
public StickerPackUrl uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
|
|
try {
|
|
return StickerPackUrl.fromUri(new URI(signal.uploadStickerPack(path.getPath())));
|
|
} catch (URISyntaxException | StickerPackUrl.InvalidStickerPackLinkException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void installStickerPack(final StickerPackUrl url) throws IOException {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public List<StickerPack> getStickerPacks() {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public void requestAllSyncData() throws IOException {
|
|
signal.sendSyncRequest();
|
|
}
|
|
|
|
@Override
|
|
public void addReceiveHandler(final ReceiveMessageHandler handler, final boolean isWeakListener) {
|
|
synchronized (messageHandlers) {
|
|
if (isWeakListener) {
|
|
weakHandlers.add(handler);
|
|
} else {
|
|
if (messageHandlers.isEmpty()) {
|
|
installMessageHandlers();
|
|
}
|
|
messageHandlers.add(handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void removeReceiveHandler(final ReceiveMessageHandler handler) {
|
|
synchronized (messageHandlers) {
|
|
weakHandlers.remove(handler);
|
|
messageHandlers.remove(handler);
|
|
if (messageHandlers.isEmpty()) {
|
|
uninstallMessageHandlers();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean isReceiving() {
|
|
synchronized (messageHandlers) {
|
|
return !messageHandlers.isEmpty();
|
|
}
|
|
}
|
|
|
|
private Thread receiveThread;
|
|
|
|
@Override
|
|
public void receiveMessages(
|
|
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();
|
|
|
|
final ReceiveMessageHandler receiveHandler = (envelope, e) -> {
|
|
lastMessage.set(System.currentTimeMillis());
|
|
handler.handleMessage(envelope, e);
|
|
if (remainingMessages.get() > 0) {
|
|
if (remainingMessages.decrementAndGet() <= 0) {
|
|
remainingMessages.set(0);
|
|
thread.interrupt();
|
|
}
|
|
}
|
|
};
|
|
addReceiveHandler(receiveHandler);
|
|
if (timeout.isPresent()) {
|
|
while (remainingMessages.get() != 0) {
|
|
try {
|
|
final var passedTime = System.currentTimeMillis() - lastMessage.get();
|
|
final var sleepTimeRemaining = timeout.get().toMillis() - passedTime;
|
|
if (sleepTimeRemaining < 0) {
|
|
break;
|
|
}
|
|
Thread.sleep(sleepTimeRemaining);
|
|
} catch (InterruptedException ignored) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
try {
|
|
synchronized (this) {
|
|
this.wait();
|
|
}
|
|
} catch (InterruptedException ignored) {
|
|
}
|
|
}
|
|
|
|
removeReceiveHandler(receiveHandler);
|
|
receiveThread = null;
|
|
}
|
|
|
|
@Override
|
|
public void stopReceiveMessages() {
|
|
if (receiveThread != null) {
|
|
receiveThread.interrupt();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setReceiveConfig(final ReceiveConfig receiveConfig) {
|
|
}
|
|
|
|
@Override
|
|
public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
|
|
return signal.isContactBlocked(recipient.getIdentifier());
|
|
}
|
|
|
|
@Override
|
|
public void sendContacts() throws IOException {
|
|
signal.sendContacts();
|
|
}
|
|
|
|
@Override
|
|
public List<Recipient> getRecipients(
|
|
final boolean onlyContacts,
|
|
final Optional<Boolean> blocked,
|
|
final Collection<RecipientIdentifier.Single> addresses,
|
|
final Optional<String> name
|
|
) {
|
|
final var numbers = addresses.stream()
|
|
.filter(s -> s instanceof RecipientIdentifier.Number)
|
|
.map(s -> ((RecipientIdentifier.Number) s).number())
|
|
.collect(Collectors.toSet());
|
|
return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
|
|
final var contactBlocked = signal.isContactBlocked(n);
|
|
if (blocked.isPresent() && blocked.get() != contactBlocked) {
|
|
return null;
|
|
}
|
|
final var contactName = signal.getContactName(n);
|
|
if (onlyContacts && contactName.isEmpty()) {
|
|
return null;
|
|
}
|
|
if (name.isPresent() && !name.get().equals(contactName)) {
|
|
return null;
|
|
}
|
|
return Recipient.newBuilder()
|
|
.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();
|
|
}
|
|
|
|
@Override
|
|
public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
|
|
return signal.getContactName(recipient.getIdentifier());
|
|
}
|
|
|
|
@Override
|
|
public Group getGroup(final GroupId groupId) {
|
|
final var groupPath = signal.getGroup(groupId.serialize());
|
|
return getGroup(groupPath);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private Group getGroup(final DBusPath groupPath) {
|
|
final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group");
|
|
final var id = (byte[]) group.get("Id").getValue();
|
|
try {
|
|
final var admins = new HashSet<>(((List<String>) group.get("Admins").getValue()));
|
|
return new Group(GroupId.unknownVersion(id),
|
|
(String) group.get("Name").getValue(),
|
|
(String) group.get("Description").getValue(),
|
|
GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()),
|
|
((List<String>) group.get("Members").getValue()).stream()
|
|
.map(m -> new GroupMember(new RecipientAddress(m), admins.contains(m), null, null))
|
|
.collect(Collectors.toSet()),
|
|
((List<String>) group.get("PendingMembers").getValue()).stream()
|
|
.map(RecipientAddress::new)
|
|
.collect(Collectors.toSet()),
|
|
((List<String>) group.get("RequestingMembers").getValue()).stream()
|
|
.map(RecipientAddress::new)
|
|
.collect(Collectors.toSet()),
|
|
((List<String>) group.get("Banned").getValue()).stream()
|
|
.map(RecipientAddress::new)
|
|
.collect(Collectors.toSet()),
|
|
(boolean) group.get("IsBlocked").getValue(),
|
|
(int) group.get("MessageExpirationTimer").getValue(),
|
|
GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()),
|
|
GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()),
|
|
GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()),
|
|
(boolean) group.get("IsMember").getValue(),
|
|
(boolean) group.get("IsAdmin").getValue());
|
|
} catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public List<Identity> getIdentities() {
|
|
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) {
|
|
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
|
|
) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
public void addAddressChangedListener(final Runnable listener) {
|
|
}
|
|
|
|
@Override
|
|
public void addClosedListener(final Runnable listener) {
|
|
synchronized (closedListeners) {
|
|
closedListeners.add(listener);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addUnidentifiedKeepAlive(final String token) {
|
|
}
|
|
|
|
@Override
|
|
public void removeUnidentifiedKeepAlive(final String token) {
|
|
}
|
|
|
|
@Override
|
|
public void addCallEventListener(final CallEventListener listener) {
|
|
// Not supported over DBus
|
|
}
|
|
|
|
@Override
|
|
public void removeCallEventListener(final CallEventListener listener) {
|
|
// Not supported over DBus
|
|
}
|
|
|
|
// --- Voice call methods (not supported over DBus) ---
|
|
|
|
@Override
|
|
public CallInfo startCall(final RecipientIdentifier.Single recipient) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public CallInfo acceptCall(final long callId) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public void hangupCall(final long callId) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public SendMessageResult rejectCall(final long callId) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public java.util.List<CallInfo> listActiveCalls() {
|
|
return java.util.List.of();
|
|
}
|
|
|
|
@Override
|
|
public void sendCallOffer(final RecipientIdentifier.Single recipient, final CallOffer offer) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public void sendCallAnswer(
|
|
final RecipientIdentifier.Single recipient,
|
|
final long callId,
|
|
final byte[] answerOpaque
|
|
) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public void sendIceUpdate(
|
|
final RecipientIdentifier.Single recipient,
|
|
final long callId,
|
|
final java.util.List<byte[]> iceCandidates
|
|
) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public void sendHangup(
|
|
final RecipientIdentifier.Single recipient,
|
|
final long callId,
|
|
final MessageEnvelope.Call.Hangup.Type type
|
|
) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public void sendBusy(final RecipientIdentifier.Single recipient, final long callId) {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public java.util.List<TurnServer> getTurnServerInfo() {
|
|
throw new UnsupportedOperationException("Voice calls are not supported over DBus");
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
synchronized (this) {
|
|
this.notify();
|
|
}
|
|
synchronized (messageHandlers) {
|
|
if (!messageHandlers.isEmpty()) {
|
|
uninstallMessageHandlers();
|
|
}
|
|
weakHandlers.clear();
|
|
messageHandlers.clear();
|
|
}
|
|
synchronized (closedListeners) {
|
|
closedListeners.forEach(Runnable::run);
|
|
closedListeners.clear();
|
|
}
|
|
}
|
|
|
|
private SendMessageResults handleMessage(
|
|
Set<RecipientIdentifier> recipients,
|
|
Function<List<String>, Long> recipientsHandler,
|
|
Supplier<Long> noteToSelfHandler,
|
|
Function<byte[], Long> groupHandler
|
|
) {
|
|
long timestamp = 0;
|
|
final var singleRecipients = recipients.stream()
|
|
.filter(r -> r instanceof RecipientIdentifier.Single)
|
|
.map(RecipientIdentifier.Single.class::cast)
|
|
.map(RecipientIdentifier.Single::getIdentifier)
|
|
.toList();
|
|
if (!singleRecipients.isEmpty()) {
|
|
timestamp = recipientsHandler.apply(singleRecipients);
|
|
}
|
|
|
|
if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) {
|
|
timestamp = noteToSelfHandler.get();
|
|
}
|
|
final var groupRecipients = recipients.stream()
|
|
.filter(r -> r instanceof RecipientIdentifier.Group)
|
|
.map(RecipientIdentifier.Group.class::cast)
|
|
.map(RecipientIdentifier.Group::groupId)
|
|
.toList();
|
|
for (final var groupId : groupRecipients) {
|
|
timestamp = groupHandler.apply(groupId.serialize());
|
|
}
|
|
return new SendMessageResults(timestamp, Map.of());
|
|
}
|
|
|
|
private String emptyIfNull(final String string) {
|
|
return string == null ? "" : string;
|
|
}
|
|
|
|
private <T extends DBusInterface> T getRemoteObject(final DBusPath path, final Class<T> type) {
|
|
try {
|
|
return connection.getRemoteObject(busname, path.getPath(), type);
|
|
} catch (DBusException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
private void installMessageHandlers() {
|
|
try {
|
|
this.dbusMsgHandler = messageReceived -> {
|
|
final var extras = messageReceived.getExtras();
|
|
final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(messageReceived.getSender())),
|
|
0,
|
|
messageReceived.getTimestamp(),
|
|
0,
|
|
0,
|
|
false,
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.of(new MessageEnvelope.Data(messageReceived.getTimestamp(),
|
|
messageReceived.getGroupId().length > 0
|
|
? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
|
|
messageReceived.getGroupId()), false, 0))
|
|
: Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.of(messageReceived.getMessage()),
|
|
0,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
getAttachments(extras),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
List.of(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
getMentions(extras),
|
|
List.of(),
|
|
List.of(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty())),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty());
|
|
notifyMessageHandlers(envelope);
|
|
};
|
|
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(messageReceived.getSender())),
|
|
0,
|
|
messageReceived.getTimestamp(),
|
|
0,
|
|
0,
|
|
false,
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.of(new MessageEnvelope.Edit(messageReceived.getTargetSentTimestamp(),
|
|
new MessageEnvelope.Data(messageReceived.getTimestamp(),
|
|
messageReceived.getGroupId().length > 0
|
|
? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
|
|
messageReceived.getGroupId()), false, 0))
|
|
: Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.of(messageReceived.getMessage()),
|
|
0,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
getAttachments(extras),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
List.of(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
getMentions(extras),
|
|
List.of(),
|
|
List.of(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty()))),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty());
|
|
notifyMessageHandlers(envelope);
|
|
};
|
|
connection.addSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler);
|
|
|
|
this.dbusRcptHandler = receiptReceived -> {
|
|
final var type = switch (receiptReceived.getReceiptType()) {
|
|
case "read" -> MessageEnvelope.Receipt.Type.READ;
|
|
case "viewed" -> MessageEnvelope.Receipt.Type.VIEWED;
|
|
case "delivery" -> MessageEnvelope.Receipt.Type.DELIVERY;
|
|
default -> MessageEnvelope.Receipt.Type.UNKNOWN;
|
|
};
|
|
final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(receiptReceived.getSender())),
|
|
0,
|
|
receiptReceived.getTimestamp(),
|
|
0,
|
|
0,
|
|
false,
|
|
Optional.of(new MessageEnvelope.Receipt(receiptReceived.getTimestamp(),
|
|
type,
|
|
List.of(receiptReceived.getTimestamp()))),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty());
|
|
notifyMessageHandlers(envelope);
|
|
};
|
|
connection.addSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
|
|
|
|
this.dbusSyncHandler = syncReceived -> {
|
|
final var extras = syncReceived.getExtras();
|
|
final var envelope = new MessageEnvelope(Optional.of(new RecipientAddress(syncReceived.getSource())),
|
|
0,
|
|
syncReceived.getTimestamp(),
|
|
0,
|
|
0,
|
|
false,
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.of(new MessageEnvelope.Sync(Optional.of(new MessageEnvelope.Sync.Sent(syncReceived.getTimestamp(),
|
|
syncReceived.getTimestamp(),
|
|
syncReceived.getDestination().isEmpty()
|
|
? Optional.empty()
|
|
: Optional.of(new RecipientAddress(syncReceived.getDestination())),
|
|
Set.of(),
|
|
Optional.of(new MessageEnvelope.Data(syncReceived.getTimestamp(),
|
|
syncReceived.getGroupId().length > 0
|
|
? Optional.of(new MessageEnvelope.Data.GroupContext(GroupId.unknownVersion(
|
|
syncReceived.getGroupId()), false, 0))
|
|
: Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.of(syncReceived.getMessage()),
|
|
0,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
getAttachments(extras),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
List.of(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
getMentions(extras),
|
|
List.of(),
|
|
List.of(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty())),
|
|
Optional.empty(),
|
|
Optional.empty())),
|
|
Optional.empty(),
|
|
List.of(),
|
|
List.of(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty())),
|
|
Optional.empty(),
|
|
Optional.empty());
|
|
notifyMessageHandlers(envelope);
|
|
};
|
|
connection.addSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
|
|
} catch (DBusException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
signal.subscribeReceive();
|
|
}
|
|
|
|
private void notifyMessageHandlers(final MessageEnvelope envelope) {
|
|
synchronized (messageHandlers) {
|
|
Stream.concat(messageHandlers.stream(), weakHandlers.stream())
|
|
.forEach(h -> h.handleMessage(envelope, null));
|
|
}
|
|
}
|
|
|
|
private void uninstallMessageHandlers() {
|
|
try {
|
|
signal.unsubscribeReceive();
|
|
connection.removeSigHandler(Signal.MessageReceivedV2.class, signal, this.dbusMsgHandler);
|
|
connection.removeSigHandler(Signal.EditMessageReceived.class, signal, this.dbusEditMsgHandler);
|
|
connection.removeSigHandler(Signal.ReceiptReceivedV2.class, signal, this.dbusRcptHandler);
|
|
connection.removeSigHandler(Signal.SyncMessageReceivedV2.class, signal, this.dbusSyncHandler);
|
|
} catch (DBusException e) {
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private List<MessageEnvelope.Data.Attachment> getAttachments(final Map<String, Variant<?>> extras) {
|
|
if (!extras.containsKey("attachments")) {
|
|
return List.of();
|
|
}
|
|
|
|
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")
|
|
? Optional.of(getValue(a, "remoteId"))
|
|
: Optional.empty(),
|
|
file != null ? Optional.of(new File(file)) : Optional.empty(),
|
|
Optional.empty(),
|
|
getValue(a, "contentType"),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
Optional.empty(),
|
|
getValue(a, "isVoiceNote"),
|
|
getValue(a, "isGif"),
|
|
getValue(a, "isBorderless"));
|
|
}).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) {
|
|
return (T) stringVariantMap.get(field).getValue();
|
|
}
|
|
}
|