Signal Bot Python Framework

repository·main·Indexed 42 days ago

https://github.com/signalbot-org/signalbot

A Python framework for building custom bots for the Signal messaging platform. Version 2.0.0 utilizes a producer-consumer model to process messages via signal-cli-rest-api, featuring a pipeline for incoming websocket messages and outgoing HTTP API requests. It includes support for persistent storage via SQLite and Redis, mock chat testing utilities, and a handler-based system using decorators like @text_triggered.

Tokens
14.1K
Snippets
45
Records
68
Agent score
53%

What's inside Signal Bot

  1. How signalbot processes messages

    main

    Signalbot operates using a producer-consumer model:

    • Producer: A single process that monitors the linked account via a websocket connection for new messages.
    • Consumers: Multiple workers that pick up tasks created by the producer. For every registered handler, the producer creates a task, and a consumer executes it.

    Scaling: If your bot's handlers perform blocking operations, you should increase the number of consumers to ensure the bot remains reactive.

    Storage Warning: If you do not provide a Redis instance or a SQLite database name in your Config, signalbot will use in-memory storage. This will cause all state to be lost upon restart. To use in-memory storage without warnings, add {'type': 'in-memory'} to your config.

  2. How generated wire models are created

    main

    The Python classes in the src/signalbot/_generated/ directory are automatically generated from JSON Schema files located in the json_schema/ folder. Specifically, the models are derived from the signal-cli-rest-api.json schema, which is sourced from the signal-cli-rest-api repository.

    Developers should treat these classes as the authoritative wire models for interacting with the Signal CLI REST API via the framework.

  3. How incoming and outgoing messages work in Signalbot

    main

    Signalbot operates using a dual-direction pipeline for message processing:

    Incoming Messages (Websocket)

    1. Source: Raw JSON arrives via a websocket from signal-cli-rest-api.
    2. Parsing: An internal parse() function converts raw JSON into a structured object (e.g., ReceivedMessage).
    3. Dispatch: The message is placed in a dispatch queue.
    4. Handling: The message is routed to a specific Handler and its corresponding Context subclass, which then executes your implementation of a handle_xxx method.

    Outgoing Messages (HTTP API)

    1. Trigger: Your code calls a method on a context object (e.g., context.send(), context.react()) or directly on bot.messages / bot.reactions.
    2. Resolution: The framework resolves the recipient and builds a request object.
    3. Transmission: An internal HTTP client sends the request to the signal-cli-rest-api HTTP endpoint.
  4. Configure persistent storage for signalbot

    main

    By default, the storage attribute of the signalbot.SignalBot class uses in-memory storage, meaning all data is lost when the bot stops or resets. To ensure data persists across restarts, you must provide a persistent storage implementation.

    Supported persistent storage options include:

    • SQLiteStorage
    • RedisStorage

    Refer to the storage configuration examples in the bot configuration guide for specific setup instructions.

  5. How incoming message flow works in signalbot

    main

    Signalbot handles incoming messages through a structured pipeline that transforms raw data from signal-cli-rest-api into typed objects. The flow follows a predictable name chain from the wire to the handler:

    1. Wire/Envelope: signal-cli-rest-api pushes a message via websocket.
    2. Parsing: The message is parsed into a MessageEnvelope.
    3. Typed Object: The envelope is converted into a wrapped class (e.g., DataMessage).
    4. Context: A specific Context class (e.g., DataMessageContext) is created to hold the message and state.
    5. Dispatch: The message is dispatched to a Handler (e.g., DataMessageHandler).
    6. Execution: The handler's specific method (e.g., handle_data_message) is called.

    Naming Convention Mapping:

    Generated classWrapped classHandler ABCContext classHandler method
    DataMessageDataMessageDataMessageHandlerDataMessageContexthandle_data_message
    ReactionReactionReactionHandlerReactionContexthandle_reaction
    RemoteDeleteRemoteDeleteRemoteDeleteHandlerRemoteDeleteContexthandle_remote_delete
    TypingMessageTypingMessageTypingHandlerTypingContexthandle_typing
    GroupInfoGroupUpdateGroupUpdateHandlerGroupUpdateContexthandle_group_update
  6. Naming patterns for new outgoing actions

    main

    When extending signalbot with new outgoing actions, you must follow a specific naming chain that connects the wire request, the request class, the bot's actions method, and the context shortcut. This ensures consistency across the library.

    Naming Rules

    • Request Class: Drop the Request suffix from the generated wire model (e.g., UpdateGroupRequest becomes UpdateGroup).
    • Context Shortcut: Use the bare verb from the Actions method. If the verb is already used in the Context namespace, qualify it with its noun (e.g., send is used by messages, so receipts use send_receipt; update is used by both groups and contacts, so they use update_group and update_contact).
    • Nesting: If a domain already has a registry (like bot.groups), attach new actions under an .actions attribute (e.g., bot.groups.actions.update) rather than at the top level.
    | Generated request              | Request class                                                                                  | Actions method                                | Context shortcut                                                                                                      |
    | --------------------------------| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
    | `SendMessageV2`                | `SendMessage`                                                                                              | `bot.messages.send`                               | `Context.send`                                                                                                             |
    | `RemoteDeleteRequest`           | built inline                                                                                              | `bot.messages.remote_delete`                       | `DataMessageContext.remote_delete`                                                                                        |
    | `TypingIndicatorRequest`       | built inline                                                                                              | `bot.messages.start_typing` / `.stop_typing`       | `Context.start_typing` / `Context.stop_typing`                                                                             |
    | `SendReactionRequest`          | built inline                                                                                              | `bot.reactions.react`                              | `DataMessageContext.react`                                                                                                |
    | `Receipt`                       | built inline                                                                                              | `bot.receipts.send`                               | `DataMessageContext.send_receipt`                                                                                          |
    | `UpdateGroupRequest`            | `UpdateGroup`                                                                                              | `bot.groups.actions.update`                        | `Context.update_group`                                                                                                     |
    | `CreatePollRequest`             | `CreatePoll`                                                                                               | `bot.polls.create`                                 | `Context.create_poll`                                                                                                      |
    | `UpdateContactRequest`          | `UpdateContact`                                                                                            | `bot.contacts.update`                              | `Context.update_contact`                                                                                                   |
  7. Steps to implement a new outgoing action

    main

    To add a new outgoing action to signalbot, follow these steps to ensure the wire models, client, actions, and context are correctly integrated:

    1. Identify Endpoint: Find the endpoint in the signal-cli-rest-api Swagger docs (HTTP verb, path, and JSON shape).
    2. Generate Wire Models: Ensure the schema exists in src/signalbot/_generated/json_schema/signal-cli-rest-api.json. If missing, update the upstream schema and re-run: uv run datamodel-codegen --profile signal-cli-rest-api.
    3. Define Domain Request Model: In the relevant package (e.g., src/signalbot/polls/), create a model:
      • Use a straight subclass of the generated request if all fields are known at construction.
      • Use a standalone BaseModel with a to_generated() method if fields like recipient or group_id_or_name are only known later. Do not use str | None for these; instead, pass them as required parameters to to_generated() or the *Actions method.
    4. Implement Client Method: In src/signalbot/_client/, add a URI method to the *URIs class and a method to the *Client class. The client method should use model_dump_json(exclude_none=True, by_alias=True) and call self._request(...).
    5. Expose on SignalAPI: If it's a new section, register it in src/signalbot/_client/signal_api.py.
    6. Implement Actions Method: In src/signalbot/_actions/, add the method to the matching *Actions class. Resolve recipients via self._recipients.resolve(...), convert to the wire request (via .to_generated() or direct construction), call the client, and log the action.
    7. Initialize in SignalBot: If it's a new Actions class, instantiate and attach it in src/signalbot/_bot_init.py.
    8. Add Context Shortcut: In src/signalbot/context/context.py, add a convenience method that delegates to the Actions layer.
    9. Test and Example: Write a unit test in tests/unit and provide a usage example in examples/commands/ or examples/handlers/.
  8. Authenticate with signal-cli-rest-api

    main

    If your signal-cli-rest-api instance requires authentication, you can provide credentials via the auth parameter in the Config object. Supported methods include BasicAuthConfig (username/password) and BearerAuthConfig (token).

    from signalbot import BasicAuthConfig, BearerAuthConfig, Config, SignalBot
    
    # Basic Authentication (User and Password)
    config_basic = Config(
        phone_number="+1234567890",
        auth=BasicAuthConfig(username="user", password="password"),
    )
    bot_basic = SignalBot(config_basic)
    
    # Token Authentication (Bearer Token)
    config_token = Config(
        phone_number="+1234567890",
        auth=BearerAuthConfig(token="token")
    )
    bot_token = SignalBot(config_token)
  9. Install and run a minimal signalbot

    main

    To get started with a basic bot, install the package and run a script with the required PHONE_NUMBER environment variable.

    1. Install:
      pip install signalbot
    2. Run: Create a bot.py file (refer to examples/simple_bot.py for a template) and execute it:
      export PHONE_NUMBER="+49123456789"
      python bot.py

    Note on Connection Modes: By default, SignalBot uses HTTPS/WSS and falls back to HTTP/WS. You can change this by setting the connection_mode (using the ConnectionMode type) within the Config argument passed to the SignalBot constructor.

    export PHONE_NUMBER="+49123456789"
    python bot.py