@xx_RuBiCoN_xx

Почему хандлер не срабатывает повторно?

Пишу код для чата онлайн поддержки. Схема работы такая: человек нажимает "Задать вопрос оператору", пишет сообщение, оно пересылается всем операторам с инлайн кнопкой "Подключиться к диалогу". Тот кто на неё нажимает активирует два хандлера между пользователем и отозвавшимся оператором. С этого момента начинается диалог. Всё работает корректно, открывается, пересылается, завершается, но если пользователь повторно обращается в поддержку - по нажатию на инлайн кнопку ничего не происходит. Условия вроде как все истинные, т.к. вначале и в конце они совпадают. Получается просто не срабатывает хандлер? Или его что-то останавливает? Какие есть мысли, почему так?

UPD: поправил код на актуальный:
spoiler
operator_mode = {}
user_mode = {}


def supportConnect(bot, message):
   mydb = mysql.connector.connect(
      host="---",
      user="---",
      password="---",
      database="---"
      )
   cursor = mydb.cursor()
   cursor.execute("SELECT user_id FROM operators")
   result = cursor.fetchall()
   mydb.close()
   global operators_id
   operators_id = [row[0] for row in result]
   
   if message.from_user.id in operators_id:
      bot.send_message(message.chat.id, text=text60)
   else:
      global user_help_id
      user_help_id = message.chat.id
      remove_keyboard = types.ReplyKeyboardRemove()
      keyboard = types.InlineKeyboardMarkup()
      keyboard.add(cancel_btn)
      bot.send_message(message.chat.id, text=text48, reply_markup=remove_keyboard)
      bot.send_message(message.chat.id, text=text49, reply_markup=keyboard)
      bot.register_next_step_handler(message, lambda msg: supportWait(bot, msg))

def supportWait(bot, message, user_help_id, operators_id):
   #if user_help_id in user_mode and user_mode[user_help_id] != 'not_in_dialog':
   #   user_mode[user_help_id] = "not_in_dialog"
   for operators in operators_id:
      keyboard = types.InlineKeyboardMarkup()
      keyboard.add(join_dialog_btn)
      bot.send_message(operator, f"Поступил новый запрос в службу поддержки.\n\nUser ID: {message.from_user.id}\nUsername: @{message.from_user.username}\n\nВопрос клиента:\n<b>{message.text}</b>", parse_mode="HTML", reply_markup=keyboard)
   bot.send_message(message.chat.id, text=text50)

   @bot.callback_query_handler(func=lambda call: call.data == 'join_dialog_btn')
   def join_dialog(call):
      # Получаем идентификатор оператора и пользователя
      operator = call.message.chat.id
      # Проверяем, наличие и состояние оператора
      if operator not in operator_mode:
         operator_mode[operator] = "not_in_dialog"

      if user_help_id in user_mode:
         match operator_mode[operator]:
            case 'in_dialog':
               bot.answer_callback_query(call.id, text54)

         match user_mode[user_help_id]:
            case 'in_dialog':
               bot.answer_callback_query(call.id, text55)
            case 'end_dialog':
               bot.answer_callback_query(call.id, text59)

      elif operator in operator_mode:
         match operator_mode[operator]:
            case 'not_in_dialog':
               #for operator in operators_id:
               #   bot.send_message(operator, text=f"@{call.from_user.username} підключився до діалогу\n\nUser ID: {message.from_user.id}\nUsername: @{message.from_user.username}\n\nПитання клієнта:\n<b>{message.text}</b>", parse_mode="HTML")
               # Отправляем сообщение оператору о подключении к диалогу
               markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
               markup.add(btn_dialog_the_end)
               bot.send_message(operator, text53, reply_markup=markup)
               # Отправляем сообщение пользователю о подключении оператора
               bot.send_message(user_help_id, text51, reply_markup=markup)
               # Устанавливаем состояние оператора как "в диалоге"
               operator_mode[operator] = 'in_dialog'
               print(operator_mode)
               # Добавляем пользователя в user_mode
               user_mode[user_help_id] = 'in_dialog'
               print(user_mode)

      # Обработчик сообщений от оператора
      @bot.message_handler(func=lambda message: message.from_user.id == operator)
      def dialog_message_operator(message):
         if message.text == text56:
            #Same menu in start.py
            markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
            markup.add(btn1)
            markup.add(btn2, btn3, btn4)
            markup.add(btn5, btn6)
            bot.send_message(user_help_id, text52, reply_markup=markup)
            bot.send_message(operator, text58, reply_markup=markup)
            operator_mode[operator] = 'not_in_dialog'
            user_mode[user_help_id] = 'end_dialog'
         elif user_mode[user_help_id] and operator_mode[operator] == 'in_dialog':
            operator_text = message.text
            bot.send_message(user_help_id, operator_text)


      # Обработчик сообщений от пользователя
      @bot.message_handler(func=lambda message: message.from_user.id == user_help_id)
      def dialog_message_user(message):
         if message.text == text56:
            #Same menu in start.py
            markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
            markup.add(btn1)
            markup.add(btn2, btn3, btn4)
            markup.add(btn5, btn6)
            bot.send_message(operator, text57, reply_markup=markup)
            bot.send_message(user_help_id, text58, reply_markup=markup)
            operator_mode[operator] = 'not_in_dialog'
            user_mode[user_help_id] = 'end_dialog'
         elif user_mode[user_help_id] and operator_mode[operator] == 'in_dialog':
            user_help_id_text = f"@{message.from_user.username}\n\n{message.text}"
            bot.send_message(operator, user_help_id_text)
  • Вопрос задан
  • 40 просмотров
