Загрузка...

Скрипт [FREE] Бот тех. поддержки - Создание топиков в чате

Тема в разделе Python создана пользователем verply 4 июн 2025 в 00:42. 94 просмотра

Загрузка...
  1. verply
    verply Автор темы 4 июн 2025 в 00:42 3 29 май 2025
    Телеграм-бот технической поддержки
    Со стороны пользователя:
    Запрашивает тему обращения

    Категорию обращения
    Опсание обращения
    Приоритет

    Медиа-файл(опционально)
    Действия бота:
    Создает топик в супер-группе(вкл функция "Темы" в настройках чата)
    Устанавливает название с UUID обращения
    Отправляет краткую сводку обращения(Тема, опсание, приоритет, медиа)
    Со стороны администратора:
    Пишет в топик - бот передает пользователю
    Пользователь пишет в бота - бот передает в топик

    Команды:
    /closed_yes - Закрыть обращение со статусом "Решено"
    /closed_no - Закрыть обращение со статусом "Не решено"
    /reassign username - Переназначить обращение другому администратору
    /remind - Отправить напоминание пользователю
    /search текст - Поиск обращений

    Скриншотики:
    [IMG]
    [IMG][IMG][IMG][IMG][IMG][IMG]
    Python
    import asyncio
    import json
    import os
    import logging
    from enum import Enum
    from dataclasses import dataclass
    from typing import Optional, List, Dict, Any
    from aiogram import Bot, Dispatcher, Router
    from aiogram.filters import Command, CommandStart
    from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, ContentType
    from aiogram.fsm.context import FSMContext
    from aiogram.fsm.state import State, StatesGroup
    from aiogram.fsm.storage.memory import MemoryStorage
    from aiogram import F
    from aiogram.types import Update
    import uuid
    from dotenv import load_dotenv
    from datetime import datetime

    logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    logger = logging.getLogger(__name__)

    load_dotenv()
    BOT_TOKEN = os.getenv("BOT_TOKEN")
    SUPPORT_CHAT_ID = int(os.getenv("SUPPORT_CHAT_ID"))
    ADMIN_IDS = [int(admin_id) for admin_id in os.getenv("ADMIN_IDS", "").split(",") if admin_id]

    if not all([BOT_TOKEN, SUPPORT_CHAT_ID, ADMIN_IDS]):
    raise ValueError("Missing required environment variables")

    class TicketStatus(Enum):
    PENDING = " В обработке"
    RESOLVED = " Решено"
    UNRESOLVED = " Не решено"

    class TicketPriority(Enum):
    LOW = " Низкий"
    MEDIUM = " Средний"
    HIGH = " Высокий"

    class TicketCategory(Enum):
    GENERAL = " Общий вопрос"
    TECHNICAL = " Техническая проблема"
    BILLING = " Оплата"
    FEATURE = " Предложение"
    OTHER = " Другое"

    @dataclass
    class Ticket:
    id: str
    user_id: int
    username: str
    first_name: str
    topic: str
    description: str
    media: List[Dict[str, str]] = None
    status: TicketStatus = TicketStatus.PENDING
    priority: TicketPriority = TicketPriority.MEDIUM
    category: TicketCategory = TicketCategory.GENERAL
    thread_id: Optional[int] = None
    created_at: str = None

    class TicketStorage:
    def __init__(self, file_path: str = "tickets.json"):
    self.file_path = file_path
    self.tickets: List[Ticket] = self.load_tickets()

    def load_tickets(self) -> List[Ticket]:
    try:
    if os.path.exists(self.file_path):
    with open(self.file_path, 'r', encoding='utf-8') as f:
    content = f.read().strip()
    if not content:
    return []
    data = json.loads(content)
    return [Ticket(
    id=ticket['id'],
    user_id=ticket['user_id'],
    username=ticket['username'],
    first_name=ticket['first_name'],
    topic=ticket['topic'],
    description=ticket['description'],
    media=ticket.get('media', []),
    status=TicketStatus(ticket['status']),
    priority=TicketPriority(ticket['priority']),
    category=TicketCategory(ticket['category']),
    thread_id=ticket.get('thread_id'),
    created_at=ticket.get('created_at')
    ) for ticket in data]
    except (json.JSONDecodeError, ValueError) as e:
    logger.error(f"Error loading tickets: {e}")
    return []
    except Exception as e:
    logger.error(f"Unexpected error loading tickets: {e}")
    return []
    return []

    def save_tickets(self):
    try:
    with open(self.file_path, 'w', encoding='utf-8') as f:
    json.dump([{
    'id': ticket.id,
    'user_id': ticket.user_id,
    'username': ticket.username,
    'first_name': ticket.first_name,
    'topic': ticket.topic,
    'description': ticket.description,
    'media': ticket.media,
    'status': ticket.status.value,
    'priority': ticket.priority.value,
    'category': ticket.category.value,
    'thread_id': ticket.thread_id,
    'created_at': ticket.created_at
    } for ticket in self.tickets], f, ensure_ascii=False, indent=2)
    except Exception as e:
    logger.error(f"Error saving tickets: {e}")
    raise

    def add_ticket(self, ticket: Ticket):
    self.tickets.append(ticket)
    self.save_tickets()

    def get_user_tickets(self, user_id: int) -> List[Ticket]:
    return [ticket for ticket in self.tickets if ticket.user_id == user_id]

    def get_ticket_by_id(self, ticket_id: str) -> Optional[Ticket]:
    return next((ticket for ticket in self.tickets if ticket.id == ticket_id), None)

    def get_ticket_by_thread_id(self, thread_id: int) -> Optional[Ticket]:
    return next((ticket for ticket in self.tickets if ticket.thread_id == thread_id), None)

    def get_latest_pending_ticket(self, user_id: int) -> Optional[Ticket]:
    user_tickets = [ticket for ticket in self.tickets if ticket.user_id == user_id and ticket.status == TicketStatus.PENDING]
    return max(user_tickets, key=lambda t: t.id, default=None)

    def update_ticket(self, ticket_id: str, **kwargs):
    ticket = self.get_ticket_by_id(ticket_id)
    if ticket:
    for key, value in kwargs.items():
    setattr(ticket, key, value)
    self.save_tickets()

    class TicketService:
    def __init__(self, bot: Bot, storage: TicketStorage, support_chat_id: int):
    self.bot = bot
    self.storage = storage
    self.support_chat_id = support_chat_id

    async def create_ticket(self, user_id: int, username: str, first_name: str, topic: str,
    description: str, media: List[Dict[str, str]] = None,
    priority: TicketPriority = TicketPriority.MEDIUM):
    try:
    ticket_id = str(uuid.uuid4())
    ticket = Ticket(
    id=ticket_id,
    user_id=user_id,
    username=username,
    first_name=first_name,
    topic=topic,
    description=description,
    media=media or [],
    status=TicketStatus.PENDING,
    priority=priority,
    created_at=datetime.now().isoformat()
    )

    topic_obj = await self.bot.create_forum_topic(
    chat_id=self.support_chat_id,
    name=f"Обращение #{ticket_id} @{username} {priority.value}"
    )
    ticket.thread_id = topic_obj.message_thread_id

    await self.bot.send_message(
    chat_id=self.support_chat_id,
    message_thread_id=ticket.thread_id,
    text=f"<b> Обращение</b> #<code>{ticket_id}</code> @{username}\n"
    f"<b> Имя:</b> {first_name}\n"
    f"<b> Тема:</b> {ticket.topic}\n"
    f"<b> Описание:</b> {description}\n"
    f"<b> Приоритет:</b> {priority.value}",
    parse_mode="HTML"
    )

    for media_item in ticket.media:
    try:
    media_type = media_item['type']
    file_id = media_item['file_id']
    if media_type == "photo":
    await self.bot.send_photo(
    chat_id=self.support_chat_id,
    photo=file_id,
    message_thread_id=ticket.thread_id
    )
    elif media_type == "video":
    await self.bot.send_video(
    chat_id=self.support_chat_id,
    video=file_id,
    message_thread_id=ticket.thread_id
    )
    elif media_type == "document":
    await self.bot.send_document(
    chat_id=self.support_chat_id,
    document=file_id,
    message_thread_id=ticket.thread_id
    )
    except Exception as e:
    logger.error(f"Error sending media for ticket {ticket_id}: {e}")

    self.storage.add_ticket(ticket)
    logger.info(f"Created new ticket {ticket_id} for user {username} with priority {priority.value}")
    return ticket
    except Exception as e:
    logger.error(f"Error creating ticket: {e}")
    raise

    async def close_ticket(self, ticket_id: str, status: TicketStatus, admin_message: Message):
    ticket = self.storage.get_ticket_by_id(ticket_id)
    if ticket:
    self.storage.update_ticket(ticket_id, status=status)
    await self.bot.send_message(
    chat_id=ticket.user_id,
    text=f"Обращение #{ticket_id} закрыто со статусом: {status.value}"
    )
    await self.bot.send_message(
    chat_id=self.support_chat_id,
    message_thread_id=ticket.thread_id,
    text=f"Обращение #{ticket_id} закрыто: {status.value}"
    )
    await self.bot.close_forum_topic(
    chat_id=self.support_chat_id,
    message_thread_id=ticket.thread_id
    )

    async def reassign_ticket(self, ticket_id: str, new_admin: str):
    ticket = self.storage.get_ticket_by_id(ticket_id)
    if ticket:
    await self.bot.send_message(
    chat_id=self.support_chat_id,
    message_thread_id=ticket.thread_id,
    text=f"Обращение переназначено на {new_admin}"
    )
    await self.bot.send_message(
    chat_id=ticket.user_id,
    text=f"Ваше обращение #{ticket_id} переназначено на другого сотрудника"
    )

    async def remind_user(self, ticket_id: str):
    ticket = self.storage.get_ticket_by_id(ticket_id)
    if ticket:
    await self.bot.send_message(
    chat_id=ticket.user_id,
    text=f"Напоминание по обращению #{ticket_id}: Пожалуйста, предоставьте дополнительную информацию или уточните статус."
    )

    class CreateTicket(StatesGroup):
    TOPIC = State()
    CATEGORY = State()
    DESCRIPTION = State()
    PRIORITY = State()
    MEDIA = State()

    class TicketServiceMiddleware:
    def __init__(self, ticket_service: TicketService):
    self.ticket_service = ticket_service

    async def __call__(self, handler, event: Update, data: Dict[str, Any]):
    data['ticket_service'] = self.ticket_service
    return await handler(event, data)

    router = Router()

    def get_main_menu():
    return InlineKeyboardMarkup(inline_keyboard=[
    [InlineKeyboardButton(text=" Создать обращение", callback_data="create_ticket", parse_mode="HTML")],
    [InlineKeyboardButton(text=" История обращений", callback_data="history", parse_mode="HTML")]
    ])

    def get_priority_keyboard():
    return InlineKeyboardMarkup(inline_keyboard=[
    [
    InlineKeyboardButton(text=" Низкий", callback_data="priority_low"),
    InlineKeyboardButton(text=" Средний", callback_data="priority_medium"),
    InlineKeyboardButton(text=" Высокий", callback_data="priority_high")
    ]
    ])

    def get_category_keyboard():
    return InlineKeyboardMarkup(inline_keyboard=[
    [
    InlineKeyboardButton(text=" Общий вопрос", callback_data="category_general"),
    InlineKeyboardButton(text=" Техническая проблема", callback_data="category_technical")
    ],
    [
    InlineKeyboardButton(text=" Оплата", callback_data="category_billing"),
    InlineKeyboardButton(text=" Предложение", callback_data="category_feature")
    ],
    [
    InlineKeyboardButton(text=" Другое", callback_data="category_other")
    ]
    ])

    @router.message(CommandStart())
    async def start_command(message: Message):
    await message.answer(
    " Добро пожаловать в бот поддержки! Выберите действие:",
    reply_markup=get_main_menu(), parse_mode="HTML"
    )

    @router.callback_query(F.data == "create_ticket")
    async def create_ticket_start(callback, state: FSMContext):
    await callback.message.answer(" Введите тему обращения:", parse_mode="HTML")
    await state.set_state(CreateTicket.TOPIC)
    await callback.answer()

    @router.message(CreateTicket.TOPIC)
    async def process_topic(message: Message, state: FSMContext):
    await state.update_data(topic=message.text)
    await message.answer(
    " Выберите категорию обращения:",
    reply_markup=get_category_keyboard(),
    parse_mode="HTML"
    )
    await state.set_state(CreateTicket.CATEGORY)

    @router.callback_query(F.data.startswith("category_"))
    async def process_category(callback, state: FSMContext):
    category_map = {
    "category_general": TicketCategory.GENERAL,
    "category_technical": TicketCategory.TECHNICAL,
    "category_billing": TicketCategory.BILLING,
    "category_feature": TicketCategory.FEATURE,
    "category_other": TicketCategory.OTHER
    }
    category = category_map[callback.data]
    await state.update_data(category=category)
    await callback.message.answer(" Опишите проблему:", parse_mode="HTML")
    await state.set_state(CreateTicket.DESCRIPTION)
    await callback.answer()

    @router.message(CreateTicket.DESCRIPTION)
    async def process_description(message: Message, state: FSMContext):
    await state.update_data(description=message.text)
    await message.answer(
    " Выберите приоритет обращения:",
    reply_markup=get_priority_keyboard(),
    parse_mode="HTML"
    )
    await state.set_state(CreateTicket.PRIORITY)

    @router.callback_query(F.data.startswith("priority_"))
    async def process_priority(callback, state: FSMContext):
    priority_map = {
    "priority_low": TicketPriority.LOW,
    "priority_medium": TicketPriority.MEDIUM,
    "priority_high": TicketPriority.HIGH
    }
    priority = priority_map[callback.data]
    await state.update_data(priority=priority)
    await callback.message.answer(
    " Прикрепите медиа (фото/видео/файл) или отправьте 'Готово' для завершения:",
    parse_mode="HTML"
    )
    await state.set_state(CreateTicket.MEDIA)
    await state.update_data(media=[])
    await callback.answer()

    @router.message(CreateTicket.MEDIA, F.content_type.in_({
    ContentType.PHOTO, ContentType.VIDEO, ContentType.DOCUMENT
    }))
    async def process_media(message: Message, state: FSMContext):
    data = await state.get_data()
    media = data.get('media', [])
    if message.photo:
    media.append({"type": "photo", "file_id": message.photo[-1].file_id})
    elif message.video:
    media.append({"type": "video", "file_id": message.video.file_id})
    elif message.document:
    media.append({"type": "document", "file_id": message.document.file_id})
    await state.update_data(media=media)
    await message.answer(" Медиа добавлено. Отправьте ещё или напишите '<code>Готово</code>'.", parse_mode="HTML")

    @router.message(CreateTicket.MEDIA, F.text.lower() == "готово")
    async def finish_ticket(message: Message, state: FSMContext, ticket_service: TicketService):
    try:
    data = await state.get_data()
    ticket = await ticket_service.create_ticket(
    user_id=message.from_user.id,
    username=message.from_user.username or "Unknown",
    first_name=message.from_user.first_name,
    topic=data['topic'],
    description=data['description'],
    media=data.get('media', []),
    priority=data.get('priority', TicketPriority.MEDIUM)
    )

    await message.answer(
    f" Обращение #{ticket.id} создано!\n"
    f"Приоритет: {ticket.priority.value}\n"
    f"Статус: {ticket.status.value}",
    reply_markup=get_main_menu(),
    parse_mode="HTML"
    )
    await state.clear()
    except Exception as e:
    logger.error(f"Error creating ticket: {e}")
    await message.answer(
    " Произошла ошибка при создании обращения. Пожалуйста, попробуйте позже.",
    reply_markup=get_main_menu(),
    parse_mode="HTML"
    )
    await state.clear()

    @router.callback_query(F.data == "history")
    async def show_history(callback, ticket_service: TicketService):
    tickets = ticket_service.storage.get_user_tickets(callback.from_user.id)
    if not tickets:
    await callback.message.answer(
    " У вас пока нет обращений",
    reply_markup=get_main_menu(),
    parse_mode="HTML"
    )
    return

    tickets.sort(key=lambda x: x.created_at, reverse=True)
    keyboard = []
    for ticket in tickets[:5]:
    keyboard.append([
    InlineKeyboardButton(
    text=f"#{ticket.id} - {ticket.topic} ({ticket.status.value})",
    callback_data=f"details_{ticket.id}"
    )
    ])

    await callback.message.answer(
    " Ваши последние обращения:",
    reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
    parse_mode="HTML"
    )
    await callback.answer()

    @router.callback_query(F.data.startswith("details_"))
    async def show_ticket_details(callback, ticket_service: TicketService):
    ticket_id = callback.data.split("_")[1]
    ticket = ticket_service.storage.get_ticket_by_id(ticket_id)
    if not ticket:
    await callback.message.answer(
    " Обращение не найдено",
    reply_markup=get_main_menu(),
    parse_mode="HTML"
    )
    return

    keyboard = [
    [InlineKeyboardButton(text=" Создать похожее", callback_data=f"recreate_{ticket_id}")],
    [InlineKeyboardButton(text=" Назад", callback_data="history")]
    ]

    await callback.message.answer(
    get_ticket_info(ticket),
    reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
    parse_mode="HTML"
    )
    await callback.answer()

    @router.callback_query(F.data.startswith("recreate_"))
    async def recreate_ticket(callback, state: FSMContext, ticket_service: TicketService):
    ticket_id = callback.data.split("_")[1]
    ticket = ticket_service.storage.get_ticket_by_id(ticket_id)
    if ticket:
    await state.update_data(topic=ticket.topic, description=ticket.description)
    await callback.message.answer(
    f" Создаём новое обращение на основе #{ticket.id}\n"
    f" Тема: {ticket.topic}\n"
    f" Описание: {ticket.description}\n"
    "Введите новую тему или подтвердите текущую:", parse_mode="HTML"
    )
    await state.set_state(CreateTicket.TOPIC)
    await callback.answer()

    @router.message(Command("closed_yes"), F.chat.type == "supergroup", lambda message: message.from_user.id in ADMIN_IDS)
    async def close_yes(message: Message, ticket_service: TicketService):
    ticket = ticket_service.storage.get_ticket_by_thread_id(message.message_thread_id)
    if ticket:
    await ticket_service.close_ticket(ticket.id, TicketStatus.RESOLVED, message)
    await message.answer(" Обращение успешно закрыто.", parse_mode="HTML")
    else:
    await message.answer(" Обращение не найдено.", parse_mode="HTML")

    @router.message(Command("closed_no"), F.chat.type == "supergroup", lambda message: message.from_user.id in ADMIN_IDS)
    async def close_no(message: Message, ticket_service: TicketService):
    ticket = ticket_service.storage.get_ticket_by_thread_id(message.message_thread_id)
    if ticket:
    await ticket_service.close_ticket(ticket.id, TicketStatus.UNRESOLVED, message)
    await message.answer(" Обращение закрыто.", parse_mode="HTML")
    else:
    await message.answer(" Обращение не найдено.", parse_mode="HTML")

    @router.message(Command("reassign"), F.chat.type == "supergroup", lambda message: message.from_user.id in ADMIN_IDS)
    async def reassign(message: Message, ticket_service: TicketService):
    ticket = ticket_service.storage.get_ticket_by_thread_id(message.message_thread_id)
    if ticket:
    args = message.text.split()
    if len(args) > 1:
    new_admin = args[1]
    await ticket_service.reassign_ticket(ticket.id, new_admin)
    await message.answer(f" Обращение переназначено на {new_admin}", parse_mode="HTML")
    else:
    await message.answer(" Укажите @username для переназначения", parse_mode="HTML")
    else:
    await message.answer(" Обращение не найдено.", parse_mode="HTML")

    @router.message(Command("remind"), F.chat.type == "supergroup", lambda message: message.from_user.id in ADMIN_IDS)
    async def remind(message: Message, ticket_service: TicketService):
    ticket = ticket_service.storage.get_ticket_by_thread_id(message.message_thread_id)
    if ticket:
    await ticket_service.remind_user(ticket.id)
    await message.answer(" Напоминание отправлено пользователю.", parse_mode="HTML")
    else:
    await message.answer(" Обращение не найдено.", parse_mode="HTML")

    @router.message(F.chat.type == "supergroup", F.message_thread_id, lambda message: message.from_user.id in ADMIN_IDS)
    async def forward_to_user(message: Message, ticket_service: TicketService):
    ticket = ticket_service.storage.get_ticket_by_thread_id(message.message_thread_id)
    if ticket:
    await ticket_service.bot.copy_message(
    chat_id=ticket.user_id,
    from_chat_id=message.chat.id,
    message_id=message.message_id
    )

    @router.message(F.chat.type == "private", ~F.text.startswith('/'), ~F.state)
    async def forward_user_message(message: Message, ticket_service: TicketService):
    ticket = ticket_service.storage.get_latest_pending_ticket(message.from_user.id)
    if ticket:
    if message.text:
    await ticket_service.bot.send_message(
    chat_id=ticket_service.support_chat_id,
    message_thread_id=ticket.thread_id,
    text=f"<b> Сообщение от пользователя:</b>\n{message.text}",
    parse_mode="HTML"
    )
    elif message.photo or message.video or message.document:
    caption = f"<b> Сообщение от пользователя:</b>\n[Медиа]"
    await ticket_service.bot.copy_message(
    chat_id=ticket_service.support_chat_id,
    from_chat_id=message.from_user.id,
    message_id=message.message_id,
    message_thread_id=ticket.thread_id,
    caption=caption,
    parse_mode="HTML"
    )

    def get_ticket_info(ticket: Ticket) -> str:
    return (
    f"<b> Обращение</b> #<code>{ticket.id}</code>\n"
    f"<b> Категория:</b> {ticket.category.value}\n"
    f"<b> Приоритет:</b> {ticket.priority.value}\n"
    f"<b> Создано:</b> {ticket.created_at}\n"
    f"<b> Тема:</b> {ticket.topic}\n"
    f"<b> Описание:</b> {ticket.description}\n"
    f"<b> Статус:</b> {ticket.status.value}"
    )

    @router.message(Command("search"))
    async def search_tickets(message: Message, ticket_service: TicketService):
    if message.from_user.id not in ADMIN_IDS:
    return

    query = message.text.split(maxsplit=1)[1] if len(message.text.split()) > 1 else ""
    if not query:
    await message.answer(" Укажите поисковый запрос")
    return

    tickets = ticket_service.storage.tickets
    results = []
    for ticket in tickets:
    if (query.lower() in ticket.topic.lower() or
    query.lower() in ticket.description.lower() or
    query.lower() in ticket.username.lower()):
    results.append(ticket)

    if not results:
    await message.answer(" Ничего не найдено")
    return

    keyboard = []
    for ticket in results[:5]:
    keyboard.append([
    InlineKeyboardButton(
    text=f"#{ticket.id} - {ticket.topic} ({ticket.status.value})",
    callback_data=f"details_{ticket.id}"
    )
    ])

    await message.answer(
    f" Найдено обращений: {len(results)}\nПоказаны первые 5:",
    reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard),
    parse_mode="HTML"
    )

    async def main():
    try:
    bot = Bot(token=BOT_TOKEN)
    storage = TicketStorage()
    ticket_service = TicketService(bot, storage, SUPPORT_CHAT_ID)

    dp = Dispatcher(storage=MemoryStorage())
    dp.update.middleware(TicketServiceMiddleware(ticket_service))
    dp.include_router(router)

    logger.info("Bot started")
    await dp.start_polling(bot)
    except Exception as e:
    logger.error(f"Error in main: {e}")
    raise
    finally:
    await bot.session.close()

    if __name__ == "__main__":
    asyncio.run(main())
    Дополнительно:
    1. Создайте .env по следующему шаблону:
    Код
    BOT_TOKEN=tokem_from_@botfather
    SUPPORT_CHAT_ID=-1001234567890
    ADMIN_IDS=1234567890
    Это один из первых петников, код прогонял несколько раз через gpt что бы было как минимум не стыдно выложить неструктруированное говнище, а выложить хочеться что бы увидеть какой-либо фитбек, эмодзи и парсмод так же устанавливалось через гпт ибо мне в падлу(

    P.s. где то видел подобное на форуме, но код и самого бота не смотрел, если что-то похожее, сорян...

    p.s.2 все обращения еще в json сохраняются​
     
    4 июн 2025 в 00:42 Изменено
    1. убежище
      verply, вообще годно, но призывы к действию из скринов асуждаю
Top