Сообщить об ошибке.

Хранение временных данных модулем python-telegram-bot

Хранение данных, связанных с ботом, пользователем и чатом

Внимание! Пакеты python-telegram-bot версии 13.x будут придерживаться многопоточной парадигмы программирования (*на данный момент актуальна версия 13.15). Пакеты версий 20.x и новее предоставляют чистый асинхронный Python интерфейс для Telegram Bot API. Дополнительно смотрите основные изменения в пакете python-telegram-bot версии 20.x.

Иногда необходимо временно сохранить некоторую информацию о текущем пользователе и/или чате для дальнейшего использования. Примером этого может быть бот для опроса, который задает пользователю ряд вопросов один за другим и сохраняет их в базе данных.

Контексты bot_data, user_data и chat_data.

Расширение telegram.ext предоставляет встроенное решение для этой общей задачи.

Пример для python-telegram-bot версии 13.x:

from uuid import uuid4
from telegram.ext import Updater, CommandHandler

def put(update, context):
    """Usage: /put value"""
    # генерируем идентификатор
    key = str(uuid4())
    # Здесь не используется context.args, 
    # т.к. значение может содержать пробелы.
    value = update.message.text.partition(' ')[2]

    # сохраняем значение в контекст
    context.user_data[key] = value
    # отправляем ключ пользователю
    update.message.reply_text(key)

def get(update, context):
    """Usage: /get uuid"""
    # отделяем идентификатор от команды
    key = context.args[0]

    # загружаем значение и отправляем пользователю
    value = context.user_data.get(key, 'Not found')
    update.message.reply_text(value)

if __name__ == '__main__':
    updater = Updater('TOKEN', use_context=True)
    dp = updater.dispatcher
    dp.add_handler(CommandHandler('put', put))
    dp.add_handler(CommandHandler('get', get))
    updater.start_polling()
    updater.idle()

Тоже самый пример для асинхронного python-telegram-bot версии 20.x:

Изменения кода для асинхронного python-telegram-bot версии 20.x смотрите в обзорном материале в подразделе "Асинхронный модуль расширения telegram.ext (версия 20.x)".

from uuid import uuid4
from telegram.ext import Application, CommandHandler

async def put(update, context): # асинхронная функция
    key = str(uuid4())
    value = update.message.text.partition(' ')[2]
    # операция не связана с сетевым подключением
    context.user_data[key] = value
    # операция связана с сетевым подключением
    # добавляем оператор `await`
    await update.message.reply_text(key)

async def get(update, context): # асинхронная функция
    key = context.args[0]
    value = context.user_data.get(key, 'Not found')
    # операция связана с сетевым подключением
    # добавляем оператор `await`
    await update.message.reply_text(value)

if __name__ == '__main__':
    # экземпляр приложения создается по новому
    application = Application.builder().token('TOKEN').build()
    application.add_handler(CommandHandler('put', put))
    application.add_handler(CommandHandler('get', get))
    application.run_polling()

Используя контекст context.user_data в любом обратном вызове обработчика, можно получить доступ к пользовательскому словарю dict.

Каждый раз, когда бот получает сообщение, обработчик этого сообщения находит или создает context.user_data пользователя, отправившего сообщение. Этот словарь используется всеми обработчиками бота.

Словарь context.chat_data работает точно так же, как context.user_data, за исключением того, что он создается для каждого чата, а не для каждого пользователя.

Начиная с версии библиотеки 12.4, также предоставляется context.bot_data и работает точно так же, как user_data, за исключением того, что это единственный словарь для вашего бота.

Примечания и советы.

  • Все хранится в памяти. Это означает, что все bot_data, user_data и chat_data удаляются после завершения процесса бота. Если это нежелательно, то загляните на страницу сохранения.
  • Если bot_data, user_data и chat_data не пустые, то они будут храниться до завершения процесса.
  • user_data и chat_data - это разные словари даже для приватных чатов.
  • новое значение для словарей bot_data, user_data или chat_data присвоить нельзя!!! Вместо вызовов context.user_data = {} и context.user_data = other_dict используйте context.user_data.clear() и/или context.user_data.update(other_dict) соответственно.