Решения вопроса 1
@xx_RuBiCoN_xx Автор вопроса
Проблема в наличии строки:
if user_help_id in user_mode and user_mode[user_help_id] != 'not_in_dialog':
       user_mode[user_help_id] = "not_in_dialog"

Удалил - всё заработало
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 2
@hardux
#telegrambot
скорее всего ты использовал в обработке нажатия на кнопку bot.next_step_register_handler, чтобы кнопка в дальнейшем срабатывала используй @bot.callback_query_handler(func=lambda call: True)
def on_click(call):
if call.data == '':
...
И на будущее, прикрепляй код.
Ответ написан
shurshur
@shurshur
Сисадмин, просто сисадмин...
Этот бот очень странно написан, и вообще непонятно, как в нём хоть что-то работает.

Начнём с того, что вот это:

global operator
operator = call.message.chat.id


будет означать, что в переменной operator всегда будет id чата последнего оператора, нажавшего кнопку join_dialog_btn. Аналогично будут проблемы с user_help_id. В итоге не будет это одновременно работать ни с несколькими операторами, ни с несколькими пользователями.

Далее, НЕ НАДО создавать в функции другие функции без чёткого понимания, зачем это нужно, и тем более не надо их оборачивать регистрирующими декораторами. Это работает совсем не так, как кажется.

Когда оператор Вася получит своего первого пользователя, будет вызван декоратор @bot.message_handler(func=lambda message: message.from_user.id == Вася). После того, как Вася отработает по пользователю и получит следующего, будет создан ещё один обработчик с таким же декоратором, но так как один такой обработчик (с такими условиями) уже в цепочке есть, то будет вызван первый, который полагается на локальные переменные от предыдущего вызова supportWait. Довольно заметно, что ты с этим уже сталкивался и именно поэтому локальные переменные (operator и user_help_id) сделал глобальными, но это, разумеется, не поможет, так как абсолютно неправильно.

Что тут надо сделать?

1. Вынести все обработчики на верхний уровень и избавиться от дурной практики регистрации их через декораторы внутри функции каждый раз при их вызове.

2. Отделить логику оператора от логики пользователя - это следует делать отдельными обработчиками со своей логикой.

