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 """