Merge branch master into dbus_mentions_attachments

This commit is contained in:
John Freed 2021-10-07 18:39:31 +02:00
commit cff5ec9a9f
73 changed files with 3294 additions and 1573 deletions

3
.gitignore vendored
View file

@ -12,7 +12,4 @@ local.properties
.settings/
out/
.DS_Store
.asciidoctorconfig.adoc
patches/
signal-cli
/bin/

View file

@ -14,7 +14,7 @@ repositories {
}
dependencies {
api("com.github.turasa:signal-service-java:2.15.3_unofficial_27")
api("com.github.turasa:signal-service-java:2.15.3_unofficial_28")
implementation("com.google.protobuf:protobuf-javalite:3.10.0")
implementation("org.bouncycastle:bcprov-jdk15on:1.69")
implementation("org.slf4j:slf4j-api:1.7.30")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -126,9 +126,9 @@ public class ProvisioningManager {
profileKey,
TrustNewIdentity.ON_FIRST_USE);
Manager m = null;
ManagerImpl m = null;
try {
m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
logger.debug("Refreshing pre keys");
try {
@ -178,7 +178,7 @@ public class ProvisioningManager {
return false;
}
final var m = new Manager(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
try (m) {
m.checkAccountState();
} catch (AuthorizationFailedException ignored) {

View file

@ -91,18 +91,18 @@ public class RegistrationManager implements Closeable {
}
public static RegistrationManager init(
String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
) throws IOException {
var pathConfig = PathConfig.createDefault(settingsPath);
final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) {
if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) {
var identityKey = KeyUtils.generateIdentityKeyPair();
var registrationId = KeyHelper.generateRegistrationId(false);
var profileKey = KeyUtils.createProfileKey();
var account = SignalAccount.create(pathConfig.getDataPath(),
username,
number,
identityKey,
registrationId,
profileKey,
@ -111,7 +111,7 @@ public class RegistrationManager implements Closeable {
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
var account = SignalAccount.load(pathConfig.getDataPath(), username, true, TrustNewIdentity.ON_FIRST_USE);
var account = SignalAccount.load(pathConfig.getDataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
@ -177,21 +177,21 @@ public class RegistrationManager implements Closeable {
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin);
Manager m = null;
ManagerImpl m = null;
try {
m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent);
m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
account = null;
m.refreshPreKeys();
if (response.isStorageCapable()) {
m.retrieveRemoteStorage();
}
// Set an initial empty profile so user can be added to groups
try {
m.setProfile(null, null, null, null, null);
} catch (NoClassDefFoundError e) {
logger.warn("Failed to set default profile: {}", e.getMessage());
}
if (response.isStorageCapable()) {
m.retrieveRemoteStorage();
}
final var result = m;
m = null;

View file

@ -4,16 +4,16 @@ import java.io.File;
public class UserAlreadyExists extends Exception {
private final String username;
private final String number;
private final File fileName;
public UserAlreadyExists(String username, File fileName) {
this.username = username;
public UserAlreadyExists(String number, File fileName) {
this.number = number;
this.fileName = fileName;
}
public String getUsername() {
return username;
public String getNumber() {
return number;
}
public File getFileName() {

View file

@ -0,0 +1,20 @@
package org.asamk.signal.manager.actions;
import org.asamk.signal.manager.jobs.Context;
public class SendSyncConfigurationAction implements HandleAction {
private static final SendSyncConfigurationAction INSTANCE = new SendSyncConfigurationAction();
private SendSyncConfigurationAction() {
}
public static SendSyncConfigurationAction create() {
return INSTANCE;
}
@Override
public void execute(Context context) throws Throwable {
context.getSyncHelper().sendConfigurationMessage();
}
}

View file

@ -6,12 +6,14 @@ public class Device {
private final String name;
private final long created;
private final long lastSeen;
private final boolean thisDevice;
public Device(long id, String name, long created, long lastSeen) {
public Device(long id, String name, long created, long lastSeen, final boolean thisDevice) {
this.id = id;
this.name = name;
this.created = created;
this.lastSeen = lastSeen;
this.thisDevice = thisDevice;
}
public long getId() {
@ -29,4 +31,8 @@ public class Device {
public long getLastSeen() {
return lastSeen;
}
public boolean isThisDevice() {
return thisDevice;
}
}

View file

@ -0,0 +1,99 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import java.util.Set;
public class Group {
private final GroupId groupId;
private final String title;
private final String description;
private final GroupInviteLinkUrl groupInviteLinkUrl;
private final Set<RecipientAddress> members;
private final Set<RecipientAddress> pendingMembers;
private final Set<RecipientAddress> requestingMembers;
private final Set<RecipientAddress> adminMembers;
private final boolean isBlocked;
private final int messageExpirationTime;
private final boolean isAnnouncementGroup;
private final boolean isMember;
public Group(
final GroupId groupId,
final String title,
final String description,
final GroupInviteLinkUrl groupInviteLinkUrl,
final Set<RecipientAddress> members,
final Set<RecipientAddress> pendingMembers,
final Set<RecipientAddress> requestingMembers,
final Set<RecipientAddress> adminMembers,
final boolean isBlocked,
final int messageExpirationTime,
final boolean isAnnouncementGroup,
final boolean isMember
) {
this.groupId = groupId;
this.title = title;
this.description = description;
this.groupInviteLinkUrl = groupInviteLinkUrl;
this.members = members;
this.pendingMembers = pendingMembers;
this.requestingMembers = requestingMembers;
this.adminMembers = adminMembers;
this.isBlocked = isBlocked;
this.messageExpirationTime = messageExpirationTime;
this.isAnnouncementGroup = isAnnouncementGroup;
this.isMember = isMember;
}
public GroupId getGroupId() {
return groupId;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public GroupInviteLinkUrl getGroupInviteLinkUrl() {
return groupInviteLinkUrl;
}
public Set<RecipientAddress> getMembers() {
return members;
}
public Set<RecipientAddress> getPendingMembers() {
return pendingMembers;
}
public Set<RecipientAddress> getRequestingMembers() {
return requestingMembers;
}
public Set<RecipientAddress> getAdminMembers() {
return adminMembers;
}
public boolean isBlocked() {
return isBlocked;
}
public int getMessageExpirationTime() {
return messageExpirationTime;
}
public boolean isAnnouncementGroup() {
return isAnnouncementGroup;
}
public boolean isMember() {
return isMember;
}
}

View file

@ -0,0 +1,65 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.whispersystems.libsignal.IdentityKey;
import java.util.Date;
public class Identity {
private final RecipientAddress recipient;
private final IdentityKey identityKey;
private final String safetyNumber;
private final byte[] scannableSafetyNumber;
private final TrustLevel trustLevel;
private final Date dateAdded;
public Identity(
final RecipientAddress recipient,
final IdentityKey identityKey,
final String safetyNumber,
final byte[] scannableSafetyNumber,
final TrustLevel trustLevel,
final Date dateAdded
) {
this.recipient = recipient;
this.identityKey = identityKey;
this.safetyNumber = safetyNumber;
this.scannableSafetyNumber = scannableSafetyNumber;
this.trustLevel = trustLevel;
this.dateAdded = dateAdded;
}
public RecipientAddress getRecipient() {
return recipient;
}
public IdentityKey getIdentityKey() {
return this.identityKey;
}
public TrustLevel getTrustLevel() {
return this.trustLevel;
}
boolean isTrusted() {
return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED;
}
public Date getDateAdded() {
return this.dateAdded;
}
public byte[] getFingerprint() {
return identityKey.getPublicKey().serialize();
}
public String getSafetyNumber() {
return safetyNumber;
}
public byte[] getScannableSafetyNumber() {
return scannableSafetyNumber;
}
}

View file

@ -1,6 +1,7 @@
package org.asamk.signal.manager.api;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
@ -12,14 +13,9 @@ public abstract class RecipientIdentifier {
public static class NoteToSelf extends RecipientIdentifier {
@Override
public boolean equals(final Object obj) {
return obj instanceof NoteToSelf;
}
public static NoteToSelf INSTANCE = new NoteToSelf();
@Override
public int hashCode() {
return 5;
private NoteToSelf() {
}
}
@ -34,6 +30,17 @@ public abstract class RecipientIdentifier {
public static Single fromAddress(SignalServiceAddress address) {
return new Uuid(address.getUuid());
}
public static Single fromAddress(RecipientAddress address) {
if (address.getNumber().isPresent()) {
return new Number(address.getNumber().get());
} else if (address.getUuid().isPresent()) {
return new Uuid(address.getUuid().get());
}
throw new AssertionError("RecipientAddress without identifier");
}
public abstract String getIdentifier();
}
public static class Uuid extends Single {
@ -58,6 +65,11 @@ public abstract class RecipientIdentifier {
public int hashCode() {
return uuid.hashCode();
}
@Override
public String getIdentifier() {
return uuid.toString();
}
}
public static class Number extends Single {
@ -82,6 +94,11 @@ public abstract class RecipientIdentifier {
public int hashCode() {
return number.hashCode();
}
@Override
public String getIdentifier() {
return number;
}
}
public static class Group extends RecipientIdentifier {

View file

@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -38,6 +39,7 @@ class LiveConfig {
private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org";
private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org";
private final static String STORAGE_URL = "https://storage.signal.org";
private final static String SIGNAL_CDSH_URL = "";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private final static Optional<Dns> dns = Optional.absent();
@ -58,6 +60,7 @@ class LiveConfig {
TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)},
interceptors,
dns,
proxy,

View file

@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.TrustStore;
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl;
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl;
import org.whispersystems.signalservice.internal.configuration.SignalProxy;
@ -38,6 +39,7 @@ class SandboxConfig {
private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api-staging.directory.signal.org";
private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org";
private final static String STORAGE_URL = "https://storage-staging.signal.org";
private final static String SIGNAL_CDSH_URL = "https://cdsh.staging.signal.org";
private final static TrustStore TRUST_STORE = new WhisperTrustStore();
private final static Optional<Dns> dns = Optional.absent();
@ -58,6 +60,7 @@ class SandboxConfig {
TRUST_STORE)},
new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)},
new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)},
new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)},
interceptors,
dns,
proxy,

View file

@ -39,7 +39,13 @@ public class ServiceConfig {
logger.warn("Failed to call libzkgroup: {}", e.getMessage());
zkGroupAvailable = false;
}
capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true);
capabilities = new AccountAttributes.Capabilities(false,
zkGroupAvailable,
false,
zkGroupAvailable,
true,
true,
false);
try {
TrustStore contactTrustStore = new IasTrustStore();

View file

@ -0,0 +1,93 @@
package org.asamk.signal.manager.configuration;
public class ConfigurationStore {
private final Saver saver;
private Boolean readReceipts;
private Boolean unidentifiedDeliveryIndicators;
private Boolean typingIndicators;
private Boolean linkPreviews;
public ConfigurationStore(final Saver saver) {
this.saver = saver;
}
public static ConfigurationStore fromStorage(Storage storage, Saver saver) {
final var store = new ConfigurationStore(saver);
store.readReceipts = storage.readReceipts;
store.unidentifiedDeliveryIndicators = storage.unidentifiedDeliveryIndicators;
store.typingIndicators = storage.typingIndicators;
store.linkPreviews = storage.linkPreviews;
return store;
}
public Boolean getReadReceipts() {
return readReceipts;
}
public void setReadReceipts(final boolean readReceipts) {
this.readReceipts = readReceipts;
saver.save(toStorage());
}
public Boolean getUnidentifiedDeliveryIndicators() {
return unidentifiedDeliveryIndicators;
}
public void setUnidentifiedDeliveryIndicators(final boolean unidentifiedDeliveryIndicators) {
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
saver.save(toStorage());
}
public Boolean getTypingIndicators() {
return typingIndicators;
}
public void setTypingIndicators(final boolean typingIndicators) {
this.typingIndicators = typingIndicators;
saver.save(toStorage());
}
public Boolean getLinkPreviews() {
return linkPreviews;
}
public void setLinkPreviews(final boolean linkPreviews) {
this.linkPreviews = linkPreviews;
saver.save(toStorage());
}
private Storage toStorage() {
return new Storage(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
}
public static final class Storage {
public Boolean readReceipts;
public Boolean unidentifiedDeliveryIndicators;
public Boolean typingIndicators;
public Boolean linkPreviews;
// For deserialization
private Storage() {
}
public Storage(
final Boolean readReceipts,
final Boolean unidentifiedDeliveryIndicators,
final Boolean typingIndicators,
final Boolean linkPreviews
) {
this.readReceipts = readReceipts;
this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators;
this.typingIndicators = typingIndicators;
this.linkPreviews = linkPreviews;
}
}
public interface Saver {
void save(Storage storage);
}
}

View file

@ -145,7 +145,10 @@ public class GroupHelper {
groupMasterKey);
}
if (group == null) {
group = groupV2Helper.getDecryptedGroup(groupSecretParams);
try {
group = groupV2Helper.getDecryptedGroup(groupSecretParams);
} catch (NotAGroupMemberException ignored) {
}
}
if (group != null) {
storeProfileKeysFromMembers(group);
@ -348,10 +351,20 @@ public class GroupHelper {
private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) {
final var group = account.getGroupStore().getGroup(groupId);
if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver);
account.getGroupStore().updateGroup(group);
if (group instanceof GroupInfoV2) {
final var groupInfoV2 = (GroupInfoV2) group;
if (forceUpdate || (!groupInfoV2.isPermissionDenied() && groupInfoV2.getGroup() == null)) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
DecryptedGroup decryptedGroup;
try {
decryptedGroup = groupV2Helper.getDecryptedGroup(groupSecretParams);
} catch (NotAGroupMemberException e) {
groupInfoV2.setPermissionDenied(true);
decryptedGroup = null;
}
groupInfoV2.setGroup(decryptedGroup, recipientResolver);
account.getGroupStore().updateGroup(group);
}
}
return group;
}

View file

@ -6,6 +6,7 @@ import org.asamk.signal.manager.groups.GroupLinkPassword;
import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientId;
@ -35,6 +36,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.File;
@ -78,10 +80,16 @@ public class GroupV2Helper {
this.addressResolver = addressResolver;
}
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
try {
final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
} catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 403) {
throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
}
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
return null;
} catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
return null;

View file

@ -15,6 +15,7 @@ import org.asamk.signal.manager.actions.SendGroupInfoRequestAction;
import org.asamk.signal.manager.actions.SendReceiptAction;
import org.asamk.signal.manager.actions.SendRetryMessageRequestAction;
import org.asamk.signal.manager.actions.SendSyncBlockedListAction;
import org.asamk.signal.manager.actions.SendSyncConfigurationAction;
import org.asamk.signal.manager.actions.SendSyncContactsAction;
import org.asamk.signal.manager.actions.SendSyncGroupsAction;
import org.asamk.signal.manager.actions.SendSyncKeysAction;
@ -144,7 +145,8 @@ public final class IncomingMessageHandler {
final var sender = account.getRecipientStore().resolveRecipient(e.getSender());
final var senderProfile = profileProvider.getProfile(sender);
final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId());
if (senderProfile != null
if (e.getSenderDevice() != account.getDeviceId()
&& senderProfile != null
&& senderProfile.getCapabilities().contains(Profile.Capability.senderKey)
&& selfProfile != null
&& selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) {
@ -270,7 +272,9 @@ public final class IncomingMessageHandler {
if (rm.isKeysRequest()) {
actions.add(SendSyncKeysAction.create());
}
// TODO Handle rm.isConfigurationRequest();
if (rm.isConfigurationRequest()) {
actions.add(SendSyncConfigurationAction.create());
}
}
if (syncMessage.getGroups().isPresent()) {
logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring.");
@ -352,7 +356,13 @@ public final class IncomingMessageHandler {
}
}
if (syncMessage.getConfiguration().isPresent()) {
// TODO
final var configurationMessage = syncMessage.getConfiguration().get();
final var configurationStore = account.getConfigurationStore();
configurationStore.setReadReceipts(configurationMessage.getReadReceipts().orNull());
configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().orNull());
configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().orNull());
configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators()
.orNull());
}
return actions;
}
@ -415,7 +425,7 @@ public final class IncomingMessageHandler {
}
final var recipientId = recipientResolver.resolveRecipient(source);
if (!group.isMember(recipientId)) {
if (!group.isMember(recipientId) && !(group.isPendingMember(recipientId) && message.isGroupV2Update())) {
return true;
}

View file

@ -32,6 +32,8 @@ import java.nio.file.Files;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import io.reactivex.rxjava3.core.Single;
@ -109,6 +111,17 @@ public final class ProfileHelper {
*/
public void setProfile(
String givenName, final String familyName, String about, String aboutEmoji, Optional<File> avatar
) throws IOException {
setProfile(true, givenName, familyName, about, aboutEmoji, avatar);
}
public void setProfile(
boolean uploadProfile,
String givenName,
final String familyName,
String about,
String aboutEmoji,
Optional<File> avatar
) throws IOException {
var profile = getRecipientProfile(account.getSelfRecipientId());
var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
@ -126,17 +139,22 @@ public final class ProfileHelper {
}
var newProfile = builder.build();
try (final var streamDetails = avatar == null
? avatarStore.retrieveProfileAvatar(account.getSelfAddress())
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
dependencies.getAccountManager()
.setVersionedProfile(account.getUuid(),
account.getProfileKey(),
newProfile.getInternalServiceName(),
newProfile.getAbout() == null ? "" : newProfile.getAbout(),
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
Optional.absent(),
streamDetails);
if (uploadProfile) {
try (final var streamDetails = avatar == null
? avatarStore.retrieveProfileAvatar(account.getSelfAddress())
: avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) {
final var avatarPath = dependencies.getAccountManager()
.setVersionedProfile(account.getUuid(),
account.getProfileKey(),
newProfile.getInternalServiceName(),
newProfile.getAbout() == null ? "" : newProfile.getAbout(),
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
Optional.absent(),
streamDetails,
List.of(/* TODO */));
builder.withAvatarUrlPath(avatarPath.orNull());
newProfile = builder.build();
}
}
if (avatar != null) {
@ -195,6 +213,7 @@ public final class ProfileHelper {
null,
null,
null,
null,
ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
ProfileUtils.getCapabilities(encryptedProfile));
}
@ -240,15 +259,23 @@ public final class ProfileHelper {
private Profile decryptProfileAndDownloadAvatar(
final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) {
if (encryptedProfile.getAvatar() != null) {
downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId),
encryptedProfile.getAvatar(),
profileKey);
}
final var avatarPath = encryptedProfile.getAvatar();
downloadProfileAvatar(recipientId, avatarPath, profileKey);
return ProfileUtils.decryptProfile(profileKey, encryptedProfile);
}
public void downloadProfileAvatar(
final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey
) {
var profile = account.getProfileStore().getProfile(recipientId);
if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) {
downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), avatarPath, profileKey);
var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
account.getProfileStore().storeProfile(recipientId, builder.withAvatarUrlPath(avatarPath).build());
}
}
private ProfileAndCredential retrieveProfileSync(
RecipientId recipientId, SignalServiceProfile.RequestType requestType
) throws IOException {
@ -308,6 +335,15 @@ public final class ProfileHelper {
private void downloadProfileAvatar(
SignalServiceAddress address, String avatarPath, ProfileKey profileKey
) {
if (avatarPath == null) {
try {
avatarStore.deleteProfileAvatar(address);
} catch (IOException e) {
logger.warn("Failed to delete local profile avatar, ignoring: {}", e.getMessage());
}
return;
}
try {
avatarStore.storeProfileAvatar(address,
outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream));

View file

@ -32,13 +32,18 @@ public class StorageHelper {
private final SignalAccount account;
private final SignalDependencies dependencies;
private final GroupHelper groupHelper;
private final ProfileHelper profileHelper;
public StorageHelper(
final SignalAccount account, final SignalDependencies dependencies, final GroupHelper groupHelper
final SignalAccount account,
final SignalDependencies dependencies,
final GroupHelper groupHelper,
final ProfileHelper profileHelper
) {
this.account = account;
this.dependencies = dependencies;
this.groupHelper = groupHelper;
this.profileHelper = profileHelper;
}
public void readDataFromStorage() throws IOException {
@ -188,13 +193,37 @@ public class StorageHelper {
return;
}
if (!accountRecord.getE164().equals(account.getUsername())) {
// TODO implement changed number handling
}
account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled());
account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled());
account.getConfigurationStore()
.setUnidentifiedDeliveryIndicators(accountRecord.isSealedSenderIndicatorsEnabled());
account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled());
if (accountRecord.getProfileKey().isPresent()) {
ProfileKey profileKey;
try {
account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get()));
profileKey = new ProfileKey(accountRecord.getProfileKey().get());
} catch (InvalidInputException e) {
logger.warn("Received invalid profile key from storage");
profileKey = null;
}
if (profileKey != null) {
account.setProfileKey(profileKey);
final var avatarPath = accountRecord.getAvatarUrlPath().orNull();
profileHelper.downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, profileKey);
}
}
profileHelper.setProfile(false,
accountRecord.getGivenName().orNull(),
accountRecord.getFamilyName().orNull(),
null,
null,
null);
}
private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException {

View file

@ -14,6 +14,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
@ -221,6 +222,15 @@ public class SyncHelper {
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage));
}
public void sendConfigurationMessage() throws IOException {
final var config = account.getConfigurationStore();
var configurationMessage = new ConfigurationMessage(Optional.fromNullable(config.getReadReceipts()),
Optional.fromNullable(config.getUnidentifiedDeliveryIndicators()),
Optional.fromNullable(config.getTypingIndicators()),
Optional.fromNullable(config.getLinkPreviews()));
sendHelper.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage));
}
public void handleSyncDeviceContacts(final InputStream input) throws IOException {
final var s = new DeviceContactsInputStream(input);
DeviceContact c;

View file

@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.asamk.signal.manager.TrustLevel;
import org.asamk.signal.manager.configuration.ConfigurationStore;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
@ -103,6 +104,8 @@ public class SignalAccount implements Closeable {
private RecipientStore recipientStore;
private StickerStore stickerStore;
private StickerStore.Storage stickerStoreStorage;
private ConfigurationStore configurationStore;
private ConfigurationStore.Storage configurationStoreStorage;
private MessageCache messageCache;
@ -159,6 +162,7 @@ public class SignalAccount implements Closeable {
account.recipientStore,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
account.configurationStore = new ConfigurationStore(account::saveConfigurationStore);
account.registered = false;
@ -267,6 +271,7 @@ public class SignalAccount implements Closeable {
account.recipientStore,
account::saveGroupStore);
account.stickerStore = new StickerStore(account::saveStickerStore);
account.configurationStore = new ConfigurationStore(account::saveConfigurationStore);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.migrateLegacyConfigs();
@ -491,6 +496,15 @@ public class SignalAccount implements Closeable {
stickerStore = new StickerStore(this::saveStickerStore);
}
if (rootNode.hasNonNull("configurationStore")) {
configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
ConfigurationStore.Storage.class);
configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage,
this::saveConfigurationStore);
} else {
configurationStore = new ConfigurationStore(this::saveConfigurationStore);
}
migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
if (migratedLegacyConfig) {
@ -617,6 +631,7 @@ public class SignalAccount implements Closeable {
profile.getFamilyName(),
profile.getAbout(),
profile.getAboutEmoji(),
null,
profile.isUnrestrictedUnidentifiedAccess()
? Profile.UnidentifiedAccessMode.UNRESTRICTED
: profile.getUnidentifiedAccess() != null
@ -677,6 +692,11 @@ public class SignalAccount implements Closeable {
save();
}
private void saveConfigurationStore(ConfigurationStore.Storage storage) {
this.configurationStoreStorage = storage;
save();
}
private void save() {
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
@ -707,7 +727,8 @@ public class SignalAccount implements Closeable {
profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
.put("registered", registered)
.putPOJO("groupStore", groupStoreStorage)
.putPOJO("stickerStore", stickerStoreStorage);
.putPOJO("stickerStore", stickerStoreStorage)
.putPOJO("configurationStore", configurationStoreStorage);
try {
try (var output = new ByteArrayOutputStream()) {
// Write to memory first to prevent corrupting the file in case of serialization errors
@ -797,6 +818,10 @@ public class SignalAccount implements Closeable {
return senderKeyStore;
}
public ConfigurationStore getConfigurationStore() {
return configurationStore;
}
public MessageCache getMessageCache() {
return messageCache;
}

View file

@ -22,16 +22,23 @@ public class GroupInfoV2 extends GroupInfo {
private boolean blocked;
private DecryptedGroup group; // stored as a file with hexadecimal groupId as name
private RecipientResolver recipientResolver;
private boolean permissionDenied;
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) {
this.groupId = groupId;
this.masterKey = masterKey;
}
public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) {
public GroupInfoV2(
final GroupIdV2 groupId,
final GroupMasterKey masterKey,
final boolean blocked,
final boolean permissionDenied
) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
this.permissionDenied = permissionDenied;
}
@Override
@ -44,6 +51,9 @@ public class GroupInfoV2 extends GroupInfo {
}
public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) {
if (group != null) {
this.permissionDenied = false;
}
this.group = group;
this.recipientResolver = recipientResolver;
}
@ -151,4 +161,12 @@ public class GroupInfoV2 extends GroupInfo {
public boolean isAnnouncementGroup() {
return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED;
}
public void setPermissionDenied(final boolean permissionDenied) {
this.permissionDenied = permissionDenied;
}
public boolean isPermissionDenied() {
return permissionDenied;
}
}

View file

@ -104,7 +104,7 @@ public class GroupStore {
throw new AssertionError("Invalid master key for group " + groupId.toBase64());
}
return new GroupInfoV2(groupId, masterKey, g2.blocked);
return new GroupInfoV2(groupId, masterKey, g2.blocked, g2.permissionDenied);
}).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g));
return new GroupStore(groupCachePath, groups, recipientResolver, saver);
@ -268,13 +268,13 @@ public class GroupStore {
final var g2 = (GroupInfoV2) g;
return new Storage.GroupV2(g2.getGroupId().toBase64(),
Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()),
g2.isBlocked());
g2.isBlocked(),
g2.isPermissionDenied());
}).collect(Collectors.toList()));
}
public static class Storage {
// @JsonSerialize(using = GroupsSerializer.class)
@JsonDeserialize(using = GroupsDeserializer.class)
public List<Storage.Group> groups;
@ -408,46 +408,24 @@ public class GroupStore {
public String groupId;
public String masterKey;
public boolean blocked;
public boolean permissionDenied;
// For deserialization
private GroupV2() {
}
public GroupV2(final String groupId, final String masterKey, final boolean blocked) {
public GroupV2(
final String groupId, final String masterKey, final boolean blocked, final boolean permissionDenied
) {
this.groupId = groupId;
this.masterKey = masterKey;
this.blocked = blocked;
this.permissionDenied = permissionDenied;
}
}
}
// private static class GroupsSerializer extends JsonSerializer<List<Storage.Group>> {
//
// @Override
// public void serialize(
// final List<Storage.Group> groups, final JsonGenerator jgen, final SerializerProvider provider
// ) throws IOException {
// jgen.writeStartArray(groups.size());
// for (var group : groups) {
// if (group instanceof GroupInfoV1) {
// jgen.writeObject(group);
// } else if (group instanceof GroupInfoV2) {
// final var groupV2 = (GroupInfoV2) group;
// jgen.writeStartObject();
// jgen.writeStringField("groupId", groupV2.getGroupId().toBase64());
// jgen.writeStringField("masterKey",
// Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize()));
// jgen.writeBooleanField("blocked", groupV2.isBlocked());
// jgen.writeEndObject();
// } else {
// throw new AssertionError("Unknown group version");
// }
// }
// jgen.writeEndArray();
// }
// }
//
private static class GroupsDeserializer extends JsonDeserializer<List<Storage.Group>> {
@Override

View file

@ -17,6 +17,8 @@ public class Profile {
private final String aboutEmoji;
private final String avatarUrlPath;
private final UnidentifiedAccessMode unidentifiedAccessMode;
private final Set<Capability> capabilities;
@ -27,6 +29,7 @@ public class Profile {
final String familyName,
final String about,
final String aboutEmoji,
final String avatarUrlPath,
final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> capabilities
) {
@ -35,6 +38,7 @@ public class Profile {
this.familyName = familyName;
this.about = about;
this.aboutEmoji = aboutEmoji;
this.avatarUrlPath = avatarUrlPath;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities;
}
@ -45,6 +49,7 @@ public class Profile {
familyName = builder.familyName;
about = builder.about;
aboutEmoji = builder.aboutEmoji;
avatarUrlPath = builder.avatarUrlPath;
unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities;
}
@ -60,6 +65,7 @@ public class Profile {
builder.familyName = copy.getFamilyName();
builder.about = copy.getAbout();
builder.aboutEmoji = copy.getAboutEmoji();
builder.avatarUrlPath = copy.getAvatarUrlPath();
builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode();
builder.capabilities = copy.getCapabilities();
return builder;
@ -107,6 +113,10 @@ public class Profile {
return aboutEmoji;
}
public String getAvatarUrlPath() {
return avatarUrlPath;
}
public UnidentifiedAccessMode getUnidentifiedAccessMode() {
return unidentifiedAccessMode;
}
@ -152,6 +162,7 @@ public class Profile {
private String familyName;
private String about;
private String aboutEmoji;
private String avatarUrlPath;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set<Capability> capabilities = Collections.emptySet();
private long lastUpdateTimestamp = 0;
@ -179,6 +190,11 @@ public class Profile {
return this;
}
public Builder withAvatarUrlPath(final String val) {
avatarUrlPath = val;
return this;
}
public Builder withUnidentifiedAccessMode(final UnidentifiedAccessMode val) {
unidentifiedAccessMode = val;
return this;

View file

@ -57,6 +57,16 @@ public class RecipientAddress {
}
}
public String getLegacyIdentifier() {
if (e164.isPresent()) {
return e164.get();
} else if (uuid.isPresent()) {
return uuid.get().toString();
} else {
throw new AssertionError("Given the checks in the constructor, this should not be possible.");
}
}
public boolean matches(RecipientAddress other) {
return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || (
e164.isPresent() && other.e164.isPresent() && e164.get().equals(other.e164.get())

View file

@ -89,6 +89,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
r.profile.familyName,
r.profile.about,
r.profile.aboutEmoji,
r.profile.avatarUrlPath,
Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
r.profile.capabilities.stream()
.map(Profile.Capability::valueOfOrNull)
@ -445,6 +446,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
recipient.getProfile().getFamilyName(),
recipient.getProfile().getAbout(),
recipient.getProfile().getAboutEmoji(),
recipient.getProfile().getAvatarUrlPath(),
recipient.getProfile().getUnidentifiedAccessMode().name(),
recipient.getProfile()
.getCapabilities()
@ -558,6 +560,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
public String familyName;
public String about;
public String aboutEmoji;
public String avatarUrlPath;
public String unidentifiedAccessMode;
public Set<String> capabilities;
@ -571,6 +574,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
final String familyName,
final String about,
final String aboutEmoji,
final String avatarUrlPath,
final String unidentifiedAccessMode,
final Set<String> capabilities
) {
@ -579,6 +583,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
this.familyName = familyName;
this.about = about;
this.aboutEmoji = aboutEmoji;
this.avatarUrlPath = avatarUrlPath;
this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities;
}

View file

@ -27,6 +27,7 @@ public class ProfileUtils {
nameParts.second(),
about,
aboutEmoji,
encryptedProfile.getAvatar(),
getUnidentifiedAccessMode(encryptedProfile, profileCipher),
getCapabilities(encryptedProfile));
} catch (InvalidCiphertextException e) {

View file

@ -44,6 +44,64 @@ Phone numbers always have the format +<countrycode><regional number>
== Methods
=== Control methods
These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`).
Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to
`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_).
Only `version()` is activated in single-user mode; the rest are disabled.
link() -> deviceLinkUri<s>::
link(newDeviceName<s>) -> deviceLinkUri<s>::
* newDeviceName : Name to give new device (defaults to "cli" if no name is given)
* deviceLinkUri : URI of newly linked device
Returns a URI of the form "tsdevice:/?uuid=...". This can be piped to a QR encoder to create a display that
can be captured by a Signal smartphone client. For example:
`dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256`
Exception: Failure
listAccounts() -> accountList<as>::
* accountList : Array of all attached accounts in DBus object path form
Exceptions: None
register(number<s>, voiceVerification<b>) -> <>::
* number : Phone number
* voiceVerification : true = use voice verification; false = use SMS verification
Exceptions: Failure, InvalidNumber, RequiresCaptcha
registerWithCaptcha(number<s>, voiceVerification<b>, captcha<s>) -> <>::
* number : Phone number
* voiceVerification : true = use voice verification; false = use SMS verification
* captcha : Captcha string
Exceptions: Failure, InvalidNumber, RequiresCaptcha
verify(number<s>, verificationCode<s>) -> <>::
* number : Phone number
* verificationCode : Code received from Signal after successful registration request
Command fails if PIN was set after previous registration; use verifyWithPin instead.
Exception: Failure, InvalidNumber
verifyWithPin(number<s>, verificationCode<s>, pin<s>) -> <>::
* number : Phone number
* verificationCode : Code received from Signal after successful registration request
* pin : PIN you set with setPin command after verifying previous registration
Exception: Failure, InvalidNumber
version() -> version<s>::
* version : Version string of signal-cli
Exceptions: None
=== Other methods
updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
* groupId : Byte array representing the internal group identifier
* newName : New name of group (empty if unchanged)
@ -52,8 +110,11 @@ updateGroup(groupId<ay>, newName<s>, members<as>, avatar<s>) -> groupId<ay>::
Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound
updateProfile(newName<s>, about <s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
* newName : New name for your own profile (empty if unchanged)
updateProfile(name<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
updateProfile(givenName<s>, familyName<s>, about<s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>::
* name : Name for your own profile (empty if unchanged)
* givenName : Given name for your own profile (empty if unchanged)
* familyName : Family name for your own profile (empty if unchanged)
* about : About message for profile (empty if unchanged)
* aboutEmoji : Emoji for profile (empty if unchanged)
* avatar : Filename of avatar picture for profile (empty if unchanged)
@ -61,6 +122,12 @@ updateProfile(newName<s>, about <s>, aboutEmoji <s>, avatar<s>, remove<b>) -> <>
Exceptions: Failure
setExpirationTimer(number<s>, expiration<i>) -> <>::
* number : Phone number of recipient
* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration.
Exceptions: Failure
setContactBlocked(number<s>, block<b>) -> <>::
* number : Phone number affected by method
* block : false=remove block , true=block
@ -107,6 +174,18 @@ sendGroupMessage(message<s>, attachments<as>, groupId<ay>) -> timestamp<x>::
Exceptions: GroupNotFound, Failure, AttachmentInvalid
sendContacts() -> <>::
Sends a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the primary device.
Exceptions: Failure
sendSyncRequest() -> <>::
Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device.
Exception: Failure
sendNoteToSelfMessage(message<s>, attachments<as>) -> timestamp<x>::
* message : Text to send (can be UTF8)
* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under)
@ -229,11 +308,59 @@ isGroupBlocked(groupId<ay>) -> state<b>::
Exceptions: None; for unknown groups false is returned
removePin() -> <>::
Removes registration PIN protection.
Exception: Failure
setPin(pin<s>) -> <>::
* pin : PIN you set after registration (resets after 7 days of inactivity)
Sets a registration lock PIN, to prevent others from registering your number.
Exception: Failure
version() -> version<s>::
* version : Version string of signal-cli
isRegistred -> result<b>::
* result : Currently always returns true
isRegistered() -> result<b>::
isRegistered(number<s>) -> result<b>::
isRegistered(numbers<as>) -> results<ab>::
* number : Phone number
* numbers : String array of phone numbers
* result : true=number is registered, false=number is not registered
* results : Boolean array of results
Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. If no number is given, returns whether you are registered (presumably true).
addDevice(deviceUri<s>) -> <>::
* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app
Exception: InvalidUri
listDevices() -> devices<as>::
* devices : String array of linked devices
Exception: Failure
removeDevice(deviceId<i>) -> <>::
* deviceId : Device ID to remove, obtained from listDevices() command
Exception: Failure
updateDeviceName(deviceName<s>) -> <>::
* deviceName : New name
Set a new name for this device (main or linked).
Exception: Failure
uploadStickerPack(stickerPackPath<s>) -> url<s>::
* stickerPackPath : Path to the manifest.json file or a zip file in the same directory
* url : URL of sticker pack after successful upload
Exception: Failure
== Signals

View file

@ -113,6 +113,23 @@ Can fix problems with receiving messages.
*-n* NAME, *--device-name* NAME::
Set a new device name for the main or linked device
=== updateConfiguration
Update signal configs and sync them to linked devices.
This command only works on the main devices.
*--read-receipts* {true,false}::
Indicates if Signal should send read receipts.
*--unidentified-delivery-indicators* {true,false}::
Indicates if Signal should show unidentified delivery indicators.
*--typing-indicators* {true,false}::
Indicates if Signal should send/show typing indicators.
*--link-previews* {true,false}::
Indicates if Signal should generate link previews.
=== setPin
Set a registration lock pin, to prevent others from registering this number.

View file

@ -2,9 +2,12 @@ package org.asamk;
import org.asamk.signal.dbus.DbusAttachment;
import org.asamk.signal.dbus.DbusMention;
import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.annotations.DBusProperty;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.freedesktop.dbus.interfaces.DBusInterface;
import org.freedesktop.dbus.interfaces.Properties;
import org.freedesktop.dbus.messages.DBusSignal;
import java.util.ArrayList;
@ -17,6 +20,8 @@ import java.util.Map;
*/
public interface Signal extends DBusInterface {
String getSelfNumber();
long sendMessage(
String message, List<String> attachmentNames, String recipient
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity;
@ -30,7 +35,7 @@ public interface Signal extends DBusInterface {
) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity;
void sendReadReceipt(
String recipient, List<Long> targetSentTimestamp
String recipient, List<Long> messageIds
) throws Error.Failure, Error.UntrustedIdentity;
long sendRemoteDeleteMessage(
@ -53,6 +58,10 @@ public interface Signal extends DBusInterface {
String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List<String> recipients
) throws Error.InvalidNumber, Error.Failure;
void sendContacts() throws Error.Failure;
void sendSyncRequest() throws Error.Failure;
long sendNoteToSelfMessage(
String message, List<String> attachmentNames
) throws Error.AttachmentInvalid, Error.Failure;
@ -71,6 +80,8 @@ public interface Signal extends DBusInterface {
void setContactName(String number, String name) throws Error.InvalidNumber;
void setExpirationTimer(final String number, final int expiration) throws Error.Failure;
void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber;
void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId;
@ -87,10 +98,37 @@ public interface Signal extends DBusInterface {
byte[] groupId, String name, List<String> members, String avatar
) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId;
boolean isRegistered() throws Error.Failure, Error.InvalidNumber;
boolean isRegistered(String number) throws Error.Failure, Error.InvalidNumber;
List<Boolean> isRegistered(List<String> numbers) throws Error.Failure, Error.InvalidNumber;
void addDevice(String uri) throws Error.InvalidUri;
DBusPath getDevice(long deviceId);
List<DBusPath> listDevices() throws Error.Failure;
DBusPath getThisDevice();
void updateProfile(
String givenName,
String familyName,
String about,
String aboutEmoji,
String avatarPath,
boolean removeAvatar
) throws Error.Failure;
void updateProfile(
String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar
) throws Error.Failure;
void removePin();
void setPin(String registrationLockPin);
String version();
List<String> listNumbers();
@ -107,6 +145,8 @@ public interface Signal extends DBusInterface {
byte[] joinGroup(final String groupLink) throws Error.Failure;
String uploadStickerPack(String stickerPackPath) throws Error.Failure;
class MessageReceived extends DBusSignal {
private final long timestamp;
@ -379,6 +419,15 @@ public interface Signal extends DBusInterface {
}
}
@DBusProperty(name = "Id", type = Integer.class, access = DBusProperty.Access.READ)
@DBusProperty(name = "Name", type = String.class)
@DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ)
@DBusProperty(name = "LastSeen", type = String.class, access = DBusProperty.Access.READ)
interface Device extends DBusInterface, Properties {
void removeDevice() throws Error.Failure;
}
interface Error {
class AttachmentInvalid extends DBusExecutionException {
@ -388,6 +437,13 @@ public interface Signal extends DBusInterface {
}
}
class InvalidUri extends DBusExecutionException {
public InvalidUri(final String message) {
super(message);
}
}
class Failure extends DBusExecutionException {
public Failure(final String message) {

View file

@ -8,7 +8,6 @@ import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.asamk.signal.commands.Command;
import org.asamk.signal.commands.Commands;
import org.asamk.signal.commands.DbusCommand;
import org.asamk.signal.commands.ExtendedDbusCommand;
import org.asamk.signal.commands.LocalCommand;
import org.asamk.signal.commands.MultiLocalCommand;
@ -19,6 +18,7 @@ import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.dbus.DbusManagerImpl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotRegisteredException;
import org.asamk.signal.manager.ProvisioningManager;
@ -29,6 +29,7 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
@ -116,8 +117,8 @@ public class App {
var username = ns.getString("username");
final var useDbus = ns.getBoolean("dbus");
final var useDbusSystem = ns.getBoolean("dbus-system");
final var useDbus = Boolean.TRUE.equals(ns.getBoolean("dbus"));
final var useDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system"));
if (useDbus || useDbusSystem) {
// If username is null, it will connect to the default object path
initDbusClient(command, username, useDbusSystem, outputWriter);
@ -161,7 +162,7 @@ public class App {
}
if (username == null) {
var usernames = Manager.getAllLocalUsernames(dataPath);
var usernames = Manager.getAllLocalNumbers(dataPath);
if (command instanceof MultiLocalCommand) {
handleMultiLocalCommand((MultiLocalCommand) command,
@ -346,8 +347,14 @@ public class App {
) throws CommandException {
if (command instanceof ExtendedDbusCommand) {
((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter);
} else if (command instanceof DbusCommand) {
((DbusCommand) command).handleCommand(ns, ts, outputWriter);
} else if (command instanceof LocalCommand) {
try {
((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts, dBusConn), outputWriter);
} catch (UnsupportedOperationException e) {
throw new UserErrorException("Command is not yet implemented via dbus", e);
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException(e.getMessage(), e);
}
} else {
throw new UserErrorException("Command is not yet implemented via dbus");
}

View file

@ -80,7 +80,7 @@ public class Main {
return false;
}
return ns.getBoolean("verbose");
return Boolean.TRUE.equals(ns.getBoolean("verbose"));
}
private static void configureLogging(final boolean verbose) {

View file

@ -61,13 +61,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender()));
writer.println(
"Use 'signal-cli -u {} listIdentities -n {}', verify the key and run 'signal-cli -u {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted",
m.getUsername(),
m.getSelfNumber(),
recipientName,
m.getUsername(),
m.getSelfNumber(),
recipientName);
writer.println(
"If you don't care about security, use 'signal-cli -u {} trust -a {}' to trust it without verification",
m.getUsername(),
m.getSelfNumber(),
recipientName);
} else {
writer.println("Exception: {} ({})", exception.getMessage(), exception.getClass().getSimpleName());
@ -657,7 +657,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
private void printMention(
PlainTextWriter writer, SignalServiceDataMessage.Mention mention
) {
final var address = m.resolveSignalServiceAddress(mention.getUuid());
final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid()));
writer.println("- {}: {} (length: {})", formatContact(address), mention.getStart(), mention.getLength());
}

View file

@ -37,7 +37,7 @@ public class BlockCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var contacts = ns.<String>getList("recipient");
for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) {
for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getSelfNumber())) {
try {
m.setContactBlocked(contact, true);
} catch (NotMasterDeviceException e) {

View file

@ -39,6 +39,7 @@ public class Commands {
addCommand(new UnblockCommand());
addCommand(new UnregisterCommand());
addCommand(new UpdateAccountCommand());
addCommand(new UpdateConfigurationCommand());
addCommand(new UpdateContactCommand());
addCommand(new UpdateGroupCommand());
addCommand(new UpdateProfileCommand());

View file

@ -54,10 +54,10 @@ public class DaemonCommand implements MultiLocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
DBusConnection.DBusBusType busType;
if (ns.getBoolean("system")) {
if (Boolean.TRUE.equals(ns.getBoolean("system"))) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
@ -83,10 +83,10 @@ public class DaemonCommand implements MultiLocalCommand {
public void handleCommand(
final Namespace ns, final List<Manager> managers, final SignalCreator c, final OutputWriter outputWriter
) throws CommandException {
boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
DBusConnection.DBusBusType busType;
if (ns.getBoolean("system")) {
if (Boolean.TRUE.equals(ns.getBoolean("system"))) {
busType = DBusConnection.DBusBusType.SYSTEM;
} else {
busType = DBusConnection.DBusBusType.SESSION;
@ -95,7 +95,7 @@ public class DaemonCommand implements MultiLocalCommand {
try (var conn = DBusConnection.getConnection(busType)) {
final var signalControl = new DbusSignalControlImpl(c, m -> {
try {
final var objectPath = DbusConfig.getObjectPath(m.getUsername());
final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber());
return run(conn, objectPath, m, outputWriter, ignoreAttachments);
} catch (DBusException e) {
logger.error("Failed to export object", e);
@ -120,7 +120,10 @@ public class DaemonCommand implements MultiLocalCommand {
private Thread run(
DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments
) throws DBusException {
conn.exportObject(new DbusSignalImpl(m, objectPath));
final var signal = new DbusSignalImpl(m, conn, objectPath);
conn.exportObject(signal);
final var initThread = new Thread(signal::initObjects);
initThread.start();
logger.info("Exported dbus object: " + objectPath);
@ -136,6 +139,11 @@ public class DaemonCommand implements MultiLocalCommand {
logger.warn("Receiving messages failed, retrying", e);
}
}
try {
initThread.join();
} catch (InterruptedException ignored) {
}
signal.close();
});
thread.start();

View file

@ -1,20 +0,0 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import org.asamk.Signal;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.dbus.DbusSignalImpl;
import org.asamk.signal.manager.Manager;
public interface DbusCommand extends LocalCommand {
void handleCommand(Namespace ns, Signal signal, OutputWriter outputWriter) throws CommandException;
default void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
handleCommand(ns, new DbusSignalImpl(m, null), outputWriter);
}
}

View file

@ -57,14 +57,14 @@ public class JoinGroupCommand implements JsonRpcLocalCommand {
var newGroupId = results.first();
if (outputWriter instanceof JsonWriter) {
final var writer = (JsonWriter) outputWriter;
if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) {
if (!m.getGroup(newGroupId).isMember()) {
writer.write(Map.of("groupId", newGroupId.toBase64(), "onlyRequested", true));
} else {
writer.write(Map.of("groupId", newGroupId.toBase64()));
}
} else {
final var writer = (PlainTextWriter) outputWriter;
if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) {
if (!m.getGroup(newGroupId).isMember()) {
writer.println("Requested to join group \"{}\"", newGroupId.toBase64());
} else {
writer.println("Joined group \"{}\"", newGroupId.toBase64());

View file

@ -65,7 +65,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
final boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
final var objectMapper = Util.createJsonObjectMapper();
final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter);

View file

@ -64,14 +64,5 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand<Map<String, Object>>
return super.getList(dest + "s");
}
@Override
public Boolean getBoolean(String dest) {
Boolean maybeGotten = this.get(dest);
if (maybeGotten == null) {
maybeGotten = false;
}
return maybeGotten;
}
}
}

View file

@ -44,7 +44,7 @@ public class LinkCommand implements ProvisioningCommand {
try {
writer.println("{}", m.getDeviceLinkUri());
try (var manager = m.finishDeviceLink(deviceName)) {
writer.println("Associated with: {}", manager.getUsername());
writer.println("Associated with: {}", manager.getSelfNumber());
}
} catch (TimeoutException e) {
throw new UserErrorException("Link request timed out, please try again.");
@ -52,7 +52,7 @@ public class LinkCommand implements ProvisioningCommand {
throw new IOErrorException("Link request error: " + e.getMessage(), e);
} catch (UserAlreadyExists e) {
throw new UserErrorException("The user "
+ e.getUsername()
+ e.getNumber()
+ " already exists\nDelete \""
+ e.getFileName()
+ "\" before trying again.");

View file

@ -8,10 +8,9 @@ import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.manager.Manager;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.asamk.signal.util.Util.getLegacyIdentifier;
public class ListContactsCommand implements JsonRpcLocalCommand {
@Override
@ -33,7 +32,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
for (var c : contacts) {
final var contact = c.second();
writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}",
getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())),
c.first().getLegacyIdentifier(),
contact.getName(),
contact.isBlocked(),
contact.getMessageExpirationTime() == 0
@ -43,10 +42,10 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
} else {
final var writer = (JsonWriter) outputWriter;
final var jsonContacts = contacts.stream().map(contactPair -> {
final var address = m.resolveSignalServiceAddress(contactPair.first());
final var address = contactPair.first();
final var contact = contactPair.second();
return new JsonContact(address.getNumber().orNull(),
address.getUuid().toString(),
return new JsonContact(address.getNumber().orElse(null),
address.getUuid().map(UUID::toString).orElse(null),
contact.getName(),
contact.isBlocked(),
contact.getMessageExpirationTime());

View file

@ -46,7 +46,7 @@ public class ListDevicesCommand implements JsonRpcLocalCommand {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;
for (var d : devices) {
writer.println("- Device {}{}:", d.getId(), (d.getId() == m.getDeviceId() ? " (this device)" : ""));
writer.println("- Device {}{}:", d.getId(), (d.isThisDevice() ? " (this device)" : ""));
writer.indent(w -> {
w.println("Name: {}", d.getName());
w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated()));

View file

@ -9,13 +9,13 @@ import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.util.Util;
import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class ListGroupsCommand implements JsonRpcLocalCommand {
@ -35,44 +35,41 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
.help("List the members and group invite links of each group. If output=json, then this is always set");
}
private static Set<String> resolveMembers(Manager m, Set<RecipientId> addresses) {
return addresses.stream()
.map(m::resolveSignalServiceAddress)
.map(Util::getLegacyIdentifier)
.collect(Collectors.toSet());
private static Set<String> resolveMembers(Set<RecipientAddress> addresses) {
return addresses.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toSet());
}
private static Set<JsonGroupMember> resolveJsonMembers(Manager m, Set<RecipientId> addresses) {
private static Set<JsonGroupMember> resolveJsonMembers(Set<RecipientAddress> addresses) {
return addresses.stream()
.map(m::resolveSignalServiceAddress)
.map(address -> new JsonGroupMember(address.getNumber().orNull(), address.getUuid().toString()))
.map(address -> new JsonGroupMember(address.getNumber().orElse(null),
address.getUuid().map(UUID::toString).orElse(null)))
.collect(Collectors.toSet());
}
private static void printGroupPlainText(
PlainTextWriter writer, Manager m, GroupInfo group, boolean detailed
PlainTextWriter writer, Group group, boolean detailed
) {
if (detailed) {
final var groupInviteLink = group.getGroupInviteLink();
final var groupInviteLink = group.getGroupInviteLinkUrl();
writer.println(
"Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Message expiration: {} Link: {}",
group.getGroupId().toBase64(),
group.getTitle(),
group.getDescription(),
group.isMember(m.getSelfRecipientId()),
group.isMember(),
group.isBlocked(),
resolveMembers(m, group.getMembers()),
resolveMembers(m, group.getPendingMembers()),
resolveMembers(m, group.getRequestingMembers()),
resolveMembers(m, group.getAdminMembers()),
resolveMembers(group.getMembers()),
resolveMembers(group.getPendingMembers()),
resolveMembers(group.getRequestingMembers()),
resolveMembers(group.getAdminMembers()),
group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s",
groupInviteLink == null ? '-' : groupInviteLink.getUrl());
} else {
writer.println("Id: {} Name: {} Active: {} Blocked: {}",
group.getGroupId().toBase64(),
group.getTitle(),
group.isMember(m.getSelfRecipientId()),
group.isMember(),
group.isBlocked());
}
}
@ -87,27 +84,27 @@ public class ListGroupsCommand implements JsonRpcLocalCommand {
final var jsonWriter = (JsonWriter) outputWriter;
var jsonGroups = groups.stream().map(group -> {
final var groupInviteLink = group.getGroupInviteLink();
final var groupInviteLink = group.getGroupInviteLinkUrl();
return new JsonGroup(group.getGroupId().toBase64(),
group.getTitle(),
group.getDescription(),
group.isMember(m.getSelfRecipientId()),
group.isMember(),
group.isBlocked(),
group.getMessageExpirationTime(),
resolveJsonMembers(m, group.getMembers()),
resolveJsonMembers(m, group.getPendingMembers()),
resolveJsonMembers(m, group.getRequestingMembers()),
resolveJsonMembers(m, group.getAdminMembers()),
resolveJsonMembers(group.getMembers()),
resolveJsonMembers(group.getPendingMembers()),
resolveJsonMembers(group.getRequestingMembers()),
resolveJsonMembers(group.getAdminMembers()),
groupInviteLink == null ? null : groupInviteLink.getUrl());
}).collect(Collectors.toList());
jsonWriter.write(jsonGroups);
} else {
final var writer = (PlainTextWriter) outputWriter;
boolean detailed = ns.getBoolean("detailed");
boolean detailed = Boolean.TRUE.equals(ns.getBoolean("detailed"));
for (var group : groups) {
printGroupPlainText(writer, m, group, detailed);
printGroupPlainText(writer, group, detailed);
}
}
}

View file

@ -8,7 +8,7 @@ import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.Hex;
import org.asamk.signal.util.Util;
@ -29,9 +29,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
return "listIdentities";
}
private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) {
final SignalServiceAddress address = m.resolveSignalServiceAddress(theirId.getRecipientId());
var digits = Util.formatSafetyNumber(m.computeSafetyNumber(address, theirId.getIdentityKey()));
private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, Identity theirId) {
final SignalServiceAddress address = theirId.getRecipient().toSignalServiceAddress();
var digits = Util.formatSafetyNumber(theirId.getSafetyNumber());
writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}",
address.getNumber().orNull(),
theirId.getTrustLevel(),
@ -52,11 +52,11 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
) throws CommandException {
var number = ns.getString("number");
List<IdentityInfo> identities;
List<Identity> identities;
if (number == null) {
identities = m.getIdentities();
} else {
identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getUsername()));
identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getSelfNumber()));
}
if (outputWriter instanceof PlainTextWriter) {
@ -67,9 +67,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand {
} else {
final var writer = (JsonWriter) outputWriter;
final var jsonIdentities = identities.stream().map(id -> {
final var address = m.resolveSignalServiceAddress(id.getRecipientId());
var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(address, id.getIdentityKey()));
var scannableSafetyNumber = m.computeSafetyNumberForScanning(address, id.getIdentityKey());
final var address = id.getRecipient().toSignalServiceAddress();
var safetyNumber = Util.formatSafetyNumber(id.getSafetyNumber());
var scannableSafetyNumber = id.getScannableSafetyNumber();
return new JsonIdentity(address.getNumber().orNull(),
address.getUuid().toString(),
Hex.toString(id.getFingerprint()),

View file

@ -50,7 +50,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand {
) throws CommandException {
final var groupId = CommandUtil.getGroupId(ns.getString("group-id"));
var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getUsername());
var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getSelfNumber());
try {
try {
@ -61,7 +61,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand {
} catch (NotAGroupMemberException e) {
logger.info("User is not a group member");
}
if (ns.getBoolean("delete")) {
if (Boolean.TRUE.equals(ns.getBoolean("delete"))) {
logger.debug("Deleting group {}", groupId);
m.deleteGroup(groupId);
}

View file

@ -147,7 +147,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand {
returnOnTimeout = false;
timeout = 3600;
}
boolean ignoreAttachments = ns.getBoolean("ignore-attachments");
boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments"));
try {
final var handler = outputWriter instanceof JsonWriter ? new JsonReceiveMessageHandler(m,
(JsonWriter) outputWriter) : new ReceiveMessageHandler(m, (PlainTextWriter) outputWriter);

View file

@ -31,7 +31,7 @@ public class RegisterCommand implements RegistrationCommand {
@Override
public void handleCommand(final Namespace ns, final RegistrationManager m) throws CommandException {
final boolean voiceVerification = ns.getBoolean("voice");
final boolean voiceVerification = Boolean.TRUE.equals(ns.getBoolean("voice"));
final var captchaString = ns.getString("captcha");
final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", "");

View file

@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.errors.UnknownObject;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import java.io.IOException;
import java.util.Map;
public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand {
public class RemoteDeleteCommand implements JsonRpcLocalCommand {
@Override
public String getName() {
@ -46,7 +43,7 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var isNoteToSelf = ns.getBoolean("note-to-self");
final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self"));
final var recipientStrings = ns.<String>getList("recipient");
final var groupIdStrings = ns.<String>getList("group-id");
@ -69,47 +66,6 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
@Override
public void handleCommand(
final Namespace ns, final Signal signal, final OutputWriter outputWriter
) throws CommandException {
final var recipients = ns.<String>getList("recipient");
final var groupIdStrings = ns.<String>getList("group-id");
final var noRecipients = recipients == null || recipients.isEmpty();
final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty();
if (noRecipients && noGroups) {
throw new UserErrorException("No recipients given");
}
if (!noRecipients && !noGroups) {
throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
}
final long targetTimestamp = ns.getLong("target-timestamp");
try {
long timestamp = 0;
if (!noGroups) {
final var groupIds = CommandUtil.getGroupIds(groupIdStrings);
for (final var groupId : groupIds) {
timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId.serialize());
}
} else {
timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients);
}
outputResult(outputWriter, timestamp);
} catch (UnknownObject e) {
throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
} catch (Signal.Error.InvalidNumber e) {
throw new UserErrorException("Invalid number: " + e.getMessage());
} catch (Signal.Error.GroupNotFound e) {
throw new UserErrorException("Failed to send to group: " + e.getMessage());
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e);
}
}
private void outputResult(final OutputWriter outputWriter, final long timestamp) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;

View file

@ -21,7 +21,7 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand {
public void attachToSubparser(final Subparser subparser) {
subparser.help("Remove a linked device.");
subparser.addArgument("-d", "--device-id", "--deviceId")
.type(int.class)
.type(long.class)
.required(true)
.help("Specify the device you want to remove. Use listDevices to see the deviceIds.");
}
@ -31,7 +31,7 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
try {
int deviceId = ns.getInt("device-id");
final var deviceId = ns.getLong("device-id");
m.removeLinkedDevices(deviceId);
} catch (IOException e) {
throw new IOErrorException("Error while removing device: " + e.getMessage(), e);

View file

@ -4,13 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.Manager;
@ -22,8 +20,6 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.errors.UnknownObject;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -33,7 +29,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SendCommand implements DbusCommand, JsonRpcLocalCommand {
public class SendCommand implements JsonRpcLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(SendCommand.class);
@ -62,7 +58,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var isNoteToSelf = ns.getBoolean("note-to-self");
final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self"));
final var recipientStrings = ns.<String>getList("recipient");
final var groupIdStrings = ns.<String>getList("group-id");
@ -71,7 +67,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand {
recipientStrings,
groupIdStrings);
final var isEndSession = ns.getBoolean("end-session");
final var isEndSession = Boolean.TRUE.equals(ns.getBoolean("end-session"));
if (isEndSession) {
final var singleRecipients = recipientIdentifiers.stream()
.filter(r -> r instanceof RecipientIdentifier.Single)
@ -116,97 +112,6 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
@Override
public void handleCommand(
final Namespace ns, final Signal signal, final OutputWriter outputWriter
) throws CommandException {
final var recipients = ns.<String>getList("recipient");
final var isEndSession = ns.getBoolean("end-session");
final var groupIdStrings = ns.<String>getList("group-id");
final var isNoteToSelf = ns.getBoolean("note-to-self");
final var noRecipients = recipients == null || recipients.isEmpty();
final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty();
if ((noRecipients && isEndSession) || (noRecipients && noGroups && !isNoteToSelf)) {
throw new UserErrorException("No recipients given");
}
if (!noRecipients && !noGroups) {
throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
}
if (!noRecipients && isNoteToSelf) {
throw new UserErrorException(
"You cannot specify recipients by phone number and note to self at the same time");
}
if (isEndSession) {
try {
signal.sendEndSessionMessage(recipients);
return;
} catch (Signal.Error.UntrustedIdentity e) {
throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")");
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e);
}
}
var messageText = ns.getString("message");
if (messageText == null) {
try {
messageText = IOUtils.readAll(System.in, Charset.defaultCharset());
} catch (IOException e) {
throw new UserErrorException("Failed to read message from stdin: " + e.getMessage());
}
}
List<String> attachments = ns.getList("attachment");
if (attachments == null) {
attachments = List.of();
}
if (!noGroups) {
final var groupIds = CommandUtil.getGroupIds(groupIdStrings);
try {
long timestamp = 0;
for (final var groupId : groupIds) {
timestamp = signal.sendGroupMessage(messageText, attachments, groupId.serialize());
}
outputResult(outputWriter, timestamp);
return;
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage(), e);
}
}
if (isNoteToSelf) {
try {
var timestamp = signal.sendNoteToSelfMessage(messageText, attachments);
outputResult(outputWriter, timestamp);
return;
} catch (Signal.Error.UntrustedIdentity e) {
throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")");
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage(), e);
}
}
try {
var timestamp = signal.sendMessage(messageText, attachments, recipients);
outputResult(outputWriter, timestamp);
} catch (UnknownObject e) {
throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
} catch (Signal.Error.UntrustedIdentity e) {
throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")");
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e);
}
}
private void outputResult(final OutputWriter outputWriter, final long timestamp) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;

View file

@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.errors.UnknownObject;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import java.io.IOException;
import java.util.Map;
public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand {
public class SendReactionCommand implements JsonRpcLocalCommand {
@Override
public String getName() {
@ -55,7 +52,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var isNoteToSelf = ns.getBoolean("note-to-self");
final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self"));
final var recipientStrings = ns.<String>getList("recipient");
final var groupIdStrings = ns.<String>getList("group-id");
@ -65,14 +62,14 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand {
groupIdStrings);
final var emoji = ns.getString("emoji");
final var isRemove = ns.getBoolean("remove");
final var isRemove = Boolean.TRUE.equals(ns.getBoolean("remove"));
final var targetAuthor = ns.getString("target-author");
final var targetTimestamp = ns.getLong("target-timestamp");
try {
final var results = m.sendMessageReaction(emoji,
isRemove,
CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
targetTimestamp,
recipientIdentifiers);
outputResult(outputWriter, results.getTimestamp());
@ -85,54 +82,6 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
@Override
public void handleCommand(
final Namespace ns, final Signal signal, final OutputWriter outputWriter
) throws CommandException {
final var recipients = ns.<String>getList("recipient");
final var groupIdStrings = ns.<String>getList("group-id");
final var noRecipients = recipients == null || recipients.isEmpty();
final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty();
if (noRecipients && noGroups) {
throw new UserErrorException("No recipients given");
}
if (!noRecipients && !noGroups) {
throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time");
}
final var emoji = ns.getString("emoji");
final var isRemove = ns.getBoolean("remove");
final var targetAuthor = ns.getString("target-author");
final var targetTimestamp = ns.getLong("target-timestamp");
try {
long timestamp = 0;
if (!noGroups) {
final var groupIds = CommandUtil.getGroupIds(groupIdStrings);
for (final var groupId : groupIds) {
timestamp = signal.sendGroupMessageReaction(emoji,
isRemove,
targetAuthor,
targetTimestamp,
groupId.serialize());
}
} else {
timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients);
}
outputResult(outputWriter, timestamp);
} catch (UnknownObject e) {
throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage());
} catch (Signal.Error.InvalidNumber e) {
throw new UserErrorException("Invalid number: " + e.getMessage());
} catch (Signal.Error.GroupNotFound e) {
throw new UserErrorException("Failed to send to group: " + e.getMessage());
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e);
}
}
private void outputResult(final OutputWriter outputWriter, final long timestamp) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;

View file

@ -37,7 +37,7 @@ public class SendReceiptCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var recipientString = ns.getString("recipient");
final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername());
final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber());
final var targetTimestamps = ns.<Long>getList("target-timestamp");
final var type = ns.getString("type");

View file

@ -41,11 +41,11 @@ public class SendTypingCommand implements JsonRpcLocalCommand {
) throws CommandException {
final var recipientStrings = ns.<String>getList("recipient");
final var groupIdStrings = ns.<String>getList("group-id");
final var action = ns.getBoolean("stop") ? TypingAction.STOP : TypingAction.START;
final var action = Boolean.TRUE.equals(ns.getBoolean("stop")) ? TypingAction.STOP : TypingAction.START;
final var recipientIdentifiers = new HashSet<RecipientIdentifier>();
if (recipientStrings != null) {
final var localNumber = m.getUsername();
final var localNumber = m.getSelfNumber();
recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber));
}
if (groupIdStrings != null) {

View file

@ -38,8 +38,8 @@ public class TrustCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
var recipentString = ns.getString("recipient");
var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername());
if (ns.getBoolean("trust-all-known-keys")) {
var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getSelfNumber());
if (Boolean.TRUE.equals(ns.getBoolean("trust-all-known-keys"))) {
boolean res = m.trustIdentityAllKeys(recipient);
if (!res) {
throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct.");

View file

@ -36,7 +36,8 @@ public class UnblockCommand implements JsonRpcLocalCommand {
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), m.getUsername())) {
for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"),
m.getSelfNumber())) {
try {
m.setContactBlocked(contactNumber, false);
} catch (NotMasterDeviceException e) {

View file

@ -31,7 +31,7 @@ public class UnregisterCommand implements LocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
try {
if (ns.getBoolean("delete-account")) {
if (Boolean.TRUE.equals(ns.getBoolean("delete-account"))) {
m.deleteAccount();
} else {
m.unregister();

View file

@ -0,0 +1,55 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotMasterDeviceException;
import java.io.IOException;
public class UpdateConfigurationCommand implements JsonRpcLocalCommand {
@Override
public String getName() {
return "updateConfiguration";
}
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.help("Update signal configs and sync them to linked devices.");
subparser.addArgument("--read-receipts")
.type(Boolean.class)
.help("Indicates if Signal should send read receipts.");
subparser.addArgument("--unidentified-delivery-indicators")
.type(Boolean.class)
.help("Indicates if Signal should show unidentified delivery indicators.");
subparser.addArgument("--typing-indicators")
.type(Boolean.class)
.help("Indicates if Signal should send/show typing indicators.");
subparser.addArgument("--link-previews")
.type(Boolean.class)
.help("Indicates if Signal should generate link previews.");
}
@Override
public void handleCommand(
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
final var readReceipts = ns.getBoolean("read-receipts");
final var unidentifiedDeliveryIndicators = ns.getBoolean("unidentified-delivery-indicators");
final var typingIndicators = ns.getBoolean("typing-indicators");
final var linkPreviews = ns.getBoolean("link-previews");
try {
m.updateConfiguration(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews);
} catch (IOException e) {
throw new IOErrorException("UpdateAccount error: " + e.getMessage(), e);
} catch (NotMasterDeviceException e) {
throw new UserErrorException("This command doesn't work on linked devices.");
}
}
}

View file

@ -33,7 +33,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand {
final Namespace ns, final Manager m, final OutputWriter outputWriter
) throws CommandException {
var recipientString = ns.getString("recipient");
var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername());
var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber());
try {
var expiration = ns.getInt("expiration");

View file

@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.Signal;
import org.asamk.signal.JsonWriter;
import org.asamk.signal.OutputWriter;
import org.asamk.signal.PlainTextWriter;
@ -21,17 +20,14 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.util.CommandUtil;
import org.asamk.signal.util.ErrorUtils;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand {
public class UpdateGroupCommand implements JsonRpcLocalCommand {
private final static Logger logger = LoggerFactory.getLogger(UpdateGroupCommand.class);
@ -116,7 +112,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand {
final var groupIdString = ns.getString("group-id");
var groupId = CommandUtil.getGroupId(groupIdString);
final var localNumber = m.getUsername();
final var localNumber = m.getSelfNumber();
var groupName = ns.getString("name");
var groupDescription = ns.getString("description");
@ -125,7 +121,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand {
var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), localNumber);
var groupRemoveAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("remove-admin"), localNumber);
var groupAvatar = ns.getString("avatar");
var groupResetLink = ns.getBoolean("reset-link");
var groupResetLink = Boolean.TRUE.equals(ns.getBoolean("reset-link"));
var groupLinkState = getGroupLinkState(ns.getString("link"));
var groupExpiration = ns.getInt("expiration");
var groupAddMemberPermission = getGroupPermission(ns.getString("set-permission-add-member"));
@ -179,43 +175,6 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand {
}
}
@Override
public void handleCommand(
final Namespace ns, final Signal signal, final OutputWriter outputWriter
) throws CommandException {
var groupId = CommandUtil.getGroupId(ns.getString("group-id"));
var groupName = ns.getString("name");
if (groupName == null) {
groupName = "";
}
List<String> groupMembers = ns.getList("member");
if (groupMembers == null) {
groupMembers = new ArrayList<>();
}
var groupAvatar = ns.getString("avatar");
if (groupAvatar == null) {
groupAvatar = "";
}
try {
var newGroupId = signal.updateGroup(groupId == null ? new byte[0] : groupId.serialize(),
groupName,
groupMembers,
groupAvatar);
if (groupId == null) {
outputResult(outputWriter, null, GroupId.unknownVersion(newGroupId));
}
} catch (Signal.Error.AttachmentInvalid e) {
throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage());
} catch (DBusExecutionException e) {
throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass()
.getSimpleName() + ")", e);
}
}
private void outputResult(final OutputWriter outputWriter, final Long timestamp, final GroupId groupId) {
if (outputWriter instanceof PlainTextWriter) {
final var writer = (PlainTextWriter) outputWriter;

View file

@ -42,7 +42,7 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand {
var about = ns.getString("about");
var aboutEmoji = ns.getString("about-emoji");
var avatarPath = ns.getString("avatar");
boolean removeAvatar = ns.getBoolean("remove-avatar");
boolean removeAvatar = Boolean.TRUE.equals(ns.getBoolean("remove-avatar"));
Optional<File> avatarFile = removeAvatar
? Optional.absent()

View file

@ -5,4 +5,8 @@ public final class UserErrorException extends CommandException {
public UserErrorException(final String message) {
super(message);
}
public UserErrorException(final String message, final Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,46 @@
package org.asamk.signal.dbus;
import org.asamk.Signal;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class DbusInterfacePropertiesHandler {
private final String interfaceName;
private final List<DbusProperty<?>> properties;
public DbusInterfacePropertiesHandler(
final String interfaceName, final List<DbusProperty<?>> properties
) {
this.interfaceName = interfaceName;
this.properties = properties;
}
public String getInterfaceName() {
return interfaceName;
}
@SuppressWarnings("unchecked")
private <T> DbusProperty<T> findProperty(String propertyName) {
final var property = properties.stream().filter(p -> p.getName().equals(propertyName)).findFirst();
if (property.isEmpty()) {
throw new Signal.Error.Failure("Property not found");
}
return (DbusProperty<T>) property.get();
}
<T> Consumer<T> getSetter(String propertyName) {
return this.<T>findProperty(propertyName).getSetter();
}
<T> Supplier<T> getGetter(String propertyName) {
return this.<T>findProperty(propertyName).getGetter();
}
Collection<DbusProperty<?>> getProperties() {
return properties;
}
}

View file

@ -0,0 +1,519 @@
package org.asamk.signal.dbus;
import org.asamk.Signal;
import org.asamk.signal.DbusConfig;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotMasterDeviceException;
import org.asamk.signal.manager.StickerPackInvalidException;
import org.asamk.signal.manager.UntrustedIdentityException;
import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.Group;
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.SendMessageResults;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupLinkState;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupPermission;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.freedesktop.dbus.DBusPath;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.interfaces.DBusInterface;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* 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;
public DbusManagerImpl(final Signal signal, DBusConnection connection) {
this.signal = signal;
this.connection = connection;
}
@Override
public String getSelfNumber() {
return signal.getSelfNumber();
}
@Override
public void checkAccountState() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public Map<String, Pair<String, UUID>> areUsersRegistered(final Set<String> numbers) throws IOException {
final var numbersList = new ArrayList<>(numbers);
final var registered = signal.isRegistered(numbersList);
final var result = new HashMap<String, Pair<String, UUID>>();
for (var i = 0; i < numbersList.size(); i++) {
result.put(numbersList.get(i),
new Pair<>(numbersList.get(i), registered.get(i) ? UuidUtil.UNKNOWN_UUID : null));
}
return result;
}
@Override
public void updateAccountAttributes(final String deviceName) throws IOException {
if (deviceName != null) {
final var devicePath = signal.getThisDevice();
getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName);
}
}
@Override
public void updateConfiguration(
final Boolean readReceipts,
final Boolean unidentifiedDeliveryIndicators,
final Boolean typingIndicators,
final Boolean linkPreviews
) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void setProfile(
final String givenName,
final String familyName,
final String about,
final String aboutEmoji,
final Optional<File> avatar
) throws IOException {
signal.updateProfile(emptyIfNull(givenName),
emptyIfNull(familyName),
emptyIfNull(about),
emptyIfNull(aboutEmoji),
avatar == null ? "" : avatar.transform(File::getPath).or(""),
avatar != null && !avatar.isPresent());
}
@Override
public void unregister() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void deleteAccount() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public List<Device> getLinkedDevices() throws IOException {
final var thisDevice = signal.getThisDevice();
return signal.listDevices().stream().map(devicePath -> {
final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device");
return new Device((long) device.get("Id").getValue(),
(String) device.get("Name").getValue(),
(long) device.get("Created").getValue(),
(long) device.get("LastSeen").getValue(),
thisDevice.equals(devicePath));
}).collect(Collectors.toList());
}
@Override
public void removeLinkedDevices(final long deviceId) throws IOException {
final var devicePath = signal.getDevice(deviceId);
getRemoteObject(devicePath, Signal.Device.class).removeDevice();
}
@Override
public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException {
signal.addDevice(linkUri.toString());
}
@Override
public void setRegistrationLockPin(final Optional<String> pin) throws IOException, UnauthenticatedResponseException {
if (pin.isPresent()) {
signal.setPin(pin.get());
} else {
signal.removePin();
}
}
@Override
public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException {
throw new UnsupportedOperationException();
}
@Override
public List<Group> getGroups() {
final var groupIds = signal.getGroupIds();
return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList());
}
@Override
public SendGroupMessageResults quitGroup(
final GroupId groupId, final Set<RecipientIdentifier.Single> groupAdmins
) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException {
if (groupAdmins.size() > 0) {
throw new UnsupportedOperationException();
}
signal.quitGroup(groupId.serialize());
return new SendGroupMessageResults(0, List.of());
}
@Override
public void deleteGroup(final GroupId groupId) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public Pair<GroupId, SendGroupMessageResults> createGroup(
final String name, final Set<RecipientIdentifier.Single> members, final File avatarFile
) throws IOException, AttachmentInvalidException {
final var newGroupId = signal.updateGroup(new byte[0],
emptyIfNull(name),
members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
avatarFile == null ? "" : avatarFile.getPath());
return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
}
@Override
public SendGroupMessageResults updateGroup(
final GroupId groupId,
final String name,
final String description,
final Set<RecipientIdentifier.Single> members,
final Set<RecipientIdentifier.Single> removeMembers,
final Set<RecipientIdentifier.Single> admins,
final Set<RecipientIdentifier.Single> removeAdmins,
final boolean resetGroupLink,
final GroupLinkState groupLinkState,
final GroupPermission addMemberPermission,
final GroupPermission editDetailsPermission,
final File avatarFile,
final Integer expirationTimer,
final Boolean isAnnouncementGroup
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException {
signal.updateGroup(groupId.serialize(),
emptyIfNull(name),
members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()),
avatarFile == null ? "" : avatarFile.getPath());
return new SendGroupMessageResults(0, List.of());
}
@Override
public Pair<GroupId, SendGroupMessageResults> joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException {
final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl());
return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of()));
}
@Override
public void sendTypingMessage(
final TypingAction action, final Set<RecipientIdentifier> recipients
) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
for (final var recipient : recipients) {
if (recipient instanceof RecipientIdentifier.Single) {
signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(),
action == TypingAction.STOP);
} else if (recipient instanceof RecipientIdentifier.Group) {
throw new UnsupportedOperationException();
}
}
}
@Override
public void sendReadReceipt(
final RecipientIdentifier.Single sender, final List<Long> messageIds
) throws IOException, UntrustedIdentityException {
signal.sendReadReceipt(sender.getIdentifier(), messageIds);
}
@Override
public void sendViewedReceipt(
final RecipientIdentifier.Single sender, final List<Long> messageIds
) throws IOException, UntrustedIdentityException {
throw new UnsupportedOperationException();
}
@Override
public SendMessageResults sendMessage(
final Message message, final Set<RecipientIdentifier> recipients
) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException {
return handleMessage(recipients,
numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers),
() -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()),
groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId));
}
@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
) 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 sendEndSessionMessage(final Set<RecipientIdentifier.Single> recipients) throws IOException {
signal.sendEndSessionMessage(recipients.stream()
.map(RecipientIdentifier.Single::getIdentifier)
.collect(Collectors.toList()));
return new SendMessageResults(0, Map.of());
}
@Override
public void setContactName(
final RecipientIdentifier.Single recipient, final String name
) throws NotMasterDeviceException, UnregisteredUserException {
signal.setContactName(recipient.getIdentifier(), name);
}
@Override
public void setContactBlocked(
final RecipientIdentifier.Single recipient, final boolean blocked
) throws NotMasterDeviceException, IOException {
signal.setContactBlocked(recipient.getIdentifier(), blocked);
}
@Override
public void setGroupBlocked(
final GroupId groupId, final boolean blocked
) throws GroupNotFoundException, IOException {
signal.setGroupBlocked(groupId.serialize(), blocked);
}
@Override
public void setExpirationTimer(
final RecipientIdentifier.Single recipient, final int messageExpirationTimer
) throws IOException {
signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer);
}
@Override
public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException {
try {
return new URI(signal.uploadStickerPack(path.getPath()));
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
@Override
public void requestAllSyncData() throws IOException {
signal.sendSyncRequest();
}
@Override
public void receiveMessages(
final long timeout,
final TimeUnit unit,
final boolean returnOnTimeout,
final boolean ignoreAttachments,
final ReceiveMessageHandler handler
) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public boolean hasCaughtUpWithOldMessages() {
throw new UnsupportedOperationException();
}
@Override
public boolean isContactBlocked(final RecipientIdentifier.Single recipient) {
return signal.isContactBlocked(recipient.getIdentifier());
}
@Override
public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) {
throw new UnsupportedOperationException();
}
@Override
public void sendContacts() throws IOException {
signal.sendContacts();
}
@Override
public List<Pair<RecipientAddress, Contact>> getContacts() {
throw new UnsupportedOperationException();
}
@Override
public String getContactOrProfileName(final RecipientIdentifier.Single recipient) {
return signal.getContactName(recipient.getIdentifier());
}
@Override
public Group getGroup(final GroupId groupId) {
final var id = groupId.serialize();
return new Group(groupId,
signal.getGroupName(id),
null,
null,
signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()),
Set.of(),
Set.of(),
Set.of(),
signal.isGroupBlocked(id),
0,
false,
signal.isMember(id));
}
@Override
public List<Identity> getIdentities() {
throw new UnsupportedOperationException();
}
@Override
public List<Identity> getIdentities(final RecipientIdentifier.Single recipient) {
throw new UnsupportedOperationException();
}
@Override
public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) {
throw new UnsupportedOperationException();
}
@Override
public boolean trustIdentityVerifiedSafetyNumber(
final RecipientIdentifier.Single recipient, final String safetyNumber
) {
throw new UnsupportedOperationException();
}
@Override
public boolean trustIdentityVerifiedSafetyNumber(
final RecipientIdentifier.Single recipient, final byte[] safetyNumber
) {
throw new UnsupportedOperationException();
}
@Override
public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) {
throw new UnsupportedOperationException();
}
@Override
public String computeSafetyNumber(
final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey
) {
throw new UnsupportedOperationException();
}
@Override
public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) {
return address;
}
@Override
public void close() throws IOException {
}
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)
.collect(Collectors.toList());
if (singleRecipients.size() > 0) {
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(g -> g.groupId)
.collect(Collectors.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 devicePath, final Class<T> type) {
try {
return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type);
} catch (DBusException e) {
throw new AssertionError(e);
}
}
}

View file

@ -0,0 +1,66 @@
package org.asamk.signal.dbus;
import org.asamk.Signal;
import org.freedesktop.dbus.interfaces.Properties;
import org.freedesktop.dbus.types.Variant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public abstract class DbusProperties implements Properties {
private final List<DbusInterfacePropertiesHandler> handlers = new ArrayList<>();
protected void addPropertiesHandler(DbusInterfacePropertiesHandler handler) {
this.handlers.add(handler);
}
DbusInterfacePropertiesHandler getHandler(String interfaceName) {
final var handler = getHandlerOptional(interfaceName);
if (handler.isEmpty()) {
throw new Signal.Error.Failure("Property not found");
}
return handler.get();
}
private java.util.Optional<DbusInterfacePropertiesHandler> getHandlerOptional(final String interfaceName) {
return handlers.stream().filter(h -> h.getInterfaceName().equals(interfaceName)).findFirst();
}
@Override
@SuppressWarnings("unchecked")
public <A> A Get(final String interface_name, final String property_name) {
final var handler = getHandler(interface_name);
final var getter = handler.getGetter(property_name);
if (getter == null) {
throw new Signal.Error.Failure("Property not found");
}
return (A) getter.get();
}
@Override
public <A> void Set(final String interface_name, final String property_name, final A value) {
final var handler = getHandler(interface_name);
final var setter = handler.getSetter(property_name);
if (setter == null) {
throw new Signal.Error.Failure("Property not found");
}
setter.accept(value);
}
@Override
public Map<String, Variant<?>> GetAll(final String interface_name) {
final var handler = getHandlerOptional(interface_name);
if (handler.isEmpty()) {
return Map.of();
}
return handler.get()
.getProperties()
.stream()
.filter(p -> p.getGetter() != null)
.collect(Collectors.toMap(DbusProperty::getName, p -> new Variant<>(p.getGetter().get())));
}
}

View file

@ -0,0 +1,35 @@
package org.asamk.signal.dbus;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class DbusProperty<T> {
private final String name;
private final Supplier<T> getter;
private final Consumer<T> setter;
public DbusProperty(final String name, final Supplier<T> getter, final Consumer<T> setter) {
this.name = name;
this.getter = getter;
this.setter = setter;
}
public DbusProperty(final String name, final Supplier<T> getter) {
this.name = name;
this.getter = getter;
this.setter = null;
}
public String getName() {
return name;
}
public Consumer<T> getSetter() {
return setter;
}
public Supplier<T> getGetter() {
return getter;
}
}

View file

@ -160,7 +160,7 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl {
synchronized (receiveThreads) {
return receiveThreads.stream()
.map(Pair::first)
.map(Manager::getUsername)
.map(Manager::getSelfNumber)
.map(u -> new DBusPath(DbusConfig.getObjectPath(u)))
.collect(Collectors.toList());
}

View file

@ -5,7 +5,9 @@ import org.asamk.signal.BaseConfig;
import org.asamk.signal.manager.AttachmentInvalidException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.NotMasterDeviceException;
import org.asamk.signal.manager.StickerPackInvalidException;
import org.asamk.signal.manager.UntrustedIdentityException;
import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.TypingAction;
@ -15,19 +17,26 @@ import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.util.ErrorUtils;
import org.asamk.signal.util.Util;
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.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
@ -35,24 +44,31 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.asamk.signal.util.Util.getLegacyIdentifier;
public class DbusSignalImpl implements Signal {
private final Manager m;
private final DBusConnection connection;
private final String objectPath;
public DbusSignalImpl(final Manager m, final String objectPath) {
private DBusPath thisDevice;
private final List<DBusPath> devices = new ArrayList<>();
public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) {
this.m = m;
this.connection = connection;
this.objectPath = objectPath;
}
@Override
public boolean isRemote() {
return false;
public void initObjects() {
updateDevices();
}
public void close() {
unExportDevices();
}
@Override
@ -60,6 +76,72 @@ public class DbusSignalImpl implements Signal {
return objectPath;
}
@Override
public String getSelfNumber() {
return m.getSelfNumber();
}
@Override
public void addDevice(String uri) {
try {
m.addDeviceLink(new URI(uri));
} catch (IOException | InvalidKeyException e) {
throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage());
} catch (URISyntaxException e) {
throw new Error.InvalidUri(e.getClass().getSimpleName()
+ " Device link uri has invalid format: "
+ e.getMessage());
}
}
@Override
public DBusPath getDevice(long deviceId) {
updateDevices();
return new DBusPath(getDeviceObjectPath(objectPath, deviceId));
}
@Override
public List<DBusPath> listDevices() {
updateDevices();
return this.devices;
}
private void updateDevices() {
List<org.asamk.signal.manager.api.Device> linkedDevices;
try {
linkedDevices = m.getLinkedDevices();
} catch (IOException | Error.Failure e) {
throw new Error.Failure("Failed to get linked devices: " + e.getMessage());
}
unExportDevices();
linkedDevices.forEach(d -> {
final var object = new DbusSignalDeviceImpl(d);
final var deviceObjectPath = object.getObjectPath();
try {
connection.exportObject(object);
} catch (DBusException e) {
e.printStackTrace();
}
if (d.isThisDevice()) {
thisDevice = new DBusPath(deviceObjectPath);
}
this.devices.add(new DBusPath(deviceObjectPath));
});
}
private void unExportDevices() {
this.devices.stream().map(DBusPath::getPath).forEach(connection::unExportObject);
this.devices.clear();
}
@Override
public DBusPath getThisDevice() {
updateDevices();
return thisDevice;
}
@Override
public long sendMessage(final String message, final List<String> attachments, final String recipient) {
var recipients = new ArrayList<String>(1);
@ -71,7 +153,7 @@ public class DbusSignalImpl implements Signal {
public long sendMessage(final String message, final List<String> attachments, final List<String> recipients) {
try {
final var results = m.sendMessage(new Message(message, attachments),
getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
@ -101,7 +183,7 @@ public class DbusSignalImpl implements Signal {
) {
try {
final var results = m.sendRemoteDeleteMessage(targetSentTimestamp,
getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
checkSendMessageResults(results.getTimestamp(), results.getResults());
@ -153,9 +235,9 @@ public class DbusSignalImpl implements Signal {
try {
final var results = m.sendMessageReaction(emoji,
remove,
getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
targetSentTimestamp,
getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
checkSendMessageResults(results.getTimestamp(), results.getResults());
@ -175,7 +257,7 @@ public class DbusSignalImpl implements Signal {
var recipients = new ArrayList<String>(1);
recipients.add(recipient);
m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START,
getSingleRecipientIdentifiers(recipients, m.getUsername()).stream()
getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream()
.map(RecipientIdentifier.class::cast)
.collect(Collectors.toSet()));
} catch (IOException e) {
@ -189,10 +271,10 @@ public class DbusSignalImpl implements Signal {
@Override
public void sendReadReceipt(
final String recipient, final List<Long> timestamps
final String recipient, final List<Long> messageIds
) throws Error.Failure, Error.UntrustedIdentity {
try {
m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps);
m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds);
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
} catch (UntrustedIdentityException e) {
@ -200,13 +282,31 @@ public class DbusSignalImpl implements Signal {
}
}
@Override
public void sendContacts() {
try {
m.sendContacts();
} catch (IOException e) {
throw new Error.Failure("SendContacts error: " + e.getMessage());
}
}
@Override
public void sendSyncRequest() {
try {
m.requestAllSyncData();
} catch (IOException e) {
throw new Error.Failure("Request sync data error: " + e.getMessage());
}
}
@Override
public long sendNoteToSelfMessage(
final String message, final List<String> attachments
) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity {
try {
final var results = m.sendMessage(new Message(message, attachments),
Set.of(new RecipientIdentifier.NoteToSelf()));
Set.of(RecipientIdentifier.NoteToSelf.INSTANCE));
checkSendMessageResults(results.getTimestamp(), results.getResults());
return results.getTimestamp();
} catch (AttachmentInvalidException e) {
@ -221,7 +321,7 @@ public class DbusSignalImpl implements Signal {
@Override
public void sendEndSessionMessage(final List<String> recipients) {
try {
final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername()));
final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber()));
checkSendMessageResults(results.getTimestamp(), results.getResults());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
@ -255,7 +355,7 @@ public class DbusSignalImpl implements Signal {
try {
final var results = m.sendMessageReaction(emoji,
remove,
getSingleRecipientIdentifier(targetAuthor, m.getUsername()),
getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()),
targetSentTimestamp,
Set.of(new RecipientIdentifier.Group(getGroupId(groupId))));
checkSendMessageResults(results.getTimestamp(), results.getResults());
@ -271,13 +371,14 @@ public class DbusSignalImpl implements Signal {
// the profile name
@Override
public String getContactName(final String number) {
return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername()));
final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber()));
return name == null ? "" : name;
}
@Override
public void setContactName(final String number, final String name) {
try {
m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name);
m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name);
} catch (NotMasterDeviceException e) {
throw new Error.Failure("This command doesn't work on linked devices.");
} catch (UnregisteredUserException e) {
@ -285,10 +386,19 @@ public class DbusSignalImpl implements Signal {
}
}
@Override
public void setExpirationTimer(final String number, final int expiration) {
try {
m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getSelfNumber()), expiration);
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
@Override
public void setContactBlocked(final String number, final boolean blocked) {
try {
m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked);
m.setContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()), blocked);
} catch (NotMasterDeviceException e) {
throw new Error.Failure("This command doesn't work on linked devices.");
} catch (IOException e) {
@ -320,7 +430,7 @@ public class DbusSignalImpl implements Signal {
@Override
public String getGroupName(final byte[] groupId) {
var group = m.getGroup(getGroupId(groupId));
if (group == null) {
if (group == null || group.getTitle() == null) {
return "";
} else {
return group.getTitle();
@ -333,27 +443,17 @@ public class DbusSignalImpl implements Signal {
if (group == null) {
return List.of();
} else {
return group.getMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(Util::getLegacyIdentifier)
.collect(Collectors.toList());
return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList());
}
}
@Override
public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
try {
if (groupId.length == 0) {
groupId = null;
}
if (name.isEmpty()) {
name = null;
}
if (avatar.isEmpty()) {
avatar = null;
}
final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername());
groupId = nullIfEmpty(groupId);
name = nullIfEmpty(name);
avatar = nullIfEmpty(avatar);
final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber());
if (groupId == null) {
final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar));
checkSendMessageResults(results.second().getTimestamp(), results.second().getResults());
@ -392,6 +492,56 @@ public class DbusSignalImpl implements Signal {
return true;
}
@Override
public boolean isRegistered(String number) {
var result = isRegistered(List.of(number));
return result.get(0);
}
@Override
public List<Boolean> isRegistered(List<String> numbers) {
var results = new ArrayList<Boolean>();
if (numbers.isEmpty()) {
return results;
}
Map<String, Pair<String, UUID>> registered;
try {
registered = m.areUsersRegistered(new HashSet<>(numbers));
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
return numbers.stream().map(number -> {
var uuid = registered.get(number).second();
return uuid != null;
}).collect(Collectors.toList());
}
@Override
public void updateProfile(
String givenName,
String familyName,
String about,
String aboutEmoji,
String avatarPath,
final boolean removeAvatar
) {
try {
givenName = nullIfEmpty(givenName);
familyName = nullIfEmpty(familyName);
about = nullIfEmpty(about);
aboutEmoji = nullIfEmpty(aboutEmoji);
avatarPath = nullIfEmpty(avatarPath);
Optional<File> avatarFile = removeAvatar
? Optional.absent()
: avatarPath == null ? null : Optional.of(new File(avatarPath));
m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile);
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
@Override
public void updateProfile(
final String name,
@ -400,16 +550,28 @@ public class DbusSignalImpl implements Signal {
String avatarPath,
final boolean removeAvatar
) {
updateProfile(name, "", about, aboutEmoji, avatarPath, removeAvatar);
}
@Override
public void removePin() {
try {
if (avatarPath.isEmpty()) {
avatarPath = null;
}
Optional<File> avatarFile = removeAvatar
? Optional.absent()
: avatarPath == null ? null : Optional.of(new File(avatarPath));
m.setProfile(name, null, about, aboutEmoji, avatarFile);
m.setRegistrationLockPin(Optional.absent());
} catch (UnauthenticatedResponseException e) {
throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage());
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
throw new Error.Failure("Remove pin error: " + e.getMessage());
}
}
@Override
public void setPin(String registrationLockPin) {
try {
m.setRegistrationLockPin(Optional.of(registrationLockPin));
} catch (UnauthenticatedResponseException e) {
throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage());
} catch (IOException e) {
throw new Error.Failure("Set pin error: " + e.getMessage());
}
}
@ -424,10 +586,9 @@ public class DbusSignalImpl implements Signal {
// all numbers the system knows
@Override
public List<String> listNumbers() {
return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId),
return Stream.concat(m.getIdentities().stream().map(Identity::getRecipient),
m.getContacts().stream().map(Pair::first))
.map(m::resolveSignalServiceAddress)
.map(a -> a.getNumber().orNull())
.map(a -> a.getNumber().orElse(null))
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
@ -440,16 +601,19 @@ public class DbusSignalImpl implements Signal {
var contacts = m.getContacts();
for (var c : contacts) {
if (name.equals(c.second().getName())) {
numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())));
numbers.add(c.first().getLegacyIdentifier());
}
}
// Try profiles if no contact name was found
for (var identity : m.getIdentities()) {
final var recipientId = identity.getRecipientId();
final var address = m.resolveSignalServiceAddress(recipientId);
var number = address.getNumber().orNull();
final var address = identity.getRecipient();
var number = address.getNumber().orElse(null);
if (number != null) {
var profile = m.getRecipientProfile(recipientId);
Profile profile = null;
try {
profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address));
} catch (UnregisteredUserException ignored) {
}
if (profile != null && profile.getDisplayName().equals(name)) {
numbers.add(number);
}
@ -490,7 +654,7 @@ public class DbusSignalImpl implements Signal {
@Override
public boolean isContactBlocked(final String number) {
return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()));
return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()));
}
@Override
@ -509,7 +673,19 @@ public class DbusSignalImpl implements Signal {
if (group == null) {
return false;
} else {
return group.isMember(m.getSelfRecipientId());
return group.isMember();
}
}
@Override
public String uploadStickerPack(String stickerPackPath) {
File path = new File(stickerPackPath);
try {
return m.uploadStickerPack(path).toString();
} catch (IOException e) {
throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage());
} catch (StickerPackInvalidException e) {
throw new Error.Failure("Invalid sticker pack: " + e.getMessage());
}
}
@ -603,4 +779,61 @@ public class DbusSignalImpl implements Signal {
throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage());
}
}
private byte[] nullIfEmpty(final byte[] array) {
return array.length == 0 ? null : array;
}
private String nullIfEmpty(final String name) {
return name.isEmpty() ? null : name;
}
private static String getDeviceObjectPath(String basePath, long deviceId) {
return basePath + "/Devices/" + deviceId;
}
public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device {
private final org.asamk.signal.manager.api.Device device;
public DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) {
super();
super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device",
List.of(new DbusProperty<>("Id", device::getId),
new DbusProperty<>("Name",
() -> device.getName() == null ? "" : device.getName(),
this::setDeviceName),
new DbusProperty<>("Created", device::getCreated),
new DbusProperty<>("LastSeen", device::getLastSeen))));
this.device = device;
}
@Override
public String getObjectPath() {
return getDeviceObjectPath(objectPath, device.getId());
}
@Override
public void removeDevice() throws Error.Failure {
try {
m.removeLinkedDevices(device.getId());
updateDevices();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
private void setDeviceName(String name) {
if (!device.isThisDevice()) {
throw new Error.Failure("Only the name of this device can be changed");
}
try {
m.updateAccountAttributes(name);
// update device list
updateDevices();
} catch (IOException e) {
throw new Error.Failure(e.getMessage());
}
}
}
}

View file

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.asamk.signal.manager.Manager;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import static org.asamk.signal.util.Util.getLegacyIdentifier;
@ -26,7 +27,7 @@ public class JsonMention {
final int length;
JsonMention(SignalServiceDataMessage.Mention mention, Manager m) {
final var address = m.resolveSignalServiceAddress(mention.getUuid());
final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid()));
this.name = getLegacyIdentifier(address);
this.number = address.getNumber().orNull();
this.uuid = address.getUuid().toString();

View file

@ -86,7 +86,7 @@ public class JsonMessageEnvelope {
}
String name;
try {
name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getUsername()));
name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getSelfNumber()));
} catch (InvalidNumberException | NullPointerException e) {
name = null;
}

View file

@ -25,10 +25,10 @@ public class CommandUtil {
) throws UserErrorException {
final var recipientIdentifiers = new HashSet<RecipientIdentifier>();
if (isNoteToSelf) {
recipientIdentifiers.add(new RecipientIdentifier.NoteToSelf());
recipientIdentifiers.add(RecipientIdentifier.NoteToSelf.INSTANCE);
}
if (recipientStrings != null) {
final var localNumber = m.getUsername();
final var localNumber = m.getSelfNumber();
recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber));
}
if (groupIdStrings != null) {