{"id":13610918,"url":"https://github.com/lyteloli/NekoGram","last_synced_at":"2025-04-13T01:33:32.009Z","repository":{"id":44001847,"uuid":"285286491","full_name":"lyteloli/NekoGram","owner":"lyteloli","description":"A wrapper over AIOGram that makes creation of Telegram bots even easier","archived":false,"fork":false,"pushed_at":"2024-02-24T19:05:48.000Z","size":710,"stargazers_count":36,"open_issues_count":4,"forks_count":4,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-10-02T06:52:20.132Z","etag":null,"topics":["aiogram","aiohttp","asynchronous","asyncio","neko","python","python3"],"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/lyteloli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2020-08-05T12:49:22.000Z","updated_at":"2024-09-24T13:47:27.000Z","dependencies_parsed_at":"2024-08-01T19:44:27.794Z","dependency_job_id":"479c3b5e-162b-4da2-b907-132b2003b01c","html_url":"https://github.com/lyteloli/NekoGram","commit_stats":{"total_commits":36,"total_committers":2,"mean_commits":18.0,"dds":0.05555555555555558,"last_synced_commit":"8e44a30b4c648ca317bf1af91d706b55ee6c6cc1"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyteloli%2FNekoGram","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyteloli%2FNekoGram/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyteloli%2FNekoGram/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lyteloli%2FNekoGram/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lyteloli","download_url":"https://codeload.github.com/lyteloli/NekoGram/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223558408,"owners_count":17165124,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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","aiohttp","asynchronous","asyncio","neko","python","python3"],"created_at":"2024-08-01T19:01:49.410Z","updated_at":"2024-11-07T17:30:51.805Z","avatar_url":"https://github.com/lyteloli.png","language":"Python","funding_links":["https://www.buymeacoffee.com/lyteloli"],"categories":["Python"],"sub_categories":[],"readme":"# NekoGram\n###### Creating bots has never been simpler.\n##### Join our [Telegram chat @NekoGramDev](https://t.me/NekoGramDev)\n##### [YouTube tutorial (RU)](https://youtu.be/68i1blPYGYU)\n##### [Example project](https://github.com/lyteloli/NekoGramBMICalculator)\n![](docs/nekogram-white.png)\n\n## Overview\nNekoGram is a data serialization format (JSON/YAML) processing layer framework over AIOGram, that makes bot \ndevelopment easier, faster and makes the code a lot more readable.\n\nIts main features include:\n- Multistep menus.\n- Multibot support.\n- Great localization support.\n- Reducing the amount of code you need to create a bot.\n- Widgets.\n- Proper MySQL storage.\n- Useful utils for various things, such as media upload that pure AIOGram lacks.\n- Routers. You no longer need to link a bunch of handlers from various files in your project.\n- Intermediate menus. These will be shown to users when you need some time to process inputs.\n- Proper input filtration, without having to write a ton of handlers to notify users about wrong input.\n- Freedom. You can combine any AIOGram feature with NekoGram if you need to do so.\n- Cool exception messages :3\n\nNekoGram is based on [AIOGram](https://github.com/aiogram/aiogram) which means you can combine all its features \nwith NekoGram.\n\n# Quick documentation\n\u003e Note: Always read the documentation for the release you are using, NekoGram is constantly evolving and newer \n\u003e releases might be incompatible with older ones.\n\n#### Current major version: 2.2\n\u003e ⚠️ This version implements changes that may be breaking, do not switch to it with existing projects unless you are \n\u003e ready to resolve them.\n\n\u003e The latest version was not yet published to PyPi so if you want to use webhooks, please clone this repository.\n\n## Installation\n```\npip install NekoGram\n```\nOR\n```\ngit clone https://github.com/lyteloli/NekoGram\n```\nSpeedups:\n```\npip install uvloop ujson cchardet aiodns\n```\nMySQL storage dependencies:\n```\npip install aiomysql\n```\nPostgreSQL storage dependencies:\n```\npip install asyncpg\n```\nSQLite storage dependencies:\n```\npip install aiosqlite\n```\nYAMLProcessor text processor dependencies:\n```\npip install pyyaml\n```\n\n## Structure, brief introduction and a bit of theory\n![](docs/update-structure.png)\n\u003e [Full image](docs/update-structure.png)\n\nEverything is quite simple (wow, really..). Let's divide this theory into topics:\n#### Idea of Menus\nFirstly, what is a Menu? We can imagine it as a class that holds menus that should be displayed to users as they \ninteract with your bot. For example you want to display the following menu to a user:\n![](docs/menu-example.png)\\\nProgrammatically it can be structured in many ways but NekoGram has its own strict Menu format which \nwould look like this:\n```json\n\"start\": {\n  \"text\": \"Hi, you have {active_subscriptions} active subscriptions\",\n  \"markup\": [\n    [{\"text\": \"⚡️Configure preferences\", \"call_data\": \"menu_configure_preferences\"}]\n  ]\n}\n```\nLet us go over the structure quickly. You can see a dictionary \"start\" which contains 2 fields: \"text\" and \"markup\".\n\"start\" is the name of the menu we want to define, \"text\" is the text that will be displayed to our users. \nWithin the value of \"text\" you can see `{active_subscriptions}`, which is a placeholder, you will understand how it \nworks later as you progress through the docs. Markup field is the keyboard that will be displayed to users along \nwith the text. Its structure is also quite simple, it is a 2 dimensional array of dictionaries. \nFirst dimension defines a list of keyboard rows with respect to row position. \nSecond dimension defines a keyboard row (each row might have multiple buttons).\nDictionaries themselves define button objects, in this case we have an inline button, therefore it has a \"text\" field \nand \"call_data\" field which defines the callback your app will get once the button is clicked, this way you can \nunderstand which menu our user wants to go to.\n\n#### How to define Menus?\nFor now NekoGram supports only JSON and YAML Menus, but you may override BaseProcessor text processor class to make it \nsupport more formats, if you decide to do so, please share it with others by submitting a pull request!\nYou may put the translation files anywhere and anyhow you want, though it is recommended to store them in a \n\"translations\" folder under the root folder of your app. \nEach file must have an [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) defined like this: \n`\"lang\": \"en\"`. Considering the previous Menu example, the whole file would look like this: \n```json\n{\n  \"lang\": \"en\",\n  \"start\": {\n    \"text\": \"Hi, you have {active_subscriptions} active subscriptions\",\n    \"markup\": [\n      [{\"text\": \"⚡️Configure preferences\", \"call_data\": \"menu_configure_preferences\"}]\n    ]\n  }\n}\n```\nNow let us get back to our [scheme](#structure-brief-introduction-and-a-bit-of-theory).\n\n#### What is an Update?\nAn Update is an AIOGram Message or CallbackQuery object, which is being fed to our app via AIOGram handlers.\nNekoGram only handles messages when a user is working with a certain menu. As for calls (CallbackQueries) it handles \nonly callbacks starting with predefined strings (menu_ and widget_ by default). If an update does not match these \ncriteria it is being ignored and AIOGram takes care about it, so you may define \nlower-level AIOGram handlers if you need to handle something NekoGram cannot.\n\n##### Update flow\nWhen we have an update that should be handled we have a couple options (refer to the schema above). \nIn any case a Menu object is being constructed in the first place. \nThis object is a class representing your menu defined in JSON/YAML. \nIt contains all the data from JSON/YAML file and a few useful methods.\n\n#### What is called a Formatter?\nFormatters are crucial part of NekoGram since they allow you to replace placeholders in your Menus with useful \ndata for users. Formatter is being called when a menu is being built, which means formatter is called before \na menu is being handled. Let us see an example of a Formatter, we will use the Menu we defined previously:\n```python\nfrom NekoGram import Neko, Menu\nfrom aiogram.types import User\nimport random\nNEKO = Neko(token='YOUR BOT TOKEN')  # Remember to initialize Neko beforehand\n\n\n@NEKO.formatter()\nasync def start(data: Menu, _: User, __: Neko):\n    await data.build(text_format={'active_subscriptions': random.randint(1, 100)})\n```\nNote that you do not need to return anything in Formatters, only call build function, which alters the Menu \nobject in-place.\n\n\n#### How to Filter?\nNekoGram supports AIOGram filters but also has its own, simpler version. Here is an example for better understanding \nif you have any experience with AIOGram:\n```python\nfrom aiogram.types import Message, CallbackQuery\nfrom aiogram.dispatcher.filters import Filter\nfrom NekoGram.storages import BaseStorage\nfrom typing import Dict, Union, Any\n\n\nclass HasMenu(Filter):\n    def __init__(self, database: BaseStorage):\n        self.database: BaseStorage = database\n\n    @classmethod\n    def validate(cls, _: Dict[str, Any]):\n        return {}\n\n    async def check(self, obj: Union[Message, CallbackQuery]) -\u003e bool:\n        return bool((await self.database.get_user_data(user_id=obj.from_user.id)).get('menu', False))\n```\nThis filter checks if a user is interacting with any Menu at the moment. Let us say you want to use it in your app. \nInitialize a Neko like this:\n```python\nfrom NekoGram import Neko\nNEKO: Neko = Neko(token='YOUR BOT TOKEN')\n```\nNow you may attach the filter in one of the following ways:\n`NEKO.add_filter(name='has_menu', callback=HasMenu)`\n`NEKO.add_filter(name='has_menu', callback=HasMenu.check)`\nWhat if you are not familiar with AIOGram or do not want to write big classes for simple filters? \nNot a problem, use a simple version!\n```python\nfrom aiogram.types import Message, CallbackQuery\nfrom typing import Union\n\n\nasync def is_int(obj: Union[Message, CallbackQuery]) -\u003e bool:\n    \"\"\"\n    Checks if message text can be converted to an integer\n    :return: True if so\n    \"\"\"\n    if isinstance(obj, CallbackQuery):  # Make sure we are working with Message text\n        obj = obj.message\n    return obj.text and obj.text.isdigit()\n```\nAnd attach it the following way: `NEKO.add_filter(name='int', callback=is_int)`.\nSounds simple, right? You may ask yourself why do you need to attach filters at all, the answer is because NekoGram \nvalidates user input automatically so that you do not have to write a ton of code.\nNow, how can we make Neko do it for us? Let us define a simple menu:\n```json\n\"menu_enter_age\": {\n  \"text\": \"Please enter your age\",\n  \"markup\": [\n    [{\"text\": \"⬅️Back\"}]\n  ],\n  \"filters\": [\"int\"],\n  \"validation_error\": \"Entered data is not an integer\"\n}\n```\nIn this example we use a reply keyboard instead of inline, this is more useful when collecting user input.\nWe defined our filter by name in \"filters\" field and a \"validation_error\" which will be displayed to users in case \ntheir input did not pass our filters.\n\u003e Note: filters only apply for messages, not callbacks. Filters are called before functions.\n\n#### What is a Function?\nWell, the naming might be bad, but you will get used to it :)\\\nFunctions give you freedom to do whatever, they are termination points of update handling process.\nLet us consider an example. Remember the menu we defined to get user's age in the previous section? \nNow we will define another Menu where our user will see his age.\n```json\n\"menu_result\": {\n  \"text\": \"Your age is {age}, you look nice today!\",\n  \"markup\": [\n    [{\"text\": \"🆗\", \"call_data\": \"menu_start\"}]\n  ]\n}\n```\nNow we can process the user input, let us define a function for that.\n```python\nfrom NekoGram import Neko, Menu\nfrom aiogram.types import Message, CallbackQuery\nfrom typing import Union\nNEKO = Neko(token='YOUR BOT TOKEN')  # Remember to initialize a Neko beforehand\n\n\n@NEKO.function()\nasync def menu_enter_age(_: Menu, message: Union[Message, CallbackQuery], __: Neko):\n    data = await NEKO.build_menu(name='menu_result', obj=message, auto_build=False)\n    await data.build(text_format={'age': message.text})\n```\nHere it is, notice how we can perform formatting within functions, but remember, a Menu must have no Formatter to do so\nand you need to pass `auto_build=False` because Neko tries to build text automatically if it does not find a formatter.\n\u003e There is a special case: \"start\" Menu, which is an entrypoint of your bot. You may define a Function for this menu \n\u003e to override default Neko behavior.\n\n#### Routers\nIn order to structure your app better and to avoid circular imports NekoGram provides NekoRouters to register \nFunctions and Formatters. It is recommended to use them instead of attaching Formatters and Functions to Neko object.\nExample:\n```python\nfrom NekoGram import NekoRouter, Neko, Menu\nfrom aiogram.types import User\n\nNEKO = Neko(token='YOUR BOT TOKEN')  # Remember to initialize a Neko beforehand\nROUTER = NekoRouter()\n\n\n@ROUTER.formatter()\nasync def test(data: Menu, user: User, neko: Neko):\n    pass\n\nNEKO.attach_router(ROUTER)  # Attach a router\n```\n\n#### App structure\n![](docs/project-structure.png)\n\nThis is an example project structure, you should structure all your Menus by relevant categories and within each \ncategory have separate files for Formatters and Functions. Later on attach the Routers to the Neko object.\n\n## Deeper understanding of components\nNekoGram has a lot of features, and it is always nice to have some reference, there you go.\n\n#### Storages\nJust like AIOGram, NekoGram uses its own storages to store user data. At the moment there are 3 types of \nstorages available: MySQLStorage, PGStorage and a MemoryStorage, let us walk through each of them quickly.\n##### MemoryStorage\nAs the name suggests, it stores data in your machine's memory, once you restart your app, all the data will be gone.\nThis storage is useful for tiny projects, testing and playing around with Neko.\n##### MySQLStorage\nThe most advanced and recommended storage of NekoGram. It checks database structure every time your app launches, \nif you do not have a database, it will create it for you. It is recommended to use Widgets only with this storage.\n##### PGStorage\nA storage for PostgreSQL databases. Has basic features of MySQLStorage.\n\u003e This storage may not work properly, it is not recommended using it.\n\n#### Menus in depth\nHere are all possible properties of a Menu:\n```json\n\"YOUR_MENU_NAME\": {\n  \"text\": \"YOUR TEXT\",\n  \"markup\": [\n    [{\"text\": \"YOUR TEXT\"}]\n  ],\n  \"markup_row_width\": 3,\n  \"no_preview\": false,\n  \"parse_mode\": \"HTML\",\n  \"silent\": false,\n  \"validation_error\": \"YOUR ERROR TEXT\",\n  \"extras\": {\n    \"YOUR_CUSTOM_KEY\": \"YOUR CUSTOM VALUE\"\n  }\n  \"prev_menu\": \"YOUR PREVIOUS MENU NAME\",\n  \"next_menu\": \"YOUR NEXT MENU NAME\",\n  \"filters\": [\"int\", \"photo\"]\n}\n```\nLet us go over each of them:\n- text: text to display to users\n- markup: keyboard to display to users\n- markup_row_width: row width of markup (max number of buttons per row)\n- no_preview: whether to hide webpage previews\n- silent: whether to deliver message without a notification\n- validation_error: text to display to users in case of input not passing filters\n- extras: a dictionary for any extra data\n- prev_menu: previous menu in multi-step menus\n- next_menu: next menu in multi-step menus\n- filters: user input filters\n\n#### Widgets\nWe strive for simplicity. That is why you have Widgets available, both builtin and third-party. \nYou may create your own widget by copying the structure of any widget in NekoGram/widgets folder.\nSome widgets may require extra database tables and Neko also takes care of that. It is recommended to use MySQLStorage \nwhen working with widgets.\n##### How to attach a widget?\n```python\nfrom NekoGram.widgets import broadcast\nfrom NekoGram import Neko\nNEKO = Neko(token='YOUR BOT TOKEN')  # Remember to initialize Neko beforehand\n\nasync def _():\n    await NEKO.attach_widget(formatters_router=broadcast.FORMATTERS_ROUTER, functions_router=broadcast.FUNCTIONS_ROUTER,\n                             startup=broadcast.startup)\n```\n##### How to customize widgets?\nThere are a few methods that override parts of widget Menus. They are: prev_menu_handlers, next_menu_handlers, \nmarkup_overriders.\nLet us try to customize the broadcast Widget to make it return user to our own defined menu, not to start Menu.\n\n```python\nfrom NekoGram import Neko, Menu\nfrom typing import List, Dict\nNEKO = Neko(token='YOUR BOT TOKEN') # Remember to initialize Neko beforehand\n\n@NEKO.prev_menu_handler()\nasync def widget_broadcast(_: Menu) -\u003e str:\n    return 'menu_test'\n\n\n@NEKO.markup_overrider(lang='en')  # Enter a language for which to override a keyboard\nasync def widget_broadcast_broadcast(_: Menu) -\u003e List[List[Dict[str, str]]]:\n    return [[{\"text\": \"🆗\", \"call_data\": \"menu_test\", \"id\": 2}]]\n```\nIn this way we have overriden the menu to which widget entrypoint should return us \n(if a user decided not to perform a broadcast) and the termination point (when a user finished their broadcast).\nWe have overridden the Menus that are inside the \n[widget folder](https://github.com/lyteloli/NekoGram/blob/master/NekoGram/widgets/broadcast/translations/en.json)\n\n##### Multi-step menus\nNekoGram allows you to reduce the amount of code by implementing multi-step Menus that may have as few as \njust one function to process the collected data all together when it is complete. Let us consider the broadcast \nwidget as an example:\n```json\n{\n  \"widget_broadcast_add_button_step_1\": {\n    \"text\": \"Please enter the button text\",\n    \"filters\": [\"text\"],\n    \"validation_error\": \"Only text is allowed\",\n    \"markup\": [\n      [{\"text\": \"⬅️Back\"}]\n    ],\n    \"markup_type\": \"reply\",\n    \"next_menu\": \"widget_broadcast_add_button_step_2\"\n  },\n  \"widget_broadcast_add_button_step_2\": {\n    \"text\": \"Please enter the button URL or mention\",\n    \"filters\": [\"url\", \"mention\"],\n    \"validation_error\": \"Only URL or mention is allowed\",\n    \"markup\": [\n      [{\"text\": \"⬅️Back\"}]\n    ],\n    \"markup_type\": \"reply\",\n    \"prev_menu\": \"widget_broadcast_add_button_step_1\"\n  }\n}\n```\nAs you can see, these menus are connected with \"prev_menu\" and \"next_menu\" fields and they both have filters defined.\nThis means that once input is submitted for the first step of the menu, Neko will write the input to a database and \ncontinue to the second step. For the last step of multistep menus (2nd step in this example) \na function has to be defined. The function should process data and redirect our user to another menu.\n\n# Webhooks\nYou can easily create \"multibots\" with NekoGram. It has a custom AIOGram executor class and a separate MySQL storage \nfor this purpose. Here's how you can start a webhook bot:\n```python\nfrom NekoGram.storages.mysql import KittyMySQLStorage\nfrom NekoGram import Neko\n\nSTORAGE: KittyMySQLStorage = KittyMySQLStorage(database='Example DB', user='example_user', password='Example password')\nNEKO: Neko = Neko(token='YOUR_TOKEN', storage=STORAGE, \n                  webhook_port=1234,  # Enter a custom port here\n                  webhook_path='/webhook/{token}', # Enter a custom path here\n                  webhook_url='https://example.com/webhook/{token}')  # Enter a custom URL here\nNEKO.start_webhook()  # Optionally pass a loop here\n```\n\n#### Neko initialization details\n1. Make sure you use KittyMySQLStorage, not MySQLStorage.\n2. Pass a proper free port for `webhook_port` parameter.\n3. Pass a custom path for `webhook_path` parameter, which must start with `/` and contain `{token}`.\n4. Pass a proper URL for `webhook_url` parameter, which cannot be localhost, you need to have a domain to run the \nwebhook or use services like [ngrok](https://ngrok.com/) to test it on your local machine. The URL must finish with the \nvalue passed in `webhook_path` parameter earlier.\n\n#### How to handle a webhook bot\nThere are a few changes to your general interaction with NekoGram in this case, here they are:\n- You now have to pass `bot_token` argument to the following storage functions: `get_user_data`, `set_user_data`, \n`set_user_menu`, `get_user_menu`.\n- `Menu` objects will now have a non-empty `bot_token` attribute.\n- You have to pass `bot_token` argument to `neko.build_menu()`.\n\u003e Important note: Do not migrate your existing polling bot to webhooks without clearing the database completely\n\n##### An example formatter with webhooks\n```json\n{\n  \"lang\": \"en\",\n  \"menu_example\": {\n    \"text\": \"Hello, you're using the bot with the following token: {token}\"\n  }\n}\n```\n```python\nfrom NekoGram import NekoRouter, Menu, Neko\nfrom aiogram import types\n\nROUTER: NekoRouter = NekoRouter()\n\n\n@ROUTER.formatter()\nasync def menu_example(data: Menu, user: types.User, neko: Neko):\n    await neko.storage.set_user_data(user_id=user.id, data={'NekoGram_is': 'awesome'}, bot_token=data.bot_token)\n    await data.build(text_format={'token': data.bot_token})\n```\n\n# Afterword\nThe documentation is still in-progress so check often for updates. It is also planned to add more widgets and make a \nseries of YouTube tutorials. If you have anything to add, comment or complain about, please do so via our \n[Telegram chat @NekoGramDev](https://t.me/NekoGramDev).\n\n### A word from lyteloli\nNekoGram is my personal creation, I implemented everything on my own and want to share it with people to build a \ncommunity of Telegram bot development enthusiasts, no matter if you're just playing around, doing personal or \ncommercial projects. I would be very grateful if you could spread a word about NekoGram, help with its development, \n[buy me a coffee](https://www.buymeacoffee.com/lyteloli) or mention NekoGram in one of your bots created with it. \nAny kind of support is warmly welcome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flyteloli%2FNekoGram","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flyteloli%2FNekoGram","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flyteloli%2FNekoGram/lists"}