meshcore-cli 1.2.1__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcore-cli
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Command line interface to meshcore companion radios
5
5
  Project-URL: Homepage, https://github.com/fdlamotte/meshcore-cli
6
6
  Project-URL: Issues, https://github.com/fdlamotte/meshcore-cli/issues
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
13
  Requires-Dist: meshcore>=2.1.19
14
14
  Requires-Dist: prompt-toolkit>=3.0.50
15
+ Requires-Dist: pycryptodome
15
16
  Requires-Dist: requests>=2.28.0
16
17
  Description-Content-Type: text/markdown
17
18
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meshcore-cli"
7
- version = "1.2.1"
7
+ version = "1.2.2"
8
8
  authors = [
9
9
  { name="Florent de Lamotte", email="florent@frizoncorrea.fr" },
10
10
  ]
@@ -17,7 +17,7 @@ classifiers = [
17
17
  ]
18
18
  license = "MIT"
19
19
  license-files = ["LICEN[CS]E*"]
20
- dependencies = [ "meshcore >= 2.1.19", "prompt_toolkit >= 3.0.50", "requests >= 2.28.0" ]
20
+ dependencies = [ "meshcore >= 2.1.19", "prompt_toolkit >= 3.0.50", "requests >= 2.28.0", "pycryptodome" ]
21
21
 
22
22
  [project.urls]
23
23
  Homepage = "https://github.com/fdlamotte/meshcore-cli"
@@ -24,13 +24,15 @@ from prompt_toolkit.key_binding import KeyBindings
24
24
  from prompt_toolkit.shortcuts import radiolist_dialog
25
25
  from prompt_toolkit.completion.word_completer import WordCompleter
26
26
  from prompt_toolkit.document import Document
27
+ from hashlib import sha256
28
+ from Crypto.Cipher import AES
27
29
 
28
30
  import re
29
31
 
30
32
  from meshcore import MeshCore, EventType, logger
31
33
 
32
34
  # Version
33
- VERSION = "v1.2.1"
35
+ VERSION = "v1.2.2"
34
36
 
35
37
  # default ble address is stored in a config file
36
38
  MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -193,6 +195,41 @@ process_event_message.print_snr=False
193
195
  process_event_message.color=True
194
196
  process_event_message.last_node=None
195
197
 
198
+ async def handle_log_rx(event):
199
+ mc = handle_log_rx.mc
200
+ if handle_log_rx.json_log_rx: # json mode ... raw dump
201
+ msg = json.dumps(event.payload)
202
+ if handle_message.above:
203
+ print_above(msg)
204
+ else :
205
+ print(msg)
206
+ return
207
+
208
+ pkt = bytes().fromhex(event.payload["payload"])
209
+
210
+ if handle_log_rx.log_channels:
211
+ if pkt[0] == 0x15:
212
+ path_len = pkt[1]
213
+ path = pkt[2:path_len+2].hex()
214
+ chan_hash = pkt[path_len+2:path_len+3].hex()
215
+ cipher_mac = int.from_bytes(pkt[path_len+3:path_len+5], byteorder="little")
216
+ msg = pkt[path_len+5:]
217
+ channel = await get_channel_by_hash(mc, chan_hash)
218
+ if channel is None :
219
+ chan_name = chan_hash
220
+ message = msg.hex()
221
+ else:
222
+ chan_name = channel["channel_name"]
223
+ aes_key = bytes.fromhex(channel["channel_secret"])
224
+ cipher = AES.new(aes_key, AES.MODE_ECB)
225
+ message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
226
+
227
+ print_above(f"{ANSI_LIGHT_GRAY}{chan_name:>10} {ANSI_GREEN}{message[0:25]:25} {ANSI_LIGHT_GRAY}({event.payload['snr']:6,.2f},{event.payload['rssi']:4}){ANSI_YELLOW} [{path}]{ANSI_END}")
228
+
229
+ handle_log_rx.json_log_rx = False
230
+ handle_log_rx.log_channels = False
231
+ handle_log_rx.mc = None
232
+
196
233
  async def handle_advert(event):
197
234
  if not handle_advert.print_adverts:
198
235
  return
@@ -320,7 +357,7 @@ class MyNestedCompleter(NestedCompleter):
320
357
  opts = self.options.keys()
321
358
  completer = WordCompleter(
322
359
  opts, ignore_case=self.ignore_case,
323
- pattern=re.compile(r"([a-zA-Z0-9_\\/]+|[^a-zA-Z0-9_\s]+)"))
360
+ pattern=re.compile(r"([a-zA-Z0-9_\\/\#]+|[^a-zA-Z0-9_\s\#]+)"))
324
361
  yield from completer.get_completions(document, complete_event)
325
362
  else: # normal behavior for remainder
326
363
  yield from super().get_completions(document, complete_event)
@@ -417,6 +454,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
417
454
  "color" : {"on":None, "off":None},
418
455
  "print_name" : {"on":None, "off":None},
419
456
  "print_adverts" : {"on":None, "off":None},
457
+ "json_log_rx" : {"on":None, "off":None},
458
+ "log_channels" : {"on":None, "off":None},
420
459
  "print_new_contacts" : {"on": None, "off":None},
