192 lines
7.4 KiB

import getpass
import os
import re
from urllib.parse import urlparse
import unicodedata
import yaml
from mastodon import Mastodon
def slugify(value, allow_unicode=False):
Taken from
Convert to ASCII if 'allow_unicode' is False. Convert spaces or repeated
dashes to single dashes. Remove characters that aren't alphanumerics,
underscores, or hyphens. Convert to lowercase. Also strip leading and
trailing whitespace, dashes, and underscores.
:param value: value to remove all bad characters
:param allow_unicode: Allow unicode characters in the final string (with false we'll get an ASCII string)
:return: the converted string without bad characters anymore
value = str(value)
if allow_unicode:
value = unicodedata.normalize('NFKC', value)
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
value = re.sub(r'[^\w\s-]', '', value.lower())
return re.sub(r'[-\s]+', '-', value).strip('-_')
def _token_file(instance_host: str, username: str = None) -> str:
Receives the access-token for a user on the instance.
:param instance_host: instance host name
:param username: username on the host
:return: the file to the access token
return os.path.join(
slugify(instance_host) + ('-' + slugify(username) if username else '') + '.usertoken')
def _secret_file(instance_host: str, name: str, default=False) -> str:
Receives the secret-file for the registered app on the specific instance.
:param instance_host: the mastodon instance
:param name: the app name
:param default: if true, we check the default registration and change the filename to the name plus instance-host
:return: the file name with the stored registration-IDs
secret_file = os.path.join(os.path.expanduser('~'), slugify(name) + '-' + slugify(instance_host) + '.secret')
if default:
secret_file_old = os.path.join(os.path.expanduser('~'), slugify(name) + '.secret')
if os.path.exists(secret_file_old) and not os.path.exists(secret_file):
# this was the old single-instance behaviour of - we repair it:
os.rename(secret_file_old, secret_file)
return secret_file
def login_ui(instance: str, def_username: str = None) -> (str, str):
Terminal UI to login into the given instance. The function can store the credentials and in future the interaction
will be skipped.
:param instance: mastodon instance URL
:param def_username: default username
:return: username and password
print(f"To login into '{urlparse(instance).hostname}'"
f", enter your account email-address and corresponding password")
username = input(f'Account E-mail [{def_username}]: '.ljust(32)) or def_username
password = getpass.getpass('Password: '.ljust(32))
return username, password
def open_or_create_app(
name: str = None, instance: str = None,
config_yaml=None, instance_id: str = "default",
username: str = None, password: str = None,
login=False, debug=False):
Checks if a secret file for the app name already exists and uses it as app registration or
creates a new registration and saves it in the secret file. The '.secret' file will be stored in
the home directory.
:param name: Name of the app
:param instance: instance (as URL, e.g.
:param config_yaml: a configuration file instead of parameters
:param instance_id: the configured instance name in the config_yaml file
:param username: username (mail address) to login (can be empty, if not needed)
:param password: password to login (can be empty, if not needed)
:param login: true for prompt a login (username and password)
:param debug: activates the debug requests from beginning
:return: the mastodon client with credentials
if config_yaml is not None:
if not os.path.isabs(config_yaml):
# First we check the home folder (will override the standard config from repository)
config_home = os.path.expanduser(os.path.join("~", config_yaml))
if os.path.exists(config_home):
config_yaml = config_home
if not os.path.exists(config_yaml):
raise ValueError(f"Given path '{config_yaml}' for the config-yaml file does not exist")
with open(config_yaml, 'r') as stream:
config = yaml.safe_load(stream)
if not config.get("application", None):
raise ValueError(f"Missing 'application' section in the yaml configuration")
if not config.get("instances", None):
raise ValueError(f"Missing 'instances' section in the yaml configuration")
instance_section = config["instances"].get(instance_id, None)
if not instance_section:
raise ValueError(f"Missing 'instances.{instance_id}' section in the yaml configuration")
if not name:
name = config["application"]["name"]
if not instance:
instance = instance_section.get("url", None)
if not username:
username = instance_section.get("username", None)
if not login:
login = instance_section.get("login", False)
if not debug:
debug = config["application"].get("debug", False)
if not name:
raise ValueError("Please specify a unique application name for your Mastodon access scripts")
if not instance:
raise ValueError("Please specify a Mastodon instance you want to connect")
instance_url = urlparse(instance)
if not instance_url.scheme:
raise ValueError("The Mastodon instance URL needs a scheme like 'https://")
if instance_url.hostname.endswith(""):
raise ValueError(f"Please specify a real Mastodon instance. "
f"'{instance_url.hostname}' is only for documentation and a placeholder")
secret_file = _secret_file(instance_url.hostname, name, instance_id == 'default')
if not os.path.exists(secret_file):
# Unknown registration, so we register this application
token_file = _token_file(instance_url.hostname, username)
if login and os.path.exists(token_file):
# We have a user-login and an existing access-token. A shortcut to directly login:
mastodon = Mastodon(
mastodon.debug_requests = debug
return mastodon
# Login as client application with the previos stored client registration data
mastodon = Mastodon(
mastodon.debug_requests = debug
if login:
# User Login required, ask for credentials:
username, password = login_ui(instance, def_username=username)
# Login with credentials and store the access token to the filesystem
mastodon.log_in(username, password, to_file=token_file)
# In any case of error we delete the credentials, if existing:
if os.path.exists(token_file):
return mastodon