This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / skills / hedgedoc / pad-edit.py
4.6 kB 153 lines
1#!/usr/bin/env python3 2"""Edit a HedgeDoc pad in-place via websocket (socket.io protocol).""" 3import sys 4import json 5import http.cookiejar 6import urllib.request 7import ssl 8 9try: 10 import websocket 11except ImportError: 12 import subprocess 13 import os 14 15 # Bootstrap: get the store path for websocket-client and re-exec with PYTHONPATH. 16 # Much faster than `nix shell` which sets up a full environment. 17 # Cache the store path in /tmp so subsequent runs skip nix entirely. 18 cache = "/tmp/.pad-edit-websocket-path" 19 store_path = None 20 if os.path.exists(cache): 21 store_path = open(cache).read().strip() 22 if not os.path.exists(store_path): 23 store_path = None 24 25 if not store_path: 26 store_path = subprocess.check_output( 27 ["nix", "build", "nixpkgs#python3Packages.websocket-client", 28 "--no-link", "--print-out-paths"], 29 text=True, 30 ).strip() 31 open(cache, "w").write(store_path) 32 33 # Find the site-packages dir inside the store path 34 import glob 35 site_dirs = glob.glob(f"{store_path}/lib/python*/site-packages") 36 if not site_dirs: 37 print("ERROR: Could not find site-packages in websocket-client store path", file=sys.stderr) 38 sys.exit(1) 39 40 env = os.environ.copy() 41 env["PYTHONPATH"] = site_dirs[0] + (":" + env["PYTHONPATH"] if "PYTHONPATH" in env else "") 42 os.execve(sys.executable, [sys.executable, __file__] + sys.argv[1:], env) 43 44HEDGEDOC_URL = "https://pad.lassul.us" 45 46 47def js_length(s): 48 """Return JavaScript string length (UTF-16 code units, not Unicode codepoints).""" 49 return len(s.encode("utf-16-le")) // 2 50 51 52def main(): 53 if len(sys.argv) < 2: 54 print("Usage: pad-edit.py <pad-id> [file]", file=sys.stderr) 55 print(" echo 'content' | pad-edit.py <pad-id>", file=sys.stderr) 56 sys.exit(1) 57 58 pad_id = sys.argv[1] 59 60 # Read new content 61 if len(sys.argv) >= 3 and sys.argv[2] != "-": 62 with open(sys.argv[2]) as f: 63 new_content = f.read() 64 else: 65 new_content = sys.stdin.read() 66 67 # Strip single trailing newline (added by shell heredoc/echo) 68 if new_content.endswith("\n"): 69 new_content = new_content[:-1] 70 71 # Step 1: Get session cookie 72 ctx = ssl.create_default_context() 73 cj = http.cookiejar.CookieJar() 74 opener = urllib.request.build_opener( 75 urllib.request.HTTPCookieProcessor(cj), 76 urllib.request.HTTPSHandler(context=ctx), 77 ) 78 opener.open(f"{HEDGEDOC_URL}/{pad_id}") 79 cookie_str = "; ".join(f"{c.name}={c.value}" for c in cj) 80 if not cookie_str: 81 print("ERROR: Failed to get session cookie", file=sys.stderr) 82 sys.exit(1) 83 84 # Step 2: Connect websocket 85 ws_url = f"wss://pad.lassul.us/socket.io/?noteId={pad_id}&EIO=4&transport=websocket" 86 ws = websocket.create_connection( 87 ws_url, 88 header=[f"Cookie: {cookie_str}", f"Origin: {HEDGEDOC_URL}"], 89 sslopt={"cert_reqs": ssl.CERT_REQUIRED}, 90 ) 91 92 ws.recv() # handshake: 0{"sid":...} 93 ws.send("40") # namespace connect 94 95 # Read until doc event, handle pings 96 revision = None 97 old_js_len = None 98 for _ in range(20): 99 ws.settimeout(5) 100 msg = ws.recv() 101 if msg == "2": 102 ws.send("3") 103 continue 104 if '"doc"' in msg: 105 payload = json.loads(msg[2:]) 106 doc_data = payload[1] 107 revision = doc_data["revision"] 108 old_js_len = js_length(doc_data["str"]) 109 break 110 111 if revision is None: 112 print("ERROR: Did not receive document state", file=sys.stderr) 113 ws.close() 114 sys.exit(1) 115 116 # Step 3: Build and send OT operation 117 escaped_new = json.dumps(new_content, ensure_ascii=False) 118 119 if old_js_len == 0: 120 op = f"[{escaped_new}]" 121 else: 122 op = f"[-{old_js_len},{escaped_new}]" 123 124 ws.send(f'42["operation",{revision},{op}]') 125 126 # Step 4: Wait for ack 127 ack_received = False 128 for _ in range(20): 129 try: 130 ws.settimeout(5) 131 msg = ws.recv() 132 if msg == "2": 133 ws.send("3") 134 continue 135 if '"ack"' in msg: 136 ack_received = True 137 break 138 except websocket.WebSocketTimeoutException: 139 break 140 141 ws.close() 142 143 if ack_received: 144 print(f"✅ Updated {HEDGEDOC_URL}/{pad_id}") 145 else: 146 print( 147 f"⚠️ No ack received. Check {HEDGEDOC_URL}/{pad_id}", file=sys.stderr 148 ) 149 sys.exit(1) 150 151 152if __name__ == "__main__": 153 main()