mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-09-02 00:58:37 +00:00
{firstlast_distance|prefix_if_nonempty: | F/L Dist: } prints "| F/L Dist: N/A"
on a direct message: the distance helpers return the literal "N/A" when there
is no path to measure, prefix_if_nonempty only asks whether the value is
non-empty, and "N/A" is. Suppressing that needed a gate, and pathbytes_min was
the only one available—so it was being used as a stand-in for "did this
message take any hops", which is not what it asks. It asks how the path is
encoded, so pathbytes_min:2 also discards a one-byte multi-hop path whose
distance is real and measurable.
hops_min:N asks about the route itself. hops_min:1 drops a clause on a direct
message and nothing else:
hops_min:1 pathbytes_min:2
direct cleared cleared
1-byte 2 hops kept cleared <- the difference
2-byte 2 hops kept kept
unknown cleared cleared
An unknown hop count clears the value: a gate that cannot confirm the route
should suppress rather than guess, matching pathbytes_min.
The hop-count logic moves to utils.message_hop_count so the filter and
BaseCommand.get_hops_display_values share one implementation instead of two.
It returns None for unknown rather than 0, since a gate must not read
"cannot tell" as "direct".
157 lines
5.5 KiB
Python
157 lines
5.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_hop_count, 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_hops_min(value: str, ctx: dict[str, Any], args: str) -> str:
|
|
"""Clear *value* unless the message travelled at least *N* hops.
|
|
|
|
Asks about the route rather than how it is encoded, which is what separates
|
|
this from ``pathbytes_min``: a one-byte multi-hop path has a real, measurable
|
|
distance, and ``pathbytes_min:2`` would throw it away along with the direct
|
|
messages it was aimed at. ``hops_min:1`` is the way to drop a clause on a
|
|
direct message and nothing else.
|
|
|
|
An unknown hop count clears the value: a gate that cannot confirm the route
|
|
should suppress rather than guess, matching ``pathbytes_min``.
|
|
"""
|
|
message = ctx.get('message')
|
|
if message is None:
|
|
return ''
|
|
try:
|
|
n = int(args.strip())
|
|
except ValueError:
|
|
return value
|
|
if n < 0:
|
|
return value
|
|
hops = message_hop_count(message)
|
|
if hops is None or hops < 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,
|
|
'hops_min': _filter_hops_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)
|