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