]> nmode's Git Repositories - signal-cli/commitdiff
Add support for blocking contacts and accounts (#260)
authorDaniel Schäufele <github@schaeufele.org>
Wed, 22 Jan 2020 07:39:28 +0000 (08:39 +0100)
committerAsamK <asamk@gmx.de>
Wed, 22 Jan 2020 07:39:28 +0000 (08:39 +0100)
* 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)

.idea/codeStyles/Project.xml
man/signal-cli.1.adoc
src/main/java/org/asamk/Signal.java
src/main/java/org/asamk/signal/commands/BlockCommand.java [new file with mode: 0644]
src/main/java/org/asamk/signal/commands/Commands.java
src/main/java/org/asamk/signal/commands/ListContactsCommand.java
src/main/java/org/asamk/signal/commands/ListGroupsCommand.java
src/main/java/org/asamk/signal/commands/UnblockCommand.java [new file with mode: 0644]
src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java
src/main/java/org/asamk/signal/storage/groups/GroupInfo.java

index f26eeb331ef98586bb87785c729e8d1cc20e4539..9f37145b97d39cc4df62d29e19955f0a1ea569c8 100644 (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>
index 2f1b2ac294f3afb10327f4c9f6331f774a8ec031..35a578f420f48334317f9ec973efaa6c0ed06f89 100644 (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.
 
index 30d8f5142da6463033880424b8e0fb4fa9c4b06d..8c9c525ff21a0755a5fa06bd7039367ca448c26b 100644 (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);
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 (file)
index 0000000..a49fc79
--- /dev/null
@@ -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;
+    }
+}
index aa53d3391d1d20a68b2b739f337732aac2383d66..1ad0987a373be22e569eef5304bac87e8fda2bc2 100644 (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());
index 44f4d0943b29ed00316148eaea1017055ce38e67..1d2b7b315c733eec29fb7e408831f698cc38ac2e 100644 (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;
     }
index 20e4590080141c3ef830143ab17307e282f4337c..9758b0e34b9fa555dff7415537294e84176af0dc 100644 (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));
         }
     }
 
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 (file)
index 0000000..be745cb
--- /dev/null
@@ -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;
+    }
+}
index 37091ad056d151b70b78d3bc79a91d10167e1bda..590610681b81d7409138d5f2ef4c75c26f2851bb 100644 (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));
index 291303b213969653b4a3ebfb890a2de4034ac033..be69b40c6ebc2d6aef1b55b5777d1a30e1bb3f89 100644 (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);
index c43bd83217ab8cb68997d9697fa36e84f76910d9..1a4e0ec24997dc95d69f3bac1f85f878321c7b57 100644 (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