Reference client for Bitmessage: a P2P encrypted decentralised communication protocol:
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1270 lines
60 KiB

"""
3 years ago
Bitmessage commandline interface
"""
# Copyright (c) 2014 Luke Montalvo <lukemontalvo@gmail.com>
# This file adds a alternative commandline interface, feel free to critique and fork
#
# This has only been tested on Arch Linux and Linux Mint
# Dependencies:
# * from python2-pip
# * python2-pythondialog
# * dialog
import ConfigParser
import curses
import os
import sys
import time
from textwrap import fill
from threading import Timer
from dialog import Dialog
import l10n
import network.stats
import queues
import shared
import shutdown
3 years ago
from addresses import addBMIfNotPresent, decodeAddress
from bmconfigparser import BMConfigParser
from helper_ackPayload import genAckPayload
from helper_sql import sqlExecute, sqlQuery
from inventory import Inventory
# pylint: disable=global-statement
3 years ago
quit_ = False
menutab = 1
menu = ["Inbox", "Send", "Sent", "Your Identities", "Subscriptions", "Address Book", "Blacklist", "Network Status"]
naptime = 100
log = ""
logpad = None
inventorydata = 0
startuptime = time.time()
inbox = []
inboxcur = 0
sentbox = []
sentcur = 0
addresses = []
addrcur = 0
addrcopy = 0
subscriptions = []
subcur = 0
addrbook = []
abookcur = 0
blacklist = []
blackcur = 0
bwtype = "black"
BROADCAST_STR = "[Broadcast subscribers]"
3 years ago
class printLog(object):
"""Printing logs"""
3 years ago
# pylint: disable=no-self-use
def write(self, output):
3 years ago
"""Write logs"""
global log
log += output
def flush(self):
3 years ago
"""Flush logs"""
pass
3 years ago
class errLog(object):
"""Error logs"""
3 years ago
# pylint: disable=no-self-use
def write(self, output):
3 years ago
"""Write error logs"""
global log
log += "!" + output
def flush(self):
3 years ago
"""Flush error logs"""
pass
printlog = printLog()
errlog = errLog()
def cpair(a):
"""Color pairs"""
r = curses.color_pair(a)
if r not in range(1, curses.COLOR_PAIRS - 1):
r = curses.color_pair(0)
return r
def ascii(s):
"""ASCII values"""
r = ""
for c in s:
if ord(c) in range(128):
r += c
return r
def drawmenu(stdscr):
"""Creating menu's"""
menustr = " "
for i, _ in enumerate(menu):
if menutab == i + 1:
menustr = menustr[:-1]
menustr += "["
menustr += str(i + 1) + menu[i]
if menutab == i + 1:
menustr += "] "
elif i != len(menu) - 1:
menustr += " "
stdscr.addstr(2, 5, menustr, curses.A_UNDERLINE)
def set_background_title(d, title):
"""Setting background title"""
try:
d.set_background_title(title)
except:
d.add_persistent_args(("--backtitle", title))
def scrollbox(d, text, height=None, width=None):
"""Setting scroll box"""
try:
d.scrollbox(text, height, width, exit_label="Continue")
except:
d.msgbox(text, height or 0, width or 0, ok_label="Continue")
def resetlookups():
"""Reset the Inventory Lookups"""
3 years ago
global inventorydata
inventorydata = Inventory().numberOfInventoryLookupsPerformed
Inventory().numberOfInventoryLookupsPerformed = 0
Timer(1, resetlookups, ()).start()
3 years ago
def drawtab(stdscr):
"""Method for drawing different tabs"""
3 years ago
# pylint: disable=too-many-branches, too-many-statements
if menutab in range(1, len(menu) + 1):
if menutab == 1: # Inbox
stdscr.addstr(3, 5, "To", curses.A_BOLD)
stdscr.addstr(3, 40, "From", curses.A_BOLD)
stdscr.addstr(3, 80, "Subject", curses.A_BOLD)
stdscr.addstr(3, 120, "Time Received", curses.A_BOLD)
stdscr.hline(4, 5, '-', 121)
for i, item in enumerate(inbox[max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0):]):
if 6 + i < curses.LINES:
a = 0
if i == inboxcur - max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0):
# Highlight current address
a = a | curses.A_REVERSE
if item[7] is False: # If not read, highlight
a = a | curses.A_BOLD
stdscr.addstr(5 + i, 5, item[1][:34], a)
stdscr.addstr(5 + i, 40, item[3][:39], a)
stdscr.addstr(5 + i, 80, item[5][:39], a)
stdscr.addstr(5 + i, 120, item[6][:39], a)
elif menutab == 3: # Sent
stdscr.addstr(3, 5, "To", curses.A_BOLD)
stdscr.addstr(3, 40, "From", curses.A_BOLD)
stdscr.addstr(3, 80, "Subject", curses.A_BOLD)
stdscr.addstr(3, 120, "Status", curses.A_BOLD)
stdscr.hline(4, 5, '-', 121)
for i, item in enumerate(sentbox[max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0):]):
if 6 + i < curses.LINES:
a = 0
if i == sentcur - max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0):
# Highlight current address
a = a | curses.A_REVERSE
stdscr.addstr(5 + i, 5, item[0][:34], a)
stdscr.addstr(5 + i, 40, item[2][:39], a)
stdscr.addstr(5 + i, 80, item[4][:39], a)
stdscr.addstr(5 + i, 120, item[5][:39], a)
elif menutab == 2 or menutab == 4: # Send or Identities
stdscr.addstr(3, 5, "Label", curses.A_BOLD)
stdscr.addstr(3, 40, "Address", curses.A_BOLD)
stdscr.addstr(3, 80, "Stream", curses.A_BOLD)
stdscr.hline(4, 5, '-', 81)
for i, item in enumerate(addresses[max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0):]):
if 6 + i < curses.LINES:
a = 0
if i == addrcur - max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0):
# Highlight current address
a = a | curses.A_REVERSE
if item[1] and item[3] not in [8, 9]: # Embolden enabled, non-special addresses
a = a | curses.A_BOLD
stdscr.addstr(5 + i, 5, item[0][:34], a)
stdscr.addstr(5 + i, 40, item[2][:39], cpair(item[3]) | a)
stdscr.addstr(5 + i, 80, str(1)[:39], a)
elif menutab == 5: # Subscriptions
stdscr.addstr(3, 5, "Label", curses.A_BOLD)
stdscr.addstr(3, 80, "Address", curses.A_BOLD)
stdscr.addstr(3, 120, "Enabled", curses.A_BOLD)
stdscr.hline(4, 5, '-', 121)
for i, item in enumerate(subscriptions[max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0):]):
if 6 + i < curses.LINES:
a = 0
if i == subcur - max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0):
# Highlight current address
a = a | curses.A_REVERSE
if item[2]: # Embolden enabled subscriptions
a = a | curses.A_BOLD
stdscr.addstr(5 + i, 5, item[0][:74], a)
stdscr.addstr(5 + i, 80, item[1][:39], a)
stdscr.addstr(5 + i, 120, str(item[2]), a)
elif menutab == 6: # Address book
stdscr.addstr(3, 5, "Label", curses.A_BOLD)
stdscr.addstr(3, 40, "Address", curses.A_BOLD)
stdscr.hline(4, 5, '-', 41)
for i, item in enumerate(addrbook[max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0):]):
if 6 + i < curses.LINES:
a = 0
if i == abookcur - max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0):
# Highlight current address
a = a | curses.A_REVERSE
stdscr.addstr(5 + i, 5, item[0][:34], a)
stdscr.addstr(5 + i, 40, item[1][:39], a)
elif menutab == 7: # Blacklist
stdscr.addstr(3, 5, "Type: " + bwtype)
stdscr.addstr(4, 5, "Label", curses.A_BOLD)
stdscr.addstr(4, 80, "Address", curses.A_BOLD)
stdscr.addstr(4, 120, "Enabled", curses.A_BOLD)
stdscr.hline(5, 5, '-', 121)
for i, item in enumerate(blacklist[max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0):]):
if 7 + i < curses.LINES:
a = 0
if i == blackcur - max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0):
# Highlight current address
a = a | curses.A_REVERSE
if item[2]: # Embolden enabled subscriptions
a = a | curses.A_BOLD
stdscr.addstr(6 + i, 5, item[0][:74], a)
stdscr.addstr(6 + i, 80, item[1][:39], a)
stdscr.addstr(6 + i, 120, str(item[2]), a)
elif menutab == 8: # Network status
# Connection data
connected_hosts = network.stats.connectedHostsList()
stdscr.addstr(
4, 5, "Total Connections: " +
str(len(connected_hosts)).ljust(2)
)
stdscr.addstr(6, 6, "Stream #", curses.A_BOLD)
stdscr.addstr(6, 18, "Connections", curses.A_BOLD)
stdscr.hline(7, 6, '-', 23)
streamcount = []
for host, stream in connected_hosts:
if stream >= len(streamcount):
streamcount.append(1)
else:
streamcount[stream] += 1
for i, item in enumerate(streamcount):
if i < 4:
if i == 0:
stdscr.addstr(8 + i, 6, "?")
else:
stdscr.addstr(8 + i, 6, str(i))
stdscr.addstr(8 + i, 18, str(item).ljust(2))
# Uptime and processing data
stdscr.addstr(6, 35, "Since startup on " + l10n.formatTimestamp(startuptime, False))
stdscr.addstr(7, 40, "Processed " + str(
shared.numberOfMessagesProcessed).ljust(4) + " person-to-person messages.")
stdscr.addstr(8, 40, "Processed " + str(
shared.numberOfBroadcastsProcessed).ljust(4) + " broadcast messages.")
stdscr.addstr(9, 40, "Processed " + str(
shared.numberOfPubkeysProcessed).ljust(4) + " public keys.")
# Inventory data
stdscr.addstr(11, 35, "Inventory lookups per second: " + str(inventorydata).ljust(3))
# Log
stdscr.addstr(13, 6, "Log", curses.A_BOLD)
n = log.count('\n')
if n > 0:
3 years ago
lg = log.split('\n')
if n > 512:
3 years ago
del lg[:(n - 256)]
logpad.erase()
3 years ago
n = len(lg)
for i, item in enumerate(lg):
a = 0
if item and item[0] == '!':
a = curses.color_pair(1)
item = item[1:]
logpad.addstr(i, 0, item, a)
logpad.refresh(n - curses.LINES + 2, 0, 14, 6, curses.LINES - 2, curses.COLS - 7)
stdscr.refresh()
def redraw(stdscr):
"""Redraw menu"""
stdscr.erase()
stdscr.border()
drawmenu(stdscr)
stdscr.refresh()
def dialogreset(stdscr):
"""Resetting dialogue"""
stdscr.clear()
stdscr.keypad(1)
curses.curs_set(0)
# pylint: disable=too-many-branches, too-many-statements
def handlech(c, stdscr):
3 years ago
"""Handle character given on the command-line interface"""
# pylint: disable=redefined-outer-name, too-many-nested-blocks, too-many-locals
if c != curses.ERR:
global inboxcur, addrcur, sentcur, subcur, abookcur, blackcur
if c in range(256):
if chr(c) in '12345678':
global menutab
menutab = int(chr(c))
elif chr(c) == 'q':
3 years ago
global quit_
quit_ = True
elif chr(c) == '\n':
curses.curs_set(1)
d = Dialog(dialog="dialog")
if menutab == 1:
set_background_title(d, "Inbox Message Dialog Box")
r, t = d.menu(
"Do what with \"" + inbox[inboxcur][5] + "\" from \"" + inbox[inboxcur][3] + "\"?",
choices=[
("1", "View message"),
("2", "Mark message as unread"),
("3", "Reply"),
("4", "Add sender to Address Book"),
("5", "Save message as text file"),
("6", "Move to trash")])
if r == d.DIALOG_OK:
if t == "1": # View
set_background_title(
d,
"\"" +
inbox[inboxcur][5] +
"\" from \"" +
inbox[inboxcur][3] +
"\" to \"" +
inbox[inboxcur][1] +
"\"")
data = "" # pyint: disable=redefined-outer-name
ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0])
if ret != []:
for row in ret:
data, = row
data = shared.fixPotentiallyInvalidUTF8Data(data)
msg = ""
for i, item in enumerate(data.split("\n")):
msg += fill(item, replace_whitespace=False) + "\n"
scrollbox(d, unicode(ascii(msg)), 30, 80)
sqlExecute("UPDATE inbox SET read=1 WHERE msgid=?", inbox[inboxcur][0])
inbox[inboxcur][7] = 1
else:
scrollbox(d, unicode("Could not fetch message."))
3 years ago
elif t == "2": # Mark unread
sqlExecute("UPDATE inbox SET read=0 WHERE msgid=?", inbox[inboxcur][0])
inbox[inboxcur][7] = 0
3 years ago
elif t == "3": # Reply
curses.curs_set(1)
m = inbox[inboxcur]
fromaddr = m[4]
ischan = False
for i, item in enumerate(addresses):
if fromaddr == item[2] and item[3] != 0:
ischan = True
break
3 years ago
if not addresses[i][1]: # pylint: disable=undefined-loop-variable
scrollbox(d, unicode(
"Sending address disabled, please either enable it"
"or choose a different address."))
return
toaddr = m[2]
if ischan:
toaddr = fromaddr
subject = m[5]
if not m[5][:4] == "Re: ":
subject = "Re: " + m[5]
body = ""
ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", m[0])
if ret != []:
body = "\n\n------------------------------------------------------\n"
for row in ret:
body, = row
sendMessage(fromaddr, toaddr, ischan, subject, body, True)
dialogreset(stdscr)
3 years ago
elif t == "4": # Add to Address Book
addr = inbox[inboxcur][4]
if addr not in [item[1] for i, item in enumerate(addrbook)]:
r, t = d.inputbox("Label for address \"" + addr + "\"")
if r == d.DIALOG_OK:
label = t
sqlExecute("INSERT INTO addressbook VALUES (?,?)", label, addr)
# Prepend entry
addrbook.reverse()
addrbook.append([label, addr])
addrbook.reverse()
else:
scrollbox(d, unicode("The selected address is already in the Address Book."))
3 years ago
elif t == "5": # Save message
set_background_title(d, "Save \"" + inbox[inboxcur][5] + "\" as text file")
r, t = d.inputbox("Filename", init=inbox[inboxcur][5] + ".txt")
if r == d.DIALOG_OK:
msg = ""
ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0])
if ret != []:
for row in ret:
msg, = row
3 years ago
fh = open(t, "a") # Open in append mode just in case
fh.write(msg)
fh.close()
else:
scrollbox(d, unicode("Could not fetch message."))
3 years ago
elif t == "6": # Move to trash
sqlExecute("UPDATE inbox SET folder='trash' WHERE msgid=?", inbox[inboxcur][0])
del inbox[inboxcur]
scrollbox(d, unicode(
"Message moved to trash. There is no interface to view your trash,"
" \nbut the message is still on disk if you are desperate to recover it."))
elif menutab == 2:
a = ""
3 years ago
if addresses[addrcur][3] != 0: # if current address is a chan
a = addresses[addrcur][2]
sendMessage(addresses[addrcur][2], a)
elif menutab == 3:
set_background_title(d, "Sent Messages Dialog Box")
r, t = d.menu(
"Do what with \"" + sentbox[sentcur][4] + "\" to \"" + sentbox[sentcur][0] + "\"?",
choices=[
("1", "View message"),
("2", "Move to trash")])
if r == d.DIALOG_OK:
if t == "1": # View
set_background_title(
d,
"\"" +
sentbox[sentcur][4] +
"\" from \"" +
sentbox[sentcur][3] +
"\" to \"" +
sentbox[sentcur][1] +
"\"")
data = ""
ret = sqlQuery(
"SELECT message FROM sent WHERE subject=? AND ackdata=?",
sentbox[sentcur][4],
sentbox[sentcur][6])
if ret != []:
for row in ret:
data, = row
data = shared.fixPotentiallyInvalidUTF8Data(data)
msg = ""
for i, item in enumerate(data.split("\n")):
msg += fill(item, replace_whitespace=False) + "\n"
scrollbox(d, unicode(ascii(msg)), 30, 80)
else:
scrollbox(d, unicode("Could not fetch message."))
3 years ago
elif t == "2": # Move to trash
sqlExecute(
"UPDATE sent SET folder='trash' WHERE subject=? AND ackdata=?",
sentbox[sentcur][4],
sentbox[sentcur][6])
del sentbox[sentcur]
scrollbox(d, unicode(
"Message moved to trash. There is no interface to view your trash"
" \nbut the message is still on disk if you are desperate to recover it."))
elif menutab == 4:
set_background_title(d, "Your Identities Dialog Box")
if len(addresses) <= addrcur:
r, t = d.menu(
"Do what with addresses?",
choices=[
("1", "Create new address")])
else:
r, t = d.menu(
"Do what with \"" + addresses[addrcur][0] + "\" : \"" + addresses[addrcur][2] + "\"?",
choices=[
("1", "Create new address"),
("2", "Send a message from this address"),
("3", "Rename"),
("4", "Enable"),
("5", "Disable"),
("6", "Delete"),
("7", "Special address behavior")])
if r == d.DIALOG_OK:
3 years ago
if t == "1": # Create new address
set_background_title(d, "Create new address")
scrollbox(
d, unicode(
"Here you may generate as many addresses as you like.\n"
"Indeed, creating and abandoning addresses is encouraged.\n"
"Deterministic addresses have several pros and cons:\n"
"\nPros:\n"
" * You can recreate your addresses on any computer from memory\n"
" * You need not worry about backing up your keys.dat file as long as you"
" \n can remember your passphrase\n"
"Cons:\n"
" * You must remember (or write down) your passphrase in order to recreate"
" \n your keys if they are lost\n"
" * You must also remember the address version and stream numbers\n"
" * If you choose a weak passphrase someone may be able to brute-force it"
" \n and then send and receive messages as you"))
r, t = d.menu(
"Choose an address generation technique",
choices=[
("1", "Use a random number generator"),
("2", "Use a passphrase")])
if r == d.DIALOG_OK:
if t == "1":
set_background_title(d, "Randomly generate address")
r, t = d.inputbox("Label (not shown to anyone except you)")
label = ""
if r == d.DIALOG_OK and t:
label = t
r, t = d.menu(
"Choose a stream",
choices=[("1", "Use the most available stream"),
("", "(Best if this is the first of many addresses you will create)"),
("2", "Use the same stream as an existing address"),
("", "(Saves you some bandwidth and processing power)")])
if r == d.DIALOG_OK:
if t == "1":
stream = 1
elif t == "2":
addrs = []
for i, item in enumerate(addresses):
addrs.append([str(i), item[2]])
r, t = d.menu("Choose an existing address's stream", choices=addrs)
if r == d.DIALOG_OK:
stream = decodeAddress(addrs[int(t)][1])[2]
shorten = False
r, t = d.checklist(
"Miscellaneous options",
choices=[(
"1",
"Spend time shortening the address",
1 if shorten else 0)])
if r == d.DIALOG_OK and "1" in t:
shorten = True
queues.addressGeneratorQueue.put((
"createRandomAddress",
4,
stream,
label,
1,
"",
shorten))
elif t == "2":
set_background_title(d, "Make deterministic addresses")