2
0
mirror of https://github.com/ComradCollective/Comrad synced 2024-11-11 13:10:45 +00:00
Comrad/comrad/cli/cli.py

918 lines
32 KiB
Python
Raw Normal View History

2020-09-10 21:32:59 +00:00
import os,sys; sys.path.append(os.path.abspath(os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__),'..')),'..')))
2020-09-29 13:09:56 +00:00
from comrad import *
from comrad.backend import *
2020-09-10 21:32:59 +00:00
import art
2020-09-11 14:35:47 +00:00
import textwrap as tw
2020-09-20 06:37:37 +00:00
import readline,logging
2020-09-16 19:30:46 +00:00
readline.set_completer_delims('\t')
2020-09-16 19:38:59 +00:00
# from tab_completer import tabCompleter
2020-09-16 19:30:46 +00:00
tabber=tabCompleter()
if 'libedit' in readline.__doc__:
readline.parse_and_bind("bind ^I rl_complete")
else:
readline.parse_and_bind("tab: complete")
2020-09-20 06:37:37 +00:00
torpy_logger = logging.getLogger('torpy')
logging.getLogger('urllib3').propagate=False
logging.getLogger('shapely').propagate=False
logging.getLogger('pyproj').propagate=False
logging.getLogger('rtree').propagate=False
torpy_logger.propagate=False
2020-09-20 12:43:34 +00:00
from shutil import get_terminal_size
2020-09-20 06:37:37 +00:00
2020-09-20 12:43:34 +00:00
CLI_WIDTH = get_terminal_size().columns
CLI_HEIGHT = get_terminal_size().lines-1
2020-09-14 20:32:17 +00:00
2020-09-10 21:32:59 +00:00
class CLI(Logger):
ROUTES = {
2020-09-13 13:58:58 +00:00
'help':'seek help',
2020-09-29 16:25:19 +00:00
'register':'join the comrades',
2020-09-13 18:06:41 +00:00
'login':'log back in',
2020-09-29 13:09:56 +00:00
'meet':'meet a comrad',
2020-09-14 20:10:31 +00:00
'who':'show contacts or info',
2020-09-18 14:27:28 +00:00
'dm':'write people',
2020-09-17 20:25:38 +00:00
'refresh':'refresh feed/DMs',
2020-09-17 19:57:07 +00:00
'dms':'read DMs',
'feed':'read posts',
2020-09-17 04:16:48 +00:00
'verbose':'show/hide log output',
'post':'post to world',
2020-09-18 05:51:34 +00:00
'feed':'fetch posts',
2020-09-29 13:09:56 +00:00
'exit':'exit comrad',
2020-09-22 14:02:00 +00:00
'clearnet':'switch to clearnet',
'tor':'switch to tor',
2020-09-10 21:32:59 +00:00
}
2020-09-12 07:55:23 +00:00
def __init__(self,name='',cmd='',persona=None):
2020-09-13 18:47:25 +00:00
self.name=name
2020-09-12 07:55:23 +00:00
self.cmd=cmd
2020-09-29 13:09:56 +00:00
self.comrad=None
2020-09-13 18:47:25 +00:00
self.loggedin=False
2020-09-16 19:30:46 +00:00
self.tabber=tabber
2020-09-18 05:32:25 +00:00
self.log('ROUTES:',self.ROUTES)
2020-09-20 06:37:37 +00:00
self.hops=[]
2020-09-16 19:30:46 +00:00
# Routes
rts=['/'+k for k in self.ROUTES]
tabber.createListCompleter(rts)
readline.set_completer(tabber.listCompleter)
2020-09-10 21:32:59 +00:00
2020-09-20 06:37:37 +00:00
# logging
2020-09-29 13:09:56 +00:00
from comrad.backend.mazes import MazeWalker
2020-09-20 06:37:37 +00:00
self.walker=MazeWalker(callbacks=self.callbacks)
self.torpy_logger = logging.getLogger('torpy')
self.torpy_logger.propagate=False
self.torpy_logger.addHandler(self.walker)
import ipinfo
ipinfo_access_token = '90df1baf7c373a'
self.ipinfo_handler = ipinfo.getHandler(ipinfo_access_token)
2020-09-15 08:51:52 +00:00
def verbose(self,*x):
self.toggle_log()
2020-09-11 14:35:47 +00:00
def run(self,inp='',name=''):
2020-09-13 18:47:25 +00:00
# if name: self.name=name
2020-09-20 12:43:34 +00:00
clear_screen()
self.boot()
2020-09-15 08:51:52 +00:00
if not inp:
self.help()
2020-09-10 21:32:59 +00:00
2020-09-15 10:33:17 +00:00
if inp:
2020-09-15 10:35:04 +00:00
for inpx in inp.split('/'):
self.route('/'+inpx.strip())
2020-09-10 21:32:59 +00:00
while True:
try:
2020-09-16 17:31:09 +00:00
inp=input(f'\n@{self.name if self.name else "?"}: ')
2020-09-16 10:23:18 +00:00
# self.print(inp,'??')
2020-09-15 08:51:52 +00:00
self.route(inp)
2020-09-13 14:48:26 +00:00
except (KeyboardInterrupt,EOFError) as e:
2020-09-16 16:51:31 +00:00
self.stat('Goodbye.')
exit()
2020-09-29 13:09:56 +00:00
except ComradException as e:
2020-09-16 16:51:31 +00:00
self.stat(f'I could not handle your request. {e}\n')
2020-09-11 14:35:47 +00:00
#await asyncio.sleep(0.5)
2020-09-20 06:37:37 +00:00
self.walker.walk=[] # reset logger's walk?
2020-09-10 21:32:59 +00:00
def route(self,inp):
inp=inp.strip()
2020-09-16 10:23:18 +00:00
# self.print('route got:',[inp])
2020-09-10 21:32:59 +00:00
if not inp.startswith('/'): return
cmd=inp.split()[0]
dat=inp[len(cmd):].strip()
cmd=cmd[1:]
2020-09-16 10:23:18 +00:00
# self.print([cmd,dat])
2020-09-10 21:32:59 +00:00
if cmd in self.ROUTES and hasattr(self,cmd):
f=getattr(self,cmd)
2020-09-16 10:23:18 +00:00
try:
res=f(dat)
2020-09-29 13:09:56 +00:00
except ComradException as e:
2020-09-16 16:15:45 +00:00
self.stat('Message not sent.',str(e),'\n')
2020-09-16 10:23:18 +00:00
2020-09-29 13:09:56 +00:00
def stat(self,*msgs,use_prefix=True,prefix=None,comrad_name=None,pause=False,clear=False,min_prefix_len=12,**kwargs):
2020-09-16 15:06:37 +00:00
if not prefix:
2020-09-29 13:09:56 +00:00
if not comrad_name: comrad_name='Telephone'
# prefix='Comrad @'+comrad_name+': '
prefix='@'+comrad_name+': '
2020-09-17 08:59:27 +00:00
# wrap msg
total_msg=wrapp(
*msgs,
use_prefix=use_prefix,
prefix=prefix if use_prefix and prefix else '',
min_prefix_len=min_prefix_len if use_prefix and prefix else 0
)
print(total_msg)
2020-09-16 15:06:37 +00:00
if pause: do_pause()
if clear: clear_screen()
2020-09-16 10:23:18 +00:00
def print(self,*x):
2020-09-16 14:54:17 +00:00
print(*x)
# x=' '.join(str(xx) for xx in x)
# x=str(x).replace('\r\n','\n').replace('\r','\n')
# for ln in x.split('\n'):
# # #scan_print(ln+'\n\n')
# if not ln: print()
# for ln2 in tw.wrap(ln,CLI_WIDTH):
# print(ln2)
# # x='\n'.join(tw.wrap(x,CLI_WIDTH))
# # print(x)
2020-09-16 10:23:18 +00:00
2020-09-20 12:43:34 +00:00
def boot(self,indent=None,scan=True):
2020-09-16 10:23:18 +00:00
logo=art.text2art(CLI_TITLE,font=CLI_FONT).replace('\r\n','\n')
2020-09-20 12:43:34 +00:00
newlogo=[]
for logo_ln in logo.split('\n'):
logo_ln = logo_ln.center(CLI_WIDTH,' ')
newlogo.append(logo_ln)
newlogo_s='\n'.join(newlogo)
scan_print(newlogo_s,speed=5) if scan else self.print(newlogo_s)
2020-09-10 21:32:59 +00:00
2020-09-19 15:56:15 +00:00
def status_str(self,unr,tot):
read=tot-unr
2020-09-19 16:48:35 +00:00
return f'({unr}*)' if unr else f'({unr})'
2020-09-19 16:17:35 +00:00
# return f'({unr}*)' if unr else f'({unr})'
2020-09-19 15:56:15 +00:00
# return f'{unr}* of {tot}' if tot else str(tot)
@property
def post_status_str(self):
2020-09-29 13:09:56 +00:00
if not self.comrad: return ''
2020-09-19 15:56:15 +00:00
return self.status_str(
2020-09-29 13:09:56 +00:00
unr=self.comrad.num_unread_posts,
tot=self.comrad.num_posts
2020-09-19 15:56:15 +00:00
)
@property
def msg_status_str(self):
2020-09-29 13:09:56 +00:00
if not self.comrad: return ''
2020-09-19 15:56:15 +00:00
return self.status_str(
2020-09-29 13:09:56 +00:00
unr=self.comrad.num_unread_msgs,
tot=self.comrad.num_msgs
2020-09-19 15:56:15 +00:00
)
2020-09-22 15:09:25 +00:00
def clearnet(self,_=''):
2020-09-29 13:16:50 +00:00
os.environ['COMRAD_USE_CLEARNET'] = '1'
os.environ['COMRAD_USE_TOR'] = '0'
2020-09-22 15:09:25 +00:00
def tor(self,_=''):
2020-09-29 13:16:50 +00:00
os.environ['COMRAD_USE_CLEARNET'] = '0'
os.environ['COMRAD_USE_TOR'] = '1'
2020-09-22 14:02:00 +00:00
2020-09-19 15:56:15 +00:00
2020-09-13 14:34:17 +00:00
def help(self,*x,**y):
2020-09-13 14:34:36 +00:00
clear_screen()
2020-09-20 12:43:34 +00:00
self.boot(scan=False)
2020-09-14 20:18:47 +00:00
2020-09-18 14:36:30 +00:00
2020-09-16 10:23:18 +00:00
if not self.logged_in:
HELPSTR=f"""
2020-09-18 05:51:34 +00:00
/login [name] --> log back in
2020-09-29 13:09:56 +00:00
/register [name] --> new comrad"""
2020-09-16 10:23:18 +00:00
else:
HELPSTR=f"""
2020-09-18 14:36:30 +00:00
/refresh --> get new data
2020-09-20 12:43:34 +00:00
/feed --> scroll feed {self.post_status_str}
2020-09-18 14:36:30 +00:00
/post --> post to all
2020-09-18 05:51:34 +00:00
2020-09-20 12:43:34 +00:00
/dms --> see DMs {self.msg_status_str}
2020-09-18 14:27:28 +00:00
/dm [name] --> send a DM
2020-09-18 05:51:34 +00:00
/meet [name] --> exchange info
/who [name] --> show contacts
"""
2020-09-16 10:23:18 +00:00
HELPSTR+=f"""
2020-09-18 05:51:34 +00:00
/help --> seek help
/exit --> exit app
2020-09-16 10:23:18 +00:00
"""
2020-09-18 14:40:50 +00:00
helpstr = tw.indent(HELPSTR.strip()+'\n\n',' '*11)
2020-09-20 12:43:34 +00:00
# self.print(helpstr)
2020-09-16 10:23:18 +00:00
# self.print(border+helpstr+'\n'+self.border)
2020-09-20 12:43:34 +00:00
lns=[ln.strip() for ln in helpstr.split('\n')]
maxlen=max([len(ln) for ln in lns])
for ln in lns:
for n in range(len(ln),maxlen): ln +=' '
print(ln.center(CLI_WIDTH))
2020-09-16 10:23:18 +00:00
2020-09-18 05:51:34 +00:00
def exit(self,dat=''):
exit('Goodbye.')
2020-09-16 10:23:18 +00:00
@property
def border(self):
border = '-' * CLI_WIDTH # (len(logo.strip().split('\n')[0]))
border=tw.indent(border, ' '*2)
return border
2020-09-10 21:32:59 +00:00
def intro(self):
2020-09-14 08:48:23 +00:00
self.status(None)
2020-09-14 08:48:40 +00:00
def who(self,whom):
2020-09-14 08:48:23 +00:00
if self.with_required_login():
2020-09-29 13:09:56 +00:00
contacts_obj = self.comrad.contacts()
2020-09-26 12:11:56 +00:00
contacts = [p.name for p in contacts_obj]
2020-09-16 10:23:18 +00:00
self.print(' ' + '\n '.join(contacts))
2020-09-14 08:48:23 +00:00
2020-09-19 17:51:01 +00:00
@property
def callbacks(self):
return {
2020-09-20 06:37:37 +00:00
'torpy_guard_node_connect':self.callback_on_hop,
'torpy_extend_circuit':self.callback_on_hop,
2020-09-19 17:51:01 +00:00
}
2020-09-20 06:37:37 +00:00
def callback_on_hop(self,rtr):
2020-09-20 12:43:34 +00:00
def run_map():
from worldmap_curses import make_map #,print_map_simple
# clear_screen()
if not hasattr(self,'_mapscr') or not self._mapscr:
self._mapscr = mapscr = make_map()
mapscr.add_base_map()
# stop
else:
mapscr = self._mapscr
# return
# mapscr.stdscr.getch()
deets = self.ipinfo_handler.getDetails(rtr.ip)
self.hops.append((rtr,deets))
places = [
(_deets.city,tuple(float(_) for _ in _deets.loc.split(',')))
for (_rtr,_deets) in [(rtr,deets)]
]
msg = ['@Tor: Hiding your IP by hopping it around the globe:'] + [f'{_deets.city}, {_deets.country_name} ({_rtr.nickname})' for _rtr,_deets in self.hops
]
mapscr.run_print_map(places,msg=msg)
# self.stat(
# msg,
2020-09-29 13:09:56 +00:00
# comrad_name='Tor'
2020-09-20 12:43:34 +00:00
# )
# print()
# input('pausing on callback: '+msg)
2020-09-20 06:37:37 +00:00
2020-09-20 12:43:34 +00:00
run_map()
# self._mapscr=None
2020-09-19 18:54:57 +00:00
2020-09-10 21:32:59 +00:00
2020-09-16 16:15:45 +00:00
def register(self,name=None):
2020-09-13 18:20:19 +00:00
if not name: name=input('name: ')
if not name: return
2020-09-29 13:09:56 +00:00
self.comrad = Comrad(name,callbacks=self.callbacks)
2020-09-16 14:50:05 +00:00
was_off=self.off
2020-09-16 14:50:50 +00:00
# if was_off: self.show_log()
2020-09-29 13:09:56 +00:00
def logfunc(*x,comrad_name='Keymaker',**y):
self.stat(*x,comrad_name=comrad_name,**y)
2020-09-16 14:52:24 +00:00
2020-09-29 13:09:56 +00:00
res=self.comrad.register(logfunc=logfunc)
2020-09-16 14:50:50 +00:00
# if was_off: self.toggle_log()
2020-09-13 14:35:41 +00:00
if res and type(res)==dict and 'success' in res and res['success']:
2020-09-29 13:09:56 +00:00
self.name=self.comrad.name
2020-09-13 18:47:25 +00:00
self.loggedin=True
2020-09-16 12:49:46 +00:00
self.help()
2020-09-29 13:09:56 +00:00
# self.stat(f'Welcome, Comrad @{self.name}.')
2020-09-13 14:29:53 +00:00
else:
2020-09-13 18:47:25 +00:00
self.name=None
self.loggedin=False
2020-09-29 13:09:56 +00:00
self.comrad=None
2020-09-16 16:15:45 +00:00
self.help()
2020-09-16 17:31:09 +00:00
if res and 'status' in res:
# self.boot()
2020-09-29 13:09:56 +00:00
self.stat(res.get('status','?'),comrad_name='Operator')
2020-09-14 06:20:02 +00:00
2020-09-13 14:15:06 +00:00
def login(self,name):
2020-09-29 13:09:56 +00:00
# self.print(self,name,self.name,self.comrad,self.loggedin)
2020-09-13 14:25:00 +00:00
if not name: name=input('name: ')
if not name: return
2020-09-29 13:09:56 +00:00
self.comrad=Comrad(name,callbacks=self.callbacks)
2020-09-18 14:24:04 +00:00
return self.refresh()
2020-09-29 13:09:56 +00:00
# res = self.comrad.login()
2020-09-18 14:24:04 +00:00
# return self.do_login(res)
2020-09-17 12:57:12 +00:00
2020-09-17 12:57:30 +00:00
def do_login(self,res):
2020-09-16 12:41:42 +00:00
# print('got login res:',res)
2020-09-29 13:09:56 +00:00
self.log('<- comrad.login() <-',res)
2020-09-16 12:41:42 +00:00
2020-09-13 14:35:41 +00:00
if res and type(res)==dict and 'success' in res and res['success']:
2020-09-17 12:57:12 +00:00
self.name=res['name']
2020-09-29 13:09:56 +00:00
self.comrad=Comrad(res['name'],callbacks=self.callbacks)
2020-09-13 18:47:25 +00:00
self.loggedin=True
2020-09-13 14:29:53 +00:00
else:
2020-09-13 18:47:25 +00:00
self.name=None
self.loggedin=False
2020-09-29 13:09:56 +00:00
self.comrad=None
2020-09-14 07:58:26 +00:00
if res and 'status' in res:
2020-09-20 12:43:34 +00:00
self.boot(scan=False)
2020-09-15 10:23:32 +00:00
self.help()
2020-09-29 13:09:56 +00:00
self.stat(res.get('status','?'),comrad_name='Operator')
2020-09-17 12:57:12 +00:00
return bool(res.get('success'))
2020-09-16 12:41:42 +00:00
2020-09-13 18:06:41 +00:00
@property
def logged_in(self):
2020-09-29 13:09:56 +00:00
return (self.loggedin and self.comrad and self.name)
2020-09-13 18:06:41 +00:00
2020-09-14 08:48:23 +00:00
2020-09-14 20:18:47 +00:00
def with_required_login(self,quiet=False):
2020-09-13 18:06:41 +00:00
if not self.logged_in:
2020-09-14 20:18:47 +00:00
if not quiet:
2020-09-16 10:23:18 +00:00
self.stat('You must be logged in first.')
2020-09-14 08:48:23 +00:00
return False
return True
2020-09-13 17:42:05 +00:00
2020-09-15 15:07:15 +00:00
def meet(self,dat,returning=False):
2020-09-15 11:12:49 +00:00
if self.with_required_login():
2020-09-16 13:38:15 +00:00
datl=dat.strip().split()
if not datl:
self.stat('Meet whom?')
return
name_or_pubkey = datl[0]
2020-09-29 13:09:56 +00:00
res = self.comrad.meet(name_or_pubkey,returning=returning)
2020-09-16 10:23:18 +00:00
status=res.get('status')
2020-09-18 05:32:25 +00:00
self.stat(status)
2020-09-15 11:12:49 +00:00
2020-09-18 14:27:28 +00:00
def dm(self,dat='',name_or_pubkey=None,msg_s=None):
2020-09-14 20:18:47 +00:00
if self.with_required_login():
2020-09-16 10:23:18 +00:00
2020-09-17 06:22:45 +00:00
if not name_or_pubkey:
dat=dat.strip()
if not dat:
self.status('Message whom? Usage: /msg [name]')
return
datl=dat.split(' ',1)
name_or_pubkey = datl[0]
2020-09-16 18:20:42 +00:00
if name_or_pubkey.startswith('@'):
name_or_pubkey=name_or_pubkey[1:]
2020-09-17 06:22:45 +00:00
if not msg_s:
2020-09-18 07:41:06 +00:00
datl=dat.split()
2020-09-17 06:22:45 +00:00
if len(datl)==1:
print()
self.stat(f'Compose your message to @{name_or_pubkey} below.', 'Press Ctrl+D to complete, or Ctrl+C to cancel.')
print()
msg_s = multiline_input().strip()
if not msg_s:
print('\n')
self.stat('Not sending. No message found.')
return
else:
msg_s = datl[1]
2020-09-16 10:23:18 +00:00
self.log(f'Composed msg to {name_or_pubkey}: {msg_s}')
2020-09-29 13:09:56 +00:00
msg_obj = self.comrad.msg(
2020-09-14 20:18:47 +00:00
name_or_pubkey,
2020-09-16 10:23:18 +00:00
msg_s
2020-09-14 20:18:47 +00:00
)
self.log(f'Sent msg obj to {name_or_pubkey}: {msg_obj}')
2020-09-16 19:30:46 +00:00
print()
2020-09-29 13:09:56 +00:00
self.stat(f'Message successfully sent to @{name_or_pubkey}.',comrad_name='Operator',pause=True)
2020-09-13 17:42:05 +00:00
2020-09-17 12:57:12 +00:00
2020-09-18 05:32:25 +00:00
def refresh(self,dat=None,res=None,statd={}):
2020-09-16 12:41:42 +00:00
self.log(f'<-- dat={dat}, res={res}')
2020-09-18 05:32:57 +00:00
# stop
2020-09-17 12:57:12 +00:00
## get updates
# this does login, msgs, and posts in one req
2020-09-20 12:43:34 +00:00
time.sleep(0.25)
2020-09-29 13:09:56 +00:00
res = self.comrad.get_updates()
2020-09-21 08:58:57 +00:00
#print(res.get('success'))
if not res.get('success'):
2020-09-29 13:09:56 +00:00
self.stat(res.get('status'),comrad_name='')
2020-09-21 08:58:57 +00:00
return
self.stat('@Telephone: Patching you through to the @Operator. One moment please...')
2020-09-20 12:43:34 +00:00
if hasattr(self,'_mapscr') and self._mapscr:
#stop
msg=self._mapscr.msg + ['???, ??? (@Operator)']
self._mapscr.run_print_map(msg=msg)
time.sleep(1)
self._mapscr.endwin()
self._mapscr=None
self.hops=[]
2020-09-17 12:57:12 +00:00
self.log('<-- get_updates',res)
# check logged in
2020-09-17 13:09:11 +00:00
res_login=res.get('res_login',{})
if not self.do_login(res_login): return
2020-09-29 13:09:56 +00:00
self.stat('',res['status'],comrad_name='Operator',**statd)
2020-09-17 12:57:30 +00:00
2020-09-18 14:24:04 +00:00
return res
2020-09-17 12:23:11 +00:00
2020-09-15 10:17:06 +00:00
2020-09-15 13:31:31 +00:00
def prompt_adduser(self,msg):
2020-09-16 10:23:18 +00:00
# self.print('prompt got:',msg)
# self.print(msg.data)
2020-09-15 14:03:00 +00:00
do_pause()
2020-09-16 13:46:54 +00:00
# clear_screen()
2020-09-16 18:58:16 +00:00
# print(dict_format(msg.data))
# do_pause()
2020-09-15 13:55:23 +00:00
meet_name = msg.data.get('meet_name')
meet_uri = msg.data.get('meet')
2020-09-29 13:09:56 +00:00
qrstr=self.comrad.qr_str(meet_uri)
2020-09-16 12:41:42 +00:00
self.stat(f"Add @{meet_name}'s public key to your address book?",f'It will allow you and @{meet_name} to read and write encrypted messages to one another.')
2020-09-29 13:09:56 +00:00
do_adduser = input(f'''\n{self.comrad} [y/N]: ''')
2020-09-15 13:52:35 +00:00
if do_adduser.strip().lower()=='y':
2020-09-16 18:58:16 +00:00
import pyqrcode
print('meet_uri',meet_uri,'???')
qr = pyqrcode.create(meet_uri)
fnfn = os.path.join(PATH_QRCODES,meet_name+'.png') # self.get_path_qrcode(name=name)
qr.png(fnfn,scale=5)
2020-09-15 14:13:25 +00:00
clear_screen()
2020-09-16 12:41:42 +00:00
self.stat(f'The public key of @{meet_name} has been saved as a QRcode to {fnfn}')
print(qrstr)
2020-09-16 10:36:16 +00:00
do_pause()
clear_screen()
2020-09-16 12:41:42 +00:00
2020-09-16 20:05:34 +00:00
# print(msg.data)
# if not msg.data.get('returning'):
# bit hacky way to tell if this has been returned or not!
if 'has agreed' not in msg.data.get('txt',''):
2020-09-29 13:09:56 +00:00
self.stat('Send this comrad your public key as well?')
do_senduser = input(f'''\n{self.comrad} [y/N]: ''')
2020-09-16 20:05:34 +00:00
if do_senduser.strip().lower()=='y':
2020-09-29 13:09:56 +00:00
res = self.comrad.meet(meet_name,returning=True)
2020-09-16 20:05:34 +00:00
if res.get('success'):
2020-09-18 13:31:41 +00:00
self.log('res?!',res)
2020-09-18 13:33:12 +00:00
self.stat('Returning the invitation:',f'"{res.get("res",{}).get("msg_d",{}).get("msg",{}).get("txt","[?]")}"',use_prefix=True)
2020-09-16 20:05:34 +00:00
do_pause()
else:
self.stat(msg.get('status'))
2020-09-15 13:31:31 +00:00
2020-09-15 14:02:42 +00:00
def prompt_msg(self,msg):
2020-09-16 13:46:54 +00:00
clear_screen()
print(msg)
2020-09-19 15:05:55 +00:00
self.stat('Type "r" to reply to this message, "d" to delete it, or hit Enter to continue.',use_prefix=False)
2020-09-29 13:09:56 +00:00
do = input(f'\n{self.comrad}: ')
2020-09-15 14:02:42 +00:00
do=do.strip().lower()
if do=='d':
2020-09-16 10:23:18 +00:00
# self.print('del',msg.post_id)
2020-09-29 13:09:56 +00:00
res=self.comrad.delete_post(msg.post_id)
2020-09-16 12:41:42 +00:00
if res.get('success'):
self.stat('Deleted message.')
else:
self.stat('Could not delete message.')
do_pause()
2020-09-15 14:02:42 +00:00
elif do=='r':
2020-09-16 19:13:41 +00:00
# self.print('@todo: replying...')
2020-09-18 14:43:04 +00:00
return self.dm(msg.from_name)
2020-09-15 14:02:42 +00:00
else:
2020-09-19 15:31:40 +00:00
# seen this msg!
2020-09-29 13:09:56 +00:00
self.comrad.seen_msg(msg)
2020-09-15 14:02:42 +00:00
pass
2020-09-15 13:31:31 +00:00
2020-09-17 12:57:12 +00:00
2020-09-17 19:57:07 +00:00
def dms(self,dat=''):
2020-09-15 10:17:06 +00:00
if self.with_required_login():
2020-09-29 13:09:56 +00:00
msgs=self.comrad.messages()
2020-09-17 19:57:07 +00:00
return self.read(msgs)
def feed(self,dat=''):
if self.with_required_login():
2020-09-29 13:09:56 +00:00
posts=self.comrad.posts()
2020-09-17 19:57:07 +00:00
return self.read(posts)
def read(self,msgs):
if not msgs:
self.stat('No messages.')
else:
clear_screen()
for i,msg in enumerate(msgs):
try:
self.stat(f'Showing message {i+1} of {len(msgs)}, from newest to oldest. Hit Ctrl+D to exit.')
print()
print(msg)
2020-09-18 13:13:25 +00:00
self.log('DATA',msg.msg_d)
2020-09-17 19:57:07 +00:00
if msg.data.get('prompt_id')=='addcontact':
self.prompt_adduser(msg)
self.prompt_msg(msg)
clear_screen()
except EOFError:
break
self.help()
2020-09-15 10:17:06 +00:00
2020-09-13 17:42:05 +00:00
2020-09-18 07:51:53 +00:00
def post(self,dat='',maxlen=MAX_POST_LEN):
2020-09-17 04:16:48 +00:00
if self.with_required_login():
2020-09-18 07:45:47 +00:00
self.stat(f'Write your post below. Maximum of 1000 characters.', 'Press Ctrl+D to complete, or Ctrl+C to cancel.')
print()
2020-09-18 07:51:53 +00:00
msg_s = multiline_input() #.strip()
2020-09-18 07:45:47 +00:00
if not msg_s:
print('\n')
self.stat('Not sending. No text entered.')
return
2020-09-18 07:51:53 +00:00
if len(msg_s)>maxlen:
2020-09-18 07:54:22 +00:00
msg1=f'Not sending. Message is {len(msg_s)-maxlen} characters over the character limit of {maxlen}.'
msg2='The message you wanted to send is copied below (in case you want to copy/paste it to edit it):',
msg3='The message you wanted to send is copied above (in case you want to copy/paste it to edit it).'
2020-09-18 07:55:43 +00:00
err = f'{msg1}\n\n{msg2}\n\n{msg_s}'
err2= f'{msg1}\n\n{msg3}'
2020-09-18 08:22:46 +00:00
print()
2020-09-18 07:56:25 +00:00
self.stat(msg1)
# self.stat(err2)
2020-09-18 07:51:53 +00:00
return
2020-09-18 07:45:47 +00:00
self.log(f'Post written: {msg_s}')
2020-09-29 13:09:56 +00:00
msg_obj = self.comrad.post(
2020-09-18 07:45:47 +00:00
msg_s
)
self.log(f'Posted: {msg_obj}')
print()
2020-09-29 13:09:56 +00:00
self.stat(f'Post sent to @{WORLD_NAME}.',comrad_name='Operator',pause=True)
2020-09-18 07:45:47 +00:00
2020-09-13 17:42:05 +00:00
2020-09-13 14:15:06 +00:00
2020-09-11 14:35:47 +00:00
### DIALOGUES
2020-09-10 21:32:59 +00:00
2020-09-11 14:35:47 +00:00
# hello, op?
2020-09-11 17:17:21 +00:00
def status_keymaker_part1(self,name):
2020-09-11 14:35:47 +00:00
self.status(None,{ART_OLDPHONE4+'\n',True},3) #,scan=False,width=None,pause=None,clear=None)
2020-09-10 21:32:59 +00:00
nm=name if name else '?'
self.status(
2020-09-29 13:09:56 +00:00
f'\n\n\n@{nm}: Uh yes hello, Operator? I would like to join Comrad, the socialist network. Could you patch me through?',clear=False)
2020-09-10 21:32:59 +00:00
while not name:
2020-09-29 13:09:56 +00:00
name=self.status(('name','@TheTelephone: Of course, Comrad...?\n@')).get('vals').get('name').strip()
2020-09-16 10:23:18 +00:00
self.print()
2020-09-10 21:32:59 +00:00
self.status(
2020-09-29 13:09:56 +00:00
f'@TheTelephone: Of course, Comrad @{name}. A fine name.',
2020-09-10 21:32:59 +00:00
'''@TheTelephone: However, I'm just the local operator who lives on your device; my only job is to communicate with the remote operator securely.''',
2020-09-29 13:09:56 +00:00
'''Comrad @TheOperator lives on the deep web. She's the one you want to speak with.''',
2020-09-10 21:32:59 +00:00
2020-09-11 14:35:47 +00:00
None,{ART_OLDPHONE4},f'''@{name}: Hm, ok. Well, could you patch me through to the remote operator then?''',
2020-09-10 21:32:59 +00:00
2020-09-13 18:47:25 +00:00
f'''@{TELEPHONEname}: I could, but it's not safe yet. Your information could be exposed. You need to cut your encryption keys first.''',
2020-09-10 21:32:59 +00:00
f'@{name}: Fine, but how do I do that?',
2020-09-13 18:47:25 +00:00
f'@{TELEPHONEname}: Visit the Keymaker.',
2020-09-10 21:32:59 +00:00
clear=False,pause=True)
### KEYMAKER
2020-09-11 14:35:47 +00:00
self.status(None,{tw.indent(ART_KEY,' '*5)+'\n',True},3) #,clear=False,indent=10,pause=False)
2020-09-10 21:32:59 +00:00
# convo
2020-09-11 14:35:47 +00:00
self.status(
2020-09-29 13:09:56 +00:00
f'\n@{name}: Hello, Comrad @Keymaker? I would like help forging a new set of keys.',
2020-09-10 21:32:59 +00:00
2020-09-29 13:09:56 +00:00
f'@Keymaker: Of course, Comrad @{name}.',
2020-09-11 14:35:47 +00:00
)
2020-09-10 21:32:59 +00:00
2020-09-11 17:17:21 +00:00
self.status(
2020-09-12 07:55:23 +00:00
'I will cut for you two matching keys, part of an "asymmetric" pair.',
2020-09-11 17:17:21 +00:00
'Please, watch me work.',
2020-09-11 14:35:47 +00:00
2020-09-11 17:17:21 +00:00
None,{tw.indent(ART_KEY,' '*5)+'\n'},
2020-09-11 14:35:47 +00:00
2020-09-11 17:17:21 +00:00
'I use a high-level cryptographic function from Themis, a well-respected open-source cryptography library.',
'I use the iron-clad Elliptic Curve algorthm to generate the asymmetric keypair.',
'> GenerateKeyPair(KEY_PAIR_TYPE.EC)',
3
)
self.status(
None,
{ART_KEY_PAIR,True}
) #,clear=False,indent=10,pause=False)
2020-09-12 07:55:23 +00:00
return name
def status_keymaker_part2(self,name,passphrase,pubkey,privkey,hasher,persona):
from getpass import getpass
# gen what we need
uri_id = pubkey.data_b64
qr_str = get_qr_str(uri_id)
qr_path = os.path.join(PATH_QRCODES,name+'.png')
# what are pub/priv?
# self.status(
# None,{ART_KEY_PAIR},
# 'A matching set of keys have been generated.',
# None,{ART_KEY_PAIR2A+'\nA matching set of keys have been generated.'+'\n'},
# 'First, I have made a "public key" which you can share with anyone:',
# f'(1) {pubkey.data_b64.decode()}',
2020-09-29 13:09:56 +00:00
# 'This key is a randomly-generated binary string, which acts as your "address" on Comrad.',
2020-09-12 07:55:23 +00:00
# 'With it, someone can write you an encrypted message which only you can read.'
# )
# self.status(
# None,{ART_KEY_PAIR2A},
# f'You can share your public key by copy/pasting it to them over a secure channel (e.g. Signal).',
# 'Or, you can share it as a QR code, especially IRL:',
# {qr_str+'\n\n',True,5},
# f'\n\n(If registration is successful, this QR code be saved as an image to your device at: {qr_path}.)'
# )
2020-09-10 21:32:59 +00:00
2020-09-11 14:35:47 +00:00
# private keys
2020-09-11 17:17:21 +00:00
self.status(None,
{ART_KEY_PAIR2B},
2020-09-12 07:55:23 +00:00
'Second, I have cut a matching "private key".',
"It's too dangerous to show in full, so here it is 66% redacted:",
f'(2) {make_key_discreet(privkey.data_b64,0.3)}',
'With it, you can decrypt and read any message sent to you via your public key.',
'You can also encrypt and send messages to other people whose public keys you have.',
2020-09-11 17:17:21 +00:00
)
2020-09-12 07:55:23 +00:00
# private keys
2020-09-11 17:17:21 +00:00
self.status(None,
2020-09-12 07:55:23 +00:00
{CUBEKEY},
'So if someone were to steal your private key, they could read your mail and forge your signature.'
'You you should never, ever give your private key to anyone.',
'In fact, this key is so dangerous that we need to lock it away immediately.',
"We'll even throw away the key we use to lock this private key with!",
"How? By regenerating it each time from your password.",
2020-09-10 21:32:59 +00:00
)
2020-09-11 14:35:47 +00:00
if not passphrase:
2020-09-11 17:17:21 +00:00
from getpass import getpass
2020-09-11 14:35:47 +00:00
self.status(
'And it looks like you haven\'t yet chosen a password.',
3,"Don't tell it to me! Never tell it to anyone.",
"Ideally, don't even save it on your computer; just remember it, or write it down on paper.",
2020-09-29 13:09:56 +00:00
"Instead, whisper it to Comrad @Hasher, who scrambles information '1-way', like a blender.",
2020-09-11 14:35:47 +00:00
)
2020-09-11 17:17:21 +00:00
2020-09-11 14:35:47 +00:00
res = self.status(None,
2020-09-11 17:17:21 +00:00
{indent_str(ART_FROG_BLENDER,10),True},
2020-09-11 14:35:47 +00:00
"@Keymaker: Go ahead, try it. Type anything to @Hasher.",
('str_to_hash',f'@{name}: ',input)
)
str_to_hash = res.get('vals').get('str_to_hash')
2020-09-11 17:17:21 +00:00
hashed_str1 = hasher(str_to_hash.encode())
2020-09-11 14:35:47 +00:00
res = self.status(
2020-09-11 17:17:21 +00:00
'@Hasher: '+hashed_str1
2020-09-11 14:35:47 +00:00
)
res = self.status(
2020-09-11 17:17:21 +00:00
'@Keymaker: Whatever you typed, there\'s no way to reconstruct it from that garbled mess.',
'But whatever you typed will always produce the *same* garbled mess.',
('str_to_hash',f'Try typing the exact same thing over again:\n@{name}: ',input)
2020-09-11 14:35:47 +00:00
)
str_to_hash = res.get('vals').get('str_to_hash')
2020-09-11 17:17:21 +00:00
hashed_str2 = hasher(str_to_hash.encode())
2020-09-11 14:35:47 +00:00
res = self.status(
2020-09-11 17:17:21 +00:00
'@Hasher: '+hashed_str2
2020-09-11 14:35:47 +00:00
)
2020-09-11 17:17:21 +00:00
if hashed_str1==hashed_str2:
self.status('See how the hashed values are also exactly the same?')
else:
self.status('See how the hashed values have also changed?')
2020-09-11 14:35:47 +00:00
res = self.status(
2020-09-11 17:17:21 +00:00
('str_to_hash',f'Now try typing something just a little bit different:\n@{name}: ',input)
2020-09-11 14:35:47 +00:00
)
str_to_hash = res.get('vals').get('str_to_hash')
2020-09-11 17:17:21 +00:00
hashed_str3 = hasher(str_to_hash.encode())
2020-09-11 14:35:47 +00:00
res = self.status(
2020-09-11 17:17:21 +00:00
'@Hasher: '+hashed_str3
2020-09-11 14:35:47 +00:00
)
2020-09-11 17:17:21 +00:00
if hashed_str2==hashed_str3:
self.status('See how the hashed values are also the same?')
2020-09-11 14:35:47 +00:00
else:
2020-09-11 17:17:21 +00:00
self.status('See how the hashed values have also changed?')
2020-09-11 14:35:47 +00:00
2020-09-11 17:17:21 +00:00
self.status(
None,{indent_str(ART_FROG_BLENDER,10)},
'@Keymaker: Behind the scenes, @Hasher is using the SHA-256 hashing function, which was designed by the NSA.',
'But @Hasher also adds a secret "salt" to the recipe, as it\'s called.',
'To whatever you type in, @Hasher adds a secret phrase: another random string of characters which never changes.',
"By doing so, the hash output is \"salted\": made even more idiosyncratic to outside observers.",
)
self.status(
None,{indent_str(ART_FROG_BLENDER,10)},
f"I've taken the liberty of generating a random secret for your device, which I show here mostly redacted:",
make_key_discreet_str(persona.crypt_keys.secret.decode(),0.25),
'The full version of this secret is silently added to every input you type into @Hasher.',
"I've saved this secret phrase to a hidden location on your device hardware.",
)
self.status(
None,{indent_str(ART_FROG_BLENDER,10)},
'However, this means that you will be unable to log in to your account from any other device.',
'This limitation provides yet another level of hardware protection against network attacks.',
'However, you can always choose (not recommended) to the secret file with another device by a secure channel.',
2020-09-29 13:09:56 +00:00
3,f'But, please excuse me Comrad @{name} -- I digress.'
2020-09-11 17:17:21 +00:00
)
while not passphrase:
res = self.status(None,
{indent_str(ART_FROG_BLENDER,10)},
"@Keymaker: Please type your chosen password into @Hasher.",
('str_to_hash',f'\n@{name}: ',getpass),
pause=False
)
str_to_hash = res.get('vals').get('str_to_hash')
hashed_pass1 = hasher(str_to_hash.encode())
res = self.status(
'\n@Hasher: '+hashed_pass1,
pause=False
)
res = self.status(
'\nNow type in the same password one more time to verify it:',
('str_to_hash',f'\n@{name}: ',getpass),
pause=False
)
str_to_hash = res.get('vals').get('str_to_hash')
hashed_pass2 = hasher(str_to_hash.encode())
res = self.status(
'\n@Hasher: '+hashed_pass2,
pause=False
)
if hashed_pass1==hashed_pass2:
self.status('','@Keymaker: Excellent. The passwords clearly matched, because the hashed values matched.',pause=False)
passphrase = hashed_pass1
else:
self.status('@Keymaker: A pity. It looks like the passwords didn\'t match, since the hashed values didn\'t match either. Try again?')
return passphrase
def status_keymaker_part3(self,privkey,privkey_decr,privkey_encr,passphrase):
2020-09-12 07:55:23 +00:00
self.status(
None,{tw.indent(ART_KEY,' '*5)+'\n',True},
# None,{ART_+'\n',True},
'Now that we have a hashed passphrase, we can generate the (2A) encryption key.',
{ART_KEY_KEY2A,True,0.1},
'''The key is formed using Themis's high-level symmetric encryption library: SecureCell, using Seal mode.''',
'This key (2A) then uses the AES-256 encryption algorithm to encrypt the super-sensitive private key (2):'
)
2020-09-11 17:17:21 +00:00
2020-09-12 07:55:23 +00:00
s0=str.center('[Encryption Process]',CLI_WIDTH)
2020-09-12 09:25:09 +00:00
s1=s0 + '\n\n' + self.printt('Now that we have (2A), we can use it to encrypt the super-sensitive private key (2):',ret=True)
s2a = self.printt(f"(2A) {make_key_discreet_str(passphrase)}",ret=True)
s2 = self.printt(f"(2) {make_key_discreet(privkey.data_b64)}",ret=True)
s2b = self.printt(f"(2B) {make_key_discreet(b64encode(privkey_encr))}",ret=True)
2020-09-12 07:55:23 +00:00
self.status(
# screen 1
None,{f'{s1}'},
False,
# 2
None,{f'{s1}\n\n{ART_KEY_PAIR_SPLITTING1}'},
{s2a,True},
False,
# 3
None,{f'{s1}\n\n{ART_KEY_PAIR_SPLITTING2}\n{s2a}'},
{'\n'+s2,True},
False,
# 4
None,{f'{s1}\n\n{ART_KEY_PAIR_SPLITTING3}\n{s2a}\n\n{s2}'},
{'\n'+s2b,True},
False,
)
2020-09-11 14:35:47 +00:00
2020-09-12 07:55:23 +00:00
2020-09-12 09:25:09 +00:00
shdr=str.center('[Decryption Process]',CLI_WIDTH) + '\n\n' + self.printt('Once we have (2B), we don\'t need (2A) or (2) anymore. We can regenerate them!',ret=True)
2020-09-12 07:55:23 +00:00
from getpass import getpass
passhash = None
2020-09-11 14:35:47 +00:00
2020-09-12 07:55:23 +00:00
while passhash!=passphrase:
res = self.status(
None,{shdr},False if passhash is None else True,
2020-09-11 14:35:47 +00:00
2020-09-12 09:25:09 +00:00
("pass",self.printt(f"Let's try. Re-type your password into @Hasher:",ret=True)+f" \n ",getpass)
2020-09-12 07:55:23 +00:00
)
passhash = self.persona.crypt_keys.hash(res.get('vals').get('pass').encode())
if passhash!=passphrase:
self.status({' Looks like they don\'t match. Try again?'},False)
2020-09-11 14:35:47 +00:00
2020-09-12 07:55:23 +00:00
self.status(
{' Excellent. We can now regenerate the decryption key:'},False,
{s2a,True},False,
)
2020-09-11 14:35:47 +00:00
2020-09-12 07:55:23 +00:00
# self.status('great')
# shdr2=
self.status(
# 2
None,{f'{shdr}\n\n{ART_KEY_PAIR_SPLITTING1}'},
{s2a,True},
False,
# 3
# None,{f'{s1}\n\n{ART_KEY_PAIR_SPLITTING2}\n{s2a}'},
# {'\n'+s2,True},
# False,
# 4
None,{f'{s1}\n\n{ART_KEY_PAIR_SPLITTING4}\n{s2a}\n\n\n\n'},
{'\n'+s2b,True},
False,
)
2020-09-11 14:35:47 +00:00
2020-09-10 21:32:59 +00:00
2020-09-14 20:10:31 +00:00
def run_cli(inp):
2020-09-10 21:32:59 +00:00
cli = CLI()
2020-09-16 16:15:45 +00:00
cli.run(inp) #'/register elon') #'/register',name='elon')
2020-09-10 21:32:59 +00:00
if __name__=='__main__':
2020-09-14 20:10:31 +00:00
inp = ' '.join(sys.argv[1:])
run_cli(inp)
2020-09-12 07:55:23 +00:00
# asyncio.run(test_async())
"""
Outtakes
self.status(None,
{ART_KEY_PAIR31A},
{ART_KEY_PAIR3B+'\n',True},
3,'Allow me to explain.',
'(2A) is a separate encryption key generated by your password.',
'(2B) is a version of (2) which has been encrypted by (2A).',
"Because (2) will be destroyed, to rebuild it requires decrypting (2B) with (2A).",
)
self.status(
None,{ART_KEY_PAIR5+'\n'},
"However, in a final move, I will now destroy (2A), too.",
None,{ART_KEY_PAIR4Z1+'\n'},
'Why? Because now only you can regenerate it by remembering the password which created it.',
# None,{ART_KEY_PAIR4Z1+'\n'},
'However, this also means that if you lose or forget your password, you\'re screwed.',
None,{ART_KEY_PAIR4Z2+'\n'},
"Because without key (2A),you couldn never unlock (2B).",
None,{ART_KEY_PAIR4Z3+'\n'},
"And without (2B) and (2A) together, you could never re-assemble the private key of (2).",
None,{ART_KEY_PAIR4Z42+'\n'},
"And without (2), you couldn't read messages sent to your public key.",
)
"""