pythonasynchronouspython-asynciotelegramtelethon

How to use context variables correctly in Telethon?


I'm developing a Telegram bot using the Telethon library.

I want to pass the value of tags_cv from send_tag_list() to show_selected_tags(). But I get this error:

`ERROR - Unhandled exception on show_selected_tags
Traceback (most recent call last):
  File "/home/kozorez/.cache/pypoetry/virtualenvs/content-telegram-bot-4LA9pChh-py3.10/lib/python3.10/site-packages/telethon/client/updates.py", line 570, in _dispatch_update
    await callback(event)
  File "/mnt/c/Users/kozor/PycharmProjects/content-telegram-bot/bot/bot.py", line 77, in show_selected_tags
    some_var = tags_cv.get()
LookupError: <ContextVar name='tags' at 0x7f7e3e222de0>`

I suppose, show_selected_tags() doesn't receive the value of tags_cv. Perhaps Telethon events run in different asynchronous contexts?

bot.py

"""Логика взаимодействия с ботом"""

import logging
import contextvars
from telethon.sync import TelegramClient, events

from services.channel import get_channel_data, add_channel_to_db
from services.post import parse_posts
from services.tag import get_tags_list
from services.keyboard import create_tags_keyboard

from config import (
    api_id,
    api_hash,
    bot_token
)


bot = TelegramClient(
    'bot',
    api_id,
    api_hash).start(bot_token=bot_token)


channel_data_cv = contextvars.ContextVar('channel_data')
tags_cv = contextvars.ContextVar('tags')


@bot.on(events.NewMessage(pattern='/start'))
async def send_welcome(event) -> None:
    """Отправка приветственного сообщения"""

    await event.reply('Привет! Я — бот, который поможет тебе составить пост с навигацией по твоему телеграм-каналу. \
                      \nПросто пришли мне ссылку на свой телеграм-канал.')
    

@bot.on(events.NewMessage(pattern='https://t\.me/(\S+)'))
async def send_tag_list(event) -> None:
    """Анализируем посты из канала и возвращаем клавиатуру с тегами"""

    channel_link = event.text
    tags = None

    try:
        channel_data = await get_channel_data(channel_link)
        channel_data_cv.set(channel_data)
    except ValueError as error:
        if 'Cannot get entity from a channel' in str(error):
            await event.reply('Канал должен быть публичным')
        logging.error(error)

    try:
        await add_channel_to_db(channel_data)
    except Exception as error:
        logging.error(error)
        
    try:     
        await event.reply('Посты анализируются, ожидайте')
        await parse_posts(channel_link, channel_data)
    except ValueError as error:
        logging.error(error)
        await event.reply('К сожалению, мне не удалось найти ни одного тега')

    try:
        tags = await get_tags_list(channel_data)
        tags_cv.set(tags)
    except Exception as error:
        logging.error(error)

    tags_keyboard = await create_tags_keyboard(tags)

    await event.respond("Выберите теги", buttons=tags_keyboard)


@bot.on(events.CallbackQuery())
async def show_selected_tags(event) -> None:
    some_var = tags_cv.get()
    print(some_var)

run.py

"""Запуск бота"""

import asyncio
import logging

from bot import bot

from db.models import *

from config import (
    engine,
    client
    )



loop = asyncio.get_event_loop()
asyncio.set_event_loop(loop)



async def init_db() -> None:
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        await conn.run_sync(Base.metadata.create_all)
        logging.info('База данных инициализирована')


async def start() -> None:
    await init_db()

    async with bot:
        await bot.run_until_disconnected()
        logging.info('Бот запущен')

    async with client:
        await client.run_until_disconnected()
        logging.info('Клиент запущен')


if __name__ == "__main__":
    loop.create_task(start())
    loop.run_forever()

Be honest, I don't understand how to fix this. The contextvars guide is too simple to make a mistake... What else besides context variables can be used? Maybe caching?

I would appreciate your help.


Solution

  • By default, Telethon creates a new task for each update. Context variables are attached to the context of a task. If the task changes, the context changes, and you won't have the variable.

    In v1, you can enable sequential_updates to use a single task for all updates:

    client = TelegramClient(..., sequential_updates=True)
    

    This would mean all update handlers use the same task.