"allDeclaredMethods":true,
"allDeclaredConstructors":true
},
+{
+ "name":"org.asamk.signal.commands.ListContactsCommand$JsonContact$JsonProfile",
+ "allDeclaredFields":true,
+ "queryAllDeclaredMethods":true,
+ "queryAllDeclaredConstructors":true,
+ "methods":[
+ {"name":"about","parameterTypes":[] },
+ {"name":"aboutEmoji","parameterTypes":[] },
+ {"name":"familyName","parameterTypes":[] },
+ {"name":"givenName","parameterTypes":[] },
+ {"name":"lastUpdateTimestamp","parameterTypes":[] },
+ {"name":"paymentAddress","parameterTypes":[] }
+ ]
+},
{
"name":"org.asamk.signal.commands.ListDevicesCommand$JsonDevice",
"allDeclaredFields":true,
{"name":"userId_"}
]
},
+{
+ "name":"org.signal.storageservice.protos.groups.GroupChanges",
+ "fields":[{"name":"groupChanges_"}]
+},
+{
+ "name":"org.signal.storageservice.protos.groups.GroupChanges$GroupChangeState",
+ "fields":[
+ {"name":"groupChange_"},
+ {"name":"groupState_"}
+ ]
+},
{
"name":"org.signal.storageservice.protos.groups.GroupInviteLink",
"fields":[
{
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"
},
+ {
+ "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DK\\E"
+ },
{
"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EC\\E"
},
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.asamk.signal.manager.storage.recipients.Recipient;
import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
import java.io.Closeable;
void sendContacts() throws IOException;
- List<Pair<RecipientAddress, Contact>> getContacts();
+ List<Recipient> getRecipients(
+ boolean onlyContacts,
+ Optional<Boolean> blocked,
+ Collection<RecipientIdentifier.Single> address,
+ Optional<String> name
+ );
String getContactOrProfileName(RecipientIdentifier.Single recipient);
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
-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.asamk.signal.manager.storage.recipients.Recipient;
import org.asamk.signal.manager.storage.recipients.RecipientId;
import org.asamk.signal.manager.storage.stickerPacks.JsonStickerPack;
import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
private SignalAccount account;
- private AccountFileUpdater accountFileUpdater;
private final SignalDependencies dependencies;
private final Context context;
String userAgent
) {
this.account = account;
- this.accountFileUpdater = accountFileUpdater;
final var sessionLock = new SignalSessionLock() {
private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
}
@Override
- public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+ public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
return context.getProfileHelper().getRecipientProfile(context.getRecipientHelper().resolveRecipient(recipient));
}
@Override
public SendMessageResults sendReadReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
- ) throws IOException {
+ ) {
final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ,
messageIds,
@Override
public SendMessageResults sendViewedReceipt(
RecipientIdentifier.Single sender, List<Long> messageIds
- ) throws IOException {
+ ) {
final var timestamp = System.currentTimeMillis();
var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED,
messageIds,
final RecipientIdentifier.Single sender,
final long timestamp,
final SignalServiceReceiptMessage receiptMessage
- ) throws IOException {
+ ) {
try {
final var result = context.getSendHelper()
.sendReceiptMessage(receiptMessage, context.getRecipientHelper().resolveRecipient(sender));
}
}
- private ArrayList<SignalServiceDataMessage.Mention> resolveMentions(final List<Message.Mention> mentionList) throws IOException, UnregisteredRecipientException {
+ private ArrayList<SignalServiceDataMessage.Mention> resolveMentions(final List<Message.Mention> mentionList) throws UnregisteredRecipientException {
final var mentions = new ArrayList<SignalServiceDataMessage.Mention>();
for (final var m : mentionList) {
final var recipientId = context.getRecipientHelper().resolveRecipient(m.recipient());
@Override
public void setContactName(
RecipientIdentifier.Single recipient, String name
- ) throws NotMasterDeviceException, IOException, UnregisteredRecipientException {
+ ) throws NotMasterDeviceException, UnregisteredRecipientException {
if (!account.isMasterDevice()) {
throw new NotMasterDeviceException();
}
final RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
- } catch (IOException | UnregisteredRecipientException e) {
+ } catch (UnregisteredRecipientException e) {
return false;
}
return context.getContactHelper().isContactBlocked(recipientId);
}
@Override
- public List<Pair<RecipientAddress, Contact>> getContacts() {
- return account.getContactStore()
- .getContacts()
- .stream()
- .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second()))
- .toList();
+ public List<Recipient> getRecipients(
+ boolean onlyContacts,
+ Optional<Boolean> blocked,
+ Collection<RecipientIdentifier.Single> recipients,
+ Optional<String> name
+ ) {
+ final var recipientIds = recipients.stream().map(a -> {
+ try {
+ return context.getRecipientHelper().resolveRecipient(a);
+ } catch (UnregisteredRecipientException e) {
+ return null;
+ }
+ }).filter(Objects::nonNull).collect(Collectors.toSet());
+ // refresh profiles of explicitly given recipients
+ context.getProfileHelper().refreshRecipientProfiles(recipientIds);
+ return account.getRecipientStore().getRecipients(onlyContacts, blocked, recipientIds, name);
}
@Override
final RecipientId recipientId;
try {
recipientId = context.getRecipientHelper().resolveRecipient(recipient);
- } catch (IOException | UnregisteredRecipientException e) {
+ } catch (UnregisteredRecipientException e) {
return null;
}
try {
identity = account.getIdentityKeyStore()
.getIdentity(context.getRecipientHelper().resolveRecipient(recipient));
- } catch (IOException | UnregisteredRecipientException e) {
+ } catch (UnregisteredRecipientException e) {
identity = null;
}
return identity == null ? List.of() : List.of(toIdentity(identity));
private boolean trustIdentity(
RecipientIdentifier.Single recipient, Function<RecipientId, Boolean> trustMethod
) throws UnregisteredRecipientException {
- RecipientId recipientId;
- try {
- recipientId = context.getRecipientHelper().resolveRecipient(recipient);
- } catch (IOException e) {
- return false;
- }
+ final var recipientId = context.getRecipientHelper().resolveRecipient(recipient);
final var updated = trustMethod.apply(recipientId);
if (updated && this.isReceiving()) {
context.getReceiveHelper().setNeedsToRetryFailedMessages(true);
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Base64;
+import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
return getRecipientProfile(recipientId, false);
}
+ public List<Profile> getRecipientProfiles(Collection<RecipientId> recipientIds) {
+ return getRecipientProfiles(recipientIds, false);
+ }
+
public void refreshRecipientProfile(RecipientId recipientId) {
getRecipientProfile(recipientId, true);
}
+ public void refreshRecipientProfiles(Collection<RecipientId> recipientIds) {
+ getRecipientProfiles(recipientIds, true);
+ }
+
public List<ProfileKeyCredential> getRecipientProfileKeyCredential(List<RecipientId> recipientIds) {
try {
account.getRecipientStore().setBulkUpdating(true);
return getRecipientProfile(account.getSelfRecipientId());
}
- public List<Profile> getRecipientProfile(List<RecipientId> recipientIds) {
+ private List<Profile> getRecipientProfiles(Collection<RecipientId> recipientIds, boolean force) {
+ final var profileStore = account.getProfileStore();
try {
account.getRecipientStore().setBulkUpdating(true);
final var profileFetches = Flowable.fromIterable(recipientIds)
- .filter(recipientId -> isProfileRefreshRequired(account.getProfileStore().getProfile(recipientId)))
+ .filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId)))
.map(recipientId -> retrieveProfile(recipientId,
SignalServiceProfile.RequestType.PROFILE).onErrorComplete());
Maybe.merge(profileFetches, 10).blockingSubscribe();
account.getRecipientStore().setBulkUpdating(false);
}
- return recipientIds.stream().map(r -> account.getProfileStore().getProfile(r)).toList();
+ return recipientIds.stream().map(profileStore::getProfile).toList();
}
private Profile getRecipientProfile(RecipientId recipientId, boolean force) {
return account.getRecipientStore().resolveRecipient(address);
}
- public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws IOException, UnregisteredRecipientException {
+ public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
final var recipientIds = new HashSet<RecipientId>(recipients.size());
for (var number : recipients) {
final var recipientId = resolveRecipient(number);
return recipientIds;
}
- public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws IOException, UnregisteredRecipientException {
+ public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
return account.getRecipientStore().resolveRecipient(ServiceId.from(uuidRecipient.uuid()));
} else {
final var senderKeyTargets = new HashSet<RecipientId>();
final var recipientList = new ArrayList<>(recipientIds);
- final var profiles = context.getProfileHelper().getRecipientProfile(recipientList).iterator();
+ final var profiles = context.getProfileHelper().getRecipientProfiles(recipientList).iterator();
for (final var recipientId : recipientList) {
final var profile = profiles.next();
if (profile == null || !profile.getCapabilities().contains(Profile.Capability.senderKey)) {
.toList();
}
+ public List<Recipient> getRecipients(
+ boolean onlyContacts, Optional<Boolean> blocked, Set<RecipientId> recipientIds, Optional<String> name
+ ) {
+ return recipients.values()
+ .stream()
+ .filter(r -> !onlyContacts || r.getContact() != null)
+ .filter(r -> blocked.isEmpty() || (
+ blocked.get() == (
+ r.getContact() != null && r.getContact().isBlocked()
+ )
+ ))
+ .filter(r -> recipientIds.isEmpty() || (recipientIds.contains(r.getRecipientId())))
+ .filter(r -> name.isEmpty()
+ || (r.getContact() != null && name.get().equals(r.getContact().getName()))
+ || (r.getProfile() != null && name.get().equals(r.getProfile().getDisplayName())))
+ .toList();
+ }
+
@Override
public void deleteContact(RecipientId recipientId) {
synchronized (recipients) {
package org.asamk.signal.commands;
+import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
+import org.asamk.signal.commands.exceptions.CommandException;
import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.storage.recipients.Contact;
+import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.output.JsonWriter;
import org.asamk.signal.output.OutputWriter;
import org.asamk.signal.output.PlainTextWriter;
+import org.asamk.signal.util.CommandUtil;
+import java.util.Base64;
+import java.util.Optional;
import java.util.UUID;
public class ListContactsCommand implements JsonRpcLocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
- subparser.help("Show a list of known contacts with names.");
+ subparser.help("Show a list of known contacts with names and profiles.");
+ subparser.addArgument("recipient").help("Specify one ore more phone numbers to show.").nargs("*");
+ subparser.addArgument("-a", "--all-recipients")
+ .action(Arguments.storeTrue())
+ .help("Include all known recipients, not only contacts.");
+ subparser.addArgument("--blocked")
+ .type(Boolean.class)
+ .help("Specify if only blocked or unblocked contacts should be shown (default: all contacts)");
+ subparser.addArgument("--name").help("Find contacts with the given contact or profile name.");
}
@Override
- public void handleCommand(final Namespace ns, final Manager m, final OutputWriter outputWriter) {
- var contacts = m.getContacts();
+ public void handleCommand(
+ final Namespace ns, final Manager m, final OutputWriter outputWriter
+ ) throws CommandException {
+ final var allRecipients = Boolean.TRUE.equals(ns.getBoolean("all-recipients"));
+ final var blocked = ns.getBoolean("blocked");
+ final var recipientStrings = ns.<String>getList("recipient");
+ final var recipientIdentifiers = CommandUtil.getSingleRecipientIdentifiers(recipientStrings, m.getSelfNumber());
+ final var name = ns.getString("name");
+ final var recipients = m.getRecipients(!allRecipients,
+ Optional.ofNullable(blocked),
+ recipientIdentifiers,
+ Optional.ofNullable(name));
if (outputWriter instanceof PlainTextWriter writer) {
- for (var c : contacts) {
- final var contact = c.second();
- writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}",
- c.first().getLegacyIdentifier(),
+ for (var r : recipients) {
+ final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact();
+ final var profile = r.getProfile() == null ? Profile.newBuilder().build() : r.getProfile();
+ writer.println("Number: {} Name: {} Profile name: {} Blocked: {} Message expiration: {}",
+ r.getAddress().getLegacyIdentifier(),
contact.getName(),
+ profile.getDisplayName(),
contact.isBlocked(),
contact.getMessageExpirationTime() == 0
? "disabled"
}
} else {
final var writer = (JsonWriter) outputWriter;
- final var jsonContacts = contacts.stream().map(contactPair -> {
- final var address = contactPair.first();
- final var contact = contactPair.second();
+ final var jsonContacts = recipients.stream().map(r -> {
+ final var address = r.getAddress();
+ final var contact = r.getContact() == null ? Contact.newBuilder().build() : r.getContact();
return new JsonContact(address.number().orElse(null),
address.uuid().map(UUID::toString).orElse(null),
contact.getName(),
contact.isBlocked(),
- contact.getMessageExpirationTime());
+ contact.getMessageExpirationTime(),
+ r.getProfile() == null
+ ? null
+ : new JsonContact.JsonProfile(r.getProfile().getLastUpdateTimestamp(),
+ r.getProfile().getGivenName(),
+ r.getProfile().getFamilyName(),
+ r.getProfile().getAbout(),
+ r.getProfile().getAboutEmoji(),
+ r.getProfile().getPaymentAddress() == null
+ ? null
+ : Base64.getEncoder()
+ .encodeToString(r.getProfile().getPaymentAddress())));
}).toList();
writer.write(jsonContacts);
}
}
- private record JsonContact(String number, String uuid, String name, boolean isBlocked, int messageExpirationTime) {}
+ private record JsonContact(
+ String number,
+ String uuid,
+ String name,
+ boolean isBlocked,
+ int messageExpirationTime,
+ JsonProfile profile
+ ) {
+
+ private record JsonProfile(
+ long lastUpdateTimestamp,
+ String givenName,
+ String familyName,
+ String about,
+ String aboutEmoji,
+ String paymentAddress
+ ) {}
+ }
}
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.Recipient;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.freedesktop.dbus.DBusMap;
import org.freedesktop.dbus.DBusPath;
}
@Override
- public List<Pair<RecipientAddress, Contact>> getContacts() {
- return signal.listNumbers().stream().map(n -> {
+ public List<Recipient> getRecipients(
+ final boolean onlyContacts,
+ final Optional<Boolean> blocked,
+ final Collection<RecipientIdentifier.Single> addresses,
+ final Optional<String> name
+ ) {
+ final var numbers = addresses.stream()
+ .filter(s -> s instanceof RecipientIdentifier.Number)
+ .map(s -> ((RecipientIdentifier.Number) s).number())
+ .collect(Collectors.toSet());
+ return signal.listNumbers().stream().filter(n -> addresses.isEmpty() || numbers.contains(n)).map(n -> {
+ final var contactBlocked = signal.isContactBlocked(n);
+ if (blocked.isPresent() && blocked.get() != contactBlocked) {
+ return null;
+ }
final var contactName = signal.getContactName(n);
- if (contactName.length() == 0) {
+ if (onlyContacts && contactName.length() == 0) {
+ return null;
+ }
+ if (name.isPresent() && !name.get().equals(contactName)) {
return null;
}
- return new Pair<>(new RecipientAddress(null, n),
- new Contact(contactName, null, 0, signal.isContactBlocked(n), false, false));
+ return Recipient.newBuilder()
+ .withAddress(new RecipientAddress(null, n))
+ .withContact(new Contact(contactName, null, 0, contactBlocked, false, false))
+ .build();
}).filter(Objects::nonNull).toList();
}
import org.asamk.signal.BaseConfig;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.manager.api.AttachmentInvalidException;
-import org.asamk.signal.manager.api.Identity;
import org.asamk.signal.manager.api.InactiveGroupLinkException;
import org.asamk.signal.manager.api.InvalidDeviceLinkException;
import org.asamk.signal.manager.api.InvalidNumberException;
import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.NotMasterDeviceException;
-import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendMessageResult;
import org.asamk.signal.manager.api.SendMessageResults;
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.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.util.SendMessageResultUtils;
import org.freedesktop.dbus.DBusPath;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
public class DbusSignalImpl implements Signal {
// all numbers the system knows
@Override
public List<String> listNumbers() {
- return Stream.concat(m.getIdentities().stream().map(Identity::recipient),
- m.getContacts().stream().map(Pair::first))
- .map(a -> a.number().orElse(null))
+ return m.getRecipients(false, Optional.empty(), Set.of(), Optional.empty())
+ .stream()
+ .map(r -> r.getAddress().number().orElse(null))
.filter(Objects::nonNull)
.distinct()
.toList();
@Override
public List<String> getContactNumber(final String name) {
- // Contact names have precedence.
- var numbers = new ArrayList<String>();
- var contacts = m.getContacts();
- for (var c : contacts) {
- if (name.equals(c.second().getName())) {
- numbers.add(c.first().getLegacyIdentifier());
- }
- }
- // Try profiles if no contact name was found
- for (var identity : m.getIdentities()) {
- final var address = identity.recipient();
- var number = address.number().orElse(null);
- if (number != null) {
- Profile profile = null;
- try {
- profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address));
- } catch (IOException | UnregisteredRecipientException ignored) {
- }
- if (profile != null && profile.getDisplayName().equals(name)) {
- numbers.add(number);
- }
- }
- }
- return numbers;
+ return m.getRecipients(false, Optional.empty(), Set.of(), Optional.of(name))
+ .stream()
+ .map(r -> r.getAddress().getLegacyIdentifier())
+ .toList();
}
@Override