使用 python-telegram-bot 的 Telegram 聊天机器人 |客户和运营商之间的消息被搞乱了

问题描述 投票:0回答:1

我们有 3 名接线员,如果他们进来与某人聊天,我希望他们很忙。当与客户的聊天结束后,操作员将有空为其他客户提供聊天服务。我不小心删除了代码部分,其中操作员在进入聊天后将变得忙碌(is_available=False),并且在结束聊天后变得可用(is_available=True)。

堆栈:Django、python-telegram-bot

模型.py 从 django.db 导入模型

class CustomUser(models.Model):
    tg_id = models.IntegerField()
    tg_first_name = models.CharField(max_length=500, blank=True, null=True)
    tg_username = models.CharField(max_length=500, blank=True, null=True)
    name = models.CharField(max_length=500, blank=True, null=True)
    choosen_lang = models.CharField(max_length=50, blank=True, null=True)
    phone_number = models.CharField(max_length=50, blank=True, null=True)
    status = models.CharField(max_length=500, blank=True, null=True)
    is_operator = models.BooleanField(default=False, blank=True, null=True)
    is_supervisor = models.BooleanField(default=False, blank=True, null=True)
    is_available = models.BooleanField(default=True, blank=True, null=True)

    def __str__(self):
        return self.tg_first_name
    
class Chat(models.Model):
    is_closed = models.BooleanField(default=False)
    client = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='client_of_chat', blank=True, null=True) 
    operator = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='operator_of_chat', blank=True, null=True)
    created = models.DateTimeField(auto_now_add=True)

class Appeal(models.Model):
    custom_user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, blank=True, null=True)
    body = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.body

按钮.py

from main.models import *

lang_btn = [
    ["O'zbek"],
    ["Русский"],
]

main_page_btn_uz = [
    ["Savol"],
    ["Shikoyat"],
]

main_page_btn_ru = [
    ["Вопрос"],
    ["Жалоба"],
]

chat_btn_uz = [
    ['Onlayn opertor'],
]

chat_btn_ru = [
    ['Онлайн-оператор'],
]

main_btn_operator = [
    ["Открытие чаты"],
    ["Закрытие чаты"],
]

close_chat_btn_ru = [
    ['✅Завершить чат'],
]

close_chat_btn_uz = [
    ['✅Chatni yopish'],
]

bot.py

# internal libs
import os, sys

# django setup
sys.dont_write_bytecode = True
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"

import django
django.setup()

from django.db.models import Q
from test_app.models import *

# logging libs
import logging

# telegram api libs
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
    ConversationHandler,
    MessageHandler,
    filters,
    CallbackQueryHandler,
)

import buttons

# logging
logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logging.getLogger("httpx").setLevel(logging.WARNING)
logger = logging.getLogger(__name__)

# phases
PHASE_LANG_CHOOSING, PHASE_SHARE_CONTACT, PHASE_MAIN_PAGE_UZ, PHASE_MAIN_PAGE_RU, PHASE_MAIN_OPERATOR, PHASE_CLIENT_CHAT, PHASE_OPERATOR_CHAT, PHASE_APPEAL = range(8)

# markups
markup_main_operator = ReplyKeyboardMarkup(buttons.main_btn_operator, one_time_keyboard=True, resize_keyboard=True)
markup_main_uz = ReplyKeyboardMarkup(buttons.main_page_btn_uz, one_time_keyboard=True, resize_keyboard=True)
markup_main_ru = ReplyKeyboardMarkup(buttons.main_page_btn_ru, one_time_keyboard=True, resize_keyboard=True)
markup_lang = ReplyKeyboardMarkup(buttons.lang_btn, one_time_keyboard=True, resize_keyboard=True)
markup_close_chat_uz = ReplyKeyboardMarkup(buttons.close_chat_btn_uz, one_time_keyboard=True, resize_keyboard=True)
markup_close_chat_ru = ReplyKeyboardMarkup(buttons.close_chat_btn_ru, one_time_keyboard=True, resize_keyboard=True)
markup_сhat_uz = ReplyKeyboardMarkup(buttons.chat_btn_uz, one_time_keyboard=True, resize_keyboard=True)
markup_сhat_ru = ReplyKeyboardMarkup(buttons.chat_btn_ru, one_time_keyboard=True, resize_keyboard=True)
# markup_done = ReplyKeyboardMarkup([[KeyboardButton("✅Завершить чат")]], one_time_keyboard=True, resize_keyboard=True)
markup_phone_ru = ReplyKeyboardMarkup([[KeyboardButton("Отправить", request_contact=True)]], one_time_keyboard=True, resize_keyboard=True)
markup_phone_uz = ReplyKeyboardMarkup([[KeyboardButton("Jo\'natish", request_contact=True)]], one_time_keyboard=True, resize_keyboard=True)

