Giter Site home page Giter Site logo

tishka17 / aiogram_dialog Goto Github PK

View Code? Open in Web Editor NEW
578.0 14.0 96.0 3.39 MB

GUI framework on top of aiogram

Home Page: https://aiogram-dialog.readthedocs.io

License: Apache License 2.0

Python 97.50% Makefile 0.16% Batchfile 0.20% HTML 2.13% Fluent 0.02%
aiogram telegram telegram-bot-api gui framework asyncio fsm

aiogram_dialog's Introduction

Aiogram Dialog

PyPI version Doc downloads license license

Version status:

  • v2.x - actual release, supports aiogram v3.x
  • v1.x - old release, supports aiogram v2.x, critical fix only

About

aiogram-dialog is a framework for developing interactive messages and menus in your telegram bot like a normal GUI application.

It is inspired by ideas of Android SDK and other tools.

Main ideas are:

  • split data retrieving, rendering and action processing - you need nothing to do for showing same content after some actions, also you can show same data in multiple ways.
  • reusable widgets - you can create calendar or multiselect at any point of your application without copy-pasting its internal logic
  • limited scope of context - any dialog keeps some data until closed, multiple opened dialogs process their data separately

Designing you bot with aiogram-dialog you think about user, what he sees and what he does. Then you split this vision into reusable parts and design your bot combining dialogs, widows and widgets. By this moment you can review interface and add your core logic.

Many components are ready for use, but you can extend and add your own widgets and even core features.

For more details see documentation and examples

Supported features:

  • Rich text rendering using format function or Jinja2 template engine.
  • Automatic message updating after user actions
  • Multiple independent dialog stacks with own data storage and transitions
  • Inline keyboard widgets like SwitchTo, Start, Cancel for state switching, Calendar for date selection and others.
  • Stateful widgets: Checkbox, Multiselect, Counter, TextInput. They record user actions and allow you to retrieve this data later.
  • Multiple buttons layouts including simple grouping (Group, Column), page scrolling (ScrollingGroup), repeating of same buttons for list of data (ListGroup).
  • Sending media (like photo or video) with fileid caching and handling switching to/from message with no media.
  • Different rules of transitions between windows/dialogs like keeping only one dialog on top of stack or force sending enw message instead of updating one.
  • Offline HTML-preview for messages and transitions diagram. They can be used to check all states without emulating real use cases or exported for demonstration purposes.

Usage

Example below is suitable for aiogram_dialog v2.x and aiogram v3.x

Declaring Window

Each window consists of:

  • Text widgets. Render text of message.
  • Keyboard widgets. Render inline keyboard
  • Media widget. Renders media if need
  • Message handler. Called when user sends a message when window is shown
  • Data getter functions (getter=). They load data from any source which can be used in text/keyboard
  • State. Used when switching between windows

Info: always create State inside StatesGroup

from aiogram.fsm.state import StatesGroup, State
from aiogram_dialog.widgets.text import Format, Const
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog import Window


class MySG(StatesGroup):
    main = State()


async def get_data(**kwargs):
    return {"name": "world"}


Window(
    Format("Hello, {name}!"),
    Button(Const("Empty button"), id="nothing"),
    state=MySG.main,
    getter=get_data,
)

Declaring dialog

Window itself can do nothing, just prepares message. To use it you need dialog:

from aiogram.fsm.state import StatesGroup, State
from aiogram_dialog import Dialog, Window


class MySG(StatesGroup):
    first = State()
    second = State()


dialog = Dialog(
    Window(..., state=MySG.first),
    Window(..., state=MySG.second),
)

Info: All windows in a dialog MUST have states from then same StatesGroup

After creating a dialog you need to register it into the Dispatcher and set it up using the setup_dialogs function:

from aiogram import Dispatcher
from aiogram_dialog import setup_dialogs

...
dp = Dispatcher(storage=storage)  # create as usual
dp.include_router(dialog)
setup_dialogs(dp)

Then start dialog when you are ready to use it. Dialog is started via start method of DialogManager instance. You should provide corresponding state to switch into (usually it is state of first window in dialog).

For example in /start command handler:

async def user_start(message: Message, dialog_manager: DialogManager):
    await dialog_manager.start(MySG.first, mode=StartMode.RESET_STACK)