3. Где-то хранить соответствие chat_id пользователя - chat_id назначенного оператора. Можно завести словарь, но в целом можно и через FSM Телебота (но тогда нельзя будет отслеживать, что пользователя уже кто-то взял).

4. Не забывать, что пользователь может успеть оставить несколько сообщений до того, как оператор примет запрос - их все нужно не потерять.

Можно сделать примерно так:

spoiler
operator_ids = [список id операторов]

@bot.message_handler(func=lambda message: message.chat.id in operator_ids)
def handle_operator_message(message):
# обработка сообщения оператора в состоянии, когда ему ничего не назначено
# можно например ругнуться чтобы ждал появления обращений

# состояния пользователей, в частности чтобы накапливать сообщения от начала обращения
# до назначения оператора
user_states = {}

@bot.message_handler(func=lambda message: True)
def handle_user_message(message):
# обработка сообщения пользователя в состоянии, когда он только что обратился
    # посылаем сообщение с кнопкой всем операторам
    # (задача со звёздочкой: всем операторам, у которых никто не назначен)
    # в кнопке передаём callback_data=join_dialog_btn:CHAT_ID_ПОЛЬЗОВАТЕЛЯ
    # запоминаем сообщение
    user_states[message.chat.id] = {"messages":[message]}
    # затем регистрируем отдельный обработчик
    bot.register_next_step_handler(message, handle_user_unassigned)

# обработчик собирает сообщения пользователя
def handle_user_unassigned(message, user_data):
    user_states[message.chat.id]["messages"].append(message)

# обработчик сообщений пользователя после назначения оператора
# этот обработчик выставится из обработчика сообщения оператора.
# используя register_next_step_handler_by_chat_id
def handle_user_assigned(message, operator_id):
    # посылаем сообщение оператору (operator_id)

def handle_operator_assigned(message, user_id):
    # посылаем сообщение юзеру (user_id)

# в кнопке передаётся chat_id пользователя
# в принципе, можно в кнопку засунуть больше данных, в том числе json до 64 символов
@bot.callback_query_handler(func=lambda call: call.data.startswith('join_dialog_btn:'))
    user_chat_id = int(call.data.split(":")[1])
    for operator_id in operators_id:
        if operator_id != call.chat.id:
             # посылаем остальным операторам "пользователя взял в работу оператор Вася"
    # также выплёвываем все накопленные сообщения пользователя этому оператору
    for msg in user_states[user_chat_id]["messages"]:
         # тут делаем forward_message
    # выставляем обработчик оператору
    bot.register_next_step_handler(message, handler_operator_assigned, user_chat_id)
    # теперь надо переключить пользователя на оператора, используя его chat_id
    bot.register_next_step_handler_by_chat_id(user_chat_id, handle_user_assigned, call.chat.id)
    # обрати внимание, что это выставляет обработчик юзеру, а не оператору!

# в итоге всей этой конструкции в чате operator_id действует обработчик handle_operator_assigned
# с параметром user_id, а в чате user_id - handle_user_assigned с параметром operator_id, и можно
# в них сразу посылать полученные сообщения (только не forward_message, конечно,
# чтобы не светить имя оператора)


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

spoiler
Важно отметить, что полноценные взрослые чат-сервисы делают совершенно не так. В них обычно даже не используется Telegram, вместо этого оператор работает через web-интерфейс (а может и отдельное приложение), в котором можно контролировать всю историю взаимодействия, переключаться между пользователями (нормальная ситуация если оператор ведёт нескольких пользователей параллельно), подтягивать данные из CRM/клиентской базы, интегрировать справочники типовых ответов или чат-ботов, итд итп. Также, например, нормальные обработки таймаутов (если пользователь или оператор не отвечает в нужный срок...)

И делать это лучше, например, на брокерах очередей (типа rabbitmq). Чтобы не нужно было вот этого замороченного собирания сообщений до назначения оператора в какой-то список и не было риска, что пользователь успеет что-то ещё написать в процессе назначения оператору.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы