aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMirek Kratochvil <exa.exa@gmail.com>2025-10-11 21:12:19 +0200
committerMirek Kratochvil <exa.exa@gmail.com>2025-10-11 21:12:19 +0200
commitc0b4d8692bbd9d7dc02608d5edba49f51e216304 (patch)
tree8b08205acfef2b4ba0370f5fbdb5fcc29da24696
parent92d36ddc7e45d5b0826fc205e570a9c9624e5e3a (diff)
downloadpatchodon-c0b4d8692bbd9d7dc02608d5edba49f51e216304.tar.gz
patchodon-c0b4d8692bbd9d7dc02608d5edba49f51e216304.tar.bz2
post version 0, zero debugging yet
technically I'm vibecoding this and I'm the LLM here
-rwxr-xr-xpatchodon.py125
1 files changed, 123 insertions, 2 deletions
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__":