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)
def group_rm_group(group_name: str, group_list: 'GroupList', settings: 'Settings', queues: Dict[bytes, 'Queue'], master_key: 'MasterKey'): """Remove group with it's members.""" if not yes(f"Remove group '{group_name}'?", head=1): raise FunctionReturn("Group removal aborted.") rm_logs = yes("Also remove logs for the group?", head=1) command = GROUP_DELETE_HEADER + group_name.encode() queue_command(command, settings, queues[COMMAND_PACKET_QUEUE]) if rm_logs: command = LOG_REMOVE_HEADER + group_name.encode() queue_command(command, settings, queues[COMMAND_PACKET_QUEUE]) with ignored(FunctionReturn): remove_logs(group_name, settings, master_key) if group_name not in group_list.get_list_of_group_names(): raise FunctionReturn(f"TxM has no group '{group_name}' to remove.") group = group_list.get_group(group_name) if group.has_members() and yes("Notify members about leaving the group?"): for member in group: queue_message(user_input=UserInput(group_name, MESSAGE), window =MockWindow(member.rx_account, [member]), settings =settings, m_queue =queues[MESSAGE_PACKET_QUEUE], header =GROUP_MSG_EXIT_GROUP_HEADER, log_as_ph =True) group_list.remove_group(group_name) raise FunctionReturn(f"Removed group '{group_name}'")
def test_user_input(self): # Setup user_input = UserInput('test_plaintext', FILE) # Test self.assertEqual(user_input.plaintext, 'test_plaintext') self.assertEqual(user_input.type, FILE)
def test_class(self): # Setup o_input = builtins.input input_list = [ '/', '', 'testmessage', '/file', '/nick Alice', 'testmessage', '/nick Alice', ' ' ] gen = iter(input_list) def mock_input(_): return str(next(gen)) builtins.input = mock_input window = Window(name='Alice', type='contact', window_contacts=[create_contact('Alice')]) settings = Settings() # Test user_input = UserInput(window, settings) self.assertEqual(user_input.plaintext, 'testmessage') self.assertEqual(user_input.type, 'message') user_input = UserInput(window, settings) self.assertEqual(user_input.plaintext, '/file') self.assertEqual(user_input.type, 'file') user_input = UserInput(window, settings) self.assertEqual(user_input.plaintext, 'nick Alice') self.assertEqual(user_input.type, 'command') window = Window(name='Testgroup', type='group', window_contacts=[]) user_input = UserInput(window, settings) self.assertEqual(user_input.plaintext, 'nick Alice') self.assertEqual(user_input.type, 'command') user_input = UserInput(window, settings) self.assertEqual(user_input.plaintext, 'clear') self.assertEqual(user_input.type, 'command') # Teardown builtins.input = o_input
def tx_loop( settings: 'Settings', queues: Dict[bytes, 'Queue'], gateway: 'Gateway', contact_list: 'ContactList', group_list: 'GroupList', master_key: 'MasterKey', file_no: int # stdin input file descriptor ) -> None: """Get input from user and process it accordingly. Tx side of TFC runs two processes -- input and output loop -- separate from one another. This approach allows queueing assembly packets and their output based on priority of different packets. tx_loop handles TxM-side functions excluding message encryption, output and hash ratchet key/counter updates in key_list database and log file writes. """ sys.stdin = os.fdopen(file_no) window = Window(contact_list, group_list) while True: try: readline.set_completer( get_tab_completer(contact_list, group_list, settings)) readline.parse_and_bind('tab: complete') window.update_group_win_members(group_list) while not contact_list.has_local_contact(): new_local_key(contact_list, settings, queues, gateway) while not contact_list.has_contacts(): add_new_contact(contact_list, group_list, settings, queues, gateway) while not window.is_selected(): window.select_tx_window(settings, queues) user_input = UserInput(window, settings) if user_input.type == 'message': queue_message(user_input, window, settings, queues[MESSAGE_PACKET_QUEUE]) elif user_input.type == 'file': queue_file(window, settings, queues[FILE_PACKET_QUEUE], gateway) elif user_input.type == 'command': process_command(user_input, window, settings, queues, contact_list, group_list, gateway, master_key) except (EOFError, FunctionReturn, KeyboardInterrupt): pass
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)
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)
def whisper(user_input: 'UserInput', window: 'TxWindow', settings: 'Settings', m_queue: 'Queue') -> None: """Send a message to contact that overrides enabled logging setting. The functionality of this feature is impossible to enforce, but if the recipient can be trusted, it can be used to send keys for to be imported files as well as off-the-record messages, without worrying they are stored into log files, ruining forward secrecy for imported (and later deleted) files. """ message = user_input.plaintext[len('whisper '):] queue_message(user_input=UserInput(message, MESSAGE), window=window, settings=settings, m_queue=m_queue, header=WHISPER_MESSAGE_HEADER, log_as_ph=True)