import os import re import socket import subprocess import tempfile import unicodedata import warnings from bs4 import BeautifulSoup from typing import Dict from toot.exceptions import ConsoleError def str_bool(b): """Convert boolean to string, in the way expected by the API.""" return "true" if b else "false" def str_bool_nullable(b): """Similar to str_bool, but leave None as None""" return None if b is None else str_bool(b) def get_text(html): """Converts html to text, strips all tags.""" # Ignore warnings made by BeautifulSoup, if passed something that looks like # a file (e.g. a dot which matches current dict), it will warn that the file # should be opened instead of passing a filename. with warnings.catch_warnings(): warnings.simplefilter("ignore") text = BeautifulSoup(html.replace(''', "'"), "html.parser").get_text() return unicodedata.normalize('NFKC', text) def parse_html(html): """Attempt to convert html to plain text while keeping line breaks. Returns a list of paragraphs, each being a list of lines. """ paragraphs = re.split("]*>", html) # Convert
s to line breaks and remove empty paragraphs paragraphs = [re.split("
", p) for p in paragraphs if p] # Convert each line in each paragraph to plain text: return [[get_text(line) for line in p] for p in paragraphs] def format_content(content): """Given a Status contents in HTML, converts it into lines of plain text. Returns a generator yielding lines of content. """ paragraphs = parse_html(content) first = True for paragraph in paragraphs: if not first: yield "" for line in paragraph: yield line first = False def domain_exists(name): try: socket.gethostbyname(name) return True except OSError: return False def assert_domain_exists(domain): if not domain_exists(domain): raise ConsoleError("Domain {} not found".format(domain)) EOF_KEY = "Ctrl-Z" if os.name == 'nt' else "Ctrl-D" def multiline_input(): """Lets user input multiple lines of text, terminated by EOF.""" lines = [] while True: try: lines.append(input()) except EOFError: break return "\n".join(lines).strip() EDITOR_DIVIDER = "------------------------ >8 ------------------------" EDITOR_INPUT_INSTRUCTIONS = f""" {EDITOR_DIVIDER} Do not modify or remove the line above. Enter your toot above it. Everything below it will be ignored. """ def editor_input(editor: str, initial_text: str): """Lets user input text using an editor.""" tmp_path = _tmp_status_path() initial_text = (initial_text or "") + EDITOR_INPUT_INSTRUCTIONS if not _use_existing_tmp_file(tmp_path): with open(tmp_path, "w") as f: f.write(initial_text) f.flush() subprocess.run([editor, tmp_path]) with open(tmp_path) as f: return f.read().split(EDITOR_DIVIDER)[0].strip() def read_char(values, default): values = [v.lower() for v in values] while True: value = input().lower() if value == "": return default if value in values: return value def delete_tmp_status_file(): try: os.unlink(_tmp_status_path()) except FileNotFoundError: pass def _tmp_status_path() -> str: tmp_dir = tempfile.gettempdir() return f"{tmp_dir}/.status.toot" def _use_existing_tmp_file(tmp_path) -> bool: from toot.output import print_out if os.path.exists(tmp_path): print_out(f"Found a draft status at: {tmp_path}") print_out("[O]pen (default) or [D]elete? ", end="") char = read_char(["o", "d"], "o") return char == "o" return False def drop_empty_values(data: Dict) -> Dict: """Remove keys whose values are null""" return {k: v for k, v in data.items() if v is not None} def args_get_instance(instance, scheme, default=None): if not instance: return default if scheme == "http": _warn_scheme_deprecated() if instance.startswith("http"): return instance.rstrip("/") else: return f"{scheme}://{instance}" def _warn_scheme_deprecated(): from toot.output import print_err print_err("\n".join([ "--disable-https flag is deprecated and will be removed.", "Please specify the instance as URL instead.", "e.g. instead of writing:", " toot instance unsafehost.com --disable-https", "instead write:", " toot instance http://unsafehost.com\n" ]))