__init__.py
1 """ 2 Bitmessage commandline interface 3 """ 4 # Copyright (c) 2014 Luke Montalvo <lukemontalvo@gmail.com> 5 # This file adds a alternative commandline interface, feel free to critique and fork 6 # 7 # This has only been tested on Arch Linux and Linux Mint 8 # Dependencies: 9 # * from python2-pip 10 # * python2-pythondialog 11 # * dialog 12 13 import configparser 14 import curses 15 import os 16 import sys 17 import time 18 from textwrap import fill 19 from threading import Timer 20 21 from dialog import Dialog 22 import helper_sent 23 import l10n 24 import network.stats 25 import queues 26 import shared 27 import shutdown 28 import state 29 30 from addresses import addBMIfNotPresent, decodeAddress 31 from bmconfigparser import config 32 from helper_sql import sqlExecute, sqlQuery 33 34 # pylint: disable=global-statement 35 36 quit_ = False 37 menutab = 1 38 menu = ["Inbox", "Send", "Sent", "Your Identities", "Subscriptions", "Address Book", "Blacklist", "Network Status"] 39 naptime = 100 40 log = "" 41 logpad = None 42 inventorydata = 0 43 startuptime = time.time() 44 45 inbox = [] 46 inboxcur = 0 47 sentbox = [] 48 sentcur = 0 49 addresses = [] 50 addrcur = 0 51 addrcopy = 0 52 subscriptions = [] 53 subcur = 0 54 addrbook = [] 55 abookcur = 0 56 blacklist = [] 57 blackcur = 0 58 bwtype = "black" 59 60 BROADCAST_STR = "[Broadcast subscribers]" 61 62 63 class printLog(object): 64 """Printing logs""" 65 # pylint: disable=no-self-use 66 67 def write(self, output): 68 """Write logs""" 69 global log 70 log += output 71 72 def flush(self): 73 """Flush logs""" 74 pass 75 76 77 class errLog(object): 78 """Error logs""" 79 # pylint: disable=no-self-use 80 81 def write(self, output): 82 """Write error logs""" 83 global log 84 log += "!" + output 85 86 def flush(self): 87 """Flush error logs""" 88 pass 89 90 91 printlog = printLog() 92 errlog = errLog() 93 94 95 def cpair(a): 96 """Color pairs""" 97 r = curses.color_pair(a) 98 if r not in list(range(1, curses.COLOR_PAIRS - 1)): 99 r = curses.color_pair(0) 100 return r 101 102 103 def ascii(s): 104 """ASCII values""" 105 r = "" 106 for c in s: 107 if ord(c) in range(128): 108 r += c 109 return r 110 111 112 def drawmenu(stdscr): 113 """Creating menu's""" 114 menustr = " " 115 for i, _ in enumerate(menu): 116 if menutab == i + 1: 117 menustr = menustr[:-1] 118 menustr += "[" 119 menustr += str(i + 1) + menu[i] 120 if menutab == i + 1: 121 menustr += "] " 122 elif i != len(menu) - 1: 123 menustr += " " 124 stdscr.addstr(2, 5, menustr, curses.A_UNDERLINE) 125 126 127 def set_background_title(d, title): 128 """Setting background title""" 129 try: 130 d.set_background_title(title) 131 except: # noqa:E722 132 d.add_persistent_args(("--backtitle", title)) 133 134 135 def scrollbox(d, text, height=None, width=None): 136 """Setting scroll box""" 137 try: 138 d.scrollbox(text, height, width, exit_label="Continue") 139 except: # noqa:E722 140 d.msgbox(text, height or 0, width or 0, ok_label="Continue") 141 142 143 def resetlookups(): 144 """Reset the Inventory Lookups""" 145 global inventorydata 146 inventorydata = state.Inventory.numberOfInventoryLookupsPerformed 147 state.Inventory.numberOfInventoryLookupsPerformed = 0 148 Timer(1, resetlookups, ()).start() 149 150 151 def drawtab(stdscr): 152 """Method for drawing different tabs""" 153 # pylint: disable=too-many-branches, too-many-statements 154 if menutab in range(1, len(menu) + 1): 155 if menutab == 1: # Inbox 156 stdscr.addstr(3, 5, "To", curses.A_BOLD) 157 stdscr.addstr(3, 40, "From", curses.A_BOLD) 158 stdscr.addstr(3, 80, "Subject", curses.A_BOLD) 159 stdscr.addstr(3, 120, "Time Received", curses.A_BOLD) 160 stdscr.hline(4, 5, '-', 121) 161 for i, item in enumerate(inbox[max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0):]): 162 if 6 + i < curses.LINES: 163 a = 0 164 if i == inboxcur - max(min(len(inbox) - curses.LINES + 6, inboxcur - 5), 0): 165 # Highlight current address 166 a = a | curses.A_REVERSE 167 if item[7] is False: # If not read, highlight 168 a = a | curses.A_BOLD 169 stdscr.addstr(5 + i, 5, item[1][:34], a) 170 stdscr.addstr(5 + i, 40, item[3][:39], a) 171 stdscr.addstr(5 + i, 80, item[5][:39], a) 172 stdscr.addstr(5 + i, 120, item[6][:39], a) 173 elif menutab == 3: # Sent 174 stdscr.addstr(3, 5, "To", curses.A_BOLD) 175 stdscr.addstr(3, 40, "From", curses.A_BOLD) 176 stdscr.addstr(3, 80, "Subject", curses.A_BOLD) 177 stdscr.addstr(3, 120, "Status", curses.A_BOLD) 178 stdscr.hline(4, 5, '-', 121) 179 for i, item in enumerate(sentbox[max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0):]): 180 if 6 + i < curses.LINES: 181 a = 0 182 if i == sentcur - max(min(len(sentbox) - curses.LINES + 6, sentcur - 5), 0): 183 # Highlight current address 184 a = a | curses.A_REVERSE 185 stdscr.addstr(5 + i, 5, item[0][:34], a) 186 stdscr.addstr(5 + i, 40, item[2][:39], a) 187 stdscr.addstr(5 + i, 80, item[4][:39], a) 188 stdscr.addstr(5 + i, 120, item[5][:39], a) 189 elif menutab == 2 or menutab == 4: # Send or Identities 190 stdscr.addstr(3, 5, "Label", curses.A_BOLD) 191 stdscr.addstr(3, 40, "Address", curses.A_BOLD) 192 stdscr.addstr(3, 80, "Stream", curses.A_BOLD) 193 stdscr.hline(4, 5, '-', 81) 194 for i, item in enumerate(addresses[max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0):]): 195 if 6 + i < curses.LINES: 196 a = 0 197 if i == addrcur - max(min(len(addresses) - curses.LINES + 6, addrcur - 5), 0): 198 # Highlight current address 199 a = a | curses.A_REVERSE 200 if item[1] and item[3] not in [8, 9]: # Embolden enabled, non-special addresses 201 a = a | curses.A_BOLD 202 stdscr.addstr(5 + i, 5, item[0][:34], a) 203 stdscr.addstr(5 + i, 40, item[2][:39], cpair(item[3]) | a) 204 stdscr.addstr(5 + i, 80, str(1)[:39], a) 205 elif menutab == 5: # Subscriptions 206 stdscr.addstr(3, 5, "Label", curses.A_BOLD) 207 stdscr.addstr(3, 80, "Address", curses.A_BOLD) 208 stdscr.addstr(3, 120, "Enabled", curses.A_BOLD) 209 stdscr.hline(4, 5, '-', 121) 210 for i, item in enumerate(subscriptions[max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0):]): 211 if 6 + i < curses.LINES: 212 a = 0 213 if i == subcur - max(min(len(subscriptions) - curses.LINES + 6, subcur - 5), 0): 214 # Highlight current address 215 a = a | curses.A_REVERSE 216 if item[2]: # Embolden enabled subscriptions 217 a = a | curses.A_BOLD 218 stdscr.addstr(5 + i, 5, item[0][:74], a) 219 stdscr.addstr(5 + i, 80, item[1][:39], a) 220 stdscr.addstr(5 + i, 120, str(item[2]), a) 221 elif menutab == 6: # Address book 222 stdscr.addstr(3, 5, "Label", curses.A_BOLD) 223 stdscr.addstr(3, 40, "Address", curses.A_BOLD) 224 stdscr.hline(4, 5, '-', 41) 225 for i, item in enumerate(addrbook[max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0):]): 226 if 6 + i < curses.LINES: 227 a = 0 228 if i == abookcur - max(min(len(addrbook) - curses.LINES + 6, abookcur - 5), 0): 229 # Highlight current address 230 a = a | curses.A_REVERSE 231 stdscr.addstr(5 + i, 5, item[0][:34], a) 232 stdscr.addstr(5 + i, 40, item[1][:39], a) 233 elif menutab == 7: # Blacklist 234 stdscr.addstr(3, 5, "Type: " + bwtype) 235 stdscr.addstr(4, 5, "Label", curses.A_BOLD) 236 stdscr.addstr(4, 80, "Address", curses.A_BOLD) 237 stdscr.addstr(4, 120, "Enabled", curses.A_BOLD) 238 stdscr.hline(5, 5, '-', 121) 239 for i, item in enumerate(blacklist[max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0):]): 240 if 7 + i < curses.LINES: 241 a = 0 242 if i == blackcur - max(min(len(blacklist) - curses.LINES + 6, blackcur - 5), 0): 243 # Highlight current address 244 a = a | curses.A_REVERSE 245 if item[2]: # Embolden enabled subscriptions 246 a = a | curses.A_BOLD 247 stdscr.addstr(6 + i, 5, item[0][:74], a) 248 stdscr.addstr(6 + i, 80, item[1][:39], a) 249 stdscr.addstr(6 + i, 120, str(item[2]), a) 250 elif menutab == 8: # Network status 251 # Connection data 252 connected_hosts = network.stats.connectedHostsList() 253 stdscr.addstr( 254 4, 5, "Total Connections: " + 255 str(len(connected_hosts)).ljust(2) 256 ) 257 stdscr.addstr(6, 6, "Stream #", curses.A_BOLD) 258 stdscr.addstr(6, 18, "Connections", curses.A_BOLD) 259 stdscr.hline(7, 6, '-', 23) 260 streamcount = [] 261 for host, stream in connected_hosts: 262 if stream >= len(streamcount): 263 streamcount.append(1) 264 else: 265 streamcount[stream] += 1 266 for i, item in enumerate(streamcount): 267 if i < 4: 268 if i == 0: 269 stdscr.addstr(8 + i, 6, "?") 270 else: 271 stdscr.addstr(8 + i, 6, str(i)) 272 stdscr.addstr(8 + i, 18, str(item).ljust(2)) 273 274 # Uptime and processing data 275 stdscr.addstr( 276 6, 35, "Since startup on " + l10n.formatTimestamp(startuptime)) 277 stdscr.addstr(7, 40, "Processed " + str( 278 state.numberOfMessagesProcessed).ljust(4) + " person-to-person messages.") 279 stdscr.addstr(8, 40, "Processed " + str( 280 state.numberOfBroadcastsProcessed).ljust(4) + " broadcast messages.") 281 stdscr.addstr(9, 40, "Processed " + str( 282 state.numberOfPubkeysProcessed).ljust(4) + " public keys.") 283 284 # Inventory data 285 stdscr.addstr(11, 35, "Inventory lookups per second: " + str(inventorydata).ljust(3)) 286 287 # Log 288 stdscr.addstr(13, 6, "Log", curses.A_BOLD) 289 n = log.count('\n') 290 if n > 0: 291 lg = log.split('\n') 292 if n > 512: 293 del lg[:(n - 256)] 294 logpad.erase() 295 n = len(lg) 296 for i, item in enumerate(lg): 297 a = 0 298 if item and item[0] == '!': 299 a = curses.color_pair(1) 300 item = item[1:] 301 logpad.addstr(i, 0, item, a) 302 logpad.refresh(n - curses.LINES + 2, 0, 14, 6, curses.LINES - 2, curses.COLS - 7) 303 stdscr.refresh() 304 305 306 def redraw(stdscr): 307 """Redraw menu""" 308 stdscr.erase() 309 stdscr.border() 310 drawmenu(stdscr) 311 stdscr.refresh() 312 313 314 def dialogreset(stdscr): 315 """Resetting dialogue""" 316 stdscr.clear() 317 stdscr.keypad(1) 318 curses.curs_set(0) 319 320 321 # pylint: disable=too-many-branches, too-many-statements 322 def handlech(c, stdscr): 323 """Handle character given on the command-line interface""" 324 # pylint: disable=redefined-outer-name, too-many-nested-blocks, too-many-locals 325 if c != curses.ERR: 326 global inboxcur, addrcur, sentcur, subcur, abookcur, blackcur 327 if c in range(256): 328 if chr(c) in '12345678': 329 global menutab 330 menutab = int(chr(c)) 331 elif chr(c) == 'q': 332 global quit_ 333 quit_ = True 334 elif chr(c) == '\n': 335 curses.curs_set(1) 336 d = Dialog(dialog="dialog") 337 if menutab == 1: 338 set_background_title(d, "Inbox Message Dialog Box") 339 r, t = d.menu( 340 "Do what with \"" + inbox[inboxcur][5] + "\" from \"" + inbox[inboxcur][3] + "\"?", 341 choices=[ 342 ("1", "View message"), 343 ("2", "Mark message as unread"), 344 ("3", "Reply"), 345 ("4", "Add sender to Address Book"), 346 ("5", "Save message as text file"), 347 ("6", "Move to trash")]) 348 if r == d.DIALOG_OK: 349 if t == "1": # View 350 set_background_title( 351 d, 352 "\"" + 353 inbox[inboxcur][5] + 354 "\" from \"" + 355 inbox[inboxcur][3] + 356 "\" to \"" + 357 inbox[inboxcur][1] + 358 "\"") 359 data = "" # pyint: disable=redefined-outer-name 360 ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0]) 361 if ret != []: 362 for row in ret: 363 data, = row 364 data = shared.fixPotentiallyInvalidUTF8Data(data) 365 msg = "" 366 for i, item in enumerate(data.split("\n")): 367 msg += fill(item, replace_whitespace=False) + "\n" 368 scrollbox(d, str(ascii(msg)), 30, 80) 369 sqlExecute("UPDATE inbox SET read=1 WHERE msgid=?", inbox[inboxcur][0]) 370 inbox[inboxcur][7] = 1 371 else: 372 scrollbox(d, str("Could not fetch message.")) 373 elif t == "2": # Mark unread 374 sqlExecute("UPDATE inbox SET read=0 WHERE msgid=?", inbox[inboxcur][0]) 375 inbox[inboxcur][7] = 0 376 elif t == "3": # Reply 377 curses.curs_set(1) 378 m = inbox[inboxcur] 379 fromaddr = m[4] 380 ischan = False 381 for i, item in enumerate(addresses): 382 if fromaddr == item[2] and item[3] != 0: 383 ischan = True 384 break 385 if not addresses[i][1]: # pylint: disable=undefined-loop-variable 386 scrollbox(d, str( 387 "Sending address disabled, please either enable it" 388 "or choose a different address.")) 389 return 390 toaddr = m[2] 391 if ischan: 392 toaddr = fromaddr 393 394 subject = m[5] 395 if not m[5][:4] == "Re: ": 396 subject = "Re: " + m[5] 397 body = "" 398 ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", m[0]) 399 if ret != []: 400 body = "\n\n------------------------------------------------------\n" 401 for row in ret: 402 body, = row 403 404 sendMessage(fromaddr, toaddr, ischan, subject, body, True) 405 dialogreset(stdscr) 406 elif t == "4": # Add to Address Book 407 addr = inbox[inboxcur][4] 408 if addr not in [item[1] for i, item in enumerate(addrbook)]: 409 r, t = d.inputbox("Label for address \"" + addr + "\"") 410 if r == d.DIALOG_OK: 411 label = t 412 sqlExecute("INSERT INTO addressbook VALUES (?,?)", label, addr) 413 # Prepend entry 414 addrbook.reverse() 415 addrbook.append([label, addr]) 416 addrbook.reverse() 417 else: 418 scrollbox(d, str("The selected address is already in the Address Book.")) 419 elif t == "5": # Save message 420 set_background_title(d, "Save \"" + inbox[inboxcur][5] + "\" as text file") 421 r, t = d.inputbox("Filename", init=inbox[inboxcur][5] + ".txt") 422 if r == d.DIALOG_OK: 423 msg = "" 424 ret = sqlQuery("SELECT message FROM inbox WHERE msgid=?", inbox[inboxcur][0]) 425 if ret != []: 426 for row in ret: 427 msg, = row 428 fh = open(t, "a") # Open in append mode just in case 429 fh.write(msg) 430 fh.close() 431 else: 432 scrollbox(d, str("Could not fetch message.")) 433 elif t == "6": # Move to trash 434 sqlExecute("UPDATE inbox SET folder='trash' WHERE msgid=?", inbox[inboxcur][0]) 435 del inbox[inboxcur] 436 scrollbox(d, str( 437 "Message moved to trash. There is no interface to view your trash," 438 " \nbut the message is still on disk if you are desperate to recover it.")) 439 elif menutab == 2: 440 a = "" 441 if addresses[addrcur][3] != 0: # if current address is a chan 442 a = addresses[addrcur][2] 443 sendMessage(addresses[addrcur][2], a) 444 elif menutab == 3: 445 set_background_title(d, "Sent Messages Dialog Box") 446 r, t = d.menu( 447 "Do what with \"" + sentbox[sentcur][4] + "\" to \"" + sentbox[sentcur][0] + "\"?", 448 choices=[ 449 ("1", "View message"), 450 ("2", "Move to trash")]) 451 if r == d.DIALOG_OK: 452 if t == "1": # View 453 set_background_title( 454 d, 455 "\"" + 456 sentbox[sentcur][4] + 457 "\" from \"" + 458 sentbox[sentcur][3] + 459 "\" to \"" + 460 sentbox[sentcur][1] + 461 "\"") 462 data = "" 463 ret = sqlQuery( 464 "SELECT message FROM sent WHERE subject=? AND ackdata=?", 465 sentbox[sentcur][4], 466 sentbox[sentcur][6]) 467 if ret != []: 468 for row in ret: 469 data, = row 470 data = shared.fixPotentiallyInvalidUTF8Data(data) 471 msg = "" 472 for i, item in enumerate(data.split("\n")): 473 msg += fill(item, replace_whitespace=False) + "\n" 474 scrollbox(d, str(ascii(msg)), 30, 80) 475 else: 476 scrollbox(d, str("Could not fetch message.")) 477 elif t == "2": # Move to trash 478 sqlExecute( 479 "UPDATE sent SET folder='trash' WHERE subject=? AND ackdata=?", 480 sentbox[sentcur][4], 481 sentbox[sentcur][6]) 482 del sentbox[sentcur] 483 scrollbox(d, str( 484 "Message moved to trash. There is no interface to view your trash" 485 " \nbut the message is still on disk if you are desperate to recover it.")) 486 elif menutab == 4: 487 set_background_title(d, "Your Identities Dialog Box") 488 if len(addresses) <= addrcur: 489 r, t = d.menu( 490 "Do what with addresses?", 491 choices=[ 492 ("1", "Create new address")]) 493 else: 494 r, t = d.menu( 495 "Do what with \"" + addresses[addrcur][0] + "\" : \"" + addresses[addrcur][2] + "\"?", 496 choices=[ 497 ("1", "Create new address"), 498 ("2", "Send a message from this address"), 499 ("3", "Rename"), 500 ("4", "Enable"), 501 ("5", "Disable"), 502 ("6", "Delete"), 503 ("7", "Special address behavior")]) 504 if r == d.DIALOG_OK: 505 if t == "1": # Create new address 506 set_background_title(d, "Create new address") 507 scrollbox( 508 d, str( 509 "Here you may generate as many addresses as you like.\n" 510 "Indeed, creating and abandoning addresses is encouraged.\n" 511 "Deterministic addresses have several pros and cons:\n" 512 "\nPros:\n" 513 " * You can recreate your addresses on any computer from memory\n" 514 " * You need not worry about backing up your keys.dat file as long as you" 515 " \n can remember your passphrase\n" 516 "Cons:\n" 517 " * You must remember (or write down) your passphrase in order to recreate" 518 " \n your keys if they are lost\n" 519 " * You must also remember the address version and stream numbers\n" 520 " * If you choose a weak passphrase someone may be able to brute-force it" 521 " \n and then send and receive messages as you")) 522 r, t = d.menu( 523 "Choose an address generation technique", 524 choices=[ 525 ("1", "Use a random number generator"), 526 ("2", "Use a passphrase")]) 527 if r == d.DIALOG_OK: 528 if t == "1": 529 set_background_title(d, "Randomly generate address") 530 r, t = d.inputbox("Label (not shown to anyone except you)") 531 label = "" 532 if r == d.DIALOG_OK and t: 533 label = t 534 r, t = d.menu( 535 "Choose a stream", 536 choices=[("1", "Use the most available stream"), 537 ("", "(Best if this is the first of many addresses you will create)"), 538 ("2", "Use the same stream as an existing address"), 539 ("", "(Saves you some bandwidth and processing power)")]) 540 if r == d.DIALOG_OK: 541 if t == "1": 542 stream = 1 543 elif t == "2": 544 addrs = [] 545 for i, item in enumerate(addresses): 546 addrs.append([str(i), item[2]]) 547 r, t = d.menu("Choose an existing address's stream", choices=addrs) 548 if r == d.DIALOG_OK: 549 stream = decodeAddress(addrs[int(t)][1])[2] 550 shorten = False 551 r, t = d.checklist( 552 "Miscellaneous options", 553 choices=[( 554 "1", 555 "Spend time shortening the address", 556 1 if shorten else 0)]) 557 if r == d.DIALOG_OK and "1" in t: 558 shorten = True 559 queues.addressGeneratorQueue.put(( 560 "createRandomAddress", 561 4, 562 stream, 563 label, 564 1, 565 "", 566 shorten)) 567 elif t == "2": 568 set_background_title(d, "Make deterministic addresses") 569 r, t = d.passwordform( 570 "Enter passphrase", 571 [ 572 ("Passphrase", 1, 1, "", 2, 1, 64, 128), 573 ("Confirm passphrase", 3, 1, "", 4, 1, 64, 128)], 574 form_height=4, insecure=True) 575 if r == d.DIALOG_OK: 576 if t[0] == t[1]: 577 passphrase = t[0] 578 r, t = d.rangebox( 579 "Number of addresses to generate", 580 width=48, 581 min=1, 582 max=99, 583 init=8) 584 if r == d.DIALOG_OK: 585 number = t 586 stream = 1 587 shorten = False 588 r, t = d.checklist( 589 "Miscellaneous options", 590 choices=[( 591 "1", 592 "Spend time shortening the address", 593 1 if shorten else 0)]) 594 if r == d.DIALOG_OK and "1" in t: 595 shorten = True 596 scrollbox( 597 d, str( 598 "In addition to your passphrase, be sure to remember the" 599 " following numbers:\n" 600 "\n * Address version number: " + str(4) + "\n" 601 " * Stream number: " + str(stream))) 602 queues.addressGeneratorQueue.put( 603 ('createDeterministicAddresses', 4, stream, 604 "unused deterministic address", number, 605 str(passphrase), shorten)) 606 else: 607 scrollbox(d, str("Passphrases do not match")) 608 elif t == "2": # Send a message 609 a = "" 610 if addresses[addrcur][3] != 0: # if current address is a chan 611 a = addresses[addrcur][2] 612 sendMessage(addresses[addrcur][2], a) 613 elif t == "3": # Rename address label 614 a = addresses[addrcur][2] 615 label = addresses[addrcur][0] 616 r, t = d.inputbox("New address label", init=label) 617 if r == d.DIALOG_OK: 618 label = t 619 config.set(a, "label", label) 620 # Write config 621 config.save() 622 addresses[addrcur][0] = label 623 elif t == "4": # Enable address 624 a = addresses[addrcur][2] 625 config.set(a, "enabled", "true") # Set config 626 # Write config 627 config.save() 628 # Change color 629 if config.safeGetBoolean(a, 'chan'): 630 addresses[addrcur][3] = 9 # orange 631 elif config.safeGetBoolean(a, 'mailinglist'): 632 addresses[addrcur][3] = 5 # magenta 633 else: 634 addresses[addrcur][3] = 0 # black 635 addresses[addrcur][1] = True 636 shared.reloadMyAddressHashes() # Reload address hashes 637 elif t == "5": # Disable address 638 a = addresses[addrcur][2] 639 config.set(a, "enabled", "false") # Set config 640 addresses[addrcur][3] = 8 # Set color to gray 641 # Write config 642 config.save() 643 addresses[addrcur][1] = False 644 shared.reloadMyAddressHashes() # Reload address hashes 645 elif t == "6": # Delete address 646 r, t = d.inputbox("Type in \"I want to delete this address\"", width=50) 647 if r == d.DIALOG_OK and t == "I want to delete this address": 648 config.remove_section(addresses[addrcur][2]) 649 config.save() 650 del addresses[addrcur] 651 elif t == "7": # Special address behavior 652 a = addresses[addrcur][2] 653 set_background_title(d, "Special address behavior") 654 if config.safeGetBoolean(a, "chan"): 655 scrollbox(d, str( 656 "This is a chan address. You cannot use it as a pseudo-mailing list.")) 657 else: 658 m = config.safeGetBoolean(a, "mailinglist") 659 r, t = d.radiolist( 660 "Select address behavior", 661 choices=[ 662 ("1", "Behave as a normal address", not m), 663 ("2", "Behave as a pseudo-mailing-list address", m)]) 664 if r == d.DIALOG_OK: 665 if t == "1" and m: 666 config.set(a, "mailinglist", "false") 667 if addresses[addrcur][1]: 668 addresses[addrcur][3] = 0 # Set color to black 669 else: 670 addresses[addrcur][3] = 8 # Set color to gray 671 elif t == "2" and m is False: 672 try: 673 mn = config.get(a, "mailinglistname") 674 except configparser.NoOptionError: 675 mn = "" 676 r, t = d.inputbox("Mailing list name", init=mn) 677 if r == d.DIALOG_OK: 678 mn = t 679 config.set(a, "mailinglist", "true") 680 config.set(a, "mailinglistname", mn) 681 addresses[addrcur][3] = 6 # Set color to magenta 682 # Write config 683 config.save() 684 elif menutab == 5: 685 set_background_title(d, "Subscriptions Dialog Box") 686 if len(subscriptions) <= subcur: 687 r, t = d.menu( 688 "Do what with subscription to \"" + subscriptions[subcur][0] + "\"?", 689 choices=[ 690 ("1", "Add new subscription")]) 691 else: 692 r, t = d.menu( 693 "Do what with subscription to \"" + subscriptions[subcur][0] + "\"?", 694 choices=[ 695 ("1", "Add new subscription"), 696 ("2", "Delete this subscription"), 697 ("3", "Enable"), 698 ("4", "Disable")]) 699 if r == d.DIALOG_OK: 700 if t == "1": 701 r, t = d.inputbox("New subscription address") 702 if r == d.DIALOG_OK: 703 addr = addBMIfNotPresent(t) 704 if not shared.isAddressInMySubscriptionsList(addr): 705 r, t = d.inputbox("New subscription label") 706 if r == d.DIALOG_OK: 707 label = t 708 # Prepend entry 709 subscriptions.reverse() 710 subscriptions.append([label, addr, True]) 711 subscriptions.reverse() 712 713 sqlExecute("INSERT INTO subscriptions VALUES (?,?,?)", label, addr, True) 714 shared.reloadBroadcastSendersForWhichImWatching() 715 elif t == "2": 716 r, t = d.inputbox("Type in \"I want to delete this subscription\"") 717 if r == d.DIALOG_OK and t == "I want to delete this subscription": 718 sqlExecute( 719 "DELETE FROM subscriptions WHERE label=? AND address=?", 720 subscriptions[subcur][0], 721 subscriptions[subcur][1]) 722 shared.reloadBroadcastSendersForWhichImWatching() 723 del subscriptions[subcur] 724 elif t == "3": 725 sqlExecute( 726 "UPDATE subscriptions SET enabled=1 WHERE label=? AND address=?", 727 subscriptions[subcur][0], 728 subscriptions[subcur][1]) 729 shared.reloadBroadcastSendersForWhichImWatching() 730 subscriptions[subcur][2] = True 731 elif t == "4": 732 sqlExecute( 733 "UPDATE subscriptions SET enabled=0 WHERE label=? AND address=?", 734 subscriptions[subcur][0], 735 subscriptions[subcur][1]) 736 shared.reloadBroadcastSendersForWhichImWatching() 737 subscriptions[subcur][2] = False 738 elif menutab == 6: 739 set_background_title(d, "Address Book Dialog Box") 740 if len(addrbook) <= abookcur: 741 r, t = d.menu( 742 "Do what with addressbook?", 743 choices=[("3", "Add new address to Address Book")]) 744 else: 745 r, t = d.menu( 746 "Do what with \"" + addrbook[abookcur][0] + "\" : \"" + addrbook[abookcur][1] + "\"", 747 choices=[ 748 ("1", "Send a message to this address"), 749 ("2", "Subscribe to this address"), 750 ("3", "Add new address to Address Book"), 751 ("4", "Delete this address")]) 752 if r == d.DIALOG_OK: 753 if t == "1": 754 sendMessage(recv=addrbook[abookcur][1]) 755 elif t == "2": 756 r, t = d.inputbox("New subscription label") 757 if r == d.DIALOG_OK: 758 label = t 759 # Prepend entry 760 subscriptions.reverse() 761 subscriptions.append([label, addr, True]) 762 subscriptions.reverse() 763 764 sqlExecute("INSERT INTO subscriptions VALUES (?,?,?)", label, addr, True) 765 shared.reloadBroadcastSendersForWhichImWatching() 766 elif t == "3": 767 r, t = d.inputbox("Input new address") 768 if r == d.DIALOG_OK: 769 addr = t 770 if addr not in [item[1] for i, item in enumerate(addrbook)]: 771 r, t = d.inputbox("Label for address \"" + addr + "\"") 772 if r == d.DIALOG_OK: 773 sqlExecute("INSERT INTO addressbook VALUES (?,?)", t, addr) 774 # Prepend entry 775 addrbook.reverse() 776 addrbook.append([t, addr]) 777 addrbook.reverse() 778 else: 779 scrollbox(d, str("The selected address is already in the Address Book.")) 780 elif t == "4": 781 r, t = d.inputbox("Type in \"I want to delete this Address Book entry\"") 782 if r == d.DIALOG_OK and t == "I want to delete this Address Book entry": 783 sqlExecute( 784 "DELETE FROM addressbook WHERE label=? AND address=?", 785 addrbook[abookcur][0], 786 addrbook[abookcur][1]) 787 del addrbook[abookcur] 788 elif menutab == 7: 789 set_background_title(d, "Blacklist Dialog Box") 790 r, t = d.menu( 791 "Do what with \"" + blacklist[blackcur][0] + "\" : \"" + blacklist[blackcur][1] + "\"?", 792 choices=[ 793 ("1", "Delete"), 794 ("2", "Enable"), 795 ("3", "Disable")]) 796 if r == d.DIALOG_OK: 797 if t == "1": 798 r, t = d.inputbox("Type in \"I want to delete this Blacklist entry\"") 799 if r == d.DIALOG_OK and t == "I want to delete this Blacklist entry": 800 sqlExecute( 801 "DELETE FROM blacklist WHERE label=? AND address=?", 802 blacklist[blackcur][0], 803 blacklist[blackcur][1]) 804 del blacklist[blackcur] 805 elif t == "2": 806 sqlExecute( 807 "UPDATE blacklist SET enabled=1 WHERE label=? AND address=?", 808 blacklist[blackcur][0], 809 blacklist[blackcur][1]) 810 blacklist[blackcur][2] = True 811 elif t == "3": 812 sqlExecute( 813 "UPDATE blacklist SET enabled=0 WHERE label=? AND address=?", 814 blacklist[blackcur][0], 815 blacklist[blackcur][1]) 816 blacklist[blackcur][2] = False 817 dialogreset(stdscr) 818 else: 819 if c == curses.KEY_UP: 820 if menutab == 1 and inboxcur > 0: 821 inboxcur -= 1 822 if (menutab == 2 or menutab == 4) and addrcur > 0: 823 addrcur -= 1 824 if menutab == 3 and sentcur > 0: 825 sentcur -= 1 826 if menutab == 5 and subcur > 0: 827 subcur -= 1 828 if menutab == 6 and abookcur > 0: 829 abookcur -= 1 830 if menutab == 7 and blackcur > 0: 831 blackcur -= 1 832 elif c == curses.KEY_DOWN: 833 if menutab == 1 and inboxcur < len(inbox) - 1: 834 inboxcur += 1 835 if (menutab == 2 or menutab == 4) and addrcur < len(addresses) - 1: 836 addrcur += 1 837 if menutab == 3 and sentcur < len(sentbox) - 1: 838 sentcur += 1 839 if menutab == 5 and subcur < len(subscriptions) - 1: 840 subcur += 1 841 if menutab == 6 and abookcur < len(addrbook) - 1: 842 abookcur += 1 843 if menutab == 7 and blackcur < len(blacklist) - 1: 844 blackcur += 1 845 elif c == curses.KEY_HOME: 846 if menutab == 1: 847 inboxcur = 0 848 if menutab == 2 or menutab == 4: 849 addrcur = 0 850 if menutab == 3: 851 sentcur = 0 852 if menutab == 5: 853 subcur = 0 854 if menutab == 6: 855 abookcur = 0 856 if menutab == 7: 857 blackcur = 0 858 elif c == curses.KEY_END: 859 if menutab == 1: 860 inboxcur = len(inbox) - 1 861 if menutab == 2 or menutab == 4: 862 addrcur = len(addresses) - 1 863 if menutab == 3: 864 sentcur = len(sentbox) - 1 865 if menutab == 5: 866 subcur = len(subscriptions) - 1 867 if menutab == 6: 868 abookcur = len(addrbook) - 1 869 if menutab == 7: 870 blackcur = len(blackcur) - 1 871 redraw(stdscr) 872 873 874 # pylint: disable=too-many-locals, too-many-arguments 875 def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=False): 876 """Method for message sending""" 877 if sender == "": 878 return 879 d = Dialog(dialog="dialog") 880 set_background_title(d, "Send a message") 881 if recv == "": 882 r, t = d.inputbox( 883 "Recipient address (Cancel to load from the Address Book or leave blank to broadcast)", 884 10, 885 60) 886 if r != d.DIALOG_OK: 887 global menutab 888 menutab = 6 889 return 890 recv = t 891 if broadcast is None and sender != recv: 892 r, t = d.radiolist( 893 "How to send the message?", 894 choices=[ 895 ("1", "Send to one or more specific people", 1), 896 ("2", "Broadcast to everyone who is subscribed to your address", 0)]) 897 if r != d.DIALOG_OK: 898 return 899 broadcast = False 900 if t == "2": # Broadcast 901 broadcast = True 902 if subject == "" or reply: 903 r, t = d.inputbox("Message subject", width=60, init=subject) 904 if r != d.DIALOG_OK: 905 return 906 subject = t 907 if body == "" or reply: 908 r, t = d.inputbox("Message body", 10, 80, init=body) 909 if r != d.DIALOG_OK: 910 return 911 body = t 912 body = body.replace("\\n", "\n").replace("\\t", "\t") 913 914 if not broadcast: 915 recvlist = [] 916 for _, item in enumerate(recv.replace(",", ";").split(";")): 917 recvlist.append(item.strip()) 918 list(set(recvlist)) # Remove exact duplicates 919 for addr in recvlist: 920 if addr != "": 921 status, version, stream = decodeAddress(addr)[:3] 922 if status != "success": 923 set_background_title(d, "Recipient address error") 924 err = "Could not decode" + addr + " : " + status + "\n\n" 925 if status == "missingbm": 926 err += "Bitmessage addresses should start with \"BM-\"." 927 elif status == "checksumfailed": 928 err += "The address was not typed or copied correctly." 929 elif status == "invalidcharacters": 930 err += "The address contains invalid characters." 931 elif status == "versiontoohigh": 932 err += ("The address version is too high. Either you need to upgrade your Bitmessage software" 933 " or your acquaintance is doing something clever.") 934 elif status == "ripetooshort": 935 err += ("Some data encoded in the address is too short. There might be something wrong with" 936 " the software of your acquaintance.") 937 elif status == "ripetoolong": 938 err += ("Some data encoded in the address is too long. There might be something wrong with" 939 " the software of your acquaintance.") 940 elif status == "varintmalformed": 941 err += ("Some data encoded in the address is malformed. There might be something wrong with" 942 " the software of your acquaintance.") 943 else: 944 err += "It is unknown what is wrong with the address." 945 scrollbox(d, str(err)) 946 else: 947 addr = addBMIfNotPresent(addr) 948 if version > 4 or version <= 1: 949 set_background_title(d, "Recipient address error") 950 scrollbox(d, str( 951 "Could not understand version number " + 952 version + 953 "of address" + 954 addr + 955 ".")) 956 continue 957 if stream > 1 or stream == 0: 958 set_background_title(d, "Recipient address error") 959 scrollbox(d, str( 960 "Bitmessage currently only supports stream numbers of 1," 961 "unlike as requested for address " + addr + ".")) 962 continue 963 if not network.stats.connectedHostsList(): 964 set_background_title(d, "Not connected warning") 965 scrollbox(d, str("Because you are not currently connected to the network, ")) 966 helper_sent.insert( 967 toAddress=addr, fromAddress=sender, subject=subject, message=body) 968 queues.workerQueue.put(("sendmessage", addr)) 969 else: # Broadcast 970 if recv == "": 971 set_background_title(d, "Empty sender error") 972 scrollbox(d, str("You must specify an address to send the message from.")) 973 else: 974 # dummy ackdata, no need for stealth 975 helper_sent.insert( 976 fromAddress=sender, subject=subject, 977 message=body, status='broadcastqueued') 978 queues.workerQueue.put(('sendbroadcast', '')) 979 980 981 # pylint: disable=redefined-outer-name, too-many-locals 982 def loadInbox(): 983 """Load the list of messages""" 984 sys.stdout = sys.__stdout__ 985 print("Loading inbox messages...") 986 sys.stdout = printlog 987 988 where = "toaddress || fromaddress || subject || message" 989 what = "%%" 990 ret = sqlQuery("""SELECT msgid, toaddress, fromaddress, subject, received, read 991 FROM inbox WHERE folder='inbox' AND %s LIKE ? 992 ORDER BY received 993 """ % (where,), what) 994 for row in ret: 995 msgid, toaddr, fromaddr, subject, received, read = row 996 subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) 997 998 # Set label for to address 999 try: 1000 if toaddr == BROADCAST_STR: 1001 tolabel = BROADCAST_STR 1002 else: 1003 tolabel = config.get(toaddr, "label") 1004 except: # noqa:E722 1005 tolabel = "" 1006 if tolabel == "": 1007 tolabel = toaddr 1008 tolabel = shared.fixPotentiallyInvalidUTF8Data(tolabel) 1009 1010 # Set label for from address 1011 fromlabel = "" 1012 if config.has_section(fromaddr): 1013 fromlabel = config.get(fromaddr, "label") 1014 if fromlabel == "": # Check Address Book 1015 qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", fromaddr) 1016 if qr != []: 1017 for r in qr: 1018 fromlabel, = r 1019 if fromlabel == "": # Check Subscriptions 1020 qr = sqlQuery("SELECT label FROM subscriptions WHERE address=?", fromaddr) 1021 if qr != []: 1022 for r in qr: 1023 fromlabel, = r 1024 if fromlabel == "": 1025 fromlabel = fromaddr 1026 fromlabel = shared.fixPotentiallyInvalidUTF8Data(fromlabel) 1027 1028 # Load into array 1029 inbox.append([ 1030 msgid, tolabel, toaddr, fromlabel, fromaddr, subject, 1031 l10n.formatTimestamp(received), read]) 1032 inbox.reverse() 1033 1034 1035 def loadSent(): 1036 """Load the messages that sent""" 1037 sys.stdout = sys.__stdout__ 1038 print("Loading sent messages...") 1039 sys.stdout = printlog 1040 1041 where = "toaddress || fromaddress || subject || message" 1042 what = "%%" 1043 ret = sqlQuery("""SELECT toaddress, fromaddress, subject, status, ackdata, lastactiontime 1044 FROM sent WHERE folder='sent' AND %s LIKE ? 1045 ORDER BY lastactiontime 1046 """ % (where,), what) 1047 for row in ret: 1048 toaddr, fromaddr, subject, status, ackdata, lastactiontime = row 1049 subject = ascii(shared.fixPotentiallyInvalidUTF8Data(subject)) 1050 1051 # Set label for to address 1052 tolabel = "" 1053 qr = sqlQuery("SELECT label FROM addressbook WHERE address=?", toaddr) 1054 if qr != []: 1055 for r in qr: 1056 tolabel, = r 1057 if tolabel == "": 1058 qr = sqlQuery("SELECT label FROM subscriptions WHERE address=?", toaddr) 1059 if qr != []: 1060 for r in qr: 1061 tolabel, = r 1062 if tolabel == "": 1063 if config.has_section(toaddr): 1064 tolabel = config.get(toaddr, "label") 1065 if tolabel == "": 1066 tolabel = toaddr 1067 1068 # Set label for from address 1069 fromlabel = "" 1070 if config.has_section(fromaddr): 1071 fromlabel = config.get(fromaddr, "label") 1072 if fromlabel == "": 1073 fromlabel = fromaddr 1074 1075 # Set status string 1076 if status == "awaitingpubkey": 1077 statstr = "Waiting for their public key. Will request it again soon" 1078 elif status == "doingpowforpubkey": 1079 statstr = "Encryption key request queued" 1080 elif status == "msgqueued": 1081 statstr = "Message queued" 1082 elif status == "msgsent": 1083 t = l10n.formatTimestamp(lastactiontime) 1084 statstr = "Message sent at " + t + ".Waiting for acknowledgement." 1085 elif status == "msgsentnoackexpected": 1086 t = l10n.formatTimestamp(lastactiontime) 1087 statstr = "Message sent at " + t + "." 1088 elif status == "doingmsgpow": 1089 statstr = "The proof of work required to send the message has been queued." 1090 elif status == "ackreceived": 1091 t = l10n.formatTimestamp(lastactiontime) 1092 statstr = "Acknowledgment of the message received at " + t + "." 1093 elif status == "broadcastqueued": 1094 statstr = "Broadcast queued." 1095 elif status == "broadcastsent": 1096 t = l10n.formatTimestamp(lastactiontime) 1097 statstr = "Broadcast sent at " + t + "." 1098 elif status == "forcepow": 1099 statstr = "Forced difficulty override. Message will start sending soon." 1100 elif status == "badkey": 1101 statstr = "Warning: Could not encrypt message because the recipient's encryption key is no good." 1102 elif status == "toodifficult": 1103 statstr = "Error: The work demanded by the recipient is more difficult than you are willing to do." 1104 else: 1105 t = l10n.formatTimestamp(lastactiontime) 1106 statstr = "Unknown status " + status + " at " + t + "." 1107 1108 # Load into array 1109 sentbox.append([ 1110 tolabel, 1111 toaddr, 1112 fromlabel, 1113 fromaddr, 1114 subject, 1115 statstr, 1116 ackdata, 1117 l10n.formatTimestamp(lastactiontime)]) 1118 sentbox.reverse() 1119 1120 1121 def loadAddrBook(): 1122 """Load address book""" 1123 sys.stdout = sys.__stdout__ 1124 print("Loading address book...") 1125 sys.stdout = printlog 1126 1127 ret = sqlQuery("SELECT label, address FROM addressbook") 1128 for row in ret: 1129 label, addr = row 1130 label = shared.fixPotentiallyInvalidUTF8Data(label) 1131 addrbook.append([label, addr]) 1132 addrbook.reverse() 1133 1134 1135 def loadSubscriptions(): 1136 """Load subscription functionality""" 1137 ret = sqlQuery("SELECT label, address, enabled FROM subscriptions") 1138 for row in ret: 1139 label, address, enabled = row 1140 subscriptions.append([label, address, enabled]) 1141 subscriptions.reverse() 1142 1143 1144 def loadBlackWhiteList(): 1145 """load black/white list""" 1146 global bwtype 1147 bwtype = config.get("bitmessagesettings", "blackwhitelist") 1148 if bwtype == "black": 1149 ret = sqlQuery("SELECT label, address, enabled FROM blacklist") 1150 else: 1151 ret = sqlQuery("SELECT label, address, enabled FROM whitelist") 1152 for row in ret: 1153 label, address, enabled = row 1154 blacklist.append([label, address, enabled]) 1155 blacklist.reverse() 1156 1157 1158 def runwrapper(): 1159 """Main method""" 1160 sys.stdout = printlog 1161 # sys.stderr = errlog 1162 1163 loadInbox() 1164 loadSent() 1165 loadAddrBook() 1166 loadSubscriptions() 1167 loadBlackWhiteList() 1168 1169 stdscr = curses.initscr() 1170 1171 global logpad 1172 logpad = curses.newpad(1024, curses.COLS) 1173 1174 stdscr.nodelay(0) 1175 curses.curs_set(0) 1176 stdscr.timeout(1000) 1177 1178 curses.wrapper(run) 1179 doShutdown() 1180 1181 1182 def run(stdscr): 1183 """Main loop""" 1184 # Schedule inventory lookup data 1185 resetlookups() 1186 1187 # Init color pairs 1188 if curses.has_colors(): 1189 curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK) # red 1190 curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK) # green 1191 curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) # yellow 1192 curses.init_pair(4, curses.COLOR_BLUE, curses.COLOR_BLACK) # blue 1193 curses.init_pair(5, curses.COLOR_MAGENTA, curses.COLOR_BLACK) # magenta 1194 curses.init_pair(6, curses.COLOR_CYAN, curses.COLOR_BLACK) # cyan 1195 curses.init_pair(7, curses.COLOR_WHITE, curses.COLOR_BLACK) # white 1196 if curses.can_change_color(): 1197 curses.init_color(8, 500, 500, 500) # gray 1198 curses.init_pair(8, 8, 0) 1199 curses.init_color(9, 844, 465, 0) # orange 1200 curses.init_pair(9, 9, 0) 1201 else: 1202 curses.init_pair(8, curses.COLOR_WHITE, curses.COLOR_BLACK) # grayish 1203 curses.init_pair(9, curses.COLOR_YELLOW, curses.COLOR_BLACK) # orangish 1204 1205 # Init list of address in 'Your Identities' tab 1206 configSections = config.addresses() 1207 for addressInKeysFile in configSections: 1208 isEnabled = config.getboolean(addressInKeysFile, "enabled") 1209 addresses.append([config.get(addressInKeysFile, "label"), isEnabled, addressInKeysFile]) 1210 # Set address color 1211 if not isEnabled: 1212 addresses[len(addresses) - 1].append(8) # gray 1213 elif config.safeGetBoolean(addressInKeysFile, 'chan'): 1214 addresses[len(addresses) - 1].append(9) # orange 1215 elif config.safeGetBoolean(addressInKeysFile, 'mailinglist'): 1216 addresses[len(addresses) - 1].append(5) # magenta 1217 else: 1218 addresses[len(addresses) - 1].append(0) # black 1219 addresses.reverse() 1220 1221 stdscr.clear() 1222 redraw(stdscr) 1223 while quit_ is False: 1224 drawtab(stdscr) 1225 handlech(stdscr.getch(), stdscr) 1226 1227 1228 def doShutdown(): 1229 """Shutting the app down""" 1230 sys.stdout = sys.__stdout__ 1231 print("Shutting down...") 1232 sys.stdout = printlog 1233 shutdown.doCleanShutdown() 1234 sys.stdout = sys.__stdout__ 1235 sys.stderr = sys.__stderr__ 1236 os._exit(0) # pylint: disable=protected-access