Files
meshcore-bot/modules/response_template.py
T
agessaman d2b83481f7 feat(placeholders): expose {packet_hash} through the placeholder service
Adds the 16-char MeshCore packet identity hash to the three formatters that
render user-facing templates: [Keywords] responses, the test command's
response_format, and the path command's reply_prefix. It is read only from
routing_info, which is attached solely from an RF packet correlated to the
message, so a hash from an unrelated transmission is never shown. Missing,
empty, and the 0000000000000000 error sentinel all render empty.

get_packet_hash_placeholder returns "" rather than a False sentinel. Empty
gives byte-identical output on every path—prefix_if_nonempty already tests
falsiness and str.format renders "" as nothing—whereas False would print
the literal string "False" into a mesh reply from any consumer of
get_standard_placeholder_fields that formatted it directly.

TestCommand.format_response now builds on get_standard_placeholder_fields
instead of duplicating it. That also honors the extra argument its docstring
already promised but silently ignored, and picks up the path-string hop
fallback, so {hops} reads "0" rather than "?" for a message whose hops and
routing_info are both unset but whose path says "Direct (0 hops)".

Documents the placeholder in config.ini.example, the default config that
core.py writes on a fresh install, the README and docs/path-command-config.md.
The generated config was also missing {elapsed}; added that too.
2026-08-25 09:16:09 -07:00

129 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Piped placeholders for command response templates (feed-style ``{field|filter:args}``).
Used by :class:`~modules.commands.test_command.TestCommand` and extensible for other
commands. Same brace limitation as feed formatting: no nested ``{}`` inside a placeholder.
"""
from __future__ import annotations
import re
from typing import Any, Callable
from .utils import message_path_bytes_per_hop
FilterFn = Callable[[str, dict[str, Any], str], str]
def _filter_pathbytes_min(value: str, ctx: dict[str, Any], args: str) -> str:
"""Clear *value* unless message path uses at least *N* bytes per hop (N in 1..3)."""
message = ctx.get('message')
if message is None:
return ''
try:
n = int(args.strip())
except ValueError:
return value
if n < 1 or n > 3:
return value
prefix_hex = int(ctx.get('prefix_hex_chars') or 2)
bph = message_path_bytes_per_hop(message, prefix_hex_chars=prefix_hex)
if bph < n:
return ''
return value
def _filter_prefix_if_nonempty(value: str, ctx: dict[str, Any], args: str) -> str:
"""Prepend *args* literal to *value* only when *value* is non-empty after prior filters."""
if not value:
return ''
return args + value
RESPONSE_TEMPLATE_FILTERS: dict[str, FilterFn] = {
'pathbytes_min': _filter_pathbytes_min,
'pathbytes': _filter_pathbytes_min,
'prefix_if_nonempty': _filter_prefix_if_nonempty,
}
def _field_and_filter_specs(inner: str) -> tuple[str, list[tuple[str, str]]]:
"""Split ``inner`` into field name and ``(filter_name, args)`` pairs.
Pipe ``|`` separates filters. ``prefix_if_nonempty`` is special: its argument may
contain ``|`` (e.g. `` | Path Dist: ``), so once that filter is reached we merge
all remaining segments and treat the rest as its args. ``prefix_if_nonempty`` must
be last in the chain if the literal includes a pipe.
"""
raw_parts = inner.split('|')
field_name = raw_parts[0].strip()
if len(raw_parts) < 2:
return field_name, []
specs: list[tuple[str, str]] = []
i = 1
while i < len(raw_parts):
if raw_parts[i].lstrip().startswith('prefix_if_nonempty'):
merged = '|'.join(raw_parts[i:])
if re.match(r'^\s*prefix_if_nonempty\s*$', merged):
specs.append(('prefix_if_nonempty', ''))
break
m = re.match(r'^\s*prefix_if_nonempty\s*:(.*)$', merged, flags=re.DOTALL)
if m:
specs.append(('prefix_if_nonempty', m.group(1)))
else:
specs.append(('prefix_if_nonempty', ''))
break
segment = raw_parts[i].strip()
name, sep, arg = segment.partition(':')
name = name.strip()
arg = arg if sep else ''
specs.append((name, arg))
i += 1
return field_name, specs
def format_piped_template(
template: str,
fields: dict[str, Any],
*,
message: Any = None,
logger: Any = None,
prefix_hex_chars: int = 2,
) -> str:
"""Replace ``{field}`` and ``{field|filter:arg|...}`` using *fields* and optional *message*.
Args:
template: Raw template string from config.
fields: Mapping of placeholder names to values (e.g. ``sender``, ``path_distance``).
An unavailable field is an empty string, which renders as nothing and
lets ``prefix_if_nonempty`` drop its literal label too.
message: Triggering mesh message; required for ``pathbytes`` / ``pathbytes_min`` filters.
logger: Optional logger for unknown filter warnings.
prefix_hex_chars: Bot prefix width for inferring bytes per hop from legacy path text.
Returns:
Fully expanded string.
"""
ctx: dict[str, Any] = {
'message': message,
'logger': logger,
'prefix_hex_chars': prefix_hex_chars,
}
def replace_placeholder(match: re.Match[str]) -> str:
inner_raw = match.group(1)
if '|' not in inner_raw:
return str(fields.get(inner_raw.strip(), ''))
field_name, filter_specs = _field_and_filter_specs(inner_raw)
value = str(fields.get(field_name, ''))
for name, arg in filter_specs:
fn = RESPONSE_TEMPLATE_FILTERS.get(name)
if fn is None:
if logger is not None:
logger.warning(f"Unknown response template filter {name!r} in {{{inner_raw}}}")
continue
value = fn(value, ctx, arg)
return value
return re.sub(r"\{([^}]+)\}", replace_placeholder, template)