Join our Telegram chat @NekoGramDev
NekoGram is a data serialization format (JSON/YAML) processing layer framework over AIOGram, that makes bot development easier, faster and makes the code a lot more readable.
Its main features include:
- Multistep menus.
- Multibot support.
- Great localization support.
- Reducing the amount of code you need to create a bot.
- Widgets.
- Proper MySQL storage.
- Useful utils for various things, such as media upload that pure AIOGram lacks.
- Routers. You no longer need to link a bunch of handlers from various files in your project.
- Intermediate menus. These will be shown to users when you need some time to process inputs.
- Proper input filtration, without having to write a ton of handlers to notify users about wrong input.
- Freedom. You can combine any AIOGram feature with NekoGram if you need to do so.
- Cool exception messages :3
NekoGram is based on AIOGram which means you can combine all its features with NekoGram.
Note: Always read the documentation for the release you are using, NekoGram is constantly evolving and newer releases might be incompatible with older ones.
⚠️ This version implements changes that may be breaking, do not switch to it with existing projects unless you are ready to resolve them.
The latest version was not yet published to PyPi so if you want to use webhooks, please clone this repository.
pip install NekoGram
OR
git clone https://github.com/lyteloli/NekoGram
Speedups:
pip install uvloop ujson cchardet aiodns
MySQL storage dependencies:
pip install aiomysql
PostgreSQL storage dependencies:
pip install asyncpg
SQLite storage dependencies:
pip install aiosqlite
YAMLProcessor text processor dependencies:
pip install pyyaml
Everything is quite simple (wow, really..). Let's divide this theory into topics:
Firstly, what is a Menu? We can imagine it as a class that holds menus that should be displayed to users as they
interact with your bot. For example you want to display the following menu to a user:
Programmatically it can be structured in many ways but NekoGram has its own strict Menu format which
would look like this:
"start": {
"text": "Hi, you have {active_subscriptions} active subscriptions",
"markup": [
[{"text": "⚡️Configure preferences", "call_data": "menu_configure_preferences"}]
]
}
Let us go over the structure quickly. You can see a dictionary "start" which contains 2 fields: "text" and "markup".
"start" is the name of the menu we want to define, "text" is the text that will be displayed to our users.
Within the value of "text" you can see {active_subscriptions}
, which is a placeholder, you will understand how it
works later as you progress through the docs. Markup field is the keyboard that will be displayed to users along
with the text. Its structure is also quite simple, it is a 2 dimensional array of dictionaries.
First dimension defines a list of keyboard rows with respect to row position.
Second dimension defines a keyboard row (each row might have multiple buttons).
Dictionaries themselves define button objects, in this case we have an inline button, therefore it has a "text" field
and "call_data" field which defines the callback your app will get once the button is clicked, this way you can
understand which menu our user wants to go to.
For now NekoGram supports only JSON and YAML Menus, but you may override BaseProcessor text processor class to make it
support more formats, if you decide to do so, please share it with others by submitting a pull request!
You may put the translation files anywhere and anyhow you want, though it is recommended to store them in a
"translations" folder under the root folder of your app.
Each file must have an IETF language tag defined like this:
"lang": "en"
. Considering the previous Menu example, the whole file would look like this:
{
"lang": "en",
"start": {
"text": "Hi, you have {active_subscriptions} active subscriptions",
"markup": [
[{"text": "⚡️Configure preferences", "call_data": "menu_configure_preferences"}]
]
}
}
Now let us get back to our scheme.
An Update is an AIOGram Message or CallbackQuery object, which is being fed to our app via AIOGram handlers. NekoGram only handles messages when a user is working with a certain menu. As for calls (CallbackQueries) it handles only callbacks starting with predefined strings (menu_ and widget_ by default). If an update does not match these criteria it is being ignored and AIOGram takes care about it, so you may define lower-level AIOGram handlers if you need to handle something NekoGram cannot.
When we have an update that should be handled we have a couple options (refer to the schema above). In any case a Menu object is being constructed in the first place. This object is a class representing your menu defined in JSON/YAML. It contains all the data from JSON/YAML file and a few useful methods.
Formatters are crucial part of NekoGram since they allow you to replace placeholders in your Menus with useful data for users. Formatter is being called when a menu is being built, which means formatter is called before a menu is being handled. Let us see an example of a Formatter, we will use the Menu we defined previously:
from NekoGram import Neko, Menu
from aiogram.types import User
import random
NEKO = Neko(token='YOUR BOT TOKEN') # Remember to initialize Neko beforehand
@NEKO.formatter()
async def start(data: Menu, _: User, __: Neko):
await data.build(text_format={'active_subscriptions': random.randint(1, 100)})
Note that you do not need to return anything in Formatters, only call build function, which alters the Menu object in-place.
NekoGram supports AIOGram filters but also has its own, simpler version. Here is an example for better understanding if you have any experience with AIOGram:
from aiogram.types import Message, CallbackQuery
from aiogram.dispatcher.filters import Filter
from NekoGram.storages import BaseStorage
from typing import Dict, Union, Any
class HasMenu(Filter):
def __init__(self, database: BaseStorage):
self.database: BaseStorage = database
@classmethod
def validate(cls, _: Dict[str, Any]):
return {}
async def check(self, obj: Union[Message, CallbackQuery]) -> bool:
return bool((await self.database.get_user_data(user_id=obj.from_user.id)).get('menu', False))
This filter checks if a user is interacting with any Menu at the moment. Let us say you want to use it in your app. Initialize a Neko like this:
from NekoGram import Neko
NEKO: Neko = Neko(token='YOUR BOT TOKEN')
Now you may attach the filter in one of the following ways:
NEKO.add_filter(name='has_menu', callback=HasMenu)
NEKO.add_filter(name='has_menu', callback=HasMenu.check)
What if you are not familiar with AIOGram or do not want to write big classes for simple filters?
Not a problem, use a simple version!
from aiogram.types import Message, CallbackQuery
from typing import Union
async def is_int(obj: Union[Message, CallbackQuery]) -> bool:
"""
Checks if message text can be converted to an integer
:return: True if so
"""
if isinstance(obj, CallbackQuery): # Make sure we are working with Message text
obj = obj.message
return obj.text and obj.text.isdigit()
And attach it the following way: NEKO.add_filter(name='int', callback=is_int)
.
Sounds simple, right? You may ask yourself why do you need to attach filters at all, the answer is because NekoGram
validates user input automatically so that you do not have to write a ton of code.
Now, how can we make Neko do it for us? Let us define a simple menu:
"menu_enter_age": {
"text": "Please enter your age",
"markup": [
[{"text": "⬅️Back"}]
],
"filters": ["int"],
"validation_error": "Entered data is not an integer"
}
In this example we use a reply keyboard instead of inline, this is more useful when collecting user input. We defined our filter by name in "filters" field and a "validation_error" which will be displayed to users in case their input did not pass our filters.
Note: filters only apply for messages, not callbacks. Filters are called before functions.
Well, the naming might be bad, but you will get used to it :)
Functions give you freedom to do whatever, they are termination points of update handling process.
Let us consider an example. Remember the menu we defined to get user's age in the previous section?
Now we will define another Menu where our user will see his age.
"menu_result": {
"text": "Your age is {age}, you look nice today!",
"markup": [
[{"text": "🆗", "call_data": "menu_start"}]
]
}
Now we can process the user input, let us define a function for that.
from NekoGram import Neko, Menu
from aiogram.types import Message, CallbackQuery
from typing import Union
NEKO = Neko(token='YOUR BOT TOKEN') # Remember to initialize a Neko beforehand
@NEKO.function()
async def menu_enter_age(_: Menu, message: Union[Message, CallbackQuery], __: Neko):
data = await NEKO.build_menu(name='menu_result', obj=message, auto_build=False)
await data.build(text_format={'age': message.text})
Here it is, notice how we can perform formatting within functions, but remember, a Menu must have no Formatter to do so
and you need to pass auto_build=False
because Neko tries to build text automatically if it does not find a formatter.
There is a special case: "start" Menu, which is an entrypoint of your bot. You may define a Function for this menu to override default Neko behavior.
In order to structure your app better and to avoid circular imports NekoGram provides NekoRouters to register Functions and Formatters. It is recommended to use them instead of attaching Formatters and Functions to Neko object. Example:
from NekoGram import NekoRouter, Neko, Menu
from aiogram.types import User
NEKO = Neko(token='YOUR BOT TOKEN') # Remember to initialize a Neko beforehand
ROUTER = NekoRouter()
@ROUTER.formatter()
async def test(data: Menu, user: User, neko: Neko):
pass
NEKO.attach_router(ROUTER) # Attach a router
This is an example project structure, you should structure all your Menus by relevant categories and within each category have separate files for Formatters and Functions. Later on attach the Routers to the Neko object.
NekoGram has a lot of features, and it is always nice to have some reference, there you go.
Just like AIOGram, NekoGram uses its own storages to store user data. At the moment there are 3 types of storages available: MySQLStorage, PGStorage and a MemoryStorage, let us walk through each of them quickly.
As the name suggests, it stores data in your machine's memory, once you restart your app, all the data will be gone. This storage is useful for tiny projects, testing and playing around with Neko.
The most advanced and recommended storage of NekoGram. It checks database structure every time your app launches, if you do not have a database, it will create it for you. It is recommended to use Widgets only with this storage.
A storage for PostgreSQL databases. Has basic features of MySQLStorage.
This storage may not work properly, it is not recommended using it.
Here are all possible properties of a Menu:
"YOUR_MENU_NAME": {
"text": "YOUR TEXT",
"markup": [
[{"text": "YOUR TEXT"}]
],
"markup_row_width": 3,
"no_preview": false,
"parse_mode": "HTML",
"silent": false,
"validation_error": "YOUR ERROR TEXT",
"extras": {
"YOUR_CUSTOM_KEY": "YOUR CUSTOM VALUE"
}
"prev_menu": "YOUR PREVIOUS MENU NAME",
"next_menu": "YOUR NEXT MENU NAME",
"filters": ["int", "photo"]
}
Let us go over each of them:
- text: text to display to users
- markup: keyboard to display to users
- markup_row_width: row width of markup (max number of buttons per row)
- no_preview: whether to hide webpage previews
- silent: whether to deliver message without a notification
- validation_error: text to display to users in case of input not passing filters
- extras: a dictionary for any extra data
- prev_menu: previous menu in multi-step menus
- next_menu: next menu in multi-step menus
- filters: user input filters
We strive for simplicity. That is why you have Widgets available, both builtin and third-party. You may create your own widget by copying the structure of any widget in NekoGram/widgets folder. Some widgets may require extra database tables and Neko also takes care of that. It is recommended to use MySQLStorage when working with widgets.
from NekoGram.widgets import broadcast
from NekoGram import Neko
NEKO = Neko(token='YOUR BOT TOKEN') # Remember to initialize Neko beforehand
async def _():
await NEKO.attach_widget(formatters_router=broadcast.FORMATTERS_ROUTER, functions_router=broadcast.FUNCTIONS_ROUTER,
startup=broadcast.startup)
There are a few methods that override parts of widget Menus. They are: prev_menu_handlers, next_menu_handlers, markup_overriders. Let us try to customize the broadcast Widget to make it return user to our own defined menu, not to start Menu.
from NekoGram import Neko, Menu
from typing import List, Dict
NEKO = Neko(token='YOUR BOT TOKEN') # Remember to initialize Neko beforehand
@NEKO.prev_menu_handler()
async def widget_broadcast(_: Menu) -> str:
return 'menu_test'
@NEKO.markup_overrider(lang='en') # Enter a language for which to override a keyboard
async def widget_broadcast_broadcast(_: Menu) -> List[List[Dict[str, str]]]:
return [[{"text": "🆗", "call_data": "menu_test", "id": 2}]]
In this way we have overriden the menu to which widget entrypoint should return us (if a user decided not to perform a broadcast) and the termination point (when a user finished their broadcast). We have overridden the Menus that are inside the widget folder
NekoGram allows you to reduce the amount of code by implementing multi-step Menus that may have as few as just one function to process the collected data all together when it is complete. Let us consider the broadcast widget as an example:
{
"widget_broadcast_add_button_step_1": {
"text": "Please enter the button text",
"filters": ["text"],
"validation_error": "Only text is allowed",
"markup": [
[{"text": "⬅️Back"}]
],
"markup_type": "reply",
"next_menu": "widget_broadcast_add_button_step_2"
},
"widget_broadcast_add_button_step_2": {
"text": "Please enter the button URL or mention",
"filters": ["url", "mention"],
"validation_error": "Only URL or mention is allowed",
"markup": [
[{"text": "⬅️Back"}]
],
"markup_type": "reply",
"prev_menu": "widget_broadcast_add_button_step_1"
}
}
As you can see, these menus are connected with "prev_menu" and "next_menu" fields and they both have filters defined. This means that once input is submitted for the first step of the menu, Neko will write the input to a database and continue to the second step. For the last step of multistep menus (2nd step in this example) a function has to be defined. The function should process data and redirect our user to another menu.
You can easily create "multibots" with NekoGram. It has a custom AIOGram executor class and a separate MySQL storage for this purpose. Here's how you can start a webhook bot:
from NekoGram.storages.mysql import KittyMySQLStorage
from NekoGram import Neko
STORAGE: KittyMySQLStorage = KittyMySQLStorage(database='Example DB', user='example_user', password='Example password')
NEKO: Neko = Neko(token='YOUR_TOKEN', storage=STORAGE,
webhook_port=1234, # Enter a custom port here
webhook_path='/webhook/{token}', # Enter a custom path here
webhook_url='https://example.com/webhook/{token}') # Enter a custom URL here
NEKO.start_webhook() # Optionally pass a loop here
- Make sure you use KittyMySQLStorage, not MySQLStorage.
- Pass a proper free port for
webhook_port
parameter. - Pass a custom path for
webhook_path
parameter, which must start with/
and contain{token}
. - Pass a proper URL for
webhook_url
parameter, which cannot be localhost, you need to have a domain to run the webhook or use services like ngrok to test it on your local machine. The URL must finish with the value passed inwebhook_path
parameter earlier.
There are a few changes to your general interaction with NekoGram in this case, here they are:
- You now have to pass
bot_token
argument to the following storage functions:get_user_data
,set_user_data
,set_user_menu
,get_user_menu
. Menu
objects will now have a non-emptybot_token
attribute.- You have to pass
bot_token
argument toneko.build_menu()
.
Important note: Do not migrate your existing polling bot to webhooks without clearing the database completely
{
"lang": "en",
"menu_example": {
"text": "Hello, you're using the bot with the following token: {token}"
}
}
from NekoGram import NekoRouter, Menu, Neko
from aiogram import types
ROUTER: NekoRouter = NekoRouter()
@ROUTER.formatter()
async def menu_example(data: Menu, user: types.User, neko: Neko):
await neko.storage.set_user_data(user_id=user.id, data={'NekoGram_is': 'awesome'}, bot_token=data.bot_token)
await data.build(text_format={'token': data.bot_token})
The documentation is still in-progress so check often for updates. It is also planned to add more widgets and make a series of YouTube tutorials. If you have anything to add, comment or complain about, please do so via our Telegram chat @NekoGramDev.
NekoGram is my personal creation, I implemented everything on my own and want to share it with people to build a community of Telegram bot development enthusiasts, no matter if you're just playing around, doing personal or commercial projects. I would be very grateful if you could spread a word about NekoGram, help with its development, buy me a coffee or mention NekoGram in one of your bots created with it. Any kind of support is warmly welcome.