Миграция чата.

Если групповой чат переходит в супер-группу, то идентификатор чата изменится. Поскольку словари context.chat_data хранятся для каждого идентификатора чата, следовательно необходимо перенести данные на новый идентификатор. Вот две ситуации, с которыми можно столкнуться:

Обновления статуса, отправленные Telegram.

Когда группа мигрирует, то Telegram отправит обновление, в котором просто указана новая информация. Чтобы их отловить, просто определите соответствующий обработчик:

def chat_migration(update, context):
    m = update.message
    dp = context.dispatcher
    # Получим старые и новые идентификаторы чата
    old_id = m.migrate_from_chat_id or m.chat_id
    new_id = m.migrate_to_chat_id or m.chat_id
    # передача данных, если старые данные все еще присутствуют
    if old_id in dp.chat_data:
        dp.chat_data[new_id].update(dp.chat_data.get(old_id))
        del dp.chat_data[old_id]
...

def main():
    updater = Updater("TOKEN", use_context=True)
    dp = updater.dispatcher # available since version 12.4

    dp.add_handler(MessageHandler(Filters.status_update.migrate, chat_migration))
...

Пример для асинхронного python-telegram-bot версии 20.x:

async def chat_migration(update, context):
    message = update.message
    application = context.application
    application.migrate_chat_data(message=message)
...

def main():
    application = Application.builder().token('TOKEN').build()
    application.add_handler(
        MessageHandler(filters.StatusUpdate.MIGRATE, chat_migration)
    )
...

Для полной уверенности, что обновление будет обработано этим обработчиком, либо сначала добавьте его, либо поместите в отдельную группу.

Ошибки ChatMigrated.

Если попробовать, например, отправить сообщение на старый идентификатор чата, то Telegram ответит неверным запросом, включая новый идентификатор чата. Можно получить к нему доступ с помощью обработчика ошибок:

def error(update, context):
    """Ошибки журнала, вызванные обновлениями."""
    logger.warning('Update "%s" caused error "%s"', update, context.error)

    if isinstance(context.error, ChatMigrated):
        new_chat_id = context.error.new_chat_id

Тоже самое для асинхронного python-telegram-bot версии 20.x:

async def error(update, context): # асинхронная функция
    """Ошибки журнала, вызванные обновлениями."""
    logger.warning('Update "%s" caused error "%s"', update, context.error)

    if isinstance(context.error, ChatMigrated):
        new_chat_id = context.error.new_chat_id

К сожалению, Telegram не передает старый идентификатор чата, поэтому в настоящее время нет простого способа выполнить передачу данных, как указано выше, в обработчике ошибок. Так что убедитесь, что ловите обновления статуса! Тем не менее, можно заключить свои запросы в предложение try/except:

def my_callback(update, context):
    # доступно с версии 12.4
    dp = context.dispatcher
    ...

    try:
        context.bot.send_message(chat_id, text)
    except ChatMigrated as e:
        new_id = e.new_chat_id

        # Отправить повторно на новый идентификатор чата
        context.bot.send_message(new_id, text)

        # Передача данных
        if chat_id in dp.chat_data:
            dp.chat_data[new_id].update(dp.chat_data.get(chat_id))
            del dp.chat_data[chat_id]
    ...

Пример для асинхронного python-telegram-bot версии 20.x:

async def my_callback(update, context):
    application = context.application
    ...

    try:
        # сетевое соединение - добавляем `await`
        await context.bot.send_message(chat_id, text)
    except ChatMigrated as exc:
        new_id = exc.new_chat_id

        # сетевое соединение - добавляем `await`
        await context.bot.send_message(new_id, text)

        # Получение старых и новых идентификаторов чата
        old_id = message.migrate_from_chat_id or message.chat_id
        new_id = message.migrate_to_chat_id or message.chat_id

        # Передача данных, только если старые данные все еще присутствуют, 
        # этот шаг важен, так как Telegram отправляет два обновления о переносе
        if old_id in application.chat_data:
        application.migrate_chat_data(
            old_chat_id=old_id,
            new_chat_id=new_id
        )
    ...