421
460
  "print_path_updates" : {"on":None,"off":None},
422
461
  "classic_prompt" : {"on" : None, "off":None},
@@ -444,6 +483,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
444
483
  "color":None,
445
484
  "print_name":None,
446
485
  "print_adverts":None,
486
+ "json_log_rx":None,
487
+ "log_channels":None,
447
488
  "print_path_updates":None,
448
489
  "print_new_contacts":None,
449
490
  "classic_prompt":None,
@@ -604,6 +645,14 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
604
645
 
605
646
  completion_list.update(slash_contacts_completion_list)
606
647
 
648
+ slash_chan_completion_list = {}
649
+ if not channels is None:
650
+ for c in channels :
651
+ if c["channel_name"] != "":
652
+ slash_chan_completion_list["/" + c["channel_name"]] = None
653
+
654
+ completion_list.update(slash_chan_completion_list)
655
+
607
656
  completion_list.update({
608
657
  "script" : None,
609
658
  "quit" : None
@@ -675,7 +724,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
675
724
  if classic :
676
725
  prompt = prompt + " > "
677
726
  else :
678
- prompt = prompt + "🭨"
727
+ prompt = prompt + f"{ANSI_NORMAL}🭬{ANSI_INVERT}"
679
728
 
680
729
  if not contact is None :
681
730
  if not last_ack:
@@ -696,7 +745,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
696
745
  prompt = prompt + f"{ANSI_INVERT}"
697
746
 
698
747
  if print_name and not classic :
699
- prompt = prompt + "🭬"
748
+ prompt = prompt + f"{ANSI_NORMAL}🭨{ANSI_INVERT}"
700
749
 
701
750
  prompt = prompt + f"{contact['adv_name']}"
702
751
  if classic :
@@ -734,8 +783,19 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
734
783
  elif line.startswith("/") :
735
784
  path = line.split(" ", 1)[0]
736
785
  if path.count("/") == 1:
737
- args = shlex.split(line[1:])
738
- await process_cmds(mc, args)
786
+ args = line[1:].split(" ")
787
+ tct = mc.get_contact_by_name(args[0])
788
+ if len(args)>1 and not tct is None: # a contact, send a message
789
+ if tct["type"] == 1 or tct["type"] == 3: # client or room
790
+ last_ack = await msg_ack(mc, tct, line.split(" ", 1)[1])
791
+ else:
792
+ print("Can only send msg to chan, client or room")
793
+ else :
794
+ ch = await get_channel_by_name(mc, args[0])
795
+ if len(args)>1 and not ch is None: # a channel, send message
796
+ await send_chan_msg(mc, ch["channel_idx"], line.split(" ", 1)[1])
797
+ else :
798
+ await process_cmds(mc, shlex.split(line[1:]))
739
799
  else:
740
800
  cmdline = line[1:].split("/",1)[1]
741
801
  contact_name = path[1:].split("/",1)[0]
@@ -744,10 +804,11 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
744
804
  print(f"{contact_name} is not a contact")
745
805
  else:
746
806
  if not await process_contact_chat_line(mc, tct, cmdline):
747
- if tct["type"] == 1:
748
- last_ack = await msg_ack(mc, tct, cmdline)
749
- else :
750
- await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
807
+ if cmdline != "":
808
+ if tct["type"] == 1:
809
+ last_ack = await msg_ack(mc, tct, cmdline)
810
+ else :
811
+ await process_cmds(mc, ["cmd", tct["adv_name"], cmdline])
751
812
 
752
813
  elif line.startswith("to ") : # dest
753
814
  dest = line[3:]
@@ -805,7 +866,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
805
866
  if ln is None :
806
867
  print("No received msg yet !")
807
868
  elif ln["type"] == 0 :
808
- await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
869
+ await send_chan_msg(mc, ln["chan_nb"], line[1:])
809
870
  else :
810
871
  last_ack = await msg_ack(mc, ln, line[1:])
811
872
  if last_ack == False :
@@ -837,7 +898,7 @@ Line starting with \"$\" or \".\" will issue a meshcli command.
837
898
  last_ack = await msg_ack(mc, contact, line)
838
899
 
839
900
  elif contact["type"] == 0 : # channel, send msg to channel
840
- await process_cmds(mc, ["chan", str(contact["chan_nb"]), line] )
901
+ await send_chan_msg(mc, contact["chan_nb"], line)
841
902
 
842
903
  elif contact["type"] == 1 : # chat, send to recipient and wait ack
843
904
  last_ack = await msg_ack(mc, contact, line)
@@ -970,7 +1031,18 @@ async def process_contact_chat_line(mc, contact, line):
970
1031
  password_file = ""
971
1032
  password = ""
972
1033
  if os.path.isdir(MCCLI_CONFIG_DIR) :
1034
+ # if a password file exists with node name open it and destroy it
973
1035
  password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
1036
+ if os.path.exists(password_file) :
1037
+ with open(password_file, "r", encoding="utf-8") as f :
1038
+ password=f.readline().strip()
1039
+ os.remove(password_file)
1040
+ password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
1041
+ with open(password_file, "w", encoding="utf-8") as f :
1042
+ f.write(password)
1043
+
1044
+ # this is the new correct password file, using pubkey
1045
+ password_file = MCCLI_CONFIG_DIR + contact["public_key"] + ".pass"
974
1046
  if os.path.exists(password_file) :
975
1047
  with open(password_file, "r", encoding="utf-8") as f :
976
1048
  password=f.readline().strip()
@@ -993,6 +1065,9 @@ async def process_contact_chat_line(mc, contact, line):
993
1065
 
994
1066
  if line.startswith("forget_password") or line.startswith("fp"):
995
1067
  password_file = MCCLI_CONFIG_DIR + contact['adv_name'] + ".pass"
1068
+ if os.path.exists(password_file):
1069
+ os.remove(password_file)
1070
+ password_file = MCCLI_CONFIG_DIR + contact['public_key'] + ".pass"
996
1071
  if os.path.exists(password_file):
997
1072
  os.remove(password_file)
998
1073
  try:
@@ -1112,6 +1187,7 @@ async def set_channel (mc, chan, name, key=None):
1112
1187
  return None
1113
1188
 
1114
1189
  info = res.payload
1190
+ info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
1115
1191
  info["channel_secret"] = info["channel_secret"].hex()
1116
1192
 
1117
1193
  if hasattr(mc,'channels') :
@@ -1129,6 +1205,16 @@ async def get_channel_by_name (mc, name):
1129
1205
 
1130
1206
  return None
1131
1207
 
1208
+ async def get_channel_by_hash (mc, hash):
1209
+ if not hasattr(mc, 'channels') :
1210
+ await_get_channels(mc)
1211
+
1212
+ for c in mc.channels:
1213
+ if c['channel_hash'] == hash:
1214
+ return c
1215
+
1216
+ return None
1217
+
1132
1218
  async def get_contacts (mc, anim=False, lastomod=0, timeout=5) :
1133
1219
  if mc._contacts:
1134
1220
  return
@@ -1200,6 +1286,7 @@ async def get_channels (mc, anim=False) :
1200
1286
  if res.type == EventType.ERROR:
1201
1287
  break
1202
1288
  info = res.payload
1289
+ info["channel_hash"] = sha256(info["channel_secret"]).digest()[0:1].hex()
1203
1290
  info["channel_secret"] = info["channel_secret"].hex()
1204
1291
  mc.channels.append(info)
1205
1292
  ch = ch + 1
@@ -1411,6 +1498,14 @@ async def next_cmd(mc, cmds, json_output=False):
1411
1498
  process_event_message.print_snr = (cmds[2] == "on")
1412
1499
  if json_output :
1413
1500
  print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1501
+ case "json_log_rx" :
1502
+ handle_log_rx.json_log_rx = (cmds[2] == "on")
1503
+ if json_output :
1504
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1505
+ case "log_channels" :
1506
+ handle_log_rx.log_channels = (cmds[2] == "on")
1507
+ if json_output :
1508
+ print(json.dumps({"cmd" : cmds[1], "param" : cmds[2]}))
1414
1509
  case "print_adverts" :
1415
1510
  handle_advert.print_adverts = (cmds[2] == "on")
1416
1511
  if json_output :
@@ -1641,6 +1736,16 @@ async def next_cmd(mc, cmds, json_output=False):
1641
1736
  print(json.dumps({"color" : process_event_message.color}))
1642
1737
  else:
1643
1738
  print(f"{'on' if process_event_message.color else 'off'}")
1739
+ case "json_log_rx":
1740
+ if json_output :
1741
+ print(json.dumps({"json_log_rx" : handle_log_rx.json_log_rx}))
1742
+ else:
1743
+ print(f"{'on' if handle_log_rx.json_log_rx else 'off'}")
1744
+ case "log_channels":
1745
+ if json_output :
1746
+ print(json.dumps({"log_channels" : handle_log_rx.log_channels}))
1747
+ else:
1748
+ print(f"{'on' if handle_log_rx.log_channels else 'off'}")
1644
1749
  case "print_adverts":
1645
1750
  if json_output :
1646
1751
  print(json.dumps({"print_adverts" : handle_advert.print_adverts}))
@@ -2881,10 +2986,12 @@ async def main(argv):
2881
2986
  handle_message.mc = mc # connect meshcore to handle_message
2882
2987
  handle_advert.mc = mc
2883
2988
  handle_path_update.mc = mc
2989
+ handle_log_rx.mc = mc
2884
2990
 
2885
2991
  mc.subscribe(EventType.ADVERTISEMENT, handle_advert)
2886
2992
  mc.subscribe(EventType.PATH_UPDATE, handle_path_update)
2887
2993
  mc.subscribe(EventType.NEW_CONTACT, handle_new_contact)
2994
+ mc.subscribe(EventType.RX_LOG_DATA, handle_log_rx)
2888
2995
 
2889
2996
  mc.auto_update_contacts = True
2890
2997
 
File without changes
File without changes
File without changes
File without changes
File without changes