patch
1 #!/usr/bin/python3 2 3 import html 4 from urllib.parse import quote_plus 5 import datetime 6 import time 7 import re 8 import string 9 import base64 10 import json 11 from util import * 12 from config import * 13 14 sys.excepthook = lambda t,v,tb: print('<pre>'+html.escape(''.join(traceback.format_exception(t,v,tb)))+'</pre>') 15 16 def print_error(msg): 17 print('<p class="error-message">'+html.escape(msg)+'</p>') 18 19 def print_info(msg): 20 print('<p>'+html.escape(msg)+'</p>') 21 22 HTML_HEAD=''' 23 <!doctype html> 24 <html lang="en"> 25 <head> 26 <meta charset="utf-8"> 27 <meta name="viewport" content="width=device-width, initial-scale=1"> 28 <title>Cradicle</title> 29 <link href="/css/bootstrap/bootstrap.min.css" rel="stylesheet"> 30 <style> 31 .bd-placeholder-img { 32 font-size: 1.125rem; 33 text-anchor: middle; 34 -webkit-user-select: none; 35 -moz-user-select: none; 36 user-select: none; 37 } 38 39 @media (min-width: 768px) { 40 .bd-placeholder-img-lg { 41 font-size: 3.5rem; 42 } 43 } 44 45 .b-example-divider { 46 height: 3rem; 47 background-color: rgba(0, 0, 0, .1); 48 border: solid rgba(0, 0, 0, .15); 49 border-width: 1px 0; 50 box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15); 51 } 52 53 .b-example-vr { 54 flex-shrink: 0; 55 width: 1.5rem; 56 height: 100vh; 57 } 58 59 .bi { 60 vertical-align: -.125em; 61 fill: currentColor; 62 } 63 64 .nav-scroller { 65 position: relative; 66 z-index: 2; 67 height: 2.75rem; 68 overflow-y: hidden; 69 } 70 71 .nav-scroller .nav { 72 display: flex; 73 flex-wrap: nowrap; 74 padding-bottom: 1rem; 75 margin-top: -1px; 76 overflow-x: auto; 77 text-align: center; 78 white-space: nowrap; 79 -webkit-overflow-scrolling: touch; 80 } 81 .diff-add { color: #22c55e; } 82 .diff-del { color: #ef4444; } 83 .diff-hunk { color: #6366f1; } 84 .diff-file { color: #facc15; font-weight: bold; } 85 </style> 86 <link rel="stylesheet" href="/assets/fontawesome/css/all.min.css"> 87 <link rel="icon" type="image/png" href="/favicon.png"> 88 ''' 89 90 print(HTML_HEAD) 91 92 form = FormData() 93 rid = form.getvalue('rid') 94 patch_id = form.getvalue('id') 95 node = form.getvalue('node') 96 tor = form.getvalue('tor') 97 98 ret,out,err = exec_command(crad_bin,["self","--alias"]) 99 self_alias = 'unknown user' 100 if not ret: 101 self_alias = html.escape(out.strip()) 102 ret,out,err = exec_command(crad_bin,["self","--did"]) 103 self_did = '' 104 if not ret: 105 self_did = html.escape(out.strip()) 106 107 repo_name = '' 108 if rid and len(rid): 109 ret,out,err = exec_command(crad_bin,['inspect','-R',rid,'--identity']) 110 if not ret: 111 doc = json.loads(out) 112 payload_val = doc['payload']['xyz.radicle.project'] 113 repo_name = payload_val['name'] 114 115 node_on = False 116 ret,out,err = exec_command(rad_node_status_wrapped_bin,['--only','nid']) 117 if not ret: 118 node_on = True 119 tor_on = False 120 ret,out,err = exec_command(crad_bin,['self','--network-mode']) 121 if not ret and out.strip() == 'tor': 122 tor_on = True 123 124 if tor == 'on' and not tor_on: 125 set_config_tor(True) 126 if node_on and node != 'off': 127 node = 'restart' 128 elif tor == 'off' and tor_on: 129 set_config_tor(False) 130 if node_on and node != 'off': 131 node = 'restart' 132 133 if node == 'on' and not node_on: 134 exec_command_bg(radicle_node_wrapped_bin,[]) 135 time.sleep(8) 136 elif node == 'off' and node_on: 137 ret,out,err = exec_command(rad_node_stop_wrapped_bin,[]) 138 elif node == 'restart': 139 if node_on: 140 ret,out,err = exec_command(rad_node_stop_wrapped_bin,[]) 141 exec_command_bg(radicle_node_wrapped_bin,[]) 142 time.sleep(8) 143 144 ret,out,err = exec_command(rad_node_status_wrapped_bin,['--only','nid']) 145 if not ret: 146 node_on = True 147 node_switch_url='&node=off' 148 node_title='click to disconnect from network' 149 else: 150 node_on = False 151 node_switch_url ='&node=on' 152 node_title ='click to connect to network' 153 154 tor_on = False 155 tor_switch_url = '&tor=on' 156 tor_title = 'click to route through Tor network' 157 ret,out,err = exec_command(crad_bin,["self","--network-mode"]) 158 if not ret and out.strip() == 'tor': 159 tor_on = True 160 tor_switch_url = '&tor=off' 161 tor_title = 'click to disconnect from Tor network' 162 163 class_node_icon = ' icon-off' 164 class_tor_icon = ' icon-off' 165 if node_on: 166 class_node_icon = ' icon-on' 167 if tor_on: 168 class_tor_icon = ' icon-on' 169 170 patch_doc = None 171 patch_err = None 172 patch_title = '' 173 if patch_id and len(patch_id): 174 ret,out,err = exec_command(crad_bin,['patch','show',patch_id,'-R',rid,'--json']) 175 if ret: 176 patch_err = err 177 elif len(out): 178 patch_doc = json.loads(out) 179 patch_title = patch_doc['title'] 180 181 diff_output = '' 182 if patch_doc and patch_id: 183 ret,out,err = exec_command(crad_bin,['patch','diff',patch_id,'-R',rid]) 184 if not ret and out: 185 diff_output = out 186 187 HTML_DASHBOARD_START=f''' 188 <link href="/css/dashboard.css" rel="stylesheet"> 189 </head> 190 <body> 191 <header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow"> 192 <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6" href="/">Cradicle</a> 193 <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"> 194 <span class="navbar-toggler-icon"></span> 195 </button> 196 <input class="form-control form-control-dark w-100 rounded-0 border-0" type="text" placeholder="Search" aria-label="Search"> 197 <div class="navbar-nav flex-row"> 198 <div class="nav-item text-nowrap"> 199 <a class="nav-link px-3 active" title="{node_title}" href="/cgi-bin/patch?id={quote_plus(patch_id)}&rid={quote_plus(rid)}{node_switch_url}"><i class="fa-solid fa-circle-nodes{class_node_icon}"></i></a> 200 </div> 201 <div class="nav-item text-nowrap"> 202 <a class="nav-link px-3 active" title="{tor_title}" href="/cgi-bin/patch?id={quote_plus(patch_id)}&rid={quote_plus(rid)}{tor_switch_url}"><i class="ft-onion{class_tor_icon}"></i></a> 203 </div> 204 <div class="nav-item text-nowrap"> 205 <a class="nav-link px-3 active" aria_current="page" href="/">{self_alias[:12]}-{self_did[8:20]}</a> 206 </div> 207 <div class="nav-item text-nowrap"> 208 <a class="nav-link px-3 active aria_current="page" href="/cgi-bin/repo?id={quote_plus(rid)}">{repo_name}</a> 209 </div> 210 <div class="nav-item text-nowrap"> 211 <a class="nav-link px-3 active aria_current="page" href="/cgi-bin/patch?id={quote_plus(patch_id)}&rid={quote_plus(rid)}">{patch_title[:16]}</a> 212 </div> 213 <div class="nav-item text-nowrap"> 214 <a class="nav-link px-3" href="/cgi-bin/settings"><i class="fa-solid fa-gear"></i> Settings</a> 215 </div> 216 <div class="nav-item text-nowrap"> 217 <a class="nav-link px-3" href="/cgi-bin/signout"><i class="fa-solid fa-sign-out"></i> Sign out</a> 218 </div> 219 </div> 220 </header> 221 <div class="container-fluid"> 222 <div class="row"> 223 <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-dark sidebar collapse"> 224 <div class="position-sticky pt-3 sidebar-sticky"> 225 <ul class="nav flex-column"> 226 <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase"> 227 <span></span> 228 </h6> 229 <ul class="nav flex-column mb-2"> 230 </ul> 231 </div> 232 </nav> 233 <main class="col-md-9 ms-sm-auto col-lg-10"> 234 <div class="container px-1 py-3"> 235 ''' 236 HTML_DASHBOARD_END = ''' 237 </div> 238 </main> 239 </div> 240 </div> 241 ''' 242 243 HTML_AUTH_START = ''' 244 <link href="/css/signin.css" rel="stylesheet"> 245 </head> 246 <body class="text-center"> 247 <main class="form-signin w-100 m-auto"> 248 <form action="/" method="post"> 249 <div class="mb-4" width="100%" style="font-size:2.5rem; font-weight:bold; color:#2470dc;">Cradicle</div> 250 ''' 251 252 HTML_AUTH_END_CREATE=''' 253 <div class="form-floating"> 254 <input type="text" class="form-control" id="floatingInput" name="alias" placeholder="user"> 255 <label for="floatingInput">Alias</label> 256 </div> 257 <div class="form-floating" style="margin-top:10px;"> 258 <input type="password" class="form-control" id="floatingPassword" name="password" placeholder=""> 259 <label for="floatingPassword">Password</label> 260 </div> 261 <div class="form-floating"> 262 <input type="password" class="form-control" id="floatingPassword2" name="password2" placeholder=""> 263 <label for="floatingPassword2">Repeat Password</label> 264 </div> 265 <button class="w-100 btn btn-lg btn-primary" type="submit">Create</button> 266 </form> 267 </main> 268 ''' 269 270 HTML_AUTH_END_LOGIN=''' 271 <div class="form-floating"> 272 <input type="password" class="form-control" id="floatingPassword" name="password" placeholder=""> 273 <label for="floatingPassword">Password</label> 274 </div> 275 <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button> 276 </form> 277 </main> 278 ''' 279 280 def format_diff(diff_text): 281 """Format diff output with color-coded lines.""" 282 result = '' 283 for line in diff_text.splitlines(): 284 escaped = html.escape(line) 285 if line.startswith('+++') or line.startswith('---'): 286 result += f'<span class="diff-file">{escaped}</span>\n' 287 elif line.startswith('@@'): 288 result += f'<span class="diff-hunk">{escaped}</span>\n' 289 elif line.startswith('+'): 290 result += f'<span class="diff-add">{escaped}</span>\n' 291 elif line.startswith('-'): 292 result += f'<span class="diff-del">{escaped}</span>\n' 293 elif line.startswith('diff '): 294 result += f'<span class="diff-file">{escaped}</span>\n' 295 else: 296 result += escaped + '\n' 297 return result 298 299 dashboard_content = '' 300 error = False 301 302 if patch_doc: 303 status_html = html.escape(patch_doc['status']) 304 labels_html = '' 305 if 'labels' in patch_doc: 306 labels_html = f'''<div><span class="text-secondary">Labels</span> <span>{html.escape(','.join(patch_doc['labels']))}</span></div>''' 307 assigned_html = '' 308 if 'assignees' in patch_doc: 309 assigned_html = f'''<div><span class="text-secondary">Assigned to</span> <span>{html.escape(','.join(patch_doc['assignees']))}</span></div>''' 310 desc_html = '' 311 if patch_doc['description'] and len(patch_doc['description']): 312 desc_html = f'''<div><span class="text-secondary">Description</span> <span>{html.escape(patch_doc['description'])}</span></div>''' 313 314 dashboard_content += f'''<div class="list-group"> 315 <div class="list-group-item"> 316 <div><span class="text-secondary">Title</span> <span>{html.escape(patch_doc['title'])}</span></div> 317 <div><span class="text-secondary">Patch ID</span> <span>{html.escape(patch_doc['patch'])}</span></div> 318 <div><span class="text-secondary">Author</span> <span class="repo-item">{html.escape(patch_doc['alias'])}</span>-{html.escape(patch_doc['author'][8:])}</div> 319 <div><span class="text-secondary">Opened on</span> <span>{html.escape(time.ctime(patch_doc['timestamp']))}</span></div> 320 <div><span class="text-secondary">Status</span> <span class="repo-item">{status_html}</span></div> 321 <div><span class="text-secondary">Head</span> <span>{html.escape(patch_doc['head'])}</span></div> 322 <div><span class="text-secondary">Base</span> <span>{html.escape(patch_doc['base'])}</span></div> 323 {labels_html} 324 {assigned_html} 325 {desc_html} 326 </div> 327 </div>''' 328 329 if diff_output: 330 dashboard_content += f''' 331 <div class="list-group mt-3"> 332 <div class="list-group-item"> 333 <div class="mb-2" style="font-weight:bold;">Diff</div> 334 <pre style="margin:0; font-size:0.85rem; overflow-x:auto;">{format_diff(diff_output)}</pre> 335 </div> 336 </div>''' 337 338 elif patch_err: 339 dashboard_content += f'<p class="error-message">{html.escape(patch_err)}</p>' 340 341 ret,out,err = exec_command(crad_bin,["self","--alias"]) 342 ret2,out2,err2 = exec_command(crad_bin,["self","--authed"]) 343 if ret: 344 print(HTML_AUTH_START) 345 if password and password != password2: 346 print_error("passwords don't match") 347 else: 348 print('<h1 class="h3 mb-3 fw-normal">Create a new identity</h1>') 349 print(HTML_AUTH_END_CREATE) 350 elif ret2: 351 print(HTML_AUTH_START) 352 if password: 353 print_error('invalid password') 354 else: 355 print('<h1 class="h3 mb-3 fw-normal">Hello <i>'+html.escape(out)+'</i></h1>') 356 print(HTML_AUTH_END_LOGIN) 357 else: 358 print(HTML_DASHBOARD_START) 359 print(dashboard_content) 360 print(HTML_DASHBOARD_END) 361 362 HTML_FOOTER=''' 363 </body> 364 </html> 365 ''' 366 367 print(HTML_FOOTER)