Files
meshcore-bot/develop-command-scripts/index.html
T
2026-09-20 19:56:33 +00:00

3653 lines
186 KiB
HTML

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Documentation for the MeshCore bot and its services">
<link rel="canonical" href="https://agessaman.github.io/meshcore-bot/develop-command-scripts/">
<link rel="prev" href="../command-reference-website/">
<link rel="next" href="../service-plugins/">
<link rel="icon" href="../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.7">
<title>Developing Commands - Meshcore Bot Documentation</title>
<link rel="stylesheet" href="../assets/stylesheets/main.ec1eaa64.min.css">
<link rel="stylesheet" href="../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="cyan">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#developing-command-scripts-for-meshcore-bot" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href=".." title="Meshcore Bot Documentation" class="md-header__button md-logo" aria-label="Meshcore Bot Documentation" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Meshcore Bot Documentation
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
Developing Commands
</span>
</div>
</div>
</div>
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="cyan" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a4 4 0 0 0-4 4 4 4 0 0 0 4 4 4 4 0 0 0 4-4 4 4 0 0 0-4-4m0 10a6 6 0 0 1-6-6 6 6 0 0 1 6-6 6 6 0 0 1 6 6 6 6 0 0 1-6 6m8-9.31V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12z"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="cyan" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 18c-.89 0-1.74-.2-2.5-.55C11.56 16.5 13 14.42 13 12s-1.44-4.5-3.5-5.45C10.26 6.2 11.11 6 12 6a6 6 0 0 1 6 6 6 6 0 0 1-6 6m8-9.31V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12z"/></svg>
</label>
</form>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://github.com/agessaman/meshcore-bot" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
meshcore-bot
</div>
</a>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href=".." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item">
<a href="../getting-started/" class="md-tabs__link">
Quick Start
</a>
</li>
<li class="md-tabs__item">
<a href="../installation/" class="md-tabs__link">
Installation
</a>
</li>
<li class="md-tabs__item">
<a href="../configuration/" class="md-tabs__link">
Configuration
</a>
</li>
<li class="md-tabs__item">
<a href="../web-viewer/" class="md-tabs__link">
Web Viewer
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../command-reference/" class="md-tabs__link">
Command Reference
</a>
</li>
<li class="md-tabs__item">
<a href="../service-plugins/" class="md-tabs__link">
Service Plugins
</a>
</li>
<li class="md-tabs__item">
<a href="../faq/" class="md-tabs__link">
FAQ
</a>
</li>
<li class="md-tabs__item">
<a href="../upgrade/" class="md-tabs__link">
Upgrade
</a>
</li>
<li class="md-tabs__item">
<a href="../solar-conditions-provenance/" class="md-tabs__link">
Project
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted md-nav--integrated" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href=".." title="Meshcore Bot Documentation" class="md-nav__button md-logo" aria-label="Meshcore Bot Documentation" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
Meshcore Bot Documentation
</label>
<div class="md-nav__source">
<a href="https://github.com/agessaman/meshcore-bot" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
meshcore-bot
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href=".." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../getting-started/" class="md-nav__link">
<span class="md-ellipsis">
Quick Start
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_3" >
<label class="md-nav__link" for="__nav_3" id="__nav_3_label" tabindex="0">
<span class="md-ellipsis">
Installation
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_3_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_3">
<span class="md-nav__icon md-icon"></span>
Installation
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../installation/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../docker/" class="md-nav__link">
<span class="md-ellipsis">
Docker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../service-installation/" class="md-nav__link">
<span class="md-ellipsis">
Service
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_4" >
<label class="md-nav__link" for="__nav_4" id="__nav_4_label" tabindex="0">
<span class="md-ellipsis">
Configuration
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_4_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_4">
<span class="md-nav__icon md-icon"></span>
Configuration
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../configuration/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../data-retention/" class="md-nav__link">
<span class="md-ellipsis">
Data retention
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../local-plugins/" class="md-nav__link">
<span class="md-ellipsis">
Local plugins and services
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../checkin-api/" class="md-nav__link">
<span class="md-ellipsis">
Check-in API
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../path-command-config/" class="md-nav__link">
<span class="md-ellipsis">
Path Command
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../config-validation/" class="md-nav__link">
<span class="md-ellipsis">
Config validation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../region-warnings/" class="md-nav__link">
<span class="md-ellipsis">
Region warnings
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../web-viewer/" class="md-nav__link">
<span class="md-ellipsis">
Web Viewer
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_6" checked>
<label class="md-nav__link" for="__nav_6" id="__nav_6_label" tabindex="">
<span class="md-ellipsis">
Command Reference
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_6_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_6">
<span class="md-nav__icon md-icon"></span>
Command Reference
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../command-reference/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../repeater-commands/" class="md-nav__link">
<span class="md-ellipsis">
Repeater Commands
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../command-reference-website/" class="md-nav__link">
<span class="md-ellipsis">
Custom Command Website
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
Developing Commands
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
Developing Commands
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#table-of-contents" class="md-nav__link">
<span class="md-ellipsis">
Table of Contents
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#getting-started" class="md-nav__link">
<span class="md-ellipsis">
Getting Started
</span>
</a>
<nav class="md-nav" aria-label="Getting Started">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#basic-command-template" class="md-nav__link">
<span class="md-ellipsis">
Basic Command Template
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#command-class-structure" class="md-nav__link">
<span class="md-ellipsis">
Command Class Structure
</span>
</a>
<nav class="md-nav" aria-label="Command Class Structure">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#required-imports" class="md-nav__link">
<span class="md-ellipsis">
Required Imports
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#optional-common-imports" class="md-nav__link">
<span class="md-ellipsis">
Optional Common Imports
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#class-level-variables-reference" class="md-nav__link">
<span class="md-ellipsis">
Class-Level Variables Reference
</span>
</a>
<nav class="md-nav" aria-label="Class-Level Variables Reference">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#class-level-variables-for-documentation" class="md-nav__link">
<span class="md-ellipsis">
Class-Level Variables for Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#command-categories" class="md-nav__link">
<span class="md-ellipsis">
Command Categories
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#settings-schema" class="md-nav__link">
<span class="md-ellipsis">
Settings Schema
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#core-methods" class="md-nav__link">
<span class="md-ellipsis">
Core Methods
</span>
</a>
<nav class="md-nav" aria-label="Core Methods">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#required-methods" class="md-nav__link">
<span class="md-ellipsis">
Required Methods
</span>
</a>
<nav class="md-nav" aria-label="Required Methods">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#async-def-executeself-message-meshmessage-bool" class="md-nav__link">
<span class="md-ellipsis">
async def execute(self, message: MeshMessage) -&gt; bool
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#optional-override-methods" class="md-nav__link">
<span class="md-ellipsis">
Optional Override Methods
</span>
</a>
<nav class="md-nav" aria-label="Optional Override Methods">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#def-can_executeself-message-meshmessage-skip_channel_check-bool-false-bool" class="md-nav__link">
<span class="md-ellipsis">
def can_execute(self, message: MeshMessage, skip_channel_check: bool = False) -&gt; bool
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#def-get_help_textself-message-meshmessage-none-str" class="md-nav__link">
<span class="md-ellipsis">
def get_help_text(self, message: MeshMessage = None) -&gt; str
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#def-matches_keywordself-message-meshmessage-bool" class="md-nav__link">
<span class="md-ellipsis">
def matches_keyword(self, message: MeshMessage) -&gt; bool
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#def-matches_custom_syntaxself-message-meshmessage-bool" class="md-nav__link">
<span class="md-ellipsis">
def matches_custom_syntax(self, message: MeshMessage) -&gt; bool
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#network-communication-apis" class="md-nav__link">
<span class="md-ellipsis">
Network Communication APIs
</span>
</a>
<nav class="md-nav" aria-label="Network Communication APIs">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#sending-messages" class="md-nav__link">
<span class="md-ellipsis">
Sending Messages
</span>
</a>
<nav class="md-nav" aria-label="Sending Messages">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#async-def-send_responsemessage-meshmessage-content-str-skip_user_rate_limit-bool-false-command_id-str-none-none-bool" class="md-nav__link">
<span class="md-ellipsis">
async def send_response(message: MeshMessage, content: str, skip_user_rate_limit: bool = False, *, command_id: str | None = None) -&gt; bool
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#async-def-send_response_chunkedmessage-meshmessage-chunks-liststr-skip_user_rate_limit_first-bool-true-bool" class="md-nav__link">
<span class="md-ellipsis">
async def send_response_chunked(message: MeshMessage, chunks: list[str], *, skip_user_rate_limit_first: bool = True) -&gt; bool
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#message-length-helpers" class="md-nav__link">
<span class="md-ellipsis">
Message Length Helpers
</span>
</a>
<nav class="md-nav" aria-label="Message Length Helpers">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#def-get_max_message_lengthself-message-meshmessage-int" class="md-nav__link">
<span class="md-ellipsis">
def get_max_message_length(self, message: MeshMessage) -&gt; int
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#meshmessage-object" class="md-nav__link">
<span class="md-ellipsis">
MeshMessage Object
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#data-persistence-apis" class="md-nav__link">
<span class="md-ellipsis">
Data Persistence APIs
</span>
</a>
<nav class="md-nav" aria-label="Data Persistence APIs">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#database-connection" class="md-nav__link">
<span class="md-ellipsis">
Database Connection
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#common-database-operations" class="md-nav__link">
<span class="md-ellipsis">
Common Database Operations
</span>
</a>
<nav class="md-nav" aria-label="Common Database Operations">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#query-with-results" class="md-nav__link">
<span class="md-ellipsis">
Query with Results
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#insertupdate-data" class="md-nav__link">
<span class="md-ellipsis">
Insert/Update Data
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#caching-apis" class="md-nav__link">
<span class="md-ellipsis">
Caching APIs
</span>
</a>
<nav class="md-nav" aria-label="Caching APIs">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#geocoding-cache" class="md-nav__link">
<span class="md-ellipsis">
Geocoding Cache
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#generic-cache" class="md-nav__link">
<span class="md-ellipsis">
Generic Cache
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#execute-query-helper" class="md-nav__link">
<span class="md-ellipsis">
Execute Query Helper
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#external-data-access" class="md-nav__link">
<span class="md-ellipsis">
External Data Access
</span>
</a>
<nav class="md-nav" aria-label="External Data Access">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#http-requests-with-aiohttp" class="md-nav__link">
<span class="md-ellipsis">
HTTP Requests with aiohttp
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#blocking-operations-with-asyncioto_thread" class="md-nav__link">
<span class="md-ellipsis">
Blocking Operations with asyncio.to_thread
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#custom-api-clients" class="md-nav__link">
<span class="md-ellipsis">
Custom API Clients
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#configuration-and-localization" class="md-nav__link">
<span class="md-ellipsis">
Configuration and Localization
</span>
</a>
<nav class="md-nav" aria-label="Configuration and Localization">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#loading-configuration" class="md-nav__link">
<span class="md-ellipsis">
Loading Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuration-section-naming" class="md-nav__link">
<span class="md-ellipsis">
Configuration Section Naming
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#localization-and-translation" class="md-nav__link">
<span class="md-ellipsis">
Localization and Translation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#auto-detecting-user-language" class="md-nav__link">
<span class="md-ellipsis">
Auto-detecting User Language
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#error-handling" class="md-nav__link">
<span class="md-ellipsis">
Error Handling
</span>
</a>
<nav class="md-nav" aria-label="Error Handling">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#general-error-handling-pattern" class="md-nav__link">
<span class="md-ellipsis">
General Error Handling Pattern
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#logging-levels" class="md-nav__link">
<span class="md-ellipsis">
Logging Levels
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#input-validation" class="md-nav__link">
<span class="md-ellipsis">
Input Validation
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#best-practices" class="md-nav__link">
<span class="md-ellipsis">
Best Practices
</span>
</a>
<nav class="md-nav" aria-label="Best Practices">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#1-always-use-cooldowns-for-external-apis" class="md-nav__link">
<span class="md-ellipsis">
1. Always Use Cooldowns for External APIs
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-record-execution-early" class="md-nav__link">
<span class="md-ellipsis">
2. Record Execution Early
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#3-handle-message-length-limits" class="md-nav__link">
<span class="md-ellipsis">
3. Handle Message Length Limits
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#4-use-caching-for-expensive-operations" class="md-nav__link">
<span class="md-ellipsis">
4. Use Caching for Expensive Operations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#5-offload-blocking-operations" class="md-nav__link">
<span class="md-ellipsis">
5. Offload Blocking Operations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#6-provide-helpful-error-messages" class="md-nav__link">
<span class="md-ellipsis">
6. Provide Helpful Error Messages
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#7-support-both-dm-and-channel-contexts" class="md-nav__link">
<span class="md-ellipsis">
7. Support Both DM and Channel Contexts
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#8-use-type-hints" class="md-nav__link">
<span class="md-ellipsis">
8. Use Type Hints
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#9-implement-admin-only-commands-securely" class="md-nav__link">
<span class="md-ellipsis">
9. Implement Admin-Only Commands Securely
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#10-document-your-command" class="md-nav__link">
<span class="md-ellipsis">
10. Document Your Command
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#testing-your-command" class="md-nav__link">
<span class="md-ellipsis">
Testing Your Command
</span>
</a>
<nav class="md-nav" aria-label="Testing Your Command">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#unit-testing" class="md-nav__link">
<span class="md-ellipsis">
Unit Testing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#manual-testing" class="md-nav__link">
<span class="md-ellipsis">
Manual Testing
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#testing-checklist" class="md-nav__link">
<span class="md-ellipsis">
Testing Checklist
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#additional-resources" class="md-nav__link">
<span class="md-ellipsis">
Additional Resources
</span>
</a>
<nav class="md-nav" aria-label="Additional Resources">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#key-files-to-reference" class="md-nav__link">
<span class="md-ellipsis">
Key Files to Reference
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#common-utilities" class="md-nav__link">
<span class="md-ellipsis">
Common Utilities
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#configuration-file-structure" class="md-nav__link">
<span class="md-ellipsis">
Configuration File Structure
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#local-commands-directory" class="md-nav__link">
<span class="md-ellipsis">
Local Commands Directory
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#summary" class="md-nav__link">
<span class="md-ellipsis">
Summary
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_7" >
<label class="md-nav__link" for="__nav_7" id="__nav_7_label" tabindex="0">
<span class="md-ellipsis">
Service Plugins
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_7_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_7">
<span class="md-nav__icon md-icon"></span>
Service Plugins
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../service-plugins/" class="md-nav__link">
<span class="md-ellipsis">
Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../discord-bridge/" class="md-nav__link">
<span class="md-ellipsis">
Discord Bridge
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../telegram-bridge/" class="md-nav__link">
<span class="md-ellipsis">
Telegram Bridge
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../packet-capture/" class="md-nav__link">
<span class="md-ellipsis">
Packet Capture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../map-uploader/" class="md-nav__link">
<span class="md-ellipsis">
Map Uploader
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../weather-service/" class="md-nav__link">
<span class="md-ellipsis">
Weather Service
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../earthquake-service/" class="md-nav__link">
<span class="md-ellipsis">
Earthquake Service
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../repeater-prefix-collision-service/" class="md-nav__link">
<span class="md-ellipsis">
Repeater Prefix Collision
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../worldcup/" class="md-nav__link">
<span class="md-ellipsis">
World Cup
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../FEEDS/" class="md-nav__link">
<span class="md-ellipsis">
Feed Management
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../faq/" class="md-nav__link">
<span class="md-ellipsis">
FAQ
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../upgrade/" class="md-nav__link">
<span class="md-ellipsis">
Upgrade
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_10" >
<label class="md-nav__link" for="__nav_10" id="__nav_10_label" tabindex="0">
<span class="md-ellipsis">
Project
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_10_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_10">
<span class="md-nav__icon md-icon"></span>
Project
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../solar-conditions-provenance/" class="md-nav__link">
<span class="md-ellipsis">
Solar conditions provenance
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="developing-command-scripts-for-meshcore-bot">Developing Command Scripts for MeshCore Bot</h1>
<p>This guide covers how to develop custom command scripts (plugins) for MeshCore
Bot. Commands are Python classes that inherit from <code>BaseCommand</code> and respond
to user messages on the mesh network.</p>
<p>Local commands should be placed in the <code>local/commands</code> directory or the
directory designated by the <code>local_dir_path</code> configuration value, which allows
you to add custom functionality without modifying the core bot code.</p>
<h2 id="table-of-contents">Table of Contents</h2>
<ul>
<li><a href="#getting-started">Getting Started</a></li>
<li><a href="#command-class-structure">Command Class Structure</a></li>
<li><a href="#class-level-variables-reference">Class-Level Variables Reference</a></li>
<li><a href="#core-methods">Core Methods</a></li>
<li><a href="#network-communication-apis">Network Communication APIs</a></li>
<li><a href="#data-persistence-apis">Data Persistence APIs</a></li>
<li><a href="#external-data-access">External Data Access</a></li>
<li><a href="#configuration-and-localization">Configuration and Localization</a></li>
<li><a href="#error-handling">Error Handling</a></li>
<li><a href="#best-practices">Best Practices</a></li>
<li><a href="#testing-your-command">Testing Your Command</a></li>
</ul>
<hr />
<h2 id="getting-started">Getting Started</h2>
<h3 id="basic-command-template">Basic Command Template</h3>
<p>Create a new file (conventions use the form <code>[base command]_command.py</code>) in
<code>local/commands/</code> with the following structure:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="ch">#!/usr/bin/env python3</span>
<a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a><span class="sd">"""</span>
<a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a><span class="sd">Your Command - Brief description of what it does</span>
<a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a><span class="sd">"""</span>
<a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a>
<a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.models</span><span class="w"> </span><span class="kn">import</span> <span class="n">MeshMessage</span>
<a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.commands.base_command</span><span class="w"> </span><span class="kn">import</span> <span class="n">BaseCommand</span>
<a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a>
<a id="__codelineno-0-9" name="__codelineno-0-9" href="#__codelineno-0-9"></a>
<a id="__codelineno-0-10" name="__codelineno-0-10" href="#__codelineno-0-10"></a><span class="k">class</span><span class="w"> </span><span class="nc">YourCommand</span><span class="p">(</span><span class="n">BaseCommand</span><span class="p">):</span>
<a id="__codelineno-0-11" name="__codelineno-0-11" href="#__codelineno-0-11"></a><span class="w"> </span><span class="sd">"""Detailed description of your command."""</span>
<a id="__codelineno-0-12" name="__codelineno-0-12" href="#__codelineno-0-12"></a>
<a id="__codelineno-0-13" name="__codelineno-0-13" href="#__codelineno-0-13"></a> <span class="c1"># Plugin metadata</span>
<a id="__codelineno-0-14" name="__codelineno-0-14" href="#__codelineno-0-14"></a> <span class="n">name</span> <span class="o">=</span> <span class="s2">"yourcommand"</span>
<a id="__codelineno-0-15" name="__codelineno-0-15" href="#__codelineno-0-15"></a> <span class="n">keywords</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"yourcommand"</span><span class="p">,</span> <span class="s2">"yc"</span><span class="p">]</span>
<a id="__codelineno-0-16" name="__codelineno-0-16" href="#__codelineno-0-16"></a> <span class="n">description</span> <span class="o">=</span> <span class="s2">"Brief description for help text"</span>
<a id="__codelineno-0-17" name="__codelineno-0-17" href="#__codelineno-0-17"></a> <span class="n">category</span> <span class="o">=</span> <span class="s2">"general"</span>
<a id="__codelineno-0-18" name="__codelineno-0-18" href="#__codelineno-0-18"></a>
<a id="__codelineno-0-19" name="__codelineno-0-19" href="#__codelineno-0-19"></a> <span class="c1"># Documentation for website generation</span>
<a id="__codelineno-0-20" name="__codelineno-0-20" href="#__codelineno-0-20"></a> <span class="n">short_description</span> <span class="o">=</span> <span class="s2">"Brief description without usage syntax"</span>
<a id="__codelineno-0-21" name="__codelineno-0-21" href="#__codelineno-0-21"></a> <span class="n">usage</span> <span class="o">=</span> <span class="s2">"yourcommand [options]"</span>
<a id="__codelineno-0-22" name="__codelineno-0-22" href="#__codelineno-0-22"></a> <span class="n">examples</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"yourcommand"</span><span class="p">,</span> <span class="s2">"yourcommand option"</span><span class="p">]</span>
<a id="__codelineno-0-23" name="__codelineno-0-23" href="#__codelineno-0-23"></a> <span class="n">parameters</span> <span class="o">=</span> <span class="p">[</span>
<a id="__codelineno-0-24" name="__codelineno-0-24" href="#__codelineno-0-24"></a> <span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"option"</span><span class="p">,</span> <span class="s2">"description"</span><span class="p">:</span> <span class="s2">"Optional parameter description"</span><span class="p">}</span>
<a id="__codelineno-0-25" name="__codelineno-0-25" href="#__codelineno-0-25"></a> <span class="p">]</span>
<a id="__codelineno-0-26" name="__codelineno-0-26" href="#__codelineno-0-26"></a>
<a id="__codelineno-0-27" name="__codelineno-0-27" href="#__codelineno-0-27"></a> <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">bot</span><span class="p">):</span>
<a id="__codelineno-0-28" name="__codelineno-0-28" href="#__codelineno-0-28"></a><span class="w"> </span><span class="sd">"""Initialize the command."""</span>
<a id="__codelineno-0-29" name="__codelineno-0-29" href="#__codelineno-0-29"></a> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
<a id="__codelineno-0-30" name="__codelineno-0-30" href="#__codelineno-0-30"></a> <span class="c1"># Load your configuration here</span>
<a id="__codelineno-0-31" name="__codelineno-0-31" href="#__codelineno-0-31"></a> <span class="bp">self</span><span class="o">.</span><span class="n">enabled</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_config_value</span><span class="p">(</span>
<a id="__codelineno-0-32" name="__codelineno-0-32" href="#__codelineno-0-32"></a> <span class="s1">'YourCommand_Command'</span><span class="p">,</span>
<a id="__codelineno-0-33" name="__codelineno-0-33" href="#__codelineno-0-33"></a> <span class="s1">'enabled'</span><span class="p">,</span>
<a id="__codelineno-0-34" name="__codelineno-0-34" href="#__codelineno-0-34"></a> <span class="n">fallback</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<a id="__codelineno-0-35" name="__codelineno-0-35" href="#__codelineno-0-35"></a> <span class="n">value_type</span><span class="o">=</span><span class="s1">'bool'</span>
<a id="__codelineno-0-36" name="__codelineno-0-36" href="#__codelineno-0-36"></a> <span class="p">)</span>
<a id="__codelineno-0-37" name="__codelineno-0-37" href="#__codelineno-0-37"></a>
<a id="__codelineno-0-38" name="__codelineno-0-38" href="#__codelineno-0-38"></a> <span class="k">def</span><span class="w"> </span><span class="nf">can_execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-0-39" name="__codelineno-0-39" href="#__codelineno-0-39"></a><span class="w"> </span><span class="sd">"""Check if command can execute."""</span>
<a id="__codelineno-0-40" name="__codelineno-0-40" href="#__codelineno-0-40"></a> <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">enabled</span><span class="p">:</span>
<a id="__codelineno-0-41" name="__codelineno-0-41" href="#__codelineno-0-41"></a> <span class="k">return</span> <span class="kc">False</span>
<a id="__codelineno-0-42" name="__codelineno-0-42" href="#__codelineno-0-42"></a> <span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">can_execute</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">)</span>
<a id="__codelineno-0-43" name="__codelineno-0-43" href="#__codelineno-0-43"></a>
<a id="__codelineno-0-44" name="__codelineno-0-44" href="#__codelineno-0-44"></a> <span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-0-45" name="__codelineno-0-45" href="#__codelineno-0-45"></a><span class="w"> </span><span class="sd">"""Execute the command logic."""</span>
<a id="__codelineno-0-46" name="__codelineno-0-46" href="#__codelineno-0-46"></a> <span class="k">try</span><span class="p">:</span>
<a id="__codelineno-0-47" name="__codelineno-0-47" href="#__codelineno-0-47"></a> <span class="c1"># Your command logic here</span>
<a id="__codelineno-0-48" name="__codelineno-0-48" href="#__codelineno-0-48"></a> <span class="n">response</span> <span class="o">=</span> <span class="s2">"Your response text"</span>
<a id="__codelineno-0-49" name="__codelineno-0-49" href="#__codelineno-0-49"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<a id="__codelineno-0-50" name="__codelineno-0-50" href="#__codelineno-0-50"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-0-51" name="__codelineno-0-51" href="#__codelineno-0-51"></a> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<a id="__codelineno-0-52" name="__codelineno-0-52" href="#__codelineno-0-52"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error in yourcommand: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-0-53" name="__codelineno-0-53" href="#__codelineno-0-53"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="s2">"An error occurred"</span><span class="p">)</span>
<a id="__codelineno-0-54" name="__codelineno-0-54" href="#__codelineno-0-54"></a> <span class="k">return</span> <span class="kc">True</span>
</code></pre></div>
<hr />
<h2 id="command-class-structure">Command Class Structure</h2>
<p>All commands must inherit from <code>BaseCommand</code> located in
<code>modules/commands/base_command.py</code>.</p>
<h3 id="required-imports">Required Imports</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.models</span><span class="w"> </span><span class="kn">import</span> <span class="n">MeshMessage</span>
<a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.commands.base_command</span><span class="w"> </span><span class="kn">import</span> <span class="n">BaseCommand</span>
</code></pre></div>
<h3 id="optional-common-imports">Optional Common Imports</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a><span class="kn">import</span><span class="w"> </span><span class="nn">asyncio</span>
<a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a><span class="kn">import</span><span class="w"> </span><span class="nn">re</span>
<a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a><span class="kn">from</span><span class="w"> </span><span class="nn">typing</span><span class="w"> </span><span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Optional</span>
<a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a><span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timezone</span>
<a id="__codelineno-2-5" name="__codelineno-2-5" href="#__codelineno-2-5"></a>
<a id="__codelineno-2-6" name="__codelineno-2-6" href="#__codelineno-2-6"></a><span class="c1"># For HTTP requests</span>
<a id="__codelineno-2-7" name="__codelineno-2-7" href="#__codelineno-2-7"></a><span class="kn">import</span><span class="w"> </span><span class="nn">aiohttp</span>
<a id="__codelineno-2-8" name="__codelineno-2-8" href="#__codelineno-2-8"></a>
<a id="__codelineno-2-9" name="__codelineno-2-9" href="#__codelineno-2-9"></a><span class="c1"># For database access</span>
<a id="__codelineno-2-10" name="__codelineno-2-10" href="#__codelineno-2-10"></a><span class="c1"># (available via self.bot.db_manager)</span>
<a id="__codelineno-2-11" name="__codelineno-2-11" href="#__codelineno-2-11"></a>
<a id="__codelineno-2-12" name="__codelineno-2-12" href="#__codelineno-2-12"></a><span class="c1"># For external API clients</span>
<a id="__codelineno-2-13" name="__codelineno-2-13" href="#__codelineno-2-13"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.clients.your_client</span><span class="w"> </span><span class="kn">import</span> <span class="n">YourClient</span>
<a id="__codelineno-2-14" name="__codelineno-2-14" href="#__codelineno-2-14"></a>
<a id="__codelineno-2-15" name="__codelineno-2-15" href="#__codelineno-2-15"></a><span class="c1"># For utilities (see modules/utils.py for more functions)</span>
<a id="__codelineno-2-16" name="__codelineno-2-16" href="#__codelineno-2-16"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.utils</span><span class="w"> </span><span class="kn">import</span> <span class="p">(</span>
<a id="__codelineno-2-17" name="__codelineno-2-17" href="#__codelineno-2-17"></a> <span class="n">geocode_city_sync</span><span class="p">,</span>
<a id="__codelineno-2-18" name="__codelineno-2-18" href="#__codelineno-2-18"></a> <span class="n">geocode_zipcode_sync</span><span class="p">,</span>
<a id="__codelineno-2-19" name="__codelineno-2-19" href="#__codelineno-2-19"></a> <span class="n">get_config_timezone</span><span class="p">,</span>
<a id="__codelineno-2-20" name="__codelineno-2-20" href="#__codelineno-2-20"></a> <span class="n">decode_escape_sequences</span><span class="p">,</span>
<a id="__codelineno-2-21" name="__codelineno-2-21" href="#__codelineno-2-21"></a> <span class="n">format_elapsed_display</span><span class="p">,</span>
<a id="__codelineno-2-22" name="__codelineno-2-22" href="#__codelineno-2-22"></a> <span class="n">format_location_for_display</span><span class="p">,</span>
<a id="__codelineno-2-23" name="__codelineno-2-23" href="#__codelineno-2-23"></a><span class="p">)</span>
</code></pre></div>
<hr />
<h2 id="class-level-variables-reference">Class-Level Variables Reference</h2>
<p>These variables define your command's metadata and behavior:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td><code>str</code></td>
<td><strong>Yes</strong></td>
<td>Primary command name (lowercase, used for config section)</td>
</tr>
<tr>
<td><code>keywords</code></td>
<td><code>list[str]</code></td>
<td><strong>Yes</strong></td>
<td>Trigger words for the command (includes name and aliases)</td>
</tr>
<tr>
<td><code>description</code></td>
<td><code>str</code></td>
<td><strong>Yes</strong></td>
<td>Brief description shown in help text</td>
</tr>
<tr>
<td><code>category</code></td>
<td><code>str</code></td>
<td>No</td>
<td>Category for grouping (see <a href="#command_categories">Command Categories</a>)</td>
</tr>
<tr>
<td><code>requires_dm</code></td>
<td><code>bool</code></td>
<td>No</td>
<td>Set to <code>True</code> if command only works in direct messages (default: <code>False</code>)</td>
</tr>
<tr>
<td><code>requires_internet</code></td>
<td><code>bool</code></td>
<td>No</td>
<td>Set to <code>True</code> if command needs internet access (default: <code>False</code>)</td>
</tr>
<tr>
<td><code>cooldown_seconds</code></td>
<td><code>int</code></td>
<td>No</td>
<td>Per-user cooldown period in seconds (default: <code>0</code>)</td>
</tr>
<tr>
<td><code>render_safe</code></td>
<td><code>bool</code></td>
<td>No</td>
<td>Set to <code>True</code> if command can be safely rendered in scheduled messages (default: <code>False</code>)</td>
</tr>
<tr>
<td><code>settings_schema</code></td>
<td><code>list[dict]</code></td>
<td>No</td>
<td>Web viewer settings schema (see <a href="#settings-schema">Settings Schema</a>)</td>
</tr>
</tbody>
</table>
<h3 id="class-level-variables-for-documentation">Class-Level Variables for Documentation</h3>
<p>One can generate an HTML document that describes all the commands that
<code>meshcore-bot</code> responds to with the command <code>generate_website.py</code>. For
more information please read [[command-reference-website.md]].</p>
<p>The following variables are used to during the generation of the HTML
document.</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>short_description</code></td>
<td><code>str</code></td>
<td>No</td>
<td>Brief description for website (without usage syntax)</td>
</tr>
<tr>
<td><code>usage</code></td>
<td><code>str</code></td>
<td>No</td>
<td>Usage syntax string (e.g., <code>"wx &lt;zipcode&gt; [tomorrow]"</code>)</td>
</tr>
<tr>
<td><code>examples</code></td>
<td><code>list[str]</code></td>
<td>No</td>
<td>Example commands for documentation</td>
</tr>
<tr>
<td><code>parameters</code></td>
<td><code>list[dict]</code></td>
<td>No</td>
<td>Parameter definitions with <code>name</code> and <code>description</code></td>
</tr>
</tbody>
</table>
<h3 id="command-categories">Command Categories</h3>
<p>The <code>category</code> class variable allows a user to search for commands based on
a number of defined categories. Currently the following categories are defined
and suggested to be used:</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>basic</td>
<td>Basic Commands</td>
</tr>
<tr>
<td>weather</td>
<td>Weather Commands</td>
</tr>
<tr>
<td>solar</td>
<td>Solar &amp; Astronomical</td>
</tr>
<tr>
<td>sports</td>
<td>Sports</td>
</tr>
<tr>
<td>games</td>
<td>Games &amp; Entertainment</td>
</tr>
<tr>
<td>fun</td>
<td>Fun Commands</td>
</tr>
<tr>
<td>entertainment</td>
<td>Entertainment</td>
</tr>
<tr>
<td>meshcore_info</td>
<td>Mesh Network Info</td>
</tr>
<tr>
<td>analytics</td>
<td>Analytics</td>
</tr>
<tr>
<td>emergency</td>
<td>Emergency</td>
</tr>
<tr>
<td>special</td>
<td>Special Commands</td>
</tr>
<tr>
<td>general</td>
<td>General Commands</td>
</tr>
</tbody>
</table>
<h3 id="settings-schema">Settings Schema</h3>
<p>The <code>settings_schema</code> definition allows a command or a service to define the
configuration settings that can be set
Commands can define a <code>settings_schema</code> to provide typed configuration in the web viewer:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a><span class="n">settings_schema</span> <span class="o">=</span> <span class="p">[</span>
<a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a> <span class="p">{</span>
<a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a> <span class="s2">"key"</span><span class="p">:</span> <span class="s2">"poll_interval"</span><span class="p">,</span>
<a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a> <span class="s2">"label"</span><span class="p">:</span> <span class="s2">"Poll interval"</span><span class="p">,</span>
<a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a> <span class="s2">"type"</span><span class="p">:</span> <span class="s2">"int"</span><span class="p">,</span>
<a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a> <span class="s2">"min"</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span>
<a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a> <span class="s2">"max"</span><span class="p">:</span> <span class="mi">86400000</span><span class="p">,</span>
<a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a> <span class="s2">"default"</span><span class="p">:</span> <span class="mi">60000</span><span class="p">,</span>
<a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a> <span class="s2">"help"</span><span class="p">:</span> <span class="s2">"Polling cadence in milliseconds"</span><span class="p">,</span>
<a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a> <span class="s2">"unit"</span><span class="p">:</span> <span class="s2">"ms"</span>
<a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a> <span class="p">},</span>
<a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a> <span class="p">{</span>
<a id="__codelineno-3-13" name="__codelineno-3-13" href="#__codelineno-3-13"></a> <span class="s2">"key"</span><span class="p">:</span> <span class="s2">"enable_notifications"</span><span class="p">,</span>
<a id="__codelineno-3-14" name="__codelineno-3-14" href="#__codelineno-3-14"></a> <span class="s2">"label"</span><span class="p">:</span> <span class="s2">"Enable notifications"</span><span class="p">,</span>
<a id="__codelineno-3-15" name="__codelineno-3-15" href="#__codelineno-3-15"></a> <span class="s2">"type"</span><span class="p">:</span> <span class="s2">"bool"</span><span class="p">,</span>
<a id="__codelineno-3-16" name="__codelineno-3-16" href="#__codelineno-3-16"></a> <span class="s2">"default"</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
<a id="__codelineno-3-17" name="__codelineno-3-17" href="#__codelineno-3-17"></a> <span class="s2">"help"</span><span class="p">:</span> <span class="s2">"Send notifications for new events"</span>
<a id="__codelineno-3-18" name="__codelineno-3-18" href="#__codelineno-3-18"></a> <span class="p">},</span>
<a id="__codelineno-3-19" name="__codelineno-3-19" href="#__codelineno-3-19"></a> <span class="p">{</span>
<a id="__codelineno-3-20" name="__codelineno-3-20" href="#__codelineno-3-20"></a> <span class="s2">"key"</span><span class="p">:</span> <span class="s2">"priority"</span><span class="p">,</span>
<a id="__codelineno-3-21" name="__codelineno-3-21" href="#__codelineno-3-21"></a> <span class="s2">"label"</span><span class="p">:</span> <span class="s2">"Priority level"</span><span class="p">,</span>
<a id="__codelineno-3-22" name="__codelineno-3-22" href="#__codelineno-3-22"></a> <span class="s2">"type"</span><span class="p">:</span> <span class="s2">"enum"</span><span class="p">,</span>
<a id="__codelineno-3-23" name="__codelineno-3-23" href="#__codelineno-3-23"></a> <span class="s2">"options"</span><span class="p">:</span> <span class="p">[</span>
<a id="__codelineno-3-24" name="__codelineno-3-24" href="#__codelineno-3-24"></a> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="s2">"low"</span><span class="p">,</span> <span class="s2">"label"</span><span class="p">:</span> <span class="s2">"Low"</span><span class="p">},</span>
<a id="__codelineno-3-25" name="__codelineno-3-25" href="#__codelineno-3-25"></a> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="s2">"medium"</span><span class="p">,</span> <span class="s2">"label"</span><span class="p">:</span> <span class="s2">"Medium"</span><span class="p">},</span>
<a id="__codelineno-3-26" name="__codelineno-3-26" href="#__codelineno-3-26"></a> <span class="p">{</span><span class="s2">"value"</span><span class="p">:</span> <span class="s2">"high"</span><span class="p">,</span> <span class="s2">"label"</span><span class="p">:</span> <span class="s2">"High"</span><span class="p">}</span>
<a id="__codelineno-3-27" name="__codelineno-3-27" href="#__codelineno-3-27"></a> <span class="p">],</span>
<a id="__codelineno-3-28" name="__codelineno-3-28" href="#__codelineno-3-28"></a> <span class="s2">"default"</span><span class="p">:</span> <span class="s2">"medium"</span><span class="p">,</span>
<a id="__codelineno-3-29" name="__codelineno-3-29" href="#__codelineno-3-29"></a> <span class="s2">"help"</span><span class="p">:</span> <span class="s2">"Event priority threshold"</span>
<a id="__codelineno-3-30" name="__codelineno-3-30" href="#__codelineno-3-30"></a> <span class="p">}</span>
<a id="__codelineno-3-31" name="__codelineno-3-31" href="#__codelineno-3-31"></a><span class="p">]</span>
</code></pre></div>
<p><strong>Supported types:</strong> <code>bool</code>, <code>int</code>, <code>float</code>, <code>str</code>, <code>enum</code>, <code>list</code>, <code>password</code></p>
<p><strong>Note:</strong> <code>enabled</code> and <code>channels</code> do not need to be defined in the
<code>settings_schema</code> as they get automatically included.</p>
<hr />
<h2 id="core-methods">Core Methods</h2>
<h3 id="required-methods">Required Methods</h3>
<h4 id="async-def-executeself-message-meshmessage-bool"><code>async def execute(self, message: MeshMessage) -&gt; bool</code></h4>
<p><strong>Purpose:</strong> Execute the command logic when triggered.</p>
<p><strong>Parameters:</strong>
- <code>message</code>: The <code>MeshMessage</code> object containing the user's message and metadata</p>
<p><strong>Returns:</strong> <code>bool</code> - <code>True</code> if executed successfully, <code>False</code> otherwise</p>
<p><strong>Example:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="w"> </span><span class="sd">"""Execute the joke command."""</span>
<a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a> <span class="k">try</span><span class="p">:</span>
<a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a> <span class="c1"># Record execution for cooldown tracking</span>
<a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a> <span class="bp">self</span><span class="o">.</span><span class="n">record_execution</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">sender_id</span><span class="p">)</span>
<a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a>
<a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a> <span class="c1"># Your command logic</span>
<a id="__codelineno-4-8" name="__codelineno-4-8" href="#__codelineno-4-8"></a> <span class="n">joke_data</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_joke_from_api</span><span class="p">()</span>
<a id="__codelineno-4-9" name="__codelineno-4-9" href="#__codelineno-4-9"></a>
<a id="__codelineno-4-10" name="__codelineno-4-10" href="#__codelineno-4-10"></a> <span class="c1"># Format and send response</span>
<a id="__codelineno-4-11" name="__codelineno-4-11" href="#__codelineno-4-11"></a> <span class="n">response</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"🎭 </span><span class="si">{</span><span class="n">joke_data</span><span class="p">[</span><span class="s1">'joke'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
<a id="__codelineno-4-12" name="__codelineno-4-12" href="#__codelineno-4-12"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<a id="__codelineno-4-13" name="__codelineno-4-13" href="#__codelineno-4-13"></a>
<a id="__codelineno-4-14" name="__codelineno-4-14" href="#__codelineno-4-14"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-4-15" name="__codelineno-4-15" href="#__codelineno-4-15"></a> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<a id="__codelineno-4-16" name="__codelineno-4-16" href="#__codelineno-4-16"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error in joke command: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-4-17" name="__codelineno-4-17" href="#__codelineno-4-17"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="s2">"Sorry, couldn't fetch a joke!"</span><span class="p">)</span>
<a id="__codelineno-4-18" name="__codelineno-4-18" href="#__codelineno-4-18"></a> <span class="k">return</span> <span class="kc">True</span>
</code></pre></div></p>
<h3 id="optional-override-methods">Optional Override Methods</h3>
<h4 id="def-can_executeself-message-meshmessage-skip_channel_check-bool-false-bool"><code>def can_execute(self, message: MeshMessage, skip_channel_check: bool = False) -&gt; bool</code></h4>
<p><strong>Purpose:</strong> Check if command can execute (permissions, cooldowns, custom checks).</p>
<p><strong>Default behavior:</strong> Checks channel access, DM requirements, cooldowns, and admin access.</p>
<p><strong>Example:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">can_execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a><span class="w"> </span><span class="sd">"""Check if command can execute with custom logic."""</span>
<a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a> <span class="c1"># Use base class checks first</span>
<a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a> <span class="k">if</span> <span class="ow">not</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">can_execute</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">):</span>
<a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a> <span class="k">return</span> <span class="kc">False</span>
<a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a>
<a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a> <span class="c1"># Check if enabled</span>
<a id="__codelineno-5-8" name="__codelineno-5-8" href="#__codelineno-5-8"></a> <span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">my_enabled</span><span class="p">:</span>
<a id="__codelineno-5-9" name="__codelineno-5-9" href="#__codelineno-5-9"></a> <span class="k">return</span> <span class="kc">False</span>
<a id="__codelineno-5-10" name="__codelineno-5-10" href="#__codelineno-5-10"></a>
<a id="__codelineno-5-11" name="__codelineno-5-11" href="#__codelineno-5-11"></a> <span class="c1"># Custom check: dark jokes only in DM</span>
<a id="__codelineno-5-12" name="__codelineno-5-12" href="#__codelineno-5-12"></a> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">is_dark_joke_request</span><span class="p">(</span><span class="n">message</span><span class="p">)</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">message</span><span class="o">.</span><span class="n">is_dm</span><span class="p">:</span>
<a id="__codelineno-5-13" name="__codelineno-5-13" href="#__codelineno-5-13"></a> <span class="k">return</span> <span class="kc">False</span>
<a id="__codelineno-5-14" name="__codelineno-5-14" href="#__codelineno-5-14"></a>
<a id="__codelineno-5-15" name="__codelineno-5-15" href="#__codelineno-5-15"></a> <span class="k">return</span> <span class="kc">True</span>
</code></pre></div></p>
<h4 id="def-get_help_textself-message-meshmessage-none-str"><code>def get_help_text(self, message: MeshMessage = None) -&gt; str</code></h4>
<p><strong>Purpose:</strong> Return help text for the command (shown in <code>help &lt;command&gt;</code>).</p>
<p><strong>Example:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_help_text</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span> <span class="o">=</span> <span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a><span class="w"> </span><span class="sd">"""Get help text, excluding dark category if not in DM."""</span>
<a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a> <span class="k">if</span> <span class="n">message</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">message</span><span class="o">.</span><span class="n">is_dm</span><span class="p">:</span>
<a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a> <span class="k">return</span> <span class="s2">"Usage: joke [category] - Categories: programming, pun, misc"</span>
<a id="__codelineno-6-5" name="__codelineno-6-5" href="#__codelineno-6-5"></a> <span class="k">else</span><span class="p">:</span>
<a id="__codelineno-6-6" name="__codelineno-6-6" href="#__codelineno-6-6"></a> <span class="k">return</span> <span class="s2">"Usage: joke [category] - Categories: programming, pun, misc, dark"</span>
</code></pre></div></p>
<h4 id="def-matches_keywordself-message-meshmessage-bool"><code>def matches_keyword(self, message: MeshMessage) -&gt; bool</code></h4>
<p><strong>Purpose:</strong> Custom keyword matching logic (default implementation is usually
sufficient).</p>
<h4 id="def-matches_custom_syntaxself-message-meshmessage-bool"><code>def matches_custom_syntax(self, message: MeshMessage) -&gt; bool</code></h4>
<p><strong>Purpose:</strong> Check for custom syntax patterns beyond simple keywords.</p>
<p><strong>Example:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">matches_custom_syntax</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a><span class="w"> </span><span class="sd">"""Match lat,lon coordinate syntax."""</span>
<a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a> <span class="k">if</span> <span class="ow">not</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">matches_custom_syntax</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
<a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a> <span class="k">return</span> <span class="kc">False</span>
<a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a>
<a id="__codelineno-7-6" name="__codelineno-7-6" href="#__codelineno-7-6"></a> <span class="n">content</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="n">content</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<a id="__codelineno-7-7" name="__codelineno-7-7" href="#__codelineno-7-7"></a> <span class="c1"># Match coordinate pattern like "48.08,-121.97"</span>
<a id="__codelineno-7-8" name="__codelineno-7-8" href="#__codelineno-7-8"></a> <span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="n">re</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$'</span><span class="p">,</span> <span class="n">content</span><span class="p">))</span>
</code></pre></div></p>
<hr />
<h2 id="network-communication-apis">Network Communication APIs</h2>
<h3 id="sending-messages">Sending Messages</h3>
<h4 id="async-def-send_responsemessage-meshmessage-content-str-skip_user_rate_limit-bool-false-command_id-str-none-none-bool"><code>async def send_response(message: MeshMessage, content: str, skip_user_rate_limit: bool = False, *, command_id: str | None = None) -&gt; bool</code></h4>
<p>Send a single response message (channel or DM).</p>
<p><strong>Parameters:</strong>
- <code>message</code>: Original message to respond to
- <code>content</code>: Response text (max 158 bytes for DM, varies for channels)
- <code>skip_user_rate_limit</code>: Skip user rate limiter (for follow-up messages)
- <code>command_id</code>: Optional ID for tracking/deduplication</p>
<p><strong>Returns:</strong> <code>bool</code> - <code>True</code> if sent successfully</p>
<p><strong>Example:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="s2">"Weather: Sunny, 72°F"</span><span class="p">)</span>
</code></pre></div></p>
<h4 id="async-def-send_response_chunkedmessage-meshmessage-chunks-liststr-skip_user_rate_limit_first-bool-true-bool"><code>async def send_response_chunked(message: MeshMessage, chunks: list[str], *, skip_user_rate_limit_first: bool = True) -&gt; bool</code></h4>
<p>Send multiple messages with rate-limit spacing (automatically delays between chunks).</p>
<p><strong>Parameters:</strong>
- <code>message</code>: Original message to respond to
- <code>chunks</code>: List of message strings to send in order
- <code>skip_user_rate_limit_first</code>: Skip rate limit for first chunk</p>
<p><strong>Returns:</strong> <code>bool</code> - <code>True</code> if all sent successfully</p>
<p><strong>Example:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a><span class="n">chunks</span> <span class="o">=</span> <span class="p">[</span>
<a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a> <span class="s2">"Part 1: Setup message"</span><span class="p">,</span>
<a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a> <span class="s2">"Part 2: Delivery message"</span>
<a id="__codelineno-9-4" name="__codelineno-9-4" href="#__codelineno-9-4"></a><span class="p">]</span>
<a id="__codelineno-9-5" name="__codelineno-9-5" href="#__codelineno-9-5"></a><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response_chunked</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">chunks</span><span class="p">)</span>
</code></pre></div></p>
<h3 id="message-length-helpers">Message Length Helpers</h3>
<h4 id="def-get_max_message_lengthself-message-meshmessage-int"><code>def get_max_message_length(self, message: MeshMessage) -&gt; int</code></h4>
<p>Calculate maximum safe message length in UTF-8 bytes.</p>
<ul>
<li><strong>DM messages:</strong> 158 bytes</li>
<li><strong>Channel messages:</strong> 160 - username_bytes - 2, minus 10 for regional flood scope</li>
</ul>
<p><strong>Example:</strong>
<div class="highlight"><pre><span></span><code><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="n">max_len</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_max_message_length</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a><span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">max_len</span><span class="p">:</span>
<a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a> <span class="n">response</span> <span class="o">=</span> <span class="n">response</span><span class="p">[:</span><span class="n">max_len</span> <span class="o">-</span> <span class="mi">3</span><span class="p">]</span> <span class="o">+</span> <span class="s2">"..."</span>
</code></pre></div></p>
<h3 id="meshmessage-object">MeshMessage Object</h3>
<p>The <code>MeshMessage</code> dataclass (from <code>modules/models.py</code>) contains:</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>content</code></td>
<td><code>str</code></td>
<td>Message text content</td>
</tr>
<tr>
<td><code>sender_id</code></td>
<td><code>Optional[str]</code></td>
<td>Sender's node ID</td>
</tr>
<tr>
<td><code>sender_pubkey</code></td>
<td><code>Optional[str]</code></td>
<td>Sender's public key (for admin checks)</td>
</tr>
<tr>
<td><code>channel</code></td>
<td><code>Optional[str]</code></td>
<td>Channel name (e.g., <code>"#general"</code>)</td>
</tr>
<tr>
<td><code>is_dm</code></td>
<td><code>bool</code></td>
<td><code>True</code> if direct message</td>
</tr>
<tr>
<td><code>timestamp</code></td>
<td><code>Optional[int]</code></td>
<td>Message timestamp (Unix epoch)</td>
</tr>
<tr>
<td><code>snr</code></td>
<td><code>Optional[float]</code></td>
<td>Signal-to-noise ratio in dB</td>
</tr>
<tr>
<td><code>rssi</code></td>
<td><code>Optional[int]</code></td>
<td>Received signal strength in dBm</td>
</tr>
<tr>
<td><code>hops</code></td>
<td><code>Optional[int]</code></td>
<td>Number of hops (may be <code>None</code>)</td>
</tr>
<tr>
<td><code>path</code></td>
<td><code>Optional[str]</code></td>
<td>Path string for display</td>
</tr>
<tr>
<td><code>routing_info</code></td>
<td><code>Optional[dict]</code></td>
<td>Detailed routing information</td>
</tr>
<tr>
<td><code>reply_scope</code></td>
<td><code>Optional[str]</code></td>
<td>Flood scope for reply</td>
</tr>
<tr>
<td><code>content_lower</code></td>
<td><code>str</code></td>
<td>Lowercased content (set by framework)</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="data-persistence-apis">Data Persistence APIs</h2>
<p>The bot provides a SQLite database through <code>self.bot.db_manager</code>. All database operations should use the context manager for proper connection handling.</p>
<h3 id="database-connection">Database Connection</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a><span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">connection</span><span class="p">()</span> <span class="k">as</span> <span class="n">conn</span><span class="p">:</span>
<a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a> <span class="n">cursor</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<a id="__codelineno-11-3" name="__codelineno-11-3" href="#__codelineno-11-3"></a> <span class="c1"># Your database operations</span>
<a id="__codelineno-11-4" name="__codelineno-11-4" href="#__codelineno-11-4"></a> <span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</code></pre></div>
<h3 id="common-database-operations">Common Database Operations</h3>
<h4 id="query-with-results">Query with Results</h4>
<div class="highlight"><pre><span></span><code><a id="__codelineno-12-1" name="__codelineno-12-1" href="#__codelineno-12-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">get_user_stats</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">dict</span><span class="p">]:</span>
<a id="__codelineno-12-2" name="__codelineno-12-2" href="#__codelineno-12-2"></a><span class="w"> </span><span class="sd">"""Get user statistics from database."""</span>
<a id="__codelineno-12-3" name="__codelineno-12-3" href="#__codelineno-12-3"></a> <span class="k">try</span><span class="p">:</span>
<a id="__codelineno-12-4" name="__codelineno-12-4" href="#__codelineno-12-4"></a> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">connection</span><span class="p">()</span> <span class="k">as</span> <span class="n">conn</span><span class="p">:</span>
<a id="__codelineno-12-5" name="__codelineno-12-5" href="#__codelineno-12-5"></a> <span class="n">cursor</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<a id="__codelineno-12-6" name="__codelineno-12-6" href="#__codelineno-12-6"></a> <span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"""</span>
<a id="__codelineno-12-7" name="__codelineno-12-7" href="#__codelineno-12-7"></a><span class="s2"> SELECT command_name, COUNT(*) as count</span>
<a id="__codelineno-12-8" name="__codelineno-12-8" href="#__codelineno-12-8"></a><span class="s2"> FROM command_stats</span>
<a id="__codelineno-12-9" name="__codelineno-12-9" href="#__codelineno-12-9"></a><span class="s2"> WHERE user_id = ?</span>
<a id="__codelineno-12-10" name="__codelineno-12-10" href="#__codelineno-12-10"></a><span class="s2"> GROUP BY command_name</span>
<a id="__codelineno-12-11" name="__codelineno-12-11" href="#__codelineno-12-11"></a><span class="s2"> """</span><span class="p">,</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,))</span>
<a id="__codelineno-12-12" name="__codelineno-12-12" href="#__codelineno-12-12"></a> <span class="n">results</span> <span class="o">=</span> <span class="n">cursor</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
<a id="__codelineno-12-13" name="__codelineno-12-13" href="#__codelineno-12-13"></a> <span class="k">return</span> <span class="p">[{</span><span class="s2">"command"</span><span class="p">:</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="s2">"count"</span><span class="p">:</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]}</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">]</span>
<a id="__codelineno-12-14" name="__codelineno-12-14" href="#__codelineno-12-14"></a> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<a id="__codelineno-12-15" name="__codelineno-12-15" href="#__codelineno-12-15"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Database error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-12-16" name="__codelineno-12-16" href="#__codelineno-12-16"></a> <span class="k">return</span> <span class="kc">None</span>
</code></pre></div>
<h4 id="insertupdate-data">Insert/Update Data</h4>
<div class="highlight"><pre><span></span><code><a id="__codelineno-13-1" name="__codelineno-13-1" href="#__codelineno-13-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">save_user_preference</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">preference</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<a id="__codelineno-13-2" name="__codelineno-13-2" href="#__codelineno-13-2"></a><span class="w"> </span><span class="sd">"""Save user preference to database."""</span>
<a id="__codelineno-13-3" name="__codelineno-13-3" href="#__codelineno-13-3"></a> <span class="k">try</span><span class="p">:</span>
<a id="__codelineno-13-4" name="__codelineno-13-4" href="#__codelineno-13-4"></a> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">connection</span><span class="p">()</span> <span class="k">as</span> <span class="n">conn</span><span class="p">:</span>
<a id="__codelineno-13-5" name="__codelineno-13-5" href="#__codelineno-13-5"></a> <span class="n">cursor</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span>
<a id="__codelineno-13-6" name="__codelineno-13-6" href="#__codelineno-13-6"></a> <span class="n">cursor</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">"""</span>
<a id="__codelineno-13-7" name="__codelineno-13-7" href="#__codelineno-13-7"></a><span class="s2"> INSERT OR REPLACE INTO user_preferences</span>
<a id="__codelineno-13-8" name="__codelineno-13-8" href="#__codelineno-13-8"></a><span class="s2"> (user_id, preference, value, updated_at)</span>
<a id="__codelineno-13-9" name="__codelineno-13-9" href="#__codelineno-13-9"></a><span class="s2"> VALUES (?, ?, ?, datetime('now'))</span>
<a id="__codelineno-13-10" name="__codelineno-13-10" href="#__codelineno-13-10"></a><span class="s2"> """</span><span class="p">,</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">preference</span><span class="p">,</span> <span class="n">value</span><span class="p">))</span>
<a id="__codelineno-13-11" name="__codelineno-13-11" href="#__codelineno-13-11"></a> <span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<a id="__codelineno-13-12" name="__codelineno-13-12" href="#__codelineno-13-12"></a> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<a id="__codelineno-13-13" name="__codelineno-13-13" href="#__codelineno-13-13"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error saving preference: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</code></pre></div>
<h3 id="caching-apis">Caching APIs</h3>
<h4 id="geocoding-cache">Geocoding Cache</h4>
<div class="highlight"><pre><span></span><code><a id="__codelineno-14-1" name="__codelineno-14-1" href="#__codelineno-14-1"></a><span class="c1"># Check cache first</span>
<a id="__codelineno-14-2" name="__codelineno-14-2" href="#__codelineno-14-2"></a><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">get_cached_geocoding</span><span class="p">(</span><span class="s2">"Seattle, WA"</span><span class="p">)</span>
<a id="__codelineno-14-3" name="__codelineno-14-3" href="#__codelineno-14-3"></a>
<a id="__codelineno-14-4" name="__codelineno-14-4" href="#__codelineno-14-4"></a><span class="k">if</span> <span class="n">lat</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">or</span> <span class="n">lon</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-14-5" name="__codelineno-14-5" href="#__codelineno-14-5"></a> <span class="c1"># Fetch from API</span>
<a id="__codelineno-14-6" name="__codelineno-14-6" href="#__codelineno-14-6"></a> <span class="n">lat</span><span class="p">,</span> <span class="n">lon</span> <span class="o">=</span> <span class="k">await</span> <span class="n">geocode_city</span><span class="p">(</span><span class="s2">"Seattle"</span><span class="p">,</span> <span class="s2">"WA"</span><span class="p">)</span>
<a id="__codelineno-14-7" name="__codelineno-14-7" href="#__codelineno-14-7"></a>
<a id="__codelineno-14-8" name="__codelineno-14-8" href="#__codelineno-14-8"></a> <span class="c1"># Cache for 30 days (720 hours)</span>
<a id="__codelineno-14-9" name="__codelineno-14-9" href="#__codelineno-14-9"></a> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">cache_geocoding</span><span class="p">(</span><span class="s2">"Seattle, WA"</span><span class="p">,</span> <span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">cache_hours</span><span class="o">=</span><span class="mi">720</span><span class="p">)</span>
</code></pre></div>
<h4 id="generic-cache">Generic Cache</h4>
<div class="highlight"><pre><span></span><code><a id="__codelineno-15-1" name="__codelineno-15-1" href="#__codelineno-15-1"></a><span class="c1"># Get cached value</span>
<a id="__codelineno-15-2" name="__codelineno-15-2" href="#__codelineno-15-2"></a><span class="n">cached</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">get_cached_value</span><span class="p">(</span>
<a id="__codelineno-15-3" name="__codelineno-15-3" href="#__codelineno-15-3"></a> <span class="n">cache_key</span><span class="o">=</span><span class="s2">"weather_98101"</span><span class="p">,</span>
<a id="__codelineno-15-4" name="__codelineno-15-4" href="#__codelineno-15-4"></a> <span class="n">cache_type</span><span class="o">=</span><span class="s2">"weather_data"</span>
<a id="__codelineno-15-5" name="__codelineno-15-5" href="#__codelineno-15-5"></a><span class="p">)</span>
<a id="__codelineno-15-6" name="__codelineno-15-6" href="#__codelineno-15-6"></a>
<a id="__codelineno-15-7" name="__codelineno-15-7" href="#__codelineno-15-7"></a><span class="k">if</span> <span class="n">cached</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-15-8" name="__codelineno-15-8" href="#__codelineno-15-8"></a> <span class="c1"># Fetch fresh data</span>
<a id="__codelineno-15-9" name="__codelineno-15-9" href="#__codelineno-15-9"></a> <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">fetch_weather_data</span><span class="p">(</span><span class="s2">"98101"</span><span class="p">)</span>
<a id="__codelineno-15-10" name="__codelineno-15-10" href="#__codelineno-15-10"></a>
<a id="__codelineno-15-11" name="__codelineno-15-11" href="#__codelineno-15-11"></a> <span class="c1"># Cache for 1 hour</span>
<a id="__codelineno-15-12" name="__codelineno-15-12" href="#__codelineno-15-12"></a> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">cache_value</span><span class="p">(</span>
<a id="__codelineno-15-13" name="__codelineno-15-13" href="#__codelineno-15-13"></a> <span class="n">cache_key</span><span class="o">=</span><span class="s2">"weather_98101"</span><span class="p">,</span>
<a id="__codelineno-15-14" name="__codelineno-15-14" href="#__codelineno-15-14"></a> <span class="n">cache_type</span><span class="o">=</span><span class="s2">"weather_data"</span><span class="p">,</span>
<a id="__codelineno-15-15" name="__codelineno-15-15" href="#__codelineno-15-15"></a> <span class="n">cache_value</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">data</span><span class="p">),</span>
<a id="__codelineno-15-16" name="__codelineno-15-16" href="#__codelineno-15-16"></a> <span class="n">cache_hours</span><span class="o">=</span><span class="mi">1</span>
<a id="__codelineno-15-17" name="__codelineno-15-17" href="#__codelineno-15-17"></a> <span class="p">)</span>
</code></pre></div>
<h3 id="execute-query-helper">Execute Query Helper</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-16-1" name="__codelineno-16-1" href="#__codelineno-16-1"></a><span class="c1"># Use the db_manager's execute_query for simpler queries</span>
<a id="__codelineno-16-2" name="__codelineno-16-2" href="#__codelineno-16-2"></a><span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">execute_query</span><span class="p">(</span>
<a id="__codelineno-16-3" name="__codelineno-16-3" href="#__codelineno-16-3"></a> <span class="s2">"SELECT * FROM command_stats WHERE user_id = ? LIMIT 10"</span><span class="p">,</span>
<a id="__codelineno-16-4" name="__codelineno-16-4" href="#__codelineno-16-4"></a> <span class="p">(</span><span class="n">user_id</span><span class="p">,)</span>
<a id="__codelineno-16-5" name="__codelineno-16-5" href="#__codelineno-16-5"></a><span class="p">)</span>
<a id="__codelineno-16-6" name="__codelineno-16-6" href="#__codelineno-16-6"></a>
<a id="__codelineno-16-7" name="__codelineno-16-7" href="#__codelineno-16-7"></a><span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
<a id="__codelineno-16-8" name="__codelineno-16-8" href="#__codelineno-16-8"></a> <span class="c1"># row is a dict with column names as keys</span>
<a id="__codelineno-16-9" name="__codelineno-16-9" href="#__codelineno-16-9"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Command: </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="s1">'command_name'</span><span class="p">]</span><span class="si">}</span><span class="s2">, Count: </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="s1">'count'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</code></pre></div>
<hr />
<h2 id="external-data-access">External Data Access</h2>
<h3 id="http-requests-with-aiohttp">HTTP Requests with aiohttp</h3>
<p>Use <code>aiohttp</code> for asynchronous HTTP requests:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-17-1" name="__codelineno-17-1" href="#__codelineno-17-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">get_data_from_api</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">dict</span><span class="p">]:</span>
<a id="__codelineno-17-2" name="__codelineno-17-2" href="#__codelineno-17-2"></a><span class="w"> </span><span class="sd">"""Fetch data from external API."""</span>
<a id="__codelineno-17-3" name="__codelineno-17-3" href="#__codelineno-17-3"></a> <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"https://api.example.com/data?q=</span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="s2">"</span>
<a id="__codelineno-17-4" name="__codelineno-17-4" href="#__codelineno-17-4"></a> <span class="n">timeout</span> <span class="o">=</span> <span class="mi">10</span> <span class="c1"># seconds</span>
<a id="__codelineno-17-5" name="__codelineno-17-5" href="#__codelineno-17-5"></a>
<a id="__codelineno-17-6" name="__codelineno-17-6" href="#__codelineno-17-6"></a> <span class="k">try</span><span class="p">:</span>
<a id="__codelineno-17-7" name="__codelineno-17-7" href="#__codelineno-17-7"></a> <span class="k">async</span> <span class="k">with</span> <span class="n">aiohttp</span><span class="o">.</span><span class="n">ClientSession</span><span class="p">()</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
<a id="__codelineno-17-8" name="__codelineno-17-8" href="#__codelineno-17-8"></a> <span class="k">async</span> <span class="k">with</span> <span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
<a id="__codelineno-17-9" name="__codelineno-17-9" href="#__codelineno-17-9"></a> <span class="n">url</span><span class="p">,</span>
<a id="__codelineno-17-10" name="__codelineno-17-10" href="#__codelineno-17-10"></a> <span class="n">timeout</span><span class="o">=</span><span class="n">aiohttp</span><span class="o">.</span><span class="n">ClientTimeout</span><span class="p">(</span><span class="n">total</span><span class="o">=</span><span class="n">timeout</span><span class="p">)</span>
<a id="__codelineno-17-11" name="__codelineno-17-11" href="#__codelineno-17-11"></a> <span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
<a id="__codelineno-17-12" name="__codelineno-17-12" href="#__codelineno-17-12"></a> <span class="k">if</span> <span class="n">response</span><span class="o">.</span><span class="n">status</span> <span class="o">==</span> <span class="mi">200</span><span class="p">:</span>
<a id="__codelineno-17-13" name="__codelineno-17-13" href="#__codelineno-17-13"></a> <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
<a id="__codelineno-17-14" name="__codelineno-17-14" href="#__codelineno-17-14"></a> <span class="k">return</span> <span class="n">data</span>
<a id="__codelineno-17-15" name="__codelineno-17-15" href="#__codelineno-17-15"></a> <span class="k">else</span><span class="p">:</span>
<a id="__codelineno-17-16" name="__codelineno-17-16" href="#__codelineno-17-16"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"API returned status </span><span class="si">{</span><span class="n">response</span><span class="o">.</span><span class="n">status</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-17-17" name="__codelineno-17-17" href="#__codelineno-17-17"></a> <span class="k">return</span> <span class="kc">None</span>
<a id="__codelineno-17-18" name="__codelineno-17-18" href="#__codelineno-17-18"></a> <span class="k">except</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">TimeoutError</span><span class="p">:</span>
<a id="__codelineno-17-19" name="__codelineno-17-19" href="#__codelineno-17-19"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">"Timeout fetching data"</span><span class="p">)</span>
<a id="__codelineno-17-20" name="__codelineno-17-20" href="#__codelineno-17-20"></a> <span class="k">return</span> <span class="kc">None</span>
<a id="__codelineno-17-21" name="__codelineno-17-21" href="#__codelineno-17-21"></a> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<a id="__codelineno-17-22" name="__codelineno-17-22" href="#__codelineno-17-22"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error fetching data: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-17-23" name="__codelineno-17-23" href="#__codelineno-17-23"></a> <span class="k">return</span> <span class="kc">None</span>
</code></pre></div>
<h3 id="blocking-operations-with-asyncioto_thread">Blocking Operations with asyncio.to_thread</h3>
<p>For blocking I/O operations (geocoding, file operations), use <code>asyncio.to_thread</code>:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-18-1" name="__codelineno-18-1" href="#__codelineno-18-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-18-2" name="__codelineno-18-2" href="#__codelineno-18-2"></a><span class="w"> </span><span class="sd">"""Execute with offloaded blocking operation."""</span>
<a id="__codelineno-18-3" name="__codelineno-18-3" href="#__codelineno-18-3"></a> <span class="n">location</span> <span class="o">=</span> <span class="s2">"Seattle, WA"</span>
<a id="__codelineno-18-4" name="__codelineno-18-4" href="#__codelineno-18-4"></a>
<a id="__codelineno-18-5" name="__codelineno-18-5" href="#__codelineno-18-5"></a> <span class="c1"># Offload blocking geocode to thread</span>
<a id="__codelineno-18-6" name="__codelineno-18-6" href="#__codelineno-18-6"></a> <span class="n">lat</span><span class="p">,</span> <span class="n">lon</span><span class="p">,</span> <span class="n">address</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">to_thread</span><span class="p">(</span>
<a id="__codelineno-18-7" name="__codelineno-18-7" href="#__codelineno-18-7"></a> <span class="n">geocode_city_sync</span><span class="p">,</span>
<a id="__codelineno-18-8" name="__codelineno-18-8" href="#__codelineno-18-8"></a> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="p">,</span>
<a id="__codelineno-18-9" name="__codelineno-18-9" href="#__codelineno-18-9"></a> <span class="n">location</span><span class="p">,</span>
<a id="__codelineno-18-10" name="__codelineno-18-10" href="#__codelineno-18-10"></a> <span class="n">default_state</span><span class="o">=</span><span class="s2">"WA"</span><span class="p">,</span>
<a id="__codelineno-18-11" name="__codelineno-18-11" href="#__codelineno-18-11"></a> <span class="n">default_country</span><span class="o">=</span><span class="s2">"US"</span><span class="p">,</span>
<a id="__codelineno-18-12" name="__codelineno-18-12" href="#__codelineno-18-12"></a> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span>
<a id="__codelineno-18-13" name="__codelineno-18-13" href="#__codelineno-18-13"></a> <span class="p">)</span>
<a id="__codelineno-18-14" name="__codelineno-18-14" href="#__codelineno-18-14"></a>
<a id="__codelineno-18-15" name="__codelineno-18-15" href="#__codelineno-18-15"></a> <span class="k">if</span> <span class="n">lat</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-18-16" name="__codelineno-18-16" href="#__codelineno-18-16"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="s2">"Location not found"</span><span class="p">)</span>
<a id="__codelineno-18-17" name="__codelineno-18-17" href="#__codelineno-18-17"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-18-18" name="__codelineno-18-18" href="#__codelineno-18-18"></a>
<a id="__codelineno-18-19" name="__codelineno-18-19" href="#__codelineno-18-19"></a> <span class="c1"># Continue with result</span>
<a id="__codelineno-18-20" name="__codelineno-18-20" href="#__codelineno-18-20"></a> <span class="n">response</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"Coordinates: </span><span class="si">{</span><span class="n">lat</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="n">lon</span><span class="si">:</span><span class="s2">.2f</span><span class="si">}</span><span class="s2">"</span>
<a id="__codelineno-18-21" name="__codelineno-18-21" href="#__codelineno-18-21"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<a id="__codelineno-18-22" name="__codelineno-18-22" href="#__codelineno-18-22"></a> <span class="k">return</span> <span class="kc">True</span>
</code></pre></div>
<h3 id="custom-api-clients">Custom API Clients</h3>
<p>Create client classes in <code>modules/clients/</code> for reusable API access:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-19-1" name="__codelineno-19-1" href="#__codelineno-19-1"></a><span class="c1"># modules/clients/my_api_client.py</span>
<a id="__codelineno-19-2" name="__codelineno-19-2" href="#__codelineno-19-2"></a><span class="k">class</span><span class="w"> </span><span class="nc">MyAPIClient</span><span class="p">:</span>
<a id="__codelineno-19-3" name="__codelineno-19-3" href="#__codelineno-19-3"></a><span class="w"> </span><span class="sd">"""Client for MyAPI service."""</span>
<a id="__codelineno-19-4" name="__codelineno-19-4" href="#__codelineno-19-4"></a>
<a id="__codelineno-19-5" name="__codelineno-19-5" href="#__codelineno-19-5"></a> <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">api_key</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<a id="__codelineno-19-6" name="__codelineno-19-6" href="#__codelineno-19-6"></a> <span class="bp">self</span><span class="o">.</span><span class="n">api_key</span> <span class="o">=</span> <span class="n">api_key</span>
<a id="__codelineno-19-7" name="__codelineno-19-7" href="#__codelineno-19-7"></a> <span class="bp">self</span><span class="o">.</span><span class="n">base_url</span> <span class="o">=</span> <span class="s2">"https://api.example.com"</span>
<a id="__codelineno-19-8" name="__codelineno-19-8" href="#__codelineno-19-8"></a>
<a id="__codelineno-19-9" name="__codelineno-19-9" href="#__codelineno-19-9"></a> <span class="k">def</span><span class="w"> </span><span class="nf">get_data</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">param</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
<a id="__codelineno-19-10" name="__codelineno-19-10" href="#__codelineno-19-10"></a><span class="w"> </span><span class="sd">"""Fetch data (blocking)."""</span>
<a id="__codelineno-19-11" name="__codelineno-19-11" href="#__codelineno-19-11"></a> <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">base_url</span><span class="si">}</span><span class="s2">/endpoint"</span>
<a id="__codelineno-19-12" name="__codelineno-19-12" href="#__codelineno-19-12"></a> <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s2">"key"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">api_key</span><span class="p">,</span> <span class="s2">"q"</span><span class="p">:</span> <span class="n">param</span><span class="p">})</span>
<a id="__codelineno-19-13" name="__codelineno-19-13" href="#__codelineno-19-13"></a> <span class="n">response</span><span class="o">.</span><span class="n">raise_for_status</span><span class="p">()</span>
<a id="__codelineno-19-14" name="__codelineno-19-14" href="#__codelineno-19-14"></a> <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</code></pre></div>
<p>Use in command with thread offloading:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-20-1" name="__codelineno-20-1" href="#__codelineno-20-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-20-2" name="__codelineno-20-2" href="#__codelineno-20-2"></a><span class="w"> </span><span class="sd">"""Use custom API client."""</span>
<a id="__codelineno-20-3" name="__codelineno-20-3" href="#__codelineno-20-3"></a> <span class="n">client</span> <span class="o">=</span> <span class="n">MyAPIClient</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">api_key</span><span class="p">)</span>
<a id="__codelineno-20-4" name="__codelineno-20-4" href="#__codelineno-20-4"></a>
<a id="__codelineno-20-5" name="__codelineno-20-5" href="#__codelineno-20-5"></a> <span class="c1"># Offload blocking call</span>
<a id="__codelineno-20-6" name="__codelineno-20-6" href="#__codelineno-20-6"></a> <span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_event_loop</span><span class="p">()</span>
<a id="__codelineno-20-7" name="__codelineno-20-7" href="#__codelineno-20-7"></a> <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="n">client</span><span class="o">.</span><span class="n">get_data</span><span class="p">(</span><span class="s2">"query"</span><span class="p">))</span>
<a id="__codelineno-20-8" name="__codelineno-20-8" href="#__codelineno-20-8"></a>
<a id="__codelineno-20-9" name="__codelineno-20-9" href="#__codelineno-20-9"></a> <span class="c1"># Process data...</span>
</code></pre></div>
<hr />
<h2 id="configuration-and-localization">Configuration and Localization</h2>
<h3 id="loading-configuration">Loading Configuration</h3>
<p>Use <code>get_config_value()</code> for type-safe config access with migration support:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-21-1" name="__codelineno-21-1" href="#__codelineno-21-1"></a><span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">bot</span><span class="p">):</span>
<a id="__codelineno-21-2" name="__codelineno-21-2" href="#__codelineno-21-2"></a> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">bot</span><span class="p">)</span>
<a id="__codelineno-21-3" name="__codelineno-21-3" href="#__codelineno-21-3"></a>
<a id="__codelineno-21-4" name="__codelineno-21-4" href="#__codelineno-21-4"></a> <span class="c1"># Load configuration values</span>
<a id="__codelineno-21-5" name="__codelineno-21-5" href="#__codelineno-21-5"></a> <span class="bp">self</span><span class="o">.</span><span class="n">enabled</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_config_value</span><span class="p">(</span>
<a id="__codelineno-21-6" name="__codelineno-21-6" href="#__codelineno-21-6"></a> <span class="s1">'MyCommand_Command'</span><span class="p">,</span> <span class="c1"># Section name: CommandName_Command</span>
<a id="__codelineno-21-7" name="__codelineno-21-7" href="#__codelineno-21-7"></a> <span class="s1">'enabled'</span><span class="p">,</span> <span class="c1"># Key</span>
<a id="__codelineno-21-8" name="__codelineno-21-8" href="#__codelineno-21-8"></a> <span class="n">fallback</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="c1"># Default value</span>
<a id="__codelineno-21-9" name="__codelineno-21-9" href="#__codelineno-21-9"></a> <span class="n">value_type</span><span class="o">=</span><span class="s1">'bool'</span> <span class="c1"># Type: 'bool', 'int', 'float', 'str', 'list'</span>
<a id="__codelineno-21-10" name="__codelineno-21-10" href="#__codelineno-21-10"></a> <span class="p">)</span>
<a id="__codelineno-21-11" name="__codelineno-21-11" href="#__codelineno-21-11"></a>
<a id="__codelineno-21-12" name="__codelineno-21-12" href="#__codelineno-21-12"></a> <span class="bp">self</span><span class="o">.</span><span class="n">timeout</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_config_value</span><span class="p">(</span>
<a id="__codelineno-21-13" name="__codelineno-21-13" href="#__codelineno-21-13"></a> <span class="s1">'MyCommand_Command'</span><span class="p">,</span>
<a id="__codelineno-21-14" name="__codelineno-21-14" href="#__codelineno-21-14"></a> <span class="s1">'timeout'</span><span class="p">,</span>
<a id="__codelineno-21-15" name="__codelineno-21-15" href="#__codelineno-21-15"></a> <span class="n">fallback</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
<a id="__codelineno-21-16" name="__codelineno-21-16" href="#__codelineno-21-16"></a> <span class="n">value_type</span><span class="o">=</span><span class="s1">'int'</span>
<a id="__codelineno-21-17" name="__codelineno-21-17" href="#__codelineno-21-17"></a> <span class="p">)</span>
<a id="__codelineno-21-18" name="__codelineno-21-18" href="#__codelineno-21-18"></a>
<a id="__codelineno-21-19" name="__codelineno-21-19" href="#__codelineno-21-19"></a> <span class="bp">self</span><span class="o">.</span><span class="n">categories</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_config_value</span><span class="p">(</span>
<a id="__codelineno-21-20" name="__codelineno-21-20" href="#__codelineno-21-20"></a> <span class="s1">'MyCommand_Command'</span><span class="p">,</span>
<a id="__codelineno-21-21" name="__codelineno-21-21" href="#__codelineno-21-21"></a> <span class="s1">'categories'</span><span class="p">,</span>
<a id="__codelineno-21-22" name="__codelineno-21-22" href="#__codelineno-21-22"></a> <span class="n">fallback</span><span class="o">=</span><span class="s1">'cat1,cat2'</span><span class="p">,</span>
<a id="__codelineno-21-23" name="__codelineno-21-23" href="#__codelineno-21-23"></a> <span class="n">value_type</span><span class="o">=</span><span class="s1">'list'</span> <span class="c1"># Returns ['cat1', 'cat2']</span>
<a id="__codelineno-21-24" name="__codelineno-21-24" href="#__codelineno-21-24"></a> <span class="p">)</span>
</code></pre></div>
<h3 id="configuration-section-naming">Configuration Section Naming</h3>
<ul>
<li>Standard format: <code>CommandName_Command</code></li>
<li>Examples: <code>Joke_Command</code>, <code>Weather_Command</code>, <code>Status_Command</code></li>
<li>CamelCase commands: <code>DadJoke_Command</code>, <code>WebViewer_Command</code></li>
</ul>
<h3 id="localization-and-translation">Localization and Translation</h3>
<p>Use the translation system for internationalized text:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-22-1" name="__codelineno-22-1" href="#__codelineno-22-1"></a><span class="c1"># Simple translation</span>
<a id="__codelineno-22-2" name="__codelineno-22-2" href="#__codelineno-22-2"></a><span class="n">message_text</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">translate</span><span class="p">(</span><span class="s1">'commands.mycommand.error_message'</span><span class="p">)</span>
<a id="__codelineno-22-3" name="__codelineno-22-3" href="#__codelineno-22-3"></a>
<a id="__codelineno-22-4" name="__codelineno-22-4" href="#__codelineno-22-4"></a><span class="c1"># Translation with parameters</span>
<a id="__codelineno-22-5" name="__codelineno-22-5" href="#__codelineno-22-5"></a><span class="n">message_text</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">translate</span><span class="p">(</span>
<a id="__codelineno-22-6" name="__codelineno-22-6" href="#__codelineno-22-6"></a> <span class="s1">'commands.mycommand.response'</span><span class="p">,</span>
<a id="__codelineno-22-7" name="__codelineno-22-7" href="#__codelineno-22-7"></a> <span class="n">location</span><span class="o">=</span><span class="s2">"Seattle"</span><span class="p">,</span>
<a id="__codelineno-22-8" name="__codelineno-22-8" href="#__codelineno-22-8"></a> <span class="n">temperature</span><span class="o">=</span><span class="mi">72</span>
<a id="__codelineno-22-9" name="__codelineno-22-9" href="#__codelineno-22-9"></a><span class="p">)</span>
<a id="__codelineno-22-10" name="__codelineno-22-10" href="#__codelineno-22-10"></a>
<a id="__codelineno-22-11" name="__codelineno-22-11" href="#__codelineno-22-11"></a><span class="c1"># Get structured data (lists, dicts)</span>
<a id="__codelineno-22-12" name="__codelineno-22-12" href="#__codelineno-22-12"></a><span class="n">categories</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">translate_get_value</span><span class="p">(</span><span class="s1">'commands.mycommand.categories'</span><span class="p">)</span>
</code></pre></div>
<h3 id="auto-detecting-user-language">Auto-detecting User Language</h3>
<p>Use the context manager to respond in the user's detected language:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-23-1" name="__codelineno-23-1" href="#__codelineno-23-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-23-2" name="__codelineno-23-2" href="#__codelineno-23-2"></a><span class="w"> </span><span class="sd">"""Execute with language detection."""</span>
<a id="__codelineno-23-3" name="__codelineno-23-3" href="#__codelineno-23-3"></a> <span class="k">with</span> <span class="bp">self</span><span class="o">.</span><span class="n">respond_in_sender_language</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
<a id="__codelineno-23-4" name="__codelineno-23-4" href="#__codelineno-23-4"></a> <span class="c1"># All translate() calls use detected language</span>
<a id="__codelineno-23-5" name="__codelineno-23-5" href="#__codelineno-23-5"></a> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">translate</span><span class="p">(</span><span class="s1">'commands.mycommand.response'</span><span class="p">)</span>
<a id="__codelineno-23-6" name="__codelineno-23-6" href="#__codelineno-23-6"></a>
<a id="__codelineno-23-7" name="__codelineno-23-7" href="#__codelineno-23-7"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<a id="__codelineno-23-8" name="__codelineno-23-8" href="#__codelineno-23-8"></a> <span class="k">return</span> <span class="kc">True</span>
</code></pre></div>
<hr />
<h2 id="error-handling">Error Handling</h2>
<h3 id="general-error-handling-pattern">General Error Handling Pattern</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-24-1" name="__codelineno-24-1" href="#__codelineno-24-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-24-2" name="__codelineno-24-2" href="#__codelineno-24-2"></a><span class="w"> </span><span class="sd">"""Execute with proper error handling."""</span>
<a id="__codelineno-24-3" name="__codelineno-24-3" href="#__codelineno-24-3"></a> <span class="k">try</span><span class="p">:</span>
<a id="__codelineno-24-4" name="__codelineno-24-4" href="#__codelineno-24-4"></a> <span class="c1"># Record execution early for cooldown</span>
<a id="__codelineno-24-5" name="__codelineno-24-5" href="#__codelineno-24-5"></a> <span class="bp">self</span><span class="o">.</span><span class="n">record_execution</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">sender_id</span><span class="p">)</span>
<a id="__codelineno-24-6" name="__codelineno-24-6" href="#__codelineno-24-6"></a>
<a id="__codelineno-24-7" name="__codelineno-24-7" href="#__codelineno-24-7"></a> <span class="c1"># Main command logic</span>
<a id="__codelineno-24-8" name="__codelineno-24-8" href="#__codelineno-24-8"></a> <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">fetch_data</span><span class="p">()</span>
<a id="__codelineno-24-9" name="__codelineno-24-9" href="#__codelineno-24-9"></a>
<a id="__codelineno-24-10" name="__codelineno-24-10" href="#__codelineno-24-10"></a> <span class="k">if</span> <span class="n">result</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-24-11" name="__codelineno-24-11" href="#__codelineno-24-11"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span>
<a id="__codelineno-24-12" name="__codelineno-24-12" href="#__codelineno-24-12"></a> <span class="n">message</span><span class="p">,</span>
<a id="__codelineno-24-13" name="__codelineno-24-13" href="#__codelineno-24-13"></a> <span class="bp">self</span><span class="o">.</span><span class="n">translate</span><span class="p">(</span><span class="s1">'commands.mycommand.no_data'</span><span class="p">)</span>
<a id="__codelineno-24-14" name="__codelineno-24-14" href="#__codelineno-24-14"></a> <span class="p">)</span>
<a id="__codelineno-24-15" name="__codelineno-24-15" href="#__codelineno-24-15"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-24-16" name="__codelineno-24-16" href="#__codelineno-24-16"></a>
<a id="__codelineno-24-17" name="__codelineno-24-17" href="#__codelineno-24-17"></a> <span class="c1"># Format and send response</span>
<a id="__codelineno-24-18" name="__codelineno-24-18" href="#__codelineno-24-18"></a> <span class="n">response</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">format_response</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<a id="__codelineno-24-19" name="__codelineno-24-19" href="#__codelineno-24-19"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
<a id="__codelineno-24-20" name="__codelineno-24-20" href="#__codelineno-24-20"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-24-21" name="__codelineno-24-21" href="#__codelineno-24-21"></a>
<a id="__codelineno-24-22" name="__codelineno-24-22" href="#__codelineno-24-22"></a> <span class="k">except</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">TimeoutError</span><span class="p">:</span>
<a id="__codelineno-24-23" name="__codelineno-24-23" href="#__codelineno-24-23"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="s2">"Timeout in mycommand"</span><span class="p">)</span>
<a id="__codelineno-24-24" name="__codelineno-24-24" href="#__codelineno-24-24"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span>
<a id="__codelineno-24-25" name="__codelineno-24-25" href="#__codelineno-24-25"></a> <span class="n">message</span><span class="p">,</span>
<a id="__codelineno-24-26" name="__codelineno-24-26" href="#__codelineno-24-26"></a> <span class="bp">self</span><span class="o">.</span><span class="n">translate</span><span class="p">(</span><span class="s1">'commands.mycommand.timeout'</span><span class="p">)</span>
<a id="__codelineno-24-27" name="__codelineno-24-27" href="#__codelineno-24-27"></a> <span class="p">)</span>
<a id="__codelineno-24-28" name="__codelineno-24-28" href="#__codelineno-24-28"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-24-29" name="__codelineno-24-29" href="#__codelineno-24-29"></a>
<a id="__codelineno-24-30" name="__codelineno-24-30" href="#__codelineno-24-30"></a> <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<a id="__codelineno-24-31" name="__codelineno-24-31" href="#__codelineno-24-31"></a> <span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error in mycommand: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-24-32" name="__codelineno-24-32" href="#__codelineno-24-32"></a> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span>
<a id="__codelineno-24-33" name="__codelineno-24-33" href="#__codelineno-24-33"></a> <span class="n">message</span><span class="p">,</span>
<a id="__codelineno-24-34" name="__codelineno-24-34" href="#__codelineno-24-34"></a> <span class="bp">self</span><span class="o">.</span><span class="n">translate</span><span class="p">(</span><span class="s1">'commands.mycommand.error'</span><span class="p">)</span>
<a id="__codelineno-24-35" name="__codelineno-24-35" href="#__codelineno-24-35"></a> <span class="p">)</span>
<a id="__codelineno-24-36" name="__codelineno-24-36" href="#__codelineno-24-36"></a> <span class="k">return</span> <span class="kc">True</span>
</code></pre></div>
<h3 id="logging-levels">Logging Levels</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-25-1" name="__codelineno-25-1" href="#__codelineno-25-1"></a><span class="c1"># Debug: Detailed diagnostic information</span>
<a id="__codelineno-25-2" name="__codelineno-25-2" href="#__codelineno-25-2"></a><span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing message: </span><span class="si">{</span><span class="n">message</span><span class="o">.</span><span class="n">content</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-25-3" name="__codelineno-25-3" href="#__codelineno-25-3"></a>
<a id="__codelineno-25-4" name="__codelineno-25-4" href="#__codelineno-25-4"></a><span class="c1"># Info: General informational messages</span>
<a id="__codelineno-25-5" name="__codelineno-25-5" href="#__codelineno-25-5"></a><span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Command executed by </span><span class="si">{</span><span class="n">message</span><span class="o">.</span><span class="n">sender_id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-25-6" name="__codelineno-25-6" href="#__codelineno-25-6"></a>
<a id="__codelineno-25-7" name="__codelineno-25-7" href="#__codelineno-25-7"></a><span class="c1"># Warning: Something unexpected but handled</span>
<a id="__codelineno-25-8" name="__codelineno-25-8" href="#__codelineno-25-8"></a><span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid category '</span><span class="si">{</span><span class="n">category</span><span class="si">}</span><span class="s2">', using default"</span><span class="p">)</span>
<a id="__codelineno-25-9" name="__codelineno-25-9" href="#__codelineno-25-9"></a>
<a id="__codelineno-25-10" name="__codelineno-25-10" href="#__codelineno-25-10"></a><span class="c1"># Error: Error that prevented normal operation</span>
<a id="__codelineno-25-11" name="__codelineno-25-11" href="#__codelineno-25-11"></a><span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Failed to fetch data: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<a id="__codelineno-25-12" name="__codelineno-25-12" href="#__codelineno-25-12"></a>
<a id="__codelineno-25-13" name="__codelineno-25-13" href="#__codelineno-25-13"></a><span class="c1"># Critical: Serious error requiring attention</span>
<a id="__codelineno-25-14" name="__codelineno-25-14" href="#__codelineno-25-14"></a><span class="bp">self</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">critical</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Database connection failed: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
</code></pre></div>
<h3 id="input-validation">Input Validation</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-26-1" name="__codelineno-26-1" href="#__codelineno-26-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">validate_input</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
<a id="__codelineno-26-2" name="__codelineno-26-2" href="#__codelineno-26-2"></a><span class="w"> </span><span class="sd">"""Validate and parse command input."""</span>
<a id="__codelineno-26-3" name="__codelineno-26-3" href="#__codelineno-26-3"></a> <span class="n">content</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="n">content</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<a id="__codelineno-26-4" name="__codelineno-26-4" href="#__codelineno-26-4"></a>
<a id="__codelineno-26-5" name="__codelineno-26-5" href="#__codelineno-26-5"></a> <span class="c1"># Remove command prefix if present</span>
<a id="__codelineno-26-6" name="__codelineno-26-6" href="#__codelineno-26-6"></a> <span class="k">if</span> <span class="n">content</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s1">'!'</span><span class="p">):</span>
<a id="__codelineno-26-7" name="__codelineno-26-7" href="#__codelineno-26-7"></a> <span class="n">content</span> <span class="o">=</span> <span class="n">content</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<a id="__codelineno-26-8" name="__codelineno-26-8" href="#__codelineno-26-8"></a>
<a id="__codelineno-26-9" name="__codelineno-26-9" href="#__codelineno-26-9"></a> <span class="c1"># Split into parts</span>
<a id="__codelineno-26-10" name="__codelineno-26-10" href="#__codelineno-26-10"></a> <span class="n">parts</span> <span class="o">=</span> <span class="n">content</span><span class="o">.</span><span class="n">split</span><span class="p">()</span>
<a id="__codelineno-26-11" name="__codelineno-26-11" href="#__codelineno-26-11"></a>
<a id="__codelineno-26-12" name="__codelineno-26-12" href="#__codelineno-26-12"></a> <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">:</span>
<a id="__codelineno-26-13" name="__codelineno-26-13" href="#__codelineno-26-13"></a> <span class="k">return</span> <span class="kc">None</span>
<a id="__codelineno-26-14" name="__codelineno-26-14" href="#__codelineno-26-14"></a>
<a id="__codelineno-26-15" name="__codelineno-26-15" href="#__codelineno-26-15"></a> <span class="c1"># Validate parameter (e.g., zip code)</span>
<a id="__codelineno-26-16" name="__codelineno-26-16" href="#__codelineno-26-16"></a> <span class="n">param</span> <span class="o">=</span> <span class="n">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<a id="__codelineno-26-17" name="__codelineno-26-17" href="#__codelineno-26-17"></a> <span class="k">if</span> <span class="ow">not</span> <span class="n">re</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="sa">r</span><span class="s1">'^\d</span><span class="si">{5}</span><span class="s1">$'</span><span class="p">,</span> <span class="n">param</span><span class="p">):</span>
<a id="__codelineno-26-18" name="__codelineno-26-18" href="#__codelineno-26-18"></a> <span class="k">return</span> <span class="kc">None</span>
<a id="__codelineno-26-19" name="__codelineno-26-19" href="#__codelineno-26-19"></a>
<a id="__codelineno-26-20" name="__codelineno-26-20" href="#__codelineno-26-20"></a> <span class="k">return</span> <span class="n">param</span>
</code></pre></div>
<hr />
<h2 id="best-practices">Best Practices</h2>
<h3 id="1-always-use-cooldowns-for-external-apis">1. Always Use Cooldowns for External APIs</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-27-1" name="__codelineno-27-1" href="#__codelineno-27-1"></a><span class="k">class</span><span class="w"> </span><span class="nc">MyCommand</span><span class="p">(</span><span class="n">BaseCommand</span><span class="p">):</span>
<a id="__codelineno-27-2" name="__codelineno-27-2" href="#__codelineno-27-2"></a> <span class="n">cooldown_seconds</span> <span class="o">=</span> <span class="mi">5</span> <span class="c1"># Prevent API abuse</span>
<a id="__codelineno-27-3" name="__codelineno-27-3" href="#__codelineno-27-3"></a> <span class="n">requires_internet</span> <span class="o">=</span> <span class="kc">True</span>
</code></pre></div>
<h3 id="2-record-execution-early">2. Record Execution Early</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-28-1" name="__codelineno-28-1" href="#__codelineno-28-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-28-2" name="__codelineno-28-2" href="#__codelineno-28-2"></a> <span class="c1"># Record before doing work (for cooldown tracking)</span>
<a id="__codelineno-28-3" name="__codelineno-28-3" href="#__codelineno-28-3"></a> <span class="bp">self</span><span class="o">.</span><span class="n">record_execution</span><span class="p">(</span><span class="n">message</span><span class="o">.</span><span class="n">sender_id</span><span class="p">)</span>
<a id="__codelineno-28-4" name="__codelineno-28-4" href="#__codelineno-28-4"></a>
<a id="__codelineno-28-5" name="__codelineno-28-5" href="#__codelineno-28-5"></a> <span class="c1"># Then proceed with command logic</span>
<a id="__codelineno-28-6" name="__codelineno-28-6" href="#__codelineno-28-6"></a> <span class="c1"># ...</span>
</code></pre></div>
<h3 id="3-handle-message-length-limits">3. Handle Message Length Limits</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-29-1" name="__codelineno-29-1" href="#__codelineno-29-1"></a><span class="n">max_len</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_max_message_length</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<a id="__codelineno-29-2" name="__codelineno-29-2" href="#__codelineno-29-2"></a><span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">response</span><span class="p">)</span> <span class="o">&gt;</span> <span class="n">max_len</span><span class="p">:</span>
<a id="__codelineno-29-3" name="__codelineno-29-3" href="#__codelineno-29-3"></a> <span class="n">response</span> <span class="o">=</span> <span class="n">response</span><span class="p">[:</span><span class="n">max_len</span> <span class="o">-</span> <span class="mi">3</span><span class="p">]</span> <span class="o">+</span> <span class="s2">"..."</span>
<a id="__codelineno-29-4" name="__codelineno-29-4" href="#__codelineno-29-4"></a>
<a id="__codelineno-29-5" name="__codelineno-29-5" href="#__codelineno-29-5"></a><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">response</span><span class="p">)</span>
</code></pre></div>
<h3 id="4-use-caching-for-expensive-operations">4. Use Caching for Expensive Operations</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-30-1" name="__codelineno-30-1" href="#__codelineno-30-1"></a><span class="c1"># Check cache first</span>
<a id="__codelineno-30-2" name="__codelineno-30-2" href="#__codelineno-30-2"></a><span class="n">cached</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">get_cached_value</span><span class="p">(</span><span class="n">cache_key</span><span class="p">,</span> <span class="n">cache_type</span><span class="p">)</span>
<a id="__codelineno-30-3" name="__codelineno-30-3" href="#__codelineno-30-3"></a>
<a id="__codelineno-30-4" name="__codelineno-30-4" href="#__codelineno-30-4"></a><span class="k">if</span> <span class="n">cached</span><span class="p">:</span>
<a id="__codelineno-30-5" name="__codelineno-30-5" href="#__codelineno-30-5"></a> <span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">cached</span><span class="p">)</span>
<a id="__codelineno-30-6" name="__codelineno-30-6" href="#__codelineno-30-6"></a>
<a id="__codelineno-30-7" name="__codelineno-30-7" href="#__codelineno-30-7"></a><span class="c1"># Fetch and cache</span>
<a id="__codelineno-30-8" name="__codelineno-30-8" href="#__codelineno-30-8"></a><span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">fetch_expensive_data</span><span class="p">()</span>
<a id="__codelineno-30-9" name="__codelineno-30-9" href="#__codelineno-30-9"></a><span class="bp">self</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">db_manager</span><span class="o">.</span><span class="n">cache_value</span><span class="p">(</span>
<a id="__codelineno-30-10" name="__codelineno-30-10" href="#__codelineno-30-10"></a> <span class="n">cache_key</span><span class="o">=</span><span class="n">cache_key</span><span class="p">,</span>
<a id="__codelineno-30-11" name="__codelineno-30-11" href="#__codelineno-30-11"></a> <span class="n">cache_type</span><span class="o">=</span><span class="n">cache_type</span><span class="p">,</span>
<a id="__codelineno-30-12" name="__codelineno-30-12" href="#__codelineno-30-12"></a> <span class="n">cache_value</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">data</span><span class="p">),</span>
<a id="__codelineno-30-13" name="__codelineno-30-13" href="#__codelineno-30-13"></a> <span class="n">cache_hours</span><span class="o">=</span><span class="mi">24</span>
<a id="__codelineno-30-14" name="__codelineno-30-14" href="#__codelineno-30-14"></a><span class="p">)</span>
</code></pre></div>
<h3 id="5-offload-blocking-operations">5. Offload Blocking Operations</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-31-1" name="__codelineno-31-1" href="#__codelineno-31-1"></a><span class="c1"># DON'T: Block the event loop</span>
<a id="__codelineno-31-2" name="__codelineno-31-2" href="#__codelineno-31-2"></a><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span> <span class="o">=</span> <span class="n">geocode_city_sync</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="c1"># BAD</span>
<a id="__codelineno-31-3" name="__codelineno-31-3" href="#__codelineno-31-3"></a>
<a id="__codelineno-31-4" name="__codelineno-31-4" href="#__codelineno-31-4"></a><span class="c1"># DO: Offload to thread</span>
<a id="__codelineno-31-5" name="__codelineno-31-5" href="#__codelineno-31-5"></a><span class="n">lat</span><span class="p">,</span> <span class="n">lon</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">to_thread</span><span class="p">(</span><span class="n">geocode_city_sync</span><span class="p">,</span> <span class="o">...</span><span class="p">)</span> <span class="c1"># GOOD</span>
</code></pre></div>
<h3 id="6-provide-helpful-error-messages">6. Provide Helpful Error Messages</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-32-1" name="__codelineno-32-1" href="#__codelineno-32-1"></a><span class="c1"># DON'T: Generic errors</span>
<a id="__codelineno-32-2" name="__codelineno-32-2" href="#__codelineno-32-2"></a><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="s2">"Error"</span><span class="p">)</span>
<a id="__codelineno-32-3" name="__codelineno-32-3" href="#__codelineno-32-3"></a>
<a id="__codelineno-32-4" name="__codelineno-32-4" href="#__codelineno-32-4"></a><span class="c1"># DO: Specific, actionable errors</span>
<a id="__codelineno-32-5" name="__codelineno-32-5" href="#__codelineno-32-5"></a><span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">send_response</span><span class="p">(</span>
<a id="__codelineno-32-6" name="__codelineno-32-6" href="#__codelineno-32-6"></a> <span class="n">message</span><span class="p">,</span>
<a id="__codelineno-32-7" name="__codelineno-32-7" href="#__codelineno-32-7"></a> <span class="s2">"Could not find location 'Seatle'. Did you mean 'Seattle'?"</span>
<a id="__codelineno-32-8" name="__codelineno-32-8" href="#__codelineno-32-8"></a><span class="p">)</span>
</code></pre></div>
<h3 id="7-support-both-dm-and-channel-contexts">7. Support Both DM and Channel Contexts</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-33-1" name="__codelineno-33-1" href="#__codelineno-33-1"></a><span class="k">def</span><span class="w"> </span><span class="nf">can_execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-33-2" name="__codelineno-33-2" href="#__codelineno-33-2"></a><span class="w"> </span><span class="sd">"""Allow in DM, restrict in channels."""</span>
<a id="__codelineno-33-3" name="__codelineno-33-3" href="#__codelineno-33-3"></a> <span class="k">if</span> <span class="ow">not</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">can_execute</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">):</span>
<a id="__codelineno-33-4" name="__codelineno-33-4" href="#__codelineno-33-4"></a> <span class="k">return</span> <span class="kc">False</span>
<a id="__codelineno-33-5" name="__codelineno-33-5" href="#__codelineno-33-5"></a>
<a id="__codelineno-33-6" name="__codelineno-33-6" href="#__codelineno-33-6"></a> <span class="c1"># DM always allowed</span>
<a id="__codelineno-33-7" name="__codelineno-33-7" href="#__codelineno-33-7"></a> <span class="k">if</span> <span class="n">message</span><span class="o">.</span><span class="n">is_dm</span><span class="p">:</span>
<a id="__codelineno-33-8" name="__codelineno-33-8" href="#__codelineno-33-8"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-33-9" name="__codelineno-33-9" href="#__codelineno-33-9"></a>
<a id="__codelineno-33-10" name="__codelineno-33-10" href="#__codelineno-33-10"></a> <span class="c1"># Channel-specific logic</span>
<a id="__codelineno-33-11" name="__codelineno-33-11" href="#__codelineno-33-11"></a> <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">is_channel_allowed</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</code></pre></div>
<h3 id="8-use-type-hints">8. Use Type Hints</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-34-1" name="__codelineno-34-1" href="#__codelineno-34-1"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-34-2" name="__codelineno-34-2" href="#__codelineno-34-2"></a><span class="w"> </span><span class="sd">"""Execute with proper type hints."""</span>
<a id="__codelineno-34-3" name="__codelineno-34-3" href="#__codelineno-34-3"></a> <span class="n">result</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">dict</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">fetch_data</span><span class="p">()</span>
<a id="__codelineno-34-4" name="__codelineno-34-4" href="#__codelineno-34-4"></a>
<a id="__codelineno-34-5" name="__codelineno-34-5" href="#__codelineno-34-5"></a> <span class="k">if</span> <span class="n">result</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<a id="__codelineno-34-6" name="__codelineno-34-6" href="#__codelineno-34-6"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-34-7" name="__codelineno-34-7" href="#__codelineno-34-7"></a>
<a id="__codelineno-34-8" name="__codelineno-34-8" href="#__codelineno-34-8"></a> <span class="n">temperature</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">'temp'</span><span class="p">,</span> <span class="mf">0.0</span><span class="p">)</span>
<a id="__codelineno-34-9" name="__codelineno-34-9" href="#__codelineno-34-9"></a> <span class="c1"># ...</span>
</code></pre></div>
<h3 id="9-implement-admin-only-commands-securely">9. Implement Admin-Only Commands Securely</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-35-1" name="__codelineno-35-1" href="#__codelineno-35-1"></a><span class="k">class</span><span class="w"> </span><span class="nc">AdminCommand</span><span class="p">(</span><span class="n">BaseCommand</span><span class="p">):</span>
<a id="__codelineno-35-2" name="__codelineno-35-2" href="#__codelineno-35-2"></a> <span class="n">requires_dm</span> <span class="o">=</span> <span class="kc">True</span> <span class="c1"># Admin commands should be DM-only</span>
<a id="__codelineno-35-3" name="__codelineno-35-3" href="#__codelineno-35-3"></a>
<a id="__codelineno-35-4" name="__codelineno-35-4" href="#__codelineno-35-4"></a> <span class="k">def</span><span class="w"> </span><span class="nf">requires_admin_access</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-35-5" name="__codelineno-35-5" href="#__codelineno-35-5"></a><span class="w"> </span><span class="sd">"""Mark as requiring admin access."""</span>
<a id="__codelineno-35-6" name="__codelineno-35-6" href="#__codelineno-35-6"></a> <span class="k">return</span> <span class="kc">True</span>
<a id="__codelineno-35-7" name="__codelineno-35-7" href="#__codelineno-35-7"></a>
<a id="__codelineno-35-8" name="__codelineno-35-8" href="#__codelineno-35-8"></a> <span class="k">def</span><span class="w"> </span><span class="nf">can_execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="n">MeshMessage</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="kc">False</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<a id="__codelineno-35-9" name="__codelineno-35-9" href="#__codelineno-35-9"></a><span class="w"> </span><span class="sd">"""Check admin access."""</span>
<a id="__codelineno-35-10" name="__codelineno-35-10" href="#__codelineno-35-10"></a> <span class="k">if</span> <span class="ow">not</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">can_execute</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">skip_channel_check</span><span class="p">):</span>
<a id="__codelineno-35-11" name="__codelineno-35-11" href="#__codelineno-35-11"></a> <span class="k">return</span> <span class="kc">False</span>
<a id="__codelineno-35-12" name="__codelineno-35-12" href="#__codelineno-35-12"></a>
<a id="__codelineno-35-13" name="__codelineno-35-13" href="#__codelineno-35-13"></a> <span class="c1"># BaseCommand handles admin pubkey verification</span>
<a id="__codelineno-35-14" name="__codelineno-35-14" href="#__codelineno-35-14"></a> <span class="k">return</span> <span class="kc">True</span>
</code></pre></div>
<h3 id="10-document-your-command">10. Document Your Command</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-36-1" name="__codelineno-36-1" href="#__codelineno-36-1"></a><span class="k">class</span><span class="w"> </span><span class="nc">MyCommand</span><span class="p">(</span><span class="n">BaseCommand</span><span class="p">):</span>
<a id="__codelineno-36-2" name="__codelineno-36-2" href="#__codelineno-36-2"></a><span class="w"> </span><span class="sd">"""Detailed description of what this command does.</span>
<a id="__codelineno-36-3" name="__codelineno-36-3" href="#__codelineno-36-3"></a>
<a id="__codelineno-36-4" name="__codelineno-36-4" href="#__codelineno-36-4"></a><span class="sd"> Includes information about:</span>
<a id="__codelineno-36-5" name="__codelineno-36-5" href="#__codelineno-36-5"></a><span class="sd"> - What data it fetches</span>
<a id="__codelineno-36-6" name="__codelineno-36-6" href="#__codelineno-36-6"></a><span class="sd"> - What APIs it uses</span>
<a id="__codelineno-36-7" name="__codelineno-36-7" href="#__codelineno-36-7"></a><span class="sd"> - Any special requirements</span>
<a id="__codelineno-36-8" name="__codelineno-36-8" href="#__codelineno-36-8"></a><span class="sd"> """</span>
<a id="__codelineno-36-9" name="__codelineno-36-9" href="#__codelineno-36-9"></a>
<a id="__codelineno-36-10" name="__codelineno-36-10" href="#__codelineno-36-10"></a> <span class="c1"># Complete metadata</span>
<a id="__codelineno-36-11" name="__codelineno-36-11" href="#__codelineno-36-11"></a> <span class="n">short_description</span> <span class="o">=</span> <span class="s2">"Get data from service"</span>
<a id="__codelineno-36-12" name="__codelineno-36-12" href="#__codelineno-36-12"></a> <span class="n">usage</span> <span class="o">=</span> <span class="s2">"mycommand &lt;param&gt; [option]"</span>
<a id="__codelineno-36-13" name="__codelineno-36-13" href="#__codelineno-36-13"></a> <span class="n">examples</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"mycommand test"</span><span class="p">,</span> <span class="s2">"mycommand test --verbose"</span><span class="p">]</span>
<a id="__codelineno-36-14" name="__codelineno-36-14" href="#__codelineno-36-14"></a> <span class="n">parameters</span> <span class="o">=</span> <span class="p">[</span>
<a id="__codelineno-36-15" name="__codelineno-36-15" href="#__codelineno-36-15"></a> <span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"param"</span><span class="p">,</span> <span class="s2">"description"</span><span class="p">:</span> <span class="s2">"Required parameter"</span><span class="p">},</span>
<a id="__codelineno-36-16" name="__codelineno-36-16" href="#__codelineno-36-16"></a> <span class="p">{</span><span class="s2">"name"</span><span class="p">:</span> <span class="s2">"option"</span><span class="p">,</span> <span class="s2">"description"</span><span class="p">:</span> <span class="s2">"Optional flag"</span><span class="p">}</span>
<a id="__codelineno-36-17" name="__codelineno-36-17" href="#__codelineno-36-17"></a> <span class="p">]</span>
</code></pre></div>
<hr />
<h2 id="testing-your-command">Testing Your Command</h2>
<h3 id="unit-testing">Unit Testing</h3>
<p>Create tests in <code>tests/commands/test_yourcommand_command.py</code>:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-37-1" name="__codelineno-37-1" href="#__codelineno-37-1"></a><span class="kn">import</span><span class="w"> </span><span class="nn">pytest</span>
<a id="__codelineno-37-2" name="__codelineno-37-2" href="#__codelineno-37-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.commands.yourcommand_command</span><span class="w"> </span><span class="kn">import</span> <span class="n">YourCommand</span>
<a id="__codelineno-37-3" name="__codelineno-37-3" href="#__codelineno-37-3"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.models</span><span class="w"> </span><span class="kn">import</span> <span class="n">MeshMessage</span>
<a id="__codelineno-37-4" name="__codelineno-37-4" href="#__codelineno-37-4"></a>
<a id="__codelineno-37-5" name="__codelineno-37-5" href="#__codelineno-37-5"></a>
<a id="__codelineno-37-6" name="__codelineno-37-6" href="#__codelineno-37-6"></a><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
<a id="__codelineno-37-7" name="__codelineno-37-7" href="#__codelineno-37-7"></a><span class="k">def</span><span class="w"> </span><span class="nf">mock_bot</span><span class="p">():</span>
<a id="__codelineno-37-8" name="__codelineno-37-8" href="#__codelineno-37-8"></a><span class="w"> </span><span class="sd">"""Create mock bot instance."""</span>
<a id="__codelineno-37-9" name="__codelineno-37-9" href="#__codelineno-37-9"></a> <span class="c1"># Implementation depends on your test framework</span>
<a id="__codelineno-37-10" name="__codelineno-37-10" href="#__codelineno-37-10"></a> <span class="k">pass</span>
<a id="__codelineno-37-11" name="__codelineno-37-11" href="#__codelineno-37-11"></a>
<a id="__codelineno-37-12" name="__codelineno-37-12" href="#__codelineno-37-12"></a>
<a id="__codelineno-37-13" name="__codelineno-37-13" href="#__codelineno-37-13"></a><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
<a id="__codelineno-37-14" name="__codelineno-37-14" href="#__codelineno-37-14"></a><span class="k">def</span><span class="w"> </span><span class="nf">command</span><span class="p">(</span><span class="n">mock_bot</span><span class="p">):</span>
<a id="__codelineno-37-15" name="__codelineno-37-15" href="#__codelineno-37-15"></a><span class="w"> </span><span class="sd">"""Create command instance."""</span>
<a id="__codelineno-37-16" name="__codelineno-37-16" href="#__codelineno-37-16"></a> <span class="k">return</span> <span class="n">YourCommand</span><span class="p">(</span><span class="n">mock_bot</span><span class="p">)</span>
<a id="__codelineno-37-17" name="__codelineno-37-17" href="#__codelineno-37-17"></a>
<a id="__codelineno-37-18" name="__codelineno-37-18" href="#__codelineno-37-18"></a>
<a id="__codelineno-37-19" name="__codelineno-37-19" href="#__codelineno-37-19"></a><span class="nd">@pytest</span><span class="o">.</span><span class="n">mark</span><span class="o">.</span><span class="n">asyncio</span>
<a id="__codelineno-37-20" name="__codelineno-37-20" href="#__codelineno-37-20"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">test_execute_success</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="n">mock_bot</span><span class="p">):</span>
<a id="__codelineno-37-21" name="__codelineno-37-21" href="#__codelineno-37-21"></a><span class="w"> </span><span class="sd">"""Test successful execution."""</span>
<a id="__codelineno-37-22" name="__codelineno-37-22" href="#__codelineno-37-22"></a> <span class="n">message</span> <span class="o">=</span> <span class="n">MeshMessage</span><span class="p">(</span>
<a id="__codelineno-37-23" name="__codelineno-37-23" href="#__codelineno-37-23"></a> <span class="n">content</span><span class="o">=</span><span class="s2">"yourcommand test"</span><span class="p">,</span>
<a id="__codelineno-37-24" name="__codelineno-37-24" href="#__codelineno-37-24"></a> <span class="n">sender_id</span><span class="o">=</span><span class="s2">"!12345678"</span><span class="p">,</span>
<a id="__codelineno-37-25" name="__codelineno-37-25" href="#__codelineno-37-25"></a> <span class="n">is_dm</span><span class="o">=</span><span class="kc">True</span>
<a id="__codelineno-37-26" name="__codelineno-37-26" href="#__codelineno-37-26"></a> <span class="p">)</span>
<a id="__codelineno-37-27" name="__codelineno-37-27" href="#__codelineno-37-27"></a>
<a id="__codelineno-37-28" name="__codelineno-37-28" href="#__codelineno-37-28"></a> <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">command</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<a id="__codelineno-37-29" name="__codelineno-37-29" href="#__codelineno-37-29"></a> <span class="k">assert</span> <span class="n">result</span> <span class="ow">is</span> <span class="kc">True</span>
<a id="__codelineno-37-30" name="__codelineno-37-30" href="#__codelineno-37-30"></a>
<a id="__codelineno-37-31" name="__codelineno-37-31" href="#__codelineno-37-31"></a>
<a id="__codelineno-37-32" name="__codelineno-37-32" href="#__codelineno-37-32"></a><span class="nd">@pytest</span><span class="o">.</span><span class="n">mark</span><span class="o">.</span><span class="n">asyncio</span>
<a id="__codelineno-37-33" name="__codelineno-37-33" href="#__codelineno-37-33"></a><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">test_execute_invalid_input</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="n">mock_bot</span><span class="p">):</span>
<a id="__codelineno-37-34" name="__codelineno-37-34" href="#__codelineno-37-34"></a><span class="w"> </span><span class="sd">"""Test with invalid input."""</span>
<a id="__codelineno-37-35" name="__codelineno-37-35" href="#__codelineno-37-35"></a> <span class="n">message</span> <span class="o">=</span> <span class="n">MeshMessage</span><span class="p">(</span>
<a id="__codelineno-37-36" name="__codelineno-37-36" href="#__codelineno-37-36"></a> <span class="n">content</span><span class="o">=</span><span class="s2">"yourcommand"</span><span class="p">,</span>
<a id="__codelineno-37-37" name="__codelineno-37-37" href="#__codelineno-37-37"></a> <span class="n">sender_id</span><span class="o">=</span><span class="s2">"!12345678"</span><span class="p">,</span>
<a id="__codelineno-37-38" name="__codelineno-37-38" href="#__codelineno-37-38"></a> <span class="n">is_dm</span><span class="o">=</span><span class="kc">True</span>
<a id="__codelineno-37-39" name="__codelineno-37-39" href="#__codelineno-37-39"></a> <span class="p">)</span>
<a id="__codelineno-37-40" name="__codelineno-37-40" href="#__codelineno-37-40"></a>
<a id="__codelineno-37-41" name="__codelineno-37-41" href="#__codelineno-37-41"></a> <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">command</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
<a id="__codelineno-37-42" name="__codelineno-37-42" href="#__codelineno-37-42"></a> <span class="k">assert</span> <span class="n">result</span> <span class="ow">is</span> <span class="kc">True</span> <span class="c1"># Should handle gracefully</span>
</code></pre></div>
<h3 id="manual-testing">Manual Testing</h3>
<ol>
<li><strong>Install your command</strong>: Place the file in <code>local/commands/</code></li>
<li><strong>Configure</strong>: Add section to <code>config.ini</code>:
<div class="highlight"><pre><span></span><code><a id="__codelineno-38-1" name="__codelineno-38-1" href="#__codelineno-38-1"></a><span class="k">[YourCommand_Command]</span>
<a id="__codelineno-38-2" name="__codelineno-38-2" href="#__codelineno-38-2"></a><span class="na">enabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span>
</code></pre></div></li>
<li><strong>Restart bot</strong>: The command will be auto-discovered from the local commands directory</li>
<li><strong>Test</strong>: Send messages to the bot to trigger your command</li>
</ol>
<h3 id="testing-checklist">Testing Checklist</h3>
<ul>
<li>[ ] Command responds to all keywords</li>
<li>[ ] Cooldown works correctly</li>
<li>[ ] DM vs. channel behavior is correct</li>
<li>[ ] Error handling works (network failures, invalid input)</li>
<li>[ ] Message length limits are respected</li>
<li>[ ] Database operations don't cause errors</li>
<li>[ ] Help text is accurate</li>
<li>[ ] Translations work (if using i18n)</li>
<li>[ ] Admin access works (if admin-only)</li>
<li>[ ] Rate limiting prevents abuse</li>
</ul>
<hr />
<h2 id="additional-resources">Additional Resources</h2>
<h3 id="key-files-to-reference">Key Files to Reference</h3>
<ul>
<li><code>modules/commands/base_command.py</code> - Base class implementation</li>
<li><code>modules/commands/joke_command.py</code> - Simple API-based command example</li>
<li><code>modules/commands/status_command.py</code> - Simple admin command example</li>
<li><code>modules/commands/aurora_command.py</code> - Complex command with geocoding</li>
<li><code>modules/commands/wx_command.py</code> - Advanced command with multiple features</li>
<li><code>modules/db_manager.py</code> - Database operations</li>
<li><code>modules/utils.py</code> - Utility functions</li>
<li><code>modules/models.py</code> - Data models (MeshMessage)</li>
</ul>
<h3 id="common-utilities">Common Utilities</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-39-1" name="__codelineno-39-1" href="#__codelineno-39-1"></a><span class="c1"># From modules.utils</span>
<a id="__codelineno-39-2" name="__codelineno-39-2" href="#__codelineno-39-2"></a><span class="kn">from</span><span class="w"> </span><span class="nn">modules.utils</span><span class="w"> </span><span class="kn">import</span> <span class="p">(</span>
<a id="__codelineno-39-3" name="__codelineno-39-3" href="#__codelineno-39-3"></a> <span class="n">geocode_city_sync</span><span class="p">,</span> <span class="c1"># Geocode city name</span>
<a id="__codelineno-39-4" name="__codelineno-39-4" href="#__codelineno-39-4"></a> <span class="n">geocode_zipcode_sync</span><span class="p">,</span> <span class="c1"># Geocode US ZIP code</span>
<a id="__codelineno-39-5" name="__codelineno-39-5" href="#__codelineno-39-5"></a> <span class="n">get_config_timezone</span><span class="p">,</span> <span class="c1"># Get timezone from config</span>
<a id="__codelineno-39-6" name="__codelineno-39-6" href="#__codelineno-39-6"></a> <span class="n">format_elapsed_display</span><span class="p">,</span> <span class="c1"># Format elapsed time</span>
<a id="__codelineno-39-7" name="__codelineno-39-7" href="#__codelineno-39-7"></a> <span class="n">message_hop_count</span><span class="p">,</span> <span class="c1"># Get hop count from message</span>
<a id="__codelineno-39-8" name="__codelineno-39-8" href="#__codelineno-39-8"></a> <span class="n">get_packet_hash_placeholder</span><span class="p">,</span> <span class="c1"># Get packet hash for display</span>
<a id="__codelineno-39-9" name="__codelineno-39-9" href="#__codelineno-39-9"></a><span class="p">)</span>
</code></pre></div>
<h3 id="configuration-file-structure">Configuration File Structure</h3>
<div class="highlight"><pre><span></span><code><a id="__codelineno-40-1" name="__codelineno-40-1" href="#__codelineno-40-1"></a><span class="k">[Yourcommand_Command]</span>
<a id="__codelineno-40-2" name="__codelineno-40-2" href="#__codelineno-40-2"></a><span class="na">enabled</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">true</span>
<a id="__codelineno-40-3" name="__codelineno-40-3" href="#__codelineno-40-3"></a><span class="na">cooldown_queue_threshold_seconds</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">5.0</span>
<a id="__codelineno-40-4" name="__codelineno-40-4" href="#__codelineno-40-4"></a><span class="na">channels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="c1">#general,#weather # Optional: restrict to specific channels</span>
<a id="__codelineno-40-5" name="__codelineno-40-5" href="#__codelineno-40-5"></a><span class="na">aliases</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">yc, ycmd</span><span class="w"> </span><span class="c1"># Optional: additional trigger words</span>
<a id="__codelineno-40-6" name="__codelineno-40-6" href="#__codelineno-40-6"></a>
<a id="__codelineno-40-7" name="__codelineno-40-7" href="#__codelineno-40-7"></a><span class="c1"># Custom settings</span>
<a id="__codelineno-40-8" name="__codelineno-40-8" href="#__codelineno-40-8"></a><span class="na">timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">10</span>
<a id="__codelineno-40-9" name="__codelineno-40-9" href="#__codelineno-40-9"></a><span class="na">max_results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">5</span>
<a id="__codelineno-40-10" name="__codelineno-40-10" href="#__codelineno-40-10"></a><span class="na">api_key</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">your_api_key_here</span>
</code></pre></div>
<p><strong>Note:</strong> The proper section name is <code>Yourcommand_Command</code> not
<code>YourCommand_Command</code>. Using the latter will cause core functions
that reference <code>enabled</code>, <code>channels</code> and <code>aliases</code> to fail.</p>
<hr />
<h2 id="local-commands-directory">Local Commands Directory</h2>
<p>Custom commands should be placed in the <code>local/commands/</code> directory:</p>
<div class="highlight"><pre><span></span><code><a id="__codelineno-41-1" name="__codelineno-41-1" href="#__codelineno-41-1"></a>meshcore-bot/
<a id="__codelineno-41-2" name="__codelineno-41-2" href="#__codelineno-41-2"></a>├── modules/
<a id="__codelineno-41-3" name="__codelineno-41-3" href="#__codelineno-41-3"></a>│ └── commands/ # Core distributed commands (do not modify)
<a id="__codelineno-41-4" name="__codelineno-41-4" href="#__codelineno-41-4"></a>│ └── base_command.py
<a id="__codelineno-41-5" name="__codelineno-41-5" href="#__codelineno-41-5"></a>├── local/
<a id="__codelineno-41-6" name="__codelineno-41-6" href="#__codelineno-41-6"></a>│ └── commands/ # Your custom commands go here
<a id="__codelineno-41-7" name="__codelineno-41-7" href="#__codelineno-41-7"></a>│ ├── __init__.py
<a id="__codelineno-41-8" name="__codelineno-41-8" href="#__codelineno-41-8"></a>│ └── yourcommand_command.py
<a id="__codelineno-41-9" name="__codelineno-41-9" href="#__codelineno-41-9"></a>└── config.ini
</code></pre></div>
<p>The bot automatically discovers and loads commands from the <code>local/commands/</code> directory at startup, allowing you to extend functionality without modifying core bot files. This separation ensures your custom commands won't be overwritten during bot updates.</p>
<hr />
<h2 id="summary">Summary</h2>
<p>Developing commands for MeshCore Bot involves:</p>
<ol>
<li><strong>Create file in <code>local/commands/</code></strong> with your command class</li>
<li><strong>Inherit from BaseCommand</strong> and set class-level metadata</li>
<li><strong>Use absolute imports</strong> from <code>modules.*</code> packages</li>
<li><strong>Implement <code>execute()</code></strong> with your command logic</li>
<li><strong>Use <code>send_response()</code></strong> to reply to users</li>
<li><strong>Access database</strong> via <code>self.bot.db_manager</code></li>
<li><strong>Offload blocking I/O</strong> with <code>asyncio.to_thread()</code></li>
<li><strong>Handle errors gracefully</strong> and log appropriately</li>
<li><strong>Test thoroughly</strong> in both DM and channel contexts</li>
</ol>
<p>Follow the patterns in existing commands and refer to this guide when implementing new functionality. The framework handles most of the complexity around message routing, rate limiting, and channel management, allowing you to focus on your command's core functionality.</p>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "..", "features": ["navigation.instant", "navigation.tracking", "navigation.tabs", "navigation.sections", "toc.integrate", "content.code.copy"], "search": "../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../assets/javascripts/bundle.d7400e89.min.js"></script>
</body>
</html>