issue
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 </style> 82 <link rel="stylesheet" href="/assets/fontawesome/css/all.min.css"> 83 <link rel="icon" type="image/png" href="/favicon.png"> 84 ''' 85 86 print(HTML_HEAD) 87 88 form = FormData() 89 rid = form.getvalue('rid') 90 issue_id = form.getvalue('id') 91 comment = form.getvalue('comment') 92 message = form.getvalue('message') 93 reply_to = form.getvalue('parent') 94 node = form.getvalue('node') 95 tor = form.getvalue('tor') 96 97 ret,out,err = exec_command(crad_bin,["self","--alias"]) 98 self_alias = 'unknown user' 99 if not ret: 100 self_alias = html.escape(out.strip()) 101 ret,out,err = exec_command(crad_bin,["self","--did"]) 102 self_did = '' 103 if not ret: 104 self_did = html.escape(out.strip()) 105 106 if rid and len(rid): 107 ret,out,err = exec_command(crad_bin,['inspect','-R',rid,'--identity']) 108 if not ret: 109 doc = json.loads(out) 110 payload_val = doc['payload']['xyz.radicle.project'] 111 repo_name = payload_val['name'] 112 113 if comment == 'new': 114 ret,out,err = exec_command(crad_bin,['issue','comment',issue_id,'--message',message,'-R',rid]) 115 elif comment == 'reply': 116 ret,out,err = exec_command(crad_bin,['issue','comment',issue_id,'--message',message,'-R',rid,'--reply-to',reply_to]) 117 118 node_on = False 119 ret,out,err = exec_command(rad_node_status_wrapped_bin,['--only','nid']) 120 if not ret: 121 node_on = True 122 tor_on = False 123 ret,out,err = exec_command(crad_bin,['self','--network-mode']) 124 if not ret and out.strip() == 'tor': 125 tor_on = True 126 127 if tor == 'on' and not tor_on: 128 set_config_tor(True) 129 if node_on and node != 'off': 130 node = 'restart' 131 elif tor == 'off' and tor_on: 132 set_config_tor(False) 133 if node_on and node != 'off': 134 node = 'restart' 135 136 if node == 'on' and not node_on: 137 exec_command_bg(radicle_node_wrapped_bin,[]) 138 time.sleep(8) 139 elif node == 'off' and node_on: 140 ret,out,err = exec_command(rad_node_stop_wrapped_bin,[]) 141 elif node == 'restart': 142 if node_on: 143 ret,out,err = exec_command(rad_node_stop_wrapped_bin,[]) 144 exec_command_bg(radicle_node_wrapped_bin,[]) 145 time.sleep(8) 146 147 ret,out,err = exec_command(rad_node_status_wrapped_bin,['--only','nid']) 148 if not ret: 149 node_on = True 150 node_switch_url='&node=off' 151 node_title='click to disconnect from network' 152 else: 153 node_on = False 154 node_switch_url ='&node=on' 155 node_title ='click to connect to network' 156 157 tor_on = False 158 tor_switch_url = '&tor=on' 159 tor_title = 'click to route through Tor network' 160 ret,out,err = exec_command(crad_bin,["self","--network-mode"]) 161 if not ret and out.strip() == 'tor': 162 tor_on = True 163 tor_switch_url = '&tor=off' 164 tor_title = 'click to disconnect from Tor network' 165 166 class_node_icon = ' icon-off' 167 class_tor_icon = ' icon-off' 168 if node_on: 169 class_node_icon = ' icon-on' 170 if tor_on: 171 class_tor_icon = ' icon-on' 172 173 issue_doc = None 174 issue_err = None 175 issue_title = '' 176 if issue_id and len(issue_id): 177 ret,out,err = exec_command(crad_bin,['issue','show',issue_id,'-R',rid,'--json']) 178 if ret: 179 issue_err = err 180 elif len(out): 181 issue_doc = json.loads(out) 182 issue_title = issue_doc['title'] 183 184 HTML_DASHBOARD_START=f''' 185 <link href="/css/dashboard.css" rel="stylesheet"> 186 </head> 187 <body> 188 <header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow"> 189 <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3 fs-6" href="/">Cradicle</a> 190 <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"> 191 <span class="navbar-toggler-icon"></span> 192 </button> 193 <input class="form-control form-control-dark w-100 rounded-0 border-0" type="text" placeholder="Search" aria-label="Search"> 194 <div class="navbar-nav flex-row"> 195 <div class="nav-item text-nowrap"> 196 <a class="nav-link px-3 active" title="{node_title}" href="/cgi-bin/issue?id={quote_plus(issue_id)}&rid={quote_plus(rid)}{node_switch_url}"><i class="fa-solid fa-circle-nodes{class_node_icon}"></i></a> 197 </div> 198 <div class="nav-item text-nowrap"> 199 <a class="nav-link px-3 active" title="{tor_title}" href="/cgi-bin/issue?id={quote_plus(issue_id)}&rid={quote_plus(rid)}{tor_switch_url}"><i class="ft-onion{class_tor_icon}"></i></a> 200 </div> 201 <div class="nav-item text-nowrap"> 202 <a class="nav-link px-3 active" aria_current="page" href="/">{self_alias[:12]}-{self_did[8:20]}</a> 203 </div> 204 <div class="nav-item text-nowrap"> 205 <a class="nav-link px-3 active aria_current="page" href="/cgi-bin/repo?id={quote_plus(rid)}">{repo_name}</a> 206 </div> 207 <div class="nav-item text-nowrap"> 208 <a class="nav-link px-3 active aria_current="page" href="/cgi-bin/issue?id={quote_plus(issue_id)}&rid={quote_plus(rid)}">{issue_title[:16]}</a> 209 </div> 210 <div class="nav-item text-nowrap"> 211 <a class="nav-link px-3" href="/cgi-bin/settings"><i class="fa-solid fa-gear"></i> Settings</a> 212 </div> 213 <div class="nav-item text-nowrap"> 214 <a class="nav-link px-3" href="/cgi-bin/signout"><i class="fa-solid fa-sign-out"></i> Sign out</a> 215 </div> 216 </div> 217 </header> 218 <div class="container-fluid"> 219 <div class="row"> 220 <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-dark sidebar collapse"> 221 <div class="position-sticky pt-3 sidebar-sticky"> 222 <ul class="nav flex-column"> 223 <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase"> 224 <span></span> 225 </h6> 226 <ul class="nav flex-column mb-2"> 227 </ul> 228 </div> 229 </nav> 230 <main class="col-md-9 ms-sm-auto col-lg-10"> 231 <div class="container px-1 py-3"> 232 ''' 233 HTML_DASHBOARD_END = ''' 234 </div> 235 </main> 236 </div> 237 </div> 238 ''' 239 240 HTML_AUTH_START = ''' 241 <link href="/css/signin.css" rel="stylesheet"> 242 </head> 243 <body class="text-center"> 244 <main class="form-signin w-100 m-auto"> 245 <form action="/" method="post"> 246 <div class="mb-4" width="100%" style="font-size:2.5rem; font-weight:bold; color:#2470dc;">Cradicle</div> 247 ''' 248 249 HTML_AUTH_END_CREATE=''' 250 <div class="form-floating"> 251 <input type="text" class="form-control" id="floatingInput" name="alias" placeholder="user"> 252 <label for="floatingInput">Alias</label> 253 </div> 254 <div class="form-floating" style="margin-top:10px;"> 255 <input type="password" class="form-control" id="floatingPassword" name="password" placeholder=""> 256 <label for="floatingPassword">Password</label> 257 </div> 258 <div class="form-floating"> 259 <input type="password" class="form-control" id="floatingPassword2" name="password2" placeholder=""> 260 <label for="floatingPassword2">Repeat Password</label> 261 </div> 262 <button class="w-100 btn btn-lg btn-primary" type="submit">Create</button> 263 </form> 264 </main> 265 ''' 266 267 HTML_AUTH_END_LOGIN=''' 268 <div class="form-floating"> 269 <input type="password" class="form-control" id="floatingPassword" name="password" placeholder=""> 270 <label for="floatingPassword">Password</label> 271 </div> 272 <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button> 273 </form> 274 </main> 275 ''' 276 277 dashboard_content = '' 278 error = False 279 280 if issue_doc: 281 status_html = html.escape(issue_doc['state']['status']) 282 reason_html = '' 283 labels_html = '' 284 if ('labels' in issue_doc): 285 labels_html = f'''<div><span class="text-secondary">Labels</span> <span>{html.escape(','.join(issue_doc['labels']))}</span></div>''' 286 assigned_html = '' 287 if ('assignees' in issue_doc): 288 assigned_html = f'''<div><span class="text-secondary">Assigned to</span> <span>{html.escape(','.join(issue_doc['assignees']))}</span></div>''' 289 comments_list = issue_doc['comments'] 290 first_comment = comments_list[0] 291 issue_id = first_comment['id'] 292 if ('reason' in issue_doc['state']): 293 reason_html = ' ('+issue_doc['state']['reason']+')' 294 dashboard_content += f'''<div class="list-group"> 295 <div class="list-group-item"> 296 <div><span class="text-secondary">Title</span> <span>{html.escape(issue_doc['title'])}{reason_html}</span></div> 297 <div><span class="text-secondary">Issue ID</span> <span>{html.escape(first_comment['id'])}</span></div> 298 <div><span class="text-secondary">Author</span> <span class="repo-item">{html.escape(first_comment['alias'])}</span>-{html.escape(first_comment['author'][8:])}</div> 299 <div><span class="text-secondary">Opened on</span> <span>{html.escape(time.ctime(first_comment['time']))}</span></div> 300 <div><span class="text-secondary">Status</span> <span class="repo-item">{status_html}</div> 301 {labels_html} 302 {assigned_html} 303 <div><span class="text-secondary">Description</span> <span>{html.escape(first_comment['body'])}<span></div> 304 </div>''' 305 for comment in comments_list[1:]: 306 reply_to_html = '' 307 reply_to = comment['replyTo'] 308 if reply_to and reply_to != issue_id: 309 reply_to_html = ' reply-to '+html.escape(reply_to) 310 dashboard_content += f'''<div class="list-group"> 311 <div class="list-group-item"> 312 <div><span class="repo-item" title="{html.escape(comment['author'])}">{html.escape(comment['alias'])}</span> {html.escape(time.ctime(comment['time']))} {html.escape(comment['id'])}{reply_to_html}</div> 313 <div>{html.escape(comment['body'])}</div> 314 <details class="mt-2"> 315 <summary class="btn btn-sm btn-outline-secondary">Reply</summary> 316 <form action="/cgi-bin/issue?id=f7fe6a783426596c79677f0357c8a5b3a6bcf6e0&rid=zntr7shus66rdGEAC8dFaPUREjyj&comment=reply&parent={quote_plus(comment['id'])}" method="post" class="mt-2"> 317 <textarea class="form-control" name="message" rows="4" placeholder="Write a reply..."></textarea> 318 <button class="btn btn-sm btn-primary mt-2" type="submit">Submit</button> 319 </form> 320 </details> 321 </div> 322 ''' 323 324 dashboard_content += f''' 325 <form action="/cgi-bin/issue?id={quote_plus(issue_id)}&rid={quote_plus(rid)}&comment=new" method="post" class="mx-auto mt-2 w-100"> 326 <textarea class="form-control" id="message" name="message" rows="12" placeholder="Add a comment..."></textarea> 327 <button class="mx-auto mt-2 btn btn-lg btn-primary d-block" type="submit">Submit</button> 328 </form>''' 329 elif issue_err: 330 dashboard_content += f'<p class="error-message">{html.escape(issue_err)}</p>' 331 332 ret,out,err = exec_command(crad_bin,["self","--alias"]) 333 ret2,out2,err2 = exec_command(crad_bin,["self","--authed"]) 334 if ret: 335 print(HTML_AUTH_START) 336 if password and password != password2: 337 print_error("passwords don't match") 338 else: 339 print('<h1 class="h3 mb-3 fw-normal">Create a new identity</h1>') 340 print(HTML_AUTH_END_CREATE) 341 elif ret2: 342 print(HTML_AUTH_START) 343 if password: 344 print_error('invalid password') 345 else: 346 print('<h1 class="h3 mb-3 fw-normal">Hello <i>'+html.escape(out)+'</i></h1>') 347 print(HTML_AUTH_END_LOGIN) 348 else: 349 print(HTML_DASHBOARD_START) 350 print(dashboard_content) 351 print(HTML_DASHBOARD_END) 352 353 HTML_FOOTER=''' 354 </body> 355 </html> 356 ''' 357 358 print(HTML_FOOTER)