@@ -380,6 +380,46 @@ def getURL(self, includeAll: bool = True):
380380 s = s .replace ("=" , "" ).replace ("+" , "-" ).replace ("/" , "_" )
381381 return f"https://meshtastic.org/e/#{ s } "
382382
383+ def getContactURL (self , node_id : Union [int , str ], should_ignore : bool = False , manually_verified : bool = False ):
384+ """Generate a shareable contact URL for the specified node"""
385+ nodeNum = to_node_num (node_id )
386+
387+ node = self .iface .nodesByNum .get (nodeNum )
388+ if not node or not node .get ("user" ):
389+ our_exit (f"Warning: Node { node_id } not found in NodeDB" )
390+
391+ contact = admin_pb2 .SharedContact ()
392+ contact .node_num = nodeNum
393+
394+ u = node ["user" ]
395+ if u .get ("id" ):
396+ contact .user .id = u ["id" ]
397+ if u .get ("macaddr" ):
398+ contact .user .macaddr = base64 .b64decode (u ["macaddr" ])
399+ if u .get ("longName" ):
400+ contact .user .long_name = u ["longName" ]
401+ if u .get ("shortName" ):
402+ contact .user .short_name = u ["shortName" ]
403+ if u .get ("hwModel" ) and u ["hwModel" ] != "UNSET" :
404+ contact .user .hw_model = mesh_pb2 .HardwareModel .Value (u ["hwModel" ])
405+ if u .get ("role" ):
406+ contact .user .role = config_pb2 .Config .DeviceConfig .Role .Value (u ["role" ])
407+ if u .get ("publicKey" ):
408+ contact .user .public_key = base64 .b64decode (u ["publicKey" ])
409+ if u .get ("isLicensed" ):
410+ contact .user .is_licensed = u ["isLicensed" ]
411+ if u .get ("isUnmessagable" ) is not None :
412+ contact .user .is_unmessagable = u ["isUnmessagable" ]
413+ if should_ignore :
414+ contact .should_ignore = True
415+ if manually_verified :
416+ contact .manually_verified = True
417+
418+ data = contact .SerializeToString ()
419+ s = base64 .urlsafe_b64encode (data ).decode ("ascii" )
420+ s = s .replace ("=" , "" ).replace ("+" , "-" ).replace ("/" , "_" )
421+ return f"https://meshtastic.org/v/#{ s } "
422+
383423 def setURL (self , url : str , addOnly : bool = False ):
384424 """Set mesh network URL"""
385425 if self .localConfig is None or self .channels is None :
@@ -445,6 +485,32 @@ def setURL(self, url: str, addOnly: bool = False):
445485 self .ensureSessionKey ()
446486 self ._sendAdmin (p )
447487
488+ def addContactURL (self , url : str ):
489+ """Add a contact (User) to the NodeDB from a shareable URL"""
490+ self .ensureSessionKey ()
491+
492+ splitURL = url .split ("/#" )
493+ if len (splitURL ) == 1 :
494+ our_exit (f"Warning: Invalid URL '{ url } '" )
495+ b64 = splitURL [- 1 ]
496+
497+ missing_padding = len (b64 ) % 4
498+ if missing_padding :
499+ b64 += "=" * (4 - missing_padding )
500+
501+ decodedURL = base64 .urlsafe_b64decode (b64 )
502+ contact = admin_pb2 .SharedContact ()
503+ contact .ParseFromString (decodedURL )
504+
505+ p = admin_pb2 .AdminMessage ()
506+ p .add_contact .CopyFrom (contact )
507+
508+ if self == self .iface .localNode :
509+ onResponse = None
510+ else :
511+ onResponse = self .onAckNak
512+ return self ._sendAdmin (p , onResponse = onResponse )
513+
448514 def onResponseRequestRingtone (self , p ):
449515 """Handle the response packet for requesting ringtone part 1"""
450516 logger .debug (f"onResponseRequestRingtone() p:{ p } " )
0 commit comments