Creating Bots ============= Basic Structure --------------- A minimal LXMFy bot involves: 1. Importing :code:`LXMFBot`. 2. Instantiating :code:`LXMFBot` with desired configuration. 3. Defining commands or event handlers. 4. Running the bot using :code:`bot.run()`. .. code-block:: python from lxmfy import LXMFBot # 1. Instantiate the bot bot = LXMFBot( name="SimpleBot", command_prefix="!", storage_path="simple_data" ) # 2. Define commands @bot.command(name="ping", description="Responds with pong") def ping_command(ctx): # ctx is a context object containing message info # ctx.sender: Sender's LXMF hash # ctx.content: Full message content # ctx.args: List of arguments after the command # ctx.reply(message): Function to send a reply # (can also take keyword arguments like title="My Title", lxmf_fields=some_fields) ctx.reply("Pong!") # For long-running tasks, you can use threaded commands: # import time # @bot.command(name="long_op", description="Performs a long operation in a separate thread", threaded=True) # def long_op_command(ctx): # ctx.reply("Starting long operation...") # time.sleep(10) # Simulate a long-running operation # ctx.reply("Long operation complete!") # Important: Threaded commands should not directly interact with RNS or lxmfy.transport.py. @bot.command(name="greet", description="Greets the user") def greet_command(ctx): if ctx.args: name = " ".join(ctx.args) ctx.reply(f"Hello, {name}!") else: ctx.reply("Hello there! Tell me your name: !greet ") # 3. Run the bot if __name__ == "__main__": print(f"Starting bot: {bot.config.name}") print(f"Bot LXMF Address: {bot.local.hash}") bot.run() Using Templates --------------- LXMFy provides several templates for common bot types. You can use the CLI to generate a bot file based on a template. .. code-block:: bash # Create an echo bot lxmfy create --template echo my_echo_bot # Create a reminder bot (uses SQLite storage) lxmfy create --template reminder my_reminder_bot # Create a note-taking bot (uses JSON storage) lxmfy create --template note my_note_bot # Create a cog test bot (tests cog loading features) lxmfy create --template cogtest my_cog_test_bot # Create an RRC room bot (joins hubs and replies to @mentions) lxmfy create --template rrc my_rrc_bot # Or run the template directly lxmfy run rrc Running these commands creates a Python file (e.g., :code:`my_echo_bot.py`) that imports and runs the chosen template. You can then modify the generated file or the template code itself (:code:`lxmfy/templates/...`). **Example generated file (:code:`my_cog_test_bot.py`):** .. code-block:: python from lxmfy.templates import CogTestBot if __name__ == "__main__": bot = CogTestBot() # Creates an instance of the CogTestBot template # You can optionally override the default name: # bot.bot.name = "My Cog Test Bot" bot.run() Bot Configuration ----------------- When creating an :code:`LXMFBot` instance, you can pass various keyword arguments to configure its behavior. See the :code:`BotConfig` section in the `API Reference `_ or the `Quick Start Guide `_ for a list of common options. .. code-block:: python from lxmfy import LXMFBot bot = LXMFBot( name="ConfiguredBot", announce=3600, # Announce every hour admins={"your_admin_hash_here"}, # Set admin user(s) command_prefix="$", # Use '$' as prefix storage_type="sqlite", # Use SQLite database storage_path="data/my_bot_data.db", # Specify DB file path rate_limit=10, # Allow 10 messages / minute cooldown=30, # Cooldown of 30 seconds permissions_enabled=True # Enable role-based permissions ) if __name__ == "__main__": # You can also modify config after instantiation # Note: some settings are best set during init bot.config.max_warnings = 5 bot.spam_protection.config.max_warnings = 5 # Update spam protector too bot.run() Setting a Bot Icon (LXMF Field) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ You can give your bot a custom icon that appears in compatible LXMF clients. This uses the :code:`LXMF.FIELD_ICON_APPEARANCE` and can be set when sending messages. First, ensure you have the necessary imports: .. code-block:: python from lxmfy import IconAppearance, pack_icon_appearance_field Then, you can define and use the icon: .. code-block:: python # In your bot class or setup icon_data = IconAppearance( icon_name="robot_2", # Choose from Material Symbols fg_color=b'\x00\xFF\x00', # Green bg_color=b'\x33\x33\x33' # Dark Grey ) self.bot_icon_field = pack_icon_appearance_field(icon_data) # When sending a message or replying: ctx.reply("Message from your bot!", lxmf_fields=self.bot_icon_field) # or # bot.send(destination, "Another message", lxmf_fields=self.bot_icon_field) This :code:`self.bot_icon_field` can be pre-calculated and reused for all messages sent by the bot. Structured Commands via LXMF Fields ----------------------------------- In addition to text-based commands, LXMFy supports commands sent through LXMF message fields using ``FIELD_COMMANDS`` (``0x09``). This is useful for structured request/response workflows between LXMF clients and bots. When a message contains ``FIELD_COMMANDS``, the bot extracts the command name and arguments, routes them through the same command registry as text commands, and automatically includes ``FIELD_RESULTS`` (``0x0A``) in the reply. **Receiving structured commands** .. code-block:: python from lxmfy import LXMFBot bot = LXMFBot(name="FieldBot") @bot.command(name="add", description="Add two numbers") def add_command(ctx): if len(ctx.args) >= 2: try: result = float(ctx.args[0]) + float(ctx.args[1]) ctx.reply(str(result)) except ValueError: ctx.reply("Invalid numbers") else: ctx.reply("Usage: add ") The ``ctx`` object in field command callbacks includes: - :code:`ctx.fields` — the raw LXMF fields dict from the incoming message - :code:`ctx.request_id` — the ``request_id`` from the incoming ``FIELD_COMMANDS`` (if any) **Sending a structured command from an LXMF client** .. code-block:: python import LXMF from lxmfy import FIELD_COMMANDS lxm = LXMF.LXMessage( destination, source, b"", # content can be empty for field-only commands desired_method=LXMF.LXMessage.DIRECT, ) lxm.fields[FIELD_COMMANDS] = { "command": "add", "args": ["3", "5"], "request_id": "req-42", # optional, for correlation } router.handle_outbound(lxm) **Disabling field commands** If you want the bot to ignore ``FIELD_COMMANDS`` and only process text commands, set: .. code-block:: python bot = LXMFBot( name="TextOnlyBot", lxmf_commands_enabled=False, ) Using Cogs (Extensions) ----------------------- Cogs allow you to organize your commands and event listeners into separate files (modules), keeping your main bot file cleaner. 1. **Create a :code:`cogs` directory** (or whatever you set :code:`cogs_dir` to in :code:`BotConfig`). 2. **Create Python files** inside the :code:`cogs` directory (e.g., :code:`utility.py`). 3. **Define a class** that inherits from :code:`lxmfy.Cog` (optional but good practice) or is just a standard class. 4. **Define commands** as methods within the class using the :code:`@Command` decorator. 5. **Create a :code:`setup(bot)` function** in the cog file, which LXMFy will call to register the cog. **Example (:code:`cogs/utility.py`):** .. code-block:: python from lxmfy import Command from lxmfy.commands import Cog # Import Cog if inheriting import time class UtilityCog: # Or class UtilityCog(Cog): def __init__(self, bot): self.bot = bot self.start_time = time.time() @Command(name="uptime", description="Shows bot uptime") # Note: Methods in cogs often take 'self' and 'ctx' def uptime_command(self, ctx): uptime_seconds = time.time() - self.start_time ctx.reply(f"Bot has been running for {uptime_seconds:.2f} seconds.") @Command(name="info", description="Shows bot info") def info_command(self, ctx): info = ( f"Bot Name: {self.bot.config.name}\n" f"Owner(s): {', '.join(self.bot.config.admins) or 'None'}\n" f"Prefix: {self.bot.config.command_prefix}" ) ctx.reply(info) @Command(name="threaded_cog_task", description="Performs a long task in a cog thread", threaded=True) def threaded_cog_task(self, ctx): ctx.reply("Starting a long cog task... this will run in a separate thread.") time.sleep(7) # Simulate a long-running operation ctx.reply("Long cog task completed!") # This function is required for the cog to be loaded def setup(bot): cog_instance = UtilityCog(bot) bot.add_cog(cog_instance) # Register the cog instance with the bot **Main Bot File (:code:`my_bot.py`):** .. code-block:: python from lxmfy import LXMFBot bot = LXMFBot( name="CogBot", cogs_enabled=True, # Make sure cogs are enabled (default) cogs_dir="cogs" # Point to the directory ) if __name__ == "__main__": # Cogs are loaded automatically during LXMFBot initialization # if cogs_enabled is True. bot.run() When the bot starts, it will automatically find :code:`utility.py`, call its :code:`setup` function, which creates an instance of :code:`UtilityCog` and registers it using :code:`bot.add_cog()`. The commands defined in the cog (:code:`uptime`, :code:`info`) will then be available. External Script Cogs (Multi-Language Support) --------------------------------------------- You can also write bot extensions in languages other than Python (e.g., Bash, Ruby, Perl, Go, C) using External Script Cogs. 1. **Create an executable script** in your :code:`cogs` directory. 2. **Add a shebang** at the top of the script (e.g., :code:`#!/bin/bash`). 3. **Ensure the script is executable** (:code:`chmod +x your_script`). When the bot starts, it will automatically register any executable file in the :code:`cogs` directory (that doesn't end in :code:`.py`) as a bot command. **Argument Protocol:** - :code:`$1`: Sender's LXMF hash. - :code:`$2`: Full message content. - :code:`$3`, :code:`$4`, ...: Individual command arguments. **Environment Variables:** - :code:`LXMFY_SENDER`: The sender's identity hash. - :code:`LXMFY_CONTENT`: The full message content. - :code:`LXMFY_HAS_ADMIN`: :code:`true` or :code:`false` depending on the sender's admin status. **Example Bash Cog (:code:`cogs/greet.sh`):** .. code-block:: bash #!/bin/bash echo "Hello from Bash! You sent: $2" When a user sends :code:`/greet hello`, the bot will execute this script and reply with its stdout: :code:`Hello from Bash! You sent: /greet hello`. Sovereign NLP (Local Intent Classification) ------------------------------------------- LXMFy includes a built-in, lightweight NLP engine for intent classification. This allows your bot to understand the "intent" of a message even if it doesn't match a command exactly. 1. **Enable NLP** in your bot configuration: :code:`nlp_enabled=True`. 2. **Define intents** using the :code:`@bot.intent` decorator. .. code-block:: python @bot.intent("help", examples=["how do I use this?", "show me commands", "help me please"]) def help_intent(msg): msg.reply("I can help! Try typing /help to see a list of commands.") The NLP engine uses mathematical vector similarity (TF-IDF and Cosine Similarity) to match incoming text against your example phrases. This processing happens entirely locally on your machine, ensuring full privacy. **Persistence and Extensibility:** For larger bots, you can export and import the trained intent model to avoid retraining on every startup: .. code-block:: python # Export the model model_data = bot.nlp.export_model() # Save model_data to a file or database # Later, import it back bot.nlp.import_model(model_data) RNS Link Support ---------------- Bots can now establish and respond to direct RNS Links. This is useful for stateful, streaming, or high-bandwidth communication that goes beyond simple message packets. 1. **Enable Link Support** in configuration: :code:`link_support_enabled=True`. 2. **Request a link**: :code:`bot.request_link(destination_hash)`. You can also specify a custom app name and aspects: :code:`bot.request_link(dest, callback, "my_app", "aspect1")`. 3. **Handle incoming links**: Register a callback with :code:`bot.on_link(handler)`. .. code-block:: python def handle_link(link): print(f"Link established with {RNS.hexrep(link.destination.hash)}") # You can now use the link for direct RNS communication bot.on_link(handle_link) **Safety & Sandboxing:** - **Timeouts:** External cogs have a default timeout (30s) to prevent hanging. This is configurable via :code:`external_cogs_timeout`. - **Threading:** All external cogs run in separate threads and do not block the bot. - **Bot process sandbox (Linux only):** When :code:`landlock_enabled=True` (default) and the kernel supports Landlock LSM (5.13+), the bot applies a filesystem sandbox to its own process after startup. Writable paths are limited to storage, config, cogs, Reticulum config, and temp directories. Override with the environment variable :code:`LXMFY_LANDLOCK=0` to disable or :code:`LXMFY_LANDLOCK=1` to force an attempt. - **External cog sandbox (Linux only):** When :code:`external_cogs_sandbox_enabled=True` (default), executable script cogs run inside a restricted environment. Set :code:`external_cogs_sandbox_type` to one of: - :code:`auto` (default): prefer Landlock when supported, otherwise :code:`bubblewrap` (:code:`bwrap`), otherwise :code:`firejail` - :code:`landlock`: Landlock-only sandbox via :code:`preexec_fn` (narrower rules than the bot process sandbox) - :code:`bwrap`: bubblewrap read-only bind sandbox - :code:`firejail`: firejail private profile with no network - :code:`none`: no subprocess sandbox - **Status:** Call :code:`bot.get_landlock_status()` to inspect kernel support, whether Landlock was requested, and whether the bot process sandbox is active. Handling Messages ----------------- LXMFy provides several ways to handle incoming messages at different stages of processing. First Message Handler ^^^^^^^^^^^^^^^^^^^^^ Handle the first message from each new user (useful for welcome messages): .. code-block:: python from lxmfy import LXMFBot bot = LXMFBot( name="WelcomeBot", first_message_enabled=True # Must be True (default) ) @bot.on_first_message() def welcome_new_user(sender, message): content = message.content.decode("utf-8") bot.send( sender, f"Welcome to the bot! You said: {content}\n\n" "Type /help to see available commands." ) return True # Return True to stop further processing of this message if __name__ == "__main__": bot.run() General Message Handler ^^^^^^^^^^^^^^^^^^^^^^^ Handle all incoming messages before command processing: .. code-block:: python from lxmfy import LXMFBot bot = LXMFBot(name="EchoBot") @bot.on_message() def echo_non_commands(sender, message): content = message.content.decode("utf-8").strip() # Check if this is a command - if so, let command handler deal with it if content.startswith(bot.config.command_prefix): command_name = content.split()[0][len(bot.config.command_prefix):] if command_name in bot.commands: return False # Let command handler process it # Not a command, echo it back bot.send(sender, f"You said: {content}") return False # Return False to continue processing (though no commands will match) @bot.command(name="hello", description="Say hello") def hello_command(ctx): ctx.reply("Hello! This is a command response.") if __name__ == "__main__": bot.run() Message Handler Processing Order: 1. **First Message Handler** (if :code:`first_message_enabled=True` and this is first message from sender) 2. **General Message Handlers** (registered with :code:`@bot.on_message()`) 3. **Command Processing** (if message matches a registered command) Handlers can return :code:`True` to stop further processing or :code:`False` to continue to the next stage. Handling Events --------------- You can register handlers for various bot events using the :code:`@bot.events.on()` decorator. .. code-block:: python from lxmfy import LXMFBot from lxmfy.events import EventPriority # Optional for priority bot = LXMFBot(name="EventBot") @bot.events.on("message_received") def log_message(event): # Event object contains details sender = event.data.get("sender") message_content = event.data.get("message").content.decode('utf-8', errors='ignore') print(f"Received message from {sender}: {message_content}") # You can cancel event processing (e.g., stop message handling) # if sender == "some_blocked_hash": # event.cancel() @bot.events.on("command_executed", priority=EventPriority.LOW) def log_command(event): # Example: event.data might contain {'command_name': 'ping', 'sender': '...', ...} command_name = event.data.get('command_name', 'unknown') sender = event.data.get('sender', 'unknown') print(f"Command '{command_name}' executed by {sender}") # You can define custom events too @bot.command(name="special") def special_command(ctx): ctx.reply("Doing something special!") # Dispatch a custom event bot.events.dispatch(Event("special_action_taken", data={"user": ctx.sender})) @bot.events.on("special_action_taken") def handle_special(event): user = event.data.get("user") print(f"Special action was taken by user: {user}") if __name__ == "__main__": bot.run() See :code:`lxmfy/events.py` for more details on the :code:`Event` structure and priorities. Storage ------- LXMFy provides JSON, SQLite, and In-Memory storage backends. * **JSON:** Simple, human-readable. Good for small datasets. Configure with :code:`storage_type="json"` and :code:`storage_path="your_data_dir"`. * **SQLite:** More efficient for larger datasets or frequent writes. Configure with :code:`storage_type="sqlite"` and :code:`storage_path="your_db_file.db"`. * **Memory:** Entirely in-RAM storage. State is lost on shutdown. Configure with :code:`storage_type="memory"`. You can access the storage interface via :code:`bot.storage`: .. code-block:: python # Save data bot.storage.set("user_prefs:" + ctx.sender, {"theme": "dark"}) # Get data (with a default value) prefs = bot.storage.get("user_prefs:" + ctx.sender, {}) theme = prefs.get("theme", "light") # Check if data exists if bot.storage.exists("some_key"): print("Key exists!") # Delete data bot.storage.delete("old_data_key") # Scan for keys with a prefix (useful for listing user data) user_keys = bot.storage.scan("user_prefs:") for key in user_keys: user_data = bot.storage.get(key) print(f"Data for {key}: {user_data}") See :code:`lxmfy/storage.py` and the API reference for more details. Permissions ----------- LXMFy includes an optional role-based permission system. Enable it with :code:`permissions_enabled=True` during :code:`LXMFBot` initialization. * **Roles:** Define roles with specific permissions (e.g., :code:`DefaultPerms.MANAGE_USERS`). * **Permissions:** Granular flags defined in :code:`DefaultPerms` (e.g., :code:`USE_COMMANDS`, :code:`BYPASS_SPAM`). * **Assignment:** Assign roles to user hashes. See :code:`lxmfy/permissions.py`, the API reference, and potentially example cogs (if any are created) for usage details. Signature Verification ---------------------- LXMFy provides configuration for LXMF's built-in cryptographic message signing and verification. All LXMF messages are automatically signed by the LXMF/RNS stack - LXMFy simply allows you to enforce signature verification policies. **Configuration:** Enable signature verification in your bot configuration: .. code-block:: python bot = LXMFBot( name="SecureBot", signature_verification_enabled=True, # Enable signature checking require_message_signatures=False # Set to True to reject unsigned messages ) **How It Works:** LXMF automatically handles all cryptographic operations: 1. **Outgoing Messages:** LXMF automatically signs all messages using the sender's RNS identity during message packing. 2. **Incoming Messages:** LXMF automatically validates signatures using the sender's RNS identity and provides validation results. 3. **LXMFy's Role:** LXMFy checks LXMF's validation results and enforces your policy: - If :code:`signature_verification_enabled=False`: All messages are accepted (default) - If :code:`signature_verification_enabled=True` and :code:`require_message_signatures=False`: Messages are accepted but unsigned/invalid signatures are logged - If :code:`signature_verification_enabled=True` and :code:`require_message_signatures=True`: Unsigned or invalid messages are rejected 4. **Permission Integration:** Users with :code:`BYPASS_SPAM` permission can bypass signature verification requirements. **CLI Management:** You can manage signature verification settings using the CLI: .. code-block:: bash # Test signature verification lxmfy signatures test # Enable signature verification lxmfy signatures enable # Disable signature verification lxmfy signatures disable **Technical Details:** LXMF uses Ed25519 signatures provided by the RNS cryptography system. Every LXMF message includes the sender's signature, which is validated against their known RNS identity. LXMFy simply reads LXMF's :code:`message.signature_validated` property and :code:`message.unverified_reason` to enforce your bot's security policy. Advanced Message Delivery -------------------------- LXMFy supports advanced message delivery options for improved reliability. Using Propagation Nodes ^^^^^^^^^^^^^^^^^^^^^^^^ Send messages through specific LXMF propagation nodes: .. code-block:: python from lxmfy import LXMFBot bot = LXMFBot(name="PropagationBot") @bot.command(name="send", description="Send via propagation node") def send_command(ctx): # Set a specific propagation node once (config-level) bot.set_propagation_node("") # Send using configured delivery strategy bot.send( ctx.sender, "This message will use direct delivery with propagation fallback as configured" ) Propagation nodes are useful when direct delivery is not possible or when you want to ensure message delivery through the Reticulum mesh network. Configuring Retries ^^^^^^^^^^^^^^^^^^^ Configure automatic retry attempts for failed message deliveries via bot config: .. code-block:: python from lxmfy import LXMFBot bot = LXMFBot(name="ReliableBot") bot = LXMFBot( name="ReliableBot", direct_delivery_retries=5, # Retry direct delivery up to 5 times propagation_fallback_enabled=True ) @bot.command(name="important", description="Send important message with retries") def important_command(ctx): bot.send(ctx.sender, "This is an important message") @bot.command(name="normal", description="Send with default retries") def normal_command(ctx): # Default direct_delivery_retries is 3 bot.send(ctx.sender, "This message uses default retry settings") The retry system: - Automatically tracks delivery attempts per destination - Retries failed direct deliveries up to :code:`direct_delivery_retries` - Resets the retry counter on successful delivery - Logs retry attempts and failures for debugging Reticulum Relay Chat (RRC) -------------------------- LXMFy bots can join `RRC `_ hubs as ordinary clients over RNS Links using CBOR envelopes. This is compatible with NomadNet and rrcd style hubs (including MeshChatX when it hosts or joins the same hub). Reticulum config matters ^^^^^^^^^^^^^^^^^^^^^^^^ The bot must use the **same** Reticulum network as the hub. MeshChatX typically uses :code:`~/.reticulum` with backbone or TCP interfaces. The project-local :code:`config/` directory often uses an isolated instance name and AutoInterface only, so hub announces never arrive and you see :code:`Hub identity unknown`. Prefer one of: * Set :code:`reticulum_config_dir` to your user config (usually :code:`~/.reticulum`) * Or export :code:`LXMFY_RETICULUM_CONFIG_DIR=~/.reticulum` * Keep MeshChatX or :code:`rnsd` running so the shared instance is up before the bot starts The :code:`rrc` template defaults to :code:`~/.reticulum` when that directory exists. Quick start with the template ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash lxmfy run rrc Defaults: * Hub: :code:`664fc0e8d2e448658e37bb3f34e6c88f` * Room: :code:`#general` * Reticulum config: :code:`~/.reticulum` (or :code:`LXMFY_RETICULUM_CONFIG_DIR`) You should see logs for hub connect, welcome, auto-join, and :code:`RRC joined #general`. Programmatic RRC bot ^^^^^^^^^^^^^^^^^^^^ .. code-block:: python from lxmfy import LXMFBot, RRCMessage bot = LXMFBot( name="RoomBot", reticulum_config_dir="~/.reticulum", rrc_enabled=True, rrc_hubs=["your_rrc_hub_destination_hash"], rrc_rooms=["general"], rrc_nick="RoomBot", rrc_auto_reconnect=True, rrc_persist_sessions=True, ) @bot.on_rrc def on_rrc(event, client, payload): if event == "welcome": bot.logger.info("Welcomed by hub") return if event != "msg" or not isinstance(payload, RRCMessage): return if payload.mention and payload.room: client.send_message( payload.room, f"Heard you, {payload.nick}", ) bot.run() Or connect at runtime: .. code-block:: python bot.connect_rrc("hub_destination_hash", rooms=["general"]) bot.rrc.send_message("general", "hello room") bot.rrc.send_action("general", "waves") bot.disconnect_rrc() Session behavior ^^^^^^^^^^^^^^^^ * HELLO / WELCOME, JOIN / PART, MSG / NOTICE / ACTION, PING / PONG, ERROR, RESOURCE_ENVELOPE * Auto-reconnect with room re-join after WELCOME * Client-side hub limit and rate-limit enforcement * Session persistence across restarts (:code:`rrc_persist_sessions`, default on) * Outgoing LXMF queue persistence is separate (:code:`message_persistence_enabled`)