کتابخانهای سبک و پیشرفته برای ساخت کیبوردهای اینلاین و دکمههای منو در رباتهای پیامرسان Bale. این کتابخانه تمام جزئیات ساخت کیبورد را به صورت خودکار مدیریت میکند و کدنویسی شما را بسیار سادهتر میسازد.
نصب
برای نصب، کافی است از pip استفاده کنید:
pip install caler-bale
نیازمندی: پایتون ۳.۱۰ یا بالاتر
شروع سریع
یک کیبورد ساده با دو دکمه بسازید:
from caler_bale import Button, ButtonStyle, Keyboard
kb = ( Keyboard() .add_row( Button.callback("شروع", "start", style=ButtonStyle.ROCKET), Button.url_button("وبسایت", "https://example.com", style=ButtonStyle.INFO), ) )
print(kb.to_dict())
خروجی یک دیکشنری استاندارد است که مستقیماً به API ربات Bale داده میشود.
استایلهای پیشفرض
کتابخانه ۱۸ استایل رنگی دارد که هرکدام یک اموجی پیشفرض دارند:
· DEFAULT ⚪ · PRIMARY 🔵 · SUCCESS 🟢 · DANGER 🔴 · WARNING 🟡 · INFO 🟣 · STAR ⭐ · HEART 💜 · FIRE 🔥 · SHIELD 🛡️ · ROCKET 🚀 · GHOST ✨ · SOFT 🌫️ · TECH 🧠 · VIP 👑 · NEW 🆕 · HOT ⚡ · CLEAN 🎯
برای هر دکمه میتوانید استایل را انتخاب کنید یا با پارامتر icon اموجی دیگری جایگزین کنید.
دکمهها (Button)
با استفاده از متدهای کلاس میتوانید انواع دکمه را بسازید:
· Button.callback(text, callback_data, ...) دکمه کالبک · Button.url_button(text, url, ...) دکمه لینک · Button.inline_query(text, query, ...) دکمه جستجوی درونخطی · Button.action(text, action, value, ...) ساخت با نام عملکرد
پارامترهای اضافی:
· style یکی از ButtonStyle ها · icon اموجی دلخواه · badge یک متن کوتاه قبل از متن اصلی · suffix یک متن کوتاه بعد از متن اصلی · compact حالت فشرده (فقط اموجی و متن)
مثال:
btn = Button.callback("پرداخت", "pay", style=ButtonStyle.SUCCESS, badge="VIP", suffix="تومان") print(btn.render_text()) # 🟢 VIP پرداخت تومان
هر دکمه متد to_dict() برای تبدیل به دیکشنری API و to_bale() برای تبدیل به شیء Bale (در صورت نصب کتابخانه bale) دارد. همچنین میتوانید با copy, with_style, with_icon, with_badge دکمه جدیدی بر اساس دکمه قبلی بسازید.
کیبوردها (Keyboard)
شیء Keyboard مجموعهای از ردیفها و دکمهها را مدیریت میکند.
ساخت دستی
kb = Keyboard() kb.add_row(btn1, btn2) # یک ردیف اضافه میکند kb.add_button(btn3) # دکمه به ردیف آخر اضافه میشود kb.add_grid(buttons, columns=2) # دکمهها را در ستونهای مشخص میچیند kb.insert_row(0, btn4) # ردیف در مکان دلخواه درج میکند kb.remove_row(1) # ردیف را حذف میکند kb.merge(other_kb) # کیبورد دیگر را به این کیبورد اضافه میکند
الگوهای آماده
- صفحهبندی (Pager)
kb = Keyboard.pager( page=2, total_pages=5, prefix="page", prev_text="قبلی", next_text="بعدی", close_text="بستن", show_counter=True )
دکمههای قبلی، شمارنده (در صورت تمایل) و بعدی را میسازد و یک دکمه بستن هم اضافه میکند. callback_data ها به صورت خودکار مثل page:1, page:3, page:close ساخته میشوند.
- تأیید و انصراف (Confirm/Cancel)
kb = Keyboard.confirm_cancel( confirm_text="بله", cancel_text="خیر", confirm_data="confirm", cancel_data="cancel" )
- شبکههای اجتماعی (Social)
kb = Keyboard.social( telegram_url="https://t.me/...", website_url="https://...", github_url="https://github.com/...", instagram_url="https://instagram.com/..." )
فقط لینکهایی که بدهید دکمه میسازد و بقیه را نادیده میگیرد.
- انتخابگر (Selector)
kb = Keyboard.selector( title="شهر خود را انتخاب کنید", options=["تهران", "اصفهان", "شیراز"], selected="تهران", prefix="city", columns=2 )
گزینهها را در ستونهای مشخص میچیند و گزینه انتخابشده را با علامت ✅ نمایش میدهد.
- کلید روشن/خاموش (Toggle Row)
kb = Keyboard.toggle_row( label="اعلان", enabled=True, on_data="notif_on", off_data="notif_off", on_text="روشن", off_text="خاموش" )
- نوار چیپ (Chip Bar)
chips = [("ورزشی", "sport"), ("علمی", "science"), ("هنری", "art")] kb = Keyboard.chip_bar(chips, prefix="filter", columns=3, active="sport")
برچسبهای قابل کلیک میسازد و برچسب فعال را برجسته میکند.
- گرید و From Rows
kb = Keyboard.grid(buttons, columns=3) kb = Keyboard.from_rows([[btn1, btn2], [btn3]])
خروجی نهایی
با kb.to_dict() یک دیکشنری استاندارد {"inline_keyboard": [[...], ...]} میگیرید. اگر کتابخانه bale نصب باشد، متد kb.to_bale_markup() یک InlineKeyboardMarkup مستقیماً قابل استفاده تحویل میدهد.
دکمههای منو (MenuButton)
برای ساخت دکمههای غیر اینلاین مثل درخواست موقعیت یا شماره تماس:
from caler_bale import MenuButton
btn_location = MenuButton(text="ارسال موقعیت", request_location=True) btn_contact = MenuButton(text="ارسال شماره", request_contact=True)
مدیریت خطاها
کتابخانه سه کلاس خطای اختصاصی دارد:
· CalerBaleError (خطای پایه) · InvalidButtonError (خطا در ساخت دکمه) · InvalidKeyboardError (خطا در ساخت کیبورد)
این خطاها در صورت ورودی نامعتبر (متن خالی، نبود عملکرد، ردیف خالی و ...) صادر میشوند و از بروز باگ جلوگیری میکنند.
نکات تکمیلی
· کتابخانه کاملاً بدون وابستگی خارجی است و فقط از کتابخانه استاندارد پایتون استفاده میکند. · همه دکمهها و کیبوردها اعتبارسنجی میشوند؛ مثلاً یک دکمه نمیتواند هم callback_data و هم url داشته باشد. · استایلها صرفاً جنبه ظاهری دارند و در سمت کلاینت Bale نمایش داده میشوند. · برای ارسال کیبورد ساختهشده به API، کافی است خروجی to_dict() را در پارامتر reply_markup قرار دهید.
مثال کامل: ربات پیشرفته با caler_bale
در زیر یک ربات کامل پیامرسان Bale آورده شده که از تمام قابلیتهای کتابخانه استفاده میکند. این ربات فقط با کتابخانه استاندارد پایتون، بدون کتابخانه bale، نوشته شده و از caler_bale برای کیبوردها بهره میبرد.
import json import time import urllib.parse import urllib.request from typing import Any, Optional
from caler_bale import Button, ButtonStyle, Keyboard
TOKEN = "YOUR_BOT_TOKEN" BASE_URL = f"https://tapi.bale.ai/bot{TOKEN}"
def api_request(method: str, params: Optional[dict[str, Any]] = None, use_get: bool = False) -> dict[str, Any]: url = f"{BASE_URL}/{method}" if use_get: if params: query = urllib.parse.urlencode(params, doseq=True) url = f"{url}?{query}" req = urllib.request.Request(url, method="GET") else: data = json.dumps(params or {}, ensure_ascii=False).encode("utf-8") req = urllib.request.Request( url, data=data, method="POST", headers={"Content-Type": "application/json; charset=utf-8"}, ) with urllib.request.urlopen(req, timeout=70) as resp: raw = resp.read().decode("utf-8") result = json.loads(raw) if not result.get("ok", False): raise RuntimeError(f"API error in {method}: {result}") return result["result"]
def get_updates(offset: Optional[int] = None, timeout: int = 30, limit: int = 100) -> list[dict[str, Any]]: params: dict[str, Any] = {"timeout": timeout, "limit": limit} if offset is not None: params["offset"] = offset return api_request("getUpdates", params, use_get=True)
def send_message(chat_id: Any, text: str, reply_markup: Optional[dict[str, Any]] = None) -> dict[str, Any]: params: dict[str, Any] = {"chat_id": chat_id, "text": text} if reply_markup is not None: params["reply_markup"] = reply_markup return api_request("sendMessage", params, use_get=False)
def answer_callback_query(callback_query_id: str, text: str = "", show_alert: bool = False) -> dict[str, Any]: params: dict[str, Any] = {"callback_query_id": callback_query_id} if text: params["text"] = text if show_alert: params["show_alert"] = True return api_request("answerCallbackQuery", params, use_get=False)
def get_chat_id_from_update(update: dict[str, Any]) -> Optional[Any]: if "message" in update and update["message"]: return update["message"]["chat"]["id"] if "edited_message" in update and update["edited_message"]: return update["edited_message"]["chat"]["id"] if "callback_query" in update and update["callback_query"]: msg = update["callback_query"].get("message") if msg: return msg["chat"]["id"] return None
def main_menu_keyboard() -> Keyboard: return ( Keyboard() .add_row( Button.callback("فروشگاه", "shop:page:1", style=ButtonStyle.ROCKET, badge="جدید"), Button.callback("تنظیمات", "settings", style=ButtonStyle.TECH), ) .add_row( Button.callback("شبکههای اجتماعی", "social", style=ButtonStyle.VIP), Button.callback("انتخاب زبان", "lang:select", style=ButtonStyle.INFO), ) .add_row( Button.callback("پروفایل", "profile", style=ButtonStyle.PRIMARY, icon="🆔"), Button.callback("خروج", "exit", style=ButtonStyle.DANGER), ) .add_row( Button.url_button("مستندات", "https://github.com/your/caler_bale", style=ButtonStyle.GHOST), ) )
def shop_page_keyboard(page: int, total_pages: int = 3) -> Keyboard:
products = {
1: ["محصول A - ۱۰۰ تومان", "محصول B - ۲۰۰ تومان"],
2: ["محصول C - ۱۵۰ تومان", "محصول D - ۲۵۰ تومان"],
3: ["محصول E - ۳۰۰ تومان", "محصول F - ۵۰ تومان"],
}
kb = Keyboard()
for product in products.get(page, []):
kb.add_row(
Button.callback(f"🛒 {product}", f"buy:{product}", style=ButtonStyle.SUCCESS, icon="💰")
)
pager = Keyboard.pager(
page=page,
total_pages=total_pages,
prefix="shop:page",
prev_text="
def settings_keyboard(notif_enabled: bool = True) -> Keyboard:
kb = Keyboard()
kb.merge(
Keyboard.toggle_row(
label="اعلانها",
enabled=notif_enabled,
on_data="notif:off",
off_data="notif:on",
on_text="روشن",
off_text="خاموش",
)
)
kb.add_row(
Button.callback("حذف حساب", "delete_account", style=ButtonStyle.DANGER, icon="
def confirm_delete_keyboard() -> Keyboard: return Keyboard.confirm_cancel( confirm_text="بله، حذف شود", cancel_text="انصراف", confirm_data="delete:confirm", cancel_data="delete:cancel", )
def social_keyboard() -> Keyboard: return Keyboard.social( telegram_url="https://t.me/yourchannel", website_url="https://yourwebsite.com", github_url="https://github.com/your", instagram_url="https://instagram.com/your", )
def language_selector_keyboard(selected_lang: Optional[str] = None) -> Keyboard: return Keyboard.selector( title="انتخاب زبان", options=["فارسی", "English", "العربية"], selected=selected_lang, prefix="lang", columns=2, )
def chip_bar_keyboard(active_category: Optional[str] = None) -> Keyboard: chips = [ ("همه", "all"), ("الکترونیک", "electronics"), ("پوشاک", "clothing"), ("کتاب", "books"), ] return Keyboard.chip_bar(chips, prefix="filter", columns=4, active=active_category)
def profile_keyboard() -> Keyboard: kb = Keyboard() kb.add_row( Button.callback("آمار", "stats", style=ButtonStyle.STAR, icon="📈", suffix="کلیک کن"), Button.inline_query("جستجوی کاربر", "user:", current_chat=True, style=ButtonStyle.GHOST), ) kb.add_row( Button.callback("ویرایش نام", "edit_name", style=ButtonStyle.WARNING, compact=True), Button.callback("تغییر عکس", "change_photo", style=ButtonStyle.INFO, compact=True), ) kb.add_row( Button.callback("بازگشت", "menu", style=ButtonStyle.DEFAULT) ) return kb
user_state = {}
def handle_callback(update: dict[str, Any]) -> None: cq = update.get("callback_query") or {} callback_query_id = cq.get("id") if not callback_query_id: return data = cq.get("data") or "" msg = cq.get("message") or {} chat_id = msg.get("chat", {}).get("id") if not chat_id: return answer_callback_query(callback_query_id)
if data == "menu": send_message(chat_id, "منوی اصلی:", reply_markup=main_menu_keyboard().to_dict()) elif data.startswith("shop:page:"): try: page = int(data.split(":")[-1]) except ValueError: page = 1 kb = shop_page_keyboard(page) chip = chip_bar_keyboard(active_category=user_state.get(chat_id, {}).get("filter")) kb.merge(chip) send_message(chat_id, "محصولات:", reply_markup=kb.to_dict()) elif data.startswith("buy:"): product = data[len("buy:"):] answer_callback_query(callback_query_id, f"محصول «{product}» به سبد خرید افزوده شد", show_alert=True) elif data in ("notif:on", "notif:off"): new_state = data == "notif:on" user_state.setdefault(chat_id, {})["notif"] = new_state kb = settings_keyboard(notif_enabled=new_state) send_message(chat_id, f"اعلانها {'روشن' if new_state else 'خاموش'} شد.", reply_markup=kb.to_dict()) elif data == "delete_account": send_message(chat_id, "آیا مطمئنی میخواهی حساب کاربریات حذف شود؟", reply_markup=confirm_delete_keyboard().to_dict()) elif data == "delete:confirm": send_message(chat_id, "حساب کاربری با موفقیت حذف شد. خدانگهدار!", reply_markup=main_menu_keyboard().to_dict()) elif data == "delete:cancel": send_message(chat_id, "عملیات حذف لغو شد.", reply_markup=settings_keyboard().to_dict()) elif data == "settings": send_message(chat_id, "تنظیمات:", reply_markup=settings_keyboard().to_dict()) elif data == "social": send_message(chat_id, "شبکههای اجتماعی ما:", reply_markup=social_keyboard().to_dict()) elif data == "lang:select": send_message(chat_id, "زبان مورد نظر را انتخاب کن:", reply_markup=language_selector_keyboard().to_dict()) elif data.startswith("lang:"): lang = data[len("lang:"):] if lang in ("فارسی", "English", "العربية"): user_state.setdefault(chat_id, {})["lang"] = lang kb = language_selector_keyboard(selected_lang=lang) send_message(chat_id, f"زبان به «{lang}» تغییر یافت.", reply_markup=kb.to_dict()) elif data.startswith("filter:"): category = data[len("filter:"):] user_state.setdefault(chat_id, {})["filter"] = category chip = chip_bar_keyboard(active_category=category) send_message(chat_id, f"دستهبندی: {category}", reply_markup=chip.to_dict()) elif data == "profile": send_message(chat_id, "پروفایل شما:", reply_markup=profile_keyboard().to_dict()) elif data == "stats": kb = Keyboard() kb.add_row(Button.callback("بازدید: ۱۲۳۴", "noop", style=ButtonStyle.STAR, badge="VIP", compact=True)) kb.add_row(Button.callback("بازگشت", "profile", style=ButtonStyle.DEFAULT)) send_message(chat_id, "آمار حساب:", reply_markup=kb.to_dict()) elif data == "exit": send_message(chat_id, "به امید دیدار دوباره 👋", reply_markup=None) else: answer_callback_query(callback_query_id, "دکمه نامعتبر", show_alert=True)
def handle_message(update: dict[str, Any]) -> None: chat_id = get_chat_id_from_update(update) if chat_id is None: return msg = update.get("message") or update.get("edited_message") or {} text = (msg.get("text") or "").strip().lower() if text in ("/start", "start", "menu"): send_message(chat_id, "سلام 👋\nبه ربات پیشرفته caler_bale خوش آمدی.", reply_markup=main_menu_keyboard().to_dict()) else: send_message(chat_id, "متوجه نشدم. منوی اصلی:", reply_markup=main_menu_keyboard().to_dict())
def main() -> None: me = api_request("getMe", use_get=True) print(f"Logged in as: @{me.get('username', 'unknown')}") offset: Optional[int] = None while True: try: updates = get_updates(offset=offset, timeout=30, limit=100) for update in updates: update_id = update["update_id"] offset = update_id + 1 if "message" in update or "edited_message" in update: handle_message(update) elif "callback_query" in update: handle_callback(update) except KeyboardInterrupt: print("Stopped.") break except Exception as e: print(f"Error: {e}") time.sleep(2)
if name == "main": main()
مشارکت
اگر پیشنهادی دارید یا اشکالی پیدا کردید، لطفاً در گیتهاب issue یا pull request ثبت کنید.
مجوز
این پروژه تحت مجوز MIT منتشر شده است.