Install
openclaw skills install telegram-colored-choices-buttonsAutomatically colors Telegram bot choice buttons as default, destructive (red), or secondary (gray) based on the action's criticality and reversibility.
openclaw skills install telegram-colored-choices-buttonsEvery time the bot presents choices to the user, it MUST automatically color each button based on how critical, irreversible, or different the choices are. The bot never sends plain/unstyled choice buttons — color is always applied.
Whenever the bot sends 2+ choices to a user, classify each choice and assign a style automatically. This is not optional.
Two fields on InlineKeyboardButton and KeyboardButton:
style (String, Optional) — Button color:
"destructive" — Red. Irreversible, dangerous, or high-stakes actions."secondary" — Gray/muted. Low-priority, dismiss, skip, or neutral actions.icon_custom_emoji_id (String, Optional) — Custom emoji icon on the button.
When the bot builds a set of choice buttons, it MUST classify every choice into one of three tiers before sending:
Apply when the choice is:
Do: omit the style field (or set to null).
Apply when the choice:
Do: set "style": "destructive".
Apply when the choice:
Do: set "style": "secondary".
When choices differ in criticality, the colors MUST reflect that contrast:
High contrast — choices have very different consequences:
"Delete my account" → destructive (red)
"Keep my account" → default (blue)
Medium contrast — one main action, one escape:
"Subscribe" → default (blue)
"Not now" → secondary (gray)
Low contrast — choices are roughly equal:
"Option A" → default (blue)
"Option B" → default (blue)
"Skip" → secondary (gray)
Multiple tiers in one set:
"Confirm purchase" → default (blue) — recommended
"Change amount" → secondary (gray) — neutral/back
"Cancel order" → destructive (red) — loses progress
Bot asks: "Approve this document?"
[
[{"text": "✅ Approve", "callback_data": "approve"},
{"text": "❌ Reject", "callback_data": "reject", "style": "destructive"}],
[{"text": "⏭ Review later", "callback_data": "skip", "style": "secondary"}]
]
Bot asks: "Pick a plan:"
[
[{"text": "Free Plan", "callback_data": "free"},
{"text": "Pro Plan", "callback_data": "pro"}],
[{"text": "Compare plans", "callback_data": "compare", "style": "secondary"}]
]
(Equal choices = both default; info link = secondary)
Bot asks: "Delete all messages in this chat?"
[
[{"text": "🗑 Delete all", "callback_data": "delete_all", "style": "destructive"}],
[{"text": "Keep messages", "callback_data": "keep"}]
]
(Destructive action is red; safe action is the default blue)
Bot asks: "Transfer 500 tokens to @user?"
[
[{"text": "Send 500 tokens", "callback_data": "send", "style": "destructive"},
{"text": "Cancel", "callback_data": "cancel", "style": "secondary"}]
]
(Spending = destructive since it costs something; cancel = secondary)
The bot MUST use a classifier function to determine style. Here is the reference implementation:
import re
# Keywords that signal each tier (case-insensitive, matched against button text + callback_data)
DESTRUCTIVE_SIGNALS = [
r"\bdelete\b", r"\bremove\b", r"\bban\b", r"\bblock\b",
r"\breject\b", r"\bdecline\b", r"\brevoke\b", r"\bterminate\b",
r"\bcancel order\b", r"\bcancel subscription\b",
r"\bunsubscribe\b", r"\bdestroy\b", r"\bpurge\b",
r"\bspend\b", r"\btransfer\b", r"\bpay\b", r"\bsend.*tokens?\b",
r"\breset\b", r"\bclear all\b", r"\bwipe\b",
r"\bleave\b", r"\bquit\b", r"\bdisconnect\b",
]
SECONDARY_SIGNALS = [
r"\bskip\b", r"\bnot now\b", r"\bmaybe later\b", r"\blater\b",
r"\bback\b", r"\bdismiss\b", r"\bclose\b",
r"\bdetails\b", r"\bmore info\b", r"\bhelp\b", r"\babout\b",
r"\bno thanks\b", r"\bnevermind\b",
r"\bcancel$", # plain "cancel" (no lost work) = secondary, not destructive
]
def classify_button_style(text: str, callback_data: str = "", context_hint: str = "") -> str | None:
"""
Automatically determine the button style based on its text and context.
Returns:
"destructive" — red button (irreversible / high-stakes)
"secondary" — gray button (low-priority / dismiss)
None — default blue button (primary / recommended)
context_hint: optional extra context like "this action costs money"
"""
combined = f"{text} {callback_data} {context_hint}".lower()
# Check destructive first (higher priority)
for pattern in DESTRUCTIVE_SIGNALS:
if re.search(pattern, combined):
return "destructive"
# Then secondary
for pattern in SECONDARY_SIGNALS:
if re.search(pattern, combined):
return "secondary"
# Default = primary (blue)
return None
def build_choice_buttons(choices: list[dict]) -> list[list[dict]]:
"""
Takes a list of raw choices and returns Bot API inline_keyboard rows
with styles automatically assigned.
Each choice dict:
text (str): Button label (required)
data (str): callback_data (required unless url is set)
url (str): URL button (optional, mutually exclusive with data)
style (str|None): Override style — if set, skip auto-classification
context (str): Extra hint for classifier (e.g. "costs money")
emoji_id (str): Custom emoji ID (optional)
row (int): Force button into a specific row (optional)
Returns list of rows suitable for inline_keyboard.
"""
# Group by row
row_map: dict[int, list[dict]] = {}
auto_row = 0
for i, choice in enumerate(choices):
btn: dict = {"text": choice["text"]}
# Action
if "url" in choice:
btn["url"] = choice["url"]
else:
btn["callback_data"] = choice.get("data", choice["text"].lower().replace(" ", "_"))
# Style — use override if provided, else auto-classify
if "style" in choice and choice["style"] is not None:
btn["style"] = choice["style"]
else:
auto_style = classify_button_style(
choice["text"],
choice.get("data", ""),
choice.get("context", ""),
)
if auto_style:
btn["style"] = auto_style
# Custom emoji
if "emoji_id" in choice:
btn["icon_custom_emoji_id"] = choice["emoji_id"]
# Row assignment
target_row = choice.get("row", auto_row)
row_map.setdefault(target_row, []).append(btn)
# Auto-advance row every 2 buttons
if len(row_map.get(auto_row, [])) >= 2:
auto_row += 1
return [row_map[k] for k in sorted(row_map.keys())]
import requests
def send_choices(bot_token, chat_id, text, choices, parse_mode="HTML"):
"""Send a message with auto-colored choice buttons."""
keyboard = build_choice_buttons(choices)
payload = {
"chat_id": chat_id,
"text": text,
"parse_mode": parse_mode,
"reply_markup": {"inline_keyboard": keyboard},
}
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
resp = requests.post(url, json=payload)
resp.raise_for_status()
return resp.json()
# The bot just passes raw choices — colors are assigned automatically:
send_choices(TOKEN, chat_id, "Approve this document?", [
{"text": "✅ Approve", "data": "approve"},
{"text": "❌ Reject", "data": "reject"}, # auto → destructive (red)
{"text": "⏭ Review later", "data": "later"}, # auto → secondary (gray)
])
If the library version does not yet expose
style, pass it viaapi_kwargs.
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
def auto_button(text, callback_data, context=""):
style = classify_button_style(text, callback_data, context)
kwargs = {"style": style} if style else {}
return InlineKeyboardButton(text, callback_data=callback_data, api_kwargs=kwargs)
keyboard = InlineKeyboardMarkup([
[auto_button("Approve", "approve"),
auto_button("Reject", "reject")],
[auto_button("Skip", "skip")],
])
await update.message.reply_text("Pick an option:", reply_markup=keyboard)
{
"chat_id": "<CHAT_ID>",
"text": "Delete your data?",
"reply_markup": {
"keyboard": [
[
{"text": "Keep my data"},
{"text": "Delete everything", "style": "destructive"}
]
],
"resize_keyboard": true,
"one_time_keyboard": true
}
}
Can be combined with style on the same button:
{"text": "Boost", "callback_data": "boost", "style": "destructive", "icon_custom_emoji_id": "5368324170671202286"}
Quick test with colored buttons:
./SKILL.sh <BOT_TOKEN> <CHAT_ID>