diff --git a/patchodon.py b/patchodon.py index dc4d0fd..8fdf95f 100755 --- a/patchodon.py +++ b/patchodon.py @@ -3,9 +3,118 @@ import sys, os import argparse import requests +import hashlib +import time + +# NOTES: html2text: html2text +# the replies are listed by context, should be link-listed to avoid issues, +# should specify next hash to provide some kind of a filter +# visibility public+unlisted, all unlisted, all private, all direct VERSION = "0.1.0" +DPASTE_URL = "https://dpaste.com" + +STATUS_LENGTH_LIMIT = 400 + + +def trace(x): + sys.stderr.write(sys.argv[0] + ": " + x + "\n") + + +def api_token(args): + if args.debug_api_token: + return args.debug_api_token + if args.env_api_token: + return os.environ["PATCHODON_API_TOKEN"] + raise ("API token not specified") + + +def do_post_status(args, body, parent=None, optional=None): + if len(body) > STATUS_LENGTH_LIMIT: + raise ("required status body too long") + + if not args.instance_url: + raise ("mastodon instance not specified") + + token = api_token(args) + + headers = {"Authorization": f"Bearer {token}"} + st = body + ( + "\n" + optional[0 : (STATUS_LENGTH_LIMIT - len(body) - 1)] + if optional + else "" + ) + data = {status: st, visibility: "unlisted"} + if parent: + data["in_reply_to_id"] = parent + + r = requests.post( + args.instance_url + "/api/v1/statuses", data=data, headers=headers + ) + if r.status_code != 200: + raise ("mastodon status posting failed ({r.status_code})") + + return r.json.id + + +def do_pastebin_file(file): + # DPASTE API USE RULES: + # - user-agent must be set properly + # - 1 second between requests + trace(f"pasting {file}...") + r = requests.post( + DPASTE_URL + "/api/v2/", + data={ + "contents": open(file, "r").read(), + "syntax": "diff", + "title": os.path.basename(file), + "expiry_days": 30, + }, + headers={"User-agent": f"patchodon v{VERSION}"}, + ) + if r.status_code != 201: + raise (f"dpaste POST failed for `{file}'") + sleep(1.1) + return r.headers["location"] + ".txt" + + +def split_off_diff(s): + return s.split("\ndiff --git ")[0] + + +def do_post(args): + files = args.patchfile + if not files: + trace("reading patchfile series from stdin") + files = sys.stdin.readlines().map(lambda x: x.rstrip(chars="\n")) + n_patches = len(files) + hashes = files.map(lambda x: hashlib.sha1(open(x, "r").read()).hexdigest()) + short_hashes = hashes.map(lambda x: x[0:8]) + full_hash = hashlib.sha1(hashes.join(" ")).hexdigest() + paste_raw_urls = map(do_pastebin_file, files) + trace("posting the header...") + parent_post_id, url = do_post_status( + args, + args.subject + + f"\n[patchodon hash {full_hash} shorts {short_hashes.join(' ')}]", + ) + for fn, pst, hsh, series in zip( + files, paste_raw_urls, hashes, range(n_patches) + ): + trace(f"posting patch {series+1}/{n_patches}...") + parent_post_id, _ = do_post_status( + args, + f"[patchodon {series+1}/{n_patches} {hsh}\n{pst}\n", + parent=parent_post_id, + optional=split_off_diff(open(fn, "r").read()), + ) + print(url) + + +def do_get(args): + pass + def main(): ap = argparse.ArgumentParser( @@ -38,7 +147,13 @@ def main(): "-i", "--instance-url", help="mastodon instance URL to post to" ) post.add_argument( - "-r", "--recipient", help="recipient to tag in the initial post" + "-s", + "--subject", + default="", + help=( + "start of the initial post (e.g. to @tag someone and name the" + " project and topic)" + ), ) post.add_argument( "patchfile", @@ -93,7 +208,13 @@ def main(): ), ) args = ap.parse_args() - print(args) + + if args.command == "post": + do_post(args) + elif args.command == "get": + do_get(args) + else: + raise ("fatal: args borked") if __name__ == "__main__":