def pwd_prompt(message: str, lc: str, rc: str) -> str: """Prompt user to enter a password. :param message: Prompt message :param lc: Upper-left corner box character :param rc: Upper-right corner box character :return: Password from user """ upper_line = (lc + (len(message) + 3) * '─' + rc) title_line = ('│' + message + 3 * ' ' + '│') lower_line = ('└' + (len(message) + 3) * '─' + '┘') upper_line = upper_line.center(get_tty_w()) title_line = title_line.center(get_tty_w()) lower_line = lower_line.center(get_tty_w()) print(upper_line) print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) indent = title_line.find('│') user_input = getpass.getpass(indent * ' ' + f'│ {message}') return user_input
def print_groups(self) -> None: """Print list of groups.""" # Columns c1 = ['Group '] c2 = ['Logging'] c3 = ['Notify'] c4 = ['Members'] for g in self.groups: c1.append(g.name) c2.append('Yes' if g.log_messages else 'No') c3.append('Yes' if g.notifications else 'No') m_indent = 40 m_string = ', '.join(sorted([m.nick for m in g.members])) wrapper = textwrap.TextWrapper( width=max(1, (get_tty_w() - m_indent))) mem_lines = wrapper.fill(m_string).split('\n') f_string = mem_lines[0] + '\n' for l in mem_lines[1:]: f_string += m_indent * ' ' + l + '\n' c4.append(f_string) lst = [] for name, log_setting, notify_setting, members in zip(c1, c2, c3, c4): lst.append('{0:{4}} {1:{5}} {2:{6}} {3}'.format( name, log_setting, notify_setting, members, len(max(c1, key=len)) + 4, len(max(c2, key=len)) + 4, len(max(c3, key=len)) + 4)) print(lst[0] + '\n' + get_tty_w() * '─') print('\n'.join(str(l) for l in lst[1:]) + '\n')
def help_printer(tuple_list: List[Union[Tuple[str, str, bool]]]) -> None: """Print help menu, style depending on terminal width and display conditions. :param tuple_list: List of command-description-display tuples """ longest_command = '' for t in tuple_list: longest_command = max(t[0], longest_command, key=len) longest_command += ' ' # Add spacing for help_cmd, description, display_condition in tuple_list: if not display_condition: continue wrapper = textwrap.TextWrapper(width=max(1, (get_tty_w() - len(longest_command)))) desc_lines = wrapper.fill(description).split('\n') spacing = (len(longest_command) - len(help_cmd)) * ' ' print(help_cmd + spacing + desc_lines[0]) # Print wrapped description lines with indent if len(desc_lines) > 1: for line in desc_lines[1:]: print(len(longest_command) * ' ' + line) print('')
def phase(string: str, done: bool = False, head: int = 0, offset: int = 2) -> None: """Print name of next phase. Message about completion will be printed on same line. :param string: String to be printed :param done: Notify with custom message :param head: N.o. inserted new lines before print :param offset: Offset of message from center to left :return: None """ for _ in range(head): print('') if string == 'Done' or done: print(string) time.sleep(0.5) else: string = '{}... '.format(string) indent = ((get_tty_w() - (len(string) + offset)) // 2) * ' ' print(indent + string, end='', flush=True)
def redraw(self): """Draw file window frame.""" ft_found = False line_ctr = 0 longest_title = 0 tty_w = get_tty_w() for p in self.packet_list: if p.type == 'file' and len(p.assembly_pt_list) > 0: title = "{} ({}) from {} ".format(p.f_name, p.f_size, p.contact.nick) longest_title = max(longest_title, len(title)) for p in self.packet_list: if p.type == 'file' and len(p.assembly_pt_list) > 0: line_ctr += 1 ft_found = True title = "{} ({}) from {} ".format(p.f_name, p.f_size, p.contact.nick) title += (longest_title - len(title)) * ' ' bar_len = max(tty_w - (4 + len(title)), 1) ready = int((len(p.assembly_pt_list) / p.f_packets) * bar_len) missing = bar_len - ready bar = title + '[' + (ready - 1) * '=' + '>' + missing * ' ' + ']' print(bar) print_on_previous_line(reps=line_ctr) if not ft_found: c_print("No file transmissions currently in progress.", head=1, tail=1) print_on_previous_line(reps=3)
def box_input(title: str, default: str = '', head: int = 0, tail: int = 0, expected_len: int = 0, validator: Callable = None, validator_args: Any = None) -> str: """Display boxed prompt for user with title. :param title: Title for data to prompt :param default: Default return value :param head: Number of new lines to print before input :param tail: Number of new lines to print after input :param expected_len Expected length of input :param validator: Input validator function :param validator_args: Arguments required by the validator :return: Input from user """ for _ in range(head): print('') tty_w = get_tty_w() input_len = tty_w - 2 if expected_len == 0 else expected_len + 2 input_top_line = '┌' + input_len * '─' + '┐' input_line = '│' + input_len * ' ' + '│' input_bot_line = '└' + input_len * '─' + '┘' input_line_indent = (tty_w - len(input_line)) // 2 input_box_indent = input_line_indent * ' ' print(input_box_indent + input_top_line) print(input_box_indent + input_line) print(input_box_indent + input_bot_line) print(4 * CURSOR_UP_ONE_LINE) print(input_box_indent + '┌─┤' + title + '├') user_input = input(input_box_indent + '│ ') if user_input == '': print(2 * CURSOR_UP_ONE_LINE) print(input_box_indent + f'│ {default}') user_input = default if validator is not None: success, error_msg = validator(user_input, validator_args) if not success: c_print("Error: {}".format(error_msg), head=1) print_on_previous_line(reps=4, delay=1.5) return box_input(title, default, head, tail, expected_len, validator, validator_args) for _ in range(tail): print('') return user_input
def yes(prompt: str, head: int = 0, tail: int = 0) -> bool: """Prompt user a question that is answered with yes / no. :param prompt: Question to be asked :param head: Number of new lines to print before prompt :param tail: Number of new lines to print after prompt :return: True if user types 'y' or 'yes' False if user types 'n' or 'no' """ for _ in range(head): print('') prompt = "{} (y/n): ".format(prompt) tty_w = get_tty_w() upper_line = ('┌' + (len(prompt) + 5) * '─' + '┐') title_line = ('│' + prompt + 5 * ' ' + '│') lower_line = ('└' + (len(prompt) + 5) * '─' + '┘') upper_line = upper_line.center(tty_w) title_line = title_line.center(tty_w) lower_line = lower_line.center(tty_w) indent = title_line.find('│') print(upper_line) print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) while True: print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) answer = input(indent * ' ' + f'│ {prompt}') print_on_previous_line() if answer == '': continue if answer.lower() in 'yes': print(indent * ' ' + f'│ {prompt}Yes │\n') for _ in range(tail): print('') return True elif answer.lower() in 'no': print(indent * ' ' + f'│ {prompt}No │\n') for _ in range(tail): print('') return False else: continue
def c_print(string: str, head: int = 0, tail: int = 0) -> None: """Print string to center of screen. :param string: String to print :param head: Number of new lines to print before string :param tail: Number of new lines to print after string :return: None """ for _ in range(head): print('') print(string.center(get_tty_w())) for _ in range(tail): print('')
def print(self, msg_tuple: Tuple['datetime.datetime', str, str, bytes]) -> None: """Print new message to window.""" ts, message, account, origin = msg_tuple if self.type == 'command': nick = '-!-' else: window_nicks = [c.nick for c in self.window_contacts] + ['Me'] len_of_longest = len(max(window_nicks, key=len)) nick = 'Me' if origin == ORIGIN_USER_HEADER else self.contact_list.get_contact( account).nick indent = len_of_longest - len(nick) nick = indent * ' ' + nick + ':' if self.previous_msg_ts.date() != ts.date(): print(f"00:00 -!- Day changed to {str(ts.date())}") self.previous_msg_ts = ts timestamp = ts.strftime('%H:%M') ts_nick = f"{timestamp} {nick} " if not self.is_active and self.type == 'group': ts_nick += f"(group {self.name}) " wrapper = textwrap.TextWrapper(initial_indent=ts_nick, subsequent_indent=(len(ts_nick)) * ' ', width=get_tty_w()) wrapped = wrapper.fill(message) # Add bold-effect after wrapping so length of injected VT100 codes does not affect wrapping. wrapped = BOLD_ON + wrapped[:len(ts_nick )] + BOLD_OFF + wrapped[len(ts_nick):] if self.is_active: print(wrapped) else: self.unread_messages += 1 if self.contact_list.get_contact(account).notifications: # Preview only first line of long message if len(wrapped.split('\n')) > 1: print(wrapped.split('\n')[0][:-3] + '...') else: print(wrapped) print_on_previous_line(delay=self.settings.new_msg_notify_dur, flush=True)
def message_printer(message: str, head: int = 0, tail: int = 0) -> None: """Print long message in the middle of the screen. :param message: Message to print :param head: Number of new lines to print before message :param tail: Number of new lines to print after message :return: None """ for _ in range(head): print('') line_list = (textwrap.fill(message, min(49, (get_tty_w() - 6))).split('\n')) for l in line_list: c_print(l) for _ in range(tail): print('')
def box_print(msg_list: Union[str, list], manual_proceed: bool = False, head: int = 0, tail: int = 0) -> None: """Print message inside a box. :param msg_list: List of lines to print :param manual_proceed: Wait for user input before continuing :param head: Number of new lines to print before box :param tail: Number of new lines to print after box :return: None """ for _ in range(head): print('') if isinstance(msg_list, str): msg_list = [msg_list] tty_w = get_tty_w() widest = max(msg_list, key=len) msg_list = ['{:^{}}'.format(m, len(widest)) for m in msg_list] top_line = '┌' + (len(msg_list[0]) + 2) * '─' + '┐' bot_line = '└' + (len(msg_list[0]) + 2) * '─' + '┘' msg_list = ['│ {} │'.format(m) for m in msg_list] top_line = top_line.center(tty_w) msg_list = [m.center(tty_w) for m in msg_list] bot_line = bot_line.center(tty_w) print(top_line) for m in msg_list: print(m) print(bot_line) for _ in range(tail): print('') if manual_proceed: input('') print_on_previous_line()
def ask_confirmation_code() -> str: """Ask user to input confirmation code from RxM to verify local key has been installed.""" title = "Enter confirmation code (from RxM): " upper_line = ('┌' + (len(title) + 8) * '─' + '┐') title_line = ('│' + title + 8 * ' ' + '│') lower_line = ('└' + (len(title) + 8) * '─' + '┘') ttyw = get_tty_w() upper_line = upper_line.center(ttyw) title_line = title_line.center(ttyw) lower_line = lower_line.center(ttyw) print(upper_line) print(title_line) print(lower_line) print(3 * CURSOR_UP_ONE_LINE) indent = title_line.find('│') return input(indent * ' ' + f'│ {title}')
def print_contacts(self, spacing: bool = True) -> None: """Print list of contacts.""" # Columns c1 = ['Contact'] c2 = ['Logging'] c3 = ['Notify'] c4 = ['Files '] c5 = ['Key Ex'] c6 = ['Account'] for c in self.contacts: if c.rx_account == 'local': continue c1.append(c.nick) c2.append('Yes' if c.log_messages else 'No') c3.append('Yes' if c.notifications else 'No') c4.append('Accept' if c.file_reception else 'Reject') c5.append('PSK' if c.tx_fingerprint == bytes(32) else 'X25519') c6.append(c.rx_account) lst = [] for nick, log_setting, notify_setting, file_reception_setting, key_exchange, account in zip(c1, c2, c3, c4, c5, c6): lst.append('{0:{6}} {1:{7}} {2:{8}} {3:{9}} {4:{10}} {5}'.format( nick, log_setting, notify_setting, file_reception_setting, key_exchange, account, len(max(c1, key=len)) + 4, len(max(c2, key=len)) + 4, len(max(c3, key=len)) + 4, len(max(c4, key=len)) + 4, len(max(c5, key=len)) + 4, len(max(c6, key=len)) + 4)) if spacing: clear_screen() print('') lst.insert(1, get_tty_w() * '─') print('\n'.join(str(l) for l in lst)) print('\n')
def access_history(window: Union['Window', 'Window_'], contact_list: 'ContactList', settings: 'Settings', master_key: 'MasterKey', msg_to_load: int = 0, export: bool = False) -> None: """Decrypt 'msg_to_load' last messages from log database and display/export it. :param window: Window object :param contact_list: ContactList object :param settings: Settings object :param master_key: Master key object :param msg_to_load: Number of messages to load :param export: When True, write logged messages into plaintext file instead of printing them. :return: None """ def read_entry(): """Read encrypted log entry. Length | Data type --------|-------------------------------- 24 | XSalsa20 nonce 4 | Timestamp 4 | UTF-32 BOM 4*255 | Padded account (UTF-32) 1 | Origin header 1 | Assembly packet header 255 | Padded assembly packet (UTF-8) 16 | Poly1305 tag """ return log_file.read(1325) ensure_dir(f'{DIR_USER_DATA}/') file_name = f'{DIR_USER_DATA}/{settings.software_operation}_logs' if not os.path.isfile(file_name): raise FunctionReturn(f"Error: Could not find '{file_name}'.") log_file = open(file_name, 'rb') ts_message_list = [] # type: List[Tuple[str, str, bytes, str]] assembly_p_buffer = dict() group_timestamp = b'' for ct in iter(read_entry, b''): pt = auth_and_decrypt(ct, key=master_key.master_key) account = bytes_to_str(pt[5:1029]) if window.type == 'contact' and window.uid != account: continue t_stamp = parse_ts_bytes(pt[0:4], settings) origin_byte = pt[4:5] origin = origin_byte.decode() assembly_header = pt[1029:1030] assembly_pt = pt[1030:] if assembly_header == M_S_HEADER: depadded = rm_padding_bytes(assembly_pt) decompressed = zlib.decompress(depadded) if decompressed[:1] == PRIVATE_MESSAGE_HEADER: if window.type == 'group': continue decoded = decompressed[1:].decode() elif decompressed[:1] == GROUP_MESSAGE_HEADER: group_name, decoded = [f.decode() for f in decompressed[9:].split(US_BYTE)] if group_name != window.name: continue if group_timestamp == decompressed[1:9]: continue else: group_timestamp = decompressed[1:9] ts_message_list.append((t_stamp, account, origin_byte, decoded)) elif assembly_header == M_L_HEADER: assembly_p_buffer[origin + account] = assembly_pt elif assembly_header == M_A_HEADER: if (origin + account) in assembly_p_buffer: assembly_p_buffer[origin + account] += assembly_pt elif assembly_header == M_E_HEADER: if (origin + account) in assembly_p_buffer: assembly_p_buffer[origin + account] += assembly_pt pt_buf = assembly_p_buffer.pop(origin + account) inner_l = rm_padding_bytes(pt_buf) msg_key = inner_l[-32:] enc_msg = inner_l[:-32] decrypted = auth_and_decrypt(enc_msg, key=msg_key) decompressed = zlib.decompress(decrypted) if decompressed[:1] == PRIVATE_MESSAGE_HEADER: if window.type == 'group': continue decoded = decompressed[1:].decode() elif decompressed[:1] == GROUP_MESSAGE_HEADER: group_name, decoded = [f.decode() for f in decompressed[9:].split(US_BYTE)] if group_name != window.name: continue if group_timestamp == decompressed[1:9]: # Skip duplicates of outgoing messages continue else: group_timestamp = decompressed[1:9] ts_message_list.append((t_stamp, account, origin_byte, decoded)) elif assembly_header == M_C_HEADER: assembly_p_buffer.pop(origin + account, None) log_file.close() if not export: clear_screen() print('') tty_w = get_tty_w() system = dict(tx="TxM", rx="RxM", ut="Unittest")[settings.software_operation] m_dir = dict(tx="sent to", rx="to/from", ut="to/from")[settings.software_operation] f_name = open(f"{system} - Plaintext log ({window.name})", 'w+') if export else sys.stdout subset = '' if msg_to_load == 0 else f"{msg_to_load} most recent " title = textwrap.fill(f"Log file of {subset}message(s) {m_dir} {window.name}", tty_w) print(title, file=f_name) print(tty_w * '═', file=f_name) for timestamp, account, origin_, message in ts_message_list[-msg_to_load:]: nick = "Me" if origin_ == ORIGIN_USER_HEADER else contact_list.get_contact(account).nick print(textwrap.fill(f"{timestamp} {nick}:", tty_w), file=f_name) print('', file=f_name) print(textwrap.fill(message, tty_w), file=f_name) print('', file=f_name) print(tty_w * '─', file=f_name) if export: f_name.close() else: print('')
def test_get_tty_w(self): self.assertIsInstance(get_tty_w(), int)
def print_help(settings: 'Settings') -> None: """Print the list of commands.""" def help_printer(tuple_list: List[Union[Tuple[str, str, bool]]]) -> None: """Print help menu, style depending on terminal width and display conditions. :param tuple_list: List of command-description-display tuples """ longest_command = '' for t in tuple_list: longest_command = max(t[0], longest_command, key=len) longest_command += ' ' # Add spacing for help_cmd, description, display_condition in tuple_list: if not display_condition: continue wrapper = textwrap.TextWrapper(width=max(1, (get_tty_w() - len(longest_command)))) desc_lines = wrapper.fill(description).split('\n') spacing = (len(longest_command) - len(help_cmd)) * ' ' print(help_cmd + spacing + desc_lines[0]) # Print wrapped description lines with indent if len(desc_lines) > 1: for line in desc_lines[1:]: print(len(longest_command) * ' ' + line) print('') common = [("/about", "Show links to project resources", True), ("/add", "Add new contact", not settings.session_trickle), ("/cf", "Cancel file transmission to recipients", True), ("/cm", "Cancel message transmission to recipients", True), ("/clear, ' '", "Clear screens from TxM, RxM and IM client", True), ("/cmd, '//'", "Display command window on RxM", True), ("/exit", "Exit TFC on TxM, NH and RxM", True), ("/export (n)", "Export (n) messages from recipient's logfile", True), ("/file", "Send file to active contact/group", True), ("/fingerprints", "Print public key fingerprints of user and contact", True), ("/fe", "Encrypt and export file to NH", not settings.session_trickle), ("/fi", "Import file from NH to RxM", not settings.session_trickle), ("/fw", "Display file reception window on RxM", True), ("/help", "Display this list of commands", True), ("/history (n)", "Print (n) messages from recipient's logfile", True), ("/localkey", "Generate new local key pair", not settings.session_trickle), ("/logging {on,off}(' all')", "Change log_messages setting (for all contacts)", True), ("/msg", "Change active recipient", not settings.session_trickle), ("/names", "List contacts and groups", True), ("/nick N", "Change nickname of active recipient to N", True), ("/notify {on,off} (' all')", "Change notification settings (for all contacts)", True), ("/passwd {tx,rx}", "Change master password on TxM/RxM", not settings.session_trickle), ("/psk", "Open PSK import dialog on RxM", True), ("/reset", "Reset ephemeral session log on TxM/RxM/IM client", not settings.session_trickle), ("/rm A", "Remove account A from TxM and RxM", not settings.session_trickle), ("/set S V", "Change setting S to value V on TxM/RxM", not settings.session_trickle), ("/settings", "List settings, default values and descriptions", not settings.session_trickle), ("/store {on,off} (' all')", "Change file reception (for all contacts)", True), ("/unread, ' '", "List windows with unread messages on RxM", True), ("Shift + PgUp/PgDn", "Scroll terminal up/down", True)] groupc = [("/group create G A1 .. An", "Create group G and add accounts A1 .. An", not settings.session_trickle), ("/group add G A1 .. An", "Add accounts A1 .. An to group G", not settings.session_trickle), ("/group rm G A1 .. An", "Remove accounts A1 .. An from group G", not settings.session_trickle), ("/group rm G", "Remove group G", not settings.session_trickle)] terminal_width = get_tty_w() clear_screen() print(textwrap.fill("List of commands:", width=terminal_width)) print('') help_printer(common) print(terminal_width * '-') if settings.session_trickle: print('') else: print("Group management:\n") help_printer(groupc) print(terminal_width * '-' + '\n')
def print_settings(self) -> None: """Print list of settings, their current and default values and setting descriptions.""" # Common desc_d = { "format_of_logfiles": "Timestamp format of logged messages", "disable_gui_dialog": "True replaces Tkinter dialogs with CLI prompts", "m_members_in_group": "Max members in group (Must be same on TxM/RxM)", "m_number_of_groups": "Max number of groups (Must be same on TxM/RxM)", "m_number_of_accnts": "Max number of accounts (Must be same on TxM/RxM)", "serial_iface_speed": "The speed of serial interface in bauds per sec", "e_correction_ratio": "N/o byte errors serial datagrams can recover from", "log_msg_by_default": "Default logging setting for new contacts", "store_file_default": "True accepts files from new contacts by default", "n_m_notify_privacy": "Default message notification setting for new contacts", "log_dummy_file_a_p": "False disables storage of placeholder data for files", # TxM "txm_serial_adapter": "False uses system's integrated serial interface", "nh_bypass_messages": "False removes NH bypass interrupt messages", "confirm_sent_files": "False sends files without asking for confirmation", "double_space_exits": "True exits with doubles space, else clears screen", "trickle_connection": "True enables trickle connection to hide metadata", "trickle_stat_delay": "Static delay between trickle packets", "trickle_rand_delay": "Max random delay for timing obfuscation", "long_packet_rand_d": "True adds spam guard evading delay", "max_val_for_rand_d": "Maximum time for random spam guard evasion delay", # RxM "rxm_serial_adapter": "False uses system's integrated serial interface", "new_msg_notify_dur": "Number of seconds new msg notification appears" } clear_screen() tty_w = get_tty_w() print( "Setting name Current value Default value Description" ) print(tty_w * '-') for key in self.defaults: def_value = str(self.defaults[key]).ljust(len('%Y-%m-%d %H:%M:%S')) description = desc_d[key] wrapper = textwrap.TextWrapper(width=max(1, (tty_w - 59))) desc_lines = wrapper.fill(description).split('\n') current_value = str(self.__getattribute__(key)).ljust(17) print(f"{key} {current_value} {def_value} {desc_lines[0]}") # Print wrapped description lines with indent if len(desc_lines) > 1: for line in desc_lines[1:]: print(58 * ' ' + line) print('') print('\n')