/ nomadnet / NomadNetworkApp.py
NomadNetworkApp.py
   1  import os
   2  import io
   3  import sys
   4  import time
   5  import shlex
   6  import atexit
   7  import threading
   8  import traceback
   9  import subprocess
  10  import contextlib
  11  
  12  import RNS
  13  import LXMF
  14  import nomadnet
  15  
  16  from nomadnet.Directory import DirectoryEntry
  17  from datetime import datetime
  18  
  19  import RNS.vendor.umsgpack as msgpack
  20  
  21  from ._version import __version__
  22  from RNS.vendor.configobj import ConfigObj
  23  
  24  class NomadNetworkApp:
  25      time_format      = "%Y-%m-%d %H:%M:%S"
  26      _shared_instance = None
  27  
  28      userdir = os.path.expanduser("~")
  29      if os.path.isdir("/etc/nomadnetwork") and os.path.isfile("/etc/nomadnetwork/config"):
  30          configdir = "/etc/nomadnetwork"
  31      elif os.path.isdir(userdir+"/.config/nomadnetwork") and os.path.isfile(userdir+"/.config/nomadnetwork/config"):
  32          configdir = userdir+"/.config/nomadnetwork"
  33      else:
  34          configdir = userdir+"/.nomadnetwork"
  35  
  36      START_ANNOUNCE_DELAY = 3
  37  
  38      def exit_handler(self):
  39          self.should_run_jobs = False
  40  
  41          RNS.log("Saving directory...", RNS.LOG_VERBOSE)
  42          self.directory.save_to_disk()
  43  
  44          if hasattr(self.ui, "restore_ixon"):
  45              if self.ui.restore_ixon:
  46                  try:
  47                      os.system("stty ixon")
  48  
  49                  except Exception as e:
  50                      RNS.log("Could not restore flow control sequences. The contained exception was: "+str(e), RNS.LOG_WARNING)
  51  
  52          if hasattr(self.ui, "restore_palette"):
  53              if self.ui.restore_palette:
  54                  try:
  55                      self.ui.screen.write("\x1b]104\x07")
  56  
  57                  except Exception as e:
  58                      RNS.log("Could not restore terminal color palette. The contained exception was: "+str(e), RNS.LOG_WARNING)
  59  
  60          RNS.log("Nomad Network Client exiting now", RNS.LOG_VERBOSE)
  61  
  62      def exception_handler(self, e_type, e_value, e_traceback):
  63          RNS.log("An unhandled exception occurred, the details of which will be dumped below", RNS.LOG_ERROR)
  64          RNS.log("Type  : "+str(e_type), RNS.LOG_ERROR)
  65          RNS.log("Value : "+str(e_value), RNS.LOG_ERROR)
  66          t_string = ""
  67          for line in traceback.format_tb(e_traceback):
  68              t_string += line
  69          RNS.log("Trace : \n"+t_string, RNS.LOG_ERROR)
  70  
  71          if issubclass(e_type, KeyboardInterrupt):
  72              sys.__excepthook__(e_type, e_value, e_traceback)
  73  
  74      def __init__(self, configdir = None, rnsconfigdir = None, daemon = False, force_console = False):
  75          self.version       = __version__
  76          self.enable_client = False
  77          self.enable_node   = False
  78          self.identity      = None
  79  
  80          self.uimode        = None
  81  
  82          if configdir == None:
  83              self.configdir = NomadNetworkApp.configdir
  84          else:
  85              self.configdir = configdir
  86  
  87          if force_console:
  88              self.force_console_log = True
  89          else:
  90              self.force_console_log = False
  91  
  92          if NomadNetworkApp._shared_instance == None:
  93              NomadNetworkApp._shared_instance = self
  94  
  95          self.rns = RNS.Reticulum(configdir = rnsconfigdir)
  96  
  97          self.configpath        = self.configdir+"/config"
  98          self.ignoredpath       = self.configdir+"/ignored"
  99          self.logfilepath       = self.configdir+"/logfile"
 100          self.errorfilepath     = self.configdir+"/errors"
 101          self.pnannouncedpath   = self.configdir+"/pnannounced"
 102          self.storagepath       = self.configdir+"/storage"
 103          self.identitypath      = self.configdir+"/storage/identity"
 104          self.cachepath         = self.configdir+"/storage/cache"
 105          self.resourcepath      = self.configdir+"/storage/resources"
 106          self.conversationpath  = self.configdir+"/storage/conversations"
 107          self.directorypath     = self.configdir+"/storage/directory"
 108          self.peersettingspath  = self.configdir+"/storage/peersettings"
 109          self.tmpfilespath      = self.configdir+"/storage/tmp"
 110  
 111          self.pagespath         = self.configdir+"/storage/pages"
 112          self.filespath         = self.configdir+"/storage/files"
 113          self.cachepath         = self.configdir+"/storage/cache"
 114          self.examplespath      = self.configdir+"/examples"
 115  
 116          self.downloads_path    = os.path.expanduser("~/Downloads")
 117  
 118          self.firstrun               = False
 119          self.should_run_jobs        = True
 120          self.job_interval           = 5
 121          self.defer_jobs             = 90
 122          self.page_refresh_interval  = 0
 123          self.file_refresh_interval  = 0
 124  
 125          self.static_peers            = []
 126          self.peer_announce_at_start  = True
 127          self.try_propagation_on_fail = True
 128          self.disable_propagation     = True
 129          self.notify_on_new_message   = True
 130  
 131          self.lxmf_max_propagation_size = None
 132          self.lxmf_max_sync_size        = None
 133          self.lxmf_max_incoming_size    = None
 134          self.node_propagation_cost     = LXMF.LXMRouter.PROPAGATION_COST
 135  
 136          self.periodic_lxmf_sync = True
 137          self.lxmf_sync_interval = 360*60
 138          self.lxmf_sync_limit    = 8
 139          self.compact_stream     = False
 140          
 141          self.required_stamp_cost   = None
 142          self.accept_invalid_stamps = False
 143  
 144  
 145          if not os.path.isdir(self.storagepath):
 146              os.makedirs(self.storagepath)
 147  
 148          if not os.path.isdir(self.cachepath):
 149              os.makedirs(self.cachepath)
 150  
 151          if not os.path.isdir(self.resourcepath):
 152              os.makedirs(self.resourcepath)
 153  
 154          if not os.path.isdir(self.conversationpath):
 155              os.makedirs(self.conversationpath)
 156  
 157          if not os.path.isdir(self.pagespath):
 158              os.makedirs(self.pagespath)
 159  
 160          if not os.path.isdir(self.filespath):
 161              os.makedirs(self.filespath)
 162  
 163          if not os.path.isdir(self.cachepath):
 164              os.makedirs(self.cachepath)
 165  
 166          if not os.path.isdir(self.tmpfilespath):
 167              os.makedirs(self.tmpfilespath)
 168          else:
 169              self.clear_tmp_dir()
 170  
 171          if os.path.isfile(self.configpath):
 172              try:
 173                  self.config = ConfigObj(self.configpath)
 174                  try:
 175                      self.applyConfig()
 176                  except Exception as e:
 177                      RNS.log("The configuration file is invalid. The contained exception was: "+str(e), RNS.LOG_ERROR)
 178                      nomadnet.panic()
 179  
 180                  RNS.log("Configuration loaded from "+self.configpath)
 181              except Exception as e:
 182                  RNS.log("Could not parse the configuration at "+self.configpath, RNS.LOG_ERROR)
 183                  RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR)
 184                  nomadnet.panic()
 185          else:
 186              if not os.path.isdir(self.examplespath):
 187                  try:
 188                      import shutil
 189                      examplespath = os.path.join(os.path.dirname(__file__), "examples")
 190                      shutil.copytree(examplespath, self.examplespath, ignore=shutil.ignore_patterns("__pycache__"))
 191                  
 192                  except Exception as e:
 193                      RNS.log("Could not copy examples into the "+self.examplespath+" directory.", RNS.LOG_ERROR)
 194                      RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
 195              
 196              RNS.log("Could not load config file, creating default configuration file...")
 197              self.createDefaultConfig()
 198              self.firstrun = True
 199  
 200          if os.path.isfile(self.identitypath):
 201              try:
 202                  self.identity = RNS.Identity.from_file(self.identitypath)
 203                  if self.identity != None:
 204                      RNS.log("Loaded Primary Identity %s from %s" % (str(self.identity), self.identitypath))
 205                  else:
 206                      RNS.log("Could not load the Primary Identity from "+self.identitypath, RNS.LOG_ERROR)
 207                      nomadnet.panic()
 208              except Exception as e:
 209                  RNS.log("Could not load the Primary Identity from "+self.identitypath, RNS.LOG_ERROR)
 210                  RNS.log("The contained exception was: %s" % (str(e)), RNS.LOG_ERROR)
 211                  nomadnet.panic()
 212          else:
 213              try:
 214                  RNS.log("No Primary Identity file found, creating new...")
 215                  self.identity = RNS.Identity()
 216                  self.identity.to_file(self.identitypath)
 217                  RNS.log("Created new Primary Identity %s" % (str(self.identity)))
 218              except Exception as e:
 219                  RNS.log("Could not create and save a new Primary Identity", RNS.LOG_ERROR)
 220                  RNS.log("The contained exception was: %s" % (str(e)), RNS.LOG_ERROR)
 221                  nomadnet.panic()
 222  
 223          if os.path.isfile(self.peersettingspath):
 224              try:
 225                  file = open(self.peersettingspath, "rb")
 226                  self.peer_settings = msgpack.unpackb(file.read())
 227                  file.close()
 228  
 229                  if not "node_last_announce" in self.peer_settings:
 230                      self.peer_settings["node_last_announce"] = None
 231  
 232                  if not "propagation_node" in self.peer_settings:
 233                      self.peer_settings["propagation_node"] = None
 234  
 235                  if not "last_lxmf_sync" in self.peer_settings:
 236                      self.peer_settings["last_lxmf_sync"] = 0
 237  
 238                  if not "node_connects" in self.peer_settings:
 239                      self.peer_settings["node_connects"] = 0
 240  
 241                  if not "served_page_requests" in self.peer_settings:
 242                      self.peer_settings["served_page_requests"] = 0
 243  
 244                  if not "served_file_requests" in self.peer_settings:
 245                      self.peer_settings["served_file_requests"] = 0
 246  
 247              except Exception as e:
 248                  RNS.logdest = RNS.LOG_STDOUT
 249                  RNS.log(f"Could not load local peer settings from {self.peersettingspath}", RNS.LOG_ERROR)
 250                  RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
 251                  RNS.log(f"This likely means that the peer settings file has become corrupt.", RNS.LOG_ERROR)
 252                  RNS.log(f"You can try deleting the file at {self.peersettingspath} and restarting nomadnet.", RNS.LOG_ERROR)
 253                  nomadnet.panic()
 254          else:
 255              try:
 256                  RNS.log("No peer settings file found, creating new...")
 257                  self.peer_settings = {
 258                      "display_name": "Anonymous Peer",
 259                      "announce_interval": None,
 260                      "last_announce": None,
 261                      "node_last_announce": None,
 262                      "propagation_node": None,
 263                      "last_lxmf_sync": 0,
 264                      "node_connects": 0,
 265                      "served_page_requests": 0,
 266                      "served_file_requests": 0
 267                  }
 268                  self.save_peer_settings()
 269                  RNS.log("Created new peer settings file")
 270              except Exception as e:
 271                  RNS.log("Could not create and save a new peer settings file", RNS.LOG_ERROR)
 272                  RNS.log("The contained exception was: %s" % (str(e)), RNS.LOG_ERROR)
 273                  nomadnet.panic()
 274  
 275          self.ignored_list = []
 276          if os.path.isfile(self.ignoredpath):
 277              try:
 278                  fh = open(self.ignoredpath, "rb")
 279                  ignored_input = fh.read()
 280                  fh.close()
 281  
 282                  ignored_hash_strs = ignored_input.splitlines()
 283  
 284                  for hash_str in ignored_hash_strs:
 285                      if len(hash_str) == RNS.Identity.TRUNCATED_HASHLENGTH//8*2:
 286                          try:
 287                              ignored_hash = bytes.fromhex(hash_str.decode("utf-8"))
 288                              self.ignored_list.append(ignored_hash)
 289  
 290                          except Exception as e:
 291                              RNS.log("Could not decode RNS Identity hash from: "+str(hash_str), RNS.LOG_DEBUG)
 292                              RNS.log("The contained exception was: "+str(e), RNS.LOG_DEBUG)
 293  
 294              except Exception as e:
 295                  RNS.log("Error while loading list of ignored destinations: "+str(e), RNS.LOG_ERROR)
 296  
 297          self.directory = nomadnet.Directory(self)
 298  
 299          static_peers = []
 300          for static_peer in self.static_peers:
 301              try:
 302                  dh = bytes.fromhex(static_peer)
 303                  if len(dh) != RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
 304                      raise ValueError("Invalid destination length")
 305                  static_peers.append(dh)
 306              except Exception as e:
 307                  RNS.log(f"Could not decode static peer destination hash {static_peer}: {e}", RNS.LOG_ERROR)
 308  
 309          self.message_router = LXMF.LXMRouter(
 310              identity = self.identity, storagepath = self.storagepath, autopeer = True,
 311              propagation_limit = self.lxmf_max_propagation_size, sync_limit = self.lxmf_max_sync_size, delivery_limit = self.lxmf_max_incoming_size,
 312              max_peers = self.max_peers, static_peers = static_peers, propagation_cost=self.node_propagation_cost
 313          )
 314  
 315          self.message_router.register_delivery_callback(self.lxmf_delivery)
 316  
 317          for destination_hash in self.ignored_list:
 318              self.message_router.ignore_destination(destination_hash)
 319  
 320          self.lxmf_destination = self.message_router.register_delivery_identity(self.identity, display_name=self.peer_settings["display_name"], stamp_cost=self.required_stamp_cost)
 321          if not self.accept_invalid_stamps:
 322              self.message_router.enforce_stamps()
 323  
 324          RNS.Identity.remember(
 325              packet_hash=None,
 326              destination_hash=self.lxmf_destination.hash,
 327              public_key=self.identity.get_public_key(),
 328              app_data=None
 329          )
 330  
 331          RNS.log("LXMF Router ready to receive on: "+RNS.prettyhexrep(self.lxmf_destination.hash))
 332  
 333          if self.enable_node:
 334              self.message_router.set_message_storage_limit(megabytes=self.message_storage_limit)
 335              for dest_str in self.prioritised_lxmf_destinations:
 336                  try:
 337                      dest_hash = bytes.fromhex(dest_str)
 338                      if len(dest_hash) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8:
 339                          self.message_router.prioritise(dest_hash)
 340  
 341                  except Exception as e:
 342                      RNS.log("Cannot prioritise "+str(dest_str)+", it is not a valid destination hash", RNS.LOG_ERROR)
 343  
 344              if self.disable_propagation:
 345                  if os.path.isfile(self.pnannouncedpath):
 346                      try:
 347                          RNS.log("Sending indication to peered LXMF Propagation Node that this node is no longer participating", RNS.LOG_DEBUG)
 348                          self.message_router.disable_propagation()
 349                          os.unlink(self.pnannouncedpath)
 350                      except Exception as e:
 351                          RNS.log("An error ocurred while indicating that this LXMF Propagation Node is no longer participating. The contained exception was: "+str(e), RNS.LOG_ERROR)
 352              else:
 353                  self.message_router.enable_propagation()
 354                  try:
 355                      with open(self.pnannouncedpath, "wb") as pnf:
 356                          pnf.write(msgpack.packb(time.time()))
 357                          pnf.close()
 358  
 359                  except Exception as e:
 360                      RNS.log("An error ocurred while writing Propagation Node announce timestamp. The contained exception was: "+str(e), RNS.LOG_ERROR)
 361  
 362              if not self.disable_propagation:
 363                  RNS.log("LXMF Propagation Node started on: "+RNS.prettyhexrep(self.message_router.propagation_destination.hash))
 364                  
 365              self.node = nomadnet.Node(self)
 366          else:
 367              self.node = None
 368              if os.path.isfile(self.pnannouncedpath):
 369                  try:
 370                      RNS.log("Sending indication to peered LXMF Propagation Node that this node is no longer participating", RNS.LOG_DEBUG)
 371                      self.message_router.disable_propagation()
 372                      os.unlink(self.pnannouncedpath)
 373                  except Exception as e:
 374                      RNS.log("An error ocurred while indicating that this LXMF Propagation Node is no longer participating. The contained exception was: "+str(e), RNS.LOG_ERROR)
 375  
 376          RNS.Transport.register_announce_handler(nomadnet.Conversation)
 377          RNS.Transport.register_announce_handler(nomadnet.Directory)
 378  
 379          self.autoselect_propagation_node()
 380  
 381          if self.peer_announce_at_start:
 382              def delayed_announce():
 383                  time.sleep(NomadNetworkApp.START_ANNOUNCE_DELAY)
 384                  self.announce_now()
 385  
 386              da_thread = threading.Thread(target=delayed_announce)
 387              da_thread.setDaemon(True)
 388              da_thread.start()
 389  
 390          atexit.register(self.exit_handler)
 391          sys.excepthook = self.exception_handler
 392  
 393          job_thread = threading.Thread(target=self.__jobs)
 394          job_thread.setDaemon(True)
 395          job_thread.start()
 396  
 397          # Override UI choice from config on --daemon switch
 398          if daemon:
 399              self.uimode = nomadnet.ui.UI_NONE
 400  
 401          # This stderr redirect is needed to stop urwid
 402          # from spewing KeyErrors to the console and thus,
 403          # messing up the UI. A pull request to fix the
 404          # bug in urwid was submitted, but until it is
 405          # merged, this hack will mitigate it.
 406          strio = io.StringIO()
 407          with contextlib.redirect_stderr(strio):
 408              nomadnet.ui.spawn(self.uimode)
 409  
 410          if strio.tell() > 0:
 411              try:
 412                  strio.seek(0)
 413                  err_file = open(self.errorfilepath, "w")
 414                  err_file.write(strio.read())
 415                  err_file.close()
 416  
 417              except Exception as e:
 418                  RNS.log("Could not write stderr output to error log file at "+str(self.errorfilepath)+".", RNS.LOG_ERROR)
 419                  RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
 420  
 421  
 422      def __jobs(self):
 423          RNS.log("Deferring scheduled jobs for "+str(self.defer_jobs)+" seconds...", RNS.LOG_DEBUG)
 424          time.sleep(self.defer_jobs)
 425  
 426          RNS.log("Starting job scheduler now", RNS.LOG_DEBUG)
 427          while self.should_run_jobs:
 428              now = time.time()
 429              
 430              if now > self.peer_settings["last_lxmf_sync"] + self.lxmf_sync_interval:
 431                  RNS.log("Initiating automatic LXMF sync", RNS.LOG_VERBOSE)
 432                  self.request_lxmf_sync(limit=self.lxmf_sync_limit)
 433  
 434              time.sleep(self.job_interval)
 435  
 436      def set_display_name(self, display_name):
 437          self.peer_settings["display_name"] = display_name
 438          self.lxmf_destination.display_name = display_name
 439          self.save_peer_settings()
 440  
 441      def get_display_name(self):
 442          return self.peer_settings["display_name"]
 443  
 444      def get_display_name_bytes(self):
 445          return self.peer_settings["display_name"].encode("utf-8")
 446  
 447      def get_sync_status(self):
 448          if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE:
 449              return "Idle"
 450          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_PATH_REQUESTED:
 451              return "Path requested"
 452          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_ESTABLISHING:
 453              return "Establishing link"
 454          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_ESTABLISHED:
 455              return "Link established"
 456          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_REQUEST_SENT:
 457              return "Sync request sent"
 458          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RECEIVING:
 459              return "Receiving messages"
 460          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RESPONSE_RECEIVED:
 461              return "Messages received"
 462          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_PATH:
 463              return "No path to node"
 464          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_FAILED:
 465              return "Link establisment failed"
 466          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_TRANSFER_FAILED:
 467              return "Sync request failed"
 468          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_IDENTITY_RCVD:
 469              return "Remote got no identity"
 470          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_NO_ACCESS:
 471              return "Node rejected request"
 472          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_FAILED:
 473              return "Sync failed"
 474          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE:
 475              new_msgs = self.message_router.propagation_transfer_last_result
 476              if new_msgs == 0:
 477                  return "Done, no new messages"
 478              else:
 479                  return "Downloaded "+str(new_msgs)+" new messages"
 480          else:
 481              return "Unknown"
 482  
 483      def sync_status_show_percent(self):
 484          if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE:
 485              return False
 486          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_PATH_REQUESTED:
 487              return False
 488          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_ESTABLISHING:
 489              return False
 490          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_LINK_ESTABLISHED:
 491              return False
 492          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_REQUEST_SENT:
 493              return False
 494          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RECEIVING:
 495              return True
 496          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_RESPONSE_RECEIVED:
 497              return True
 498          elif self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_COMPLETE:
 499              return False
 500          else:
 501              return False
 502  
 503      def get_sync_progress(self):
 504          return self.message_router.propagation_transfer_progress
 505  
 506      def request_lxmf_sync(self, limit = None):
 507          if self.message_router.propagation_transfer_state == LXMF.LXMRouter.PR_IDLE or self.message_router.propagation_transfer_state >= LXMF.LXMRouter.PR_COMPLETE:
 508              self.peer_settings["last_lxmf_sync"] = time.time()
 509              self.save_peer_settings()
 510              self.message_router.request_messages_from_propagation_node(self.identity, max_messages = limit)
 511  
 512      def cancel_lxmf_sync(self):
 513          if self.message_router.propagation_transfer_state != LXMF.LXMRouter.PR_IDLE:
 514              self.message_router.cancel_propagation_node_requests()
 515  
 516      def announce_now(self):
 517          self.message_router.set_inbound_stamp_cost(self.lxmf_destination.hash, self.required_stamp_cost)
 518          self.lxmf_destination.display_name = self.peer_settings["display_name"]
 519          self.message_router.announce(self.lxmf_destination.hash)
 520          self.peer_settings["last_announce"] = time.time()
 521          self.save_peer_settings()
 522  
 523      def autoselect_propagation_node(self):
 524          selected_node = None
 525  
 526          if "propagation_node" in self.peer_settings and self.peer_settings["propagation_node"] != None:
 527              selected_node = self.peer_settings["propagation_node"]
 528          else:
 529              nodes = self.directory.known_nodes()
 530              trusted_nodes = []
 531  
 532              best_hops = RNS.Transport.PATHFINDER_M+1
 533  
 534              for node in nodes:
 535                  if node.trust_level == DirectoryEntry.TRUSTED:
 536                      hops = RNS.Transport.hops_to(node.source_hash)
 537  
 538                      if hops < best_hops:
 539                          best_hops = hops
 540                          selected_node = node.source_hash
 541  
 542          if selected_node == None:
 543              RNS.log("Could not autoselect a propagation node! LXMF propagation will not be available until a trusted node announces on the network, or a propagation node is manually selected.", RNS.LOG_WARNING)
 544          else:
 545              pn_name_str = ""
 546              RNS.log("Selecting "+RNS.prettyhexrep(selected_node)+pn_name_str+" as default LXMF propagation node", RNS.LOG_DEBUG)
 547              self.message_router.set_outbound_propagation_node(selected_node)
 548  
 549      def get_user_selected_propagation_node(self):
 550          if "propagation_node" in self.peer_settings:
 551              return self.peer_settings["propagation_node"]
 552          else:
 553              return None
 554  
 555      def set_user_selected_propagation_node(self, node_hash):
 556          self.peer_settings["propagation_node"] = node_hash
 557          self.save_peer_settings()
 558          self.autoselect_propagation_node()
 559      
 560      def get_default_propagation_node(self):
 561          return self.message_router.get_outbound_propagation_node()
 562  
 563      def save_peer_settings(self):
 564          tmp_path = f"{self.peersettingspath}.tmp"
 565          with open(tmp_path, "wb") as file: file.write(msgpack.packb(self.peer_settings))
 566          os.replace(tmp_path, self.peersettingspath)
 567  
 568      def lxmf_delivery(self, message):
 569          time_string = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp))
 570          signature_string = "Signature is invalid, reason undetermined"
 571          if message.signature_validated:
 572              signature_string = "Validated"
 573          else:
 574              if message.unverified_reason == LXMF.LXMessage.SIGNATURE_INVALID:
 575                  signature_string = "Invalid signature"
 576              if message.unverified_reason == LXMF.LXMessage.SOURCE_UNKNOWN:
 577                  signature_string = "Cannot verify, source is unknown"
 578  
 579          nomadnet.Conversation.ingest(message, self)
 580  
 581          if self.notify_on_new_message:
 582              self.notify_message_recieved()
 583  
 584          if self.should_print(message):
 585              self.print_message(message)
 586  
 587      def should_print(self, message):
 588          if self.print_messages:
 589              if self.print_all_messages:
 590                  return True
 591              
 592              else:
 593                  source_hash_text = RNS.hexrep(message.source_hash, delimit=False)
 594  
 595                  if self.print_trusted_messages:
 596                      trust_level = self.directory.trust_level(message.source_hash)
 597                      if trust_level == DirectoryEntry.TRUSTED:
 598                          return True
 599  
 600                  if type(self.allowed_message_print_destinations) is list:
 601                      if source_hash_text in self.allowed_message_print_destinations:
 602                          return True
 603  
 604          return False
 605  
 606      def print_file(self, filename):
 607          print_command = self.print_command+" "+filename
 608  
 609          try:
 610              return_code = subprocess.call(shlex.split(print_command), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
 611  
 612          except Exception as e:
 613              RNS.log("An error occurred while executing print command: "+str(print_command), RNS.LOG_ERROR)
 614              RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
 615              return False
 616  
 617          if return_code == 0:
 618              RNS.log("Successfully printed "+str(filename)+" using print command: "+print_command, RNS.LOG_DEBUG)
 619              return True
 620  
 621          else:
 622              RNS.log("Printing "+str(filename)+" failed using print command: "+print_command, RNS.LOG_DEBUG)
 623              return False
 624  
 625  
 626      def print_message(self, message, received = None):
 627          try:
 628              template = self.printing_template_msg
 629  
 630              if received == None:
 631                  received = time.time()
 632  
 633              g = self.ui.glyphs
 634              
 635              m_rtime = datetime.fromtimestamp(message.timestamp)
 636              stime = m_rtime.strftime(self.time_format)
 637  
 638              message_time = datetime.fromtimestamp(received)
 639              rtime = message_time.strftime(self.time_format)
 640  
 641              display_name = self.directory.simplest_display_str(message.source_hash)
 642              title = message.title_as_string()
 643              if title == "":
 644                  title = "None"
 645  
 646              output = template.format(
 647                  origin=display_name,
 648                  stime=stime,
 649                  rtime=rtime,
 650                  mtitle=title,
 651                  mbody=message.content_as_string(),
 652              )
 653  
 654              filename = "/tmp/"+RNS.hexrep(RNS.Identity.full_hash(output.encode("utf-8")), delimit=False)
 655              with open(filename, "wb") as f:
 656                  f.write(output.encode("utf-8"))
 657                  f.close()
 658  
 659              self.print_file(filename)
 660  
 661              os.unlink(filename)
 662  
 663          except Exception as e:
 664              RNS.log("Error while printing incoming LXMF message. The contained exception was: "+str(e))
 665  
 666      def conversations(self):
 667          return nomadnet.Conversation.conversation_list(self)
 668  
 669      def has_unread_conversations(self):
 670          if len(nomadnet.Conversation.unread_conversations) > 0:
 671              return True
 672          else:
 673              return False
 674  
 675      def conversation_is_unread(self, source_hash):
 676          if bytes.fromhex(source_hash) in nomadnet.Conversation.unread_conversations:
 677              return True
 678          else:
 679              return False
 680  
 681      def mark_conversation_read(self, source_hash):
 682          if bytes.fromhex(source_hash) in nomadnet.Conversation.unread_conversations:
 683              nomadnet.Conversation.unread_conversations.pop(bytes.fromhex(source_hash))
 684              if os.path.isfile(self.conversationpath + "/" + source_hash + "/unread"):
 685                  os.unlink(self.conversationpath + "/" + source_hash + "/unread")
 686  
 687      def notify_message_recieved(self):
 688          if self.uimode == nomadnet.ui.UI_TEXT:
 689              sys.stdout.write("\a")
 690              sys.stdout.flush()
 691  
 692      def clear_tmp_dir(self):
 693          if os.path.isdir(self.tmpfilespath):
 694              for file in os.listdir(self.tmpfilespath):
 695                  fpath = self.tmpfilespath+"/"+file
 696                  os.unlink(fpath)
 697  
 698      def createDefaultConfig(self):
 699          self.config = ConfigObj(__default_nomadnet_config__)
 700          self.config.filename = self.configpath
 701          
 702          if not os.path.isdir(self.configdir):
 703              os.makedirs(self.configdir)
 704          self.config.write()
 705          self.applyConfig()
 706  
 707  
 708      def applyConfig(self):
 709          if "logging" in self.config:
 710              for option in self.config["logging"]:
 711                  value = self.config["logging"][option]
 712                  if option == "loglevel":
 713                      RNS.loglevel = int(value)
 714                      if RNS.loglevel < 0:
 715                          RNS.loglevel = 0
 716                      if RNS.loglevel > 7:
 717                          RNS.loglevel = 7
 718                  if option == "destination":
 719                      if value.lower() == "file" and not self.force_console_log:
 720                          RNS.logdest = RNS.LOG_FILE
 721                          if "logfile" in self.config["logging"]:
 722                              self.logfilepath = self.config["logging"]["logfile"]
 723                          RNS.logfile = self.logfilepath
 724                      else:
 725                          RNS.logdest = RNS.LOG_STDOUT
 726  
 727          if "client" in self.config:
 728              for option in self.config["client"]:
 729                  value = self.config["client"][option]
 730  
 731                  if option == "enable_client":
 732                      value = self.config["client"].as_bool(option)
 733                      self.enable_client = value
 734  
 735                  if option == "downloads_path":
 736                      value = self.config["client"]["downloads_path"]
 737                      self.downloads_path = os.path.expanduser(value)
 738  
 739                  if option == "announce_at_start":
 740                      value = self.config["client"].as_bool(option)
 741                      self.peer_announce_at_start = value
 742  
 743                  if option == "try_propagation_on_send_fail":
 744                      value = self.config["client"].as_bool(option)
 745                      self.try_propagation_on_fail = value
 746  
 747                  if option == "periodic_lxmf_sync":
 748                      value = self.config["client"].as_bool(option)
 749                      self.periodic_lxmf_sync = value
 750  
 751                  if option == "lxmf_sync_interval":
 752                      value = self.config["client"].as_int(option)*60
 753  
 754                      if value >= 60:
 755                          self.lxmf_sync_interval = value
 756  
 757                  if option == "lxmf_sync_limit":
 758                      value = self.config["client"].as_int(option)    
 759  
 760                      if value > 0:
 761                          self.lxmf_sync_limit = value
 762                      else:
 763                          self.lxmf_sync_limit = None
 764  
 765                  if option == "required_stamp_cost":
 766                      value = self.config["client"][option]
 767                      if value.lower() == "none":
 768                          self.required_stamp_cost = None
 769                      else:
 770                          value = self.config["client"].as_int(option)
 771  
 772                          if value > 0:
 773                              if value > 255:
 774                                  value = 255
 775                              self.required_stamp_cost = value
 776                          else:
 777                              self.required_stamp_cost = None
 778  
 779                  if option == "accept_invalid_stamps":
 780                      value = self.config["client"].as_bool(option)
 781                      self.accept_invalid_stamps = value
 782  
 783                  if option == "max_accepted_size":
 784                      value = self.config["client"].as_float(option)    
 785  
 786                      if value > 0:
 787                          self.lxmf_max_incoming_size = value
 788                      else:
 789                          self.lxmf_max_incoming_size = 500
 790  
 791                  if option == "compact_announce_stream":
 792                      value = self.config["client"].as_bool(option)
 793                      self.compact_stream = value
 794  
 795                  if option == "notify_on_new_message":
 796                      value = self.config["client"].as_bool(option)
 797                      self.notify_on_new_message = value
 798  
 799                  if option == "user_interface":
 800                      value = value.lower()
 801                      if value == "none":
 802                          self.uimode = nomadnet.ui.UI_NONE
 803                      if value == "menu":
 804                          self.uimode = nomadnet.ui.UI_MENU
 805                      if value == "text":
 806                          self.uimode = nomadnet.ui.UI_TEXT
 807                          if "textui" in self.config:
 808                              if not "intro_time" in self.config["textui"]:
 809                                  self.config["textui"]["intro_time"] = 1
 810                              else:
 811                                  self.config["textui"]["intro_time"] = self.config["textui"].as_float("intro_time")
 812  
 813                              if not "intro_text" in self.config["textui"]:
 814                                  self.config["textui"]["intro_text"] = "Nomad Network"
 815  
 816                              if not "editor" in self.config["textui"]:
 817                                  self.config["textui"]["editor"] = "nano"
 818  
 819                              if not "glyphs" in self.config["textui"]:
 820                                  self.config["textui"]["glyphs"] = "unicode"
 821  
 822                              if not "mouse_enabled" in self.config["textui"]:
 823                                  self.config["textui"]["mouse_enabled"] = True
 824                              else:
 825                                  self.config["textui"]["mouse_enabled"] = self.config["textui"].as_bool("mouse_enabled")
 826  
 827                              if not "hide_guide" in self.config["textui"]:
 828                                  self.config["textui"]["hide_guide"] = False
 829                              else:
 830                                  self.config["textui"]["hide_guide"] = self.config["textui"].as_bool("hide_guide")
 831  
 832                              if not "animation_interval" in self.config["textui"]:
 833                                  self.config["textui"]["animation_interval"] = 1
 834                              else:
 835                                  self.config["textui"]["animation_interval"] = self.config["textui"].as_int("animation_interval")
 836  
 837                              if not "colormode" in self.config["textui"]:
 838                                  self.config["textui"]["colormode"] = nomadnet.ui.COLORMODE_16
 839                              else:
 840                                  if self.config["textui"]["colormode"].lower() == "monochrome":
 841                                      self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_MONO
 842                                  elif self.config["textui"]["colormode"].lower() == "16":
 843                                      self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_16
 844                                  elif self.config["textui"]["colormode"].lower() == "88":
 845                                      self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_88
 846                                  elif self.config["textui"]["colormode"].lower() == "256":
 847                                      self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_256
 848                                  elif self.config["textui"]["colormode"].lower() == "24bit":
 849                                      self.config["textui"]["colormode"] = nomadnet.ui.TextUI.COLORMODE_TRUE
 850                                  else:
 851                                      raise ValueError("The selected Text UI color mode is invalid")
 852  
 853                              if not "theme" in self.config["textui"]:
 854                                  self.config["textui"]["theme"] = nomadnet.ui.TextUI.THEME_DARK
 855                              else:
 856                                  if self.config["textui"]["theme"].lower() == "dark":
 857                                      self.config["textui"]["theme"] = nomadnet.ui.TextUI.THEME_DARK
 858                                  elif self.config["textui"]["theme"].lower() == "light":
 859                                      self.config["textui"]["theme"] = nomadnet.ui.TextUI.THEME_LIGHT
 860                                  else:
 861                                      raise ValueError("The selected Text UI theme is invalid")
 862                          else:
 863                              raise KeyError("Text UI selected in configuration file, but no [textui] section found")
 864                      if value == "graphical":
 865                          self.uimode = nomadnet.ui.UI_GRAPHICAL
 866                      if value == "web":
 867                          self.uimode = nomadnet.ui.UI_WEB
 868  
 869          if "node" in self.config:
 870              if not "enable_node" in self.config["node"]:
 871                  self.enable_node = False
 872              else:
 873                  self.enable_node = self.config["node"].as_bool("enable_node")
 874  
 875              if not "node_name" in self.config["node"]:
 876                  self.node_name = None
 877              else:
 878                  value = self.config["node"]["node_name"]
 879                  if value.lower() == "none":
 880                      self.node_name = None
 881                  else:
 882                      self.node_name = self.config["node"]["node_name"]
 883  
 884              if not "disable_propagation" in self.config["node"]:
 885                  self.disable_propagation = True
 886              else:
 887                  self.disable_propagation = self.config["node"].as_bool("disable_propagation")
 888  
 889              if not "max_transfer_size" in self.config["node"]:
 890                  self.lxmf_max_propagation_size = 256
 891              else:
 892                  value = self.config["node"].as_float("max_transfer_size")
 893                  if value < 1:
 894                      value = 1
 895                  self.lxmf_max_propagation_size = value
 896  
 897              if not "max_sync_size" in self.config["node"]:
 898                  self.lxmf_max_sync_size = 256*40
 899              else:
 900                  value = self.config["node"].as_float("max_sync_size")
 901                  if value < self.lxmf_max_propagation_size:
 902                      value = self.lxmf_max_propagation_size
 903                  self.lxmf_max_sync_size = value
 904  
 905              if not "announce_at_start" in self.config["node"]:
 906                  self.node_announce_at_start = False
 907              else:
 908                  value = self.config["node"].as_bool("announce_at_start")
 909                  self.node_announce_at_start = value
 910  
 911              if not "announce_interval" in self.config["node"]:
 912                  self.node_announce_interval = 720
 913              else:
 914                  value = self.config["node"].as_int("announce_interval")
 915                  if value < 1:
 916                      value = 1
 917                  self.node_announce_interval = value
 918  
 919              if not "propagation_cost" in self.config["node"]:
 920                  self.node_propagation_cost = 16
 921              else:
 922                  value = self.config["node"].as_int("propagation_cost")
 923                  if value < 13: value = 13
 924                  self.node_propagation_cost = value
 925                  
 926              if "pages_path" in self.config["node"]:
 927                  self.pagespath = self.config["node"]["pages_path"]
 928                  
 929              if not "page_refresh_interval" in self.config["node"]:
 930                  self.page_refresh_interval = 0
 931              else:
 932                  value = self.config["node"].as_int("page_refresh_interval")
 933                  if value < 0:
 934                      value = 0
 935                  self.page_refresh_interval = value
 936                  
 937  
 938              if "files_path" in self.config["node"]:
 939                  self.filespath = self.config["node"]["files_path"]
 940                  
 941              if not "file_refresh_interval" in self.config["node"]:
 942                  self.file_refresh_interval = 0
 943              else:
 944                  value = self.config["node"].as_int("file_refresh_interval")
 945                  if value < 0:
 946                      value = 0
 947                  self.file_refresh_interval = value
 948                  
 949  
 950              if "prioritise_destinations" in self.config["node"]:
 951                  self.prioritised_lxmf_destinations = self.config["node"].as_list("prioritise_destinations")
 952              else:
 953                  self.prioritised_lxmf_destinations = []
 954  
 955              if "static_peers" in self.config["node"]:
 956                  self.static_peers = self.config["node"].as_list("static_peers")
 957              else:
 958                  self.static_peers = []
 959                  
 960              if not "max_peers" in self.config["node"]:
 961                  self.max_peers = None
 962              else:
 963                  value = self.config["node"].as_int("max_peers")
 964                  if value < 0:
 965                      value = 0
 966                  self.max_peers = value
 967  
 968              if not "message_storage_limit" in self.config["node"]:
 969                  self.message_storage_limit = 2000
 970              else:
 971                  value = self.config["node"].as_float("message_storage_limit")
 972                  if value < 0.005:
 973                      value = 0.005
 974                  self.message_storage_limit = value
 975  
 976          self.print_command = "lp"
 977          self.print_messages = False
 978          self.print_all_messages = False
 979          self.print_trusted_messages = False
 980          if "printing" in self.config:
 981              if not "print_messages" in self.config["printing"]:
 982                  self.print_messages = False
 983              else:
 984                  self.print_messages = self.config["printing"].as_bool("print_messages")
 985  
 986                  if "print_command" in self.config["printing"]:
 987                      self.print_command = self.config["printing"]["print_command"]
 988  
 989                  if self.print_messages:
 990                      if not "print_from" in self.config["printing"]:
 991                          self.allowed_message_print_destinations = None
 992                      else:
 993                          if type(self.config["printing"]["print_from"]) == str:
 994                              self.allowed_message_print_destinations = []
 995                              if self.config["printing"]["print_from"].lower() == "everywhere":
 996                                  self.print_all_messages = True
 997  
 998                              if self.config["printing"]["print_from"].lower() == "trusted":
 999                                  
1000                                  self.print_all_messages = False
1001                                  self.print_trusted_messages = True
1002  
1003                              if len(self.config["printing"]["print_from"]) == (RNS.Identity.TRUNCATED_HASHLENGTH//8)*2:
1004                                  self.allowed_message_print_destinations.append(self.config["printing"]["print_from"])
1005                          
1006                          if type(self.config["printing"]["print_from"]) == list:
1007                                  self.allowed_message_print_destinations =  self.config["printing"].as_list("print_from")
1008                                  for allowed_entry in self.allowed_message_print_destinations:
1009                                      if allowed_entry.lower() == "trusted":
1010                                          self.print_trusted_messages = True
1011  
1012  
1013                      if not "message_template" in self.config["printing"]:
1014                          self.printing_template_msg = __printing_template_msg__
1015                      else:
1016                          mt_path = os.path.expanduser(self.config["printing"]["message_template"])
1017                          if os.path.isfile(mt_path):
1018                              template_file = open(mt_path, "rb")
1019                              self.printing_template_msg = template_file.read().decode("utf-8")
1020                          else:
1021                              template_file = open(mt_path, "wb")
1022                              template_file.write(__printing_template_msg__.encode("utf-8"))
1023                              self.printing_template_msg = __printing_template_msg__
1024  
1025                
1026      @staticmethod
1027      def get_shared_instance():
1028          if NomadNetworkApp._shared_instance != None:
1029              return NomadNetworkApp._shared_instance
1030          else:
1031              raise UnboundLocalError("No Nomad Network applications have been instantiated yet")
1032  
1033  
1034      def quit(self):
1035          RNS.log("Nomad Network Client shutting down...")
1036          os._exit(0)
1037  
1038  
1039  # Default configuration file:
1040  __default_nomadnet_config__ = '''# This is the default Nomad Network config file.
1041  # You should probably edit it to suit your needs and use-case,
1042  
1043  [logging]
1044  # Valid log levels are 0 through 7:
1045  #   0: Log only critical information
1046  #   1: Log errors and lower log levels
1047  #   2: Log warnings and lower log levels
1048  #   3: Log notices and lower log levels
1049  #   4: Log info and lower (this is the default)
1050  #   5: Verbose logging
1051  #   6: Debug logging
1052  #   7: Extreme logging
1053  
1054  loglevel = 4
1055  destination = file
1056  
1057  [client]
1058  
1059  enable_client = yes
1060  user_interface = text
1061  downloads_path = ~/Downloads
1062  notify_on_new_message = yes
1063  
1064  # By default, the peer is announced at startup
1065  # to let other peers reach it immediately.
1066  announce_at_start = yes
1067  
1068  # By default, the client will try to deliver a
1069  # message via the LXMF propagation network, if
1070  # a direct delivery to the recipient is not
1071  # possible.
1072  try_propagation_on_send_fail = yes
1073  
1074  # Nomadnet will periodically sync messages from
1075  # LXMF propagation nodes by default, if any are
1076  # present. You can disable this if you want to
1077  # only sync when manually initiated.
1078  periodic_lxmf_sync = yes
1079  
1080  # The sync interval in minutes. This value is
1081  # equal to 6 hours (360 minutes) by default.
1082  lxmf_sync_interval = 360
1083  
1084  # By default, automatic LXMF syncs will only
1085  # download 8 messages at a time. You can change
1086  # this number, or set the option to 0 to disable
1087  # the limit, and download everything every time.
1088  lxmf_sync_limit = 8
1089  
1090  # You can specify a required stamp cost for
1091  # inbound messages to be accepted. Specifying
1092  # a stamp cost will require untrusted senders
1093  # that message you to include a cryptographic
1094  # stamp in their messages. Performing this
1095  # operation takes the sender an amount of time
1096  # proportional to the stamp cost. As a rough
1097  # estimate, a stamp cost of 8 will take less
1098  # than a second to compute, and a stamp cost
1099  # of 20 could take several minutes, even on
1100  # a fast computer.
1101  required_stamp_cost = None
1102  
1103  # You can signal stamp requirements to senders,
1104  # but still accept messages with invalid stamps
1105  # by setting this option to True.
1106  accept_invalid_stamps = False
1107  
1108  # The maximum accepted unpacked size for mes-
1109  # sages received directly from other peers,
1110  # specified in kilobytes. Messages larger than
1111  # this will be rejected before the transfer
1112  # begins.
1113  max_accepted_size = 500
1114  
1115  # The announce stream will only show one entry
1116  # per destination or node by default. You can
1117  # change this to show as many announces as have
1118  # been received, for every destination.
1119  compact_announce_stream = yes
1120  
1121  [textui]
1122  
1123  # Amount of time to show intro screen
1124  intro_time = 1
1125  
1126  # You can specify the display theme.
1127  # theme = light
1128  theme = dark
1129  
1130  # Specify the number of colors to use
1131  # valid colormodes are:
1132  # monochrome, 16, 88, 256 and 24bit
1133  #
1134  # The default is a conservative 256 colors.
1135  # If your terminal does not support this,
1136  # you can lower it. Some terminals support
1137  # 24 bit color.
1138  
1139  # colormode = monochrome
1140  # colormode = 16
1141  # colormode = 88
1142  colormode = 256
1143  # colormode = 24bit
1144  
1145  # By default, unicode glyphs are used. If
1146  # you have a Nerd Font installed, you can
1147  # enable this for a better user interface.
1148  # You can also enable plain text glyphs if
1149  # your terminal doesn't support unicode.
1150  
1151  # glyphs = plain
1152  glyphs = unicode
1153  # glyphs = nerdfont
1154  
1155  # You can specify whether mouse events
1156  # should be considered as input to the
1157  # application. On by default.
1158  mouse_enabled = True
1159  
1160  # What editor to use for editing text.
1161  editor = nano
1162  
1163  # If you don't want the Guide section to
1164  # show up in the menu, you can disable it.
1165  hide_guide = no
1166  
1167  [node]
1168  
1169  # Whether to enable node hosting
1170  enable_node = no
1171  
1172  # The node name will be visible to other
1173  # peers on the network, and included in
1174  # announces.
1175  node_name = None
1176  
1177  # Automatic announce interval in minutes.
1178  # 6 hours by default.
1179  announce_interval = 360
1180  
1181  # Whether to announce when the node starts.
1182  announce_at_start = Yes
1183  
1184  # When Nomad Network is hosting a page-serving
1185  # node, it can also act as an LXMF propagation
1186  # node. This is a convenient feature that lets
1187  # you easily set up and run a propagation node
1188  # on the network, but it is not as fully
1189  # featured as using the lxmd program to host a
1190  # propagation node. For complete control and
1191  # flexibility, use lxmd to run a PN. For a
1192  # small local system or network, the built-in
1193  # PN functionality will suffice for most cases.
1194  #
1195  # If there is already a large amount of
1196  # propagation nodes on the network, or you
1197  # simply want to run a pageserving-only node,
1198  # you should disable running a propagation node.
1199  # Due to lots of propagation nodes being
1200  # available, this is currently the default.
1201  
1202  disable_propagation = Yes
1203  
1204  # For clients and other propagation nodes
1205  # delivering messages via this node, you can
1206  # configure the minimum required propagation
1207  # stamp costs. All messages delivered to the
1208  # propagation node network must have a valid
1209  # propagation stamp, or they will be rejected.
1210  # Clients automatically detect the stamp cost
1211  # for the node they are delivering to, and
1212  # compute a corresponding stamp before trying
1213  # to deliver the message to the propagation
1214  # node.
1215  #
1216  # Propagation stamps are easier to verify in
1217  # large batches, and therefore also somewhat
1218  # easier to compute for the senders. As such,
1219  # a reasonable propagation stamp cost should
1220  # be a bit higher than the normal peer-to-peer
1221  # stamp costs.
1222  #
1223  # Propagation stamps does not incur any extra
1224  # load for propagation nodes processing them,
1225  # since they are only required to verify that
1226  # they are correct, and only the generation
1227  # is computationally costly. Setting a sensible
1228  # propagation stamp cost (and periodically
1229  # checking the average network consensus) helps
1230  # keep spam and misuse out of the propagation
1231  # node network.
1232  
1233  propagation_cost = 16
1234  
1235  # The maximum amount of storage to use for
1236  # the LXMF Propagation Node message store,
1237  # specified in megabytes. When this limit
1238  # is reached, LXMF will periodically remove
1239  # messages in its message store. By default,
1240  # LXMF prioritises keeping messages that are
1241  # new and small. Large and old messages will
1242  # be removed first. This setting is optional
1243  # and defaults to 2 gigabytes.
1244  
1245  # message_storage_limit = 2000
1246  
1247  # The maximum accepted transfer size per in-
1248  # coming propagation message, in kilobytes.
1249  # This sets the upper limit for the size of
1250  # single messages accepted onto this node.
1251  
1252  max_transfer_size = 256
1253  
1254  # The maximum accepted transfer size per in-
1255  # coming propagation node sync.
1256  #
1257  # If a node wants to propagate a larger number
1258  # of messages to this node, than what can fit
1259  # within this limit, it will prioritise sending
1260  # the smallest messages first, and try again
1261  # with any remaining messages at a later point.
1262  
1263  max_sync_size = 10240
1264  
1265  # You can tell the LXMF message router to
1266  # prioritise storage for one or more
1267  # destinations. If the message store reaches
1268  # the specified limit, LXMF will prioritise
1269  # keeping messages for destinations specified
1270  # with this option. This setting is optional,
1271  # and generally you do not need to use it.
1272  
1273  # prioritise_destinations = 41d20c727598a3fbbdf9106133a3a0ed, d924b81822ca24e68e2effea99bcb8cf
1274  
1275  # You can configure the maximum number of other
1276  # propagation nodes that this node will peer
1277  # with automatically. The default is 20.
1278  
1279  # max_peers = 20
1280  
1281  # You can configure a list of static propagation
1282  # node peers, that this node will always be
1283  # peered with, by specifying a list of
1284  # destination hashes.
1285  
1286  # static_peers = e17f833c4ddf8890dd3a79a6fea8161d, 5a2d0029b6e5ec87020abaea0d746da4
1287  
1288  # You can specify the interval in minutes for
1289  # rescanning the hosted pages path. By default,
1290  # this option is disabled, and the pages path
1291  # will only be scanned on startup.
1292  
1293  # page_refresh_interval = 0
1294  
1295  # You can specify the interval in minutes for
1296  # rescanning the hosted files path. By default,
1297  # this option is disabled, and the files path
1298  # will only be scanned on startup.
1299  
1300  # file_refresh_interval = 0
1301  
1302  [printing]
1303  
1304  # You can configure Nomad Network to print
1305  # various kinds of information and messages.
1306  
1307  # Printing messages is disabled by default
1308  
1309  print_messages = No
1310  
1311  # You can configure a custom template for
1312  # message printing. If you uncomment this
1313  # option, set a path to the template and
1314  # restart Nomad Network, a default template
1315  # will be created that you can edit.
1316  
1317  # message_template = ~/.nomadnetwork/print_template_msg.txt
1318  
1319  # You can configure Nomad Network to only
1320  # print messages from trusted destinations.
1321  
1322  # print_from = trusted
1323  
1324  # Or specify the source LXMF addresses that
1325  # will automatically have messages printed
1326  # on arrival.
1327  
1328  # print_from = 76fe5751a56067d1e84eef3e88eab85b, 0e70b5848eb57c13154154feaeeb89b7
1329  
1330  # Or allow printing from anywhere, if you
1331  # are feeling brave and adventurous.
1332  
1333  # print_from = everywhere
1334  
1335  # You can configure the printing command.
1336  # This will use the default CUPS printer on
1337  # your system.
1338  
1339  print_command = lp
1340  
1341  # You can specify what printer to use
1342  # print_command = lp -d [PRINTER_NAME]
1343  
1344  # Or specify more advanced options. This
1345  # example works well for small thermal-
1346  # roll printers:
1347  # print_command = lp -d [PRINTER_NAME] -o cpi=16 -o lpi=8
1348  
1349  # This one is more suitable for full-sheet
1350  # printers. It will print a QR code at the center of any media
1351  # your printer will accept, print in portrait mode, and move the message to
1352  # the top of the print queue:
1353  # print_command = lp -d [PRINTER_NAME] -o job-priority=100 -o media=Custom.75x75mm -o orientation-requested=3
1354  
1355  # But you can modify the size to fit your needs.
1356  # The custom media option accepts millimeters, centimeters, and
1357  # inches in a width by length format like so:
1358  # -o media=Custom.[WIDTH]x[LENGTH][mm,cm,in]
1359  #
1360  # The job priority option accepts 1-100, though you can remove it
1361  # entirely if you aren't concerned with a print queue:
1362  # -o job-priority=[1-100]
1363  #
1364  # Finally, the orientation option allows for 90 degree rotations beginning with 3, so:
1365  # -o orientation-requested=4 (landscape, 90 degrees)
1366  # -o orientation-requested=5 (reverse portrait, 180 degrees)
1367  #
1368  # Here is the full command with the recommended customizable variables:
1369  # print_command = lp -d [PRINTER_NAME] -o job-priority=[N] -o media=[MEDIA_SIZE] -o orientation-requested=[N] -o sides=one-sided
1370  
1371  # For example, here's a configuration for USB thermal printer that uses the POS-58 PPD driver
1372  # with rolls 47.98x209.9mm in size:
1373  # print_command = lp -d [PRINTER_NAME] -o job-priority=100 -o media=custom_47.98x209.9mm_47.98x209.9mm -o sides=one-sided
1374  
1375  '''.splitlines()
1376  
1377  __printing_template_msg__ = """
1378  ---------------------------
1379  From: {origin}
1380  Sent: {stime}
1381  Rcvd: {rtime}
1382  Title: {mtitle}
1383  
1384  {mbody}
1385  ---------------------------
1386  """