Split 1424-line monolith into orac/ package with clean separation: - constants.py — bot identity, radio config, triggers, limits - crypto.py — Ed25519 identity, channel/peer crypto, ECDH - meshcore.py — packet parsing/building, routing, dedup, node registry - ai.py — Claude API integration, query extraction - state.py — JSON persistence, history tracking - bot.py — main loop, packet dispatch, connection management Removed duplicated donglora library code (cobs_frame, read_frame, encode_radio_config, encode_command, decode_response) — now uses donglora.send/recv/connect directly. Added MeshCore Public channel (fixed PSK 8b3387e9...cd72) and expanded built-in hashtag channels: #nodeops, #repeater-ops, #test, #weather, #protest, #general, #bot, #net, #emcomm. Replaced ANSI print() with stdlib logging (color formatter for TTY). Fixed dependency (donglora-python -> donglora). Strict ruff linting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97 lines
4.7 KiB
Python
97 lines
4.7 KiB
Python
"""Orac bot constants: names, radio config, triggers, limits, paths."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
# ── Bot identity ─────────────────────────────────────────────────
|
|
|
|
BOT_NAME: str = "Orac"
|
|
|
|
# ── Payload limits ───────────────────────────────────────────────
|
|
|
|
MAX_GRP_TEXT: int = 163 # max bytes for "SenderName: message" in a GRP_TXT
|
|
MAX_RESPONSE_CHARS: int = MAX_GRP_TEXT - len(BOT_NAME) - 2 # subtract "Orac: "
|
|
# DM plaintext: timestamp(4) + txt_type_attempt(1) + text + padding to 16
|
|
# Max payload 184 - dst(1) - src(1) - mac(2) = 180 bytes ciphertext
|
|
# 180 bytes ciphertext / 16 = 11 blocks -> 176 bytes plaintext max
|
|
# 176 - 5 (header) = 171 bytes text, minus null terminator padding
|
|
MAX_DM_TEXT: int = 170
|
|
|
|
# ── Trigger words ────────────────────────────────────────────────
|
|
|
|
TRIGGERS: tuple[str, ...] = ("!askai", "!ai", "!claude", "!orac")
|
|
AT_MENTIONS: tuple[str, ...] = (f"@{BOT_NAME}".lower(), f"@[{BOT_NAME}]".lower())
|
|
|
|
# ── History / rate-limit sizing ──────────────────────────────────
|
|
|
|
CHANNEL_HISTORY_SIZE: int = 20
|
|
DM_HISTORY_SIZE: int = 20
|
|
|
|
RATE_LIMIT_PER_SENDER: float = 5 # seconds, channel messages
|
|
RATE_LIMIT_GLOBAL: float = 2 # seconds, any two responses
|
|
RATE_LIMIT_DM: float = 2 # seconds, DMs (more interactive)
|
|
RATE_LIMIT_REPLY_COOLDOWN: float = 30 # seconds between rate-limit replies
|
|
|
|
# ── Timers ───────────────────────────────────────────────────────
|
|
|
|
ADVERT_INTERVAL: float = 7200 # 2 hours in seconds
|
|
|
|
# ── Paths ────────────────────────────────────────────────────────
|
|
|
|
DATA_DIR: Path = Path.home() / ".donglora"
|
|
KEY_FILE: Path = DATA_DIR / "orac_key.bin"
|
|
STATE_FILE: Path = DATA_DIR / "orac_state.json"
|
|
|
|
# ── Radio ────────────────────────────────────────────────────────
|
|
|
|
RADIO_CONFIG: dict[str, int] = {
|
|
"freq_hz": 910_525_000,
|
|
"bw": 6, # 62.5 kHz
|
|
"sf": 7,
|
|
"cr": 5, # CR 4/5
|
|
"sync_word": 0x3444,
|
|
"tx_power_dbm": -128, # TX_POWER_MAX
|
|
}
|
|
|
|
# ── AI system prompt ─────────────────────────────────────────────
|
|
|
|
SYSTEM_PROMPT: str = """\
|
|
You are Orac, a terse AI assistant on a MeshCore LoRa radio mesh network.
|
|
|
|
ABSOLUTE HARD LIMIT: Your ENTIRE response must be {{max_chars}} characters or fewer.
|
|
This is a physical constraint of the radio protocol. Every character beyond this limit
|
|
is permanently lost and never transmitted. There is NO exception.
|
|
|
|
Stay well under {{max_chars}}. Aim for 120 or fewer. NEVER show a character count or HTML tags in your reply.
|
|
|
|
You can see recent messages for context. When someone asks you a question, they may
|
|
be referring to the ongoing conversation — use the chat history to understand what they mean.
|
|
For example, "what do you think?" refers to whatever was just discussed. "tell me more" means
|
|
elaborate on the recent topic. Read the room.
|
|
|
|
Rules:
|
|
- One sentence or short phrase. Never more.
|
|
- Emoji OK but don't overdo it. No markdown, no bullet points, no formatting.
|
|
- Use common abbreviations freely (e.g., w/, b/c, approx, etc).
|
|
- Skip all pleasantries, greetings, and filler.
|
|
- You have a dry, sardonic wit. Be helpful but never wordy.
|
|
- If the question requires a long answer, give the most important point only.
|
|
- NEVER repeat a previous answer. Always find a fresh angle, new phrasing, or different take.
|
|
- Be creative and unpredictable. Surprise the reader.\
|
|
"""
|
|
|
|
# ── MeshCore protocol constants ──────────────────────────────────
|
|
|
|
ADVERT_MAX_AGE: float = 43200 # 12 hours (display only, not key expiry)
|
|
PENDING_DM_TTL: float = 300 # 5 minutes
|
|
|
|
DEDUP_TTL: float = 120 # seconds
|
|
DM_DEDUP_TTL: float = 60 # seconds
|
|
|
|
RESP_SERVER_LOGIN_OK: int = 0x01
|
|
|
|
# ── Channels CSV path ────────────────────────────────────────────
|
|
# Resolved at import time relative to the project root (ai_bot.py's directory).
|
|
CHANNELS_CSV: Path = Path(__file__).resolve().parent.parent / "channels.csv"
|