DSL framework for creating Telegram bots

I had an idea! I want to create a framework that will allow users to write their Telegram bots using a Domain Specific Language (DSL) or a visual representation such as a UML diagram. Based on the data provided, the framework will generate the necessary Python code to create a fully functional Telegram bot. Which can be immediately launched somewhere on the hosting.

Well then. You have to start with a plan.


  1. Domain Specific Language (DSL) or visual representation: We need to develop a DSL or visual representation (such as UML diagrams) that will allow users to describe the menu structure, logic, and transitions of the bot. We can use a combination of JSON, YAML, or XML to define a DSL, or create a simple GUI for designing UML diagrams.

  2. parser: Develop a parser that can read and interpret the DSL or visual representation. The parser must be able to understand the menu structure, logic, and user-defined transitions and translate them into an internal representation that can be used to generate code.

  3. Code generation: Implement a code generation module that takes an internal representation generated by the parser and generates Python code using the aiogram library.

  4. Deployment and integration: Provide the ability to deploy the generated bot code on the hosting platform and integrate it with the Telegram API to make the bot functional. Also, create a simple process for updating the bot when changes are made to the DSL or visual representation.

  5. Documentation and Tutorials: Develop complete documentation and tutorials to help users understand how to use the framework and create their own Telegram bots using the provided DSL or visual representation.

Items 4 and 5 are still under development. But we will do the first three today.

  1. I chose to use YAML for DSL as it is human readable and easy to understand. Here is a simple example of a DSL structure to describe the menus, logic, and transitions of a Telegram bot:

bot:
  token: YOUR_TELEGRAM_BOT_TOKEN
  start_message: Welcome to the sample bot!

states:
  START:
    actions:
      - type: send_message
        text: "Choose an option:"
      - type: show_keyboard
        options:
          - text: Show random number
            target_state: RANDOM_NUMBER
          - text: Tell a joke
            target_state: JOKE

  RANDOM_NUMBER:
    actions:
      - type: send_random_number
        min: 1
        max: 100
        text: "Here's a random number between 1 and 100: {}"
      - type: back_to_start
        text: Back to main menu

  JOKE:
    actions:
      - type: send_joke
        text: "Here's a funny joke: {}"
      - type: back_to_start
        text: Back to main menu

In this example, the YAML file is structured into different sections:

  1. bot: Contains bot-specific information such as token and start message.

  2. states: Represents various states or menu items of the bot. Each state has a unique identifier (for example, START, RANDOM_NUMBER, JOKE) and contains a list of actions.

  3. actions: Defines the actions to be performed when the user interacts with the bot in a certain state. Actions include sending a message, showing a keyboard, sending a random number, and more.

Field type In chapter actions specifies the type of action to be performed, such as sending a message or showing a keyboard. Field target_state is used to define the state to go to when the user selects a certain option.

This is a simple DSL that can be extended to include more complex logic and additional actions. Over time, it will be possible to tie some kind of plugin to Miro or Figma. The next step is to create a parser that can read this DSL and generate the necessary Python code using the aiogram library.

To create a parser that can read and interpret the YAML DSL, we can use the library PyYAML to load a YAML file and further process the data. Below is a simple parser implementation:

import yaml


class BotDefinition:
    def __init__(self, yaml_file):
        with open(yaml_file, "r") as f:
            self.data = yaml.safe_load(f)

    def get_bot_token(self):
        return self.data["bot"]["token"]

    def get_start_message(self):
        return self.data["bot"]["start_message"]

    def get_states(self):
        return self.data["states"]


def parse_bot_definition(yaml_file):
    return BotDefinition(yaml_file)


if __name__ == "__main__":
    bot_definition = parse_bot_definition("example.yaml")
    print(bot_definition.get_bot_token())
    print(bot_definition.get_start_message())
    print(bot_definition.get_states())

In this implementation, the class BotDefinition is responsible for loading and processing the YAML file. Function parse_bot_definition takes the path to the YAML file as an argument and returns an object BotDefinition.

An object BotDefinition provides methods for accessing bot-specific information (for example, get_bot_token And get_start_message), as well as states and actions defined in the DSL (for example, get_states).

You can test this implementation with the previously provided YAML DSL example by saving it to a file named “example.yaml” and running the parser script.

After the parser works correctly, the next step is to create a code generation module that accepts an object BotDefinition and generates Python code using the aiogram library. This code will define the menu structure, logic and transitions for the Telegram bot as specified in the YAML DSL.

YOUR\_TELEGRAM\_BOT\_TOKEN Welcome to the sample bot! {'START': {'actions': \[{'type': 'send\_message', 'text': 'Choose an option:'}, {'type': 'show\_keyboard', 'options': \[{'text': 'Show random number', 'target\_state': 'RANDOM\_NUMBER'}, {'text': 'Tell a joke', 'target\_state': 'JOKE'}\]}\]}, 'RANDOM\_NUMBER': {'actions': \[{'type': 'send\_random\_number', 'min': 1, 'max': 100, 'text': "Here's a random number between 1 and 100: {}"}, {'type': 'back\_to\_start', 'text': 'Back to main menu'}\]}, 'JOKE': {'actions': \[{'type': 'send\_joke', 'text': "Here's a funny joke: {}"}, {'type': 'back\_to\_start', 'text': 'Back to main menu'}\]}}

