maxbot-chatbot-python 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ from maxbot_chatbot_python.bot import Bot
2
+ from maxbot_chatbot_python.state import MapStateManager, Scene, State
3
+ from maxbot_chatbot_python.notification import Notification
4
+ from maxbot_chatbot_python.router import Router
5
+
6
+ __all__ = [
7
+ "Bot",
8
+ "MapStateManager",
9
+ "Scene",
10
+ "State",
11
+ "Notification",
12
+ "Router"
13
+ ]
@@ -0,0 +1,42 @@
1
+ import asyncio
2
+ from maxbot_chatbot_python.router import Router
3
+ from maxbot_chatbot_python.notification import Notification
4
+
5
+ class Bot:
6
+ def __init__(self, api_client):
7
+ self.api = api_client
8
+ self.router = Router()
9
+ self.state_manager = None
10
+ self.marker = 0
11
+
12
+ async def start_polling(self):
13
+ print("Bot is running. Start polling...")
14
+
15
+ while True:
16
+ try:
17
+ resp = await self.api.subscriptions.GetUpdatesAsync(
18
+ marker=self.marker,
19
+ timeout=25
20
+ )
21
+
22
+ if getattr(resp, 'marker', 0) != 0:
23
+ self.marker = resp.marker
24
+
25
+ updates = getattr(resp, 'updates', [])
26
+ for update in updates:
27
+ asyncio.create_task(self.process_update(update))
28
+
29
+ except asyncio.CancelledError:
30
+ print("Stop polling...")
31
+ break
32
+ except Exception as e:
33
+ print(f"Error receiving updates: {e}")
34
+ await asyncio.sleep(2)
35
+
36
+ async def process_update(self, update):
37
+ notif = Notification(
38
+ update=update,
39
+ bot_api=self.api,
40
+ state_manager=self.state_manager
41
+ )
42
+ await self.router.publish(notif)
@@ -0,0 +1,214 @@
1
+ import asyncio, logging
2
+
3
+ from maxbot_api_client_python import utils
4
+ from maxbot_api_client_python.types.models import Attachment, KeyboardButton
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class Notification:
9
+ def __init__(self, update, bot_api, state_manager=None):
10
+ self.update = update
11
+ self.bot_api = bot_api
12
+ self.state_manager = state_manager
13
+ self.state_id = "global"
14
+ self.create_state_id()
15
+
16
+ async def send(self, **kwargs):
17
+ """
18
+ Internal method to send a constructed message request to the chat and log it.
19
+ """
20
+ if "chat_id" not in kwargs or kwargs["chat_id"] == 0:
21
+ kwargs["chat_id"] = self.chat_id()
22
+
23
+ try:
24
+ await self.bot_api.messages.SendMessageAsync(**kwargs)
25
+ logger.info(f"Reply sent to {kwargs['chat_id']}")
26
+ except Exception as e:
27
+ logger.error(f"Sending reply error: {e}, target_id: {kwargs['chat_id']}")
28
+ raise
29
+
30
+ def type(self) -> str:
31
+ """Returns the type of the incoming update."""
32
+ if hasattr(self.update, 'update_type'):
33
+ return self.update.update_type
34
+ elif isinstance(self.update, dict):
35
+ return self.update.get('update_type')
36
+ return "unknown"
37
+
38
+ @property
39
+ def _is_message(self) -> bool:
40
+ return self.type() in ("message_created", "message_edited") and self.update.message is not None
41
+
42
+ @property
43
+ def _is_callback(self) -> bool:
44
+ return self.type() == "message_callback" and self.update.callback is not None
45
+
46
+ def text(self) -> str:
47
+ if self._is_message and self.update.message.body:
48
+ return self.update.message.body.text
49
+ if self._is_callback:
50
+ return self.update.callback.payload
51
+ raise ValueError(f"Text is not applicable or missing for type: {self.type()}")
52
+
53
+ def sender_name(self) -> str:
54
+ if self._is_message and self.update.message.sender:
55
+ return self.update.message.sender.first_name
56
+ if self._is_callback and self.update.callback.user:
57
+ return self.update.callback.user.first_name
58
+ raise ValueError(f"Sender name not found for type: {self.type()}")
59
+
60
+ def sender_id(self) -> int:
61
+ if self._is_message and self.update.message.sender:
62
+ return self.update.message.sender.user_id
63
+ if self._is_callback and self.update.callback.user:
64
+ return self.update.callback.user.user_id
65
+ raise ValueError(f"Sender ID not found for type: {self.type()}")
66
+
67
+ def chat_id(self) -> int:
68
+ if self._is_message:
69
+ chat_id = getattr(self.update.message.recipient, 'chat_id', 0) if self.update.message.recipient else 0
70
+ return chat_id if chat_id != 0 else getattr(self.update.message.sender, 'user_id', 0)
71
+
72
+ if self._is_callback:
73
+ if getattr(self.update, 'chat_id', 0):
74
+ return self.update.chat_id
75
+ if getattr(self.update.callback, 'chat_id', 0):
76
+ return getattr(self.update.callback, 'chat_id')
77
+ if getattr(self.update, 'message', None) and getattr(self.update.message.recipient, 'chat_id', 0):
78
+ return self.update.message.recipient.chat_id
79
+ if self.update.callback.user:
80
+ return self.update.callback.user.user_id
81
+
82
+ raise ValueError(f"Chat ID not found for type: {self.type()}")
83
+
84
+ async def reply(self, text: str, format_type: str | None = "markdown"):
85
+ await self.send(
86
+ chat_id=0,
87
+ text=text,
88
+ format=format_type if format_type else None,
89
+ notify=True
90
+ )
91
+
92
+ async def reply_with_media(self, text: str, format_type: str | None, file_source: str, keyboard: list[list[KeyboardButton]] | None = None):
93
+ target_id = self.chat_id()
94
+ kwargs = {
95
+ "chat_id": target_id,
96
+ "text": text,
97
+ "format": format_type if format_type else None,
98
+ "file_source": file_source,
99
+ "notify": True
100
+ }
101
+
102
+ if keyboard:
103
+ kwargs["attachments"] = [utils.attach_keyboard(keyboard)]
104
+
105
+ for i in range(5):
106
+ try:
107
+ await self.bot_api.helpers.SendFileAsync(**kwargs)
108
+ logger.info(f"Media reply sent successfully to {target_id}")
109
+ return
110
+ except Exception as e:
111
+ err_str = str(e)
112
+ if "not.ready" in err_str or "not.found" in err_str:
113
+ logger.info(f"File is processing, attempt {i+1}/5")
114
+ await asyncio.sleep(3)
115
+ continue
116
+ logger.error(f"Sending media reply error: {e}")
117
+ raise e
118
+
119
+ async def reply_with_contact(self, name: str, phone: str, contact_id: int | None = None):
120
+ await self.send(
121
+ chat_id=0,
122
+ attachments=[utils.attach_contact(name, phone, contact_id)],
123
+ notify=True
124
+ )
125
+
126
+ async def reply_with_location(self, lat: float, lon: float):
127
+ await self.send(
128
+ chat_id=0,
129
+ attachments=[utils.attach_location(lat, lon)],
130
+ notify=True
131
+ )
132
+
133
+ async def reply_with_keyboard(self, text: str, format_type: str | None, buttons: list[list[KeyboardButton]]):
134
+ await self.send(
135
+ chat_id=0,
136
+ text=text,
137
+ format=format_type if format_type else None,
138
+ attachments=[utils.attach_keyboard(buttons)],
139
+ notify=True
140
+ )
141
+
142
+ async def reply_with_sticker(self, url: str, code: str):
143
+ await self.send(
144
+ chat_id=0,
145
+ attachments=[utils.attach_sticker(url, code)],
146
+ notify=True
147
+ )
148
+
149
+ async def reply_with_share(self, text: str, url: str, title: str, desc: str):
150
+ await self.send(
151
+ chat_id=0,
152
+ text=text,
153
+ attachments=[utils.attach_share(url, title, desc)],
154
+ notify=True
155
+ )
156
+
157
+ async def reply_with_attachments(self, text: str, format_type: str | None, attachments: list[Attachment]):
158
+ await self.send(
159
+ chat_id=0,
160
+ text=text,
161
+ format=format_type if format_type else None,
162
+ attachments=attachments,
163
+ notify=True
164
+ )
165
+
166
+ async def answer_callback(self, text: str = ""):
167
+ if not self._is_callback:
168
+ raise ValueError("cannot answer callback: update is not a callback")
169
+
170
+ try:
171
+ await self.bot_api.messages.AnswerCallbackAsync(
172
+ callback_id=self.update.callback.callback_id,
173
+ notification=text if text else " "
174
+ )
175
+ except Exception as e:
176
+ logger.error(f"AnswerCallback error: {e}")
177
+ raise
178
+
179
+ async def show_action(self, action: str):
180
+ try:
181
+ chat_id = self.chat_id()
182
+ if chat_id == 0:
183
+ raise ValueError("missing chat ID")
184
+
185
+ res = await self.bot_api.chats.SendActionAsync(
186
+ chat_id=chat_id,
187
+ action=action
188
+ )
189
+
190
+ if res and not getattr(res, 'success', True):
191
+ logger.warning(f"API rejected the action {action}: {res}")
192
+ else:
193
+ logger.info(f"Action {action} sent successfully to {chat_id}")
194
+
195
+ except Exception as e:
196
+ logger.error(f"Failed to send action {action} due to API error {e}")
197
+ raise
198
+
199
+ def create_state_id(self):
200
+ try:
201
+ chat_id = self.chat_id()
202
+ if chat_id:
203
+ self.state_id = f"chat_{chat_id}"
204
+ except ValueError:
205
+ self.state_id = "global"
206
+
207
+ def activate_next_scene(self, scene):
208
+ if self.state_manager:
209
+ self.state_manager.activate_next_scene(self.state_id, scene)
210
+
211
+ def get_current_scene(self):
212
+ if self.state_manager:
213
+ return self.state_manager.get_current_scene(self.state_id)
214
+ return None
@@ -0,0 +1,105 @@
1
+ class Router:
2
+ def __init__(self):
3
+ self.handlers = {}
4
+ self.commands = {}
5
+ self.callbacks = {}
6
+
7
+ def command(self, cmd: str):
8
+ """
9
+ Decorator to register a handler for a specific text command (e.g., /start).
10
+
11
+ Example:
12
+ @router.command("/help")
13
+ async def handle_help(notification):
14
+ await notification.reply("Commands list...")
15
+ """
16
+ def decorator(func):
17
+ self.commands[cmd] = func
18
+ return func
19
+ return decorator
20
+
21
+ def callback(self, payload: str):
22
+ """
23
+ Decorator to register a handler for a specific callback payload from buttons.
24
+
25
+ Example:
26
+ @router.callback("settings_press")
27
+ async def handle_settings(notification):
28
+ await notification.answer_callback("Opening settings...")
29
+ """
30
+ def decorator(func):
31
+ self.callbacks[payload] = func
32
+ return func
33
+ return decorator
34
+
35
+ def register(self, update_type: str):
36
+ """
37
+ Decorator to register a generic handler for a specific update type.
38
+
39
+ Example:
40
+ @router.register("message_created")
41
+ async def on_message(notification):
42
+ print("Process message")
43
+ """
44
+ def decorator(func):
45
+ if update_type not in self.handlers:
46
+ self.handlers[update_type] = []
47
+ self.handlers[update_type].append(func)
48
+ return func
49
+ return decorator
50
+
51
+ async def publish(self, notification):
52
+ """
53
+ Processes an incoming notification and routes it to the first matching handler.
54
+ Order: Commands -> Callbacks -> Update Type Handlers.
55
+ """
56
+ u_type = notification.type()
57
+ try:
58
+ chat_id = notification.chat_id()
59
+ sender_id = notification.sender_id()
60
+ except ValueError:
61
+ chat_id, sender_id = 0, 0
62
+
63
+ match u_type:
64
+ case "message_created":
65
+ try:
66
+ text = notification.text()
67
+ if text and text.startswith("/"):
68
+ cmd = text.split(" ", 1)[0]
69
+ print(f"Received new command | chat_id: {chat_id}, user_id: {sender_id}, command: {cmd}")
70
+ if cmd in self.commands:
71
+ await self.commands[cmd](notification)
72
+ return
73
+ else:
74
+ print(f"Received new message | chat_id: {chat_id}, user_id: {sender_id}, text: {text}")
75
+ except Exception:
76
+ pass
77
+
78
+ case "message_callback":
79
+ try:
80
+ payload = notification.text()
81
+ print(f"Received new callback | chat_id: {chat_id}, user_id: {sender_id}, callback: {payload}")
82
+ if payload in self.callbacks:
83
+ await self.callbacks[payload](notification)
84
+ return
85
+ except Exception:
86
+ pass
87
+
88
+ case "bot_added" | "bot_started" | "bot_stopped":
89
+ print(f"Bot status updated | type: {u_type}, chat_id: {chat_id}")
90
+
91
+ case "user_added" | "user_removed":
92
+ print(f"User membership changed | type: {u_type}, chat_id: {chat_id}")
93
+
94
+ case "dialog_muted" | "dialog_unmuted" | "dialog_cleared" | "dialog_removed":
95
+ print(f"Dialog state updated | type: {u_type}, chat_id: {chat_id}")
96
+
97
+ case "message_edited" | "message_removed":
98
+ print(f"Message modified | type: {u_type}, chat_id: {chat_id}")
99
+
100
+ case "chat_title_changed":
101
+ print(f"Chat settings modified | type: {u_type}, chat_id: {chat_id}")
102
+
103
+ if u_type in self.handlers:
104
+ for func in self.handlers[u_type]:
105
+ await func(notification)
@@ -0,0 +1,65 @@
1
+ from typing import Any
2
+
3
+ class Scene:
4
+ async def start(self, app):
5
+ pass
6
+
7
+ class State:
8
+ def get_data(self) -> dict[str, Any]: pass
9
+ def set_data(self, data: dict[str, Any]): pass
10
+ def update_data(self, data: dict[str, Any]): pass
11
+ def get_scene(self) -> Scene | None: pass
12
+ def set_scene(self, scene: Scene): pass
13
+
14
+ class MapState(State):
15
+ def __init__(self, data: dict[str, Any], scene: Scene | None):
16
+ self.data = data.copy()
17
+ self.scene = scene
18
+
19
+ def get_data(self) -> dict[str, Any]: return self.data
20
+ def set_data(self, data: dict[str, Any]): self.data = data
21
+ def update_data(self, data: dict[str, Any]): self.data.update(data)
22
+ def get_scene(self) -> Scene | None: return self.scene
23
+ def set_scene(self, scene: Scene): self.scene = scene
24
+
25
+ class MapStateManager:
26
+ def __init__(self, init_data: dict[str, Any]):
27
+ self.states: dict[str, State] = {}
28
+ self.init_data = init_data
29
+ self.start_scene: Scene | None = None
30
+
31
+ def get_start_scene(self) -> Scene | None:
32
+ return self.start_scene
33
+
34
+ def set_start_scene(self, start_scene: Scene):
35
+ self.start_scene = start_scene
36
+
37
+ def get(self, state_id: str) -> State | None:
38
+ return self.states.get(state_id)
39
+
40
+ def create(self, state_id: str) -> State:
41
+ self.states[state_id] = MapState(self.init_data, self.start_scene)
42
+ return self.states[state_id]
43
+
44
+ def delete(self, state_id: str):
45
+ self.states.pop(state_id, None)
46
+
47
+ def get_state_data(self, state_id: str) -> dict[str, Any] | None:
48
+ state = self.get(state_id)
49
+ return state.get_data() if state else None
50
+
51
+ def set_state_data(self, state_id: str, new_state_data: dict[str, Any]):
52
+ state = self.get(state_id)
53
+ if state: state.set_data(new_state_data)
54
+
55
+ def update_state_data(self, state_id: str, new_state_data: dict[str, Any]):
56
+ state = self.get(state_id)
57
+ if state: state.update_data(new_state_data)
58
+
59
+ def activate_next_scene(self, state_id: str, scene: Scene):
60
+ state = self.get(state_id)
61
+ if state: state.set_scene(scene)
62
+
63
+ def get_current_scene(self, state_id: str) -> Scene | None:
64
+ state = self.get(state_id)
65
+ return state.get_scene() if state else None
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: maxbot-chatbot-python
3
+ Version: 1.1.0
4
+ Summary: Python SDK and Chatbot Framework for MAX API
5
+ Home-page: https://github.com/green-api/maxbot-chatbot-python
6
+ Author: Green-API
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: maxbot-api-client-python>=1.1.0
11
+ Requires-Dist: httpx>=0.24.0
12
+ Requires-Dist: pydantic>=2.0.0
13
+ Dynamic: author
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: license-file
18
+ Dynamic: requires-dist
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # maxbot-chatbot-python
23
+
24
+ `maxbot-chatbot-python` — это асинхронный фреймворк для создания масштабируемых ботов для **MAX BOT API** на языке **Python**.
25
+
26
+ Построенная на основе [`maxbot_api_client_python`](https://github.com/green-api/maxbot-api-client-python), эта библиотека предоставляет чистый маршрутизатор, автоматическое получение обновлений (*Long Polling*) и надежный менеджер состояний (*FSM*) для построения многошаговых диалоговых сценариев.
27
+
28
+ Для использования библиотеки требуется получить токен бота в консоли разработчика **MAX API**.
29
+ Ознакомиться с инструкцией можно [по ссылке](https://green-api.com/max-bot-api/docs/before-start/).
30
+
31
+ ## API
32
+
33
+ Документацию по **REST API MAX** можно найти по ссылке [dev.max.ru/docs-api](https://dev.max.ru/docs-api). Библиотека является оберткой для REST API, поэтому документация по указанной выше ссылке также применима к используемым здесь моделям.
34
+
35
+ Документацию по **MAX BOT API** можно найти по ссылке [green-api.com/max-bot-api/docs](https://green-api.com/max-bot-api/docs/).
36
+
37
+ ## Поддержка
38
+
39
+ [![Support](https://img.shields.io/badge/support@green--api.com-D14836?style=for-the-badge&logo=gmail&logoColor=white)](mailto:support@green-api.com)
40
+ [![Support](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/greenapi_support_ru_bot)
41
+ [![Support](https://img.shields.io/badge/WhatsApp-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://wa.me/77780739095)
42
+
43
+ ## Руководства и новости
44
+
45
+ [![Guides](https://img.shields.io/badge/YouTube-%23FF0000.svg?style=for-the-badge&logo=YouTube&logoColor=white)](https://www.youtube.com/@green-api)
46
+ [![News](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/green_api)
47
+ [![News](https://img.shields.io/badge/WhatsApp-25D366?style=for-the-badge&logo=whatsapp&logoColor=white)](https://whatsapp.com/channel/0029VaHUM5TBA1f7cG29nO1C)
48
+
49
+ ## Установка
50
+
51
+ **Убедитесь, что у вас установлен Python версии 3.12 или выше.**
52
+
53
+ ```bash
54
+ python --version
55
+ ```
56
+
57
+ **Установите библиотеку:**
58
+
59
+ ```bash
60
+ pip install maxbot-chatbot-python
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Использование и примеры
66
+
67
+ **Параметры конфигурации:**
68
+
69
+ - `base_url` - Базовый URL-адрес серверов платформы MaxBot. Все методы API будут маршрутизироваться по этому корневому адресу. Актуальный адрес указан в [официальной документации](https://dev.max.ru/docs-api).
70
+ - `token` - Уникальный секретный ключ авторизации (API-ключ) вашего бота. Получить его можно в личном кабинете после [регистрации или создании бота](https://green-api.com/max-bot-api/docs/before-start/) на платформе [business.max.ru](https://business.max.ru/).
71
+ - `ratelimiter` - Встроенный ограничитель частоты запросов. Он контролирует количество исходящих запросов в секунду (RPS), защищая бота от блокировки со стороны сервера за превышение лимитов. Рекомендуемое значение — не менее 25.
72
+ - `timeout` - Максимальное время ожидания ответа от сервера (в секундах). Если сервер не ответит в течение этого времени, запрос будет завершен с ошибкой. Оптимальное значение — 30 секунд.
73
+
74
+ ### Инициализация бота
75
+
76
+ Использование асинхронного контекстного менеджера (`async with API(...)`) гарантирует безопасное закрытие сетевых соединений при остановке бота.
77
+
78
+ ```python
79
+ import asyncio
80
+
81
+ from maxbot_api_client_python import API, Config
82
+ from maxbot_chatbot_python import Bot, MapStateManager
83
+
84
+ async def main():
85
+ cfg = Config(
86
+ base_url="https://platform-api.max.ru/",
87
+ token="YOUR_BOT_TOKEN",
88
+ ratelimiter=25,
89
+ timeout=35
90
+ )
91
+
92
+ async with API(cfg) as api_client:
93
+ bot = Bot(api_client)
94
+ bot.state_manager = MapStateManager(init_data={})
95
+
96
+ polling_task = asyncio.create_task(bot.start_polling())
97
+
98
+ try:
99
+ await polling_task
100
+ except asyncio.CancelledError:
101
+ pass
102
+
103
+ if __name__ == "__main__":
104
+ try:
105
+ asyncio.run(main())
106
+ except KeyboardInterrupt:
107
+ print("Bot stopped by user")
108
+ ```
109
+
110
+ ### Маршрутизация команд, сообщений и коллбэков
111
+
112
+ Встроенный **маршрутизатор** (`Router`) позволяет легко обрабатывать конкретные команды (начинающиеся со слэша `/`) и нажатия на **inline-кнопки** (коллбэки).
113
+
114
+ ```python
115
+ @bot.router.command("/start")
116
+ async def start_command(notification):
117
+ await notification.reply("Hello! Welcome to the MAX Bot.")
118
+
119
+ @bot.router.register("message_created")
120
+ async def ping_handler(notification):
121
+ try:
122
+ if notification.text() == "ping":
123
+ await notification.reply("pong")
124
+ except ValueError:
125
+ pass
126
+
127
+ @bot.router.callback("accept_rules")
128
+ async def rules_callback(notification):
129
+ await notification.reply("*Thank you for accepting the rules!*", format_type="markdown")
130
+ await notification.answer_callback("Success!")
131
+ ```
132
+
133
+ ### Управление состояниями и Сцены (FSM)
134
+
135
+ Для сложных многошаговых диалогов (например, регистрация или анкетирование) используйте **Менеджер состояний** (`StateManager`) и **Сцены** (`Scene`).
136
+
137
+ ```python
138
+ from maxbot_chatbot_python import Scene
139
+
140
+ class RegistrationScene(Scene):
141
+ async def start(self, notification):
142
+ try:
143
+ text = notification.text()
144
+ except ValueError:
145
+ return
146
+
147
+ if text == "/start":
148
+ await notification.reply("Let's register! What is your *login*?", "markdown")
149
+ return
150
+
151
+ if len(text) >= 4:
152
+ if notification.state_manager:
153
+ notification.state_manager.update_state_data(notification.state_id, {"login": text})
154
+
155
+ await notification.reply(f"**Login** `{text}` accepted. Now enter your **password**:", "markdown")
156
+ notification.activate_next_scene(PasswordScene())
157
+ else:
158
+ await notification.reply("Login must be **at least 4 characters long**.", "markdown")
159
+
160
+ class PasswordScene(Scene):
161
+ async def start(self, notification):
162
+ try:
163
+ password = notification.text()
164
+ except ValueError:
165
+ return
166
+
167
+ state_data = notification.state_manager.get_state_data(notification.state_id)
168
+ login = state_data.get("login", "Unknown")
169
+
170
+ await notification.reply(f"Success! Profile created.\nLogin: `{login}`\nPass: `{password}`", "markdown")
171
+
172
+ notification.activate_next_scene(RegistrationScene())
173
+
174
+ @bot.router.register("message_created")
175
+ async def fsm_handler(notification):
176
+ current_scene = notification.get_current_scene()
177
+ if current_scene:
178
+ await current_scene.start(notification)
179
+ ```
180
+
181
+ ### Ответ с медиафайлами
182
+
183
+ Обертка `Notification` содержит готовые асинхронные методы для отправки файлов, геолокаций, стикеров и статусов набора текста.
184
+
185
+ ```python
186
+ @bot.router.command("/photo")
187
+ async def send_photo(notification):
188
+ await notification.show_action("sending_photo")
189
+
190
+ await notification.reply_with_media(
191
+ text="Check out this image!",
192
+ format_type="markdown",
193
+ file_source="https://storage.yandexcloud.net/sw-prod-03-test/ChatBot/corgi.jpg"
194
+ )
195
+ ```
196
+
197
+ ### Эхо-бот
198
+
199
+ ```python
200
+ import asyncio
201
+
202
+ from maxbot_api_client_python import API, Config
203
+ from maxbot_chatbot_python import Bot, MapStateManager
204
+
205
+ async def main():
206
+ cfg = Config(
207
+ base_url="https://platform-api.max.ru/",
208
+ token="YOUR_BOT_TOKEN",
209
+ ratelimiter=25
210
+ )
211
+
212
+ async with API(cfg) as api_client:
213
+ bot = Bot(api_client)
214
+ bot.state_manager = MapStateManager(init_data={})
215
+
216
+ @bot.router.register("message_created")
217
+ async def echo_handler(notification):
218
+ try:
219
+ text = notification.text()
220
+ await notification.reply(f"**Echo:** {text}", "markdown")
221
+ except Exception as e:
222
+ print(f"Error handling message: {e}")
223
+
224
+ polling_task = asyncio.create_task(bot.start_polling())
225
+
226
+ try:
227
+ await polling_task
228
+ except asyncio.CancelledError:
229
+ pass
230
+
231
+ if __name__ == "__main__":
232
+ try:
233
+ asyncio.run(main())
234
+ except KeyboardInterrupt:
235
+ print("Bot stopped by user (KeyboardInterrupt)")
236
+ ```
237
+ ```
@@ -0,0 +1,10 @@
1
+ maxbot_chatbot_python/__init__.py,sha256=dtPlQZLMPkDYDUMJ4gdi3K8tuvf9B8WKMx7UH6iY1dk,343
2
+ maxbot_chatbot_python/bot.py,sha256=iPupJZuVaZhZJdJBcMa9_NJUWE6mGBuj9yrpMVeAmyY,1341
3
+ maxbot_chatbot_python/notification.py,sha256=wc4CK3IIetB_6sidHHgZ37e73hb1xWVKqypImpGxzCM,8225
4
+ maxbot_chatbot_python/router.py,sha256=RLADmGdIZFExHI9Fl3bRr4tRgxfdJrSA1zp-Y6LXZJo,4103
5
+ maxbot_chatbot_python/state.py,sha256=7zSAvTdLxu_iu_LdapbXI5ySucGsBloUhmgGJCEYNQs,2342
6
+ maxbot_chatbot_python-1.1.0.dist-info/licenses/LICENSE,sha256=v1Mdo-KDvHFJnCaCcCPm4pR4JiovXR-v1BcS2YCvFLg,1085
7
+ maxbot_chatbot_python-1.1.0.dist-info/METADATA,sha256=Hd-Q32ieAdO_lh_-OKkd5GoYkNkt8HLRDqJCbjSn-mA,10620
8
+ maxbot_chatbot_python-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ maxbot_chatbot_python-1.1.0.dist-info/top_level.txt,sha256=MlT4FWJq3fIKUuZfOBfb4tJF51Bqcm0RrJiSutboQns,22
10
+ maxbot_chatbot_python-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GREEN-API
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ maxbot_chatbot_python