├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-311.pyc │ ├── gpt.cpython-311.pyc │ ├── map.cpython-311.pyc │ ├── parser.cpython-311.pyc │ └── sql.cpython-311.pyc └── sql.py ├── config.py ├── main.py └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | USDT: TWss5Hb3i92Wo8g2ELLW521AsnzJEx38NA 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /__pycache__ 3 | app/database.db 4 | config.py 5 | app/__pycache__/sql.cpython-311.pyc 6 | app/__pycache__/sql.cpython-311.pyc 7 | config.py 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sergey Milleu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HelpDesk Telegram Bot 2 | 3 | 4 | **Новые возможности: v0.4 beta** 5 | 6 | > - Отправка уведомлений о задачах как пользователям, так и администраторам. 7 | > - Добавлена кнопка выполнения задачи внутри уведомления. 8 | > - Список истории задач с пагинацией. 9 | > - По закрытию задачи теперь можно и нужно оставлять комментарий. 10 | > - Учитывается время исполнения задачи в часах 11 | > - Предусмотрены модераторы и главный админ(который получает все сообщения о поставке задач). 12 | 13 | ![История заявок](https://github.com/prasx/HelpDesk/assets/33151983/9111ab96-6199-4d89-a79b-8e700907aa5a)![Исполнение заявок](https://github.com/prasx/HelpDesk/assets/33151983/80c75a73-d3d0-48f4-9fe3-f0c325855b04) 14 | 15 | 16 | ## Описание 17 | Этот проект представляет собой Telegram бота, который предоставляет функционал HelpDesk системы. В текущей версии реализованы следующие функции: 18 | 19 | - **Анкетирование:** Пользователи могут заполнять анкету, предоставляя информацию о наименовании организации, адресе заявки и контактном номере телефона. 20 | 21 | - **Хранение заявок в базе данных:** Все созданные заявки сохраняются в базе данных. Информация о заявках доступна как администратору, так и пользователю в соответствующем разделе бота. 22 | 23 | - **Уведомления о новых заявках:** Администратор получает уведомление с подробностями о новой заявке, что помогает оперативно реагировать на запросы пользователей. 24 | 25 | - **База данных:** Проект использует базу данных, состоящую из таблицы заявок и таблицы пользователей для хранения информации. 26 | 27 | - **Административная панель:** Администратору доступен вывод всех заявок в очереди, а также изменение статуса с "В работе" на "Завершён". 28 | 29 | 30 | Необходимые пакеты: 31 | ``` pip install aiogram==2.25.2 ``` 32 | 33 | Запуск: 34 | ``` python main.py ``` 35 | 36 | ## Репозиторий 37 | Этот репозиторий является первым публичным для проекта. Здесь содержится исходный код бота, документация, и другие файлы, необходимые для развертывания и работы бота. Разработка ведется с акцентом на обеспечение удобства пользователей и оперативного реагирования на заявки. 38 | 39 | ## Примечание 40 | Этот README.md может быть дополнен дальнейшей информацией о конфигурации, установке, использовании и других функциях бота по мере развития проекта. 41 | 42 | 43 | ## Спонсорство 44 | Если вы считаете этот проект полезным, рассмотрите возможность поддержки его развития: 45 | 46 | - **SberBank:** [SberBank](https://messenger.online.sberbank.ru/sl/phavjfiRFywAoLOKf) 47 | - **USDT:** `TWss5Hb3i92Wo8g2ELLW521AsnzJEx38NA` 48 | 49 | Мы очень ценим вашу поддержку! 50 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Политика безопасности 2 | 3 | ## Обзор 4 | 5 | Целью этой политики безопасности является обеспечение защиты конфиденциальности данных и безопасности работы пользователей с HelpDesk Telegram Bot v0.4 beta, предоставляющим функционал HelpDesk системы. 6 | 7 | ## Область применения 8 | 9 | Эта политика применяется ко всем разработчикам, администраторам и пользователям, взаимодействующим с HelpDesk Telegram Bot версии v0.4 beta. 10 | 11 | ## Основные принципы 12 | 13 | - Безопасность информации - наш приоритет. 14 | - Соблюдение стандартов и законодательства по защите данных. 15 | - Постоянное обновление и аудит безопасности кода и инфраструктуры. 16 | 17 | ## Меры безопасности 18 | 19 | 1. **Аутентификация и авторизация:** 20 | - Реализация безопасной аутентификации пользователей в Telegram. 21 | - Ограничение доступа к административной панели только для уполномоченных администраторов. 22 | 23 | 2. **Хранение и обработка данных:** 24 | - Защита конфиденциальных данных пользователей (например, номеров телефонов и адресов) с помощью механизмов шифрования. 25 | - Соблюдение стандартов GDPR и других законов о защите данных при обработке информации пользователей. 26 | 27 | 3. **Тестирование безопасности:** 28 | - Проведение регулярных тестов на проникновение и аудит безопасности кода. 29 | - Использование инструментов статического анализа кода и сканеров уязвимостей для выявления потенциальных угроз. 30 | 31 | 4. **Обучение и осведомленность:** 32 | - Обучение сотрудников основам безопасности информации и процедурам защиты данных. 33 | - Проведение регулярных обзоров безопасности кода и обновление обучающих материалов. 34 | 35 | 5. **Реагирование на инциденты:** 36 | - Ведение журнала инцидентов безопасности и оперативное реагирование на них в соответствии с установленной процедурой. 37 | - Сообщение об обнаруженных уязвимостях в коде и инфраструктуре и оперативное их устранение. 38 | 39 | ## Обновления политики 40 | 41 | Эта политика безопасности будет пересматриваться и обновляться по мере необходимости, чтобы соответствовать изменениям в требованиях безопасности и стратегии проекта Telegram бота версии v0.4 beta. 42 | 43 | Последнее обновление: 02/05/2024 44 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prasx/HelpDesk/928425476499949adb91c7fd2de4259caa1c656c/app/__init__.py -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prasx/HelpDesk/928425476499949adb91c7fd2de4259caa1c656c/app/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /app/__pycache__/gpt.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prasx/HelpDesk/928425476499949adb91c7fd2de4259caa1c656c/app/__pycache__/gpt.cpython-311.pyc -------------------------------------------------------------------------------- /app/__pycache__/map.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prasx/HelpDesk/928425476499949adb91c7fd2de4259caa1c656c/app/__pycache__/map.cpython-311.pyc -------------------------------------------------------------------------------- /app/__pycache__/parser.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prasx/HelpDesk/928425476499949adb91c7fd2de4259caa1c656c/app/__pycache__/parser.cpython-311.pyc -------------------------------------------------------------------------------- /app/__pycache__/sql.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prasx/HelpDesk/928425476499949adb91c7fd2de4259caa1c656c/app/__pycache__/sql.cpython-311.pyc -------------------------------------------------------------------------------- /app/sql.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | 4 | DB_PATH = 'app/database.db' 5 | 6 | def execute_query(query, params=None): 7 | """ 8 | Выполняет SQL-запрос к базе данных. 9 | 10 | Parameters: 11 | query (str): SQL-запрос. 12 | params (tuple): Параметры для запроса (по умолчанию None). 13 | 14 | Returns: 15 | list: Результат выполнения запроса. 16 | """ 17 | with sqlite3.connect(DB_PATH) as conn: 18 | cursor = conn.cursor() 19 | if params: 20 | cursor.execute(query, params) 21 | else: 22 | cursor.execute(query) 23 | conn.commit() 24 | return cursor.fetchall() 25 | 26 | def create_tables(): 27 | """ 28 | Создает таблицы в базе данных, если они не существуют. 29 | """ 30 | users = ''' 31 | CREATE TABLE IF NOT EXISTS users ( 32 | tg_id INTEGER PRIMARY KEY, 33 | pos TEXT, 34 | data_reg TEXT, 35 | profile TEXT 36 | ) 37 | ''' 38 | ticket = ''' 39 | CREATE TABLE IF NOT EXISTS ticket ( 40 | number_ticket INTEGER PRIMARY KEY AUTOINCREMENT, 41 | tg_id_ticket INTEGER, 42 | organization TEXT, 43 | addres_ticket TEXT, 44 | message_ticket TEXT, 45 | time_ticket TEXT, 46 | state_ticket TEXT, 47 | ticket_comm TEXT 48 | ) 49 | ''' 50 | execute_query(users) 51 | execute_query(ticket) 52 | 53 | 54 | def add_user(tg_id, pos, data_reg, profile): 55 | """ 56 | Добавляет нового пользователя в базу данных. 57 | 58 | Parameters: 59 | tg_id (int): Telegram ID пользователя. 60 | pos (str): Позиция пользователя. 61 | data_reg (str): Дата регистрации пользователя. 62 | profile (dict): Профиль пользователя в формате словаря. 63 | """ 64 | query = '''INSERT INTO users (tg_id, pos, data_reg, profile) VALUES (?, ?, ?, ?)''' 65 | profile_json = json.dumps(profile, ensure_ascii=False) 66 | execute_query(query, (tg_id, pos, data_reg, profile_json)) 67 | 68 | 69 | def get_user_by_id(tg_id): 70 | """ 71 | Возвращает информацию о пользователе по его Telegram ID. 72 | 73 | Parameters: 74 | tg_id (int): Telegram ID пользователя. 75 | 76 | Returns: 77 | dict: Информация о пользователе. 78 | """ 79 | query = 'SELECT * FROM users WHERE tg_id=?' 80 | result = execute_query(query, (tg_id,)) 81 | if result: 82 | return { 83 | 'tg_id': result[0][0], 84 | 'pos': result[0][1], 85 | 'data_reg': result[0][2], 86 | 'profile': json.loads(result[0][3]) 87 | } 88 | return None 89 | 90 | 91 | def add_ticket(tg_id_ticket, organization, addres_ticket, message_ticket, time_ticket, state_ticket, ticket_comm): 92 | """ 93 | Добавляет новую задачу в базу данных. 94 | 95 | Parameters: 96 | tg_id_ticket (int): Telegram ID пользователя, создавшего задачу. 97 | organization (str): Название организации. 98 | addres_ticket (str): Адрес задачи. 99 | message_ticket (str): Сообщение задачи. 100 | time_ticket (str): Время создания задачи. 101 | state_ticket (str): Статус задачи. 102 | ticket_comm (str): Комментарий к задаче. 103 | """ 104 | query = ''' 105 | INSERT INTO ticket (tg_id_ticket, organization, addres_ticket, message_ticket, time_ticket, state_ticket, ticket_comm) 106 | VALUES (?, ?, ?, ?, ?, ?, ?) 107 | ''' 108 | execute_query(query, (tg_id_ticket, organization, addres_ticket, message_ticket, time_ticket, state_ticket, ticket_comm)) 109 | 110 | 111 | def get_last_ticket_number(): 112 | """ 113 | Возвращает номер последней задачи в таблице ticket. 114 | 115 | Returns: 116 | int: Номер последней задачи. 117 | """ 118 | query = "SELECT number_ticket FROM ticket ORDER BY number_ticket DESC LIMIT 1" 119 | result = execute_query(query) 120 | return result[0][0] if result else 0 121 | 122 | 123 | def get_total_tickets_by_status(tg_id, status): 124 | """ 125 | Возвращает общее количество задач с указанным статусом для заданного пользователя. 126 | 127 | Parameters: 128 | tg_id (int): Telegram ID пользователя. 129 | status (str): Статус задачи. 130 | 131 | Returns: 132 | int: Общее количество задач с указанным статусом. 133 | """ 134 | query = 'SELECT COUNT(*) FROM ticket WHERE tg_id_ticket=? AND state_ticket=?' 135 | result = execute_query(query, (tg_id, status)) 136 | return result[0][0] if result else 0 137 | 138 | 139 | def get_total_tickets_by_status_admin(status): 140 | """ 141 | Возвращает общее количество задач с указанным статусом для администратора. 142 | 143 | Parameters: 144 | status (str): Статус задачи. 145 | 146 | Returns: 147 | int: Общее количество задач с указанным статусом. 148 | """ 149 | query = 'SELECT COUNT(*) FROM ticket WHERE state_ticket=?' 150 | result = execute_query(query, (status,)) 151 | return result[0][0] if result else 0 152 | 153 | 154 | def get_total_tickets_by_status_for_user(tg_id, status): 155 | """ 156 | Возвращает общее количество задач с указанным статусом для заданного пользователя. 157 | 158 | Parameters: 159 | tg_id (int): Telegram ID пользователя. 160 | status (str): Статус задачи. 161 | 162 | Returns: 163 | str: Общее количество задач с указанным статусом (в виде строки). 164 | """ 165 | total_tickets = get_total_tickets_by_status(tg_id, status) 166 | return str(total_tickets) if total_tickets else "0" 167 | 168 | 169 | def get_tickets_in_progress_by_user_id(tg_id): 170 | """ 171 | Возвращает список задач в работе для указанного пользователя. 172 | 173 | Parameters: 174 | tg_id (int): Telegram ID пользователя. 175 | 176 | Returns: 177 | list: Список задач в работе. 178 | """ 179 | query = 'SELECT * FROM ticket WHERE tg_id_ticket=? AND state_ticket=?' 180 | result = execute_query(query, (tg_id, "В работе")) 181 | return result 182 | 183 | 184 | def update_pos(pos_value, column, value): 185 | """ 186 | Обновляет позицию пользователя в базе данных. 187 | 188 | Parameters: 189 | pos_value (str): Новое значение позиции. 190 | column (str): Название столбца, в котором нужно обновить значение. 191 | value (int): Значение, по которому происходит обновление. 192 | 193 | Returns: 194 | None 195 | """ 196 | query = f"UPDATE users SET pos = ? WHERE {column} = ?" 197 | execute_query(query, (pos_value, value)) 198 | 199 | 200 | def update_profile_data(tg_id, field_name, new_value): 201 | """ 202 | Обновляет данные профиля пользователя внутри ячейки profile. 203 | 204 | Parameters: 205 | tg_id (int): Telegram ID пользователя. 206 | field_name (str): Название поля, которое нужно обновить. 207 | new_value: Новое значение для поля. 208 | 209 | Returns: 210 | None 211 | """ 212 | query_select = 'SELECT profile FROM users WHERE tg_id=?' 213 | profile_json = execute_query(query_select, (tg_id,))[0][0] 214 | if profile_json: 215 | profile_dict = json.loads(profile_json) 216 | profile_dict[field_name] = new_value 217 | query_update = "UPDATE users SET profile = ? WHERE tg_id = ?" 218 | execute_query(query_update, (json.dumps(profile_dict, ensure_ascii=False), tg_id)) 219 | 220 | 221 | def read_cell(column, condition_column, condition_value): 222 | """ 223 | Читает данные из указанной ячейки в таблице пользователей. 224 | 225 | Parameters: 226 | column (str): Название столбца, из которого нужно прочитать данные. 227 | condition_column (str): Название столбца для условия выборки. 228 | condition_value: Значение, по которому происходит выборка. 229 | 230 | Returns: 231 | str: Значение ячейки (в виде строки). 232 | """ 233 | query = f"SELECT {column} FROM users WHERE {condition_column} = ?" 234 | result = execute_query(query, (condition_value,)) 235 | return str(result[0])[2:-3] if result else None 236 | 237 | 238 | def read_profile(tg_id): 239 | """ 240 | Возвращает данные профиля пользователя из ячейки profile. 241 | 242 | Parameters: 243 | tg_id (int): Telegram ID пользователя. 244 | 245 | Returns: 246 | dict: Данные профиля пользователя. 247 | """ 248 | query = "SELECT profile FROM users WHERE tg_id=?" 249 | result = execute_query(query, (tg_id,)) 250 | if result: 251 | return json.loads(result[0][0]) 252 | return None 253 | 254 | 255 | def get_all_tickets_in_progress(): 256 | """ 257 | Возвращает список всех тикетов, находящихся в процессе выполнения. 258 | 259 | Returns: 260 | list: Список кортежей с данными о тикетах. 261 | """ 262 | conn = sqlite3.connect(DB_PATH) 263 | cursor = conn.cursor() 264 | cursor.execute('SELECT * FROM ticket WHERE state_ticket=?', ("В работе",)) 265 | all_tickets_in_progress = cursor.fetchall() 266 | conn.close() 267 | return all_tickets_in_progress 268 | 269 | 270 | def get_ticket_info(ticket_id): 271 | """ 272 | Возвращает информацию о заданном тикете. 273 | 274 | Parameters: 275 | ticket_id (int): Номер тикета. 276 | 277 | Returns: 278 | tuple: Кортеж с данными о тикете. 279 | """ 280 | conn = sqlite3.connect(DB_PATH) 281 | cursor = conn.cursor() 282 | cursor.execute('SELECT * FROM ticket WHERE number_ticket=?', (ticket_id,)) 283 | ticket_info = cursor.fetchone() 284 | conn.close() 285 | return ticket_info 286 | 287 | 288 | def update_ticket_status(ticket_id, new_status): 289 | """ 290 | Обновляет статус заданного тикета. 291 | 292 | Parameters: 293 | ticket_id (int): Номер тикета. 294 | new_status (str): Новый статус. 295 | 296 | Returns: 297 | None 298 | """ 299 | conn = sqlite3.connect(DB_PATH) 300 | cursor = conn.cursor() 301 | query = "UPDATE ticket SET state_ticket=? WHERE number_ticket=?" 302 | cursor.execute(query, (new_status, ticket_id)) 303 | conn.commit() 304 | conn.close() 305 | 306 | 307 | def get_completed_tickets_by_user(tg_id): 308 | """ 309 | Возвращает список завершенных тикетов для указанного пользователя. 310 | 311 | Parameters: 312 | tg_id (int): Telegram ID пользователя. 313 | 314 | Returns: 315 | list: Список кортежей с данными о завершенных тикетах. 316 | """ 317 | conn = sqlite3.connect(DB_PATH) 318 | cursor = conn.cursor() 319 | cursor.execute('SELECT * FROM ticket WHERE tg_id_ticket=? AND state_ticket=?', (tg_id, "Завершена")) 320 | completed_tickets = cursor.fetchall() 321 | conn.close() 322 | return completed_tickets 323 | 324 | 325 | def update_ticket_comment(ticket_id, ticket_comm): 326 | """ 327 | Обновляет комментарий в существующем тикете. 328 | 329 | Parameters: 330 | ticket_id (int): Номер тикета. 331 | ticket_comm (str): Новый комментарий. 332 | 333 | Returns: 334 | bool: Флаг успешного выполнения операции (True - успешно, False - ошибка). 335 | """ 336 | query = 'UPDATE ticket SET ticket_comm = ? WHERE number_ticket = ?' 337 | execute_query(query, (ticket_comm, ticket_id)) 338 | return True 339 | 340 | 341 | def read_ticket_comment(ticket_id): 342 | """ 343 | Читает комментарий из существующего тикета. 344 | 345 | Parameters: 346 | ticket_id (int): Номер тикета. 347 | 348 | Returns: 349 | str: Комментарий к тикету. 350 | """ 351 | query = 'SELECT ticket_comm FROM ticket WHERE number_ticket = ?' 352 | result = execute_query(query, (ticket_id,)) 353 | if result: 354 | return result[0][0] 355 | else: 356 | return None -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | BOT_TOKEN = 'YOU_BOT_TOKEN' #test 2 | 3 | 4 | ADMIN_USERS = [ID MODER, ID MODER] 5 | ADMIN_MESSAGE = ID ADMIN 6 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import aiogram 2 | from aiogram import Bot, Dispatcher, types 3 | from aiogram.contrib.middlewares.logging import LoggingMiddleware 4 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 5 | from aiogram import executor 6 | from app import sql 7 | import config 8 | from config import ADMIN_USERS, ADMIN_MESSAGE 9 | import datetime 10 | 11 | import asyncio 12 | loop = asyncio.get_event_loop() 13 | import logging 14 | logging.basicConfig(level=logging.INFO) 15 | 16 | 17 | bot = Bot(token=config.BOT_TOKEN) 18 | dp = Dispatcher(bot) 19 | dp.middleware.setup(LoggingMiddleware()) 20 | # Создание таблиц в базе данных SQLite 21 | sql.create_tables() 22 | 23 | 24 | @dp.message_handler(commands=['start']) 25 | async def send_start(message: types.Message): 26 | user_id = message.from_user.id 27 | data_reg = message.date 28 | user = sql.get_user_by_id(user_id) 29 | 30 | if not user: 31 | # Если пользователь отсутствует, добавляем его 32 | user_info = { 33 | 'tg_id': user_id, 34 | 'pos': 'main_menu', 35 | 'data_reg': data_reg, 36 | 'profile': {"organization": "Нет данных", "organization_adress": "Нет данных", "organization_inn": "Нет данных", "organization_phone": "Нет данных", "history_ticket": "", "data_ticket": "", "user_name": ""} 37 | } 38 | sql.add_user(**user_info) 39 | text_no_user = f"Добро пожаловать в HelpDesk компании ЭниКей! Для работы в сервисе необходимо заполнить данные." 40 | keyboard = InlineKeyboardMarkup() 41 | keyboard.add(InlineKeyboardButton(text="🏢 Моя компания", callback_data="my_company")) 42 | await message.answer(text_no_user, reply_markup=keyboard, parse_mode="HTML") 43 | 44 | else: 45 | # Проверка открытых\закрытых тикетов 46 | open_ticket = sql.get_total_tickets_by_status_for_user(user_id, "В работе") 47 | closed_ticket = sql.get_total_tickets_by_status_for_user(user_id, "Завершена") 48 | # Чтение профиля 49 | profile = sql.read_profile(user_id) 50 | sql.update_pos('main_menu', 'tg_id', user_id) 51 | organization = profile.get("organization", "Нет данных") 52 | organization_phone = profile.get("organization_phone", "Нет данных") 53 | 54 | text_user = (f"🧑‍💻 Главное меню \n\n" 55 | f"📋 Компания: {organization}\n" 56 | f"☎️ Контактный номер: {organization_phone}\n\n" 57 | 58 | f"📬Открытых заявок: {open_ticket}\n" 59 | f"📭Закрытых заявок: {closed_ticket}\n" 60 | f"\nВыберите интересующее действие ⬇️" 61 | ) 62 | keyboard = InlineKeyboardMarkup() 63 | keyboard.add(InlineKeyboardButton(text="🏢 Моя компания", callback_data="my_company"), 64 | InlineKeyboardButton(text="📥 Мои заявки", callback_data="my_ticket")) 65 | keyboard.add(InlineKeyboardButton(text="📤 Новая заявка", callback_data="new_ticket")) 66 | 67 | # Проверяем, является ли пользователь администратором 68 | if user_id in ADMIN_USERS: 69 | ticket_menu_admin = types.InlineKeyboardButton("🤘 Тикет меню", callback_data="admin_panel") 70 | keyboard.add(ticket_menu_admin) 71 | await message.answer(text_user, reply_markup=keyboard, parse_mode="HTML") 72 | 73 | 74 | # Главное меню пользователя мимикрия под /start 75 | def main_menu(tg_id): 76 | sql.update_pos('main_menu', 'tg_id', tg_id) 77 | user_id = tg_id 78 | open_ticket = sql.get_total_tickets_by_status_for_user(tg_id, "В работе") 79 | closed_ticket = sql.get_total_tickets_by_status_for_user(tg_id, "Завершена") 80 | profile = sql.read_profile(tg_id) 81 | organization = profile.get("organization", "Нет данных") 82 | organization_phone = profile.get("organization_phone", "Нет данных") 83 | 84 | text = (f"🧑‍💻 Главное меню \n\n" 85 | f"📋 Компания: {organization}\n" 86 | f"☎️ Контактный номер: {organization_phone}\n\n" 87 | 88 | f"📬Открытых заявок: {open_ticket}\n" 89 | f"📭Закрытых заявок: {closed_ticket}\n" 90 | f"\nВыберите интересующее действие ⬇️" 91 | ) 92 | keyboard = InlineKeyboardMarkup() 93 | keyboard.add(InlineKeyboardButton(text="🏢 Моя компания", callback_data="my_company"), 94 | InlineKeyboardButton(text="📥 Мои заявки", callback_data="my_ticket")) 95 | keyboard.add(InlineKeyboardButton(text="📤 Новая заявка", callback_data="new_ticket")) 96 | 97 | # Проверяем, является ли пользователь администратором 98 | if user_id in ADMIN_USERS: 99 | ticket_menu_admin = types.InlineKeyboardButton("🤘Тикет меню", callback_data="admin_panel") 100 | keyboard.add(ticket_menu_admin) 101 | return text, keyboard 102 | 103 | 104 | def new_ticket(tg_id): 105 | text = (f"📤 Создание новой заявки\n\n" 106 | # f" - 📝 Опишите вашу проблему.\n" 107 | f" - 🧩 Пожалуйста, опишите вашу проблему и укажите как можно подробнее.\n\n" 108 | f"Пример оформления заявки: \nНе работает принтер на 4 ПК, необходимо проверить подключение.") 109 | keyboard = InlineKeyboardMarkup() 110 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="main_menu")) 111 | return text, keyboard 112 | 113 | 114 | def my_ticket(tg_id): 115 | profile = sql.read_profile(tg_id) 116 | user_tickets_in_progress = sql.get_tickets_in_progress_by_user_id(tg_id) 117 | total_user_tickets_in_progress = len(user_tickets_in_progress) 118 | open_ticket = str(total_user_tickets_in_progress) if total_user_tickets_in_progress else "0" 119 | organization = profile.get("organization") 120 | organization_address = profile.get("organization_adress") 121 | 122 | if user_tickets_in_progress: 123 | text = (f"📥 Мои заявки в работе\n\n" 124 | f"Компания: {organization}\n" 125 | f"Адрес заявки: {organization_address}\n" 126 | f"Заявок в работе: {open_ticket}\n\n" 127 | ) 128 | for ticket in user_tickets_in_progress: 129 | # Использование индексов для доступа к данным кортежа 130 | text += (f"Номер заявки: #{ticket[0]} \n" 131 | f"Описание: {ticket[4]}\n" 132 | f"Дата: {ticket[5]}\n" 133 | f"Статус: {ticket[6]}\n" 134 | ) 135 | else: 136 | text = '📥 Мои заявки \n\nУ вас пока нет заявок в работе.. 🤷‍♂️ \n- Что бы оставить заявку воспользуйтесь меню "📤 Новая заявка"' 137 | 138 | keyboard = InlineKeyboardMarkup() 139 | keyboard.add(InlineKeyboardButton(text="☑️ История заявок", callback_data="my_ticket_history")) 140 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="main_menu")) 141 | return text, keyboard 142 | 143 | 144 | def my_ticket_history(tg_id, page=1, page_size=4): 145 | completed_tickets = sql.get_completed_tickets_by_user(tg_id) 146 | # Проверяем, есть ли завершенные заявки 147 | if completed_tickets: 148 | # Проверяем, нужна ли пагинация 149 | if len(completed_tickets) > page_size: 150 | start_index = (page - 1) * page_size 151 | end_index = start_index + page_size 152 | current_page_tickets = completed_tickets[start_index:end_index] 153 | text = f"📨 История ваших завершенных заявок (страница {page}):\n\n" 154 | else: 155 | current_page_tickets = completed_tickets 156 | text = "📨 История ваших завершенных заявок:\n\n" 157 | 158 | for ticket in current_page_tickets: 159 | text += f"✅\n" \ 160 | f"├ Номер заявки: #{ticket[0]}\n" \ 161 | f"├ Время создания: {ticket[5]}\n" \ 162 | f"├ Сообщение: - {ticket[4]}\n" \ 163 | f"└ Комментарий исполнителя: - {ticket[7]}\n\n" 164 | 165 | else: 166 | text = "🤷‍♂️ Упс.. У вас нет истории заявок." 167 | 168 | keyboard = InlineKeyboardMarkup() 169 | # Создаем кнопки для навигации по страницам, если нужно 170 | if len(completed_tickets) > page_size: 171 | if page > 1: 172 | keyboard.row(InlineKeyboardButton(text="🔙 Предыдущая", callback_data=f"my_ticket_page_{page - 1}")) 173 | if end_index < len(completed_tickets): 174 | keyboard.insert(InlineKeyboardButton(text="🔜 Следующая", callback_data=f"my_ticket_page_{page + 1}")) 175 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="my_ticket")) 176 | return text, keyboard 177 | 178 | 179 | def my_company(tg_id): 180 | profile = sql.read_profile(tg_id) 181 | organization = profile.get("organization", "Нет данных") 182 | organization_address = profile.get("organization_adress", "Нет данных") 183 | organization_inn = profile.get("organization_inn", "Нет данных") 184 | organization_phone = profile.get("organization_phone", "Нет данных") 185 | 186 | # Формирование текста для отображения данных о компании 187 | text = (f"🏢 Информация о компании\n\n" 188 | f"📋 Компания: {organization}\n" 189 | f"📍 Адрес: {organization_address}\n" 190 | f"📑 ИНН: {organization_inn}\n" 191 | f"☎️ Контактный номер: {organization_phone}\n\n" 192 | f"ЗАПОЛНИТЬ ДАННЫЕ О КОМПАНИИ ⬇️ " ) 193 | 194 | keyboard = InlineKeyboardMarkup() 195 | keyboard.add(InlineKeyboardButton(text=f"{'✅' if organization != 'Нет данных' else '❌'} Наименование компании", callback_data="edit_company_name")) 196 | keyboard.add(InlineKeyboardButton(text=f"{'✅' if organization_address != 'Нет данных' else '❌'} Фактический адрес", callback_data="edit_company_adress")) 197 | keyboard.add(InlineKeyboardButton(text=f"{'✅' if organization_inn != 'Нет данных' else '❌'} ИНН", callback_data="edit_company_inn")) 198 | keyboard.add(InlineKeyboardButton(text=f"{'✅' if organization_phone != 'Нет данных' else '❌'} Контактный номер", callback_data="edit_company_phone")) 199 | keyboard.add(InlineKeyboardButton(text="⬅️ В меню", callback_data="main_menu")) 200 | return text, keyboard 201 | 202 | 203 | def edit_company_name(tg_id): 204 | text = f"📋 Введите наименование организации. \nПример: ООО РОГА И КОПЫТА " 205 | keyboard = InlineKeyboardMarkup() 206 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="my_company")) 207 | return text, keyboard 208 | 209 | def edit_company_adress(tg_id): 210 | text = f"📍Введите фактический адрес организации. \nПример: г. Иваново, ул. Пушкина, д. 3 оф. 1 " 211 | keyboard = InlineKeyboardMarkup() 212 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="my_company")) 213 | return text, keyboard 214 | 215 | def edit_company_inn(tg_id): 216 | text = f"📑 Введите ИНН организации. \nПример: 3700010101 " 217 | keyboard = InlineKeyboardMarkup() 218 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="my_company")) 219 | return text, keyboard 220 | 221 | def edit_company_phone(tg_id): 222 | text = f"☎️ Введите контактный номер телефона. \nПример: +79100009999 " 223 | keyboard = InlineKeyboardMarkup() 224 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="my_company")) 225 | return text, keyboard 226 | 227 | def done_ticket(tg_id): 228 | last_ticket_number = sql.get_last_ticket_number() 229 | text = f'🎉🥳 Успех, ваша заявка зарегистрирована! \n\nНомер заявки: #{last_ticket_number}. \n\nPS: Отслеживайте статус поставленных задач в разделе "📥 Мои заявки"' 230 | keyboard = InlineKeyboardMarkup() 231 | keyboard.add(InlineKeyboardButton(text="🧑‍💻 Главное меню", parse_mode="HTML", callback_data="main_menu")) 232 | return text, keyboard 233 | 234 | 235 | # Административный раздел 236 | def admin_panel(): 237 | total_open_tickets = sql.get_total_tickets_by_status_admin("В работе") # Получаем общее количество заявок "В работе" 238 | total_closed_tickets = sql.get_total_tickets_by_status_admin("Завершена") # Получаем общее количество завершенных заявок 239 | all_tickets_in_progress = sql.get_all_tickets_in_progress() 240 | 241 | text = f"🤘 Тикет меню 💲\n\n" 242 | text += f"🔥Заявок в работе: {total_open_tickets}\n" 243 | text += f"👍Завершенных заявок: {total_closed_tickets}\n\n" 244 | text += f"⚠️ Внимание! Закрытые задачи не могут быть возвращены в работу. Пожалуйста, будьте внимательны при их закрытии!" 245 | 246 | keyboard = InlineKeyboardMarkup() 247 | for ticket in all_tickets_in_progress: 248 | ticket_info = f"Заявка #{ticket[0]} - {ticket[5]}" # Номер и описание заявки 249 | keyboard.add(InlineKeyboardButton(text=ticket_info, callback_data=f"ticket_{ticket[0]}")) 250 | keyboard.add(InlineKeyboardButton(text="⬅️ Назад", callback_data="main_menu")) 251 | return text, keyboard 252 | 253 | 254 | 255 | @dp.callback_query_handler(lambda query: query.data.startswith(('ticket_', 'my_ticket_page_'))) 256 | async def handle_ticket_callback(query: types.CallbackQuery): 257 | user_id = query.from_user.id 258 | tg_id = user_id 259 | 260 | 261 | if query.data.startswith('ticket_'): 262 | ticket_id = query.data.split('_')[1] 263 | ticket_info = sql.get_ticket_info(ticket_id) 264 | sql.update_pos(f'ticket_details_{ticket_info[0]}', 'tg_id', user_id) 265 | await query.answer() 266 | text = f"Детали заявки: #{ticket_info[0]}\n\n" \ 267 | f"Пользователь ID: {ticket_info[1]}\n" \ 268 | f"Организация: {ticket_info[2]}\n" \ 269 | f"Адрес: {ticket_info[3]}\n\n" \ 270 | f"Сообщение от пользователя: - {ticket_info[4]}\n\n" \ 271 | f"Время создания: {ticket_info[5]}\n" \ 272 | f"Статус: {ticket_info[6]}\n\n" \ 273 | f"⚠️ Для завершения задачи введите комментарий. В ответ вам придет сообщение с подтвержением!" 274 | 275 | keyboard = types.InlineKeyboardMarkup() 276 | back_button = types.InlineKeyboardButton("⬅️ Назад", callback_data="admin_panel") 277 | 278 | keyboard.add(back_button) 279 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 280 | 281 | 282 | if query.data.startswith('my_ticket_page_'): 283 | page = int(query.data.split('_')[3]) # Получаем номер страницы из колбека 284 | await query.answer() # Ответим на колбек, чтобы убрать "крутилку" 285 | tg_id = query.from_user.id 286 | # Получаем текст сообщения и клавиатуру с учетом текущей страницы 287 | text, keyboard = my_ticket_history(tg_id, page) 288 | # Редактируем сообщение с новым текстом и клавиатурой 289 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 290 | 291 | 292 | 293 | # Группа колбеков на батоны 294 | @dp.callback_query_handler() 295 | async def inline_kb_answer_callback_handler(query: types.CallbackQuery): 296 | user_id = query.from_user.id 297 | tg_id = user_id 298 | 299 | if query.data == 'admin_panel': 300 | # Обновление ячейки 'pos' в базе данных 301 | sql.update_pos('admin_panel', 'tg_id', user_id) 302 | await query.answer() 303 | text, keyboard = admin_panel() 304 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 305 | 306 | if query.data == 'main_menu': 307 | # Обновление ячейки 'pos' в базе данных 308 | sql.update_pos('main_menu', 'tg_id', user_id) 309 | await query.answer() 310 | text, keyboard = main_menu(tg_id) 311 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 312 | 313 | if query.data.startswith('complete_'): 314 | ticket_id = query.data.split('_')[1] 315 | # Обновление ячейки 'pos' в базе данных 316 | sql.update_pos('complete_', 'tg_id', user_id) 317 | await query.answer() 318 | sql.update_ticket_status(ticket_id, "Завершена") 319 | ticket_comm_done = sql.read_ticket_comment(ticket_id) 320 | ticket_info = sql.get_ticket_info(ticket_id) 321 | 322 | current_time = datetime.datetime.now() 323 | time_ticket = datetime.datetime.strptime(ticket_info[5], "%Y-%m-%d %H:%M:%S") 324 | time_difference = current_time - time_ticket 325 | 326 | # Преобразуем общее количество секунд в объект timedelta 327 | total_seconds = time_difference.total_seconds() 328 | hours = int(total_seconds // 3600) 329 | 330 | # Отправка сообщения пользователю о завершении задачи 331 | user_id = ticket_info[1] # ID пользователя, поставившего задачу 332 | completion_message = f"🎉 Задача #{ticket_id} выполнена!\nВремя выполнения: {hours} часа(ов).\n\nОтвет исполнителя: - {ticket_comm_done}\n\n⚠️ Пожалуйста, проверьте корректность исполнения задачи." 333 | 334 | back_button_user = types.InlineKeyboardButton("🧑‍💻 Главное меню", callback_data="main_menu") 335 | history_ticket = InlineKeyboardButton(text="☑️ История заявок", callback_data="my_ticket_history") 336 | keyboard_markup_user = types.InlineKeyboardMarkup().add(history_ticket, back_button_user) 337 | 338 | back_button_admin = types.InlineKeyboardButton("🤘Тикет меню", callback_data="admin_panel") 339 | keyboard_markup_admin = types.InlineKeyboardMarkup().add(back_button_admin) 340 | 341 | await bot.send_message(user_id, completion_message, reply_markup=keyboard_markup_user, parse_mode="HTML") 342 | await bot.send_message(query.from_user.id, completion_message, reply_markup=keyboard_markup_admin, parse_mode="HTML") 343 | 344 | 345 | if query.data == 'my_company': 346 | # Обновление ячейки 'pos' в базе данных 347 | sql.update_pos('my_company', 'tg_id', user_id) 348 | await query.answer() 349 | text, keyboard = my_company(tg_id) 350 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 351 | 352 | if query.data == 'edit_company_name': 353 | # Обновление ячейки 'pos' в базе данных 354 | sql.update_pos('edit_company_name', 'tg_id', user_id) 355 | await query.answer() 356 | text, keyboard = edit_company_name(tg_id) 357 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 358 | 359 | if query.data == 'edit_company_adress': 360 | # Обновление ячейки 'pos' в базе данных 361 | sql.update_pos('edit_company_adress', 'tg_id', user_id) 362 | await query.answer() 363 | text, keyboard = edit_company_adress(tg_id) 364 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 365 | 366 | if query.data == 'edit_company_inn': 367 | # Обновление ячейки 'pos' в базе данных 368 | sql.update_pos('edit_company_inn', 'tg_id', user_id) 369 | await query.answer() 370 | text, keyboard = edit_company_inn(tg_id) 371 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 372 | 373 | if query.data == 'edit_company_phone': 374 | # Обновление ячейки 'pos' в базе данных 375 | sql.update_pos('edit_company_phone', 'tg_id', user_id) 376 | await query.answer() 377 | text, keyboard = edit_company_phone(tg_id) 378 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 379 | 380 | if query.data == 'new_ticket': 381 | # Обновление ячейки 'pos' в базе данных 382 | sql.update_pos('new_ticket', 'tg_id', user_id) 383 | await query.answer() 384 | text, keyboard = new_ticket(tg_id) 385 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 386 | 387 | if query.data == 'my_ticket': 388 | # Обновление ячейки 'pos' в базе данных 389 | sql.update_pos('my_ticket', 'tg_id', user_id) 390 | await query.answer() 391 | text, keyboard = my_ticket(tg_id) 392 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 393 | 394 | if query.data == 'my_ticket_history': 395 | # Обновление ячейки 'pos' в базе данных 396 | sql.update_pos('my_ticket_history', 'tg_id', user_id) 397 | await query.answer() 398 | text, keyboard = my_ticket_history(tg_id) 399 | await query.message.edit_text(text, reply_markup=keyboard, parse_mode="HTML") 400 | 401 | 402 | 403 | 404 | # Обратотка текстовых сообщений 405 | @dp.message_handler() 406 | async def handle_text_input(message: types.Message): 407 | 408 | user_id = message.from_user.id 409 | username = message.from_user.username 410 | profile = sql.read_profile(user_id) 411 | organization_name = profile.get("organization", "") 412 | organization_address = profile.get("organization_adress", "") 413 | organization_phone = profile.get("organization_phone", "Нет данных") 414 | user_position = sql.read_cell('pos', 'tg_id', user_id) 415 | 416 | if user_position.startswith('ticket_details_'): 417 | parts = user_position.split('_') 418 | if len(parts) == 3 and parts[2].isdigit(): 419 | ticket_id = int(parts[2]) 420 | # Обновление комментария в базе данных 421 | comment_text = message.text 422 | sql.update_ticket_comment(ticket_id, comment_text) 423 | 424 | # Создаем кнопку "✅ Выполнить" 425 | complete_button = types.InlineKeyboardButton("✅ Завершить задачу", callback_data=f"complete_{ticket_id}") 426 | keyboard = types.InlineKeyboardMarkup() 427 | keyboard.add(complete_button) 428 | 429 | # Вставляем переменные в текст сообщения 430 | success_message = f"Комментарий к тикету #{ticket_id} успешно записан!\n\nОтвет исполнителя: - {comment_text}\n\n⚠️ Если вы допустили ошибку, просто отправьте исправленное сообщение еще раз." 431 | await message.reply(success_message, reply_markup=keyboard, parse_mode="HTML") 432 | else: 433 | await message.reply("Ошибка формата номера тикета", parse_mode="HTML") 434 | 435 | if user_position == 'edit_company_name': 436 | sql.update_profile_data(user_id, 'organization', message.text) 437 | text, keyboard = my_company(user_id) 438 | await message.reply(text, reply_markup=keyboard, parse_mode="HTML") 439 | 440 | 441 | if user_position == 'edit_company_adress': 442 | sql.update_profile_data(user_id, 'organization_adress', message.text) 443 | text, keyboard = my_company(user_id) 444 | await message.reply(text, reply_markup=keyboard, parse_mode="HTML") 445 | 446 | if user_position == 'edit_company_inn': 447 | sql.update_profile_data(user_id, 'organization_inn', message.text) 448 | text, keyboard = my_company(user_id) 449 | await message.reply(text, reply_markup=keyboard, parse_mode="HTML") 450 | 451 | if user_position == 'edit_company_phone': 452 | sql.update_profile_data(user_id, 'organization_phone', message.text) 453 | text, keyboard = my_company(user_id) 454 | await message.reply(text, reply_markup=keyboard, parse_mode="HTML") 455 | 456 | if user_position == 'new_ticket': 457 | user_ticket = user_id 458 | organization = organization_name 459 | addres_ticket = organization_address 460 | message_ticket = message.text 461 | time_ticket = message.date 462 | state_ticket = "В работе" 463 | ticket_comm = "" 464 | 465 | # Добавляем новую заявку в базу данных 466 | sql.add_ticket(user_ticket, organization, addres_ticket, message_ticket, time_ticket, state_ticket, ticket_comm) 467 | # Получаем номер последней добавленной заявки 468 | last_ticket_number = sql.get_last_ticket_number() 469 | 470 | if last_ticket_number: 471 | # Обновляем профиль пользователя с номером последней добавленной заявки 472 | sql.update_profile_data(user_id, 'history_ticket', str(last_ticket_number)) 473 | sql.update_profile_data(user_id, 'data_ticket', str(time_ticket)) 474 | sql.update_profile_data(user_id, 'user_name', str(username)) 475 | 476 | 477 | # Меню благодарочки 478 | text, keyboard = done_ticket(user_id) 479 | await message.reply(text, reply_markup=keyboard, parse_mode="HTML") 480 | 481 | admin_panel = types.InlineKeyboardButton("🤘Тикет меню🫰", callback_data="admin_panel") 482 | keyboard_markup = types.InlineKeyboardMarkup().add(admin_panel) 483 | 484 | # Отправка сообщения администратору 485 | admin_text = (f"📬❗️\nПользователь @{username} создал новую заявку с номером #{last_ticket_number}." 486 | f"\n\nСообщение от пользователя:\n - {message_ticket}" 487 | f"\n\nТелефон: {organization_phone}\n" 488 | f"Компания: {organization}\n" 489 | f"Адрес: {addres_ticket}\n" 490 | ) 491 | 492 | # Добавляем клавиатуру к уведомлению 493 | await bot.send_message(ADMIN_MESSAGE, admin_text, parse_mode="HTML", reply_markup=keyboard_markup) 494 | else: 495 | await message.reply("Ошибка при получении заявки.") 496 | 497 | 498 | if __name__ == '__main__': 499 | executor = aiogram.executor.Executor(dp, loop=loop, skip_updates=True) 500 | executor.start_polling() 501 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==2.25.2 2 | --------------------------------------------------------------------------------