To create a code generation module that accepts an object BotDefinition and generates Python code using the aiogram library, we will first create a template for the aiogram bot code and then use the Jinja2 library to render the final code with the parsed YAML data.

First install Jinja2:

pip install jinja2

Create a template file named bot_template.py.j2 with the following content:

import logging
import random
from aiogram import Bot, Dispatcher, types
from aiogram.contrib.middlewares.logging import LoggingMiddleware
from aiogram.types import ParseMode
from aiogram.utils import executor

logging.basicConfig(level=logging.INFO)

API_TOKEN = '{{ bot_token }}'

bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
dp.middleware.setup(LoggingMiddleware())

{% for state, actions in states.items() %}
{% for action in actions['actions'] %}
{% if action['type'] == 'send_message' %}
async def {{ state }}_send_message(message: types.Message):
    await message.reply("{{ action['text'] }}", parse_mode=ParseMode.HTML)
{% endif %}

{% if action['type'] == 'show_keyboard' %}
async def {{ state }}_show_keyboard(message: types.Message):
    keyboard_markup = types.ReplyKeyboardMarkup(row_width=2)
    {% for option in action['options'] %}
    button = types.KeyboardButton("{{ option['text'] }}")
    keyboard_markup.add(button)
    {% endfor %}
    await message.reply("Choose an option:", reply_markup=keyboard_markup)
{% endif %}
{% endfor %}
{% endfor %}

# Register message handlers
{% for state, actions in states.items() %}
@dp.message_handler(commands=['{{ state.lower() }}'])
async def {{ state.lower() }}_command_handler(message: types.Message):
    {% for action in actions['actions'] %}
    await {{ state }}_{{ action['type'] }}(message)
    {% endfor %}
{% endfor %}

async def on_start(dp):
    await bot.send_message(chat_id=config.ADMIN_ID, text="Bot has been started")

async def on_shutdown(dp):
    await bot.send_message(chat_id=config.ADMIN_ID, text="Bot has been stopped")

    await dp.storage.close()
    await dp.storage.wait_closed()

    await bot.session.close()

if __name__ == '__main__':
    from aiogram import executor
    executor.start_polling(dp, on_startup=on_start, on_shutdown=on_shutdown)

Now create a python script that reads the object BotDefinition and renders the bot code using Jinja2:

from jinja2 import Environment, FileSystemLoader

import parser_bot


def generate_bot_code(bot_definition):
    env = Environment(loader=FileSystemLoader('.'))
    template = env.get_template('bot_template.py.j2')

    bot_token = bot_definition.get_bot_token()
    states = bot_definition.get_states()

    rendered_code = template.render(bot_token=bot_token, states=states)

    with open('generated_bot.py', 'w') as f:
        f.write(rendered_code)


if __name__ == '__main__':
    # First, parse the YAML file to create a BotDefinition object
    bot_definition = parser_bot.parse_bot_definition("example.yaml")

    # Then, generate the bot code using the BotDefinition object
    generate_bot_code(bot_definition)

This script uses Jinja2 to render the aiogram bot code based on the template and parsed YAML data. It writes the generated code to a file named generated_bot.py.
It remains only to add to it the methods that we want to be executed when choosing RANDOM_NUMBER_send_random_number and JOKE_send_joke

Like this:

import random

async def RANDOM_NUMBER_send_random_number(message: types.Message):
    random_number = random.randint(1, 100)
    await message.reply(f"Here's a random number between 1 and 100: {random_number}", parse_mode=ParseMode.HTML)

@dp.message_handler(lambda message: message.text.lower() == 'show random number')
async def random_number_handler(message: types.Message):
    await RANDOM_NUMBER_send_random_number(message)


async def RANDOM_NUMBER_back_to_start(message: types.Message):
    await start_send_message(message)
    await start_show_keyboard(message)


@dp.message_handler(lambda message: message.text.lower() == 'back to main menu')
async def back_to_start_handler(message: types.Message):
    await RANDOM_NUMBER_back_to_start(message)
import random

async def JOKE_send_joke(message: types.Message):
    jokes = [
        "Why don't scientists trust atoms? Because they make up everything!",
        "Why did the chicken cross the road? To get to the other side!",
        "Why couldn't the bicycle stand up by itself? Because it was two-tired!",
        "Why do we never tell secrets on a farm? Because the potatoes have eyes and the corn has ears!",
        "What's orange and sounds like a parrot? A carrot!"
    ]

    selected_joke = random.choice(jokes)
    await message.reply("Here's a funny joke: {}".format(selected_joke), parse_mode=ParseMode.HTML)

@dp.message_handler(lambda message: message.text.lower() == 'tell a joke')
async def joke_handler(message: types.Message):
    await JOKE_send_joke(message)


async def JOKE_back_to_start(message: types.Message):
    await start_show_keyboard(message)

@dp.message_handler(lambda message: message.text.lower() == 'back to main menu' and current_state == "JOKE")
async def joke_back_to_start_handler(message: types.Message):
    await JOKE_back_to_start(message)

Now you can run this bot 🙂

It seems to be not an easy topic. But we got over it quickly.

Code on GitHub – https://github.com/omeh2003/DSL_FrameWork_Bot

If you liked it start. You are subscribed to social networks 🙂

https://twitter.com/semenov1981
https://t.me/InfoTechPulse

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *