Add support for blocking contacts and accounts (#260)

* Add blockContact and unblockContact subcommands

* Send blocked status in contacts sync

* Use only one method for blocking and unblocking

* Add blocking/unblocking for groups

* Prevent blocked messages from being printed

* Print blocked property in listContacts and listGroups commands

* Handle BlockedListMessages

* Store blocked state from incoming contact and group sync messages

* Minor changes and corrections

* Add block and unblock commands to man file (and also fix some headings of commands)
This commit is contained in:
Daniel Schäufele 2020-01-22 08:39:28 +01:00 committed by AsamK
parent 7f9379f78b
commit 8b9640ba14
11 changed files with 255 additions and 20 deletions

View file

@ -16,7 +16,6 @@
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
</codeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="0" />
<arrangement>
<rules />
</arrangement>

View file

@ -209,7 +209,7 @@ number::
the fingerprint.
updateProfile
--------------
~~~~~~~~~~~~~
Update the name and/or avatar image visible by message recipients for the current users.
The profile is stored encrypted on the Signal servers. The decryption key is sent
with every outgoing messages (excluding group messages).
@ -224,7 +224,7 @@ with every outgoing messages (excluding group messages).
Remove the avatar visible by message recipients.
updateContact
--------------
~~~~~~~~~~~~~
Update the info associated to a number on our contact list. This change is only
local but can be synchronized to other devices by using `sendContacts` (see
below).
@ -236,8 +236,32 @@ NUMBER::
*-n*, *--name*::
Specify the new name for this contact.
block
~~~~~
Block the given contacts or groups (no messages will be received). This change is only
local but can be synchronized to other devices by using `sendContacts` (see
below).
[CONTACT [CONTACT ...]]::
Specify the phone numbers of contacts that should be blocked.
*-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]::
Specify the group IDs that should be blocked in base64 encoding.
unblock
~~~~~~~
Unblock the given contacts or groups (messages will be received again). This change is only
local but can be synchronized to other devices by using `sendContacts` (see
below).
[CONTACT [CONTACT ...]]::
Specify the phone numbers of contacts that should be unblocked.
*-g* [GROUP [GROUP ...]], *--group* [GROUP [GROUP ...]]::
Specify the group IDs that should be unblocked in base64 encoding.
sendContacts
------------
~~~~~~~~~~~~
Send a synchronization message with the local contacts list to all linked devices.
This command should only be used if this is the master device.

View file

@ -25,6 +25,10 @@ public interface Signal extends DBusInterface {
void setContactName(String number, String name) throws InvalidNumberException;
void setContactBlocked(String number, boolean blocked) throws InvalidNumberException;
void setGroupBlocked(byte[] groupId, boolean blocked) throws GroupNotFoundException;
List<byte[]> getGroupIds();
String getGroupName(byte[] groupId);

View file

@ -0,0 +1,52 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
public class BlockCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("contact")
.help("Contact number")
.nargs("*");
subparser.addArgument("-g", "--group")
.help("Group ID")
.nargs("*");
subparser.help("Block the given contacts or groups (no messages will be received)");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
for (String contact_number : ns.<String>getList("contact")) {
try {
m.setContactBlocked(contact_number, true);
} catch (InvalidNumberException e) {
System.err.println(e.getMessage());
}
}
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
try {
byte[] groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, true);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());
}
}
}
return 0;
}
}

View file

@ -9,6 +9,7 @@ public class Commands {
static {
addCommand("addDevice", new AddDeviceCommand());
addCommand("block", new BlockCommand());
addCommand("daemon", new DaemonCommand());
addCommand("link", new LinkCommand());
addCommand("listContacts", new ListContactsCommand());
@ -25,6 +26,7 @@ public class Commands {
addCommand("updateContact", new UpdateContactCommand());
addCommand("setPin", new SetPinCommand());
addCommand("trust", new TrustCommand());
addCommand("unblock", new UnblockCommand());
addCommand("unregister", new UnregisterCommand());
addCommand("updateAccount", new UpdateAccountCommand());
addCommand("updateGroup", new UpdateGroupCommand());

View file

@ -20,7 +20,7 @@ public class ListContactsCommand implements LocalCommand {
}
List<ContactInfo> contacts = m.getContacts();
for (ContactInfo c : contacts) {
System.out.println(String.format("Number: %s Name: %s", c.number, c.name));
System.out.println(String.format("Number: %s Name: %s Blocked: %b", c.number, c.name, c.blocked));
}
return 0;
}

View file

@ -14,11 +14,11 @@ public class ListGroupsCommand implements LocalCommand {
private static void printGroup(GroupInfo group, boolean detailed) {
if (detailed) {
System.out.println(String.format("Id: %s Name: %s Active: %s Members: %s",
Base64.encodeBytes(group.groupId), group.name, group.active, group.members));
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b Members: %s",
Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked, group.members));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s", Base64.encodeBytes(group.groupId),
group.name, group.active));
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
Base64.encodeBytes(group.groupId), group.name, group.active, group.blocked));
}
}

