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