From daf737fa08a1478cebc5a48f871d81f33a73ebf4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 13 Oct 2025 21:29:09 +0200 Subject: [PATCH] add visibility and dpaste expirations --- src/patchodon/__init__.py | 98 ++++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/src/patchodon/__init__.py b/src/patchodon/__init__.py index 64e28b0..db21732 100644 --- a/src/patchodon/__init__.py +++ b/src/patchodon/__init__.py @@ -54,7 +54,22 @@ def auth_headers(args): return {"Authorization": f"Bearer {token}"} -def do_post_status(args, body, parent=None, optional=None): +def post_visibility(args, is_head): + """ + choose the status visibility based on args and head-ness + """ + if args.direct: + return "direct" + if args.private: + return "private" + if args.public: + return "public" if is_head else "unlisted" + if args.all_public: + return "public" + return "public" if is_head else "unlisted" + + +def do_post_status(args, body, is_head, parent=None, optional=None): """ POST a new status with body, optionally in reply-to `parent` post ID, and with attached `optional` contents to body. @@ -67,7 +82,7 @@ def do_post_status(args, body, parent=None, optional=None): if optional else "" ) - data = {"status": st, "visibility": "direct"} # TODO parametrize direct + data = {"status": st, "visibility": post_visibility(args, is_head)} # visibility options: public head+unlisted, all unlisted, all private, all direct if parent: data["in_reply_to_id"] = parent @@ -86,29 +101,33 @@ def do_post_status(args, body, parent=None, optional=None): return (rj["id"], rj["url"]) -def do_pastebin_file(file): +def do_pastebin_file(args): """ Send the `file` to dpaste, returning URL for the raw file. """ - # DPASTE API USE RULES: - # - user-agent must be set properly - # - 1 second between requests - trace(f"sending `{file}' to dpaste...") - r = requests.post( - DPASTE_URL + "/api/v2/", - data={ - "content": Path(file).read_text(), - "syntax": "diff", - "title": os.path.basename(file), - "expiry_days": 1, # TODO remove after testing - }, - headers={"User-agent": f"patchodon v{__version__}"}, - timeout=300, # TODO passthrough args - ) - time.sleep(1.1) - if r.status_code != 201: - raise RuntimeError("dpaste POST failed for `{file}'") - return r.headers["location"] + ".txt" + + def f(file): + # DPASTE API USE RULES: + # - user-agent must be set properly + # - 1 second between requests + trace(f"sending `{file}' to dpaste...") + r = requests.post( + DPASTE_URL + "/api/v2/", + data={ + "content": Path(file).read_text(), + "syntax": "diff", + "title": os.path.basename(file), + "expiry_days": args.paste_expire_days, + }, + headers={"User-agent": f"patchodon v{__version__}"}, + timeout=args.timeout, + ) + time.sleep(1.1) + if r.status_code != 201: + raise RuntimeError("dpaste POST failed for `{file}'") + return r.headers["location"] + ".txt" + + return f def split_off_diff(s): @@ -150,12 +169,13 @@ def do_post(args): ) short_hashes = mapl(lambda x: x[0:8], hashes) full_hash = hashlib.sha1(" ".join(hashes).encode()).hexdigest() - paste_raw_urls = mapl(do_pastebin_file, files) + paste_raw_urls = mapl(do_pastebin_file(args), files) trace("posting the header...") parent_post_id, url = do_post_status( args, f"{mayline(args.recipient)}{mayline(args.subject)}" f"[patchodon: {full_hash} / {' '.join(short_hashes)}]", + True, ) for fn, pst, hsh, series in zip( files, paste_raw_urls, hashes, range(n_patches) @@ -166,6 +186,7 @@ def do_post(args): f"{mayline(args.recipient)}" f"[patchodon {series+1}/{n_patches} {hsh}]\n" f"{pst}\n", + False, parent=parent_post_id, optional=split_off_diff(Path(fn).read_text()), ) @@ -384,6 +405,12 @@ def main(): " target project and patch topic" ), ) + post.add_argument( + "-x", + "--paste-expire-days", + default=14, + help="how many days should dpaste.com hold the patches (default: 14)", + ) post.add_argument( "patchfile", nargs="*", @@ -393,6 +420,31 @@ def main(): " into patchodon)" ), ) + visibility = post.add_mutually_exclusive_group() + visibility.add_argument( + "--public", + action="store_true", + help=( + "post head status publicly, patches unlisted (this is the default)" + ), + ) + visibility.add_argument( + "--all-public", + action="store_true", + help="post head status and all patches publicly", + ) + visibility.add_argument( + "--private", + action="store_true", + help=( + "post statuses as private (visible by followers and recipient only)" + ), + ) + visibility.add_argument( + "--direct", + action="store_true", + help="post statuses as direct (visible only by the tagged recipients)", + ) get = cmds.add_parser("get") get.add_argument(