diff --git a/meshterm/core/channels.py b/meshterm/core/channels.py index 239d711..2b70f16 100644 --- a/meshterm/core/channels.py +++ b/meshterm/core/channels.py @@ -35,17 +35,21 @@ from urllib.parse import parse_qs, quote, urlsplit # not one time for each candidate channel. Thus the reception path pays almost nothing. #: Matches the ``Name: message`` convention that channel senders use to identify themselves -#: (the protocol has no sender field). The name is 1–20 characters that are not colons, and +#: (the protocol has no sender field). The name is 1–31 characters that are not colons, and #: ``": "`` must follow it. This rule is strict enough that it does not match ``http://…`` -#: or ``note:x``. -SENDER_PREFIX = re.compile(r"^([^\s:][^:]{0,19}):[ \t]+(.*)$", re.DOTALL) +#: or ``note:x``. The firmware keeps a node name in 32 bytes with its terminator, so 31 is +#: the longest name that a sender can have. A limit of 20 left +#: a longer name in the body. Then the transcript showed no sender for it, and a reaction +#: from it did not read as a reaction. +SENDER_PREFIX = re.compile(r"^([^\s:][^:]{0,30}):[ \t]+(.*)$", re.DOTALL) #: Matches an ``@[Name]`` mention token, which the reply flow puts into the compose line in #: advance. A transcript renders each token as a bare ``@Name`` in the hue of that sender, #: and does not show the brackets. The history face of the CLI also takes out the same #: names. Thus the token is defined here, with the remainder of the message-body parsing, and -#: not in one of the two renderers. The name is 1–20 characters that are not ``]``. -MENTION = re.compile(r"@\[([^\]]{1,20})\]") +#: not in one of the two renderers. The name is 1–31 characters that are not ``]``, the +#: same limit as :data:`SENDER_PREFIX`. +MENTION = re.compile(r"@\[([^\]]{1,31})\]") def split_channel_sender(text: str) -> tuple[str | None, str]: diff --git a/tests/test_reactions.py b/tests/test_reactions.py index 45ce1e0..9b75e71 100644 --- a/tests/test_reactions.py +++ b/tests/test_reactions.py @@ -393,3 +393,18 @@ def test_the_messages_list_previews_a_reaction_in_words() -> None: assert "Bob" in preview and "reacted 👍" in preview and "@[" not in preview ours = ChatMessage(text=reactions.direct_text("😂", "hi", _T), outbound=True) assert "reacted 😂" in _preview_text(ours, lambda name: None).plain + + +def test_a_reaction_from_a_node_with_a_long_name_folds() -> None: + """A node name can have 31 characters. A sender of 22 is still split off its reaction. + + The two messages are from the hardware log that found the fault: the name limit was 20, + so the reaction of ``Hazelwood r0b3.Anduril`` showed as a new message. + """ + fold = ReactionFold(channel=True) + target = _channel("Hazelwood: ⚽ Union 2-1 at Chicago Fire (59')", 1_792_029_645) + fold.take(target) + react = _channel("Hazelwood r0b3.Anduril: @[Hazelwood]🔥\nmzjsjjpk", 1_792_029_654) + assert fold.take(react) is True + (badge,) = fold.badges(target) + assert badge.emoji == "🔥" and badge.reactors == [reactions.Reactor("Hazelwood r0b3.Anduril")]