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("?p[^>]*>", 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"
]))