Creating Bots

Basic Structure

A minimal LXMFy bot involves:

  1. Importing LXMFBot.

  2. Instantiating LXMFBot with desired configuration.

  3. Defining commands or event handlers.

  4. Running the bot using bot.run().

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 <your_name>")

# 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.

# 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 meme bot (fetches memes from an API)
lxmfy create --template meme my_meme_bot

Running these commands creates a Python file (e.g., my_echo_bot.py) that imports and runs the chosen template. You can then modify the generated file or the template code itself (lxmfy/templates/...).

Example generated file (:code:`my_meme_bot.py`):

from lxmfy.templates import MemeBot

if __name__ == "__main__":
    bot = MemeBot() # Creates an instance of the MemeBot template
    # You can optionally override the default name:
    # bot.bot.name = "My Awesome Meme Bot"
    bot.run()

Bot Configuration

When creating an LXMFBot instance, you can pass various keyword arguments to configure its behavior. See the BotConfig section in the API Reference or the Quick Start Guide for a list of common options.

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 LXMF.FIELD_ICON_APPEARANCE and can be set when sending messages.

First, ensure you have the necessary imports:

from lxmfy import IconAppearance, pack_icon_appearance_field

Then, you can define and use the icon:

# 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 self.bot_icon_field can be pre-calculated and reused for all messages sent by the bot.

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 cogs_dir to in BotConfig).

  2. Create Python files inside the cogs directory (e.g., utility.py).

  3. Define a class that inherits from lxmfy.Cog (optional but good practice) or is just a standard class.

  4. Define commands as methods within the class using the @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`):

from lxmfy import Command, 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`):

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 utility.py, call its setup function, which creates an instance of UtilityCog and registers it using bot.add_cog(). The commands defined in the cog (uptime, info) will then be available.

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):

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:

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 first_message_enabled=True and this is first message from sender)

  2. General Message Handlers (registered with @bot.on_message())

  3. Command Processing (if message matches a registered command)

Handlers can return True to stop further processing or False to continue to the next stage.

Handling Events

You can register handlers for various bot events using the @bot.events.on() decorator.

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 lxmfy/events.py for more details on the Event structure and priorities.

Storage

LXMFy provides JSON and SQLite storage backends.

  • JSON: Simple, human-readable. Good for small datasets. Configure with storage_type="json" and storage_path="your_data_dir".

  • SQLite: More efficient for larger datasets or frequent writes. Configure with storage_type="sqlite" and storage_path="your_db_file.db".

You can access the storage interface via bot.storage:

# 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 lxmfy/storage.py and the API reference for more details.

Permissions

LXMFy includes an optional role-based permission system. Enable it with permissions_enabled=True during LXMFBot initialization.

  • Roles: Define roles with specific permissions (e.g., DefaultPerms.MANAGE_USERS).

  • Permissions: Granular flags defined in DefaultPerms (e.g., USE_COMMANDS, BYPASS_SPAM).

  • Assignment: Assign roles to user hashes.

See 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:

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 signature_verification_enabled=False: All messages are accepted (default)

    • If signature_verification_enabled=True and require_message_signatures=False: Messages are accepted but unsigned/invalid signatures are logged

    • If signature_verification_enabled=True and require_message_signatures=True: Unsigned or invalid messages are rejected

  4. Permission Integration: Users with BYPASS_SPAM permission can bypass signature verification requirements.

CLI Management:

You can manage signature verification settings using the CLI:

# 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 message.signature_validated property and 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:

from lxmfy import LXMFBot

bot = LXMFBot(name="PropagationBot")

@bot.command(name="send", description="Send via propagation node")
def send_command(ctx):
    # Send through a specific propagation node
    bot.send(
        ctx.sender,
        "This message was routed through a propagation node",
        propagation_node="<propagation_node_hash_here>"
    )

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:

from lxmfy import LXMFBot

bot = LXMFBot(name="ReliableBot")

@bot.command(name="important", description="Send important message with retries")
def important_command(ctx):
    # Send with custom retry count
    bot.send(
        ctx.sender,
        "This is an important message that will retry up to 5 times on failure",
        max_retries=5
    )

@bot.command(name="normal", description="Send with default retries")
def normal_command(ctx):
    # Default max_retries is 3
    bot.send(ctx.sender, "This message uses default retry settings")

The retry system:

  • Automatically tracks delivery attempts per destination

  • Retries failed deliveries up to the specified max_retries

  • Resets the retry counter on successful delivery

  • Logs retry attempts and failures for debugging