diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index f26eeb33..9f37145b 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -16,7 +16,6 @@
-
diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc
index 2f1b2ac2..35a578f4 100644
--- a/man/signal-cli.1.adoc
+++ b/man/signal-cli.1.adoc
@@ -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.
diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java
index 30d8f514..8c9c525f 100644
--- a/src/main/java/org/asamk/Signal.java
+++ b/src/main/java/org/asamk/Signal.java
@@ -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 getGroupIds();
String getGroupName(byte[] groupId);
diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java
new file mode 100644
index 00000000..a49fc798
--- /dev/null
+++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java
@@ -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.getList("contact")) {
+ try {
+ m.setContactBlocked(contact_number, true);
+ } catch (InvalidNumberException e) {
+ System.err.println(e.getMessage());
+ }
+ }
+
+ if (ns.getList("group") != null) {
+ for (String groupIdString : ns.getList("group")) {
+ try {
+ byte[] groupId = Util.decodeGroupId(groupIdString);
+ m.setGroupBlocked(groupId, true);
+ } catch (GroupIdFormatException | GroupNotFoundException e) {
+ System.err.println(e.getMessage());
+ }
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java
index aa53d339..1ad0987a 100644
--- a/src/main/java/org/asamk/signal/commands/Commands.java
+++ b/src/main/java/org/asamk/signal/commands/Commands.java
@@ -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());
diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
index 44f4d094..1d2b7b31 100644
--- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java
@@ -20,7 +20,7 @@ public class ListContactsCommand implements LocalCommand {
}
List 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;
}
diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
index 20e45900..9758b0e3 100644
--- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
+++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
@@ -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));
}
}
diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
new file mode 100644
index 00000000..be745cb0
--- /dev/null
+++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java
@@ -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.getList("contact")) {
+ try {
+ m.setContactBlocked(contact_number, false);
+ } catch (InvalidNumberException e) {
+ System.err.println(e.getMessage());
+ }
+ }
+
+ if (ns.getList("group") != null) {
+ for (String groupIdString : ns.getList("group")) {
+ try {
+ byte[] groupId = Util.decodeGroupId(groupIdString);
+ m.setGroupBlocked(groupId, false);
+ } catch (GroupIdFormatException | GroupNotFoundException e) {
+ System.err.println(e.getMessage());
+ }
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java
index 37091ad0..59061068 100644
--- a/src/main/java/org/asamk/signal/manager/Manager.java
+++ b/src/main/java/org/asamk/signal/manager/Manager.java
@@ -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 getGroupIds() {
List 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 addresses = new ArrayList<>();
+ for (ContactInfo record : account.getContactStore().getContacts()) {
+ if (record.blocked) {
+ addresses.add(record.getAddress());
+ }
+ }
+ List 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));
diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
index 291303b2..be69b40c 100644
--- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
+++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
@@ -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);
diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
index c43bd832..1a4e0ec2 100644
--- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
+++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java
@@ -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 members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color) {
+ public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection 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