Esempio n. 1
0
def group_create(group_name:   str,
                 purp_members: List[bytes],
                 contact_list: 'ContactList',
                 group_list:   'GroupList',
                 settings:     'Settings',
                 queues:       'QueueDict',
                 _:            'MasterKey',
                 group_id:     Optional[bytes] = None
                 ) -> None:
    """Create a new group.
    
    Validate the group name and determine what members can be added.
    """
    error_msg = validate_group_name(group_name, contact_list, group_list)
    if error_msg:
        raise FunctionReturn(error_msg, head_clear=True)

    public_keys   = set(contact_list.get_list_of_pub_keys())
    purp_pub_keys = set(purp_members)
    accepted      = list(purp_pub_keys & public_keys)
    rejected      = list(purp_pub_keys - public_keys)

    if len(accepted) > settings.max_number_of_group_members:
        raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_group_members} "
                             f"members per group.", head_clear=True)

    if len(group_list) == settings.max_number_of_groups:
        raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_groups} groups.", head_clear=True)

    header = GROUP_MSG_INVITE_HEADER if group_id is None else GROUP_MSG_JOIN_HEADER

    if group_id is None:
        while True:
            group_id = os.urandom(GROUP_ID_LENGTH)
            if group_id not in group_list.get_list_of_group_ids():
                break

    group_list.add_group(group_name,
                         group_id,
                         settings.log_messages_by_default,
                         settings.show_notifications_by_default,
                         members=[contact_list.get_contact_by_pub_key(k) for k in accepted])

    command = GROUP_CREATE + group_id + group_name.encode() + US_BYTE + b''.join(accepted)
    queue_command(command, settings, queues)

    group_management_print(NEW_GROUP,        accepted, contact_list, group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name)

    if accepted:
        if yes("Publish the list of group members to participants?", abort=False):
            create_packet = header + group_id + b''.join(accepted)
            queue_to_nc(create_packet, queues[RELAY_PACKET_QUEUE])

    else:
        m_print(f"Created an empty group '{group_name}'.", bold=True, head=1)
