New incoming messages
An incoming message is pushed by signal-cli-rest-api over the websocket, parsed by signalbot into a typed object and dispatched to a handler (see Architecture). Adding support for a new one touches a small, predictable set of files.
The steps below are not exhaustive — some message types need extra plumbing (e.g.
GroupUpdate also touches the group registry in
src/signalbot/groups/) —
so find the closest existing example and follow its shape.
Naming¶
Every incoming type follows the same name chain from the wire to the handler. Match it when
adding a new one — Xxx (wrapped class) → XxxHandler → XxxContext → handle_xxx
(snake_case of Xxx):
| Generated class | Wrapped class | Handler ABC | Context class | Handler method |
|---|---|---|---|---|
DataMessage |
DataMessage |
DataMessageHandler |
DataMessageContext |
handle_data_message |
Reaction |
Reaction |
ReactionHandler |
ReactionContext |
handle_reaction |
RemoteDelete |
RemoteDelete |
RemoteDeleteHandler |
RemoteDeleteContext |
handle_remote_delete |
TypingMessage |
TypingMessage |
TypingHandler |
TypingContext |
handle_typing |
GroupInfo |
GroupUpdate |
GroupUpdateHandler |
GroupUpdateContext |
handle_group_update |
Steps¶
-
Get a message envelope. Set a breakpoint in
_parse_main_messages,_parse_sync_messages, or_parse_data_message_variantinsrc/signalbot/messages/parser.py— i.e. on the already-validatedmessage_envelope: MessageEnvelopeobject. Trigger the message from another Signal client so that it is received by the bot. -
Identify the relevant envelope fields. Check whether the fields exists and are already covered by the generated
_generated.receivemodels (src/signalbot/_generated/receive/). Decide whether this is a genuinely new type or a variant ofdata_message/sync_message— compare with_parse_data_message_variantinparser.py, which already branches on thereaction/remote_delete/group-update sub-fields of a data message. If a field is missing from the generated models, it is missing from upstream — get it added there and regenerate the models, as described in New signal-cli-rest-api version. -
Write the parsed message class with a
from_message_envelope(...)classmethod, following an existing example —src/signalbot/messages/typing_message.pyfor a simple case, orsrc/signalbot/reactions/reaction.pyfor one that reaches back into the envelope's nested data. SubclassBaseMessageorBaseMessageWithGroupas appropriate. -
Wrap any nested
_generatedtypes your class exposes. Nosignalbot._generatedtype may appear on the public API, directly or nested inside a field —tests/unit/test_public_api_surface.pywill fail your PR if you skip it. Follow the rules in Wrapped types for each generated type your class reaches. -
Wire it into the parser. Add a branch in
_parse_main_messagesand/or_parse_sync_messagesinparser.py, and extend theReceivedMessagetype alias. -
Write a
Contextclass insrc/signalbot/context/(pattern:TypingContextintyping_context.py), and aHandlerABC insrc/signalbot/handlers.py(pattern:TypingHandler) with one abstracthandle_xxx(self, context: ...)method. -
Register the dispatch. Add an entry to
_MESSAGE_DISPATCHinsrc/signalbot/_pipeline.pymapping your new class to(YourHandler, YourContext, "handle_xxx"). This is the one place that ties parsing to dispatch — nothing reaches a handler without an entry here. -
(Optional) Write a trigger decorator in
handlers.pyif handlers for this type commonly filter on a field — followreaction_triggeredas the smallest example: it checksisinstance(context, YourContext), filters, and calls through. -
Write a test. Follow
tests/unit/messages/test_message.py's pattern: build the raw envelope JSON inline (this repo doesn't use fixture files for envelopes), callparse(signal, raw_json_str), and assertisinstance(result, YourClass)plus field values. Add dispatch coverage intests/unit/test_pipeline.pyif relevant. -
Write an example handler under
examples/handlers/orexamples/commands/, followingexamples/commands/reaction.py(@text_triggered+context.react(...)) orexamples/handlers/reaction.py(ReactionHandler+@reaction_triggered) as templates, and register it in one of the example bots (examples/simple_bot.py/examples/bot.py) withbot.register(YourHandler()). -
Update the docs. Add the new type to the message types table in How it works, and add a new page under
docs/examples/, under the section of the bot that you are editing.