Introduction to FunPayCardinal Plugins
Launch Your FunPay Cardinal Bot in Minutes!
With our platform, you can effortlessly set up your own FunPay Cardinal bot and install numerous plugins from our free Plugin Marketplace. No technical difficulties - everything works seamlessly out of the box!
FunPayCardinal is a powerful automation platform for FunPay that allows you to extend its functionality through a comprehensive plugin system. This documentation will guide you through creating, installing, and managing plugins.
Plugin System
Modular architecture for easy extensibility
Event-Driven
React to FunPay events in real-time
Telegram Integration
Built-in Telegram bot commands
Quick Start Guide
Create Your Plugin File
Start with our plugin template or create a new Python file in the plugins directory.
Define Plugin Metadata
Set the required fields: NAME, VERSION, DESCRIPTION, CREDITS, UUID, and SETTINGS_PAGE.
Implement Plugin Logic
Add event handlers, Telegram commands, and your custom functionality.
Install and Test
Place your plugin in the plugins folder or upload via Telegram bot interface.
Plugin Installation Methods
There are two convenient ways to install plugins in FunPayCardinal:
Method 1: Manual Installation
Copy your plugin file directly to the plugins directory:
# Navigate to your FunPayCardinal directory
cd /path/to/FunPayCardinal
# Copy plugin to plugins folder
cp your_plugin.py plugins/
# Restart FunPayCardinal
python main.py
Advantages:
- Direct file system access
- No size limitations
- Batch installation possible
Method 2: Telegram Bot Interface
Upload plugins directly through your Telegram bot:
Advantages:
- Remote installation capability
- Automatic validation and safety checks
- Instant plugin management
- No server access required
Pro Tip
For development and testing, use manual installation. For production deployment and remote management, the Telegram bot interface is more convenient and secure.
Plugin Structure
Every FunPayCardinal plugin must follow a specific structure and include mandatory fields. This ensures compatibility with the Cardinal system and proper plugin lifecycle management.
# ================================
# MANDATORY PLUGIN METADATA
# ================================
NAME = "My Awesome Plugin"
VERSION = "1.0.0"
DESCRIPTION = "Comprehensive plugin for FunPayCardinal automation"
CREDITS = "@your_username"
UUID = "a1b2c3d4-e5f6-4789-a012-b3c4d5e6f789"
SETTINGS_PAGE = True
BIND_TO_DELETE = ["temp_files/", "cache.json"]
# ================================
# IMPORTS
# ================================
import logging
import json
from typing import Dict, List, Optional
from Cardinal import Cardinal
from FunPayAPI.types import MessageTypes
from FunPayAPI.common.events import *
# ================================
# PLUGIN CONFIGURATION
# ================================
logger = logging.getLogger("FPC.plugin.my_plugin")
# Plugin settings with defaults
PLUGIN_SETTINGS = {
"auto_reply": True,
"notification_enabled": True,
"max_retries": 3
}
# ================================
# CORE PLUGIN FUNCTIONS
# ================================
def init_plugin(cardinal: Cardinal) -> bool:
"""
Initialize the plugin when Cardinal starts.
Args:
cardinal: The Cardinal instance
Returns:
bool: True if initialization successful, False otherwise
"""
try:
# Register event handlers
cardinal.add_handlers(NewMessageEvent, handle_new_message)
cardinal.add_handlers(NewOrderEvent, handle_new_order)
# Register Telegram commands
cardinal.add_telegram_commands({
"plugin_status": cmd_plugin_status,
"plugin_config": cmd_plugin_config
})
logger.info(f"Plugin {NAME} v{VERSION} initialized successfully")
return True
except Exception as e:
logger.error(f"Failed to initialize plugin: {e}")
return False
def deinit_plugin(cardinal: Cardinal) -> bool:
"""
Cleanup when plugin is disabled or Cardinal shuts down.
Args:
cardinal: The Cardinal instance
Returns:
bool: True if cleanup successful, False otherwise
"""
try:
# Perform cleanup operations
cleanup_temp_files()
save_plugin_state()
logger.info(f"Plugin {NAME} deinitialized successfully")
return True
except Exception as e:
logger.error(f"Failed to deinitialize plugin: {e}")
return False
# ================================
# EVENT HANDLERS
# ================================
def handle_new_message(cardinal: Cardinal, event: NewMessageEvent):
"""Handle incoming messages"""
message = event.message
if not message.by_bot and PLUGIN_SETTINGS["auto_reply"]:
# Process message logic here
pass
def handle_new_order(cardinal: Cardinal, event: NewOrderEvent):
"""Handle new orders"""
order = event.order
if PLUGIN_SETTINGS["notification_enabled"]:
cardinal.telegram.send_notification(
f"💰 New order: {order.sum}₽ from {order.buyer_username}"
)
# ================================
# TELEGRAM COMMANDS
# ================================
def cmd_plugin_status(cardinal: Cardinal, message, args):
"""Show plugin status"""
status_text = f"🔌 {NAME} v{VERSION}\n✅ Status: Active"
cardinal.telegram.bot.reply_to(message, status_text)
def cmd_plugin_config(cardinal: Cardinal, message, args):
"""Show plugin configuration"""
config_text = "⚙️ Plugin Configuration:\n"
for key, value in PLUGIN_SETTINGS.items():
config_text += f"• {key}: {value}\n"
cardinal.telegram.bot.reply_to(message, config_text)
# ================================
# UTILITY FUNCTIONS
# ================================
def cleanup_temp_files():
"""Clean up temporary files"""
pass
def save_plugin_state():
"""Save plugin state to file"""
pass
Mandatory Fields Reference
NAME: str
"Auto Reply Manager", "Order Statistics"
VERSION: str
"1.0.0", "2.1.3", "0.5.0-beta"
DESCRIPTION: str
"Automatically replies to customer messages with predefined responses"
CREDITS: str
"@developer_username", "John Doe (@john_dev)"
UUID: str
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
SETTINGS_PAGE: bool
True (has settings page), False (no settings page)
BIND_TO_DELETE: List[str]
["temp/", "cache.json", "logs/plugin.log"]
Plugin Lifecycle
Cardinal manages plugins through a complete lifecycle:
- Loading: Plugin file is imported and metadata validated
- Initialization:
init_plugin()is called to set up handlers - Runtime: Event handlers process incoming events
- Deinitialization:
deinit_plugin()is called for cleanup - Unloading: Files in
BIND_TO_DELETEare removed
Plugin System Architecture
FunPayCardinal использует мощную систему плагинов, основанную на классе PluginData и процессе динамической загрузки. Понимание архитектуры системы поможет вам создавать более эффективные и надежные плагины.
Структура PluginData
class PluginData
class PluginData:
def __init__(self, name: str, version: str, desc: str,
credentials: str, uuid: str, path: str,
plugin: ModuleType, settings_page: bool,
delete_handler: Callable | None, enabled: bool):
self.name = name # Название плагина
self.version = version # Версия плагина
self.description = desc # Описание плагина
self.credits = credentials # Авторы плагина
self.uuid = uuid # UUID плагина
self.path = path # Путь до файла плагина
self.plugin = plugin # Экземпляр модуля плагина
self.settings_page = settings_page # Есть ли страница настроек
self.commands = {} # Telegram команды плагина
self.delete_handler = delete_handler # Обработчик удаления
self.enabled = enabled # Включен ли плагин
Процесс загрузки плагинов
1. Сканирование папки plugins/
plugins/ и находит все файлы с расширением .py.
- Файл должен иметь расширение
.py - Первая строка не должна содержать
#noplug - Файл должен быть доступен для чтения
2. Динамический импорт модуля
importlib.
def load_plugin(from_file: str) -> tuple:
"""
Создает модуль из файла-плагина и получает необходимые поля.
"""
# Создание спецификации модуля
spec = importlib.util.spec_from_file_location(
f"plugins.{from_file[:-3]}",
f"plugins/{from_file}"
)
# Создание модуля из спецификации
plugin = importlib.util.module_from_spec(spec)
sys.modules[f"plugins.{from_file[:-3]}"] = plugin
# Выполнение кода модуля
spec.loader.exec_module(plugin)
# Извлечение обязательных полей
fields = ["NAME", "VERSION", "DESCRIPTION", "CREDITS",
"SETTINGS_PAGE", "UUID", "BIND_TO_DELETE"]
result = {}
for field in fields:
try:
value = getattr(plugin, field)
except AttributeError:
raise FieldNotExistsError(field, from_file)
result[field] = value
return plugin, result
3. Валидация UUID
@staticmethod
def is_uuid_valid(uuid: str) -> bool:
"""
Проверяет, является ли UUID плагина валидным.
"""
try:
uuid_obj = UUID(uuid, version=4)
except ValueError:
return False
return str(uuid_obj) == uuid
# Проверка уникальности UUID
if data["UUID"] in self.plugins:
logger.error(f"UUID {data['UUID']} уже зарегистрирован для плагина {data['NAME']}")
continue
4. Создание PluginData
PluginData и добавляется в реестр плагинов.
# Создание экземпляра PluginData
plugin_data = PluginData(
name=data["NAME"],
version=data["VERSION"],
desc=data["DESCRIPTION"],
credentials=data["CREDITS"],
uuid=data["UUID"],
path=f"plugins/{file}",
plugin=plugin,
settings_page=data["SETTINGS_PAGE"],
delete_handler=data["BIND_TO_DELETE"],
enabled=False if data["UUID"] in self.disabled_plugins else True
)
# Добавление в реестр плагинов
self.plugins[data["UUID"]] = plugin_data
Регистрация обработчиков
add_handlers_from_plugin()
def add_handlers_from_plugin(self, plugin, uuid: str | None = None):
"""
Добавляет обработчики из плагина и присваивает каждому UUID плагина.
"""
# Список переменных с обработчиками
handler_vars = [
"BIND_TO_PRE_INIT", "BIND_TO_INIT", "BIND_TO_POST_INIT",
"BIND_TO_PRE_START", "BIND_TO_START", "BIND_TO_POST_START",
"BIND_TO_PRE_STOP", "BIND_TO_STOP", "BIND_TO_POST_STOP",
"BIND_TO_NEW_MESSAGE", "BIND_TO_ORDER_CONFIRMED",
"BIND_TO_ORDER_COMPLETED", "BIND_TO_REVIEW"
]
for var_name in handler_vars:
try:
functions = getattr(plugin, var_name)
except AttributeError:
continue
# Присваиваем UUID каждой функции
for func in functions:
func.plugin_uuid = uuid
# Добавляем в соответствующий список обработчиков
self.handler_bind_var_names[var_name].extend(functions)
logger.info(f"Обработчики зарегистрированы для {plugin.__name__}")
Важные особенности системы плагинов
- UUID должен быть уникальным: Дублирование UUID приведет к отказу загрузки плагина
- Обязательные поля: Отсутствие любого из обязательных полей приведет к ошибке загрузки
- Изоляция плагинов: Каждый плагин загружается в отдельном пространстве имен
- Отключение плагинов: Плагины можно отключить через файл конфигурации без удаления
Configuration Management
Эффективное управление конфигурацией является ключевым аспектом разработки плагинов. FunPayCardinal предоставляет гибкую систему настроек для плагинов.
Основные параметры конфигурации
Структура конфигурации плагина
# Настройки по умолчанию
DEFAULT_CONFIG = {
# Основные настройки
"enabled": True, # Включен ли плагин
"debug_mode": False, # Режим отладки
"log_level": "INFO", # Уровень логирования
# Настройки автоответов
"auto_reply_enabled": True, # Включены ли автоответы
"reply_delay_min": 1, # Минимальная задержка (сек)
"reply_delay_max": 5, # Максимальная задержка (сек)
# Настройки времени работы
"work_hours": {
"enabled": False, # Ограничение по времени
"start": "09:00", # Начало работы
"end": "18:00", # Конец работы
"timezone": "Europe/Moscow" # Часовой пояс
},
# Настройки уведомлений
"notifications": {
"telegram_enabled": True, # Уведомления в Telegram
"email_enabled": False, # Email уведомления
"webhook_url": None # URL для webhook
},
# Настройки производительности
"performance": {
"max_concurrent_requests": 10, # Макс. одновременных запросов
"request_timeout": 30, # Таймаут запроса (сек)
"retry_attempts": 3, # Количество повторов
"cache_ttl": 300 # Время жизни кэша (сек)
}
}
Управление конфигурацией
Загрузка и сохранение настроек
import json
import os
from threading import Lock
from typing import Dict, Any
# Файл конфигурации
CONFIG_FILE = f"plugins/{NAME.lower().replace(' ', '_')}_config.json"
config_lock = Lock()
def load_config() -> Dict[str, Any]:
"""
Загружает конфигурацию плагина из файла
"""
global config
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
loaded_config = json.load(f)
# Объединяем с настройками по умолчанию
config = {**DEFAULT_CONFIG, **loaded_config}
logger.info("Конфигурация плагина загружена")
else:
config = DEFAULT_CONFIG.copy()
save_config()
logger.info("Создана конфигурация по умолчанию")
except Exception as e:
logger.error(f"Ошибка при загрузке конфигурации: {e}")
config = DEFAULT_CONFIG.copy()
return config
def save_config():
"""
Сохраняет текущую конфигурацию в файл
"""
try:
with config_lock:
# Создаем папку plugins, если её нет
os.makedirs("plugins", exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=4)
logger.info("Конфигурация сохранена")
except Exception as e:
logger.error(f"Ошибка при сохранении конфигурации: {e}")
def get_config_value(key: str, default=None):
"""
Получает значение из конфигурации с поддержкой вложенных ключей
"""
keys = key.split('.')
value = config
try:
for k in keys:
value = value[k]
return value
except (KeyError, TypeError):
return default
def set_config_value(key: str, value: Any):
"""
Устанавливает значение в конфигурации с поддержкой вложенных ключей
"""
keys = key.split('.')
target = config
# Навигация к родительскому объекту
for k in keys[:-1]:
if k not in target:
target[k] = {}
target = target[k]
# Установка значения
target[keys[-1]] = value
save_config()
Валидация конфигурации
Проверка корректности настроек
class ConfigValidationError(Exception):
"""Ошибка валидации конфигурации"""
pass
def validate_config(config_data: Dict[str, Any]) -> bool:
"""
Валидирует конфигурацию плагина
"""
try:
# Проверка обязательных полей
required_fields = ["enabled", "debug_mode", "log_level"]
for field in required_fields:
if field not in config_data:
raise ConfigValidationError(f"Отсутствует обязательное поле: {field}")
# Проверка типов данных
if not isinstance(config_data["enabled"], bool):
raise ConfigValidationError("Поле 'enabled' должно быть boolean")
if not isinstance(config_data["debug_mode"], bool):
raise ConfigValidationError("Поле 'debug_mode' должно быть boolean")
# Проверка уровня логирования
valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
if config_data["log_level"] not in valid_log_levels:
raise ConfigValidationError(f"Недопустимый уровень логирования: {config_data['log_level']}")
# Проверка настроек времени работы
if "work_hours" in config_data:
work_hours = config_data["work_hours"]
if work_hours.get("enabled", False):
import re
time_pattern = r"^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
if not re.match(time_pattern, work_hours.get("start", "")):
raise ConfigValidationError("Некорректный формат времени начала работы")
if not re.match(time_pattern, work_hours.get("end", "")):
raise ConfigValidationError("Некорректный формат времени окончания работы")
# Проверка настроек производительности
if "performance" in config_data:
perf = config_data["performance"]
if perf.get("max_concurrent_requests", 0) <= 0:
raise ConfigValidationError("max_concurrent_requests должно быть положительным числом")
if perf.get("request_timeout", 0) <= 0:
raise ConfigValidationError("request_timeout должно быть положительным числом")
return True
except ConfigValidationError as e:
logger.error(f"Ошибка валидации конфигурации: {e}")
return False
except Exception as e:
logger.error(f"Неожиданная ошибка при валидации: {e}")
return False
Рекомендации по конфигурации
- Используйте значения по умолчанию: Всегда предоставляйте разумные значения по умолчанию
- Валидируйте входные данные: Проверяйте корректность всех настроек при загрузке
- Документируйте параметры: Добавляйте комментарии к каждому параметру конфигурации
- Используйте типизацию: Указывайте типы данных для всех параметров
- Группируйте настройки: Объединяйте связанные параметры в логические группы
Error Handling in Plugins
Правильная обработка ошибок является критически важным аспектом разработки надежных плагинов. Этот раздел покрывает типовые сценарии ошибок и лучшие практики их обработки.
Типовые сценарии ошибок
1. Ошибки загрузки плагина
import logging
import traceback
from typing import Optional
logger = logging.getLogger("FunPayCardinal.Plugin")
class PluginLoadError(Exception):
"""Базовый класс для ошибок загрузки плагина"""
pass
class ConfigurationError(PluginLoadError):
"""Ошибка конфигурации плагина"""
pass
class DependencyError(PluginLoadError):
"""Ошибка зависимостей плагина"""
pass
def safe_plugin_init():
"""
Безопасная инициализация плагина с обработкой ошибок
"""
try:
# Проверка зависимостей
check_dependencies()
# Загрузка конфигурации
config = load_config()
validate_config(config)
# Инициализация компонентов
initialize_components()
logger.info(f"Плагин {NAME} успешно инициализирован")
except ConfigurationError as e:
logger.error(f"Ошибка конфигурации плагина {NAME}: {e}")
raise
except DependencyError as e:
logger.error(f"Отсутствуют зависимости для плагина {NAME}: {e}")
raise
except Exception as e:
logger.error(f"Неожиданная ошибка при инициализации {NAME}: {e}")
logger.debug(f"Трассировка ошибки: {traceback.format_exc()}")
raise PluginLoadError(f"Не удалось инициализировать плагин: {e}")
def check_dependencies():
"""Проверка наличия необходимых зависимостей"""
required_modules = ["requests", "json", "datetime"]
for module in required_modules:
try:
__import__(module)
except ImportError:
raise DependencyError(f"Отсутствует модуль: {module}")
def initialize_components():
"""Инициализация компонентов плагина"""
try:
# Инициализация API клиентов
# Подключение к базе данных
# Настройка обработчиков
pass
except Exception as e:
raise PluginLoadError(f"Ошибка инициализации компонентов: {e}")
2. Ошибки обработки сообщений
import asyncio
from functools import wraps
def safe_message_handler(func):
"""
Декоратор для безопасной обработки сообщений
"""
@wraps(func)
async def wrapper(cardinal, event, *args, **kwargs):
try:
return await func(cardinal, event, *args, **kwargs)
except asyncio.TimeoutError:
logger.warning(f"Таймаут при обработке сообщения в {func.__name__}")
except ConnectionError as e:
logger.error(f"Ошибка соединения в {func.__name__}: {e}")
# Попытка переподключения
await reconnect_if_needed()
except ValueError as e:
logger.error(f"Некорректные данные в {func.__name__}: {e}")
# Отправка уведомления об ошибке
await send_error_notification(event, str(e))
except Exception as e:
logger.error(f"Неожиданная ошибка в {func.__name__}: {e}")
logger.debug(f"Трассировка: {traceback.format_exc()}")
# Критическая ошибка - уведомляем администратора
await notify_admin_critical_error(func.__name__, str(e))
return wrapper
@safe_message_handler
async def handle_new_message(cardinal, event):
"""
Обработчик новых сообщений с защитой от ошибок
"""
message = event.message
# Валидация входных данных
if not message or not message.text:
raise ValueError("Получено пустое сообщение")
# Проверка длины сообщения
if len(message.text) > 4000:
logger.warning("Получено слишком длинное сообщение")
return
# Обработка сообщения
response = await process_message(message.text)
# Отправка ответа с повторными попытками
await send_response_with_retry(cardinal, message.chat_id, response)
async def send_response_with_retry(cardinal, chat_id: int, text: str, max_retries: int = 3):
"""
Отправка ответа с повторными попытками при ошибках
"""
for attempt in range(max_retries):
try:
await cardinal.telegram.bot.send_message(chat_id, text)
return
except Exception as e:
logger.warning(f"Попытка {attempt + 1} отправки сообщения неудачна: {e}")
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt) # Экспоненциальная задержка
else:
logger.error(f"Не удалось отправить сообщение после {max_retries} попыток")
raise
3. Ошибки API и сетевых запросов
import aiohttp
import asyncio
from typing import Optional, Dict, Any
class APIError(Exception):
"""Базовый класс для API ошибок"""
def __init__(self, message: str, status_code: Optional[int] = None):
super().__init__(message)
self.status_code = status_code
class RateLimitError(APIError):
"""Ошибка превышения лимита запросов"""
pass
class APIClient:
def __init__(self, base_url: str, timeout: int = 30):
self.base_url = base_url
self.timeout = aiohttp.ClientTimeout(total=timeout)
self.session: Optional[aiohttp.ClientSession] = None
async def __aenter__(self):
self.session = aiohttp.ClientSession(timeout=self.timeout)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
if self.session:
await self.session.close()
async def make_request(self, method: str, endpoint: str,
max_retries: int = 3, **kwargs) -> Dict[str, Any]:
"""
Выполняет HTTP запрос с обработкой ошибок и повторными попытками
"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
for attempt in range(max_retries):
try:
async with self.session.request(method, url, **kwargs) as response:
# Обработка различных статус кодов
if response.status == 200:
return await response.json()
elif response.status == 429:
# Rate limit - ждем и повторяем
retry_after = int(response.headers.get('Retry-After', 60))
logger.warning(f"Rate limit достигнут, ожидание {retry_after} секунд")
await asyncio.sleep(retry_after)
continue
elif response.status >= 500:
# Серверная ошибка - повторяем запрос
logger.warning(f"Серверная ошибка {response.status}, попытка {attempt + 1}")
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
continue
else:
# Клиентская ошибка - не повторяем
error_text = await response.text()
raise APIError(f"API ошибка {response.status}: {error_text}",
response.status)
except asyncio.TimeoutError:
logger.warning(f"Таймаут запроса к {url}, попытка {attempt + 1}")
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
continue
else:
raise APIError(f"Таймаут запроса после {max_retries} попыток")
except aiohttp.ClientError as e:
logger.error(f"Ошибка клиента при запросе к {url}: {e}")
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
continue
else:
raise APIError(f"Ошибка соединения: {e}")
raise APIError(f"Не удалось выполнить запрос после {max_retries} попыток")
# Пример использования
async def get_user_data(user_id: int) -> Optional[Dict[str, Any]]:
"""
Получает данные пользователя с обработкой ошибок
"""
try:
async with APIClient("https://api.example.com") as client:
data = await client.make_request("GET", f"/users/{user_id}")
return data
except APIError as e:
logger.error(f"Ошибка получения данных пользователя {user_id}: {e}")
return None
except Exception as e:
logger.error(f"Неожиданная ошибка: {e}")
return None
Система логирования ошибок
Настройка логирования
import logging
import logging.handlers
import os
from datetime import datetime
def setup_plugin_logging(plugin_name: str, log_level: str = "INFO"):
"""
Настраивает систему логирования для плагина
"""
# Создание логгера для плагина
logger = logging.getLogger(f"FunPayCardinal.Plugin.{plugin_name}")
logger.setLevel(getattr(logging, log_level.upper()))
# Очистка существующих обработчиков
logger.handlers.clear()
# Создание папки для логов
log_dir = "logs/plugins"
os.makedirs(log_dir, exist_ok=True)
# Файловый обработчик с ротацией
log_file = f"{log_dir}/{plugin_name.lower().replace(' ', '_')}.log"
file_handler = logging.handlers.RotatingFileHandler(
log_file,
maxBytes=10*1024*1024, # 10MB
backupCount=5,
encoding='utf-8'
)
# Консольный обработчик
console_handler = logging.StreamHandler()
# Форматтер для логов
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# Добавление обработчиков
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
# Инициализация логгера для плагина
logger = setup_plugin_logging(NAME, get_config_value("log_level", "INFO"))
class PluginErrorReporter:
"""
Класс для отчетности об ошибках плагина
"""
def __init__(self, plugin_name: str):
self.plugin_name = plugin_name
self.error_count = 0
self.last_errors = []
self.max_stored_errors = 50
def report_error(self, error: Exception, context: str = ""):
"""
Регистрирует ошибку и отправляет уведомления при необходимости
"""
self.error_count += 1
error_info = {
"timestamp": datetime.now().isoformat(),
"error_type": type(error).__name__,
"error_message": str(error),
"context": context,
"traceback": traceback.format_exc()
}
# Сохранение информации об ошибке
self.last_errors.append(error_info)
if len(self.last_errors) > self.max_stored_errors:
self.last_errors.pop(0)
# Логирование ошибки
logger.error(f"Ошибка в контексте '{context}': {error}")
logger.debug(f"Трассировка ошибки: {error_info['traceback']}")
# Критические ошибки требуют немедленного уведомления
if self._is_critical_error(error):
asyncio.create_task(self._send_critical_error_notification(error_info))
def _is_critical_error(self, error: Exception) -> bool:
"""Определяет, является ли ошибка критической"""
critical_errors = (
MemoryError,
SystemError,
KeyboardInterrupt,
SystemExit
)
return isinstance(error, critical_errors)
async def _send_critical_error_notification(self, error_info: Dict[str, Any]):
"""Отправляет уведомление о критической ошибке"""
try:
# Отправка уведомления администратору
message = f"🚨 Критическая ошибка в плагине {self.plugin_name}:\n"
message += f"Тип: {error_info['error_type']}\n"
message += f"Сообщение: {error_info['error_message']}\n"
message += f"Контекст: {error_info['context']}"
# Здесь должна быть логика отправки уведомления
logger.critical(f"КРИТИЧЕСКАЯ ОШИБКА: {message}")
except Exception as e:
logger.error(f"Не удалось отправить уведомление о критической ошибке: {e}")
# Глобальный репортер ошибок для плагина
error_reporter = PluginErrorReporter(NAME)
Лучшие практики обработки ошибок
- Никогда не игнорируйте ошибки: Всегда логируйте или обрабатывайте исключения
- Используйте специфичные исключения: Создавайте собственные классы исключений для разных типов ошибок
- Логируйте контекст: Включайте информацию о том, что происходило во время ошибки
- Graceful degradation: Плагин должен продолжать работать даже при частичных сбоях
- Мониторинг ошибок: Ведите статистику ошибок для выявления проблемных мест
UUID Generation Guide
Every plugin requires a unique UUID (Universally Unique Identifier) to ensure no conflicts between different plugins. Understanding the UUID generation process is essential for plugin development.
UUID Generation Process
UUID Version 4 (Random)
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
Example:
f47ac10b-58cc-4372-a567-0e02b2c3d479
import uuid
# Generate a new UUID4
plugin_uuid = str(uuid.uuid4())
print(f"Generated UUID: {plugin_uuid}")
# Example output: f47ac10b-58cc-4372-a567-0e02b2c3d479
# Use in plugin metadata
NAME = "My Awesome Plugin"
VERSION = "1.0.0"
DESCRIPTION = "A plugin that does amazing things"
CREDITS = "@myusername"
UUID = plugin_uuid # Use the generated UUID here
SETTINGS_PAGE = None
BIND_TO_DELETE = []
UUID Structure Breakdown
xxxxxxxx- 32 bits: Time-low field (random)xxxx- 16 bits: Time-mid field (random)4xxx- 16 bits: Version (4) + time-high fieldyxxx- 16 bits: Variant bits + clock sequencexxxxxxxxxxxx- 48 bits: Node field (random)
from uuid import UUID
def is_uuid_valid(uuid: str) -> bool:
"""
Validates if a UUID string is a valid UUID4 format.
This is the exact validation function used by Cardinal.
Args:
uuid (str): UUID4 string to validate
Returns:
bool: True if valid UUID4, False otherwise
"""
try:
uuid_obj = UUID(uuid, version=4)
except ValueError:
return False
return str(uuid_obj) == uuid
# Usage examples
valid_uuid = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
invalid_uuid = "not-a-uuid"
invalid_format = "f47ac10b58cc4372a5670e02b2c3d479" # Missing hyphens
print(f"'{valid_uuid}' is valid: {is_uuid_valid(valid_uuid)}") # True
print(f"'{invalid_uuid}' is valid: {is_uuid_valid(invalid_uuid)}") # False
print(f"'{invalid_format}' is valid: {is_uuid_valid(invalid_format)}") # False
Uniqueness Guarantee
UUID4 provides excellent uniqueness guarantees:
- Collision Probability: Approximately 1 in 2^122 (extremely low)
- Random Bits: 122 bits of randomness ensure global uniqueness
- No Central Authority: Can be generated independently anywhere
- Time Independence: No dependency on system clock or MAC address
Best Practices
- Never Reuse: Each plugin must have its own unique UUID
- Persistence: Keep the same UUID when updating your plugin
- Generation: Always use cryptographically secure random generators
- Validation: Validate UUIDs before using them in production
- Documentation: Document your plugin's UUID for reference
Event System
FunPayCardinal uses an event-driven architecture that allows plugins to respond to various system events. This provides a flexible and powerful way to extend Cardinal's functionality without modifying core code.
Event Registration
Events are registered during plugin initialization using cardinal.add_handlers(). Each event type can have multiple handlers from different plugins.
Available Events
NewMessageEvent
event.message.id- Message IDevent.message.text- Message text contentevent.message.author- Message author usernameevent.message.chat_id- Chat ID where message was sentevent.message.by_bot- Whether message was sent by botevent.message.html- HTML-formatted message content
def handle_new_message(cardinal: Cardinal, event: NewMessageEvent):
"""Handle incoming messages with auto-reply functionality"""
message = event.message
# Ignore messages sent by the bot itself
if message.by_bot:
return
# Auto-reply to greetings
greetings = ["hello", "hi", "привет", "здравствуйте"]
if any(greeting in message.text.lower() for greeting in greetings):
reply_text = f"Hello {message.author}! How can I help you today?"
cardinal.send_message(message.chat_id, reply_text)
# Log all customer messages
logger.info(f"Message from {message.author} in chat {message.chat_id}: {message.text}")
# Check for specific keywords
if "price" in message.text.lower():
cardinal.send_message(message.chat_id, "Please check our current prices in the lot description.")
# Register the handler
def init_plugin(cardinal: Cardinal) -> bool:
cardinal.add_handlers(NewMessageEvent, handle_new_message)
return True
NewOrderEvent
event.order.id- Order IDevent.order.buyer_username- Buyer's usernameevent.order.sum- Order amount in rublesevent.order.description- Order descriptionevent.order.status- Current order status
def handle_new_order(cardinal: Cardinal, event: NewOrderEvent):
"""Handle new orders with notifications and logging"""
order = event.order
# Send Telegram notification for high-value orders
if order.sum >= 1000:
notification_text = (
f"💰 High-Value Order Alert!\n"
f"Order ID: {order.id}\n"
f"Amount: {order.sum}₽\n"
f"Buyer: {order.buyer_username}\n"
f"Description: {order.description}"
)
cardinal.telegram.send_notification(notification_text)
# Log all orders
logger.info(f"New order {order.id}: {order.sum}₽ from {order.buyer_username}")
# Update statistics
update_order_statistics(order)
# Send welcome message to buyer
welcome_message = (
f"Thank you for your order #{order.id}! "
f"We'll process it shortly. If you have any questions, feel free to ask."
)
# Note: You would need the chat_id associated with this order
# cardinal.send_message(chat_id, welcome_message)
OrderConfirmedEvent
event.order- Same properties as NewOrderEvent
OrderCompletedEvent
ReviewEvent
event.review.id- Review IDevent.review.author- Review authorevent.review.text- Review textevent.review.rating- Star rating (1-5)event.review.order_id- Associated order ID
def handle_review(cardinal: Cardinal, event: ReviewEvent):
"""Handle new reviews with rating-based responses"""
review = event.review
if review.rating <= 3:
# Alert for low ratings
alert_text = (
f"⚠️ Low Rating Alert!\n"
f"Rating: {review.rating}⭐\n"
f"Author: {review.author}\n"
f"Review: {review.text}\n"
f"Order ID: {review.order_id}"
)
cardinal.telegram.send_notification(alert_text)
elif review.rating >= 5:
# Celebrate excellent reviews
celebration_text = (
f"🎉 Excellent Review!\n"
f"Rating: {review.rating}⭐\n"
f"Author: {review.author}\n"
f"Review: {review.text}"
)
cardinal.telegram.send_notification(celebration_text)
def handle_initial_chat(cardinal: Cardinal, event: InitialChatEvent):
"""Handle chat initialization during startup"""
chat = event.chat
logger.info(f"Initialized chat: {chat.name} (ID: {chat.id})")
# Store chat information for later use
store_chat_info(chat.id, chat.name, chat.last_message)
# Register all handlers in init_plugin
def init_plugin(cardinal: Cardinal) -> bool:
cardinal.add_handlers(NewMessageEvent, handle_new_message)
cardinal.add_handlers(NewOrderEvent, handle_new_order)
cardinal.add_handlers(OrderConfirmedEvent, handle_order_confirmed)
cardinal.add_handlers(OrderCompletedEvent, handle_order_completed)
cardinal.add_handlers(ReviewEvent, handle_review)
cardinal.add_handlers(InitialChatEvent, handle_initial_chat)
logger.info("All event handlers registered successfully")
return True
Best Practices
- Error Handling: Always wrap event handlers in try-catch blocks
- Performance: Keep handlers lightweight; use background tasks for heavy operations
- Logging: Log important events for debugging and monitoring
- State Management: Be careful with shared state between event handlers
- Bot Messages: Check
message.by_botto avoid infinite loops
Telegram Bot Integration
FunPayCardinal includes comprehensive Telegram bot integration for notifications, commands, and interactive features. This allows you to manage your FunPay operations remotely and receive real-time updates.
Bot Setup Required
Before using Telegram features, ensure your bot token is configured in Cardinal's main configuration file and the bot is properly initialized.
Notification Methods
cardinal.telegram.send_notification(text: str, parse_mode: str = "HTML")
text- Message text (supports HTML/Markdown formatting)parse_mode- Formatting mode: "HTML", "Markdown", or None
# Basic notification
cardinal.telegram.send_notification("New order received!")
# HTML formatted notification
notification_html = """
🎉 New High-Value Order!
Order Details:
• ID: #{order.id}
• Amount: {order.sum}₽
• Buyer: @{order.buyer_username}
• Status: {order.status}
"""
cardinal.telegram.send_notification(notification_html, parse_mode="HTML")
# Markdown formatted notification
notification_md = f"""
*⚠️ Low Stock Alert*
Product: `{product_name}`
Remaining: *{stock_count}* items
Action Required: Restock immediately
"""
cardinal.telegram.send_notification(notification_md, parse_mode="Markdown")
cardinal.telegram.bot.send_message(chat_id: int, text: str, **kwargs)
chat_id- Target chat IDtext- Message textreply_markup- Inline keyboard markupparse_mode- Text formatting modedisable_notification- Send silently
cardinal.telegram.bot.reply_to(message, text: str, **kwargs)
Custom Commands
def handle_stats_command(cardinal: Cardinal, message, args: list):
"""Handle /stats command to show sales statistics"""
try:
# Get statistics from your plugin's data
total_orders = get_total_orders()
total_revenue = get_total_revenue()
active_chats = get_active_chats_count()
stats_text = f"""
📊 Sales Statistics
📦 Total Orders: {total_orders}
💰 Total Revenue: {total_revenue}₽
💬 Active Chats: {active_chats}
📅 Period: Last 30 days
Updated: {datetime.now().strftime('%Y-%m-%d %H:%M')}
"""
cardinal.telegram.bot.reply_to(message, stats_text, parse_mode="HTML")
except Exception as e:
error_text = f"❌ Error retrieving statistics: {str(e)}"
cardinal.telegram.bot.reply_to(message, error_text)
def handle_orders_command(cardinal: Cardinal, message, args: list):
"""Handle /orders command to show recent orders"""
try:
# Get recent orders (limit to last 5)
recent_orders = get_recent_orders(limit=5)
if not recent_orders:
cardinal.telegram.bot.reply_to(message, "📭 No recent orders found.")
return
orders_text = "📋 Recent Orders\n\n"
for order in recent_orders:
status_emoji = "✅" if order.status == "completed" else "⏳"
orders_text += f"""
{status_emoji} Order #{order.id}
💰 Amount: {order.sum}₽
👤 Buyer: @{order.buyer_username}
📅 Date: {order.created_at.strftime('%m/%d %H:%M')}
━━━━━━━━━━━━━━━━━━━━
"""
cardinal.telegram.bot.reply_to(message, orders_text, parse_mode="HTML")
except Exception as e:
error_text = f"❌ Error retrieving orders: {str(e)}"
cardinal.telegram.bot.reply_to(message, error_text)
def handle_help_command(cardinal: Cardinal, message, args: list):
"""Handle /help command to show available commands"""
help_text = """
🤖 Available Commands
/stats - Show sales statistics
/orders - Show recent orders
/status - Check bot status
/help - Show this help message
💡 Tip: Use these commands to monitor your FunPay operations remotely!
"""
cardinal.telegram.bot.reply_to(message, help_text, parse_mode="HTML")
# Register commands in init_plugin
def init_plugin(cardinal: Cardinal) -> bool:
# Register Telegram commands
commands = {
"stats": handle_stats_command,
"orders": handle_orders_command,
"help": handle_help_command
}
cardinal.add_telegram_commands(commands)
logger.info("Telegram commands registered successfully")
return True
Interactive Features
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
def send_order_notification_with_actions(cardinal: Cardinal, order):
"""Send order notification with action buttons"""
# Create inline keyboard
keyboard = InlineKeyboardMarkup()
keyboard.row(
InlineKeyboardButton("✅ Approve", callback_data=f"approve_{order.id}"),
InlineKeyboardButton("❌ Reject", callback_data=f"reject_{order.id}")
)
keyboard.row(
InlineKeyboardButton("📋 View Details", callback_data=f"details_{order.id}")
)
notification_text = f"""
🔔 New Order Requires Action
📦 Order ID: #{order.id}
💰 Amount: {order.sum}₽
👤 Buyer: @{order.buyer_username}
📝 Description: {order.description}
Choose an action below:
"""
cardinal.telegram.bot.send_message(
chat_id=cardinal.telegram.admin_chat_id,
text=notification_text,
parse_mode="HTML",
reply_markup=keyboard
)
def handle_callback_query(cardinal: Cardinal, call):
"""Handle inline keyboard button presses"""
try:
action, order_id = call.data.split("_", 1)
if action == "approve":
# Approve order logic
approve_order(order_id)
response_text = f"✅ Order #{order_id} has been approved!"
elif action == "reject":
# Reject order logic
reject_order(order_id)
response_text = f"❌ Order #{order_id} has been rejected!"
elif action == "details":
# Show detailed order information
order = get_order_details(order_id)
response_text = f"""
📋 Order Details #{order_id}
👤 Buyer: @{order.buyer_username}
💰 Amount: {order.sum}₽
📅 Created: {order.created_at}
📝 Description: {order.description}
🏷️ Status: {order.status}
"""
# Answer the callback query
cardinal.telegram.bot.answer_callback_query(call.id)
# Edit the original message
cardinal.telegram.bot.edit_message_text(
text=response_text,
chat_id=call.message.chat.id,
message_id=call.message.message_id,
parse_mode="HTML"
)
except Exception as e:
cardinal.telegram.bot.answer_callback_query(
call.id,
text=f"Error: {str(e)}",
show_alert=True
)
Security Considerations
- Admin Verification: Always verify user permissions before executing sensitive commands
- Rate Limiting: Implement rate limiting to prevent spam
- Error Handling: Never expose sensitive information in error messages
- Input Validation: Validate all user inputs and command arguments
- Logging: Log all Telegram interactions for security auditing
API Reference
Cardinal Class
The main Cardinal instance provides access to all FunPayCardinal functionality:
cardinal.send_message(chat_id: str, text: str)
cardinal.add_handlers(handlers: dict)
cardinal.add_telegram_commands(commands: dict)
Configuration Management
# Access main configuration
main_config = cardinal.MAIN_CFG
# Access auto-response configuration
ar_config = cardinal.AR_CFG
# Save configuration changes
cardinal.save_config()
Plugin Examples
Explore our collection of example plugins to understand different implementation patterns:
Best Practices
Security
- Never log sensitive information
- Validate all user inputs
- Use secure file permissions
Performance
- Avoid blocking operations in event handlers
- Use efficient data structures
- Implement proper caching
Code Quality
- Follow PEP 8 style guidelines
- Write comprehensive docstrings
- Handle exceptions gracefully
Debugging and Testing
Logging
Use proper logging levels and structured messages for easier debugging.
import logging
logger = logging.getLogger(__name__)
def handle_event(cardinal, event):
logger.info(f"Processing event: {event.type}")
try:
# Your code here
logger.debug("Event processed successfully")
except Exception as e:
logger.error(f"Error processing event: {e}")
Testing
Test your plugin thoroughly before deployment.
- Test with different message types
- Verify error handling
- Check resource cleanup
Support and Community
Telegram Channel
Join our official Telegram channel for updates, support, and community discussions.
Join @fpc_host