Esempio n. 2
0
    def test_group_management_print(self):
        group_management_print(NEW_GROUP, self.lines, self.contact_list, self.group_name)
        self.assert_prints("""\
        ┌──────────────────────────────────────────────────────────────┐        
        │    Created new group 'test_group' with following members:    │        
        │   * Alice                                                    │        
        │   * zwp3dykiztmeils2u5eqjtdtx5x3kti5ktjthpkznku3ws5u5fq2bnad │        
        └──────────────────────────────────────────────────────────────┘        
""", group_management_print, NEW_GROUP, self.lines, self.contact_list, self.group_name)

        self.assert_prints("""\
        ┌──────────────────────────────────────────────────────────────┐        
        │       Added following accounts to group 'test_group':        │        
        │   * Alice                                                    │        
        │   * zwp3dykiztmeils2u5eqjtdtx5x3kti5ktjthpkznku3ws5u5fq2bnad │        
        └──────────────────────────────────────────────────────────────┘        
""", group_management_print, ADDED_MEMBERS, self.lines, self.contact_list, self.group_name)

        self.assert_prints("""\
        ┌──────────────────────────────────────────────────────────────┐        
        │    Following accounts were already in group 'test_group':    │        
        │   * Alice                                                    │        
        │   * zwp3dykiztmeils2u5eqjtdtx5x3kti5ktjthpkznku3ws5u5fq2bnad │        
        └──────────────────────────────────────────────────────────────┘        
""", group_management_print, ALREADY_MEMBER, self.lines, self.contact_list, self.group_name)

        self.assert_prints("""\
        ┌──────────────────────────────────────────────────────────────┐        
        │      Removed following members from group 'test_group':      │        
        │   * Alice                                                    │        
        │   * zwp3dykiztmeils2u5eqjtdtx5x3kti5ktjthpkznku3ws5u5fq2bnad │        
        └──────────────────────────────────────────────────────────────┘        
""", group_management_print, REMOVED_MEMBERS, self.lines, self.contact_list, self.group_name)

        self.assert_prints("""\
        ┌──────────────────────────────────────────────────────────────┐        
        │      Following accounts were not in group 'test_group':      │        
        │   * Alice                                                    │        
        │   * zwp3dykiztmeils2u5eqjtdtx5x3kti5ktjthpkznku3ws5u5fq2bnad │        
        └──────────────────────────────────────────────────────────────┘        
""", group_management_print, NOT_IN_GROUP, self.lines, self.contact_list, self.group_name)

        self.assert_prints("""\
        ┌──────────────────────────────────────────────────────────────┐        
        │           Following unknown accounts were ignored:           │        
        │   * Alice                                                    │        
        │   * zwp3dykiztmeils2u5eqjtdtx5x3kti5ktjthpkznku3ws5u5fq2bnad │        
        └──────────────────────────────────────────────────────────────┘        
""", group_management_print, UNKNOWN_ACCOUNTS, self.lines, self.contact_list, self.group_name)
Esempio n. 3
0
def group_create(group_name:   str,
                 purp_members: List[str],
                 group_list:   'GroupList',
                 contact_list: 'ContactList',
                 settings:     'Settings',
                 queues:       Dict[bytes, 'Queue'],
                 _:            'MasterKey') -> None:
    """Create a new group.
    
    Validate group name and determine what members that can be added.
    """
    validate_group_name(group_name, contact_list, group_list)

    accounts      = set(contact_list.get_list_of_accounts())
    purp_accounts = set(purp_members)
    accepted      = list(accounts      & purp_accounts)
    rejected      = list(purp_accounts - accounts)

    if len(accepted) > settings.max_number_of_group_members:
        raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group.")

    if len(group_list) == settings.max_number_of_groups:
        raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_groups} groups.")

    group_list.add_group(group_name,
                         settings.log_messages_by_default,
                         settings.show_notifications_by_default,
                         members=[contact_list.get_contact(c) for c in accepted])

    fields  = [f.encode() for f in ([group_name] + accepted)]
    command = GROUP_CREATE_HEADER + US_BYTE.join(fields)
    queue_command(command, settings, queues[COMMAND_PACKET_QUEUE])

    group_management_print(NEW_GROUP,        accepted, contact_list, group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list, group_name)

    if accepted:
        if yes("Publish list of group members to participants?"):
            for member in accepted:
                m_list = [m for m in accepted if m != member]
                queue_message(user_input=UserInput(US_STR.join([group_name] + m_list), MESSAGE),
                              window    =MockWindow(member, [contact_list.get_contact(member)]),
                              settings  =settings,
                              m_queue   =queues[MESSAGE_PACKET_QUEUE],
                              header    =GROUP_MSG_INVITEJOIN_HEADER,
                              log_as_ph =True)
    else:
        box_print(f"Created an empty group '{group_name}'", head=1)