dp.message.register(user_start, CommandStart())

Info: Always set mode=StartMode.RESET_STACK in your top level start command. Otherwise, dialogs are stacked just as they do on your mobile phone, so you can reach stackoverflow error

aiogram_dialog's People

Contributors

abrikk avatar bomzheg avatar bondarev avatar bralbral avatar chirizxc avatar codwizer avatar darksidecat avatar forden avatar fregat17 avatar gabbhack avatar hum4noidx avatar ismirnov-aligntech avatar ivankarmanow avatar ivankirpichnikov avatar jrootjunior avatar kurokoka551 avatar lamroy95 avatar lubaskinc0de avatar prostmich avatar q-user avatar samwarden avatar searchcore avatar shalmeo avatar t3m8ch avatar tappress avatar tishka17 avatar vladyslav49 avatar vlkorsakov avatar yarqr avatar yuriimotov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aiogram_dialog's Issues

Need help: How to modify user context variables from external task

Hi, I'm working to integrate aiogram-dialog with IoT project. The basic idea is to read temperature from device and show into Telegram bot (It is a MQTT/Telegram integration).

I would like if someone could give me an example to understand how to change an specific user context variable from a task periodic added by me. In my working example I have did it but using global variables, which is not the best solution.
Also I have readed about middleware class but I didn't understand if it has something to do here.
I will appreciate any kind of help.

import asyncio
import logging
import random

from aiogram import Bot, Dispatcher
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher.filters.state import StatesGroup, State
from aiogram.types import Message, CallbackQuery

from aiogram_dialog import Dialog, DialogManager, Window, DialogRegistry, BaseDialogManager, \
    StartMode
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const, Multi, Progress, Format

API_TOKEN = "YOUR TOKEN"

temperature = 0

# ---------------------------------------------------------------------------------

# States
class MainSG(StatesGroup):
    get_temp = State()

# Getter
async def get_bg_data(dialog_manager: DialogManager, **kwargs):
    return {
        "temp": dialog_manager.current_context().dialog_data.get("temp", 0),
    }

async def refresh_temp(c: CallbackQuery, button: Button, manager: DialogManager):
    await manager.update({
            "temp": temperature ,
        })

main_menu = Dialog(
    Window(
        Const("Clik to obtain temperature"),
        Button(Format("Your temp:  {temp} °C"), id="b1", on_click=refresh_temp ),
        state=MainSG.get_temp,
        getter=get_bg_data
    ),
)

async def start(m: Message, dialog_manager: DialogManager):
    await dialog_manager.start(MainSG.get_temp, mode=StartMode.RESET_STACK)
 
logging.basicConfig(level=logging.INFO)
logging.getLogger("aiogram_dialog").setLevel(logging.DEBUG)
storage = MemoryStorage()
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot, storage=storage)
registry = DialogRegistry(dp)
registry.register(main_menu)
dp.register_message_handler(start, text="/start", state="*")

async def main():
    await dp.start_polling()

# MyTask Function that receive temperature and pass to context variables
async def periodic(sleep_for):
    global temperature
    while True:
        ID = 111111111  # Simulated ID from know user linked to device
        # -----------------------------------
        # Here I thin we must add the magic but at the moment I'm using
        # a global variable
        temperature = random.randint(20,35)   # simulate temperature reading
        # -----------------------------------
        await asyncio.sleep(sleep_for)
   
async def runner():
    await asyncio.gather(main(),periodic(2))

if __name__ == '__main__':
    asyncio.run(runner())

Interactive dialogs preview

It is possible to generate HTML preview for all windows, so user can view it from any place without using telegram.

Requirements for user:

  • user must provide example data for getter
  • user must provide example widget states (depends on widget)
  • user must provide example start data for dialog

Requirements for reuslt:

  • Only one window is visible at moment
  • Any window can be accessed by clicking a separate button
  • All text is rendered with provided example data
  • Bot and window parse_mode is used for correct rendering
  • All buttons that switch state must switch window as well. Other actions are not working.
  • Must be a separate mark if window requires text input

[Bug] Multiselect widget is not clickable if min_selected is not null

Example from docs:

