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