Esempio n. 4
0
def group_create(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', group_list: 'GroupList',
                 settings: 'Settings') -> None:
    """Create a new group."""
    group_id, variable_len_data = separate_header(cmd_data, GROUP_ID_LENGTH)
    group_name_bytes, ser_members = variable_len_data.split(US_BYTE, 1)
    group_name = group_name_bytes.decode()

    purp_pub_keys = set(
        split_byte_string(ser_members, ONION_SERVICE_PUBLIC_KEY_LENGTH))
    pub_keys = set(contact_list.get_list_of_pub_keys())
    accepted = list(purp_pub_keys & pub_keys)
    rejected = list(purp_pub_keys - pub_keys)

    if len(accepted) > settings.max_number_of_group_members:
        raise SoftError(
            f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group."
        )

    if len(group_list) == settings.max_number_of_groups:
        raise SoftError(
            f"Error: TFC settings only allow {settings.max_number_of_groups} groups."
        )

    accepted_contacts = [
        contact_list.get_contact_by_pub_key(k) for k in accepted
    ]
    group_list.add_group(group_name, group_id,
                         settings.log_messages_by_default,
                         settings.show_notifications_by_default,
                         accepted_contacts)

    group = group_list.get_group(group_name)
    window = window_list.get_window(group.group_id)
    window.window_contacts = accepted_contacts
    window.message_log = []
    window.unread_messages = 0
    window.create_handle_dict()

    group_management_print(NEW_GROUP, accepted, contact_list, group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    cmd_win = window_list.get_window(WIN_UID_COMMAND)
    cmd_win.add_new(ts, f"Created new group {group_name}.")
Esempio n. 5
0
def group_rm_member(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                    contact_list: 'ContactList',
                    group_list: 'GroupList') -> None:
    """Remove member(s) from group."""
    fields = [f.decode() for f in cmd_data.split(US_BYTE)]
    group_name = fields[0]

    purp_accounts = set(fields[1:])
    accounts = set(contact_list.get_list_of_accounts())
    before_removal = set(
        group_list.get_group(group_name).get_list_of_member_accounts())
    ok_accounts_set = set(purp_accounts & accounts)
    removable_set = set(before_removal & ok_accounts_set)

    not_in_group = list(ok_accounts_set - before_removal)
    rejected = list(purp_accounts - accounts)
    removable = list(removable_set)

    group = group_list.get_group(group_name)
    group.remove_members(removable)

    window = window_list.get_window(group_name)
    window.remove_contacts(removable)

    group_management_print(REMOVED_MEMBERS, removable, contact_list,
                           group_name)
    group_management_print(NOT_IN_GROUP, not_in_group, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    local_win = window_list.get_window(LOCAL_ID)
    local_win.add_new(ts, f"Removed members from group {group_name}.")
Esempio n. 6
0
def group_add_member(group_name:   str,
                     purp_members: List['str'],
                     group_list:   'GroupList',
                     contact_list: 'ContactList',
                     settings:     'Settings',
                     queues:       Dict[bytes, 'Queue'],
                     master_key:   'MasterKey') -> None:
    """Add new member(s) to group."""
    if group_name not in group_list.get_list_of_group_names():
        if yes(f"Group {group_name} was not found. Create new group?", head=1):
            group_create(group_name, purp_members, group_list, contact_list, settings, queues, master_key)
            return None
        else:
            raise FunctionReturn("Group creation aborted.")

    purp_accounts    = set(purp_members)
    accounts         = set(contact_list.get_list_of_accounts())
    before_adding    = set(group_list.get_group(group_name).get_list_of_member_accounts())
    ok_accounts_set  = set(accounts        & purp_accounts)
    new_in_group_set = set(ok_accounts_set - before_adding)

    end_assembly = list(before_adding | new_in_group_set)
    rejected     = list(purp_accounts - accounts)
    already_in_g = list(before_adding & purp_accounts)
    new_in_group = list(new_in_group_set)
    ok_accounts  = list(ok_accounts_set)

    if len(end_assembly) > settings.max_number_of_group_members:
        raise FunctionReturn(f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group.")

    group = group_list.get_group(group_name)
    group.add_members([contact_list.get_contact(a) for a in new_in_group])

    fields  = [f.encode() for f in ([group_name] + ok_accounts)]
    command = GROUP_ADD_HEADER + US_BYTE.join(fields)
    queue_command(command, settings, queues[COMMAND_PACKET_QUEUE])

    group_management_print(ADDED_MEMBERS,    new_in_group, contact_list, group_name)
    group_management_print(ALREADY_MEMBER,   already_in_g, contact_list, group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected,     contact_list, group_name)

    if new_in_group:
        if yes("Publish new list of members to involved?"):
            for member in before_adding:
                queue_message(user_input=UserInput(US_STR.join([group_name] + new_in_group), MESSAGE),
                              window    =MockWindow(member, [contact_list.get_contact(member)]),
                              settings  =settings,
                              m_queue   =queues[MESSAGE_PACKET_QUEUE],
                              header    =GROUP_MSG_MEMBER_ADD_HEADER,
                              log_as_ph =True)

            for member in new_in_group:
                m_list = [m for m in end_assembly if m != member]
                queue_message(user_input=UserInput(US_STR.join([group_name] + m_list), MESSAGE),
                              window    =MockWindow(member, [contact_list.get_contact(member)]),
                              settings  =settings,
                              m_queue   =queues[MESSAGE_PACKET_QUEUE],
                              header    =GROUP_MSG_INVITEJOIN_HEADER,
                              log_as_ph =True)
Esempio n. 7
0
def group_add_member(group_name: str,
                     purp_members: List['bytes'],
                     contact_list: 'ContactList',
                     group_list: 'GroupList',
                     settings: 'Settings',
                     queues: 'QueueDict',
                     master_key: 'MasterKey',
                     _: Optional[bytes] = None) -> None:
    """Add new member(s) to a specified group."""
    if group_name not in group_list.get_list_of_group_names():
        if not yes(f"Group {group_name} was not found. Create new group?",
                   abort=False,
                   head=1):
            raise SoftError("Group creation aborted.",
                            head=0,
                            delay=1,
                            tail_clear=True)
        group_create(group_name, purp_members, contact_list, group_list,
                     settings, queues, master_key)
        return None

    purp_pub_keys = set(purp_members)
    pub_keys = set(contact_list.get_list_of_pub_keys())
    before_adding = set(
        group_list.get_group(group_name).get_list_of_member_pub_keys())
    ok_pub_keys_set = set(pub_keys & purp_pub_keys)
    new_in_group_set = set(ok_pub_keys_set - before_adding)

    end_assembly = list(before_adding | new_in_group_set)
    rejected = list(purp_pub_keys - pub_keys)
    already_in_g = list(before_adding & purp_pub_keys)
    new_in_group = list(new_in_group_set)
    ok_pub_keys = list(ok_pub_keys_set)

    if len(end_assembly) > settings.max_number_of_group_members:
        raise SoftError(
            f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group.",
            head_clear=True)

    group = group_list.get_group(group_name)
    group.add_members(
        [contact_list.get_contact_by_pub_key(k) for k in new_in_group])

    command = GROUP_ADD + group.group_id + b''.join(ok_pub_keys)
    queue_command(command, settings, queues)

    group_management_print(ADDED_MEMBERS, new_in_group, contact_list,
                           group_name)
    group_management_print(ALREADY_MEMBER, already_in_g, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    if new_in_group:
        if yes("Publish the list of new members to involved?", abort=False):
            add_packet = (GROUP_MSG_MEMBER_ADD_HEADER + group.group_id +
                          int_to_bytes(len(before_adding)) +
                          b''.join(before_adding) + b''.join(new_in_group))
            queue_to_nc(add_packet, queues[RELAY_PACKET_QUEUE])
Esempio n. 8
0
def group_create(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', group_list: 'GroupList',
                 settings: 'Settings') -> None:
    """Create a new group."""
    fields = [f.decode() for f in cmd_data.split(US_BYTE)]
    group_name = fields[0]

    purp_accounts = set(fields[1:])
    accounts = set(contact_list.get_list_of_accounts())
    accepted = list(accounts & purp_accounts)
    rejected = list(purp_accounts - accounts)

    if len(accepted) > settings.max_number_of_group_members:
        raise FunctionReturn(
            f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group."
        )

    if len(group_list) == settings.max_number_of_groups:
        raise FunctionReturn(
            f"Error: TFC settings only allow {settings.max_number_of_groups} groups."
        )

    accepted_contacts = [contact_list.get_contact(c) for c in accepted]
    group_list.add_group(group_name, settings.log_messages_by_default,
                         settings.show_notifications_by_default,
                         accepted_contacts)

    window = window_list.get_window(group_name)
    window.window_contacts = accepted_contacts
    window.message_log = []
    window.unread_messages = 0
    window.create_handle_dict()

    group_management_print(NEW_GROUP, accepted, contact_list, group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    local_win = window_list.get_window(LOCAL_ID)
    local_win.add_new(ts, f"Created new group {group_name}.")
Esempio n. 9
0
def group_rm_member(group_name: str,
                    purp_members: List[bytes],
                    contact_list: 'ContactList',
                    group_list: 'GroupList',
                    settings: 'Settings',
                    queues: 'QueueDict',
                    master_key: 'MasterKey',
                    _: Optional[bytes] = None) -> None:
    """Remove member(s) from the specified group or remove the group itself."""
    if not purp_members:
        group_rm_group(group_name, contact_list, group_list, settings, queues,
                       master_key)

    if group_name not in group_list.get_list_of_group_names():
        raise FunctionReturn(f"Group '{group_name}' does not exist.",
                             head_clear=True)

    purp_pub_keys = set(purp_members)
    pub_keys = set(contact_list.get_list_of_pub_keys())
    before_removal = set(
        group_list.get_group(group_name).get_list_of_member_pub_keys())
    ok_pub_keys_set = set(purp_pub_keys & pub_keys)
    removable_set = set(before_removal & ok_pub_keys_set)

    remaining = list(before_removal - removable_set)
    not_in_group = list(ok_pub_keys_set - before_removal)
    rejected = list(purp_pub_keys - pub_keys)
    removable = list(removable_set)
    ok_pub_keys = list(ok_pub_keys_set)

    group = group_list.get_group(group_name)
    group.remove_members(removable)

    command = GROUP_REMOVE + group.group_id + b''.join(ok_pub_keys)
    queue_command(command, settings, queues)

    group_management_print(REMOVED_MEMBERS, removable, contact_list,
                           group_name)
    group_management_print(NOT_IN_GROUP, not_in_group, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    if removable and remaining and yes(
            "Publish the list of removed members to remaining members?",
            abort=False):
        rem_packet = (GROUP_MSG_MEMBER_REM_HEADER + group.group_id +
                      int_to_bytes(len(remaining)) + b''.join(remaining) +
                      b''.join(removable))
        queue_to_nc(rem_packet, queues[RELAY_PACKET_QUEUE])
Esempio n. 10
0
def group_add(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
              contact_list: 'ContactList', group_list: 'GroupList',
              settings: 'Settings') -> None:
    """Add member(s) to group."""
    group_id, ser_members = separate_header(cmd_data, GROUP_ID_LENGTH)
    purp_pub_keys = set(
        split_byte_string(ser_members, ONION_SERVICE_PUBLIC_KEY_LENGTH))

    try:
        group_name = group_list.get_group_by_id(group_id).name
    except StopIteration:
        raise SoftError(
            f"Error: No group with ID '{b58encode(group_id)}' found.")

    pub_keys = set(contact_list.get_list_of_pub_keys())
    before_adding = set(
        group_list.get_group(group_name).get_list_of_member_pub_keys())
    ok_accounts = set(pub_keys & purp_pub_keys)
    new_in_group_set = set(ok_accounts - before_adding)

    end_assembly = list(before_adding | new_in_group_set)
    already_in_g = list(purp_pub_keys & before_adding)
    rejected = list(purp_pub_keys - pub_keys)
    new_in_group = list(new_in_group_set)

    if len(end_assembly) > settings.max_number_of_group_members:
        raise SoftError(
            f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group."
        )

    group = group_list.get_group(group_name)
    group.add_members(
        [contact_list.get_contact_by_pub_key(k) for k in new_in_group])

    window = window_list.get_window(group.group_id)
    window.add_contacts(new_in_group)
    window.create_handle_dict()

    group_management_print(ADDED_MEMBERS, new_in_group, contact_list,
                           group_name)
    group_management_print(ALREADY_MEMBER, already_in_g, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    cmd_win = window_list.get_window(WIN_UID_COMMAND)
    cmd_win.add_new(ts, f"Added members to group {group_name}.")
Esempio n. 11
0
def group_rm_member(group_name:   str,
                    purp_members: List[str],
                    group_list:   'GroupList',
                    contact_list: 'ContactList',
                    settings:     'Settings',
                    queues:       Dict[bytes, 'Queue'],
                    master_key:   'MasterKey') -> None:
    """Remove member(s) from group or group itself."""
    if not purp_members:
        group_rm_group(group_name, group_list, settings, queues, master_key)

    if group_name not in group_list.get_list_of_group_names():
        raise FunctionReturn(f"Group '{group_name}' does not exist.")

    purp_accounts   = set(purp_members)
    accounts        = set(contact_list.get_list_of_accounts())
    before_removal  = set(group_list.get_group(group_name).get_list_of_member_accounts())
    ok_accounts_set = set(purp_accounts  & accounts)
    removable_set   = set(before_removal & ok_accounts_set)

    end_assembly = list(before_removal  - removable_set)
    not_in_group = list(ok_accounts_set - before_removal)
    rejected     = list(purp_accounts   - accounts)
    removable    = list(removable_set)
    ok_accounts  = list(ok_accounts_set)

    group = group_list.get_group(group_name)
    group.remove_members(removable)

    fields  = [f.encode() for f in ([group_name] + ok_accounts)]
    command = GROUP_REMOVE_M_HEADER + US_BYTE.join(fields)
    queue_command(command, settings, queues[COMMAND_PACKET_QUEUE])

    group_management_print(REMOVED_MEMBERS,  removable,    contact_list, group_name)
    group_management_print(NOT_IN_GROUP,     not_in_group, contact_list, group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected,     contact_list, group_name)

    if removable and end_assembly and yes("Publish list of removed members to remaining members?"):
        for member in end_assembly:
            queue_message(user_input=UserInput(US_STR.join([group_name] + removable), MESSAGE),
                          window    =MockWindow(member, [contact_list.get_contact(member)]),
                          settings  =settings,
                          m_queue   =queues[MESSAGE_PACKET_QUEUE],
                          header    =GROUP_MSG_MEMBER_REM_HEADER,
                          log_as_ph =True)
Esempio n. 12
0
def group_add_member(cmd_data: bytes, ts: 'datetime',
                     window_list: 'WindowList', contact_list: 'ContactList',
                     group_list: 'GroupList', settings: 'Settings') -> None:
    """Add member(s) to group."""
    fields = [f.decode() for f in cmd_data.split(US_BYTE)]
    group_name = fields[0]

    purp_accounts = set(fields[1:])
    accounts = set(contact_list.get_list_of_accounts())
    before_adding = set(
        group_list.get_group(group_name).get_list_of_member_accounts())
    ok_accounts = set(accounts & purp_accounts)
    new_in_group_set = set(ok_accounts - before_adding)

    end_assembly = list(before_adding | new_in_group_set)
    rejected = list(purp_accounts - accounts)
    already_in_g = list(before_adding & purp_accounts)
    new_in_group = list(new_in_group_set)

    if len(end_assembly) > settings.max_number_of_group_members:
        raise FunctionReturn(
            f"Error: TFC settings only allow {settings.max_number_of_group_members} members per group."
        )

    group = group_list.get_group(group_name)
    group.add_members([contact_list.get_contact(a) for a in new_in_group])

    window = window_list.get_window(group_name)
    window.add_contacts(new_in_group)
    window.create_handle_dict()

    group_management_print(ADDED_MEMBERS, new_in_group, contact_list,
                           group_name)
    group_management_print(ALREADY_MEMBER, already_in_g, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    local_win = window_list.get_window(LOCAL_ID)
    local_win.add_new(ts, f"Added members to group {group_name}.")
Esempio n. 13
0
def group_remove(cmd_data: bytes, ts: 'datetime', window_list: 'WindowList',
                 contact_list: 'ContactList', group_list: 'GroupList') -> None:
    """Remove member(s) from the group."""
    group_id, ser_members = separate_header(cmd_data, GROUP_ID_LENGTH)
    purp_pub_keys = set(
        split_byte_string(ser_members, ONION_SERVICE_PUBLIC_KEY_LENGTH))

    try:
        group_name = group_list.get_group_by_id(group_id).name
    except StopIteration:
        raise SoftError(
            f"Error: No group with ID '{b58encode(group_id)}' found.")

    pub_keys = set(contact_list.get_list_of_pub_keys())
    before_removal = set(
        group_list.get_group(group_name).get_list_of_member_pub_keys())
    ok_accounts_set = set(purp_pub_keys & pub_keys)
    removable_set = set(before_removal & ok_accounts_set)

    not_in_group = list(ok_accounts_set - before_removal)
    rejected = list(purp_pub_keys - pub_keys)
    removable = list(removable_set)

    group = group_list.get_group(group_name)
    group.remove_members(removable)

    window = window_list.get_window(group.group_id)
    window.remove_contacts(removable)

    group_management_print(REMOVED_MEMBERS, removable, contact_list,
                           group_name)
    group_management_print(NOT_IN_GROUP, not_in_group, contact_list,
                           group_name)
    group_management_print(UNKNOWN_ACCOUNTS, rejected, contact_list,
                           group_name)

    cmd_win = window_list.get_window(WIN_UID_COMMAND)
    cmd_win.add_new(ts, f"Removed members from group {group_name}.")