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

1

Create Your Plugin File

Start with our plugin template or create a new Python file in the plugins directory.

2

Define Plugin Metadata

Set the required fields: NAME, VERSION, DESCRIPTION, CREDITS, UUID, and SETTINGS_PAGE.

3

Implement Plugin Logic

Add event handlers, Telegram commands, and your custom functionality.

4

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:

1 Send /plugins command to your bot
2 Click "Upload Plugin" button
3 Send your .py file as document
4 Confirm installation

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.

Complete Plugin Template
# ================================
# 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
Human-readable name of your plugin. Should be descriptive and unique.
Example: "Auto Reply Manager", "Order Statistics"
VERSION: str
Semantic version following the format MAJOR.MINOR.PATCH.
Example: "1.0.0", "2.1.3", "0.5.0-beta"
DESCRIPTION: str
Brief description of plugin functionality and purpose.
Example: "Automatically replies to customer messages with predefined responses"
CREDITS: str
Author information including username or contact details.
Example: "@developer_username", "John Doe (@john_dev)"
UUID: str
Unique identifier for your plugin. Must be a valid UUID v4 format.
Format: "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
SETTINGS_PAGE: bool
Whether the plugin provides a web-based settings interface.
Values: True (has settings page), False (no settings page)
BIND_TO_DELETE: List[str]
List of files/directories to delete when plugin is removed.
Example: ["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_DELETE are removed

Plugin System Architecture

FunPayCardinal использует мощную систему плагинов, основанную на классе PluginData и процессе динамической загрузки. Понимание архитектуры системы поможет вам создавать более эффективные и надежные плагины.

Структура PluginData

class PluginData
Основной класс, описывающий плагин в системе Cardinal. Каждый загруженный плагин представлен экземпляром этого класса.
Структура 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/
Cardinal сканирует папку plugins/ и находит все файлы с расширением .py.
Проверки:
  • Файл должен иметь расширение .py
  • Первая строка не должна содержать #noplug
  • Файл должен быть доступен для чтения
2. Динамический импорт модуля
Каждый плагин импортируется как отдельный Python модуль с использованием 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
UUID плагина проверяется на соответствие формату UUID v4 и уникальность.
Валидация 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
# Создание экземпляра 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()
После загрузки плагина Cardinal регистрирует все обработчики событий из плагина.
Регистрация обработчиков
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 и сетевых запросов
Обработка ошибок при работе с внешними API и сетевыми запросами.
Надежные 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)
FunPayCardinal uses UUID version 4, which is based on random or pseudo-random numbers.
Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
Example: f47ac10b-58cc-4372-a567-0e02b2c3d479
UUID Generation in Python
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
Understanding the components of a UUID4:
Components:
  • xxxxxxxx - 32 bits: Time-low field (random)
  • xxxx - 16 bits: Time-mid field (random)
  • 4xxx - 16 bits: Version (4) + time-high field
  • yxxx - 16 bits: Variant bits + clock sequence
  • xxxxxxxxxxxx - 48 bits: Node field (random)
UUID Validation Function
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
Triggered when a new message is received in any chat.
Event Properties:
  • event.message.id - Message ID
  • event.message.text - Message text content
  • event.message.author - Message author username
  • event.message.chat_id - Chat ID where message was sent
  • event.message.by_bot - Whether message was sent by bot
  • event.message.html - HTML-formatted message content
NewMessageEvent Handler Example
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
Triggered when a new order is created on FunPay.
Event Properties:
  • event.order.id - Order ID
  • event.order.buyer_username - Buyer's username
  • event.order.sum - Order amount in rubles
  • event.order.description - Order description
  • event.order.status - Current order status
NewOrderEvent Handler Example
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
Triggered when an order is confirmed by the buyer.
Event Properties:
  • event.order - Same properties as NewOrderEvent
OrderCompletedEvent
Triggered when an order is completed successfully.
Use Cases: Update sales statistics, send completion notifications, trigger post-sale actions
ReviewEvent
Triggered when a new review is received.
Event Properties:
  • event.review.id - Review ID
  • event.review.author - Review author
  • event.review.text - Review text
  • event.review.rating - Star rating (1-5)
  • event.review.order_id - Associated order ID
Multiple Event Handlers Example
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_bot to 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")
Send a notification message to the configured admin chat.
Parameters:
  • text - Message text (supports HTML/Markdown formatting)
  • parse_mode - Formatting mode: "HTML", "Markdown", or None
Notification Examples
# 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)
Send a message to any Telegram chat (requires chat_id).
Common Parameters:
  • chat_id - Target chat ID
  • text - Message text
  • reply_markup - Inline keyboard markup
  • parse_mode - Text formatting mode
  • disable_notification - Send silently
cardinal.telegram.bot.reply_to(message, text: str, **kwargs)
Reply to a specific Telegram message.
Use Cases: Command responses, interactive conversations, confirmation messages

Custom Commands

Telegram Command Registration
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

Inline Keyboards and Callbacks
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)
Send a message to a specific chat
cardinal.add_handlers(handlers: dict)
Register event handlers for your plugin
cardinal.add_telegram_commands(commands: dict)
Add Telegram bot commands

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

Documentation

Comprehensive documentation and guides for all aspects of FunPayCardinal.

View Docs

Plugin Marketplace

Browse and download plugins created by the community.

Browse Plugins