diff --git a/README.md b/README.md index 469d328..f932b16 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ pip install maxapi==0.1 -Запуск бота из папки example: +Запуск бота из папки https://github.com/love-apples/maxapi/tree/main/example: python example.py ``` diff --git a/example.py b/example.py new file mode 100644 index 0000000..213f65d --- /dev/null +++ b/example.py @@ -0,0 +1,151 @@ +import asyncio +import logging + +from maxapi import Bot, Dispatcher, F +from maxapi.context import MemoryContext, State, StatesGroup +from maxapi.types import Command, MessageCreated, CallbackButton, MessageCallback, BotCommand +from maxapi.utils.inline_keyboard import InlineKeyboardBuilder + +from for_example import router + +logging.basicConfig(level=logging.INFO) + +bot = Bot('f9LHodD0cOL5NY7All_9xJRh5ZhPw6bRvq_0Adm8-1bZZEHdRy6_ZHDMNVPejUYNZg7Zhty-wKHNv2X2WJBQ') +dp = Dispatcher() +dp.include_routers(router) + + +start_text = '''Пример чат-бота для MAX 💙 + +Мои команды: + +/clear очищает ваш контекст +/state или /context показывают ваше контекстное состояние +/data показывает вашу контекстную память +''' + + +class Form(StatesGroup): + name = State() + age = State() + + +@dp.on_started() +async def _(): + logging.info('Бот стартовал!') + + +@dp.message_created(Command('clear')) +async def hello(event: MessageCreated, context: MemoryContext): + await context.clear() + await event.message.answer(f"Ваш контекст был очищен!") + + +@dp.message_created(Command('data')) +async def hello(event: MessageCreated, context: MemoryContext): + data = await context.get_data() + await event.message.answer(f"Ваша контекстная память: {str(data)}") + + +@dp.message_created(Command('context')) +@dp.message_created(Command('state')) +async def hello(event: MessageCreated, context: MemoryContext): + data = await context.get_state() + await event.message.answer(f"Ваше контекстное состояние: {str(data)}") + + +@dp.message_created(Command('start')) +async def hello(event: MessageCreated): + builder = InlineKeyboardBuilder() + + builder.row( + CallbackButton( + text='Ввести свое имя', + payload='btn_1' + ), + CallbackButton( + text='Ввести свой возраст', + payload='btn_2' + ) + ) + builder.row( + CallbackButton( + text='Не хочу', + payload='btn_3' + ) + ) + + await event.message.answer( + text=start_text, + attachments=[builder.as_markup()] # Для MAX клавиатура это вложение, + ) # поэтому она в списке вложений + + +@dp.message_callback(F.callback.payload == 'btn_1') +async def hello(event: MessageCallback, context: MemoryContext): + await context.set_state(Form.name) + await event.message.delete() + await event.message.answer(f'Отправьте свое имя:') + + +@dp.message_callback(F.callback.payload == 'btn_2') +async def hello(event: MessageCallback, context: MemoryContext): + await context.set_state(Form.age) + await event.message.delete() + await event.message.answer(f'Отправьте ваш возраст:') + + +@dp.message_callback(F.callback.payload == 'btn_3') +async def hello(event: MessageCallback, context: MemoryContext): + await event.message.delete() + await event.message.answer(f'Ну ладно 🥲') + + +@dp.message_created(F.message.body.text, Form.name) +async def hello(event: MessageCreated, context: MemoryContext): + await context.update_data(name=event.message.body.text) + + data = await context.get_data() + + await event.message.answer(f"Приятно познакомиться, {data['name'].title()}!") + + +@dp.message_created(F.message.body.text, Form.age) +async def hello(event: MessageCreated, context: MemoryContext): + await context.update_data(age=event.message.body.text) + + await event.message.answer(f"Ого! А мне всего пару недель 😁") + + +async def main(): + await bot.set_my_commands( + BotCommand( + name='/start', + description='Перезапустить бота' + ), + BotCommand( + name='/clear', + description='Очищает ваш контекст' + ), + BotCommand( + name='/state', + description='Показывают ваше контекстное состояние' + ), + BotCommand( + name='/data', + description='Показывает вашу контекстную память' + ), + BotCommand( + name='/context', + description='Показывают ваше контекстное состояние' + ) + ) + await dp.start_polling(bot) + # await dp.handle_webhook( + # bot=bot, + # host='localhost', + # port=8080 + # ) + + +asyncio.run(main()) \ No newline at end of file diff --git a/for_example.py b/for_example.py new file mode 100644 index 0000000..58f3d08 --- /dev/null +++ b/for_example.py @@ -0,0 +1,10 @@ +from maxapi import F, Router +from maxapi.types import Command, MessageCreated + +router = Router() + + +@router.message_created(Command('router')) +async def hello(obj: MessageCreated): + file = __file__.split('\\')[-1] + await obj.message.answer(f"Пишу тебе из роута {file}") \ No newline at end of file diff --git a/maxapi/bot.py b/maxapi/bot.py index 056ba4c..0b23da4 100644 --- a/maxapi/bot.py +++ b/maxapi/bot.py @@ -34,7 +34,8 @@ from .enums.sender_action import SenderAction from .types.attachments.attachment import Attachment from .types.attachments.image import PhotoAttachmentRequestPayload from .types.message import NewMessageLink -from .types.users import BotCommand, ChatAdmin +from .types.users import ChatAdmin +from .types.command import BotCommand from .connection.base import BaseConnection @@ -46,12 +47,11 @@ class Bot(BaseConnection): def __init__(self, token: str): super().__init__() + self.bot = self self.__token = token - self.params = { - 'access_token': self.__token - } + self.params = {'access_token': self.__token} self.marker_updates = None async def send_message( @@ -335,4 +335,13 @@ class Bot(BaseConnection): ): return await GetUpdates( bot=self, + ).request() + + async def set_my_commands( + self, + *commands: BotCommand + ): + return await ChangeInfo( + bot=self, + commands=list(commands) ).request() \ No newline at end of file diff --git a/maxapi/context/__init__.py b/maxapi/context/__init__.py index 98eeb6f..9aa6640 100644 --- a/maxapi/context/__init__.py +++ b/maxapi/context/__init__.py @@ -26,11 +26,14 @@ class MemoryContext: self._context.update(kwargs) async def set_state(self, state: State | str = None): - self._state = state + async with self._lock: + self._state = state async def get_state(self): - return self._state + async with self._lock: + return self._state async def clear(self): - self._state = None - self._context = {} + async with self._lock: + self._state = None + self._context = {} diff --git a/maxapi/dispatcher.py b/maxapi/dispatcher.py index fe3e9a9..68c1640 100644 --- a/maxapi/dispatcher.py +++ b/maxapi/dispatcher.py @@ -57,6 +57,8 @@ class Dispatcher: return new_ctx async def handle(self, event_object: UpdateUnion): + is_handled = False + for handler in self.event_handlers: if not handler.update_type == event_object.update_type: @@ -66,9 +68,9 @@ class Dispatcher: if not filter_attrs(event_object, *handler.filters): continue - memory_context = self.get_memory_context( - *event_object.get_ids() - ) + ids = event_object.get_ids() + + memory_context = self.get_memory_context(*ids) if not handler.state == await memory_context.get_state() \ and handler.state: @@ -82,14 +84,16 @@ class Dispatcher: if not key in func_args: del kwargs[key] - if kwargs: - await handler.func_event(event_object, **kwargs) - else: - await handler.func_event(event_object, **kwargs) + await handler.func_event(event_object, **kwargs) - logger_dp.info(f'Обработано: {event_object.update_type}') + logger_dp.info(f'Обработано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') + + is_handled = True break + if not is_handled: + logger_dp.info(f'Проигнорировано: {event_object.update_type} | chat_id: {ids[0]}, user_id: {ids[1]}') + async def start_polling(self, bot: Bot): self.bot = bot diff --git a/maxapi/filters/handler.py b/maxapi/filters/handler.py index fbd9b17..0e32869 100644 --- a/maxapi/filters/handler.py +++ b/maxapi/filters/handler.py @@ -5,6 +5,7 @@ from magic_filter import F, MagicFilter from ..types.command import Command from ..context.state_machine import State from ..enums.update import UpdateType +from ..loggers import logger_dp class Handler: @@ -28,4 +29,7 @@ class Handler: elif isinstance(arg, State): self.state = arg elif isinstance(arg, Command): - self.filters.insert(0, F.message.body.text == arg.command) \ No newline at end of file + self.filters.insert(0, F.message.body.text == arg.command) + else: + logger_dp.info(f'Обнаружен неизвестный фильтр `{arg}` при ' + f'регистрации функции `{func_event.__name__}`') \ No newline at end of file diff --git a/maxapi/methods/change_info.py b/maxapi/methods/change_info.py index 027679f..f5f2820 100644 --- a/maxapi/methods/change_info.py +++ b/maxapi/methods/change_info.py @@ -2,7 +2,8 @@ from typing import Any, Dict, List, TYPE_CHECKING -from ..types.users import BotCommand, User +from ..types.users import User +from ..types.command import BotCommand from ..enums.http_method import HTTPMethod from ..enums.api_path import ApiPath diff --git a/maxapi/methods/edit_chat.py b/maxapi/methods/edit_chat.py index 40be72a..fc1319b 100644 --- a/maxapi/methods/edit_chat.py +++ b/maxapi/methods/edit_chat.py @@ -5,10 +5,8 @@ from typing import Any, Dict, List, TYPE_CHECKING from collections import Counter from ..types.attachments.image import PhotoAttachmentRequestPayload - from ..types.chats import Chat - -from ..types.users import BotCommand, User +from ..types.command import Command from ..enums.http_method import HTTPMethod from ..enums.api_path import ApiPath diff --git a/maxapi/types/__init__.py b/maxapi/types/__init__.py index cad08ad..f7775f2 100644 --- a/maxapi/types/__init__.py +++ b/maxapi/types/__init__.py @@ -21,9 +21,10 @@ from ..types.attachments.buttons.link_button import LinkButton from ..types.attachments.buttons.request_contact import RequestContact from ..types.attachments.buttons.request_geo_location_button import RequestGeoLocationButton -from ..types.command import Command +from ..types.command import Command, BotCommand __all__ = [ + BotCommand, CallbackButton, ChatButton, LinkButton, diff --git a/maxapi/types/command.py b/maxapi/types/command.py index f859cfe..f40fe48 100644 --- a/maxapi/types/command.py +++ b/maxapi/types/command.py @@ -1,4 +1,8 @@ +from typing import Optional +from pydantic import BaseModel + + class Command: def __init__(self, text: str, prefix: str = '/'): self.text = text @@ -6,4 +10,9 @@ class Command: @property def command(self): - return self.prefix + self.text \ No newline at end of file + return self.prefix + self.text + + +class BotCommand(BaseModel): + name: str + description: Optional[str] = None \ No newline at end of file diff --git a/maxapi/types/updates/message_callback.py b/maxapi/types/updates/message_callback.py index c69427c..c4af974 100644 --- a/maxapi/types/updates/message_callback.py +++ b/maxapi/types/updates/message_callback.py @@ -51,7 +51,7 @@ class MessageCallback(Update): bot: Optional[Bot] def get_ids(self): - return (self.message.recipient.chat_id, self.message.recipient.user_id) + return (self.message.recipient.chat_id, self.callback.user.user_id) async def answer( self, diff --git a/maxapi/types/updates/message_created.py b/maxapi/types/updates/message_created.py index dd82b23..02c265b 100644 --- a/maxapi/types/updates/message_created.py +++ b/maxapi/types/updates/message_created.py @@ -19,4 +19,4 @@ class MessageCreated(Update): bot: Optional[Bot] def get_ids(self): - return (self.message.recipient.chat_id, self.message.recipient.user_id) \ No newline at end of file + return (self.message.recipient.chat_id, self.message.sender.user_id) \ No newline at end of file diff --git a/maxapi/types/users.py b/maxapi/types/users.py index 5cfa87c..28569e4 100644 --- a/maxapi/types/users.py +++ b/maxapi/types/users.py @@ -3,11 +3,7 @@ from typing import List, Optional from datetime import datetime from ..enums.chat_permission import ChatPermission - - -class BotCommand(BaseModel): - name: str - description: Optional[str] = None +from ..types.command import BotCommand class User(BaseModel): diff --git a/setup.py b/setup.py index 8be3768..be8ef83 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="maxapi", - version="0.1", + version="0.3", packages=find_packages(), description="Библиотека для взаимодействия с API мессенджера MAX", long_description=open("README.md", encoding='utf-8').read(),