diff --git a/src/bitmessagecurses/__init__.py b/src/bitmessagecurses/__init__.py new file mode 100644 index 00000000..91c99df8 --- /dev/null +++ b/src/bitmessagecurses/__init__.py @@ -0,0 +1,1040 @@ +# Copyright (c) 2014 Luke Montalvo +# 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 os +import sys +import StringIO +from textwrap import * + +import time +from time import strftime, localtime +from threading import Timer + +import curses +import dialog +from dialog import Dialog +from helper_sql import * + +import shared +import ConfigParser +from addresses import * +from pyelliptic.openssl import OpenSSL + +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]" + +class printLog: + def write(self, output): + global log + log += output + def flush(self): + pass +class errLog: + def write(self, output): + global log + log += "!"+output + def flush(self): + pass +printlog = printLog() +errlog = errLog() + + +def cpair(a): + 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): + r = "" + for c in s: + if ord(c) in range(128): + r += c + return r + +def drawmenu(stdscr): + menustr = " " + for i in range(0, len(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 resetlookups(): + global inventorydata + inventorydata = shared.numberOfInventoryLookupsPerformed + shared.numberOfInventoryLookupsPerformed = 0 + Timer(1, resetlookups, ()).start() +def drawtab(stdscr): + 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] == 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] == True 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] == True: # 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] == True: # 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 + stdscr.addstr(4, 5, "Total Connections: "+str(len(shared.connectedHostsList)).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 shared.connectedHostsList.items(): + 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 "+unicode(strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(startuptime))))) + 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: + l = log.split('\n') + if n > 512: + del l[:(n-256)] + logpad.erase() + n = len(l) + for i, item in enumerate(l): + a = 0 + if len(item) > 0 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): + stdscr.erase() + stdscr.border() + drawmenu(stdscr) + stdscr.refresh() +def dialogreset(stdscr): + stdscr.clear() + stdscr.keypad(1) + curses.curs_set(0) +def handlech(c, stdscr): + 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': + global quit + quit = True + elif chr(c) == '\n': + curses.curs_set(1) + d = Dialog(dialog="dialog") + if menutab == 1: + d.set_background_title("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 + d.set_background_title("\""+inbox[inboxcur][5]+"\" from \""+inbox[inboxcur][3]+"\" to \""+inbox[inboxcur][1]+"\"") + data = "" + 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" + d.scrollbox(unicode(ascii(msg)), 30, 80, exit_label="Continue") + sqlExecute("UPDATE inbox SET read=1 WHERE msgid=?", inbox[inboxcur][0]) + inbox[inboxcur][7] = 1 + else: + d.scrollbox(unicode("Could not fetch message."), exit_label="Continue") + elif t == "2": # Mark unread + sqlExecute("UPDATE inbox SET read=0 WHERE msgid=?", inbox[inboxcur][0]) + inbox[inboxcur][7] = 0 + 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 + if not addresses[i][1]: + d.scrollbox(unicode("Sending address disabled, please either enable it or choose a different address."), exit_label="Continue") + 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) + elif t == "4": # Add to Address Book + global addrbook + 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: + d.scrollbox(unicode("The selected address is already in the Address Book."), exit_label="Continue") + elif t == "5": # Save message + d.set_background_title("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 + fh = open(t, "a") # Open in append mode just in case + fh.write(msg) + fh.close() + else: + d.scrollbox(unicode("Could not fetch message."), exit_label="Continue") + elif t == "6": # Move to trash + sqlExecute("UPDATE inbox SET folder='trash' WHERE msgid=?", inbox[inboxcur][0]) + del inbox[inboxcur] + d.scrollbox(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."), + exit_label="Continue") + elif menutab == 2: + a = "" + if addresses[addrcur][3] != 0: # if current address is a chan + a = addresses[addrcur][2] + sendMessage(addresses[addrcur][2], a) + elif menutab == 3: + d.set_background_title("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 + d.set_background_title("\""+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" + d.scrollbox(unicode(ascii(msg)), 30, 80, exit_label="Continue") + else: + d.scrollbox(unicode("Could not fetch message."), exit_label="Continue") + 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] + d.scrollbox(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."), + exit_label="Continue") + elif menutab == 4: + d.set_background_title("Your Identities Dialog Box") + 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: + if t == "1": # Create new address + d.set_background_title("Create new address") + d.scrollbox(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"), + exit_label="Continue") + 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": + d.set_background_title("Randomly generate address") + r, t = d.inputbox("Label (not shown to anyone except you)") + label = "" + if r == d.DIALOG_OK and len(t) > 0: + 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", shorten)]) + if r == d.DIALOG_OK and "1" in t: + shorten = True + shared.addressGeneratorQueue.put(("createRandomAddress", 4, stream, label, 1, "", shorten)) + elif t == "2": + d.set_background_title("Make deterministic addresses") + r, t = d.passwordform("Enter passphrase", + [("Passphrase", 1, 1, "", 2, 1, 64, 128), + ("Confirm passphrase", 3, 1, "", 4, 1, 64, 128)], + form_height=4, insecure=True) + if r == d.DIALOG_OK: + if t[0] == t[1]: + passphrase = t[0] + r, t = d.rangebox("Number of addresses to generate", + width=48, min=1, max=99, init=8) + if r == d.DIALOG_OK: + number = t + stream = 1 + shorten = False + r, t = d.checklist("Miscellaneous options", + choices=[("1", "Spend time shortening the address", shorten)]) + if r == d.DIALOG_OK and "1" in t: + shorten = True + d.scrollbox(unicode("In addition to your passphrase, be sure to remember the following numbers:\n" + "\n * Address version number: "+str(4)+"\n" + " * Stream number: "+str(stream)), + exit_label="Continue") + shared.addressGeneratorQueue.put(('createDeterministicAddresses', 4, stream, "unused deterministic address", number, str(passphrase), shorten)) + else: + d.scrollbox(unicode("Passphrases do not match"), exit_label="Continue") + elif t == "2": # Send a message + a = "" + if addresses[addrcur][3] != 0: # if current address is a chan + a = addresses[addrcur][2] + sendMessage(addresses[addrcur][2], a) + elif t == "3": # Rename address label + a = addresses[addrcur][2] + label = addresses[addrcur][0] + r, t = d.inputbox("New address label", init=label) + if r == d.DIALOG_OK: + label = t + shared.config.set(a, "label", label) + # Write config + with open(shared.appdata + 'keys.dat', 'wb') as configfile: + shared.config.write(configfile) + addresses[addrcur][0] = label + elif t == "4": # Enable address + a = addresses[addrcur][2] + shared.config.set(a, "enabled", "true") # Set config + # Write config + with open(shared.appdata + 'keys.dat', 'wb') as configfile: + shared.config.write(configfile) + # Change color + if shared.safeConfigGetBoolean(a, 'chan'): + addresses[addrcur][3] = 9 # orange + elif shared.safeConfigGetBoolean(a, 'mailinglist'): + addresses[addrcur][3] = 5 # magenta + else: + addresses[addrcur][3] = 0 # black + addresses[addrcur][1] = True + shared.reloadMyAddressHashes() # Reload address hashes + elif t == "5": # Disable address + a = addresses[addrcur][2] + shared.config.set(a, "enabled", "false") # Set config + addresses[addrcur][3] = 8 # Set color to gray + # Write config + with open(shared.appdata + 'keys.dat', 'wb') as configfile: + shared.config.write(configfile) + addresses[addrcur][1] = False + shared.reloadMyAddressHashes() # Reload address hashes + elif t == "6": # Delete address + r, t = d.inputbox("Type in \"I want to delete this address\"", width=50) + if r == d.DIALOG_OK and t == "I want to delete this address": + shared.config.remove_section(addresses[addrcur][2]) + with open(shared.appdata + 'keys.dat', 'wb') as configfile: + shared.config.write(configfile) + del addresses[addrcur] + elif t == "7": # Special address behavior + a = addresses[addrcur][2] + d.set_background_title("Special address behavior") + if shared.safeConfigGetBoolean(a, "chan"): + d.scrollbox(unicode("This is a chan address. You cannot use it as a pseudo-mailing list."), exit_label="Continue") + else: + m = shared.safeConfigGetBoolean(a, "mailinglist") + r, t = d.radiolist("Select address behavior", + choices=[("1", "Behave as a normal address", not m), + ("2", "Behave as a pseudo-mailing-list address", m)]) + if r == d.DIALOG_OK: + if t == "1" and m == True: + shared.config.set(a, "mailinglist", "false") + if addresses[addrcur][1]: + addresses[addrcur][3] = 0 # Set color to black + else: + addresses[addrcur][3] = 8 # Set color to gray + elif t == "2" and m == False: + try: + mn = shared.config.get(a, "mailinglistname") + except ConfigParser.NoOptionError: + mn = "" + r, t = d.inputbox("Mailing list name", init=mn) + if r == d.DIALOG_OK: + mn = t + shared.config.set(a, "mailinglist", "true") + shared.config.set(a, "mailinglistname", mn) + addresses[addrcur][3] = 6 # Set color to magenta + # Write config + with open(shared.appdata + 'keys.dat', 'wb') as configfile: + shared.config.write(configfile) + elif menutab == 5: + d.set_background_title("Subscriptions Dialog Box") + r, t = d.menu("Do what with subscription to \""+subscriptions[subcur][0]+"\"?", + choices=[("1", "Add new subscription"), + ("2", "Delete this subscription"), + ("3", "Enable"), + ("4", "Disable")]) + if r == d.DIALOG_OK: + if t == "1": + r, t = d.inputbox("New subscription address") + if r == d.DIALOG_OK: + addr = addBMIfNotPresent(t) + if not shared.isAddressInMySubscriptionsList(addr): + r, t = d.inputbox("New subscription label") + if r == d.DIALOG_OK: + label = t + # Prepend entry + subscriptions.reverse() + subscriptions.append([label, addr, True]) + subscriptions.reverse() + + sqlExecute("INSERT INTO subscriptions VALUES (?,?,?)", label, address, True) + shared.reloadBroadcastSendersForWhichImWatching() + elif t == "2": + r, t = d.inpuxbox("Type in \"I want to delete this subscription\"") + if r == d.DIALOG_OK and t == "I want to delete this subscription": + sqlExecute("DELETE FROM subscriptions WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) + shared.reloadBroadcastSendersForWhichImWatching() + del subscriptions[subcur] + elif t == "3": + sqlExecute("UPDATE subscriptions SET enabled=1 WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) + shared.reloadBroadcastSendersForWhichImWatching() + subscriptions[subcur][2] = True + elif t == "4": + sqlExecute("UPDATE subscriptions SET enabled=0 WHERE label=? AND address=?", subscriptions[subcur][0], subscriptions[subcur][1]) + shared.reloadBroadcastSendersForWhichImWatching() + subscriptions[subcur][2] = False + elif menutab == 6: + d.set_background_title("Address Book Dialog Box") + r, t = d.menu("Do what with \""+addrbook[abookcur][0]+"\" : \""+addrbook[abookcur][1]+"\"", + choices=[("1", "Send a message to this address"), + ("2", "Subscribe to this address"), + ("3", "Add new address to Address Book"), + ("4", "Delete this address")]) + if r == d.DIALOG_OK: + if t == "1": + sendMessage(recv=addrbook[abookcur][1]) + elif t == "2": + r, t = d.inputbox("New subscription label") + if r == d.DIALOG_OK: + label = t + # Prepend entry + subscriptions.reverse() + subscriptions.append([label, addr, True]) + subscriptions.reverse() + + sqlExecute("INSERT INTO subscriptions VALUES (?,?,?)", label, address, True) + shared.reloadBroadcastSendersForWhichImWatching() + elif t == "3": + r, t = d.inputbox("Input new address") + if r == d.DIALOG_OK: + addr = t + 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: + sqlExecute("INSERT INTO addressbook VALUES (?,?)", t, addr) + # Prepend entry + addrbook.reverse() + addrbook.append([t, addr]) + addrbook.reverse() + else: + d.scrollbox(unicode("The selected address is already in the Address Book."), exit_label="Continue") + elif t == "4": + r, t = d.inputbox("Type in \"I want to delete this Address Book entry\"") + if r == d.DIALOG_OK and t == "I want to delete this Address Book entry": + sqlExecute("DELETE FROM addressbook WHERE label=? AND address=?", addrbook[abookcur][0], addrbook[abookcur][1]) + del addrbook[abookcur] + elif menutab == 7: + d.set_background_title("Blacklist Dialog Box") + r, t = d.menu("Do what with \""+blacklist[blackcur][0]+"\" : \""+blacklist[blackcur][1]+"\"?", + choices=[("1", "Delete"), + ("2", "Enable"), + ("3", "Disable")]) + if r == d.DIALOG_OK: + if t == "1": + r, t = d.inputbox("Type in \"I want to delete this Blacklist entry\"") + if r == d.DIALOG_OK and t == "I want to delete this Blacklist entry": + sqlExecute("DELETE FROM blacklist WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) + del blacklist[blackcur] + elif t == "2": + sqlExecute("UPDATE blacklist SET enabled=1 WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) + blacklist[blackcur][2] = True + elif t== "3": + sqlExecute("UPDATE blacklist SET enabled=0 WHERE label=? AND address=?", blacklist[blackcur][0], blacklist[blackcur][1]) + blacklist[blackcur][2] = False + dialogreset(stdscr) + else: + if c == curses.KEY_UP: + if menutab == 1 and inboxcur > 0: + inboxcur -= 1 + if (menutab == 2 or menutab == 4) and addrcur > 0: + addrcur -= 1 + if menutab == 3 and sentcur > 0: + sentcur -= 1 + if menutab == 5 and subcur > 0: + subcur -= 1 + if menutab == 6 and abookcur > 0: + abookcur -= 1 + if menutab == 7 and blackcur > 0: + blackcur -= 1 + elif c == curses.KEY_DOWN: + if menutab == 1 and inboxcur < len(inbox)-1: + inboxcur += 1 + if (menutab == 2 or menutab == 4) and addrcur < len(addresses)-1: + addrcur += 1 + if menutab == 3 and sentcur < len(sentbox)-1: + sentcur += 1 + if menutab == 5 and subcur < len(subscriptions)-1: + subcur += 1 + if menutab == 6 and abookcur < len(addrbook)-1: + abookcur += 1 + if menutab == 7 and blackcur < len(blacklist)-1: + blackcur += 1 + elif c == curses.KEY_HOME: + if menutab == 1: + inboxcur = 0 + if menutab == 2 or menutab == 4: + addrcur = 0 + if menutab == 3: + sentcur = 0 + if menutab == 5: + subcur = 0 + if menutab == 6: + abookcur = 0 + if menutab == 7: + blackcur = 0 + elif c == curses.KEY_END: + if menutab == 1: + inboxcur = len(inbox)-1 + if menutab == 2 or menutab == 4: + addrcur = len(addresses)-1 + if menutab == 3: + sentcur = len(sentbox)-1 + if menutab == 5: + subcur = len(subscriptions)-1 + if menutab == 6: + abookcur = len(addrbook)-1 + if menutab == 7: + blackcur = len(blackcur)-1 + redraw(stdscr) +def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=False): + if sender == "": + return + d = Dialog(dialog="dialog") + d.set_background_title("Send a message") + if recv == "": + r, t = d.inputbox("Recipient address (Cancel to load from the Address Book or leave blank to broadcast)", 10, 60) + if r != d.DIALOG_OK: + global menutab + menutab = 6 + return + recv = t + if broadcast == None and sender != recv: + r, t = d.radiolist("How to send the message?", + choices=[("1", "Send to one or more specific people", True), + ("2", "Broadcast to everyone who is subscribed to your address", False)]) + if r != d.DIALOG_OK: + return + broadcast = False + if t == "2": # Broadcast + broadcast = True + if subject == "" or reply: + r, t = d.inputbox("Message subject", width=60, init=subject) + if r != d.DIALOG_OK: + return + subject = t + if body == "" or reply: + r, t = d.inputbox("Message body", 10, 80, init=body) + if r != d.DIALOG_OK: + return + body = t + body = body.replace("\\n", "\n").replace("\\t", "\t") + + if not broadcast: + recvlist = [] + for i, item in enumerate(recv.replace(",", ";").split(";")): + recvlist.append(item.strip()) + list(set(recvlist)) # Remove exact duplicates + for addr in recvlist: + if addr != "": + status, version, stream, ripe = decodeAddress(addr) + if status != "success": + d.set_background_title("Recipient address error") + err = "Could not decode" + addr + " : " + status + "\n\n" + if status == "missingbm": + err += "Bitmessage addresses should start with \"BM-\"." + elif status == "checksumfailed": + err += "The address was not typed or copied correctly." + elif status == "invalidcharacters": + err += "The address contains invalid characters." + elif status == "versiontoohigh": + err += "The address version is too high. Either you need to upgrade your Bitmessage software or your acquaintance is doing something clever." + elif status == "ripetooshort": + err += "Some data encoded in the address is too short. There might be something wrong with the software of your acquaintance." + elif status == "ripetoolong": + err += "Some data encoded in the address is too long. There might be something wrong with the software of your acquaintance." + else: + err += "It is unknown what is wrong with the address." + d.scrollbox(unicode(err), exit_label="Continue") + else: + addr = addBMIfNotPresent(addr) + if version > 4 or version <= 1: + d.set_background_title("Recipient address error") + d.scrollbox(unicode("Could not understand version number " + version + "of address" + addr + "."), + exit_label="Continue") + continue + if stream > 1 or stream == 0: + d.set_background_title("Recipient address error") + d.scrollbox(unicode("Bitmessage currently only supports stream numbers of 1, unlike as requested for address " + addr + "."), + exit_label="Continue") + continue + if len(shared.connectedHostsList) == 0: + d.set_background_title("Not connected warning") + d.scrollbox(unicode("Because you are not currently connected to the network, "), + exit_label="Continue") + ackdata = OpenSSL.rand(32) + sqlExecute( + "INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", + "", + addr, + ripe, + sender, + subject, + body, + ackdata, + int(time.time()), + "msgqueued", + 1, + 1, + "sent", + 2) + shared.workerQueue.put(("sendmessage", addr)) + else: # Broadcast + if recv == "": + d.set_background_title("Empty sender error") + d.scrollbox(unicode("You must specify an address to send the message from."), + exit_label="Continue") + else: + ackdata = OpenSSL.rand(32) + recv = BROADCAST_STR + ripe = "" + sqlExecute( + "INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", + "", + recv, + ripe, + sender, + subject, + body, + ackdata, + int(time.time()), + "broadcastqueued", + 1, + 1, + "sent", + 2) + shared.workerQueue.put(('sendbroadcast', '')) + +def loadInbox(): + sys.stdout = sys.__stdout__ + print("Loading inbox messages...") + sys.stdout = printlog + + where = "toaddress || fromaddress || subject || message" + what = "%%" + ret = sqlQuery("""SELECT msgid, toaddress, fromaddress, subject, received, read + FROM inbox WHERE folder='inbox' AND %s LIKE ? + ORDER BY received + """ % (where,), what) + global inbox + for row in ret: + msgid, toaddr, fromaddr, subject, received, read = row + subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) + + # Set label for to address + try: + if toaddr == BROADCAST_STR: + tolabel = BROADCAST_STR + else: + tolabel = shared.config.get(toaddr, "label") + except: + tolabel = "" + if tolabel == "": + tolabel = toaddr + tolabel = shared.fixPotentiallyInvalidUTF8Data(tolabel) + + # Set label for from address + fromlabel = "" + if shared.config.has_section(fromaddr): + fromlabel = shared.config.get(fromaddr, "label") + if fromlabel == "": # Check Address Book + qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", fromaddr) + if qr != []: + for r in qr: + fromlabel, = r + if fromlabel == "": # Check Subscriptions + qr = sqlQuery("SELECT label FROM subscriptions WHERE address=?", fromaddr) + if qr != []: + for r in qr: + fromlabel, = r + if fromlabel == "": + fromlabel = fromaddr + fromlabel = shared.fixPotentiallyInvalidUTF8Data(fromlabel) + + # Load into array + inbox.append([msgid, tolabel, toaddr, fromlabel, fromaddr, subject, + strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(received))), + read]) + inbox.reverse() +def loadSent(): + sys.stdout = sys.__stdout__ + print("Loading sent messages...") + sys.stdout = printlog + + where = "toaddress || fromaddress || subject || message" + what = "%%" + ret = sqlQuery("""SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime + FROM sent WHERE folder='sent' AND %s LIKE ? + ORDER BY lastactiontime + """ % (where,), what) + global sent + for row in ret: + toaddr, fromaddr, subject, status, ackdata, lastactiontime = row + subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) + + # Set label for to address + tolabel = "" + qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", toaddr) + if qr != []: + for r in qr: + tolabel, = r + if tolabel == "": + qr = sqlQuery("SELECT label FROM subscriptions WHERE address=?", toaddr) + if qr != []: + for r in qr: + tolabel, = r + if tolabel == "": + if shared.config.has_section(toaddr): + tolabel = shared.config.get(toaddr, "label") + if tolabel == "": + tolabel = toaddr + + # Set label for from address + fromlabel = "" + if shared.config.has_section(fromaddr): + fromlabel = shared.config.get(fromaddr, "label") + if fromlabel == "": + fromlabel = fromaddr + + # Set status string + if status == "awaitingpubkey": + statstr = "Waiting for their public key. Will request it again soon" + elif status == "doingpowforpubkey": + statstr = "Encryption key request queued" + elif status == "msgqueued": + statstr = "Message queued" + elif status == "msgsent": + t = strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(lastactiontime))) + statstr = "Message sent at "+t+".Waiting for acknowledgement." + elif status == "msgsentnoackexpected": + t = strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(lastactiontime))) + statstr = "Message sent at "+t+"." + elif status == "doingmsgpow": + statstr = "The proof of work required to send the message has been queued." + elif status == "askreceived": + t = strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(lastactiontime))) + statstr = "Acknowledgment of the message received at "+t+"." + elif status == "broadcastqueued": + statstr = "Broadcast queued." + elif status == "broadcastsent": + t = strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(lastactiontime))) + statstr = "Broadcast sent at "+t+"." + elif status == "forcepow": + statstr = "Forced difficulty override. Message will start sending soon." + elif status == "badkey": + statstr = "Warning: Could not encrypt message because the recipient's encryption key is no good." + elif status == "toodifficult": + statstr = "Error: The work demanded by the recipient is more difficult than you are willing to do." + else: + t = strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(lastactiontime))) + statstr = "Unknown status "+status+" at "+t+"." + + # Load into array + sentbox.append([tolabel, toaddr, fromlabel, fromaddr, subject, statstr, ackdata, + strftime(shared.config.get('bitmessagesettings', 'timeformat'), localtime(int(lastactiontime)))]) + sentbox.reverse() +def loadAddrBook(): + sys.stdout = sys.__stdout__ + print("Loading address book...") + sys.stdout = printlog + + ret = sqlQuery("SELECT label, address FROM addressbook") + global addrbook + for row in ret: + label, addr = row + label = shared.fixPotentiallyInvalidUTF8Data(label) + addrbook.append([label, addr]) + addrbook.reverse() +def loadSubscriptions(): + ret = sqlQuery("SELECT label, address, enabled FROM subscriptions") + for row in ret: + label, address, enabled = row + subscriptions.append([label, address, enabled]) + subscriptions.reverse() +def loadBlackWhiteList(): + global bwtype + bwtype = shared.config.get("bitmessagesettings", "blackwhitelist") + if bwtype == "black": + ret = sqlQuery("SELECT label, address, enabled FROM blacklist") + else: + ret = sqlQuery("SELECT label, address, enabled FROM whitelist") + for row in ret: + label, address, enabled = row + blacklist.append([label, address, enabled]) + blacklist.reverse() + +def runwrapper(): + sys.stdout = printlog + #sys.stderr = errlog + + # Load messages from database + loadInbox() + loadSent() + loadAddrBook() + loadSubscriptions() + loadBlackWhiteList() + + stdscr = curses.initscr() + + global logpad + logpad = curses.newpad(1024, curses.COLS) + + stdscr.nodelay(0) + curses.curs_set(0) + stdscr.timeout(1000) + + curses.wrapper(run) + shutdown() + +def run(stdscr): + # Schedule inventory lookup data + resetlookups() + + # Init color pairs + if curses.has_colors(): + curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red + curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # green + curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow + curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue + curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta + curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan + curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # white + if curses.can_change_color(): + curses.init_color(8, 500, 500, 500) # gray + curses.init_pair(8, 8, 0) + curses.init_color(9, 844, 465, 0) # orange + curses.init_pair(9, 9, 0) + else: + curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish + curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish + + # Init list of address in 'Your Identities' tab + configSections = shared.config.sections() + for addressInKeysFile in configSections: + if addressInKeysFile != "bitmessagesettings": + isEnabled = shared.config.getboolean(addressInKeysFile, "enabled") + addresses.append([shared.config.get(addressInKeysFile, "label"), isEnabled, addressInKeysFile]) + # Set address color + if not isEnabled: + addresses[len(addresses)-1].append(8) # gray + elif shared.safeConfigGetBoolean(addressInKeysFile, 'chan'): + addresses[len(addresses)-1].append(9) # orange + elif shared.safeConfigGetBoolean(addressInKeysFile, 'mailinglist'): + addresses[len(addresses)-1].append(5) # magenta + else: + addresses[len(addresses)-1].append(0) # black + addresses.reverse() + + stdscr.clear() + redraw(stdscr) + while quit == False: + drawtab(stdscr) + handlech(stdscr.getch(), stdscr) + +def shutdown(): + sys.stdout = sys.__stdout__ + print("Shutting down...") + sys.stdout = printlog + shared.doCleanShutdown() + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + + os._exit(0) diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index 4bcdcd1b..b46e5a03 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2.7 # Copyright (c) 2012 Jonathan Warren # Copyright (c) 2012 The Bitmessage developers # Distributed under the MIT/X11 software license. See the accompanying @@ -154,6 +154,11 @@ class Main: # is the application already running? If yes then exit. thisapp = singleton.singleinstance() + # get curses flag + curses = False + if '-c' in sys.argv: + curses = True + signal.signal(signal.SIGINT, helper_generic.signal_handler) # signal.signal(signal.SIGINT, signal.SIG_DFL) @@ -214,10 +219,16 @@ class Main: except Exception as err: print 'PyBitmessage requires PyQt unless you want to run it as a daemon and interact with it using the API. You can download PyQt from http://www.riverbankcomputing.com/software/pyqt/download or by searching Google for \'PyQt Download\'. If you want to run in daemon mode, see https://bitmessage.org/wiki/Daemon' print 'Error message:', err + print 'You can also run PyBitmessage with the new curses interface by providing \'-c\' as a commandline argument.' os._exit(0) - import bitmessageqt - bitmessageqt.run() + if curses == False: + import bitmessageqt + bitmessageqt.run() + else: + print 'Running with curses' + import bitmessagecurses + bitmessagecurses.runwrapper() else: shared.config.remove_option('bitmessagesettings', 'dontconnect') diff --git a/src/debug.py b/src/debug.py index fe7815e7..02ab94f8 100644 --- a/src/debug.py +++ b/src/debug.py @@ -19,6 +19,7 @@ Use: `from debug import logger` to import this facility into whatever module you import logging import logging.config import shared +import sys # TODO(xj9): Get from a config file. log_level = 'DEBUG' @@ -69,7 +70,10 @@ def configureLogging(): # TODO (xj9): Get from a config file. #logger = logging.getLogger('console_only') configureLogging() -logger = logging.getLogger('both') +if '-c' in sys.argv: + logger = logging.getLogger('file_only') +else: + logger = logging.getLogger('both') def restartLoggingInUpdatedAppdataLocation(): global logger @@ -78,4 +82,8 @@ def restartLoggingInUpdatedAppdataLocation(): i.flush() i.close() configureLogging() - logger = logging.getLogger('both') \ No newline at end of file + if '-c' in sys.argv: + logger = logging.getLogger('file_only') + else: + logger = logging.getLogger('both') +