Skip to content

Commit 47c855e

Browse files
committed
implement apply_to command that runs a command to a set of contacts specified with a filter
1 parent 04f2f3c commit 47c855e

File tree

2 files changed

+119
-10
lines changed

2 files changed

+119
-10
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "meshcore-cli"
7-
version = "1.2.5"
7+
version = "1.2.6"
88
authors = [
99
{ name="Florent de Lamotte", email="[email protected]" },
1010
]

src/meshcore_cli/meshcore_cli.py

Lines changed: 118 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from meshcore import MeshCore, EventType, logger
3434

3535
# Version
36-
VERSION = "v1.2.5"
36+
VERSION = "v1.2.6"
3737

3838
# default ble address is stored in a config file
3939
MCCLI_CONFIG_DIR = str(Path.home()) + "/.config/meshcore/"
@@ -210,7 +210,7 @@ async def handle_log_rx(event):
210210
pkt = bytes().fromhex(event.payload["payload"])
211211

212212
if handle_log_rx.channel_echoes:
213-
if pkt[0] == 0x15:
213+
if pkt[0] == 0x15:
214214
chan_name = ""
215215
path_len = pkt[1]
216216
path = pkt[2:path_len+2].hex()
@@ -235,7 +235,7 @@ async def handle_log_rx(event):
235235
aes_key = bytes.fromhex(channel["channel_secret"])
236236
cipher = AES.new(aes_key, AES.MODE_ECB)
237237
message = cipher.decrypt(msg)[5:].decode("utf-8").strip("\x00")
238-
238+
239239
if chan_name != "" :
240240
width = os.get_terminal_size().columns
241241
cars = width - 13 - 2 * path_len - len(chan_name) - 1
@@ -245,7 +245,7 @@ async def handle_log_rx(event):
245245
print_above(txt)
246246
else:
247247
print(txt)
248-
248+
249249
handle_log_rx.json_log_rx = False
250250
handle_log_rx.channel_echoes = False
251251
handle_log_rx.mc = None
@@ -366,7 +366,7 @@ async def subscribe_to_msgs(mc, json_output=False, above=False):
366366
class MyNestedCompleter(NestedCompleter):
367367
def get_completions( self, document, complete_event):
368368
txt = document.text_before_cursor.lstrip()
369-
if not " " in txt:
369+
if not " " in txt:
370370
if txt != "" and txt[0] == "/" and txt.count("/") == 1:
371371
opts = []
372372
for k in self.options.keys():
@@ -465,6 +465,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
465465
"set_channel": None,
466466
"get_channels": None,
467467
"remove_channel": None,
468+
"apply_to": None,
469+
"at": None,
468470
"set" : {
469471
"name" : None,
470472
"pin" : None,
@@ -531,6 +533,8 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
531533

532534
contact_completion_list = {
533535
"contact_info": None,
536+
"contact_name": None,
537+
"contact_lastmod": None,
534538
"export_contact" : None,
535539
"share_contact" : None,
536540
"upload_contact" : None,
@@ -652,7 +656,7 @@ def make_completion_dict(contacts, pending={}, to=None, channels=None):
652656
slash_root_completion_list = {}
653657
for k,v in root_completion_list.items():
654658
slash_root_completion_list["/"+k]=v
655-
659+
656660
completion_list.update(slash_root_completion_list)
657661

658662
slash_contacts_completion_list = {}
@@ -905,6 +909,13 @@ def _(event):
905909
if last_ack == False :
906910
contact = ln
907911

912+
elif contact is None and\
913+
(line.startswith("apply_to ") or line.startswith("at ")):
914+
try:
915+
await apply_command_to_contacts(mc, line.split(" ",2)[1], line.split(" ",2)[2])
916+
except IndexError:
917+
logger.error(f"Error with apply_to command parameters")
918+
908919
# commands are passed through if at root
909920
elif contact is None or line.startswith(".") :
910921
try:
@@ -966,6 +977,23 @@ async def process_contact_chat_line(mc, contact, line):
966977
await process_cmds(mc, args)
967978
return True
968979

980+
if line.startswith("contact_name") or line.startswith("cn"):
981+
print(contact['adv_name'],end="")
982+
if " " in line:
983+
print(" ", end="")
984+
secline = line.split(" ", 1)[1]
985+
await process_contact_chat_line(mc, contact, secline)
986+
else:
987+
print("")
988+
return True
989+
990+
if line == "contact_lastmod":
991+
timestamp = contact["lastmod"]
992+
print(f"{contact['adv_name']} updated"
993+
f" {datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d at %H:%M:%S')}"
994+
f" ({timestamp})")
995+
return True
996+
969997
# commands that take contact as second arg will be sent to recipient
970998
if line == "sc" or line == "share_contact" or\
971999
line == "ec" or line == "export_contact" or\
@@ -1053,7 +1081,7 @@ async def process_contact_chat_line(mc, contact, line):
10531081
return True
10541082

10551083
# same but for commands with a parameter
1056-
if line.startswith("cmd ") or\
1084+
if line.startswith("cmd ") or line.startswith("msg ") or\
10571085
line.startswith("cp ") or line.startswith("change_path ") or\
10581086
line.startswith("cf ") or line.startswith("change_flags ") or\
10591087
line.startswith("req_binary ") or\
@@ -1085,7 +1113,7 @@ async def process_contact_chat_line(mc, contact, line):
10851113

10861114
if password == "":
10871115
try:
1088-
sess = PromptSession("Password: ", is_password=True)
1116+
sess = PromptSession(f"Password for {contact['adv_name']}: ", is_password=True)
10891117
password = await sess.prompt_async()
10901118
except EOFError:
10911119
logger.info("Canceled")
@@ -1124,6 +1152,87 @@ async def process_contact_chat_line(mc, contact, line):
11241152

11251153
return False
11261154

1155+
async def apply_command_to_contacts(mc, contact_filter, line):
1156+
upd_before = None
1157+
upd_after = None
1158+
contact_type = None
1159+
min_hops = None
1160+
max_hops = None
1161+
1162+
filters = contact_filter.split(",")
1163+
for f in filters:
1164+
if f == "all":
1165+
pass
1166+
elif f[0] == "u": #updated
1167+
val_str = f[2:]
1168+
t = time.time()
1169+
if val_str[-1] == "d": # value in days
1170+
t = t - float(val_str[0:-1]) * 86400
1171+
elif val_str[-1] == "h": # value in hours
1172+
t = t - float(val_str[0:-1]) * 3600
1173+
elif val_str[-1] == "m": # value in minutes
1174+
t = t - float(val_str[0:-1]) * 60
1175+
else:
1176+
t = int(val_str)
1177+
if f[1] == "<": #before
1178+
upd_before = t
1179+
elif f[1] == ">":
1180+
upd_after = t
1181+
else:
1182+
logger.error(f"Time filter can only be < or >")
1183+
return
1184+
elif f[0] == "t": # type
1185+
if f[1] == "=":
1186+
contact_type = int(f[2:])
1187+
else:
1188+
logger.error(f"Type can only be equals to a value")
1189+
return
1190+
elif f[0] == "d": # direct
1191+
min_hops=0
1192+
elif f[0] == "f": # flood
1193+
max_hops=-1
1194+
elif f[0] == "h": # hop number
1195+
if f[1] == ">":
1196+
min_hops = int(f[2:])+1
1197+
elif f[1] == "<":
1198+
max_hops = int(f[2:])-1
1199+
elif f[1] == "=":
1200+
min_hops = int(f[2:])
1201+
max_hops = int(f[2:])
1202+
else:
1203+
logger.error(f"Unknown filter {f}")
1204+
return
1205+
1206+
for c in dict(mc._contacts).items():
1207+
contact = c[1]
1208+
if (contact_type is None or contact["type"] == contact_type) and\
1209+
(upd_before is None or contact["lastmod"] < upd_before) and\
1210+
(upd_after is None or contact["lastmod"] > upd_after) and\
1211+
(min_hops is None or contact["out_path_len"] >= min_hops) and\
1212+
(max_hops is None or contact["out_path_len"] <= max_hops):
1213+
if await process_contact_chat_line(mc, contact, line):
1214+
pass
1215+
1216+
elif line == "remove_contact":
1217+
args = [line, contact['adv_name']]
1218+
await process_cmds(mc, args)
1219+
1220+
elif line.startswith("send") or line.startswith("\"") :
1221+
if line.startswith("send") :
1222+
line = line[5:]
1223+
if line.startswith("\"") :
1224+
line = line[1:]
1225+
await msg_ack(mc, contact, line)
1226+
1227+
elif contact["type"] == 2 or\
1228+
contact["type"] == 3 or\
1229+
contact["type"] == 4 : # repeater, room, sensor send cmd
1230+
await process_cmds(mc, ["cmd", contact["adv_name"], line])
1231+
# wait for a reply from cmd
1232+
await mc.wait_for_event(EventType.MESSAGES_WAITING)
1233+
1234+
else:
1235+
logger.error(f"Can't send {line} to {contact['adv_name']}")
11271236

11281237
async def send_cmd (mc, contact, cmd) :
11291238
res = await mc.commands.send_cmd(contact, cmd)
@@ -2718,7 +2827,7 @@ async def next_cmd(mc, cmds, json_output=False):
27182827
await mc.ensure_contacts()
27192828
contact = mc.get_contact_by_name(cmds[0])
27202829
if contact is None:
2721-
logger.error(f"Unknown command : {cmd}. {cmds} not executed ...")
2830+
logger.error(f"Unknown command : {cmd}, {cmds} not executed ...")
27222831
return None
27232832

27242833
await interactive_loop(mc, to=contact)

0 commit comments

Comments
 (0)