Core Components¶
LXMFBot¶
The main bot class that handles message routing, command processing, and bot lifecycle management.
from lxmfy import LXMFBot
bot = LXMFBot(
name="MyBot",
announce=600,
announce_immediately=True,
admins=set(),
hot_reloading=False,
rate_limit=5,
cooldown=60,
max_warnings=3,
warning_timeout=300,
command_prefix="/",
cogs_dir="cogs",
cogs_enabled=True,
permissions_enabled=False,
storage_type="json", # "json", "sqlite", or "memory"
storage_path="data",
first_message_enabled=True,
event_logging_enabled=True,
max_logged_events=1000,
event_middleware_enabled=True,
announce_enabled=True,
signature_verification_enabled=False,
require_message_signatures=False,
identity_pinning_enabled=False,
message_persistence_enabled=True,
dynamic_cogs_enabled=True,
external_cogs_enabled=True,
external_cogs_sandbox_enabled=True,
external_cogs_sandbox_type="auto", # "auto", "landlock", "bwrap", "firejail", "none"
external_cogs_timeout=30,
landlock_enabled=True,
nlp_enabled=False,
nlp_threshold=0.5,
link_support_enabled=False,
lxmf_commands_enabled=True,
message_queue_size=50,
reticulum_config_dir=None, # or LXMFY_RETICULUM_CONFIG_DIR / "~/.reticulum"
rrc_enabled=False,
rrc_hubs=[],
rrc_rooms=[],
rrc_nick=None,
rrc_dest_name="rrc.hub",
rrc_auto_reconnect=True,
rrc_persist_sessions=True,
)
Key Methods¶
get_landlock_status(): Return Landlock LSM sandbox availability and activation state for the bot processrun(delay=10): Start the bot's main loopsend(destination, message, title="Reply", lxmf_fields=None, stamp_cost=None, opportunistic=None): Send a message to a destination, optionally with custom LXMF fields, stamp cost override, and opportunistic sending (tries direct, falls back to propagation immediately if configured).send_with_attachment(destination, message, attachment, title="Reply", stamp_cost=None, opportunistic=None): Send a message with an attachmentcommand(name, description="No description provided", admin_only=False, threaded=False): Decorator for registering commands. Setthreaded=Trueto run the command's callback in a separate thread. Commands support type-hinted arguments for automatic conversion.intent(name, examples): Decorator for registering NLP intent handlers.nlp.export_model(): Export trained NLP model data.nlp.import_model(model_data): Import previously exported NLP model data.request_link(destination_hash, callback=None, app_name="lxmf", *aspects): Request an RNS link to a destination. Allows customapp_nameandaspects(defaults to "lxmf" and "delivery").on_link(callback): Register a handler for incoming RNS links.load_extension(name): Load a cog extension module by name (e.g., "cogs.utility").reload_extension(name): Reload a cog extension module.add_cog(cog_instance): Add a cog class instance to the bot.remove_cog(cog_name): Remove a cog from the bot by its class name.on_first_message(): Decorator for handling first messages from userson_message(): Decorator for handling all messages (called before command processing)on_reaction(): Decorator for handling inbound reactions. Handlers receive(sender, reaction)where reaction carriesreaction_to,reaction_emoji, andreaction_senderkeysreact(destination, message_hash, reaction): Send a reaction to a message via the LXMFFIELD_REACTIONfieldvalidate(): Run validation checks on the bot configurationconnect_rrc(hub_hash, rooms=None, nick=None, dest_name=None, auto_reconnect=None): Connect to an RRC hub as a clientdisconnect_rrc(hub_hash=None): Disconnect one or all RRC hub sessionson_rrc(callback=None): Decorator or register handler for RRC events (handler(event, client, payload))rrc:RRCManagerinstance for multi-hub sessions
Structured Commands via LXMF Fields¶
Bots can receive commands sent via LXMF FIELD_COMMANDS (0x09) and
automatically reply with FIELD_RESULTS (0x0A). This enables
structured request/response workflows alongside normal text commands.
Incoming FIELD_COMMANDS are parsed and routed through the same command
registry as text commands, sharing permission checks, type-hinted
argument parsing, threading, and middleware.
from lxmfy import LXMFBot, FIELD_COMMANDS, FIELD_RESULTS, pack_result, unpack_commands
bot = LXMFBot(name="FieldBot")
@bot.command(name="status", description="Return bot status")
def status_cmd(ctx):
# ctx.fields contains the raw LXMF fields dict
# ctx.request_id is set automatically if the command included one
ctx.reply("Bot is online")
# Sending a structured command from another LXMF client:
# lxm.fields[FIELD_COMMANDS] = {"command": "status", "args": [], "request_id": "abc123"}
# router.handle_outbound(lxm)
# The bot reply automatically includes FIELD_RESULTS with the response and request_id.
To disable field command processing, set lxmf_commands_enabled=False
in BotConfig.
Reactions¶
Reactions travel as the LXMF FIELD_REACTION (0x40) field on an
otherwise empty message. pack_reaction and unpack_reaction build
and parse that field.
from lxmfy import pack_reaction, unpack_reaction
# Send a reaction to a message
bot.react(destination_hash, message_hash_hex, "thumbs up emoji")
# Receive reactions
@bot.on_reaction()
def on_reaction(sender, reaction):
# reaction["reaction_to"] - hex hash of the target message
# reaction["reaction_emoji"] - reaction text (up to 16 chars)
# reaction["reaction_sender"] - sender
print(f"{sender} reacted {reaction['reaction_emoji']} to {reaction['reaction_to']}")
return True
Reaction text is capped at 16 printable characters. The raw field stays
available in ctx.fields and msg.fields for compatibility.
Reply Threading¶
Replies can carry the LXMF FIELD_REPLY_TO (0x30), FIELD_REPLY_QUOTE
(0x31), and FIELD_THREAD (0x08) fields. Clients that render
threads, like MeshChatX and Sideband, show these as proper quote
replies instead of flat messages.
msg.reply() threads automatically: it sets FIELD_REPLY_TO to the
inbound message hash and FIELD_THREAD to the conversation root.
@bot.command("status")
def status(msg):
msg.reply("all systems nominal") # threaded reply
msg.reply("flat", reply_to=None) # opt out of threading
msg.reply("noted", quote=True) # quote the inbound text
For sends that are not replies, pass the fields explicitly:
Inbound replies are parsed onto the message context:
@bot.command("ctx")
def ctx_cmd(msg):
msg.reply_to # hex hash this message replies to, or None
msg.reply_quote # quoted text carried by the reply, or None
msg.thread # hex thread root hash, or None
pack_reply(message_hash, quote=..., thread=...) and
unpack_reply(fields) are exported for manual field handling.
Conversations¶
Commands can ask the sender a question and treat their next message as the answer, instead of dispatching it as a command:
@bot.command("report")
def report(msg):
title = msg.ask("Report title?", timeout=300)
if title is None:
msg.reply("Timed out.")
return
body = msg.ask("Describe the issue.", timeout=600)
if body is None:
msg.reply("Timed out.")
return
msg.reply(f"Filed: {title.content}")
msg.ask(prompt, timeout=..., validator=...) blocks the handler until
the answer arrives, the timeout fires, or the conversation is
cancelled. It returns an Answer with content, fields, hash,
sender, and a reply(text) shortcut.
A validator rejects bad answers and re-prompts:
num = msg.ask(
"Pick a number",
validator=lambda a: None if a.content.isdigit() else "Digits only",
)
In async command handlers use await msg.ask_async(...). For long
waits, or when many conversations may be open, use the callback style so
no thread stays parked:
msg.ask(
"Send the log file",
on_answer=lambda ans: ans.reply("received"),
on_timeout=lambda sender: bot.send(sender, "Too slow."),
timeout=3600,
)
Notes:
- Sending a registered command while a question is pending cancels the question and runs the command. Users always have an escape hatch.
bot.conversations.pending_count()andbot.conversations.cancel(sender)expose the registry for diagnostics and admin tools.- The registry is capped at 1024 pending questions.
askreturnsNonewhen it is full. - Blocking
askparks the delivery thread handling that message. That is safe for direct deliveries, but bots that sync large batches from a propagation node should preferon_answercallbacks.
Storage¶
The framework provides three storage backends:
JSONStorage¶
SQLiteStorage¶
MemoryStorage¶
Commands¶
Command registration and handling:
@bot.command(name="hello", description="Says hello")
def hello(ctx):
ctx.reply(f"Hello {ctx.sender}!")
Type-Hinted Arguments¶
Commands automatically parse and convert arguments based on type hints in the callback function.
@bot.command(name="add", description="Adds two numbers")
def add(ctx, a: int, b: int):
result = a + b
ctx.reply(f"The result is {result}")
Per-Command Rate Limits¶
Limit how often a single sender can invoke a command inside the global
cooldown window. Hitting the limit rejects the invocation only; it
never adds warnings or bans.
@bot.command(name="report", rate_limit=3)
def report(ctx):
# each sender can call this 3 times per cooldown period
...
Requires permissions_enabled=True, like the global rate limit. Users
with the admin role or BYPASS_SPAM skip the check.
Help System¶
The framework includes an interactive help generator that provides beautiful, categorized help menus based on Cog and Command metadata.
Threaded Commands¶
For long-running or blocking operations that do not interact with the Reticulum Network Stack directly, you can run commands in a separate thread to keep the bot responsive.
import time
@bot.command(name="long_task", description="Performs a long-running task in a separate thread", threaded=True)
def long_task_command(ctx):
ctx.reply("Starting a long task... please wait.")
time.sleep(10) # This runs in a separate thread
ctx.reply("Long task completed!")
Thread safety
Functions marked as threaded=True must not directly interact
with the Reticulum Network Stack (RNS) or any components that rely
on lxmfy.transport.py, as these are generally not thread-safe.
Use ctx.reply() for sending messages back to the user from within
a threaded command.
Events¶
Event system for handling various bot events:
@bot.events.on("message_received", EventPriority.HIGHEST)
def handle_message(event):
# Handle message event
pass
Testing¶
lxmfy.testing.TestBot is an LXMFBot preconfigured for tests. No
Reticulum instance starts. Inbound messages go through the real receive
pipeline (middleware, spam checks, permissions, dispatch) and outbound
sends are captured for assertions.
from lxmfy import TestBot
def test_ping():
with TestBot() as bot:
@bot.command("ping")
def ping(msg):
msg.reply("pong")
sent = bot.receive("/ping", sender="alice")
assert sent[0].content == "pong"
assert sent[0].destination == bot.sender_hex("alice")
bot.receive(content, sender=..., fields=..., message_hash=...)injects a message and returns theSentMessageobjects it produced. Senders are named:"alice"maps to a stable fake hash, or pass a hex destination hash directly.bot.drain()pops queued outbound messages.bot.outboxaccumulates everything sent.bot.last_sent(sender=...)fetches the latest.bot.wait_sent(n, timeout=...)waits for threaded commands.bot.receive_later(content, sender=..., delay=...)answers blockingmsg.askcalls from a daemon thread.fake_message(content, source_hash=..., ...)builds an inbound message for drivingbot._message_receiveddirectly.
The repository test suite also includes reliability and stress scenarios. Use the repository's test runner to execute them.
Advanced Reliability Suite¶
The framework includes an extensive suite of automated tests for harsh environments:
- Manifold Testing: Validates the mathematical topology of NLP intent vector space.
- Chaos Engineering: Simulates bit-rot, SD card failure, and storage corruption.
- Temporal Drift: Verifies resilience against system clock jumps (±1 year).
- Leak Detection: Long-term tracking of memory, file descriptors, and threads.
Permissions¶
Permission system for controlling access to bot features:
from lxmfy import DefaultPerms
@bot.command(name="admin", description="Admin command", admin_only=True)
def admin_command(ctx):
if ctx.is_admin:
ctx.reply("Admin command executed")
Middleware¶
Middleware system for processing messages and events:
@bot.middleware.register(MiddlewareType.PRE_COMMAND)
def pre_command_middleware(ctx):
# Process before command execution
pass
Attachments¶
Support for sending files, images, and audio:
from lxmfy import Attachment, AttachmentType
attachment = Attachment(
type=AttachmentType.IMAGE,
name="image.jpg",
data=image_data,
format="jpg"
)
bot.send_with_attachment(destination, "Here's an image", attachment)
Icon Appearance (LXMF Field)¶
You can set a custom icon for your bot that compliant LXMF clients can
display. This uses the LXMF.FIELD_ICON_APPEARANCE.
from lxmfy import IconAppearance, pack_icon_appearance_field
import LXMF # Required for LXMF.FIELD_ICON_APPEARANCE
# Define the icon appearance
icon_data = IconAppearance(
icon_name="smart_toy", # Name from Material Symbols
fg_color=b'\xFF\xFF\xFF', # White foreground (3 bytes)
bg_color=b'\x4A\x90\xE2' # Blue background (3 bytes)
)
# Pack it into the LXMF field format
icon_lxmf_field = pack_icon_appearance_field(icon_data)
# Send a message with this icon
bot.send(
destination_hash_str,
"Hello from your friendly bot!",
title="Bot Message",
lxmf_fields=icon_lxmf_field
)
# You can also combine it with other fields, like attachments:
# attachment_field = pack_attachment(some_attachment)
# combined_fields = {**icon_lxmf_field, **attachment_field}
# bot.send(destination, "Message with icon and attachment", lxmf_fields=combined_fields)
Scheduler¶
Task scheduling system:
@bot.scheduler.schedule(name="daily_task", cron_expr="0 0 * * *")
def daily_task():
# Run daily at midnight
pass
Signatures¶
LXMFy provides configuration options for LXMF's built-in cryptographic message signing and verification:
from lxmfy import LXMFBot
bot = LXMFBot(
name="SecureBot",
signature_verification_enabled=True, # Enable signature checks
require_message_signatures=False # Set to True to reject unsigned messages
)
Signature handling
LXMF automatically handles all cryptographic signing and
verification using RNS identities. LXMFy's SignatureManager is a
configuration layer that:
- Controls whether to enforce signature verification
- Determines policy for unsigned messages (accept or reject)
- Integrates with the permission system (e.g., bypass verification for trusted users)
The actual cryptographic operations are performed by LXMF/RNS, not by LXMFy.
Landlock LSM Sandbox¶
On Linux kernels with Landlock support (5.13+), LXMFy can restrict filesystem access for the bot process and for external script cogs.
Bot process sandbox
When landlock_enabled=True (default) and not running in test_mode,
the bot calls apply_landlock_sandbox() during initialization. System
directories are read-only; bot storage, config, cogs, Reticulum config,
and temp paths remain writable.
bot = LXMFBot(
name="SecureBot",
landlock_enabled=True,
)
status = bot.get_landlock_status()
# status keys: landlock_kernel_supported, landlock_requested,
# landlock_auto_enabled, landlock_disabled_by_env, landlock_active
Environment override
LXMFY_LANDLOCK=0: disable Landlock even on supported kernelsLXMFY_LANDLOCK=1: attempt Landlock on Linux regardless of auto-detection- unset: follow
landlock_enabledand kernel auto-detection
External cog sandbox
Script cogs use external_cogs_sandbox_type. In auto mode, Landlock
is preferred when available because it requires no external tools. See
the Creating Bots guide for the full sandbox
option list.
Identity Pinning¶
LXMFy supports optional identity pinning to prevent impersonation if an identity is rotated or compromised. When enabled, the bot "pins" an LXMF address to its first-seen public key.
SignatureManager Methods¶
The SignatureManager is available as bot.signature_manager when
signature_verification_enabled=True:
should_verify_message(sender): Determine if a message from the given sender should be verifiedhandle_unsigned_message(sender, message_hash): Handle messages that lack valid signatures based on policy
How LXMF Signatures Work¶
LXMF automatically signs all outgoing messages using the sender's RNS
identity during the pack() operation. When messages are received, LXMF
validates signatures and provides:
message.signature_validated: Boolean indicating if the signature is validmessage.unverified_reason: Reason code if validation failed (e.g.,SIGNATURE_INVALID,SOURCE_UNKNOWN)
LXMFy uses these built-in LXMF properties to enforce your bot's signature policy.
Message Delivery¶
LXMFy provides advanced message delivery features including propagation nodes and automatic retries:
Propagation Nodes¶
Send messages through specific propagation nodes for improved reliability on the Reticulum network:
# Configure the propagation node once at config/runtime level
bot.set_propagation_node("<propagation_node_hash>")
# Send using configured delivery behavior
bot.send(
destination_hash,
"Message content"
)
# The propagation node hash should be a valid LXMF propagation node
# on the Reticulum network
Automatic Retries¶
Configure automatic retry attempts for failed direct deliveries:
bot = LXMFBot(
name="ReliableBot",
direct_delivery_retries=5, # Retry direct delivery up to 5 times
propagation_fallback_enabled=True
)
bot.send(destination_hash, "Important message")
# Default direct_delivery_retries is 3
# Retry logic automatically handles delivery callbacks
The retry system tracks delivery attempts per destination and automatically retries failed deliveries. Successful deliveries reset the retry counter for that destination.
Message Persistence¶
Outgoing messages can be persisted to disk to ensure they are delivered
even after a bot restart. Persistence is enabled by default. The
in-memory outbound queue is bounded (message_queue_size, default 50)
and drops the oldest message when full. Invalid destination hashes are
not restored.
Delivery Events¶
bot.delivery records a bounded stream of outbound lifecycle events so
you can watch message flow without reading logs. Stages: queued,
deferred, dispatched, delivered, failed, cancelled, dropped.
The recent tail is persisted to storage and restored on startup.
@bot.on_delivery_event()
def watch(event):
print(event["stage"], event.get("destination"), event.get("reason"))
# Or inspect directly
recent = bot.delivery.recent(20)
failures = bot.delivery.recent(stage="failed")
to_peer = bot.delivery.recent(destination="aa11bb...")
Each event is a dict with ts, stage, and optional destination,
message_id, hash, method, attempts, reason, title.
Admins get a /delivery [limit] command that renders the same timeline
in chat, and lxmfy debug shows a delivery timeline summary in the
send pipeline checks.
Built-in Admin Commands¶
These commands are registered automatically and require the sender to
be in admins when permissions are enabled:
| Command | Action |
|---|---|
/queue |
Show router outbound queue, internal queue, and held sends |
/cancel <id|all> |
Cancel pending outbound messages |
/inbox [cancel <hash|all>] |
List or cancel active inbound transfers |
/delivery [n] |
Show the last n delivery events (default 15, max 50) |
/loadext <name> |
Load a cog extension |
/reloadext <name> |
Reload a loaded cog extension |
Router Controls¶
Thin wrappers over the underlying LXMRouter for sender control,
tickets, outbound queue management, and propagation node sync. All take
destination hashes as hex strings and return False when the router is
not running (for example in test_mode).
Sender control (inbound)
ignore_destination(destination)/unignore_destination(destination)/is_ignored(destination): Drop inbound messages from a senderallow_destination(destination)/disallow_destination(destination): Whitelist management when the router runs in allow-list modeprioritise_destination(destination)/unprioritise_destination(destination): Prioritized sender listset_inbound_stamp_cost(stamp_cost): Require a stamp cost on inbound messages (Noneclears)enforce_stamps()/ignore_stamps(): Inbound stamp enforcement toggles
Tickets
generate_ticket(destination, expiry=None): Issue an inbound stamp ticket for a senderget_inbound_tickets(destination): Tickets held for a senderget_outbound_ticket(destination)/get_outbound_ticket_expiry(destination)/get_outbound_stamp_cost(destination): Outbound ticket state learned from the network
Outbound queue
outbound_queue(): Snapshot of pending outbound messagesget_outbound_progress(lxm_hash): Delivery progress for a message hash, orNonecancel_outbound(message_id): Remove a queued message before deliverydelivery_link_available(destination): Whether an active RNS link exists to the destination
Inbound queue
has_message(message_hash): Whether an inbound LXM hash was already deliveredinbound_count(): Active inbound resource transfers in progressinbound_transfers(): Snapshot of each transfer with hash, size, progress, and statuscancel_inbound(resource_hash): Abort an active inbound transfercancel_all_inbound(): Abort every active inbound transfer, returns the count cancelled
Peer discovery
Announce metadata for destinations this node has heard:
get_peer_app_data(destination): Raw announced app_data bytesget_peer_lxmf_data(destination): Decoded LXMF announce metadata (display_name,stamp_cost,capabilities), orNonewhen the peer has not announced valid LXMF dataget_peer_announce(destination): Full announce record with hops, received_at, interface, and app_datalist_peer_announces(limit=100): All heard announces, newest first
Propagation
sync_propagation_node(max_messages=None): Pull messages from the configured propagation nodecancel_propagation_sync(): Stop an in-progress syncget_propagation_stats(): Node transfer state and limits, orNoneset_retain_on_node(retain): Keep delivered messages on the nodeannounce_propagation_node(): Announce this node as a propagation nodeallow_control_identity(destination)/disallow_control_identity(destination): Propagation control channel whitelist
Ingest
ingest_lxm_uri(uri): Import anlxm://URI message into the inbound queue
Message Handlers¶
LXMFy provides decorators for handling different types of incoming messages:
First Message Handler¶
Handle the first message from each user:
@bot.on_first_message()
def welcome_user(sender, message):
content = message.content.decode("utf-8")
bot.send(sender, f"Welcome! You said: {content}")
return True # Return True to stop further processing
General Message Handler¶
Handle all incoming messages before command processing:
@bot.on_message()
def handle_all_messages(sender, message):
content = message.content.decode("utf-8").strip()
# Custom logic here
if content.startswith("echo:"):
bot.send(sender, content[5:])
return True # Stop further processing
return False # Continue to command processing
Message handlers are called in this order: 1. First message handler (if
this is the first message from this sender) 2. General message handlers
(registered with on_message()) 3. Command processing (if message
starts with command prefix)
Reticulum Relay Chat (RRC)¶
Bots can join RRC hubs over RNS Links with
CBOR envelopes. Package: lxmfy.rrc.
BotConfig options¶
rrc_enabled(bool, defaultFalse): Connect configured hubs on startuprrc_hubs(list of hex hashes): Hub destination hashesrrc_rooms(list of str): Rooms to auto-join after WELCOMErrc_nick(str or None): Nickname on HELLO and room messagesrrc_dest_name(str, default"rrc.hub"): Destination name used to build the hub destinationrrc_auto_reconnect(bool, defaultTrue): Reconnect after link lossrrc_persist_sessions(bool, defaultTrue): Persist hubs and rooms across restartsreticulum_config_dir(str or None): Reticulum config directory. Also set viaLXMFY_RETICULUM_CONFIG_DIR. Use the same config as MeshChatX (often~/.reticulum) so hub announces are visible.
Example¶
from lxmfy import LXMFBot, RRCMessage
bot = LXMFBot(
name="RoomBot",
reticulum_config_dir="~/.reticulum",
rrc_enabled=True,
rrc_hubs=["664fc0e8d2e448658e37bb3f34e6c88f"],
rrc_rooms=["general"],
rrc_nick="RoomBot",
)
@bot.on_rrc
def on_rrc(event, client, payload):
if event == "msg" and isinstance(payload, RRCMessage) and payload.mention:
client.send_message(payload.room, f"Hi {payload.nick}")
# Runtime API
# bot.connect_rrc(hub_hash, rooms=["general"])
# bot.rrc.send_message("general", "hello")
# bot.rrc.send_notice("general", "notice")
# bot.rrc.send_action("general", "waves")
# bot.rrc.join("ops")
# bot.rrc.part("ops")
# bot.rrc.status()
# bot.disconnect_rrc()
Exported types¶
RRCClient: Single-hub sessionRRCManager: Multi-hub manager (bot.rrc)RRCMessage: Room event payload (kind,room,text,nick,src,mention, ...)RRC_VERSION: Wire protocol version constant
Common events passed to @bot.on_rrc handlers include status,
welcome, joined, parted, msg, notice, action, motd,
error, and rtt.
Templates¶
The framework includes several ready-to-use bot templates:
EchoBot¶
Simple echo bot that repeats messages:
NoteBot¶
Note-taking bot with JSON storage:
ReminderBot¶
Reminder bot with SQLite storage:
RRCBot¶
RRC room bot that joins configured hubs and replies to @mentions.
Defaults to hub 664fc0e8d2e448658e37bb3f34e6c88f, room #general, and
~/.reticulum when available.
from lxmfy.templates import RRCBot
bot = RRCBot(
hubs=["664fc0e8d2e448658e37bb3f34e6c88f"],
rooms=["general"],
nick="RRCBot",
reticulum_config_dir="~/.reticulum",
)
bot.run()
CLI Tools¶
The framework provides command-line tools for bot management:
# Create a new bot
lxmfy create mybot
# Create a bot from template
lxmfy create --template echo mybot
lxmfy create --template rrc my_rrc_bot
# Run a template bot
lxmfy run echo
lxmfy run rrc
# Test signature verification with a message
lxmfy signatures test
# Enable signature verification
lxmfy signatures enable
# Disable signature verification
lxmfy signatures disable
Error Handling¶
Catch shutdown and runtime failures around bot.run():
try:
bot.run()
except KeyboardInterrupt:
bot.cleanup()
except Exception as e:
logger.error(f"Error running bot: {str(e)}")
Module Reference¶
Generated from source docstrings.
Bases: AnnounceMixin, DispatchMixin, CogMixin, InboundMixin, OutboundMixin, LinkMixin, RRCMixin, PropagationMixin
Main bot class for handling LXMF messages and commands.
This class manages the bot's lifecycle, including: - Message routing and delivery - Command registration and execution - Cog (extension) loading and management - Spam protection - Admin privileges
Source code in lxmfy/core.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 | |
__init__(name=None, **kwargs)
¶
Initialize a new LXMFBot instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Optional bot name, same as the name config key. |
None
|
**kwargs
|
Any
|
Override default configuration settings |
{}
|
Source code in lxmfy/core.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | |
cleanup()
¶
Clean up resources.
Source code in lxmfy/core.py
diagnose_connectivity(destination=None, *, request_path=False, wait=0.0)
¶
Run a connectivity doctor report for this bot.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
destination
|
str | None
|
Optional peer hash to include in send diagnosis. |
None
|
request_path
|
bool
|
Request a path when probing destination. |
False
|
wait
|
float
|
Seconds to wait for path discovery. |
0.0
|
Returns:
| Type | Description |
|---|---|
dict
|
Structured doctor report dict. |
Source code in lxmfy/core.py
diagnose_destination(destination, *, request_path=False, wait=0.0)
¶
Probe path and identity for a destination hash.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
destination
|
str
|
Hex destination hash. |
required |
request_path
|
bool
|
Whether to request a path if missing. |
False
|
wait
|
float
|
Seconds to wait for a path after requesting. |
0.0
|
Returns:
| Type | Description |
|---|---|
dict
|
Dict describing identity/path status and hints. |
Source code in lxmfy/core.py
get_debugger()
¶
get_landlock_status()
¶
Return Landlock LSM sandbox availability and activation state.
on_delivery_event(callback=None)
¶
Subscribe to the outbound delivery event stream.
Usable as bot.on_delivery_event(fn) or as a decorator @bot.on_delivery_event(). Subscribers receive event dicts with ts, stage (queued, deferred, dispatched, delivered, failed, cancelled, dropped), and destination/message_id/hash/reason fields when available.
Source code in lxmfy/core.py
request_page(destination_hash, page_path, field_data=None)
¶
Request a page from a destination.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
destination_hash
|
str
|
The destination hash. |
required |
page_path
|
str
|
The path to the page. |
required |
field_data
|
dict | None
|
Optional field data to send with the request. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
The response from the destination. |
Source code in lxmfy/core.py
run(delay=10)
¶
Run the bot
Source code in lxmfy/core.py
Configuration settings for LXMFBot.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The name of the bot. Defaults to "LXMFBot". |
announce |
int
|
The announce interval in seconds. Defaults to 600. |
announce_immediately |
bool
|
Whether to announce immediately on startup. Defaults to True. |
admins |
set
|
A set of admin identity hashes. Defaults to an empty set. |
hot_reloading |
bool
|
Whether to enable hot reloading of cogs. Defaults to False. |
rate_limit |
int
|
The maximum number of messages allowed per cooldown period. Defaults to 5. |
cooldown |
int
|
The cooldown period in seconds. Defaults to 60. |
max_warnings |
int
|
The maximum number of spam warnings before action is taken. Defaults to 3. |
warning_timeout |
int
|
The duration in seconds for which a spam warning is active. Defaults to 300. |
command_prefix |
str
|
The prefix for bot commands. Defaults to "/". |
cogs_dir |
str
|
The directory to load cogs from. Defaults to "cogs". |
cogs_enabled |
bool
|
Whether to enable cogs. Defaults to True. |
permissions_enabled |
bool
|
Whether to enable the permission system. Defaults to False. |
storage_type |
str
|
The type of storage to use ("json" or "sqlite"). Defaults to "json". |
storage_path |
str
|
The path to the storage file or directory. Defaults to "data". |
first_message_enabled |
bool
|
Whether to enable first message handling. Defaults to True. |
event_logging_enabled |
bool
|
Whether to enable event logging. Defaults to True. |
max_logged_events |
int
|
The maximum number of events to log. Defaults to 1000. |
event_middleware_enabled |
bool
|
Whether to enable event middleware. Defaults to True. |
announce_enabled |
bool
|
Whether to enable bot announcements. Defaults to True. |
signature_verification_enabled |
bool
|
Whether to enable cryptographic signature verification for incoming messages. Defaults to False. |
require_message_signatures |
bool
|
Whether to reject unsigned messages when signature verification is enabled. Defaults to False. |
require_stamps |
bool
|
Whether to reject messages with invalid stamps. Defaults to False. |
request_unknown_identities |
bool
|
Whether to request unknown identities from the network when a message is received from an unknown source. Defaults to False. |
stamp_cost |
int
|
Stamp cost required for messages to this bot. Outbound cost still comes from the peer announce unless send() overrides it. None disables inbound stamps. Defaults to None. |
include_tickets |
bool
|
Include reply tickets on outbound messages so peers that require stamps can answer without stamp generation. Defaults to True. |
direct_delivery_retries |
int
|
Number of times to retry direct delivery before falling back to propagation. Defaults to 3. |
propagation_fallback_enabled |
bool
|
Whether to use propagation nodes as fallback after direct delivery fails. Defaults to True. |
propagation_node |
str
|
The destination hash of the outbound propagation node. If None and autopeer_propagation is True, automatically discovers nodes. Defaults to None. |
autopeer_propagation |
bool
|
Whether to automatically discover and peer with propagation nodes from announces. Defaults to False. |
autopeer_maxdepth |
int
|
Maximum hop depth for auto-peering with propagation nodes. None = no limit. Defaults to 4. |
enable_propagation_node |
bool
|
Whether to run this bot as a propagation node. Defaults to False. |
message_storage_limit_mb |
float
|
Maximum storage for propagation node messages in megabytes. Only applies when enable_propagation_node is True. Defaults to 500 MB. |
config_path |
str
|
The path to the bot configuration directory. If None, defaults to "config" in the current working directory. Defaults to None. |
reticulum_config_dir |
str
|
The Reticulum config directory used for RNS shared instance/auth state. If None, uses LXMFY_RETICULUM_CONFIG_DIR when set, otherwise discovers the user/system Reticulum config (/etc/reticulum, ~/.config/reticulum, ~/.reticulum), and only then falls back to config_path. Isolated bot configs force share_instance=No to avoid RPC digest rejection with NomadNet/Columba. |
test_mode |
bool
|
Whether to run in test mode (skips RNS initialization). Defaults to False. |
log_level |
str or int or None
|
Logging level for lxmfy's own logger ("DEBUG", "INFO", "WARNING", ...). None leaves logging untouched. Defaults to "INFO". |
loglevel |
int
|
RNS log level (0-7) passed to RNS.Reticulum. None uses the Reticulum config file setting. Defaults to None. |
pending_sends_enabled |
bool
|
Hold outbound messages when the destination identity is not yet known, retrying on announces and periodic sweeps. Defaults to True. |
pending_sends_max |
int
|
Maximum deferred messages kept for unknown destinations. Oldest are dropped beyond this. Defaults to 200. |
pending_sends_ttl |
int
|
Seconds a deferred message is kept before being dropped. Defaults to 604800 (7 days). |
pending_sends_retry |
int
|
Minimum seconds between deferred-send sweeps in run(). Defaults to 300. |
announce_display_name_file |
str
|
Optional filename under config_path whose UTF-8 contents override the bot display name for LXMF delivery announces. If unset, |
Source code in lxmfy/config.py
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | |
__post_init__()
¶
Post-initialization to ensure admins is a set.
Source code in lxmfy/config.py
__str__()
¶
Return a string representation of the BotConfig object.
Source code in lxmfy/config.py
Represents a generic attachment.
Attributes:
| Name | Type | Description |
|---|---|---|
type |
AttachmentType
|
The type of the attachment (AttachmentType). |
name |
str
|
The name of the attachment. |
data |
bytes
|
The binary data of the attachment. |
format |
str | None
|
Optional format specifier (e.g., "png" for images). |
Source code in lxmfy/attachments.py
Bases: IntEnum
Enumerates the different types of attachments supported.
FILE: Represents a generic file attachment. IMAGE: Represents an image attachment. AUDIO: Represents an audio attachment.
Source code in lxmfy/attachments.py
Bases: Flag
Default permission set
Source code in lxmfy/permissions.py
Manages permissions, roles, and user assignments
Source code in lxmfy/permissions.py
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | |
assign_role(user, role_name)
¶
Assign a role to a user
Source code in lxmfy/permissions.py
create_role(name, permissions, priority=0, description=None)
¶
Create a new role
Source code in lxmfy/permissions.py
delete_role(name)
¶
Delete a role
Source code in lxmfy/permissions.py
get_user_permissions(user)
¶
Get combined permissions for a user
Source code in lxmfy/permissions.py
has_permission(user, permission)
¶
Check if user has specific permission
load_data()
¶
Load permission data from storage
Source code in lxmfy/permissions.py
remove_role(user, role_name)
¶
Remove a role from a user
Source code in lxmfy/permissions.py
save_data()
¶
Save permission data to storage
Source code in lxmfy/permissions.py
Manages scheduled tasks and background processes.
Source code in lxmfy/scheduler.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | |
__init__(bot)
¶
Initialize the TaskScheduler.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bot
|
Any
|
The bot instance. |
required |
Source code in lxmfy/scheduler.py
add_task(name, callback, cron_expr)
¶
Add a scheduled task.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the task. |
required |
callback
|
Callable
|
The function to execute when the task runs. |
required |
cron_expr
|
str
|
A cron-style expression defining when the task should run. |
required |
Source code in lxmfy/scheduler.py
remove_task(name)
¶
Remove a scheduled task.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the task to remove. |
required |
schedule(name, cron_expr)
¶
Decorator to schedule a task.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The name of the task. |
required |
cron_expr
|
str
|
The cron expression for the task. |
required |
Source code in lxmfy/scheduler.py
start()
¶
A scheduled task with cron-style timing.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The name of the task. |
callback |
Callable
|
The function to execute when the task runs. |
cron_expr |
str
|
A cron-style expression defining when the task should run (min hour day month weekday). |
last_run |
Optional[datetime]
|
The last time the task was run. |
enabled |
bool
|
Whether the task is currently enabled. |
Source code in lxmfy/scheduler.py
should_run(current_time)
¶
Check if the task should run at the given time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_time
|
datetime
|
The current datetime. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if the task should run, False otherwise. |
Source code in lxmfy/scheduler.py
Facade for the underlying storage backend.
Source code in lxmfy/storage.py
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 | |
__init__(backend)
¶
Initialize a new Storage instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
backend
|
StorageBackend
|
The storage backend to use. |
required |
delete(key)
¶
exists(key)
¶
Check if a key exists in storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the key exists, False otherwise. |
get(key, default=None)
¶
Retrieve a value from storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to retrieve. |
required |
default
|
Any
|
The default value to return if the key is not found. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The value associated with the key, or the default value if not found. |
Source code in lxmfy/storage.py
get_role_data(role_name)
¶
Helper method for permission system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
role_name
|
str
|
The name of the role. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The role data. |
get_user_roles(user_hash)
¶
Helper method for permission system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_hash
|
str
|
The hash of the user. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
The list of roles for the user. |
Source code in lxmfy/storage.py
scan(prefix)
¶
Scan for keys with a given prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
The prefix to scan for. |
required |
Returns:
| Type | Description |
|---|---|
list
|
A list of keys that start with the prefix. |
set(key, value)
¶
Store a value in storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to store the value under. |
required |
value
|
Any
|
The value to store. |
required |
set_role_data(role_name, data)
¶
Helper method for permission system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
role_name
|
str
|
The name of the role. |
required |
data
|
dict
|
The role data. |
required |
set_user_roles(user_hash, roles)
¶
Helper method for permission system.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_hash
|
str
|
The hash of the user. |
required |
roles
|
list[str]
|
The list of roles for the user. |
required |
Bases: StorageBackend
JSON file-based storage backend.
Source code in lxmfy/storage.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | |
__init__(directory)
¶
Initialize a new JSONStorage instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
str
|
The directory to store the JSON files in. |
required |
Source code in lxmfy/storage.py
delete(key)
¶
Delete a value from storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to delete. |
required |
Source code in lxmfy/storage.py
exists(key)
¶
Check if a key exists in storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the key exists, False otherwise. |
get(key, default=None)
¶
Retrieve a value from storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to retrieve. |
required |
default
|
Any
|
The default value to return if the key is not found. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The value associated with the key, or the default value if not found. |
Source code in lxmfy/storage.py
scan(prefix)
¶
Scan for keys with a given prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
The prefix to scan for. |
required |
Returns:
| Type | Description |
|---|---|
list
|
A list of keys that start with the prefix. |
Source code in lxmfy/storage.py
set(key, value)
¶
Store a value in storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to store the value under. |
required |
value
|
Any
|
The value to store. |
required |
Source code in lxmfy/storage.py
Bases: StorageBackend
SQLite database storage backend.
Source code in lxmfy/storage.py
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 | |
__init__(database_path)
¶
Initialize a new SQLiteStorage instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
database_path
|
str
|
The path to the SQLite database file. |
required |
Source code in lxmfy/storage.py
delete(key)
¶
Delete a value from storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to delete. |
required |
Source code in lxmfy/storage.py
exists(key)
¶
Check if a key exists in storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the key exists, False otherwise. |
Source code in lxmfy/storage.py
get(key, default=None)
¶
Retrieve a value from storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to retrieve. |
required |
default
|
Any
|
The default value to return if the key is not found. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The value associated with the key, or the default value if not found. |
Source code in lxmfy/storage.py
scan(prefix)
¶
Scan for keys with a given prefix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
prefix
|
str
|
The prefix to scan for. |
required |
Returns:
| Type | Description |
|---|---|
list
|
A list of keys that start with the prefix. |
Source code in lxmfy/storage.py
set(key, value)
¶
Store a value in storage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to store the value under. |
required |
value
|
Any
|
The value to store. |
required |