{"id":21977317,"url":"https://github.com/iwatkot/aiogram_events","last_synced_at":"2026-02-14T12:37:03.593Z","repository":{"id":246436598,"uuid":"820145490","full_name":"iwatkot/aiogram_events","owner":"iwatkot","description":"A simple way to catch and process events in the aiogram library.","archived":false,"fork":false,"pushed_at":"2024-07-04T00:51:28.000Z","size":112,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-28T13:52:04.484Z","etag":null,"topics":["aiogram","aiogram-bot","aiogram3","async","telegram","telegram-bot","telegram-bot-api"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iwatkot.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-06-25T22:32:53.000Z","updated_at":"2025-09-25T10:49:23.000Z","dependencies_parsed_at":"2025-04-28T16:46:15.800Z","dependency_job_id":"81f72fba-04ac-4750-8502-4f2abeafe908","html_url":"https://github.com/iwatkot/aiogram_events","commit_stats":null,"previous_names":["iwatkot/aiogram_events"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/iwatkot/aiogram_events","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwatkot%2Faiogram_events","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwatkot%2Faiogram_events/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwatkot%2Faiogram_events/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwatkot%2Faiogram_events/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iwatkot","download_url":"https://codeload.github.com/iwatkot/aiogram_events/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iwatkot%2Faiogram_events/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29443492,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T10:51:12.367Z","status":"ssl_error","status_checked_at":"2026-02-14T10:50:52.088Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aiogram","aiogram-bot","aiogram3","async","telegram","telegram-bot","telegram-bot-api"],"created_at":"2024-11-29T16:14:16.796Z","updated_at":"2026-02-14T12:37:03.578Z","avatar_url":"https://github.com/iwatkot.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\" markdown\u003e\n\u003cimg src=\"https://github.com/iwatkot/aiogram_events/assets/118521851/8719b400-7778-4c25-94db-030d6c98c3fd\"\u003e\n\nA simple way to catch and process events in the aiogram library.\n\n\u003cp align=\"center\"\u003e\n    \u003ca href=\"#Overview\"\u003eOverview\u003c/a\u003e •\n    \u003ca href=\"#Quick-Start\"\u003eQuick Start\u003c/a\u003e •\n    \u003ca href=\"#Core-Components\"\u003eCore Components\u003c/a\u003e •\n    \u003ca href=\"#Tutorial\"\u003eTutorial\u003c/a\u003e •\n    \u003ca href=\"#Bugs-and-Feature-Requests\"\u003eBugs and Feature Requests\u003c/a\u003e •\n    \u003ca href=\"https://pypi.org/project/aiogram_events/\"\u003ePyPI\u003c/a\u003e\n\u003c/p\u003e\n\n![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/iwatkot/aiogram_events)\n![GitHub issues](https://img.shields.io/github/issues/iwatkot/aiogram_events)\n[![Build Status](https://github.com/iwatkot/py3xui/actions/workflows/checks.yml/badge.svg)](https://github.com/iwatkot/aiogram_events/actions)\n[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/)\n![PyPI - Downloads](https://img.shields.io/pypi/dm/aiogram_events)\u003cbr\u003e\n![PyPI - Python Version](https://img.shields.io/pypi/pyversions/aiogram_events)\n![PyPI - Version](https://img.shields.io/pypi/v/aiogram_events)\n[![Maintainability](https://api.codeclimate.com/v1/badges/7eba14b75914b09a4d50/maintainability)](https://codeclimate.com/github/iwatkot/aiogram_events/maintainability)\n\u003c/div\u003e\n\n## Overview\nThe `aiogram_events` library is a simple way to catch and process events in the `aiogram` library, including menus and multi-step forms. It's designed to simplify the process of creating bots and to make the code more readable and maintainable.\u003cbr\u003e\n\n## Quick Start\n**Step 1:** Install the library.\u003cbr\u003e\n```bash\npip install aiogram_events\n```\nℹ️ The library supports only `aigoram` \u003e= 3.0.0 and Python \u003e= 3.10.0.\u003cbr\u003e\n\n**Step 2:** Import library routers and add them to the bot.\u003cbr\u003e\n```python\nfrom aiogram_events import event_router, stepper_router\n\ndp.include_routers(event_router, stepper_router)\n```\nℹ️ Order matters! The event router should be added before the stepper router in most cases.\u003cbr\u003e\n\n**Step 3:** Create events.\u003cbr\u003e\nFor text events:\n```python\nfrom aiogram_events import TextEvent\n\nclass MainMenuEvent(TextEvent):\n    _button = BUTTON_MAIN_MENU\n    _answer = \"Now you are in the main menu.\"\n    _menu = [BUTTON_FORM, BUTTON_MAIN_MENU]\n    _admin_menu = [BUTTON_OPTIONS, BUTTON_FORM, BUTTON_MAIN_MENU]\n```\n\nFor callback events:\n\n```python\nfrom aiogram_events import CallbackEvent, Team\nfrom aiogram_events.stepper import NumberEntry\n\nclass AddAdmin(CallbackEvent):\n    _callback = \"admin__add_admin\"\n    _data_type = int\n    _complete = \"Admin added.\"\n\n    _entries = [\n        NumberEntry(\"Telegram ID\", \"Incorrect user ID.\", \"Enter the user Telegram ID to add it.\")\n    ]\n\n    async def process(self) -\u003e None:\n        await super().process(main_menu=BUTTON_MAIN_MENU, cancel=BUTTON_CANCEL, skip=BUTTON_SKIP)\n        if self.answers not in Team.admins:\n            Team.admins.append(self.answers)\n```\nShort explanation:\n- `_button` - the button that will trigger the event.\n- `_answer` - the message that the bot will send to the user.\n- `_menu` - a list of buttons that will be displayed to the user after the message.\n- `_admin_menu` - a list of buttons that will be displayed to the user if the user is an admin.\n- `_callback` - the prefix for the callback that will trigger the event.\n- `_data_type` - the type of data that comes with the callback.\n- `_complete` - the message to be sent to the user after the form is completed.\n- `_entries` - a list of `Entry` objects that will start a multi-step form.\n- `process()` - a method that will be called when the event is triggered, should be reimplemented in the event class. If the event contains a form, don't forget to call the `super().process()` method to start and process the form (or reimplement this logic manually).\n\n**Step 4:** Group events (optional).\u003cbr\u003e\n```python\nfrom aiogram_events import TextEventGroup, CallbackEventGroup\n\nclass StartGroup(TextEventGroup):\n    _events = [StartEvent, MainMenuEvent]\n\nclass AdminsCallbacksGroup(CallbackEventGroup):\n    _events = [AddAdmin, RemoveAdmin]\n    _prefix = \"admin__\"\n```\nShort explanation:\n- `_events` - a list of events that belong to this group.\n- `_prefix` - the prefix for the callbacks that will trigger the events in this group.\n\nℹ️ Grouping events does not change the behavior of the bot or make it faster, it's just a way to organize your code.\n\n**Step 5:** Register events.\u003cbr\u003e\n```python\nfrom aiogram_events.decorators import text_events, callback_events\n\n@text_events(StartGroup)\nasync def start(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n\n@callback_events(AdminsCallbacksGroup)\nasync def admins_callbacks(event: CallbackEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n```\n\nℹ️ Of course, you can add more methods to the individual events and handle them as you like. The decorators will just catch the event and pass it to the function. All other things are up to you.\n\n**Step 6:** Check out the tutorial and read the docs.\u003cbr\u003e\nYou can find a detailed tutorial in [this section](#Tutorial) and the detailed docs in the corresponding package directories.\n\n## Core Components\nYou'll find detailed docs with usage examples in the corresponding package directories:\n- [Decorators](aiogram_events/decorators/README.md)\n- [Event](aiogram_events/event/README.md)\n- [Stepper](aiogram_events/stepper/README.md)\n\nThe library is based on three main components: `Event` and `EventGroup` - which represent a single event and a group of events, `Stepper` - which is used for multi-step forms and the decorators that are used to register events.\n\n### Event\n`Event` is a base class for all events. It contains the main logic for catching and processing events. You can inherit from this class to create custom events, but it's recommended to use as a parent class one of the following classes: `TextEvent` or `CallbackEvent`. These classes are designed to work with text and callback events respectively.\n\n#### TextEvent\nThese events are triggered by text messages when the text of the message matches the `_button` attribute of the event. The `TextEvent` class has the following attributes:\n\n|     Attribute      |        Required         |                         Description                                             |\n| :----------------: | :----------------------:|:------------------------------------------------------------------------------: |\n|      `_button`     |           ✅            | The button that will trigger the event.                                         |\n|      `_answer`     |           *️⃣            | The message that the bot will send to the user, required if `_menu` is set.     |\n|       `_menu`      |           ❌            | A list of buttons that will be displayed to the user.                           |\n|   `_complete`      |           *️⃣            | The message to be sent to the user after the form is completed. Required if `_entries` is set. |\n|     `_entries`     |           ❌            | A list of `Entry` objects that will start a multi-step form.           |\n\n✅ - required\u003cbr\u003e\n❌ - optional\u003cbr\u003e\n*️⃣ - required for specific conditions.\u003cbr\u003e         \n\n#### CallbackEvent\nThese events are triggered by inline buttons when the callback of the button starts with the `_callback` attribute of the event. The `CallbackEvent` class has the following attributes:\n\n|     Attribute      |        Required         |                         Description                                             |\n| :----------------: | :----------------------:|:------------------------------------------------------------------------------: |\n|    `_callback`     |           ✅            | The prefix for the callback that will trigger the event.                        |\n|    `_data_type`    |           ✅            | The type of data that comes with the callback.                                  |\n|     `_answer`      |           *️⃣            | The message that the bot will send to the user.                                 |\n|     `_menu`        |           ❌            | A list of buttons that will be displayed to the user.                           |\n|   `_complete`      |           *️⃣            | The message to be sent to the user after the form is completed. Required if `_entries` is set. |\n|     `_entries`     |           ❌            | A list of `Entry` objects that will start a multi-step form.           |\n\n✅ - required\u003cbr\u003e\n❌ - optional\u003cbr\u003e\n*️⃣ - required for specific conditions.\u003cbr\u003e\n\n\n### EventGroup\nTo work with multiple events in one function, you can use one of the following classes: `TextEventGroup` or `CallbackEventGroup`. These classes are used to group events that are related to each other.\n\n#### TextEventGroup\nThis class is used to group text events. It has the following attributes:\n\n|     Attribute      |        Required         |                         Description                                             |\n| :----------------: | :----------------------:|:------------------------------------------------------------------------------: |\n|     `_events`      |           ✅            | A list of events that belong to this group.                                     |\n\n✅ - required\u003cbr\u003e\n\n#### CallbackEventGroup\nThis class is used to group callback events. It has the following attributes:\n\n|     Attribute      |        Required         |                         Description                                             |\n| :----------------: | :----------------------:|:------------------------------------------------------------------------------: |\n|     `_events`      |           ✅            | A list of events that belong to this group.                                     |\n|     `_prefix`      |           ✅            | The prefix for the callbacks that will trigger the events in this group.        |\n\n✅ - required\u003cbr\u003e\u003cbr\u003e\n\nℹ️ It's important to use the same prefix for all events in the group, otherwise, the events won't be triggered correctly.\n\n### Stepper\nYou don't need to work with the `Stepper` class directly, but you can use the `Entry` class to create custom entries or use the built-in entries e.g. `TextEntry` or `NumberEntry`. If you need to add some custom logic to the `Stepper` class, you can inherit from it and reimplement the required methods.\n\n#### Entry\nThe library contains several built-in entries that can be used to create forms. To create a custom entry, you need to inherit from the `Entry` class, add the `base_type` attribute to it and reimplement the `validate_answer()` method. The attribute is a type to which the answer will be converted. The method should return the boolean value, `True` if the answer is correct and `False` otherwise.\n\n### Decorators\nThe decorators are used to register events in the bot and to restrict access to the events by user role. The library contains the following decorators:\n- `@text_event` - to register a single text event.\n- `@text_events` - to register a group of text events.\n- `@callback_event` - to register a single callback event.\n- `@callback_events` - to register a group of callback events.\n- `@admin_only` - to allow access to the event only for admin users.\n- `@moderator_admin_only` - to allow access to the event only for moderator and admin users.\n\n## Tutorial\nIn this step-by-step guide, you will learn how to create a simple bot from scratch using `aiogram_events`.\u003cbr\u003e\nOf course, we will start with the installation of the library. It already has the `aiogram` library as a dependency, so you don't need to install it separately. To install the library, you can use the following command:\n```bash\npip install aiogram_events\n```\nTo debug this tutorial, you'll need to obtain a bot token from the BotFather. If you don't know how to do this, you can read the official documentation [here](https://core.telegram.org/bots#6-botfather).\u003cbr\u003e\nAnd now let's start coding! \u003cbr\u003e\u003cbr\u003e\n**Step 1:** Import the necessary modules.\u003cbr\u003e\n```python\nimport asyncio\nimport os\n\nfrom aiogram import Bot, Dispatcher\nfrom aiogram.client.default import DefaultBotProperties\nfrom aiogram.enums import ParseMode\nfrom dotenv import load_dotenv\n\nfrom aiogram_events import (\n    CallbackEvent,\n    CallbackEventGroup,\n    Team,\n    TextEvent,\n    TextEventGroup,\n    event_router,\n    stepper_router,\n)\nfrom aiogram_events.decorators import (\n    admin_only,\n    callback_events,\n    text_event,\n    text_events,\n)\nfrom aiogram_events.stepper import NumberEntry, TextEntry\nfrom aiogram_events.utils import inline_keyboard\n```\n\nLet's break down the code above (just in case, for the `aiogram` modules, you can read the official documentation [here](https://docs.aiogram.dev/en/latest/)). The most important imports here are `event_router` and `stepper_router`. It's better to start with them, so you won't forget to add them to your bot. While the `event_router` is responsible for handling events and is required for the correct operation of the library, the `stepper_router` is only needed if you're creating events containing forms with multiple steps. So, if you don't need forms, you can omit the `stepper_router` import. But don't forget to add it, if you decide to add some forms later. Since the library does not have any access to the bot, it won't raise any errors if the routers aren't added to the bot, but event catching would simply not work.\u003cbr\u003e\nWe will talk about other imports later when we need them. Now let's move on to the next step.\u003cbr\u003e\u003cbr\u003e\n\n**Step 2:** Create a bot.\u003cbr\u003e\n```python\nload_dotenv(\"local.env\")\nbot_token = os.getenv(\"TOKEN\")\n\ndp = Dispatcher()\nbot = Bot(token=bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))\n```\nTo know how to work with the `aiogram`, you can read the official documentation [here](https://docs.aiogram.dev/en/latest/). In this section, I'll only recommend using environment variables to store your bot token and for local debug you can use the `python-dotenv` library. You can install it using the following command:\n```bash\npip install python-dotenv\n```\nAnd then load the environment variables from the `.env` file. In this example, I used the `local.env` file, but you can use any name you want. Just don't forget to add this file to the `.gitignore` file, so you won't accidentally push your bot token to the repository.\u003cbr\u003e\nThe example structure of the `.env` file:\n```env\nTOKEN=your_bot_token\n```\n\n**Step 3:** Create a list of buttons (optional).\u003cbr\u003e\n```python\nBUTTON_START = \"/start\"\nBUTTON_SKIP = \"⏭ Skip\"\nBUTTON_CANCEL = \"❌ Cancel\"\nBUTTON_MAIN_MENU = \"🏠 Main Menu\"\nBUTTON_OPTIONS = \"⚙️ Options\"\nBUTTON_ADMINS = \"👥 Admins\"\nBUTTON_FORM = \"📝 Form\"\n```\nYou can store the strings for buttons in any way you like. In this example, I used constants for this purpose, but for large bots with a lot of buttons and/or for bots with multiple languages, it won't be very convenient. So it's up to you how to handle it. Friendly reminder: the `pydantic` library can be very helpful in this case. Check the official documentation [here](https://docs.pydantic.dev/latest).\u003cbr\u003e\u003cbr\u003e\n**Step 4:** Create the first simple text events.\u003cbr\u003e\n```python\nclass MainMenuEvent(TextEvent):\n    _button = BUTTON_MAIN_MENU\n    _answer = \"Now you are in the main menu.\"\n    _menu = [BUTTON_FORM, BUTTON_MAIN_MENU]\n    _admin_menu = [BUTTON_OPTIONS, BUTTON_FORM, BUTTON_MAIN_MENU]\n\n\nclass StartEvent(MainMenuEvent):\n    _button = BUTTON_START\n    _answer = \"Welcome to the bot!\"\n\n\nclass StartGroup(TextEventGroup):\n    _events = [StartEvent, MainMenuEvent]\n\n\n@text_events(StartGroup)\nasync def start(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n```\nSo, what we've got here? Let's talk about it in more detail:\n- `MainMenuEvent` is triggered when the user clicks the \"Main Menu\" button. To make this work we added the `_button` attribute with a string value of the button. The `_answer` attribute is the message that the bot will send to the user. The `_menu` attribute is a list of buttons that will be displayed to the user. The `_admin_menu` attribute is a list of buttons that will be displayed if the user is an admin. You can omit the `_admin_menu` attribute if you don't need it, all users will get the same menu then. NOTE: the `_answer` attribute is optional, if it's not set, the bot won't send any message to the user, but it's required if the `_menu` or `_admin_menu` attributes are set because Telegram sents the buttons inside of the message.\n- `StartEvent` is triggered when the user sends the \"/start\" command. It's inherited from the `MainMenuEvent` class since in both cases we want the bot to send the same menu to the user. But you can create a separate class for this event if you need to send a different menu to the user. It's just an example of inheritance, you can use it as you like.\n- `StartGroup` is a group of events that will be triggered when the user sends the \"/start\" command or clicks the \"Main Menu\" button. It's not necessary to use groups, you can handle each event individually, but it's more convenient to use groups when you have a lot of events that are related to each other. Each group class must contain the `_events` attribute with a list of events that belong to this group.\n- `@text_events(StartGroup)` is a decorator that registers the `StartGroup`. It will identify the event and pass it to the decorated function. The decorator function must have the event as an argument. You can implement any needed logic in the function, but by default, all Events have the `reply()` and `process()` methods. The `reply()` method sends the message to the user, and the `process()` method is expected to be reimplemented in the event class. You can add other methods to events and handle them whatever you like. It's completely up to you, the decorator will just catch the event and pass it to the function.\u003cbr\u003e\u003cbr\u003e\n\n**Step 5:** Reimplement the `process()` method.\u003cbr\u003e\nThe events in the previous step will work, but they won't do anything particular, we can consider them as events to navigate the user through the bot menu. But what if we want to add some logic to the event? The simplest way to do this is to reimplement the `process()` method. So let's add the event for the `Cancel` button.\n\n```python\nclass CancelEvent(MainMenuEvent):\n    _button = BUTTON_CANCEL\n    _answer = \"Operation canceled.\"\n\n    async def process(self) -\u003e None:\n        await self.state.clear()\n\n\n@text_event(CancelEvent)\nasync def cancel(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n```\nAs we did before, we inherited the `MainMenuEvent` class to send the same menu to the user. But this time we reimplemented the `process()` method. In this case, we just clear the state of the event. I won't explain here, what's the `State` and how it works, you can read about it in the detailed documentation [here](https://docs.aiogram.dev/en/dev-3.x/dispatcher/finite_state_machine/index.html). But in short, when working with forms (multiple-step events), sometimes you need to clear the state and for this case, we can use a `Cancel` button. The `@text_event(CancelEvent)` decorator is the same as `@text_events(StartGroup)`, but it's used for a single event, not a group. Just keep in mind that grouping events does not change the behavior of the bot or doesn't make it faster, it's just a way to organize your code. You can group all events in one group or create a separate function for each event, it's up to you.\u003cbr\u003e\u003cbr\u003e\n\n**Step 6:** Use inline keyboards.\nNow we're ready for something more interesting and use some inline keyboards. You can learn more about inline keyboards in the official documentation [here](https://docs.aiogram.dev/en/dev-3.x/utils/keyboard.html). In this tutorial, I assuming that you're familiar with inline keyboards and I won't explain how they work, I'll just show you how to use them with the `aiogram_events` library.\n\n```python\nTeam.admins = [1234567890, 9876543210]\n\n\nclass OptionsEvent(TextEvent):\n    _button = BUTTON_OPTIONS\n    _answer = \"Now you are in the options menu.\"\n    _menu = [BUTTON_ADMINS, BUTTON_MAIN_MENU]\n\n\nclass AdminsEvent(TextEvent):\n    _button = BUTTON_ADMINS\n\n    async def process(self) -\u003e None:\n        reply = \"Here is the list of admins. You can add or remove an admin.\"\n        data = {\n            f\"Remove admin with ID: {admin}\": f\"{RemoveAdmin._callback}{admin}\"\n            for admin in Team.admins\n        }\n        data.update({\"Add admin\": AddAdmin._callback})\n        await self.content.answer(reply, reply_markup=inline_keyboard(data))\n```\nFirst of all, we changed the list of admins in the `Team` class. It's not recommended to use this class in production mode, you should implement a way to store this data that meets your needs and reimplement the required functions in the `Event` class. You will find more information in the corresponding section of README.\u003cbr\u003e\nAfter we added a new sub-menu, but we already talked a lot about it, the important thing here is the `process()` method. In this method, we created a dictionary with the buttons and their callbacks. The key is the text of the button, and the value is the callback. So later we'll need to catch these callbacks and extract the necessary data from them. But along that way, we'll have one extra stop.\u003cbr\u003e\u003cbr\u003e\n\n**Step 7:** Restrict access to the event by user role.\u003cbr\u003e\nI guess it's not surprising that you need to restrict access to some events by user role. In this example, we have an event that can be accessed only by admins. Let's see how to do this.\n\n```python\nclass AdminsTextGroup(TextEventGroup):\n    _events = [AdminsEvent, OptionsEvent]\n\n\n@text_events(AdminsTextGroup)\n@admin_only\nasync def admins_texts(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n```\nNot sure that there's something to explain here. You can use the `@admin_only` decorator to restrict access to the event by user role. So only admin users will be able to trigger this event. Friendly reminder: it can be convenient to group events by user role, so you can use the `@admin_only` decorator only once for the whole group, not for each event separately.\u003cbr\u003e\u003cbr\u003e\n\n**Step 8:** Add callback events.\u003cbr\u003e\nOk, we're almost on a home stretch. Previously we created some inline buttons, but we didn't catch the callbacks. Let's do this now.\n\n```python\nclass AddAdmin(CallbackEvent):\n    _callback = \"admin__add_admin\"\n    _data_type = int\n    _complete = \"Admin added.\"\n\n    _entries = [\n        NumberEntry(\"Telegram ID\", \"Incorrect user ID.\", \"Enter the user Telegram ID to add it.\")\n    ]\n\n    async def process(self) -\u003e None:\n        await super().process(main_menu=BUTTON_MAIN_MENU, cancel=BUTTON_CANCEL, skip=BUTTON_SKIP)\n        if self.answers not in Team.admins:\n            Team.admins.append(self.answers)\n\n\nclass RemoveAdmin(CallbackEvent):\n    _callback = \"admin__remove_admin\"\n    _data_type = int\n    _answer = \"Admin removed.\"\n\n    async def process(self) -\u003e None:\n        if self.data in Team.admins:\n            Team.admins.remove(self.data)\n\n\nclass AdminsCallbacksGroup(CallbackEventGroup):\n    _events = [AddAdmin, RemoveAdmin]\n    _prefix = \"admin__\"\n\n\n@callback_events(AdminsCallbacksGroup)\n@admin_only\nasync def admins_callbacks(event: CallbackEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n```\n`CallbackEvent` works the same as `TextEvent`, but of course, it has its nuances. While the `TextEvent` has the `_button` attribute, the `CallbackEvent` has the `_callback` attribute. And the core difference is that callback will not be matched by equality but by the `.startswith()` method. This is because it's common practice to use prefixes for callbacks and add some data after the prefix. So if you're not familiar with this, here's advice: use unique prefixes for your callbacks, so you won't accidentally catch the wrong callback. Friendly reminder: if you add two events, for example, one with the prefix \"info_\" and another with the prefix \"info_name\", the second event will never be triggered, because the first event will catch all callbacks that start with \"info_\".\u003cbr\u003e\nThe second important thing is the `_data_type` attribute. It's used to validate the data that comes with the callback. So, it's pretty simple: if you expect some integers in the callback data, you can set the `_data_type` attribute to `int` and so on.\u003cbr\u003e\nIn the code example above you can also see the `_entries` attribute. We did not add it before, but it's important to mention that it can be added both in `TextEvent` and `CallbackEvent`. This list expects the `Entry` objects and will start a multi-step form if the list is not empty. You'll find detailed information about forms in the corresponding section of README. But when you add the `_entries` attribute, don't forget to add the `_complete` attribute as well. And in the process method, you should call the `super().process()` method to start and process the form. The `main_menu`, `cancel`, and `skip` arguments are optional and can be omitted. It's just a way to customize the buttons in the form.\u003cbr\u003e\nNow let's talk about the `CallbacksGroup` class. It's the same as the `TextEventGroup`, but you need to add one more attribute: the `_prefix` attribute. It's used to filter the callbacks by prefix. Ensure that all events in the group have the same prefix, otherwise, there can be some uncatchable callbacks. The `@callback_events(AdminsCallbacksGroup)` decorator is the same as `@text_events(StartGroup)`, but it's used for callback events.\u003cbr\u003e\u003cbr\u003e\n\n**Step 9:** Add custom form.\u003cbr\u003e\nAnd finally, let's add a custom form. It will be very simple, but it will show you how to work with forms in the `aiogram_events` library.\u003cbr\u003e\n\n```python\nclass FormEvent(TextEvent):\n    _button = BUTTON_FORM\n    _complete = \"Form completed.\"\n    _entries = [\n        TextEntry(\"Name\", \"Incorrect name.\", \"Enter your name.\"),\n        TextEntry(\"Surname\", \"Incorrect surname.\", \"Enter your surname.\", skippable=True),\n        NumberEntry(\"Age\", \"Incorrect age.\", \"Enter your age.\"),\n    ]\n\n    async def process(self) -\u003e None:\n        await super().process(main_menu=BUTTON_MAIN_MENU, cancel=BUTTON_CANCEL, skip=BUTTON_SKIP)\n\n        reply = \"\"\n        for field_name, answer in self.results.items():\n            reply += f\"{field_name}: {answer}\\n\"\n        await self.content.answer(reply)\n\n\n@text_event(FormEvent)\nasync def form(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n```\nSo, we already saw everything that contains this snippet in the previous steps. But now, we'll pay attention to working with form answers. If you will reimplement the `process()` method (and you definitely will), don't forget to call the `super().process()` method to start and process the form. After that, you can access the form answers in the `self.results` attribute. It's a dictionary where the key is the field name and the value is the answer. You can use this data as you like. In this example, we just send the answers back to the user, but you can do whatever you want with this data.\u003cbr\u003e\u003cbr\u003e\n\n**Step 10:** Add routers to the bot and finally run it.\u003cbr\u003e\n```python\nasync def main() -\u003e None:\n    dp.include_routers(event_router, stepper_router)\n    await dp.start_polling(bot)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\nNow our code is ready to run. One more thing I want to mention is that you must add the `event_router` and `stepper_router` to the bot, otherwise the events won't be caught. And one more very important thing: the order of the routers matters! The event will be caught by the first router that can catch it. So if you have two routers and the first one can catch the event, the second one will never catch it. So be careful with the order of the routers. Also I recommend always adding the `event_router` first and then the `stepper_router` since it will be more convenient to clear states in the `event_router` with the `Cancel` button or something like that.\u003cbr\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eFull code (click to expand)\u003c/summary\u003e\n\n```python\nimport asyncio\nimport os\n\nfrom aiogram import Bot, Dispatcher\nfrom aiogram.client.default import DefaultBotProperties\nfrom aiogram.enums import ParseMode\nfrom dotenv import load_dotenv\n\nfrom aiogram_events import (\n    CallbackEvent,\n    CallbackEventGroup,\n    Team,\n    TextEvent,\n    TextEventGroup,\n    event_router,\n    stepper_router,\n)\nfrom aiogram_events.decorators import (\n    admin_only,\n    callback_events,\n    text_event,\n    text_events,\n)\nfrom aiogram_events.stepper import NumberEntry, TextEntry\nfrom aiogram_events.utils import inline_keyboard\n\nload_dotenv(\"local.env\")\nbot_token = os.getenv(\"TOKEN\")\n\ndp = Dispatcher()\nbot = Bot(token=bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))\n\nBUTTON_START = \"/start\"\nBUTTON_SKIP = \"⏭ Skip\"\nBUTTON_CANCEL = \"❌ Cancel\"\nBUTTON_MAIN_MENU = \"🏠 Main Menu\"\nBUTTON_OPTIONS = \"⚙️ Options\"\nBUTTON_ADMINS = \"👥 Admins\"\nBUTTON_FORM = \"📝 Form\"\n\n\nclass MainMenuEvent(TextEvent):\n    _button = BUTTON_MAIN_MENU\n    _answer = \"Now you are in the main menu.\"\n    _menu = [BUTTON_FORM, BUTTON_MAIN_MENU]\n    _admin_menu = [BUTTON_OPTIONS, BUTTON_FORM, BUTTON_MAIN_MENU]\n\n\nclass StartEvent(MainMenuEvent):\n    _button = BUTTON_START\n    _answer = \"Welcome to the bot!\"\n\n\nclass StartGroup(TextEventGroup):\n    _events = [StartEvent, MainMenuEvent]\n\n\n@text_events(StartGroup)\nasync def start(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n\n\nclass CancelEvent(MainMenuEvent):\n    _button = BUTTON_CANCEL\n    _answer = \"Operation canceled.\"\n\n    async def process(self) -\u003e None:\n        await self.state.clear()\n\n\n@text_event(CancelEvent)\nasync def cancel(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n\n\nTeam.admins = [1234567890, 9876543210]\n\n\nclass OptionsEvent(TextEvent):\n    _button = BUTTON_OPTIONS\n    _answer = \"Now you are in the options menu.\"\n    _menu = [BUTTON_ADMINS, BUTTON_MAIN_MENU]\n\n\nclass AdminsEvent(TextEvent):\n    _button = BUTTON_ADMINS\n\n    async def process(self) -\u003e None:\n        reply = \"Here is the list of admins. You can add or remove an admin.\"\n        data = {\n            f\"Remove admin with ID: {admin}\": f\"{RemoveAdmin._callback}{admin}\"\n            for admin in Team.admins\n        }\n        data.update({\"Add admin\": AddAdmin._callback})\n        await self.content.answer(reply, reply_markup=inline_keyboard(data))\n\n\nclass AdminsTextGroup(TextEventGroup):\n    _events = [AdminsEvent, OptionsEvent]\n\n\n@text_events(AdminsTextGroup)\n@admin_only\nasync def admins_texts(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n\n\nclass AddAdmin(CallbackEvent):\n    _callback = \"admin__add_admin\"\n    _data_type = int\n    _complete = \"Admin added.\"\n\n    _entries = [\n        NumberEntry(\"Telegram ID\", \"Incorrect user ID.\", \"Enter the user Telegram ID to add it.\")\n    ]\n\n    async def process(self) -\u003e None:\n        await super().process(main_menu=BUTTON_MAIN_MENU, cancel=BUTTON_CANCEL, skip=BUTTON_SKIP)\n        if self.answers not in Team.admins:\n            Team.admins.append(self.answers)\n\n\nclass RemoveAdmin(CallbackEvent):\n    _callback = \"admin__remove_admin\"\n    _data_type = int\n    _answer = \"Admin removed.\"\n\n    async def process(self) -\u003e None:\n        if self.data in Team.admins:\n            Team.admins.remove(self.data)\n\n\nclass AdminsCallbacksGroup(CallbackEventGroup):\n    _events = [AddAdmin, RemoveAdmin]\n    _prefix = \"admin__\"\n\n\n@callback_events(AdminsCallbacksGroup)\n@admin_only\nasync def admins_callbacks(event: CallbackEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n\n\nclass FormEvent(TextEvent):\n    _button = BUTTON_FORM\n    _complete = \"Form completed.\"\n    _entries = [\n        TextEntry(\"Name\", \"Incorrect name.\", \"Enter your name.\"),\n        TextEntry(\"Surname\", \"Incorrect surname.\", \"Enter your surname.\", skippable=True),\n        NumberEntry(\"Age\", \"Incorrect age.\", \"Enter your age.\"),\n    ]\n\n    async def process(self) -\u003e None:\n        await super().process(main_menu=BUTTON_MAIN_MENU, cancel=BUTTON_CANCEL, skip=BUTTON_SKIP)\n\n        reply = \"\"\n        for field_name, answer in self.results.items():\n            reply += f\"{field_name}: {answer}\\n\"\n        await self.content.answer(reply)\n\n\n@text_event(FormEvent)\nasync def form(event: TextEvent) -\u003e None:\n    await event.reply()\n    await event.process()\n\n\nasync def main() -\u003e None:\n    dp.include_routers(event_router, stepper_router)\n    await dp.start_polling(bot)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n\u003c/details\u003e\n\nNow, let's launch our bot and take a look at how it works.\u003cbr\u003e\u003cbr\u003e\n[![Watch the video](https://github.com/iwatkot/aiogram_events/assets/118521851/405c808d-a9c3-45d2-b0d9-23939f55e14f)](https://github.com/iwatkot/aiogram_events/assets/118521851/ff2b0fc7-9812-4463-a16e-9da483f4a4ef)\n\n\u003cbr\u003e\n\n## Bugs and Feature Requests\nIf you find a bug or have a feature request, please open an issue on the GitHub repository.\u003cbr\u003e\nYou're also welcome to contribute to the project by opening a pull request.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiwatkot%2Faiogram_events","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiwatkot%2Faiogram_events","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiwatkot%2Faiogram_events/lists"}