# global attrs
ch_id = None
operator = None
client = None
supervisor = None

# starting function
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('start()')
    global operator, client, supervisor

    tg_id = update.message.from_user.id
    check_user = CustomUser.objects.filter(tg_id=tg_id).exists()

    if check_user:
        u = CustomUser.objects.get(tg_id=tg_id)
        if u.is_operator:
            operator = u.tg_id
            await update.message.reply_text(
                "Главнaя страница",
                reply_markup=markup_main_operator,
            )
            return PHASE_MAIN_OPERATOR
        else:
            client = u
            if u.choosen_lang:
                if u.phone_number:
                    if u.choosen_lang == "O'zbek":
                        await update.message.reply_text(
                            "Asosiy sahifa",
                            reply_markup=markup_main_uz,
                        )
                        return PHASE_MAIN_PAGE_UZ
                    await update.message.reply_text(
                        "Главная страница",
                        reply_markup=markup_main_ru,
                    )
                    return PHASE_MAIN_PAGE_RU
                else:
                    if u.choosen_lang == "O'zbek":
                        await update.message.reply_text(
                                "Telefon raqamingizni jo'nating",
                                reply_markup=markup_phone_uz
                            )
                        return PHASE_SHARE_CONTACT
                    else:
                        await update.message.reply_text(
                                "Отправьте номер телефона",
                                reply_markup=markup_phone_ru
                            )
                        return PHASE_SHARE_CONTACT
            else:
                await update.message.reply_text(
                    f"Assalomu alaykum {update.message.from_user.first_name}, xush kelibsiz!\n"
                    "Tilni tanlang:\n\n"
                    f"Здравствуйте {update.message.from_user.first_name}, добро пожаловать!\n"
                    "Выберите язык:",
                    reply_markup=markup_lang,
                )
                return PHASE_LANG_CHOOSING
    else:
        u = CustomUser.objects.create(
            tg_id=update.message.from_user.id,
            tg_first_name=update.message.from_user.first_name,
            tg_username=update.message.from_user.username,
        )
        client = u
        await update.message.reply_text(
            f"Assalomu alaykum {update.message.from_user.first_name}, xush kelibsiz!\n"
            "Tilni tanlang:\n\n"
            f"Здравствуйте {update.message.from_user.first_name}, добро пожаловать!\n"
            "Выберите язык:",
            reply_markup=markup_lang,
        )
        return PHASE_LANG_CHOOSING

# language choosing and telephone number submission
async def lang_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('lang_choice()')
    u = CustomUser.objects.get(tg_id=update.message.from_user.id)
    u.choosen_lang = update.message.text
    u.save()

    if update.message.text == "O'zbek":
        await update.message.reply_text("Telefon raqamingizni jo'nating", reply_markup=markup_phone_uz)
        return PHASE_SHARE_CONTACT
    elif update.message.text == "Русский":
        await update.message.reply_text("Отправьте номер телефона", reply_markup=markup_phone_ru)
        return PHASE_SHARE_CONTACT

