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
3.7 kB 138 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: re-exec under nix shell with websocket-client 16 os.execvp( 17 "nix", 18 [ 19 "nix", 20 "shell", 21 "nixpkgs#python3Packages.websocket-client", 22 "-c", 23 "python3", 24 __file__, 25 ] 26 + sys.argv[1:], 27 ) 28 29HEDGEDOC_URL = "https://pad.lassul.us" 30 31 32def js_length(s): 33 """Return JavaScript string length (UTF-16 code units, not Unicode codepoints).""" 34 return len(s.encode("utf-16-le")) // 2 35 36 37def main(): 38 if len(sys.argv) < 2: 39 print("Usage: pad-edit.py <pad-id> [file]", file=sys.stderr) 40 print(" echo 'content' | pad-edit.py <pad-id>", file=sys.stderr) 41 sys.exit(1) 42 43 pad_id = sys.argv[1] 44 45 # Read new content 46 if len(sys.argv) >= 3 and sys.argv[2] != "-": 47 with open(sys.argv[2]) as f: 48 new_content = f.read() 49 else: 50 new_content = sys.stdin.read() 51 52 # Strip single trailing newline (added by shell heredoc/echo) 53 if new_content.endswith("\n"): 54 new_content = new_content[:-1] 55 56 # Step 1: Get session cookie 57 ctx = ssl.create_default_context() 58 cj = http.cookiejar.CookieJar() 59 opener = urllib.request.build_opener( 60 urllib.request.HTTPCookieProcessor(cj), 61 urllib.request.HTTPSHandler(context=ctx), 62 ) 63 opener.open(f"{HEDGEDOC_URL}/{pad_id}") 64 cookie_str = "; ".join(f"{c.name}={c.value}" for c in cj) 65 if not cookie_str: 66 print("ERROR: Failed to get session cookie", file=sys.stderr) 67 sys.exit(1) 68 69 # Step 2: Connect websocket 70 ws_url = f"wss://pad.lassul.us/socket.io/?noteId={pad_id}&EIO=4&transport=websocket" 71 ws = websocket.create_connection( 72 ws_url, 73 header=[f"Cookie: {cookie_str}", f"Origin: {HEDGEDOC_URL}"], 74 sslopt={"cert_reqs": ssl.CERT_REQUIRED}, 75 ) 76 77 ws.recv() # handshake: 0{"sid":...} 78 ws.send("40") # namespace connect 79 80 # Read until doc event, handle pings 81 revision = None 82 old_js_len = None 83 for _ in range(20): 84 ws.settimeout(5) 85 msg = ws.recv() 86 if msg == "2": 87 ws.send("3") 88 continue 89 if '"doc"' in msg: 90 payload = json.loads(msg[2:]) 91 doc_data = payload[1] 92 revision = doc_data["revision"] 93 old_js_len = js_length(doc_data["str"]) 94 break 95 96 if revision is None: 97 print("ERROR: Did not receive document state", file=sys.stderr) 98 ws.close() 99 sys.exit(1) 100 101 # Step 3: Build and send OT operation 102 escaped_new = json.dumps(new_content, ensure_ascii=False) 103 104 if old_js_len == 0: 105 op = f"[{escaped_new}]" 106 else: 107 op = f"[-{old_js_len},{escaped_new}]" 108 109 ws.send(f'42["operation",{revision},{op}]') 110 111 # Step 4: Wait for ack 112 ack_received = False 113 for _ in range(20): 114 try: 115 ws.settimeout(5) 116 msg = ws.recv() 117 if msg == "2": 118 ws.send("3") 119 continue 120 if '"ack"' in msg: 121 ack_received = True 122 break 123 except websocket.WebSocketTimeoutException: 124 break 125 126 ws.close() 127 128 if ack_received: 129 print(f"✅ Updated {HEDGEDOC_URL}/{pad_id}") 130 else: 131 print( 132 f"⚠️ No ack received. Check {HEDGEDOC_URL}/{pad_id}", file=sys.stderr 133 ) 134 sys.exit(1) 135 136 137if __name__ == "__main__": 138 main()