Skip to main content

Creating bots has never been simpler.

Project description

NekoGram

Creating bots has never been simpler.
Join our Telegram chat @NekoGramDev

Overview

The idea of NekoGram is to let developers write code as little as possible but still be able to implement complex solutions.
NekoGram is based on AIOGram which means you can combine all its features with NekoGram.

Quick documentation

Note: Always read the documentation for the release you are using, NekoGram is constantly evolving and newer releases might be incompatible with older ones.

Current version: 2.0

Installation

Required:

pip install aiogram

Speedups:

pip install uvloop ujson cchardet aiodns

MySQL storage dependencies:

pip install aiomysql

PostgreSQL storage dependencies:

pip install aiopg

Structure, brief introduction and a bit of theory

Full image

Everything is quite simple (wow, really..). Let's divide this theory into topics:

Idea of Menus

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.

How to define Menus?

For now NekoGram supports only JSON Menus, but you may override BaseProcessor text processor class to make it support more formats, if you plan 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.

What is an Update?

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.

Update flow

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 JSON-defined menu. It contains all the data from JSON file and a few useful methods.

What is called a Formatter?

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.

How to Filter?

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.

What is a Function?

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)
    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.

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.

Routers

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

App structure

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.

Deeper understanding of components

NekoGram has a lot of features, and it is always nice to have some reference, there you go.

Storages

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.

MemoryStorage

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.

MySQLStorage

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.

PGStorage

A storage for PostgreSQL databases. Has basic features of MySQLStorage but is not tested, may not work.

Menus in depth

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

Widgets

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.

How to attach a widget?
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)
How to customize widgets?

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()
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

Multi-step menus

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 multi-step menus (2nd step in this example) a function has to be defined. The function should process data and redirect our user to another menu.

Afterword

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.

A word from lyteloli

NekoGram is my personal creation, I implemented everything on my own and try 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 apps created with it. Any kind of support is warmly welcome.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

NekoGram-2.2.7.tar.gz (46.5 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page