View file

@ -0,0 +1,52 @@
package org.asamk.signal.commands;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.asamk.signal.GroupIdFormatException;
import org.asamk.signal.GroupNotFoundException;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.util.Util;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
public class UnblockCommand implements LocalCommand {
@Override
public void attachToSubparser(final Subparser subparser) {
subparser.addArgument("contact")
.help("Contact number")
.nargs("*");
subparser.addArgument("-g", "--group")
.help("Group ID")
.nargs("*");
subparser.help("Unblock the given contacts or groups (messages will be received again)");
}
@Override
public int handleCommand(final Namespace ns, final Manager m) {
if (!m.isRegistered()) {
System.err.println("User is not registered.");
return 1;
}
for (String contact_number : ns.<String>getList("contact")) {
try {
m.setContactBlocked(contact_number, false);
} catch (InvalidNumberException e) {
System.err.println(e.getMessage());
}
}
if (ns.<String>getList("group") != null) {
for (String groupIdString : ns.<String>getList("group")) {
try {
byte[] groupId = Util.decodeGroupId(groupIdString);
m.setGroupBlocked(groupId, false);
} catch (GroupIdFormatException | GroupNotFoundException e) {
System.err.println(e.getMessage());
}
}
}
return 0;
}
}

View file

@ -71,6 +71,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream;
@ -702,6 +703,35 @@ public class Manager implements Signal {
account.save();
}
@Override
public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException {
number = Utils.canonicalizeNumber(number, username);
ContactInfo contact = account.getContactStore().getContact(number);
if (contact == null) {
contact = new ContactInfo();
contact.number = number;
System.err.println("Adding and " + (blocked ? "blocking" : "unblocking") + " contact " + number);
} else {
System.err.println((blocked ? "Blocking" : "Unblocking") + " contact " + number);
}
contact.blocked = blocked;
account.getContactStore().updateContact(contact);
account.save();
}
@Override
public void setGroupBlocked(final byte[] groupId, final boolean blocked) throws GroupNotFoundException {
GroupInfo group = getGroup(groupId);
if (group == null) {
throw new GroupNotFoundException(groupId);
} else {
System.err.println((blocked ? "Blocking" : "Unblocking") + " group " + Base64.encodeBytes(groupId));
group.blocked = blocked;
account.getGroupStore().updateGroup(group);
account.save();
}
}
@Override
public List<byte[]> getGroupIds() {
List<GroupInfo> groups = getGroups();
@ -1170,7 +1200,9 @@ public class Manager implements Signal {
handleMessage(envelope, content, ignoreAttachments);
}
account.save();
handler.handleMessage(envelope, content, exception);
if (!isMessageBlocked(envelope, content)) {
handler.handleMessage(envelope, content, exception);
}
if (!(exception instanceof ProtocolUntrustedIdentityException)) {
File cacheFile = null;
try {
@ -1191,6 +1223,33 @@ public class Manager implements Signal {
}
}
private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) {
SignalServiceAddress source;
if (!envelope.isUnidentifiedSender() && envelope.hasSource()) {
source = envelope.getSourceAddress();
} else if (content != null) {
source = content.getSender();
} else {
return false;
}
ContactInfo sourceContact = getContact(source.getNumber().get());
if (sourceContact != null && sourceContact.blocked) {
return true;
}
if (content != null && content.getDataMessage().isPresent()) {
SignalServiceDataMessage message = content.getDataMessage().get();
if (message.getGroupInfo().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupInfo().get();
GroupInfo group = getGroup(groupInfo.getGroupId());
if (groupInfo.getType() == SignalServiceGroup.Type.DELIVER && group != null && group.blocked) {
return true;
}
}
}
return false;
}
private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
if (content != null) {
SignalServiceAddress sender;
@ -1226,7 +1285,14 @@ public class Manager implements Signal {
e.printStackTrace();
}
}
// TODO Handle rm.isBlockedListRequest(); rm.isConfigurationRequest();
if (rm.isBlockedListRequest()) {
try {
sendBlockedList();
} catch (UntrustedIdentityException | IOException e) {
e.printStackTrace();
}
}
// TODO Handle rm.isConfigurationRequest();
}
if (syncMessage.getGroups().isPresent()) {
File tmpFile = null;
@ -1245,6 +1311,7 @@ public class Manager implements Signal {
}
syncGroup.addMembers(g.getMembers());
syncGroup.active = g.isActive();
syncGroup.blocked = g.isBlocked();
if (g.getColor().isPresent()) {
syncGroup.color = g.getColor().get();
}
@ -1268,7 +1335,23 @@ public class Manager implements Signal {
}
}
if (syncMessage.getBlockedList().isPresent()) {
// TODO store list of blocked numbers
final BlockedListMessage blockedListMessage = syncMessage.getBlockedList().get();
for (SignalServiceAddress address : blockedListMessage.getAddresses()) {
if (address.getNumber().isPresent()) {
try {
setContactBlocked(address.getNumber().get(), true);
} catch (InvalidNumberException e) {
e.printStackTrace();
}
}
}
for (byte[] groupId : blockedListMessage.getGroupIds()) {
try {
setGroupBlocked(groupId, true);
} catch (GroupNotFoundException e) {
System.err.println("BlockedListMessage contained groupID that was not found in GroupStore: " + Base64.encodeBytes(groupId));
}
}
}
if (syncMessage.getContacts().isPresent()) {
File tmpFile = null;
@ -1312,9 +1395,7 @@ public class Manager implements Signal {
thread.messageExpirationTime = c.getExpirationTimer().get();
account.getThreadStore().updateThread(thread);
}
if (c.isBlocked()) {
// TODO store list of blocked numbers
}
contact.blocked = c.isBlocked();
account.getContactStore().updateContact(contact);
if (c.getAvatar().isPresent()) {
@ -1442,7 +1523,7 @@ public class Manager implements Signal {
out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
new ArrayList<>(record.getMembers()), createGroupAvatarAttachment(record.groupId),
record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null),
Optional.fromNullable(record.color), false));
Optional.fromNullable(record.color), record.blocked));
}
}
@ -1488,11 +1569,10 @@ public class Manager implements Signal {
}
byte[] profileKey = record.profileKey == null ? null : Base64.decode(record.profileKey);
// TODO store list of blocked numbers
boolean blocked = false;
out.write(new DeviceContact(record.getAddress(), Optional.fromNullable(record.name),
createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), blocked, Optional.fromNullable(info != null ? info.messageExpirationTime : null)));
Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), record.blocked,
Optional.fromNullable(info != null ? info.messageExpirationTime : null)));
}
if (account.getProfileKey() != null) {
@ -1525,6 +1605,22 @@ public class Manager implements Signal {
}
}
private void sendBlockedList() throws IOException, UntrustedIdentityException {
List<SignalServiceAddress> addresses = new ArrayList<>();
for (ContactInfo record : account.getContactStore().getContacts()) {
if (record.blocked) {
addresses.add(record.getAddress());
}
}
List<byte[]> groupIds = new ArrayList<>();
for (GroupInfo record : account.getGroupStore().getGroups()) {
if (record.blocked) {
groupIds.add(record.groupId);
}
}
sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds)));
}
private void sendVerifiedMessage(SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel) throws IOException, UntrustedIdentityException {
VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis());
sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));

View file

@ -19,6 +19,9 @@ public class ContactInfo {
@JsonProperty
public String profileKey;
@JsonProperty(defaultValue = "false")
public boolean blocked;
@JsonIgnore
public SignalServiceAddress getAddress() {
return new SignalServiceAddress(null, number);

View file

@ -23,6 +23,8 @@ public class GroupInfo {
public boolean active;
@JsonProperty
public String color;
@JsonProperty(defaultValue = "false")
public boolean blocked;
private long avatarId;
@ -30,12 +32,13 @@ public class GroupInfo {
this.groupId = groupId;
}
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<String> members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color) {
public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection<String> members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color, @JsonProperty("blocked") boolean blocked) {
this.groupId = groupId;
this.name = name;
this.members.addAll(members);
this.avatarId = avatarId;
this.color = color;
this.blocked = blocked;
}
@JsonIgnore