refactor!: Start major cli rewrite #1

This commit is contained in:
moanos [he/him] 2023-12-21 10:30:22 +01:00
parent 44939d275f
commit 9eeaed3b14

View File

@ -2,14 +2,53 @@
import argparse import argparse
import json import json
import logging import logging
from typing import List
import requests import requests
import os import os
import toml import toml
import validators
from fediverse_blocklist_tool.models import Instance from fediverse_blocklist_tool.models import Instance
from fediverse_blocklist_tool.helpers import blocklist_to_markdown, blocklist_to_toml, blocklist_to_csv, \ from fediverse_blocklist_tool.helpers import blocklist_to_markdown, blocklist_to_toml, blocklist_to_csv, \
blocklist_to_json blocklist_to_json
SUPPORTED_LOADING_FORMATS = ["toml", "csv", "json"]
def determine_location(location: str) -> (bool, str, str):
"""
Determines whether a location is remote and splits off an access token if necessary
:return: A set of (is_remote, location, access_token) where access token can be None
"""
# If the file extension is in supported formats the location is assumed to be local
if location.split(".")[-1] in SUPPORTED_LOADING_FORMATS:
return False, location, None
elif "@" in location:
access_token, remote_server = location.split("@")
else:
remote_server = location
access_token = None
# Validate that the domain is valid
if not validators.domain(remote_server):
error_message = f"Could not parse blocklist location: {location}. Local blocklists must have one of the following formats: {SUPPORTED_LOADING_FORMATS} or a valid domain"
logging.error(error_message)
raise ValueError(error_message)
return True, remote_server, access_token
def load_blocklist(location: str) -> List[Instance]:
"""Load blocklist from the given location
:param location: location of the blocklist. Remote locations are allowed to contain an access token. Examples: local_blocklist.toml, ACCESS_TOKEN@fediserver1.org
"""
is_remote, location, access_token = determine_location(location)
if not is_remote:
return load_blocklist_file(location)
else:
if access_token is None:
access_token = os.getenv('FBT_TOKEN')
return load_blocklist_from_instance(location, access_token)
def load_blocklist_file(filename: str) -> [Instance]: def load_blocklist_file(filename: str) -> [Instance]:
if filename.endswith("json"): if filename.endswith("json"):
@ -55,7 +94,7 @@ def exporter(blocklist, output=None, format: str = "toml", private: bool = False
exported_text = blocklist_to_toml(blocklist, private) exported_text = blocklist_to_toml(blocklist, private)
if format == "csv": if format == "csv":
exported_text = blocklist_to_csv(blocklist, private) exported_text = blocklist_to_csv(blocklist, private)
if format == "markdown": if format == "md":
exported_text = blocklist_to_markdown(blocklist, private) exported_text = blocklist_to_markdown(blocklist, private)
if format == "json": if format == "json":
exported_text = blocklist_to_json(blocklist, private) exported_text = blocklist_to_json(blocklist, private)
@ -68,91 +107,127 @@ def exporter(blocklist, output=None, format: str = "toml", private: bool = False
print(exported_text) print(exported_text)
def merge(input_file, merge_target, format: str = "toml", private: bool = False, overwrite=False): def merge_lists(input_blocklist, merge_target_blocklist, overwrite=False):
"""Shows a table in the CLI comparing the local and remote blocklist""" """Shows a table in the CLI comparing the local and remote blocklist"""
from rich.table import Table from rich.table import Table
from rich.console import Console from rich.console import Console
input_blocklist = load_blocklist_file(input_file)
merge_target_blocklist = load_blocklist_file(merge_target)
for input_instance in input_blocklist: for input_instance in input_blocklist:
# If the block is already there with the same parameters we do nothing # If the block is already there with the same parameters we do nothing
if input_instance in merge_target_blocklist: if input_instance in merge_target_blocklist:
continue continue
# Check if there is a domain in the merge target where the input domain is similar # Check if there is a domain in the merge target where the input domain is similar
try: try:
merge_target_instance = [merge_target_instance for merge_target_instance in merge_target_blocklist if input_instance.domain == merge_target_instance.domain][0] merge_target_instance = [merge_target_instance for merge_target_instance in merge_target_blocklist if
if not overwrite: input_instance.domain == merge_target_instance.domain][0]
key_input = "" key_input = ""
if not overwrite:
while key_input not in ("i", "O"): while key_input not in ("i", "O"):
print(f"Different settings for {input_instance.domain} detected.") print(f"Different settings for {input_instance.domain} detected.")
Instance.show_diff(input_instance, merge_target_instance) Instance.show_diff(input_instance, merge_target_instance)
key_input = input("Keep input (i) or original (o) [i/O]") key_input = input("Keep input (i) or original (O) [i/O]")
elif key_input == "i": elif key_input == "i":
merge_target_blocklist.append(merge_target_instance) merge_target_blocklist.append(merge_target_instance)
else: else:
merge_target_blocklist.append(input_instance) merge_target_blocklist.append(input_instance)
except KeyError: except KeyError:
pass pass
return merge_target_blocklist
def get_format_from_filename(filename: str) -> str:
detected_format = filename.split(".")[-1]
if detected_format not in ["toml", "csv", "md", "json"]:
detected_format = "toml"
return detected_format
def diff(args):
if len(args.blocklist) > 2:
raise NotImplemented("Comparing more than two blocklists is not yet supported")
blocklists_to_compare = args.blocklist
blocklist_a = load_blocklist_file(blocklists_to_compare[0])
blocklist_b = load_blocklist_file(blocklists_to_compare[1])
Instance.show_diffs(blocklist_a, blocklist_b)
def export(args):
blocklist_to_export = load_blocklist_file(args.blocklist_to_export)
export_format = get_format_from_filename(args.target_blocklist)
exporter(blocklist_to_export, args.target, export_format, args.private)
def deploy(args):
blocklist_to_deploy = load_blocklist_file(args.blocklist_to_deploy)
target_blocklist = load_blocklist_file(args.target_blocklist)
diffs = Instance.list_diffs(blocklist_to_deploy, target_blocklist)
Instance.apply_blocks_from_diff(diffs, args.server, token, args.no_delete)
def merge(args):
blocklist_to_merge = load_blocklist_file(args.blocklist_to_merge)
target_blocklist = load_blocklist_file(args.target)
export_format = get_format_from_filename(args.target)
merge_lists(blocklist_to_merge, target_blocklist, export_format)
def cli(): def cli():
parser = argparse.ArgumentParser(description='Compare, merge, export and deploy blocklist of a fediverse server') parser = argparse.ArgumentParser(description='Compare, merge, export and deploy blocklist of a fediverse server')
parser.add_argument('action', choices=['diff', 'deploy', 'export', 'merge'],
help="Either use 'diff' to check the difference between local blockĺist and the blocklist on "
"the server, 'deploy' to apply the current local blocklist or 'export' to export the remote "
"blocklist into a local file. merge can be used to merge a blocklist (given by -i) into "
"another (-o)")
parser.add_argument('-s', '--server', help="The address of the server where you want to deploy (e.g. "
"mastodon.social)")
parser.add_argument('-t', '--token', help="Authorization token")
parser.add_argument('-i', '--input-file', help="The blocklist to use")
parser.add_argument('-r', '--remote-blocklist', help="The remote blocklist as json for debugging reasons")
parser.add_argument('-o', '--output', help="Filename where to export the blocklist")
parser.add_argument('-v', '--verbose', action='store_true') parser.add_argument('-v', '--verbose', action='store_true')
parser.add_argument('-n', '--no-delete', action='store_true', help="Do not delete existing blocks") subparsers = parser.add_subparsers(required=True)
parser.add_argument('--format', default="toml", type=str, help="Export format: toml|markdown|csv|json")
parser.add_argument('--private', action='store_true', help="When the flag is set, private comment will also be " diff_parser = subparsers.add_parser('diff')
"exported.") diff_parser.add_argument('blocklist',
nargs="+",
type=str,
help="The blocklists you want to compare. Provide at least two. Example: "
"lockalblocklist.toml ACCESS_TOKEN@fediserver.org")
diff_parser.set_defaults(func=diff)
export_parser = subparsers.add_parser('export')
export_parser.add_argument('blocklist_to_export',
type=str,
help="The blocklist you want to export. Example: ACCESS_TOKEN@fediserver.org")
export_parser.add_argument('target',
type=str,
help="Filename where to save the blocklist. The extension will determine the format, "
"if none ist given it'll default to toml")
export_parser.add_argument('--private',
action='store_true',
default=False,
help="When the flag is set, private comment will also be exported.")
export_parser.set_defaults(func=export)
merge_parser = subparsers.add_parser('merge')
merge_parser.add_argument('blocklist_to_merge',
type=str,
help="The blocklist you want to merge to the target. Example: lockalblocklist.toml "
"ACCESS_TOKEN@fediserver.org")
merge_parser.add_argument('target',
type=str,
help="Merge target that will hold the merged blocklist. Example: lockalblocklist.toml "
"ACCESS_TOKEN@fediserver.org")
merge_parser.set_defaults(func=merge)
deploy_parser = subparsers.add_parser('deploy')
deploy_parser.add_argument('blocklist_to_deploy',
type=str,
help="The blocklist you want to deploy to the target. Example: lockalblocklist.toml "
"ACCESS_TOKEN@fediserver.org")
deploy_parser.add_argument('target',
type=str,
help="Deploy target that will hold the merged blocklist. Example: lockalblocklist.toml "
"ACCESS_TOKEN@fediserver.org")
deploy_parser.add_argument('-n', '--no-delete', action='store_true', help="Do not delete existing blocks")
deploy_parser.set_defaults(func=deploy)
args = parser.parse_args() args = parser.parse_args()
if args.verbose: if args.verbose:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
else: else:
logging.basicConfig(level=logging.WARN) logging.basicConfig(level=logging.WARN)
args.func(args)
if args.token:
token = args.token
else:
token = os.getenv('MBD_TOKEN')
"""Get a remote blocklist only when necessary"""
if args.action in ["diff", "deploy", "export"]:
"""if there is a remote blocklist provided load this instead of fetching it from a server (for debugging reasons)"""
if args.remote_blocklist:
remote_blocklist = load_blocklist_file(args.remote_blocklist)
else:
remote_blocklist = load_blocklist_from_instance(server=args.server, token=token)
"""Load local blocklist only when needed"""
if args.action in ["diff", "deploy", "merge"]:
if args.input_file:
blocklist_filename = args.input_file
else:
blocklist_filename = "../blocklist.toml"
try:
local_blocklist = load_blocklist_file(blocklist_filename)
except FileNotFoundError:
print("Local blocklist file was not found. Make sure to specify it's location via -i")
exit()
if args.action == "diff":
Instance.show_diffs(local_blocklist, remote_blocklist)
elif args.action == "deploy":
diffs = Instance.list_diffs(local_blocklist, remote_blocklist)
Instance.apply_blocks_from_diff(diffs, args.server, token, args.no_delete)
elif args.action == "export":
exporter(remote_blocklist, args.output, args.format, args.private)
elif args.action == "merge":
merge(args.input_file, args.output, args.format, args.private)
if __name__ == "__main__": if __name__ == "__main__":