import operator

from aiogram_dialog.widgets.kbd import Multiselect
from aiogram_dialog.widgets.text import Format


# let's assume this is our window data getter
async def get_data(**kwargs):
    fruits = [
        ("Apple", '1'),
        ("Pear", '2'),
        ("Orange", '3'),
        ("Banana", '4'),
    ]
    return {
        "fruits": fruits,
        "count": len(fruits),
    }


fruits_kbd = Multiselect(
    Format("✓ {item[0]}"),  # E.g `✓ Apple`
    Format("{item[0]}"),
    id="m_fruits",
    item_id_getter=operator.itemgetter(1),
    items="fruits",
)

So try to set a min_selected parameter:

fruits_kbd = Multiselect(
    Format("✓ {item[0]}"),  # E.g `✓ Apple`
    Format("{item[0]}"),
    id="m_fruits",
    item_id_getter=operator.itemgetter(1),
    items="fruits",
    min_selected=3
)

Bug: if you click on item it will be marked as selected (✓ Apple), second click on item (to disable them) will thrown an error:

aiogram.utils.exceptions.MessageNotModified: Message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message

Object data in text widgets

There is not enough widget with the ability to format text with object data. For instance:

Window(
    FormatFromObj('Hello, {obj.from_user.first_name}')  # obj is instance of CallbackQuery or Message
)

Split html and plain text formatting

  1. New method render_html added to render_text
  2. For Const/Format render_html should do autoescaping.
  3. For Const/Format mode=html disables escaping and forbids rendering as simple text
  4. For Jinja only html is supported
  5. Progressbar works like Format
  6. Window check its parse_mode or bot's default and choses which method of renderig it should call
  7. Keyboards always call render_text

unhandled MessageNotModified if user clicks very fast

MessageNotModified appears on quick presses, not depending on min_selected

