/ src / api.py
api.py
   1  # Copyright (c) 2012-2016 Jonathan Warren
   2  # Copyright (c) 2012-2023 The Bitmessage developers
   3  
   4  """
   5  This is not what you run to start the Bitmessage API.
   6  Instead, `enable the API <https://bitmessage.org/wiki/API>`_
   7  and optionally `enable daemon mode <https://bitmessage.org/wiki/Daemon>`_
   8  then run the PyBitmessage.
   9  
  10  The PyBitmessage API is provided either as
  11  `XML-RPC <http://xmlrpc.scripting.com/spec.html>`_ or
  12  `JSON-RPC <https://www.jsonrpc.org/specification>`_ like in bitcoin.
  13  It's selected according to 'apivariant' setting in config file.
  14  
  15  Special value ``apivariant=legacy`` is to mimic the old pre 0.6.3
  16  behaviour when any results are returned as strings of json.
  17  
  18  .. list-table:: All config settings related to API:
  19    :header-rows: 0
  20  
  21    * - apienabled = true
  22      - if 'false' the `singleAPI` wont start
  23    * - apiinterface = 127.0.0.1
  24      - this is the recommended default
  25    * - apiport = 8442
  26      - the API listens apiinterface:apiport if apiport is not used,
  27        random in range (32767, 65535) otherwice
  28    * - apivariant = xml
  29      - current default for backward compatibility, 'json' is recommended
  30    * - apiusername = username
  31      - set the username
  32    * - apipassword = password
  33      - and the password
  34    * - apinotifypath =
  35      - not really the API setting, this sets a path for the executable to be ran
  36        when certain internal event happens
  37  
  38  To use the API concider such simple example:
  39  
  40  .. code-block:: python
  41  
  42      from jsonrpclib import jsonrpc
  43  
  44      from pybitmessage import helper_startup
  45      from pybitmessage.bmconfigparser import config
  46  
  47      helper_startup.loadConfig()  # find and load local config file
  48      api_uri = "http://%s:%s@127.0.0.1:%s/" % (
  49          config.safeGet('bitmessagesettings', 'apiusername'),
  50          config.safeGet('bitmessagesettings', 'apipassword'),
  51          config.safeGet('bitmessagesettings', 'apiport')
  52      )
  53      api = jsonrpc.ServerProxy(api_uri)
  54      print(api.clientStatus())
  55  
  56  
  57  For further examples please reference `.tests.test_api`.
  58  """
  59  
  60  import base64
  61  import errno
  62  import hashlib
  63  import json
  64  import random
  65  import socket
  66  import subprocess  # nosec B404
  67  import time
  68  from binascii import hexlify, unhexlify
  69  from struct import pack, unpack
  70  
  71  import six
  72  from six.moves import configparser, http_client, xmlrpc_server
  73  from six.moves.reprlib import repr
  74  
  75  from . import helper_inbox
  76  from . import helper_sent
  77  from . import proofofwork
  78  from . import protocol
  79  from . import queues
  80  from . import shared
  81  from . import shutdown
  82  from . import state
  83  from .addresses import (addBMIfNotPresent, decodeAddress, decodeVarint,
  84                         varintDecodeError)
  85  from .bmconfigparser import config
  86  from .debug import logger
  87  from .defaults import (networkDefaultPayloadLengthExtraBytes,
  88                        networkDefaultProofOfWorkNonceTrialsPerByte)
  89  from .helper_sql import (SqlBulkExecute, sql_ready, sqlExecute, sqlQuery,
  90                          sqlStoredProcedure)
  91  from .highlevelcrypto import calculateInventoryHash
  92  
  93  try:
  94      from .network import connectionpool
  95  except ImportError:
  96      connectionpool = None
  97  
  98  from .network import StoppableThread, invQueue, stats
  99  from .version import softwareVersion
 100  
 101  try:  # TODO: write tests for XML vulnerabilities
 102      from defusedxml.xmlrpc import monkey_patch
 103  except ImportError:
 104      logger.warning(
 105          'defusedxml not available, only use API on a secure, closed network.')
 106  else:
 107      monkey_patch()
 108  
 109  
 110  str_chan = '[chan]'
 111  str_broadcast_subscribers = '[Broadcast subscribers]'
 112  
 113  
 114  class ErrorCodes(type):
 115      """Metaclass for :class:`APIError` documenting error codes."""
 116      _CODES = {
 117          0: 'Invalid command parameters number',
 118          1: 'The specified passphrase is blank.',
 119          2: 'The address version number currently must be 3, 4, or 0'
 120          ' (which means auto-select).',
 121          3: 'The stream number must be 1 (or 0 which means'
 122          ' auto-select). Others aren\'t supported.',
 123          4: 'Why would you ask me to generate 0 addresses for you?',
 124          5: 'You have (accidentally?) specified too many addresses to'
 125          ' make. Maximum 999. This check only exists to prevent'
 126          ' mischief; if you really want to create more addresses than'
 127          ' this, contact the Bitmessage developers and we can modify'
 128          ' the check or you can do it yourself by searching the source'
 129          ' code for this message.',
 130          6: 'The encoding type must be 2 or 3.',
 131          7: 'Could not decode address',
 132          8: 'Checksum failed for address',
 133          9: 'Invalid characters in address',
 134          10: 'Address version number too high (or zero)',
 135          11: 'The address version number currently must be 2, 3 or 4.'
 136          ' Others aren\'t supported. Check the address.',
 137          12: 'The stream number must be 1. Others aren\'t supported.'
 138          ' Check the address.',
 139          13: 'Could not find this address in your keys.dat file.',
 140          14: 'Your fromAddress is disabled. Cannot send.',
 141          15: 'Invalid ackData object size.',
 142          16: 'You are already subscribed to that address.',
 143          17: 'Label is not valid UTF-8 data.',
 144          18: 'Chan name does not match address.',
 145          19: 'The length of hash should be 32 bytes (encoded in hex'
 146          ' thus 64 characters).',
 147          20: 'Invalid method:',
 148          21: 'Unexpected API Failure',
 149          22: 'Decode error',
 150          23: 'Bool expected in eighteenByteRipe',
 151          24: 'Chan address is already present.',
 152          25: 'Specified address is not a chan address.'
 153          ' Use deleteAddress API call instead.',
 154          26: 'Malformed varint in address: ',
 155          27: 'Message is too long.',
 156          28: 'Invalid parameter'
 157      }
 158  
 159      def __new__(mcs, name, bases, namespace):
 160          result = super(ErrorCodes, mcs).__new__(mcs, name, bases, namespace)
 161          for code in six.iteritems(mcs._CODES):
 162              # beware: the formatting is adjusted for list-table
 163              result.__doc__ += """   * - %04i
 164           - %s
 165      """ % code
 166          return result
 167  
 168  
 169  class APIError(xmlrpc_server.Fault, metaclass=ErrorCodes):
 170      """
 171      APIError exception class
 172  
 173      .. list-table:: Possible error values
 174         :header-rows: 1
 175         :widths: auto
 176  
 177         * - Error Number
 178           - Message
 179      """
 180  
 181      def __str__(self):
 182          return "API Error %04i: %s" % (self.faultCode, self.faultString)
 183  
 184  
 185  # This thread, of which there is only one, runs the API.
 186  class singleAPI(StoppableThread):
 187      """API thread"""
 188  
 189      name = "singleAPI"
 190  
 191      def stopThread(self):
 192          super(singleAPI, self).stopThread()
 193          s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 194          try:
 195              s.connect((
 196                  config.get('bitmessagesettings', 'apiinterface'),
 197                  config.getint('bitmessagesettings', 'apiport')
 198              ))
 199              s.shutdown(socket.SHUT_RDWR)
 200              s.close()
 201          except BaseException:
 202              pass
 203  
 204      def run(self):
 205          """
 206          The instance of `SimpleXMLRPCServer.SimpleXMLRPCServer` or
 207          :class:`jsonrpclib.SimpleJSONRPCServer` is created and started here
 208          with `BMRPCDispatcher` dispatcher.
 209          """
 210          port = config.getint('bitmessagesettings', 'apiport')
 211          try:
 212              getattr(errno, 'WSAEADDRINUSE')
 213          except AttributeError:
 214              errno.WSAEADDRINUSE = errno.EADDRINUSE
 215  
 216          RPCServerBase = xmlrpc_server.SimpleXMLRPCServer
 217          ct = 'text/xml'
 218          if config.safeGet(
 219                  'bitmessagesettings', 'apivariant') == 'json':
 220              try:
 221                  from jsonrpclib.SimpleJSONRPCServer import \
 222                      SimpleJSONRPCServer as RPCServerBase
 223              except ImportError:
 224                  logger.warning(
 225                      'jsonrpclib not available, failing back to XML-RPC')
 226              else:
 227                  ct = 'application/json-rpc'
 228  
 229          # Nested class. FIXME not found a better solution.
 230          class StoppableRPCServer(RPCServerBase):
 231              """A SimpleXMLRPCServer that honours state.shutdown"""
 232              allow_reuse_address = True
 233              content_type = ct
 234  
 235              def serve_forever(self, poll_interval=None):
 236                  """Start the RPCServer"""
 237                  sql_ready.wait()
 238                  while state.shutdown == 0:
 239                      self.handle_request()
 240  
 241          for attempt in range(50):
 242              try:
 243                  if attempt > 0:
 244                      logger.warning(
 245                          'Failed to start API listener on port %s', port)
 246                      port = random.randint(32767, 65535)  # nosec B311
 247                  se = StoppableRPCServer(
 248                      (config.get(
 249                          'bitmessagesettings', 'apiinterface'),
 250                       port),
 251                      BMXMLRPCRequestHandler, True, encoding='UTF-8')
 252              except socket.error as e:
 253                  if e.errno in (errno.EADDRINUSE, errno.WSAEADDRINUSE):
 254                      continue
 255              else:
 256                  if attempt > 0:
 257                      logger.warning('Setting apiport to %s', port)
 258                      config.set(
 259                          'bitmessagesettings', 'apiport', str(port))
 260                      config.save()
 261                  break
 262  
 263          se.register_instance(BMRPCDispatcher())
 264          se.register_introspection_functions()
 265  
 266          apiNotifyPath = config.safeGet(
 267              'bitmessagesettings', 'apinotifypath')
 268  
 269          if apiNotifyPath:
 270              logger.info('Trying to call %s', apiNotifyPath)
 271              try:
 272                  subprocess.call([apiNotifyPath, "startingUp"])  # nosec B603
 273              except OSError:
 274                  logger.warning(
 275                      'Failed to call %s, removing apinotifypath setting',
 276                      apiNotifyPath)
 277                  config.remove_option(
 278                      'bitmessagesettings', 'apinotifypath')
 279  
 280          se.serve_forever()
 281  
 282  
 283  class CommandHandler(type):
 284      """
 285      The metaclass for `BMRPCDispatcher` which fills _handlers dict by
 286      methods decorated with @command
 287      """
 288      def __new__(mcs, name, bases, namespace):
 289          # pylint: disable=protected-access
 290          result = super(CommandHandler, mcs).__new__(
 291              mcs, name, bases, namespace)
 292          result.config = config
 293          result._handlers = {}
 294          apivariant = result.config.safeGet('bitmessagesettings', 'apivariant')
 295          for func in list(namespace.values()):
 296              try:
 297                  for alias in getattr(func, '_cmd'):
 298                      try:
 299                          prefix, alias = alias.split(':')
 300                          if apivariant != prefix:
 301                              continue
 302                      except ValueError:
 303                          pass
 304                      result._handlers[alias] = func
 305              except AttributeError:
 306                  pass
 307          return result
 308  
 309  
 310  class testmode(object):  # pylint: disable=too-few-public-methods
 311      """Decorator to check testmode & route to command decorator"""
 312  
 313      def __init__(self, *aliases):
 314          self.aliases = aliases
 315  
 316      def __call__(self, func):
 317          """Testmode call method"""
 318  
 319          if not state.testmode:
 320              return None
 321          return command(self.aliases[0]).__call__(func)
 322  
 323  
 324  class command(object):  # pylint: disable=too-few-public-methods
 325      """Decorator for API command method"""
 326      def __init__(self, *aliases):
 327          self.aliases = aliases
 328  
 329      def __call__(self, func):
 330  
 331          if config.safeGet(
 332                  'bitmessagesettings', 'apivariant') == 'legacy':
 333              def wrapper(*args):
 334                  """
 335                  A wrapper for legacy apivariant which dumps the result
 336                  into string of json
 337                  """
 338                  result = func(*args)
 339                  return result if isinstance(result, (int, str)) \
 340                      else json.dumps(result, indent=4)
 341              wrapper.__doc__ = func.__doc__
 342          else:
 343              wrapper = func
 344          # pylint: disable=protected-access
 345          wrapper._cmd = self.aliases
 346          wrapper.__doc__ = """Commands: *%s*
 347  
 348          """ % ', '.join(self.aliases) + wrapper.__doc__.lstrip()
 349          return wrapper
 350  
 351  
 352  # This is one of several classes that constitute the API
 353  # This class was written by Vaibhav Bhatia.
 354  # Modified by Jonathan Warren (Atheros).
 355  # Further modified by the Bitmessage developers
 356  # http://code.activestate.com/recipes/501148
 357  class BMXMLRPCRequestHandler(xmlrpc_server.SimpleXMLRPCRequestHandler):
 358      """The main API handler"""
 359  
 360      # pylint: disable=protected-access
 361      def do_POST(self):
 362          """
 363          Handles the HTTP POST request.
 364  
 365          Attempts to interpret all HTTP POST requests as XML-RPC calls,
 366          which are forwarded to the server's _dispatch method for handling.
 367  
 368          .. note:: this method is the same as in
 369            `SimpleXMLRPCServer.SimpleXMLRPCRequestHandler`,
 370            just hacked to handle cookies
 371          """
 372  
 373          # Check that the path is legal
 374          if not self.is_rpc_path_valid():
 375              self.report_404()
 376              return
 377  
 378          try:
 379              # Get arguments by reading body of request.
 380              # We read this in chunks to avoid straining
 381              # socket.read(); around the 10 or 15Mb mark, some platforms
 382              # begin to have problems (bug #792570).
 383              max_chunk_size = 10 * 1024 * 1024
 384              size_remaining = int(self.headers["content-length"])
 385              L = []
 386              while size_remaining:
 387                  chunk_size = min(size_remaining, max_chunk_size)
 388                  chunk = self.rfile.read(chunk_size)
 389                  if not chunk:
 390                      break
 391                  L.append(chunk)
 392                  size_remaining -= len(L[-1])
 393              data = b''.join(L)
 394  
 395              # data = self.decode_request_content(data)
 396              # pylint: disable=attribute-defined-outside-init
 397              self.cookies = []
 398  
 399              validuser = self.APIAuthenticateClient()
 400              if not validuser:
 401                  time.sleep(2)
 402                  self.send_response(http_client.UNAUTHORIZED)
 403                  self.end_headers()
 404                  return
 405                  # "RPC Username or password incorrect or HTTP header"
 406                  # " lacks authentication at all."
 407              else:
 408                  # In previous versions of SimpleXMLRPCServer, _dispatch
 409                  # could be overridden in this class, instead of in
 410                  # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
 411                  # check to see if a subclass implements _dispatch and dispatch
 412                  # using that method if present.
 413  
 414                  response = self.server._marshaled_dispatch(
 415                      data, getattr(self, '_dispatch', None)
 416                  )
 417          except Exception:  # This should only happen if the module is buggy
 418              # internal error, report as HTTP server error
 419              self.send_response(http_client.INTERNAL_SERVER_ERROR)
 420              self.end_headers()
 421          else:
 422              # got a valid XML RPC response
 423              self.send_response(http_client.OK)
 424              self.send_header("Content-type", self.server.content_type)
 425              self.send_header("Content-length", str(len(response)))
 426  
 427              # HACK :start -> sends cookies here
 428              if self.cookies:
 429                  for cookie in self.cookies:
 430                      self.send_header('Set-Cookie', cookie.output(header=''))
 431              # HACK :end
 432  
 433              self.end_headers()
 434              self.wfile.write(response)
 435  
 436              # shut down the connection
 437              self.wfile.flush()
 438              self.connection.shutdown(1)
 439  
 440              # actually handle shutdown command after sending response
 441              if state.shutdown is False:
 442                  shutdown.doCleanShutdown()
 443  
 444      def APIAuthenticateClient(self):
 445          """
 446          Predicate to check for valid API credentials in the request header
 447          """
 448  
 449          if 'Authorization' in self.headers:
 450              # handle Basic authentication
 451              encstr = self.headers.get('Authorization').split()[1]
 452              emailid, password = base64.b64decode(
 453                  encstr).decode('utf-8').split(':')
 454              return (
 455                  emailid == config.get(
 456                      'bitmessagesettings', 'apiusername'
 457                  ) and password == config.get(
 458                      'bitmessagesettings', 'apipassword'))
 459          else:
 460              logger.warning(
 461                  'Authentication failed because header lacks'
 462                  ' Authentication field')
 463              time.sleep(2)
 464  
 465          return False
 466  
 467  
 468  # pylint: disable=no-self-use,no-member,too-many-public-methods
 469  @six.add_metaclass(CommandHandler)
 470  class BMRPCDispatcher(object):
 471      """This class is used to dispatch API commands"""
 472  
 473      @staticmethod
 474      def _decode(text, decode_type):
 475          try:
 476              if decode_type == 'hex':
 477                  return unhexlify(text)
 478              elif decode_type == 'base64':
 479                  return base64.b64decode(text)
 480          except Exception as e:
 481              raise APIError(
 482                  22, 'Decode error - %s. Had trouble while decoding string: %r'
 483                  % (e, text)
 484              )
 485  
 486      def _verifyAddress(self, address):
 487          status, addressVersionNumber, streamNumber, ripe = \
 488              decodeAddress(address)
 489          if status != 'success':
 490              if status == 'checksumfailed':
 491                  raise APIError(8, 'Checksum failed for address: ' + address)
 492              if status == 'invalidcharacters':
 493                  raise APIError(9, 'Invalid characters in address: ' + address)
 494              if status == 'versiontoohigh':
 495                  raise APIError(
 496                      10, 'Address version number too high (or zero) in address: '
 497                      + address)
 498              if status == 'varintmalformed':
 499                  raise APIError(26, 'Malformed varint in address: ' + address)
 500              raise APIError(
 501                  7, 'Could not decode address: %s : %s' % (address, status))
 502          if addressVersionNumber < 2 or addressVersionNumber > 4:
 503              raise APIError(
 504                  11, 'The address version number currently must be 2, 3 or 4.'
 505                  ' Others aren\'t supported. Check the address.'
 506              )
 507          if streamNumber != 1:
 508              raise APIError(
 509                  12, 'The stream number must be 1. Others aren\'t supported.'
 510                  ' Check the address.'
 511              )
 512  
 513          return {
 514              'status': status,
 515              'addressVersion': addressVersionNumber,
 516              'streamNumber': streamNumber,
 517              'ripe': base64.b64encode(ripe)
 518          } if self._method == 'decodeAddress' else (
 519              status, addressVersionNumber, streamNumber, ripe)
 520  
 521      @staticmethod
 522      def _dump_inbox_message(
 523              msgid, toAddress, fromAddress, subject, received,
 524              message, encodingtype, read):
 525          subject = shared.fixPotentiallyInvalidUTF8Data(subject)
 526          message = shared.fixPotentiallyInvalidUTF8Data(message)
 527          return {
 528              'msgid': hexlify(msgid),
 529              'toAddress': toAddress,
 530              'fromAddress': fromAddress,
 531              'subject': base64.b64encode(subject),
 532              'message': base64.b64encode(message),
 533              'encodingType': encodingtype,
 534              'receivedTime': received,
 535              'read': read
 536          }
 537  
 538      @staticmethod
 539      def _dump_sent_message(  # pylint: disable=too-many-arguments
 540              msgid, toAddress, fromAddress, subject, lastactiontime,
 541              message, encodingtype, status, ackdata):
 542          subject = shared.fixPotentiallyInvalidUTF8Data(subject)
 543          message = shared.fixPotentiallyInvalidUTF8Data(message)
 544          return {
 545              'msgid': hexlify(msgid),
 546              'toAddress': toAddress,
 547              'fromAddress': fromAddress,
 548              'subject': base64.b64encode(subject),
 549              'message': base64.b64encode(message),
 550              'encodingType': encodingtype,
 551              'lastActionTime': lastactiontime,
 552              'status': status,
 553              'ackData': hexlify(ackdata)
 554          }
 555  
 556      @staticmethod
 557      def _blackwhitelist_entries(kind='black'):
 558          queryreturn = sqlQuery(
 559              "SELECT label, address FROM %slist WHERE enabled = 1" % kind
 560          )
 561          data = [
 562              {'label': base64.b64encode(
 563                  shared.fixPotentiallyInvalidUTF8Data(label)),
 564               'address': address} for label, address in queryreturn
 565          ]
 566          return {'addresses': data}
 567  
 568      def _blackwhitelist_add(self, address, label, kind='black'):
 569          label = self._decode(label, "base64")
 570          address = addBMIfNotPresent(address)
 571          self._verifyAddress(address)
 572          queryreturn = sqlQuery(
 573              "SELECT address FROM %slist WHERE address=?" % kind, address)
 574          if queryreturn != []:
 575              sqlExecute(
 576                  "UPDATE %slist SET label=?, enabled=1 WHERE address=?" % kind,
 577                  address)
 578          else:
 579              sqlExecute(
 580                  "INSERT INTO %slist VALUES (?,?,1)" % kind, label, address)
 581          queues.UISignalQueue.put(('rerenderBlackWhiteList', ''))
 582  
 583      def _blackwhitelist_del(self, address, kind='black'):
 584          address = addBMIfNotPresent(address)
 585          self._verifyAddress(address)
 586          sqlExecute("DELETE FROM %slist WHERE address=?" % kind, address)
 587          queues.UISignalQueue.put(('rerenderBlackWhiteList', ''))
 588  
 589      # Request Handlers
 590  
 591      @command('decodeAddress')
 592      def HandleDecodeAddress(self, address):
 593          """
 594          Decode given address and return dict with
 595          status, addressVersion, streamNumber and ripe keys
 596          """
 597          return self._verifyAddress(address)
 598  
 599      @command('listAddresses', 'listAddresses2')
 600      def HandleListAddresses(self):
 601          """
 602          Returns dict with a list of all used addresses with their properties
 603          in the *addresses* key.
 604          """
 605          data = []
 606          for address in self.config.addresses():
 607              streamNumber = decodeAddress(address)[2]
 608              label = self.config.get(address, 'label')
 609              if self._method == 'listAddresses2':
 610                  label = base64.b64encode(label)
 611              data.append({
 612                  'label': label,
 613                  'address': address,
 614                  'stream': streamNumber,
 615                  'enabled': self.config.safeGetBoolean(address, 'enabled'),
 616                  'chan': self.config.safeGetBoolean(address, 'chan')
 617              })
 618          return {'addresses': data}
 619  
 620      # the listAddressbook alias should be removed eventually.
 621      @command('listAddressBookEntries', 'legacy:listAddressbook')
 622      def HandleListAddressBookEntries(self, label=None):
 623          """
 624          Returns dict with a list of all address book entries (address and label)
 625          in the *addresses* key.
 626          """
 627          queryreturn = sqlQuery(
 628              "SELECT label, address from addressbook WHERE label = ?",
 629              label
 630          ) if label else sqlQuery("SELECT label, address from addressbook")
 631          data = []
 632          for label, address in queryreturn:
 633              label = shared.fixPotentiallyInvalidUTF8Data(label)
 634              data.append({
 635                  'label': base64.b64encode(label),
 636                  'address': address
 637              })
 638          return {'addresses': data}
 639  
 640      # the addAddressbook alias should be deleted eventually.
 641      @command('addAddressBookEntry', 'legacy:addAddressbook')
 642      def HandleAddAddressBookEntry(self, address, label):
 643          """Add an entry to address book. label must be base64 encoded."""
 644          label = self._decode(label, "base64")
 645          address = addBMIfNotPresent(address)
 646          self._verifyAddress(address)
 647          # TODO: add unique together constraint in the table
 648          queryreturn = sqlQuery(
 649              "SELECT address FROM addressbook WHERE address=?", address)
 650          if queryreturn != []:
 651              raise APIError(
 652                  16, 'You already have this address in your address book.')
 653  
 654          sqlExecute("INSERT INTO addressbook VALUES(?,?)", label, address)
 655          queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
 656          queues.UISignalQueue.put(('rerenderMessagelistToLabels', ''))
 657          queues.UISignalQueue.put(('rerenderAddressBook', ''))
 658          return "Added address %s to address book" % address
 659  
 660      # the deleteAddressbook alias should be deleted eventually.
 661      @command('deleteAddressBookEntry', 'legacy:deleteAddressbook')
 662      def HandleDeleteAddressBookEntry(self, address):
 663          """Delete an entry from address book."""
 664          address = addBMIfNotPresent(address)
 665          self._verifyAddress(address)
 666          sqlExecute('DELETE FROM addressbook WHERE address=?', address)
 667          queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
 668          queues.UISignalQueue.put(('rerenderMessagelistToLabels', ''))
 669          queues.UISignalQueue.put(('rerenderAddressBook', ''))
 670          return "Deleted address book entry for %s if it existed" % address
 671  
 672      @command('getBlackWhitelistKind')
 673      def HandleGetBlackWhitelistKind(self):
 674          """Get the list kind set in config - black or white."""
 675          return self.config.get('bitmessagesettings', 'blackwhitelist')
 676  
 677      @command('setBlackWhitelistKind')
 678      def HandleSetBlackWhitelistKind(self, kind):
 679          """Set the list kind used - black or white."""
 680          blackwhitelist_kinds = ('black', 'white')
 681          if kind not in blackwhitelist_kinds:
 682              raise APIError(
 683                  28, 'Invalid kind, should be one of %s'
 684                  % (blackwhitelist_kinds,))
 685          return self.config.set('bitmessagesettings', 'blackwhitelist', kind)
 686  
 687      @command('listBlacklistEntries')
 688      def HandleListBlacklistEntries(self):
 689          """
 690          Returns dict with a list of all blacklist entries (address and label)
 691          in the *addresses* key.
 692          """
 693          return self._blackwhitelist_entries('black')
 694  
 695      @command('listWhitelistEntries')
 696      def HandleListWhitelistEntries(self):
 697          """
 698          Returns dict with a list of all whitelist entries (address and label)
 699          in the *addresses* key.
 700          """
 701          return self._blackwhitelist_entries('white')
 702  
 703      @command('addBlacklistEntry')
 704      def HandleAddBlacklistEntry(self, address, label):
 705          """Add an entry to blacklist. label must be base64 encoded."""
 706          self._blackwhitelist_add(address, label, 'black')
 707          return "Added address %s to blacklist" % address
 708  
 709      @command('addWhitelistEntry')
 710      def HandleAddWhitelistEntry(self, address, label):
 711          """Add an entry to whitelist. label must be base64 encoded."""
 712          self._blackwhitelist_add(address, label, 'white')
 713          return "Added address %s to whitelist" % address
 714  
 715      @command('deleteBlacklistEntry')
 716      def HandleDeleteBlacklistEntry(self, address):
 717          """Delete an entry from blacklist."""
 718          self._blackwhitelist_del(address, 'black')
 719          return "Deleted blacklist entry for %s if it existed" % address
 720  
 721      @command('deleteWhitelistEntry')
 722      def HandleDeleteWhitelistEntry(self, address):
 723          """Delete an entry from whitelist."""
 724          self._blackwhitelist_del(address, 'white')
 725          return "Deleted whitelist entry for %s if it existed" % address
 726  
 727      @command('createRandomAddress')
 728      def HandleCreateRandomAddress(
 729          self, label, eighteenByteRipe=False, totalDifficulty=0,
 730          smallMessageDifficulty=0
 731      ):
 732          """
 733          Create one address using the random number generator.
 734  
 735          :param str label: base64 encoded label for the address
 736          :param bool eighteenByteRipe: is telling Bitmessage whether to
 737            generate an address with an 18 byte RIPE hash
 738            (as opposed to a 19 byte hash).
 739          """
 740  
 741          nonceTrialsPerByte = self.config.get(
 742              'bitmessagesettings', 'defaultnoncetrialsperbyte'
 743          ) if not totalDifficulty else int(
 744              networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty)
 745          payloadLengthExtraBytes = self.config.get(
 746              'bitmessagesettings', 'defaultpayloadlengthextrabytes'
 747          ) if not smallMessageDifficulty else int(
 748              networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty)
 749  
 750          if not isinstance(eighteenByteRipe, bool):
 751              raise APIError(
 752                  23, 'Bool expected in eighteenByteRipe, saw %s instead'
 753                  % type(eighteenByteRipe))
 754          label = self._decode(label, "base64")
 755          try:
 756              label.decode('utf-8')
 757          except UnicodeDecodeError:
 758              raise APIError(17, 'Label is not valid UTF-8 data.')
 759          queues.apiAddressGeneratorReturnQueue.queue.clear()
 760          # FIXME hard coded stream no
 761          streamNumberForAddress = 1
 762          queues.addressGeneratorQueue.put((
 763              'createRandomAddress', 4, streamNumberForAddress, label, 1, "",
 764              eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes
 765          ))
 766          return queues.apiAddressGeneratorReturnQueue.get()
 767  
 768      @command('createDeterministicAddresses')
 769      def HandleCreateDeterministicAddresses(
 770          self, passphrase, numberOfAddresses=1, addressVersionNumber=0,
 771          streamNumber=0, eighteenByteRipe=False, totalDifficulty=0,
 772          smallMessageDifficulty=0
 773      ):
 774          """
 775          Create many addresses deterministically using the passphrase.
 776  
 777          :param str passphrase: base64 encoded passphrase
 778          :param int numberOfAddresses: number of addresses to create,
 779            up to 999
 780  
 781          *addressVersionNumber* and *streamNumber* may be set to 0
 782          which will tell Bitmessage to use the most up-to-date
 783          address version and the most available stream.
 784          """
 785  
 786          nonceTrialsPerByte = self.config.get(
 787              'bitmessagesettings', 'defaultnoncetrialsperbyte'
 788          ) if not totalDifficulty else int(
 789              networkDefaultProofOfWorkNonceTrialsPerByte * totalDifficulty)
 790          payloadLengthExtraBytes = self.config.get(
 791              'bitmessagesettings', 'defaultpayloadlengthextrabytes'
 792          ) if not smallMessageDifficulty else int(
 793              networkDefaultPayloadLengthExtraBytes * smallMessageDifficulty)
 794  
 795          if not passphrase:
 796              raise APIError(1, 'The specified passphrase is blank.')
 797          if not isinstance(eighteenByteRipe, bool):
 798              raise APIError(
 799                  23, 'Bool expected in eighteenByteRipe, saw %s instead'
 800                  % type(eighteenByteRipe))
 801          passphrase = self._decode(passphrase, "base64")
 802          # 0 means "just use the proper addressVersionNumber"
 803          if addressVersionNumber == 0:
 804              addressVersionNumber = 4
 805          if addressVersionNumber not in (3, 4):
 806              raise APIError(
 807                  2, 'The address version number currently must be 3, 4, or 0'
 808                  ' (which means auto-select). %i isn\'t supported.'
 809                  % addressVersionNumber)
 810          if streamNumber == 0:  # 0 means "just use the most available stream"
 811              streamNumber = 1  # FIXME hard coded stream no
 812          if streamNumber != 1:
 813              raise APIError(
 814                  3, 'The stream number must be 1 (or 0 which means'
 815                  ' auto-select). Others aren\'t supported.')
 816          if numberOfAddresses == 0:
 817              raise APIError(
 818                  4, 'Why would you ask me to generate 0 addresses for you?')
 819          if numberOfAddresses > 999:
 820              raise APIError(
 821                  5, 'You have (accidentally?) specified too many addresses to'
 822                  ' make. Maximum 999. This check only exists to prevent'
 823                  ' mischief; if you really want to create more addresses than'
 824                  ' this, contact the Bitmessage developers and we can modify'
 825                  ' the check or you can do it yourself by searching the source'
 826                  ' code for this message.')
 827          queues.apiAddressGeneratorReturnQueue.queue.clear()
 828          logger.debug(
 829              'Requesting that the addressGenerator create %s addresses.',
 830              numberOfAddresses)
 831          queues.addressGeneratorQueue.put((
 832              'createDeterministicAddresses', addressVersionNumber, streamNumber,
 833              'unused API address', numberOfAddresses, passphrase,
 834              eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes
 835          ))
 836  
 837          return {'addresses': queues.apiAddressGeneratorReturnQueue.get()}
 838  
 839      @command('getDeterministicAddress')
 840      def HandleGetDeterministicAddress(
 841              self, passphrase, addressVersionNumber, streamNumber):
 842          """
 843          Similar to *createDeterministicAddresses* except that the one
 844          address that is returned will not be added to the Bitmessage
 845          user interface or the keys.dat file.
 846          """
 847  
 848          numberOfAddresses = 1
 849          eighteenByteRipe = False
 850          if not passphrase:
 851              raise APIError(1, 'The specified passphrase is blank.')
 852          passphrase = self._decode(passphrase, "base64")
 853          if addressVersionNumber not in (3, 4):
 854              raise APIError(
 855                  2, 'The address version number currently must be 3 or 4. %i'
 856                  ' isn\'t supported.' % addressVersionNumber)
 857          if streamNumber != 1:
 858              raise APIError(
 859                  3, ' The stream number must be 1. Others aren\'t supported.')
 860          queues.apiAddressGeneratorReturnQueue.queue.clear()
 861          logger.debug(
 862              'Requesting that the addressGenerator create %s addresses.',
 863              numberOfAddresses)
 864          queues.addressGeneratorQueue.put((
 865              'getDeterministicAddress', addressVersionNumber, streamNumber,
 866              'unused API address', numberOfAddresses, passphrase,
 867              eighteenByteRipe
 868          ))
 869          return queues.apiAddressGeneratorReturnQueue.get()
 870  
 871      @command('createChan')
 872      def HandleCreateChan(self, passphrase):
 873          """
 874          Creates a new chan. passphrase must be base64 encoded.
 875          Returns the corresponding Bitmessage address.
 876          """
 877  
 878          passphrase = self._decode(passphrase, "base64")
 879          if not passphrase:
 880              raise APIError(1, 'The specified passphrase is blank.')
 881          # It would be nice to make the label the passphrase but it is
 882          # possible that the passphrase contains non-utf-8 characters.
 883          try:
 884              passphrase.decode('utf-8')
 885              label = str_chan + ' ' + passphrase
 886          except UnicodeDecodeError:
 887              label = str_chan + ' ' + repr(passphrase)
 888  
 889          addressVersionNumber = 4
 890          streamNumber = 1
 891          queues.apiAddressGeneratorReturnQueue.queue.clear()
 892          logger.debug(
 893              'Requesting that the addressGenerator create chan %s.', passphrase)
 894          queues.addressGeneratorQueue.put((
 895              'createChan', addressVersionNumber, streamNumber, label,
 896              passphrase, True
 897          ))
 898          queueReturn = queues.apiAddressGeneratorReturnQueue.get()
 899          try:
 900              return queueReturn[0]
 901          except IndexError:
 902              raise APIError(24, 'Chan address is already present.')
 903  
 904      @command('joinChan')
 905      def HandleJoinChan(self, passphrase, suppliedAddress):
 906          """
 907          Join a chan. passphrase must be base64 encoded. Returns 'success'.
 908          """
 909  
 910          passphrase = self._decode(passphrase, "base64")
 911          if not passphrase:
 912              raise APIError(1, 'The specified passphrase is blank.')
 913          # It would be nice to make the label the passphrase but it is
 914          # possible that the passphrase contains non-utf-8 characters.
 915          try:
 916              passphrase.decode('utf-8')
 917              label = str_chan + ' ' + passphrase
 918          except UnicodeDecodeError:
 919              label = str_chan + ' ' + repr(passphrase)
 920  
 921          self._verifyAddress(suppliedAddress)
 922          suppliedAddress = addBMIfNotPresent(suppliedAddress)
 923          queues.apiAddressGeneratorReturnQueue.queue.clear()
 924          queues.addressGeneratorQueue.put((
 925              'joinChan', suppliedAddress, label, passphrase, True
 926          ))
 927          queueReturn = queues.apiAddressGeneratorReturnQueue.get()
 928          try:
 929              if queueReturn[0] == 'chan name does not match address':
 930                  raise APIError(18, 'Chan name does not match address.')
 931          except IndexError:
 932              raise APIError(24, 'Chan address is already present.')
 933  
 934          return "success"
 935  
 936      @command('leaveChan')
 937      def HandleLeaveChan(self, address):
 938          """
 939          Leave a chan. Returns 'success'.
 940  
 941          .. note:: at this time, the address is still shown in the UI
 942            until a restart.
 943          """
 944          self._verifyAddress(address)
 945          address = addBMIfNotPresent(address)
 946          if not self.config.safeGetBoolean(address, 'chan'):
 947              raise APIError(
 948                  25, 'Specified address is not a chan address.'
 949                  ' Use deleteAddress API call instead.')
 950          try:
 951              self.config.remove_section(address)
 952          except configparser.NoSectionError:
 953              raise APIError(
 954                  13, 'Could not find this address in your keys.dat file.')
 955          self.config.save()
 956          queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
 957          queues.UISignalQueue.put(('rerenderMessagelistToLabels', ''))
 958          return "success"
 959  
 960      @command('deleteAddress')
 961      def HandleDeleteAddress(self, address):
 962          """
 963          Permanently delete the address from keys.dat file. Returns 'success'.
 964          """
 965          self._verifyAddress(address)
 966          address = addBMIfNotPresent(address)
 967          try:
 968              self.config.remove_section(address)
 969          except configparser.NoSectionError:
 970              raise APIError(
 971                  13, 'Could not find this address in your keys.dat file.')
 972          self.config.save()
 973          queues.UISignalQueue.put(('writeNewAddressToTable', ('', '', '')))
 974          shared.reloadMyAddressHashes()
 975          return "success"
 976  
 977      @command('enableAddress')
 978      def HandleEnableAddress(self, address, enable=True):
 979          """Enable or disable the address depending on the *enable* value"""
 980          self._verifyAddress(address)
 981          address = addBMIfNotPresent(address)
 982          config.set(address, 'enabled', str(enable))
 983          self.config.save()
 984          shared.reloadMyAddressHashes()
 985          return "success"
 986  
 987      @command('getAllInboxMessages')
 988      def HandleGetAllInboxMessages(self):
 989          """
 990          Returns a dict with all inbox messages in the *inboxMessages* key.
 991          The message is a dict with such keys:
 992          *msgid*, *toAddress*, *fromAddress*, *subject*, *message*,
 993          *encodingType*, *receivedTime*, *read*.
 994          *msgid* is hex encoded string.
 995          *subject* and *message* are base64 encoded.
 996          """
 997  
 998          queryreturn = sqlQuery(
 999              "SELECT msgid, toaddress, fromaddress, subject, received, message,"
1000              " encodingtype, read FROM inbox WHERE folder='inbox'"
1001              " ORDER BY received"
1002          )
1003          return {"inboxMessages": [
1004              self._dump_inbox_message(*data) for data in queryreturn
1005          ]}
1006  
1007      @command('getAllInboxMessageIds', 'getAllInboxMessageIDs')
1008      def HandleGetAllInboxMessageIds(self):
1009          """
1010          The same as *getAllInboxMessages* but returns only *msgid*s,
1011          result key - *inboxMessageIds*.
1012          """
1013  
1014          queryreturn = sqlQuery(
1015              "SELECT msgid FROM inbox where folder='inbox' ORDER BY received")
1016  
1017          return {"inboxMessageIds": [
1018              {'msgid': hexlify(msgid)} for msgid, in queryreturn
1019          ]}
1020  
1021      @command('getInboxMessageById', 'getInboxMessageByID')
1022      def HandleGetInboxMessageById(self, hid, readStatus=None):
1023          """
1024          Returns a dict with list containing single message in the result
1025          key *inboxMessage*. May also return None if message was not found.
1026  
1027          :param str hid: hex encoded msgid
1028          :param bool readStatus: sets the message's read status if present
1029          """
1030  
1031          msgid = self._decode(hid, "hex")
1032          if readStatus is not None:
1033              if not isinstance(readStatus, bool):
1034                  raise APIError(
1035                      23, 'Bool expected in readStatus, saw %s instead.'
1036                      % type(readStatus))
1037              queryreturn = sqlQuery(
1038                  "SELECT read FROM inbox WHERE msgid=?", msgid)
1039              # UPDATE is slow, only update if status is different
1040              try:
1041                  if (queryreturn[0][0] == 1) != readStatus:
1042                      sqlExecute(
1043                          "UPDATE inbox set read = ? WHERE msgid=?",
1044                          readStatus, msgid)
1045                      queues.UISignalQueue.put(('changedInboxUnread', None))
1046              except IndexError:
1047                  pass
1048          queryreturn = sqlQuery(
1049              "SELECT msgid, toaddress, fromaddress, subject, received, message,"
1050              " encodingtype, read FROM inbox WHERE msgid=?", msgid
1051          )
1052          try:
1053              return {"inboxMessage": [
1054                  self._dump_inbox_message(*queryreturn[0])]}
1055          except IndexError:
1056              pass  # FIXME inconsistent
1057  
1058      @command('getAllSentMessages')
1059      def HandleGetAllSentMessages(self):
1060          """
1061          The same as *getAllInboxMessages* but for sent,
1062          result key - *sentMessages*. Message dict keys are:
1063          *msgid*, *toAddress*, *fromAddress*, *subject*, *message*,
1064          *encodingType*, *lastActionTime*, *status*, *ackData*.
1065          *ackData* is also a hex encoded string.
1066          """
1067  
1068          queryreturn = sqlQuery(
1069              "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
1070              " message, encodingtype, status, ackdata FROM sent"
1071              " WHERE folder='sent' ORDER BY lastactiontime"
1072          )
1073          return {"sentMessages": [
1074              self._dump_sent_message(*data) for data in queryreturn
1075          ]}
1076  
1077      @command('getAllSentMessageIds', 'getAllSentMessageIDs')
1078      def HandleGetAllSentMessageIds(self):
1079          """
1080          The same as *getAllInboxMessageIds* but for sent,
1081          result key - *sentMessageIds*.
1082          """
1083  
1084          queryreturn = sqlQuery(
1085              "SELECT msgid FROM sent WHERE folder='sent'"
1086              " ORDER BY lastactiontime"
1087          )
1088          return {"sentMessageIds": [
1089              {'msgid': hexlify(msgid)} for msgid, in queryreturn
1090          ]}
1091  
1092      # after some time getInboxMessagesByAddress should be removed
1093      @command('getInboxMessagesByReceiver', 'legacy:getInboxMessagesByAddress')
1094      def HandleInboxMessagesByReceiver(self, toAddress):
1095          """
1096          The same as *getAllInboxMessages* but returns only messages
1097          for toAddress.
1098          """
1099  
1100          queryreturn = sqlQuery(
1101              "SELECT msgid, toaddress, fromaddress, subject, received,"
1102              " message, encodingtype, read FROM inbox WHERE folder='inbox'"
1103              " AND toAddress=?", toAddress)
1104          return {"inboxMessages": [
1105              self._dump_inbox_message(*data) for data in queryreturn
1106          ]}
1107  
1108      @command('getSentMessageById', 'getSentMessageByID')
1109      def HandleGetSentMessageById(self, hid):
1110          """
1111          Similiar to *getInboxMessageById* but doesn't change message's
1112          read status (sent messages have no such field).
1113          Result key is *sentMessage*
1114          """
1115  
1116          msgid = self._decode(hid, "hex")
1117          queryreturn = sqlQuery(
1118              "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
1119              " message, encodingtype, status, ackdata FROM sent WHERE msgid=?",
1120              msgid
1121          )
1122          try:
1123              return {"sentMessage": [
1124                  self._dump_sent_message(*queryreturn[0])
1125              ]}
1126          except IndexError:
1127              pass  # FIXME inconsistent
1128  
1129      @command('getSentMessagesByAddress', 'getSentMessagesBySender')
1130      def HandleGetSentMessagesByAddress(self, fromAddress):
1131          """
1132          The same as *getAllSentMessages* but returns only messages
1133          from fromAddress.
1134          """
1135  
1136          queryreturn = sqlQuery(
1137              "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
1138              " message, encodingtype, status, ackdata FROM sent"
1139              " WHERE folder='sent' AND fromAddress=? ORDER BY lastactiontime",
1140              fromAddress
1141          )
1142          return {"sentMessages": [
1143              self._dump_sent_message(*data) for data in queryreturn
1144          ]}
1145  
1146      @command('getSentMessageByAckData')
1147      def HandleGetSentMessagesByAckData(self, ackData):
1148          """
1149          Similiar to *getSentMessageById* but searches by ackdata
1150          (also hex encoded).
1151          """
1152  
1153          ackData = self._decode(ackData, "hex")
1154          queryreturn = sqlQuery(
1155              "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
1156              " message, encodingtype, status, ackdata FROM sent"
1157              " WHERE ackdata=?", ackData
1158          )
1159  
1160          try:
1161              return {"sentMessage": [
1162                  self._dump_sent_message(*queryreturn[0])
1163              ]}
1164          except IndexError:
1165              pass  # FIXME inconsistent
1166  
1167      @command('trashMessage')
1168      def HandleTrashMessage(self, msgid):
1169          """
1170          Trash message by msgid (encoded in hex). Returns a simple message
1171          saying that the message was trashed assuming it ever even existed.
1172          Prior existence is not checked.
1173          """
1174          msgid = self._decode(msgid, "hex")
1175          # Trash if in inbox table
1176          helper_inbox.trash(msgid)
1177          # Trash if in sent table
1178          sqlExecute("UPDATE sent SET folder='trash' WHERE msgid=?", msgid)
1179          return 'Trashed message (assuming message existed).'
1180  
1181      @command('trashInboxMessage')
1182      def HandleTrashInboxMessage(self, msgid):
1183          """Trash inbox message by msgid (encoded in hex)."""
1184          msgid = self._decode(msgid, "hex")
1185          helper_inbox.trash(msgid)
1186          return 'Trashed inbox message (assuming message existed).'
1187  
1188      @command('trashSentMessage')
1189      def HandleTrashSentMessage(self, msgid):
1190          """Trash sent message by msgid (encoded in hex)."""
1191          msgid = self._decode(msgid, "hex")
1192          sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid)
1193          return 'Trashed sent message (assuming message existed).'
1194  
1195      @command('sendMessage')
1196      def HandleSendMessage(
1197          self, toAddress, fromAddress, subject, message,
1198          encodingType=2, TTL=4 * 24 * 60 * 60
1199      ):
1200          """
1201          Send the message and return ackdata (hex encoded string).
1202          subject and message must be encoded in base64 which may optionally
1203          include line breaks. TTL is specified in seconds; values outside
1204          the bounds of 3600 to 2419200 will be moved to be within those
1205          bounds. TTL defaults to 4 days.
1206          """
1207          # pylint: disable=too-many-locals
1208          if encodingType not in (2, 3):
1209              raise APIError(6, 'The encoding type must be 2 or 3.')
1210          subject = self._decode(subject, "base64")
1211          message = self._decode(message, "base64")
1212          if len(subject + message) > (2 ** 18 - 500):
1213              raise APIError(27, 'Message is too long.')
1214          if TTL < 60 * 60:
1215              TTL = 60 * 60
1216          if TTL > 28 * 24 * 60 * 60:
1217              TTL = 28 * 24 * 60 * 60
1218          toAddress = addBMIfNotPresent(toAddress)
1219          fromAddress = addBMIfNotPresent(fromAddress)
1220          self._verifyAddress(fromAddress)
1221          try:
1222              fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled')
1223          except configparser.NoSectionError:
1224              raise APIError(
1225                  13, 'Could not find your fromAddress in the keys.dat file.')
1226          if not fromAddressEnabled:
1227              raise APIError(14, 'Your fromAddress is disabled. Cannot send.')
1228  
1229          ackdata = helper_sent.insert(
1230              toAddress=toAddress, fromAddress=fromAddress,
1231              subject=subject, message=message, encoding=encodingType, ttl=TTL)
1232  
1233          toLabel = ''
1234          queryreturn = sqlQuery(
1235              "SELECT label FROM addressbook WHERE address=?", toAddress)
1236          try:
1237              toLabel = queryreturn[0][0]
1238          except IndexError:
1239              pass
1240  
1241          queues.UISignalQueue.put(('displayNewSentMessage', (
1242              toAddress, toLabel, fromAddress, subject, message, ackdata)))
1243          queues.workerQueue.put(('sendmessage', toAddress))
1244  
1245          return hexlify(ackdata)
1246  
1247      @command('sendBroadcast')
1248      def HandleSendBroadcast(
1249          self, fromAddress, subject, message, encodingType=2,
1250              TTL=4 * 24 * 60 * 60):
1251          """Send the broadcast message. Similiar to *sendMessage*."""
1252  
1253          if encodingType not in (2, 3):
1254              raise APIError(6, 'The encoding type must be 2 or 3.')
1255  
1256          subject = self._decode(subject, "base64")
1257          message = self._decode(message, "base64")
1258          if len(subject + message) > (2 ** 18 - 500):
1259              raise APIError(27, 'Message is too long.')
1260          if TTL < 60 * 60:
1261              TTL = 60 * 60
1262          if TTL > 28 * 24 * 60 * 60:
1263              TTL = 28 * 24 * 60 * 60
1264          fromAddress = addBMIfNotPresent(fromAddress)
1265          self._verifyAddress(fromAddress)
1266          try:
1267              fromAddressEnabled = self.config.getboolean(fromAddress, 'enabled')
1268          except configparser.NoSectionError:
1269              raise APIError(
1270                  13, 'Could not find your fromAddress in the keys.dat file.')
1271          if not fromAddressEnabled:
1272              raise APIError(14, 'Your fromAddress is disabled. Cannot send.')
1273  
1274          toAddress = str_broadcast_subscribers
1275  
1276          ackdata = helper_sent.insert(
1277              fromAddress=fromAddress, subject=subject,
1278              message=message, status='broadcastqueued',
1279              encoding=encodingType)
1280  
1281          toLabel = str_broadcast_subscribers
1282          queues.UISignalQueue.put(('displayNewSentMessage', (
1283              toAddress, toLabel, fromAddress, subject, message, ackdata)))
1284          queues.workerQueue.put(('sendbroadcast', ''))
1285  
1286          return hexlify(ackdata)
1287  
1288      @command('getStatus')
1289      def HandleGetStatus(self, ackdata):
1290          """
1291          Get the status of sent message by its ackdata (hex encoded).
1292          Returns one of these strings: notfound, msgqueued,
1293          broadcastqueued, broadcastsent, doingpubkeypow, awaitingpubkey,
1294          doingmsgpow, forcepow, msgsent, msgsentnoackexpected or ackreceived.
1295          """
1296  
1297          if len(ackdata) < 76:
1298              # The length of ackData should be at least 38 bytes (76 hex digits)
1299              raise APIError(15, 'Invalid ackData object size.')
1300          ackdata = self._decode(ackdata, "hex")
1301          queryreturn = sqlQuery(
1302              "SELECT status FROM sent where ackdata=?", ackdata)
1303          try:
1304              return queryreturn[0][0]
1305          except IndexError:
1306              return 'notfound'
1307  
1308      @command('addSubscription')
1309      def HandleAddSubscription(self, address, label=''):
1310          """Subscribe to the address. label must be base64 encoded."""
1311  
1312          if label:
1313              label = self._decode(label, "base64")
1314              try:
1315                  label.decode('utf-8')
1316              except UnicodeDecodeError:
1317                  raise APIError(17, 'Label is not valid UTF-8 data.')
1318          self._verifyAddress(address)
1319          address = addBMIfNotPresent(address)
1320          # First we must check to see if the address is already in the
1321          # subscriptions list.
1322          queryreturn = sqlQuery(
1323              "SELECT * FROM subscriptions WHERE address=?", address)
1324          if queryreturn:
1325              raise APIError(16, 'You are already subscribed to that address.')
1326          sqlExecute(
1327              "INSERT INTO subscriptions VALUES (?,?,?)", label, address, True)
1328          shared.reloadBroadcastSendersForWhichImWatching()
1329          queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
1330          queues.UISignalQueue.put(('rerenderSubscriptions', ''))
1331          return 'Added subscription.'
1332  
1333      @command('deleteSubscription')
1334      def HandleDeleteSubscription(self, address):
1335          """
1336          Unsubscribe from the address. The program does not check whether
1337          you were subscribed in the first place.
1338          """
1339  
1340          address = addBMIfNotPresent(address)
1341          sqlExecute("DELETE FROM subscriptions WHERE address=?", address)
1342          shared.reloadBroadcastSendersForWhichImWatching()
1343          queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
1344          queues.UISignalQueue.put(('rerenderSubscriptions', ''))
1345          return 'Deleted subscription if it existed.'
1346  
1347      @command('listSubscriptions')
1348      def ListSubscriptions(self):
1349          """
1350          Returns dict with a list of all subscriptions
1351          in the *subscriptions* key.
1352          """
1353  
1354          queryreturn = sqlQuery(
1355              "SELECT label, address, enabled FROM subscriptions")
1356          data = []
1357          for label, address, enabled in queryreturn:
1358              label = shared.fixPotentiallyInvalidUTF8Data(label)
1359              data.append({
1360                  'label': base64.b64encode(label),
1361                  'address': address,
1362                  'enabled': enabled == 1
1363              })
1364          return {'subscriptions': data}
1365  
1366      @command('disseminatePreEncryptedMsg', 'disseminatePreparedObject')
1367      def HandleDisseminatePreparedObject(
1368          self, encryptedPayload,
1369          nonceTrialsPerByte=networkDefaultProofOfWorkNonceTrialsPerByte,
1370          payloadLengthExtraBytes=networkDefaultPayloadLengthExtraBytes
1371      ):
1372          """
1373          Handle a request to disseminate an encrypted message.
1374  
1375          The device issuing this command to PyBitmessage supplies an object
1376          that has already been encrypted but which may still need the PoW
1377          to be done. PyBitmessage accepts this object and sends it out
1378          to the rest of the Bitmessage network as if it had generated
1379          the message itself.
1380  
1381          *encryptedPayload* is a hex encoded string starting with the nonce,
1382          8 zero bytes in case of no PoW done.
1383          """
1384          encryptedPayload = self._decode(encryptedPayload, "hex")
1385  
1386          nonce, = unpack('>Q', encryptedPayload[:8])
1387          objectType, toStreamNumber, expiresTime = \
1388              protocol.decodeObjectParameters(encryptedPayload)
1389  
1390          if nonce == 0:  # Let us do the POW and attach it to the front
1391              encryptedPayload = encryptedPayload[8:]
1392              TTL = expiresTime - time.time() + 300  # a bit of extra padding
1393              # Let us do the POW and attach it to the front
1394              logger.debug("expiresTime: %s", expiresTime)
1395              logger.debug("TTL: %s", TTL)
1396              logger.debug("objectType: %s", objectType)
1397              logger.info(
1398                  '(For msg message via API) Doing proof of work. Total required'
1399                  ' difficulty: %s\nRequired small message difficulty: %s',
1400                  float(nonceTrialsPerByte)
1401                  / networkDefaultProofOfWorkNonceTrialsPerByte,
1402                  float(payloadLengthExtraBytes)
1403                  / networkDefaultPayloadLengthExtraBytes,
1404              )
1405              powStartTime = time.time()
1406              trialValue, nonce = proofofwork.calculate(
1407                  encryptedPayload, TTL,
1408                  nonceTrialsPerByte, payloadLengthExtraBytes
1409              )
1410              logger.info(
1411                  '(For msg message via API) Found proof of work %s\nNonce: %s\n'
1412                  'POW took %s seconds. %s nonce trials per second.',
1413                  trialValue, nonce, int(time.time() - powStartTime),
1414                  nonce / (time.time() - powStartTime)
1415              )
1416              encryptedPayload = pack('>Q', nonce) + encryptedPayload
1417  
1418          inventoryHash = calculateInventoryHash(encryptedPayload)
1419          state.Inventory[inventoryHash] = (
1420              objectType, toStreamNumber, encryptedPayload, expiresTime, b'')
1421          logger.info(
1422              'Broadcasting inv for msg(API disseminatePreEncryptedMsg'
1423              ' command): %s', hexlify(inventoryHash))
1424          invQueue.put((toStreamNumber, inventoryHash))
1425          return hexlify(inventoryHash).decode()
1426  
1427      @command('trashSentMessageByAckData')
1428      def HandleTrashSentMessageByAckDAta(self, ackdata):
1429          """Trash a sent message by ackdata (hex encoded)"""
1430          # This API method should only be used when msgid is not available
1431          ackdata = self._decode(ackdata, "hex")
1432          sqlExecute("UPDATE sent SET folder='trash' WHERE ackdata=?", ackdata)
1433          return 'Trashed sent message (assuming message existed).'
1434  
1435      @command('disseminatePubkey')
1436      def HandleDissimatePubKey(self, payload):
1437          """Handle a request to disseminate a public key"""
1438  
1439          # The device issuing this command to PyBitmessage supplies a pubkey
1440          # object to be disseminated to the rest of the Bitmessage network.
1441          # PyBitmessage accepts this pubkey object and sends it out to the rest
1442          # of the Bitmessage network as if it had generated the pubkey object
1443          # itself. Please do not yet add this to the api doc.
1444          payload = self._decode(payload, "hex")
1445  
1446          # Let us do the POW
1447          target = 2 ** 64 / ((
1448              len(payload) + networkDefaultPayloadLengthExtraBytes + 8
1449          ) * networkDefaultProofOfWorkNonceTrialsPerByte)
1450          logger.info('(For pubkey message via API) Doing proof of work...')
1451          initialHash = hashlib.sha512(payload).digest()
1452          trialValue, nonce = proofofwork.run(target, initialHash)
1453          logger.info(
1454              '(For pubkey message via API) Found proof of work %s Nonce: %s',
1455              trialValue, nonce
1456          )
1457          payload = pack('>Q', nonce) + payload
1458  
1459          pubkeyReadPosition = 8  # bypass the nonce
1460          if payload[pubkeyReadPosition:pubkeyReadPosition + 4] == \
1461                  '\x00\x00\x00\x00':  # if this pubkey uses 8 byte time
1462              pubkeyReadPosition += 8
1463          else:
1464              pubkeyReadPosition += 4
1465          addressVersionLength = decodeVarint(
1466              payload[pubkeyReadPosition:pubkeyReadPosition + 10])[1]
1467          pubkeyReadPosition += addressVersionLength
1468          pubkeyStreamNumber = decodeVarint(
1469              payload[pubkeyReadPosition:pubkeyReadPosition + 10])[0]
1470          inventoryHash = calculateInventoryHash(payload)
1471          objectType = 1  # .. todo::: support v4 pubkeys
1472          TTL = 28 * 24 * 60 * 60
1473          state.Inventory[inventoryHash] = (
1474              objectType, pubkeyStreamNumber, payload, int(time.time()) + TTL, ''
1475          )
1476          logger.info(
1477              'broadcasting inv within API command disseminatePubkey with'
1478              ' hash: %s', hexlify(inventoryHash))
1479          invQueue.put((pubkeyStreamNumber, inventoryHash))
1480  
1481      @command(
1482          'getMessageDataByDestinationHash', 'getMessageDataByDestinationTag')
1483      def HandleGetMessageDataByDestinationHash(self, requestedHash):
1484          """Handle a request to get message data by destination hash"""
1485  
1486          # Method will eventually be used by a particular Android app to
1487          # select relevant messages. Do not yet add this to the api
1488          # doc.
1489          if len(requestedHash) != 32:
1490              raise APIError(
1491                  19, 'The length of hash should be 32 bytes (encoded in hex'
1492                  ' thus 64 characters).')
1493          requestedHash = self._decode(requestedHash, "hex")
1494  
1495          # This is not a particularly commonly used API function. Before we
1496          # use it we'll need to fill out a field in our inventory database
1497          # which is blank by default (first20bytesofencryptedmessage).
1498          queryreturn = sqlQuery(
1499              "SELECT hash, payload FROM inventory WHERE tag = ''"
1500              " and objecttype = 2")
1501          with SqlBulkExecute() as sql:
1502              for hash01, payload in queryreturn:
1503                  readPosition = 16  # Nonce length + time length
1504                  # Stream Number length
1505                  readPosition += decodeVarint(
1506                      payload[readPosition:readPosition + 10])[1]
1507                  t = (payload[readPosition:readPosition + 32], hash01)
1508                  sql.execute("UPDATE inventory SET tag=? WHERE hash=?", *t)
1509  
1510          queryreturn = sqlQuery(
1511              "SELECT payload FROM inventory WHERE tag = ?", requestedHash)
1512          return {"receivedMessageDatas": [
1513              {'data': hexlify(payload)} for payload, in queryreturn
1514          ]}
1515  
1516      @command('clientStatus')
1517      def HandleClientStatus(self):
1518          """
1519          Returns the bitmessage status as dict with keys *networkConnections*,
1520          *numberOfMessagesProcessed*, *numberOfBroadcastsProcessed*,
1521          *numberOfPubkeysProcessed*, *pendingDownload*, *networkStatus*,
1522          *softwareName*, *softwareVersion*. *networkStatus* will be one of
1523          these strings: "notConnected",
1524          "connectedButHaveNotReceivedIncomingConnections",
1525          or "connectedAndReceivingIncomingConnections".
1526          """
1527  
1528          connections_num = len(stats.connectedHostsList())
1529  
1530          if connections_num == 0:
1531              networkStatus = 'notConnected'
1532          elif state.clientHasReceivedIncomingConnections:
1533              networkStatus = 'connectedAndReceivingIncomingConnections'
1534          else:
1535              networkStatus = 'connectedButHaveNotReceivedIncomingConnections'
1536          return {
1537              'networkConnections': connections_num,
1538              'numberOfMessagesProcessed': state.numberOfMessagesProcessed,
1539              'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed,
1540              'numberOfPubkeysProcessed': state.numberOfPubkeysProcessed,
1541              'pendingDownload': stats.pendingDownload(),
1542              'networkStatus': networkStatus,
1543              'softwareName': 'PyBitmessage',
1544              'softwareVersion': softwareVersion
1545          }
1546  
1547      @command('listConnections')
1548      def HandleListConnections(self):
1549          """
1550          Returns bitmessage connection information as dict with keys *inbound*,
1551          *outbound*.
1552          """
1553          if connectionpool is None:
1554              raise APIError(21, 'Could not import BMConnectionPool.')
1555          inboundConnections = []
1556          outboundConnections = []
1557          for i in list(connectionpool.pool.inboundConnections.values()):
1558              inboundConnections.append({
1559                  'host': i.destination.host,
1560                  'port': i.destination.port,
1561                  'fullyEstablished': i.fullyEstablished,
1562                  'userAgent': str(i.userAgent)
1563              })
1564          for i in list(connectionpool.pool.outboundConnections.values()):
1565              outboundConnections.append({
1566                  'host': i.destination.host,
1567                  'port': i.destination.port,
1568                  'fullyEstablished': i.fullyEstablished,
1569                  'userAgent': str(i.userAgent)
1570              })
1571          return {
1572              'inbound': inboundConnections,
1573              'outbound': outboundConnections
1574          }
1575  
1576      @command('helloWorld')
1577      def HandleHelloWorld(self, a, b):
1578          """Test two string params"""
1579          return a + '-' + b
1580  
1581      @command('add')
1582      def HandleAdd(self, a, b):
1583          """Test two numeric params"""
1584          return a + b
1585  
1586      @command('statusBar')
1587      def HandleStatusBar(self, message):
1588          """Update GUI statusbar message"""
1589          queues.UISignalQueue.put(('updateStatusBar', message))
1590          return "success"
1591  
1592      @testmode('undeleteMessage')
1593      def HandleUndeleteMessage(self, msgid):
1594          """Undelete message"""
1595          msgid = self._decode(msgid, "hex")
1596          helper_inbox.undeleteMessage(msgid)
1597          return "Undeleted message"
1598  
1599      @command('deleteAndVacuum')
1600      def HandleDeleteAndVacuum(self):
1601          """Cleanup trashes and vacuum messages database"""
1602          sqlStoredProcedure('deleteandvacuume')
1603          return 'done'
1604  
1605      @command('shutdown')
1606      def HandleShutdown(self):
1607          """Shutdown the bitmessage. Returns 'done'."""
1608          # backward compatible trick because False == 0 is True
1609          state.shutdown = False
1610          return 'done'
1611  
1612      def _handle_request(self, method, params):
1613          try:
1614              # pylint: disable=attribute-defined-outside-init
1615              self._method = method
1616              func = self._handlers[method]
1617              return func(self, *params)
1618          except KeyError:
1619              raise APIError(20, 'Invalid method: %s' % method)
1620          except TypeError as e:
1621              msg = 'Unexpected API Failure - %s' % e
1622              if 'argument' not in str(e):
1623                  raise APIError(21, msg)
1624              argcount = len(params)
1625              maxcount = func.__code__.co_argcount
1626              if argcount > maxcount:
1627                  msg = (
1628                      'Command %s takes at most %s parameters (%s given)'
1629                      % (method, maxcount, argcount))
1630              else:
1631                  mincount = maxcount - len(func.__defaults__ or [])
1632                  if argcount < mincount:
1633                      msg = (
1634                          'Command %s takes at least %s parameters (%s given)'
1635                          % (method, mincount, argcount))
1636              raise APIError(0, msg)
1637          finally:
1638              state.last_api_response = time.time()
1639  
1640      def _dispatch(self, method, params):
1641          _fault = None
1642  
1643          try:
1644              return self._handle_request(method, params)
1645          except APIError as e:
1646              _fault = e
1647          except varintDecodeError as e:
1648              logger.error(e)
1649              _fault = APIError(
1650                  26, 'Data contains a malformed varint. Some details: %s' % e)
1651          except Exception as e:
1652              logger.exception(e)
1653              _fault = APIError(21, 'Unexpected API Failure - %s' % e)
1654  
1655          if _fault:
1656              if self.config.safeGet(
1657                      'bitmessagesettings', 'apivariant') == 'legacy':
1658                  return str(_fault)
1659              else:
1660                  raise _fault  # pylint: disable=raising-bad-type
1661  
1662      def _listMethods(self):
1663          """List all API commands"""
1664          return list(self._handlers.keys())
1665  
1666      def _methodHelp(self, method):
1667          return self._handlers[method].__doc__