/ cgi-bin / patch
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)