async def get_phone(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('get_phone()')
    u = CustomUser.objects.get(tg_id=update.message.from_user.id)
    u.phone_number = update.message.contact.phone_number
    u.save()

    if u.choosen_lang == "O'zbek":
        await update.message.reply_text(
            "Asosiy sahifa",
            reply_markup=markup_main_uz,
        )
        return PHASE_MAIN_PAGE_UZ
    await update.message.reply_text(
        "Главная страница",
        reply_markup=markup_main_ru,
    )
    return PHASE_MAIN_PAGE_RU

async def get_phone_wrong(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('get_phone_wrong()')
    global client

    if client.choosen_lang == "O'zbek":
        await update.message.reply_text(
            "Iltimos raqamingizni \"jo'natish\" tugmasi orqali yuboring!",
            reply_markup=markup_phone_uz,
        )
        return PHASE_SHARE_CONTACT
    await update.message.reply_text(
        "Пожалуйста отправьте номер телефона вместо текста!",
        reply_markup=markup_phone_ru,
    )
    return PHASE_SHARE_CONTACT

# main menu for operators
async def open_chats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('open_chats()')
    return PHASE_MAIN_OPERATOR

async def closed_chats(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('closed_chats()')
    return PHASE_MAIN_OPERATOR

# chat
async def move_to_get_reply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('move_to_get_reply()')
    global ch_id
    global operator
    query = update.callback_query
    await query.answer()

    # ch_id = query.data
    
    if ch_id is not None:
        created_chat = Chat.objects.get(id=ch_id)
        created_chat.operator = CustomUser.objects.get(tg_id=operator)
        created_chat.save()

        await context.bot.send_message(created_chat.operator.tg_id, 'Чат открыт, напишите ...', reply_markup=markup_close_chat_ru)

        if created_chat.client.choosen_lang == "O'zbek":
            await context.bot.send_message(created_chat.client.tg_id, 'Assalomu alaykum, nima yordam bera olaman?', reply_markup=markup_close_chat_uz)
        else:
            await context.bot.send_message(created_chat.client.tg_id, 'Здравствуйте, чем могу помочь?', reply_markup=markup_close_chat_ru)

        return PHASE_OPERATOR_CHAT
    else:
        await context.bot.send_message(created_chat.operator.tg_id, 'Клиент уже завершил чат.', reply_markup=markup_main_operator)
        return PHASE_MAIN_OPERATOR


async def operator_chat(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('operator_chat()')
    global ch_id
    ch = Chat.objects.get(id=ch_id)
    client_tg = ch.client.tg_id

    await context.bot.send_message(client_tg, update.message.text)
    
    return PHASE_OPERATOR_CHAT

async def lets_chat(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('lets_chat()')
    global ch_id

    created_chat = Chat.objects.create(
        is_closed=False,
        client=CustomUser.objects.get(tg_id=update.message.from_user.id),
    )
    
    ch_id = created_chat.id
    keyboard = [
        [
            InlineKeyboardButton("Присоединиться к чату", callback_data=ch_id),
        ],
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    
    available_operator = CustomUser.objects.filter(is_operator=True, is_available=True).first()

    notification_message = (
        f"<u>Появился новый чат!</u>\n"
        f'\n'
        f'<b>Имя пользователя:</b> <i>{update.message.from_user.first_name}</i>'
        f'\n'
        f'<b>Обрашения:</b> <i>{update.message.text}</i>' 
    )
    if available_operator:
        await context.bot.send_message(available_operator.tg_id, notification_message, reply_markup=reply_markup, parse_mode='HTML')

    if created_chat.client.choosen_lang == "O'zbek":
        await update.message.reply_text("Iltimos, kutib turing! Biz sizni mavjud operator bilan bog'layabmiz.", reply_markup=markup_close_chat_uz,)
    else:
        await update.message.reply_text("Пожалуйста, ожидайте! Мы соединяем Вас со свободным оператором.", reply_markup=markup_close_chat_ru,)
    
    return PHASE_CLIENT_CHAT

async def chat(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('chat()')
    global ch_id

    if ch_id is None:
        await update.message.reply_text("Чат не найден.", reply_markup=markup_main_ru)
        return PHASE_MAIN_PAGE_RU

    try:
        ch = Chat.objects.get(id=ch_id)
    except Chat.DoesNotExist:
        await update.message.reply_text("Чат не найден.", reply_markup=markup_main_ru)
        return PHASE_MAIN_PAGE_RU

    if ch.operator is None:
        await context.bot.send_message(ch.client.tg_id, "Пожалуйста подождите, оператор скоро подключится!")
    else:
        await context.bot.send_message(ch.operator.tg_id, update.message.text)

    return PHASE_CLIENT_CHAT

# question and chat 
async def question(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('question()')
    global client

    if client.choosen_lang == "O'zbek":
        await update.message.reply_text(
            'Bu yerda barcha "Ko\'p berildagan savollar" bo\'ladi...',
            # reply_markup=markup_сhat_ru,
        )
        await update.message.reply_text(
            'Savollaringiz qoldimi? "Onlayn operator" tugmasini bosing va operator siz bilan bog\'lanadi!',
            reply_markup=markup_сhat_ru,
        )
        return PHASE_MAIN_PAGE_UZ
    
    # await update.message.reply_text(
    #         'Здесь будет все "Часто задаваемые вопросы"...',
    #         # reply_markup=markup_сhat_ru,
    #     )

    await update.message.reply_html(
"""
Some text
"""
    )
    await update.message.reply_text(
            'Выше указаны все вопросы и ответы раздела FAQ. Если не нашли ответ, нажмите на кнопку "Онлайн-оператор" и оператор вам ответит!',
            reply_markup=markup_сhat_ru,
        )
    return PHASE_MAIN_PAGE_RU

# appeal
async def appeal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('appeal()')
    global client

    if client.choosen_lang == "O'zbek":
        update.message.reply_text("Shikoyat mazmunini yozib jo'nating", reply_markup=ReplyKeyboardRemove(),)
        return PHASE_APPEAL

    await update.message.reply_text("Отправьте вашy жалобу", reply_markup=ReplyKeyboardRemove(),)
    return PHASE_APPEAL

async def get_appeal(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('get_appeal()')
    global client

    Appeal.objects.create(
        custom_user = client,
        body = update.message.text
    )

    if client.choosen_lang == "O'zbek":
        update.message.reply_text("Shikoyat yuborildi!", reply_markup=markup_main_uz,)
        return PHASE_MAIN_PAGE_UZ

    await update.message.reply_text("Жалоба принята, спасибо!", reply_markup=markup_main_ru,)
    return PHASE_MAIN_PAGE_RU

# wrong selection
async def wrong(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('wrong()')
    await update.message.reply_text("Выберите из списка.", reply_markup=markup_main_ru,)

    return PHASE_MAIN_PAGE_RU

# end conversationhandler function
async def done(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('done()')
    global ch_id, operator, client, supervisor

    ch_id = None
    operator = None
    client = None
    supervisor = None

    await update.message.reply_text('Rahmat!', reply_markup=ReplyKeyboardRemove(),)
    return ConversationHandler.END

# end chat functions
async def done_operator(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('done_operator()')
    global ch_id
    await update.message.reply_text("Вы завершили чат.", reply_markup=markup_main_operator)

    try:
        chat = Chat.objects.get(id=ch_id)
    except Chat.DoesNotExist:
        # await update.message.reply_text("Chat not found.")
        print('Chat not found')

    chat.is_closed = True
    chat.save()

    client_tg_id = chat.client.tg_id
    await context.bot.send_message(client_tg_id, 'Оператор завершил чат', reply_markup=markup_main_ru,)

    # Reset the ch_id after chat is done
    ch_id = None

    return PHASE_MAIN_OPERATOR

async def done_client(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    print('done_client()')
    global ch_id
    await update.message.reply_text("Вы завершили чат.", reply_markup=markup_main_ru)

    try:
        chat = Chat.objects.get(id=ch_id)
    except Chat.DoesNotExist:
        await update.message.reply_text("Chat not found.")
        print('Chat not found')

    chat.is_closed = True
    chat.save()

    if chat.operator is None:
        pass
    else:
        await context.bot.send_message(chat.operator.tg_id, 'Клиент завершил чат', reply_markup=markup_main_operator)

    # Reset the ch_id after chat is done
    ch_id = None

    return PHASE_MAIN_PAGE_RU

# main function
def main() -> None:
    application = Application.builder().token("token").build()

    conv_handler = ConversationHandler(
        entry_points=[CommandHandler("start", start)],
        states={
            PHASE_LANG_CHOOSING: [
                MessageHandler(filters.Regex("^(O'zbek|Русский)$"), lang_choice),
            ],
            PHASE_SHARE_CONTACT: [
                MessageHandler(filters.CONTACT, get_phone,),
                MessageHandler(filters.TEXT, get_phone_wrong,),
                # MessageHandler(filters.ALL, wrong,),
            ],
            PHASE_APPEAL: [
                MessageHandler(filters.TEXT, get_appeal,),
                MessageHandler(filters.ALL, wrong,),
            ],
            PHASE_MAIN_PAGE_UZ: [
                MessageHandler(filters.Regex("^Savol$"), question,),
                MessageHandler(filters.Regex("^Shikoyat$"), appeal,),
                MessageHandler(filters.Regex("^Onlayn opertor$"), lets_chat,),
                # MessageHandler(filters.ALL, wrong,),
            ],
            PHASE_MAIN_PAGE_RU: [
                MessageHandler(filters.Regex("^Вопрос$"), question,),
                MessageHandler(filters.Regex("^Жалоба$"), appeal,),
                MessageHandler(filters.Regex("^Онлайн-оператор$"), lets_chat,),
                # MessageHandler(filters.ALL, wrong,),
            ],
            PHASE_MAIN_OPERATOR: [
                MessageHandler(filters.Regex("^✅Завершить чат$"), done_operator,),
                MessageHandler(filters.Regex("^Открытие чаты$"), open_chats,),
                MessageHandler(filters.Regex("^Закрытие чаты$"), closed_chats,),
                CallbackQueryHandler(move_to_get_reply),
                # MessageHandler(filters.ALL, wrong,),
            ],
            PHASE_OPERATOR_CHAT: [
                MessageHandler(filters.Regex("^✅Завершить чат$"), done_operator,),
                MessageHandler(filters.Regex("^Открытие чаты$"), open_chats,),
                MessageHandler(filters.Regex("^Закрытие чаты$"), closed_chats,),
                MessageHandler(filters.TEXT, operator_chat,),
                CallbackQueryHandler(move_to_get_reply),
                # MessageHandler(filters.ALL, wrong,),
            ],
            PHASE_CLIENT_CHAT: [
                MessageHandler(filters.Regex("^✅Завершить чат$"), done_client,),
                MessageHandler(filters.Regex("^✅Chatni yopish$"), done_client,),
                MessageHandler(filters.Regex("^Вопрос$"), question,),
                MessageHandler(filters.Regex("^Жалоба$"), appeal,),
                MessageHandler(filters.Regex("^Savol$"), question,),
                MessageHandler(filters.Regex("^Shikoyat$"), appeal,),
                MessageHandler(filters.TEXT, chat,),
                # MessageHandler(filters.ALL, wrong,),
            ],
        },
        fallbacks=[CommandHandler("cancel", done)],
    )

    application.add_handler(conv_handler)
    application.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
    main()

我已经对一名操作员和一名客户端进行了测试,一切工作正常,但是当我添加另一名操作员和更多客户端消息时,他们之间的消息变得混乱......我做错了什么,请帮助我?!)

python django telegram python-telegram-bot
1个回答
0
投票

这确实归结为全局变量和完全缺乏并发安全性。 PTB 有一个内置解决方案用于存储此类变量。在这种情况下,我可能会使用

bot_data
进行存储,但您应该阅读整篇文章并自己考虑最佳选择。

© www.soinside.com 2019 - 2024. All rights reserved.