We have 3 operators and I want them to be busy if they entered to chat with someone. When chat is ended with client, operator will be free for other clients and can chat. I accidentally removed the part of code where operator after entering to chat will become busy (is_available=False) and after ending chat becomes available (is_available=True).
Stack: Django, python-telegram-bot
models.py:
from django.db import models
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
buttons.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:nn"
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:nn"
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()
I have tested with one operator and one client everything is working fine, but when I add another operator and more clients messages are messing between them… What I am doing wrong, please help me?!)
2
Answers
It really does come down to global variables and a complete lack of concurrency safety.
PTB has a built-in solution for storing variables like that. I’d probably use
bot_data
for storage in this case, but you should go through the entire article and consider the best option yourself.To manage operator availability and ensure that the messages are routed correctly between clients and operators, you need to handle the
is_available
flag properly. You also need to ensure that each chat is correctly mapped to the respective operator and client.I have made a few modifications to ur code 2manage the is_available status of operators:
move_to_get_reply
function to mark the operator as busy when they start a chatdone_operator
function 2mark operator as available when the chat endsdone_client
function similarly to handle when a client ends the chatlets_chat
function assigns an available operator to the chat and handles multiple operators correctlyI hope this changes will ensure that an operator is marked as busy (
is_available=False
) when they start a chat and available (is_available=True
) once the chat ends. This should resolve the issue of messages getting mixed up between different clients and operators.