File "/home/tishka17/src/aiogram_dialog/aiogram_dialog/dialog.py", line 119, in _callback_handler
await self.show(dialog_manager)
File "/home/tishka17/src/aiogram_dialog/aiogram_dialog/dialog.py", line 104, in show
message = await window.show(self, manager)
File "/home/tishka17/src/aiogram_dialog/aiogram_dialog/window.py", line 66, in show
return await event.message.edit_text(
File "/home/tishka17/src/aiogram_dialog/venv/lib/python3.8/site-packages/aiogram/types/message.py", line 2526, in edit_text
return await self.bot.edit_message_text(
File "/home/tishka17/src/aiogram_dialog/venv/lib/python3.8/site-packages/aiogram/bot/bot.py", line 2216, in edit_message_text
result = await self.request(api.Methods.EDIT_MESSAGE_TEXT, payload)
File "/home/tishka17/src/aiogram_dialog/venv/lib/python3.8/site-packages/aiogram/bot/base.py", line 208, in request
return await api.make_request(self.session, self.server, self.__token, method, data, files,
File "/home/tishka17/src/aiogram_dialog/venv/lib/python3.8/site-packages/aiogram/bot/api.py", line 140, in make_request
return check_result(method, response.content_type, response.status, await response.text())
File "/home/tishka17/src/aiogram_dialog/venv/lib/python3.8/site-packages/aiogram/bot/api.py", line 115, in check_result
exceptions.BadRequest.detect(description)
File "/home/tishka17/src/aiogram_dialog/venv/lib/python3.8/site-packages/aiogram/utils/exceptions.py", line 139, in detect
raise err(cls.text or description)
aiogram.utils.exceptions.MessageNotModified: Message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message

Dialog for user in multiuser chat

в диалогах встроить возможность диалог открыть для юзера или для чата. То есть записать user_id в Intent и при обработке сообщений/кликов проверять его

Required inputs

Do not allow clicking "next" until user fills Rasio/Multiselect or other widgets.

More usecases?

can it work in Python 3.7?

Hi, first THX for the library :-)
i try to use python 3.7 and aiogram-dialog 1.0.2
If i use
from aiogram_dialog import Dialog
i get the error
ImportError: cannot import name 'Protocol' from 'typing'
seems that on line 3 in dialog.py
from typing import Protocol
is used
according to my typing.pyi - file, and some other documentation

# Protocol is only present in 3.8 and later, but mypy needs it unconditionally
Protocol: _SpecialForm = ...

Protocol needs at least python 3.8
ist there any older version that support python 3.7?

Calendar does not support RedisStorage2

# /app/test.py
from aiogram import Bot, Dispatcher, executor
from aiogram.contrib.fsm_storage.redis import RedisStorage2
from aiogram.dispatcher.filters.state import StatesGroup, State
from aiogram.types import Message

from aiogram_dialog import Window, Dialog, DialogRegistry, DialogManager, StartMode
from aiogram_dialog.widgets.kbd import Button
from aiogram_dialog.widgets.text import Const
from datetime import date

from aiogram.types import CallbackQuery

from aiogram_dialog import DialogManager
from aiogram_dialog.widgets.kbd import Calendar


async def on_date_selected(c: CallbackQuery, widget, manager: DialogManager, selected_date: date):
    await c.answer(str(selected_date))


storage = RedisStorage2('redis')
bot = Bot(token='TOKEN')
dp = Dispatcher(bot, storage=storage)
registry = DialogRegistry(dp)


class MySG(StatesGroup):
    main = State()


main_window = Window(
    Const("Hello, unknown person"),
    Calendar(id='calendar', on_click=on_date_selected),
    state=MySG.main,
)
dialog = Dialog(main_window)
registry.register(dialog)


@dp.message_handler(commands=["start"])
async def start(m: Message, dialog_manager: DialogManager):
    await dialog_manager.start(MySG.main, mode=StartMode.RESET_STACK)


if __name__ == '__main__':
    executor.start_polling(dp, skip_updates=True)
sid@83cfd5fbfc97:/app$ python test.py 
Updates were skipped successfully.
Task exception was never retrieved
future: <Task finished name='Task-9' coro=<Dispatcher._process_polling_updates() done, defined at /home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py:409> exception=TypeError('Object of type date is not JSON serializable')>
Traceback (most recent call last):
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py", line 417, in _process_polling_updates
    for responses in itertools.chain.from_iterable(await self.process_updates(updates, fast)):
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py", line 238, in process_updates
    return await asyncio.gather(*tasks)
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/handler.py", line 116, in notify
    response = await handler_obj.handler(*args, **partial_data)
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py", line 259, in process_update
    return await self.message_handlers.notify(update.message)
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/handler.py", line 129, in notify
    await self.dispatcher.middleware.trigger(f"post_process_{self.middleware_key}",
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/middlewares.py", line 53, in trigger
    await app.trigger(action, args)
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/dispatcher/middlewares.py", line 106, in trigger
    await handler(*args)
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram_dialog/context/intent_filter.py", line 66, in on_post_process_message
    await proxy.save_context(data.pop(CONTEXT_KEY))
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram_dialog/context/storage.py", line 45, in save_context
    await self.storage.set_data(
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/contrib/fsm_storage/redis.py", line 307, in set_data
    await redis.set(key, json.dumps(data), expire=self._data_ttl)
  File "/home/sid/.local/lib/python3.9/site-packages/aiogram/utils/json.py", line 43, in dumps
    return json.dumps(data, ensure_ascii=False)
  File "/usr/local/lib/python3.9/json/__init__.py", line 234, in dumps
    return cls(
  File "/usr/local/lib/python3.9/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/local/lib/python3.9/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "/usr/local/lib/python3.9/json/encoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type date is not JSON serializable

Enhancement of ScrollingGroup 📜

Hey there 👋

Currently, the ScrollingGroup doesn't stop to scroll when it gets to the last page.

So instead of stopping and doing nothing, it scrolls through the same page "in-place". Whenever you want to go back --> you have to click the same amount of times.

Would be nice to have no scrolling as a default

[BUG] Inline mode.

If in an answer of inline mode(input_message_content), add the inline button (with registered dialogs), then when it is pressed, middleware will give the error.

File "/home/*****/PycharmProjects/*****/venv/lib/python3.9/site-packages/aiogram_dialog/context/intent_filter.py", line 104, in on_pre_process_callback_query
    chat_id=event.message.chat.id,
AttributeError: 'NoneType' object has no attribute 'chat' 

Temporary solution:
In aiogram_dialog/context/intent_filter.py in on_pre_process_callback_query add the following line

chat_id=event.message.chat.id if hasattr(event, "chat") else ""

Add Photo widget

How about to add new widget Photo (to add photo in Window)?
Using like a:

Dialog launch mode

launch_mod для диалогов, влияющий на стек. Например:

  • SingleInstance - Если в стеке уже есть такой диалог, все вышележащие вместе с ним удаляются и новый кладется поверх
  • SingleTop - если наверху стека такой же диалог как новый, он закрывается перед открытием нового

Get items list from dialog manager or state

First of all, thank you @Tishka17 for your great work.

Description
I want to create something like Store Bucket, where user can write manually item that he needs, and this item that he adds will appear in Store Bucket.

  • Items in bucket limited to 5
  • User can interact with each item in the bucket, interaction means showing the item (just write it name e.x.) and removing the item from bucket
  • From scratch, the bucket for each user empty, and after interaction with a bot, the user can add some items and then manage them.

Questions

  1. How I can interact with StateGroup or data_getter for showing user's bucket if it's empty by default and user should add this item manually?
        Select(
            Format("{item}"),
            items=[], # How to get list of items from state like 'ManageItemsSG.show_items'?
            item_id_getter=lambda x: x,
            id="Item",
            on_click=show_item,
        ),
  1. How to save user's progress after he added few items and for example Cancelled interaction with the dialogue window? Are StartMode.NORMAL will help?
async def show_items(m: Message, dialog_manager: DialogManager):
    # Will StartMode.NORMAL save user's items list?
    await dialog_manager.start(ManageItemsSG.show_items, mode=StartMode.NORMAL)

My code:

import asyncio
import logging

from aiogram import Bot, Dispatcher
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher.filters.state import StatesGroup, State
from aiogram.types import Message

from aiogram_dialog import (
    Dialog, DialogManager, DialogRegistry, Window, StartMode, ChatEvent
)
from aiogram_dialog.widgets.input import MessageInput
from aiogram_dialog.widgets.kbd import (
    Button, Back, Cancel, Row, Select
)
from aiogram_dialog.widgets.text import Const, Format

API_TOKEN = "API_TOKEN"

class ManageItemsSG(StatesGroup):
    show_items = State()
    item_manager = State()
    new_item = State()

async def get_item_list(dialog_manager: DialogManager, **kwargs):
    Item = dialog_manager.current_context().dialog_data.get("Item", None)
    return {
        "Item": Item,
    }

async def add_item_request(c: ChatEvent, select: Select, dialog_manager: DialogManager):
    await dialog_manager.start(ManageItemsSG.new_item)


async def add_item(m: Message, dialog: Dialog, dialog_manager: DialogManager):
    dialog_manager.current_context().dialog_data["Item"] = m.text
    await m.answer(f"Your Item '{m.text}' added")
    await dialog_manager.start(ManageItemsSG.show_items, mode=StartMode.NORMAL)


async def show_item(c: ChatEvent, select: Select, dialog_manager: DialogManager, item_id: str):
    dialog_manager.current_context().dialog_data["Item"] = item_id
    await dialog_manager.dialog().next(dialog_manager)

async def show_item_data(c: ChatEvent, select: Select, dialog_manager: DialogManager):
    Item = dialog_manager.current_context().dialog_data.get("Item", "")
    await c.message.answer(f"Show data for: {Item}")
    await dialog_manager.done()

async def remove_item(c: ChatEvent, select: Select, dialog_manager: DialogManager):
    Item = dialog_manager.current_context().dialog_data.get("Item", "")
    await c.message.answer(f"Item {Item} removed")
    await dialog_manager.done()

dial_items_manager = Dialog(
    Window(
        'Your item list',
        Select(
            Format("{item}"),
            items=[], # How to get list of items from state like 'ManageItemsSG.show_items'?
            item_id_getter=lambda x: x,
            id="Item",
            on_click=show_item,
        ),
        Row(
            Button(
                Const('Add item to list'),
                id="add_item",
                on_click=add_item_request,
            )
        ),
        Cancel(),
        state=ManageItemsSG.show_items,
        getter=get_item_list,
    ),
    Window(
        Format("What to do with Item {Item}?"),
        Row(
            Back(),
            Button(Const("Show Item"), on_click=show_item_data, id="show_item_data"),
            Button(Const("Remove Item"), on_click=remove_item, id="remove_item"),
        ),
        state=ManageItemsSG.item_manager,
        getter=get_item_list,
    ),
    Window(
        Const("Write Item which need to add"),
        MessageInput(add_item),
        state=ManageItemsSG.new_item,
        getter=get_item_list,
    ),
)

async def show_items(m: Message, dialog_manager: DialogManager):
    # Will StartMode.NORMAL save user's items list?
    await dialog_manager.start(ManageItemsSG.show_items, mode=StartMode.NORMAL)

async def main():
    # real main
    logging.basicConfig(level=logging.INFO)
    logging.getLogger("aiogram_dialog").setLevel(logging.DEBUG)
    storage = MemoryStorage()
    bot = Bot(token=API_TOKEN)
    dp = Dispatcher(bot, storage=storage)
    registry = DialogRegistry(dp)
    dp.register_message_handler(show_items, text="/show_items", state="*")
    registry.register(dial_items_manager)

    await dp.start_polling()


if __name__ == '__main__':
    asyncio.run(main())

Thanks for any your response.

List text widget

Widget to render list of items as text. Format must get same variables as in Select button widget.

Usage example:

List(Format("* [{n}] {item.name}"), item="users", sep="\n")

Prototype to start with:

class List(Text):
    def __init__(self, field: str, when: WhenCondition = None):
        super().__init__(when)
        self.field = field

    async def _render_text(self, data: Dict, manager: DialogManager) -> str:
        return "\n".join(data[self.field])

Simplify widgets creation

  • str can be automatically converted to Const (or Format?)
  • list/tuple can be converted to Mutli/Group
  • ItemIdGetter in Select probably can be replaced with Format-like text widget

Graph rendering

Example:

from typing import List, Sequence, Tuple

from aiogram.dispatcher.filters.state import State
from graphviz import Digraph

from aiogram_dialog import Dialog
from aiogram_dialog.widgets.kbd import Group, Back, Cancel, Start, SwitchTo, Next


def edge(dot, state1, state2, *args, **kwargs):
    dot.edge(str(id(state1)), str(id(state2)), *args, **kwargs)


def walk_keyboard(dot, dialog, starts: List[Tuple[State, State]], current_state: State, keyboards: Sequence):
    for kbd in keyboards:
        if isinstance(kbd, Group):
            walk_keyboard(dot, dialog, starts, current_state, kbd.buttons)
        elif isinstance(kbd, Start):
            edge(dot, current_state, kbd.state, label="start")
        elif isinstance(kbd, SwitchTo):
            edge(dot, current_state, kbd.state, label="switch")
        elif isinstance(kbd, Next):
            new_state = dialog.states[dialog.states.index(current_state) + 1]
            edge(dot, current_state, new_state, label="next")
        elif isinstance(kbd, Back):
            new_state = dialog.states[dialog.states.index(current_state) - 1]
            edge(dot, current_state, new_state, label="back")
        elif isinstance(kbd, Cancel):
            for from_, to_ in starts:
                if to_.group == current_state.group:
                    edge(dot, current_state, from_, label="cancel")


def find_starts(current_state, keyboards: Sequence):
    for kbd in keyboards:
        if isinstance(kbd, Group):
            yield from find_starts(current_state, kbd.buttons)
        elif isinstance(kbd, Start):
            yield current_state, kbd.state


def render(dialogs: List[Dialog]):
    dot = Digraph(name='AiogramDialog', engine='fdp')
    dot.attr(label='Aiogram Dialog')
    dot.attr(fontname='Ubuntu')
    dot.attr(fontcolor='#000088')
    dot.attr(fontcolor='#000088')
    for dialog in dialogs:
        with dot.subgraph(name=f"cluster_{dialog.states_group_name()}") as subgraph:
            subgraph.attr(label=dialog.states_group_name())
            subgraph.attr(style='filled', color='#f0f0f0')
            for window in dialog.windows.values():
                subgraph.node(str(id(window.get_state())), window.get_state().state)

    starts = []
    for dialog in dialogs:
        for window in dialog.windows.values():
            starts.extend(find_starts(window.get_state(), [window.keyboard]))

    for dialog in dialogs:
        for window in dialog.windows.values():
            walk_keyboard(dot, dialog, starts, window.get_state(), [window.keyboard])
    dot.render("graph")  # filename will be `graph.pdf`


if __name__ == '__main__':
    render([
        # here put you dialogs
    ])

Deal with callback.answer

Sometimes we need to show alert on button click.
It is better to do automatically.

Also it can be done when clicking only some buttons in select

Index Error in dialogs.py ! next ! method

    async def next(self, manager: DialogManager):
        if not manager.current_context():
            raise ValueError("No intent")
        print(self.states)
        print(self.states.index(manager.current_context().state))
        print(self.states.index(manager.current_context().state) + 1)
        new_state = self.states[self.states.index(manager.current_context().state) + 1]
        await self.switch_to(new_state, manager)

When I'm pressing scroll arrows -> throwing an index error.
Here is the output with the additional print statements

[<State 'Link[0.0.1:1'>, <State 'Link[0.0.1:2'>]
1
2
Task exception was never retrieved
future: <Task finished name='Task-28' coro=<Dispatcher._process_polling_updates() done, defined at /Users/mac/Library/Caches/pypoetry/virtualenvs/custombot-VvSOzSsp-py3.9/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py:409> exception=IndexError('list index out of range')>
Traceback (most recent call last):
  File "/Users/mac/Library/Caches/pypoetry/virtualenvs/custombot-VvSOzSsp-py3.9/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py", line 417, in _process_polling_updates
    for responses in itertools.chain.from_iterable(await self.process_updates(updates, fast)):
  File "/Users/mac/Library/Caches/pypoetry/virtualenvs/custombot-VvSOzSsp-py3.9/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py", line 238, in process_updates
    return await asyncio.gather(*tasks)
  File "/Users/mac/Library/Caches/pypoetry/virtualenvs/custombot-VvSOzSsp-py3.9/lib/python3.9/site-packages/aiogram/dispatcher/handler.py", line 116, in notify
    response = await handler_obj.handler(*args, **partial_data)
  File "/Users/mac/Library/Caches/pypoetry/virtualenvs/custombot-VvSOzSsp-py3.9/lib/python3.9/site-packages/aiogram/dispatcher/dispatcher.py", line 286, in process_update
    return await self.callback_query_handlers.notify(update.callback_query)
  File "/Users/mac/Library/Caches/pypoetry/virtualenvs/custombot-VvSOzSsp-py3.9/lib/python3.9/site-packages/aiogram/dispatcher/handler.py", line 116, in notify
    response = await handler_obj.handler(*args, **partial_data)
  File "/Users/mac/Documents/PycharmProjects/CustomBot/main.py", line 321, in callback
    await dialog_manager.dialog().next(dialog_manager)
  File "/Users/mac/Library/Caches/pypoetry/virtualenvs/custombot-VvSOzSsp-py3.9/lib/python3.9/site-packages/aiogram_dialog/dialog.py", line 78, in next
    new_state = self.states[self.states.index(manager.current_context().state) + 1]
IndexError: list index out of range

last_media_id in stack

last_media_id по идеи должен быть типа MediaId а не int, но вот нужно както с циклическим импортом разобратся

Dynamic defaults for widgets

We want to see some widgets filled on dialog start.

Probably we can pass defaults through window getter or other way for all current existing statefull widgets

Stateless dialogs

Dialogs, that can work without switching global state.

Idea:

  1. Attach Intent id to callback data
  2. Forbid usage of message_handler
  3. Save dialog state separately. Maybe in dialog context as internal data
  4. Store intent outside of current stack. Probably, have multiple stacks

ContextProcessor

Kinda of middleware, that injects data into result of data getter

Including recurring task into loop question

Hi @Tishka17 ! , first of all cograts for your project, I'm very impressed about it.

I 'm new in programming, but may be you can give me a help.
I'm working in an IOT application where I have to create a task named periodic(sleep_for) in which I read a tuple (Telegram ID user , str message) from a queue every 5 seconds and then message is sent to user id.

In this simplified code I have created a task to do something similar.

How could I implement the task execution working under aiogram_dialog ?

Thank you!!

import logging
import asyncio
import aiogram.utils.markdown as md
from aiogram import Bot, Dispatcher, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters import Text
from aiogram.dispatcher.filters.state import State, StatesGroup
from aiogram.types import ParseMode
from aiogram.utils import executor

logging.basicConfig(level=logging.INFO)

# ---------------------------------------------------
API_TOKEN = 'Your TOKEN'
YOUR_ID = xxxxxxx36
# ---------------------------------------------------

bot = Bot(token=API_TOKEN)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)

class Form(StatesGroup):
    name = State()  
    looper = State() 

@dp.message_handler(commands='start')
async def cmd_start(message: types.Message):
    #Conversation's entry point
    await Form.name.set()
    await message.reply("Hi there! What's your name?")

@dp.message_handler(state=Form.name)
async def process_name(message: types.Message, state: FSMContext):
    async with state.proxy() as data:
        data['name'] = message.text
        await message.reply("Your name is:" + str( data['name'] + "\nWrite anything and I will replay your name"))
        await Form.looper.set()
    
@dp.message_handler(state=Form.looper)
async def process_loop(message: types.Message, state: FSMContext):
    async with state.proxy() as data:
        await message.reply("Your name is:" + str( data['name']))
        await Form.looper.set()

async def periodic(sleep_for):
    global YOUR_ID
    counter = 0
    while True:
        msg = "Counter: " + str (counter)
        counter +=1
        await bot.send_message(chat_id=YOUR_ID, text = msg , disable_notification=True)
        await asyncio.sleep(sleep_for)

if __name__ == '__main__':
    loop= asyncio.get_event_loop ()
    loop.create_task (periodic(5))
    executor.start_polling(dp, skip_updates=True)

library globally intercepts callback_query_handler, how fix?

i created a couple of forms, through aiogram_dialog everything works great. But I also want to use the custom shapes I created. By creating a handler @ dp.callback_query_handler (lambda call: True) All callbacks are handled by aiogram_dialog.registry or so. My global @ dp.callback_query_handler (lambda call: True) doesn't fire. How to register my custom callback? The goal is to use a callback for the InlineKeyboardButton

@dp.message_handler()
async def echo(message: types.Message):
    inline = [InlineKeyboardButton('Управление', callback_data='control'),
              InlineKeyboardButton('Настройка', callback_data='settings')]

    markup = InlineKeyboardMarkup(inline, one_time_keyboard=True, resize_keyboard=True)

    await message.answer(message.text, reply_markup=markup)


#this block of code is ignored when callback
@dp.callback_query_handler(lambda call: True)
async def process_callback_button(callback_query: types.CallbackQuery):
    await bot.answer_callback_query(callback_query.id)
    await bot.send_message(callback_query.from_user.id, 'Тест калбек!')



async def main():
        storage = MemoryStorage()
        bot = Bot(token=API_TOKEN)
        dp = Dispatcher(bot, storage=storage)
        dp.register_message_handler(pwd_handler, text="/start", state="*")
        registry = DialogRegistry(dp) #
        registry.register(main_window)
        registry.register(pwd_window)
        await dp.start_polling()

. . .

Documentation

  • Help about each widget type
    • Text
      • Const
      • Format
      • Multi
      • Case
      • Jinja
      • Progress
      • List
    • Keyboard
      • Button
      • Url
      • SwitchState
      • Next, Back
      • Start, Cancel
      • Group, Row
      • Checkbox
      • Select
    • Media
      • StaticMedia
      • DynamicMedia
      • MediaSCroll
    • Inputs:
      • MessageInput
      • TextInput
  • Reply keyboards
  • Working with widget data
  • Working with dialog data
  • Inter-dialog communication (start data, result)
  • Compatibility with normal handlers
  • Background tasks
  • Creating custom widgets
  • Best practice and deisgn patterns

"loading.py" example brakes when write something

I have started to play around this wondeful library. I have found that "loading.py" example brakes when you write something while the progress bar is working, it duplicate the progress bar as response with different values eachones. What could be the problem? I can not find it.

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.