Compare commits
46 Commits
search_bot
...
d1ad0b16a1
| Author | SHA1 | Date | |
|---|---|---|---|
| d1ad0b16a1 | |||
| 32f1cd9513 | |||
| 51b632ec7d | |||
| 54b019f3b9 | |||
| 7c900e5502 | |||
| 7d67e3ffc2 | |||
| af0caee963 | |||
| 0dca28ef20 | |||
| de47c39e76 | |||
| f0be37b18f | |||
| 9aedbd4c1f | |||
| 977e7369e3 | |||
| 23a7d20722 | |||
| 1caae96a97 | |||
| 90bac4e343 | |||
| 7517448508 | |||
| f98ba35a60 | |||
| 0ff328694c | |||
| 04430e56ef | |||
| 0b76fbaf17 | |||
| f4c21c825c | |||
| 3fa7543477 | |||
| 064451dc7c | |||
| c76914444c | |||
| 09b19f9ced | |||
| 9f76064826 | |||
| 49a986fa65 | |||
| 2c39e5b50b | |||
| 26b58f2909 | |||
| d9528486ef | |||
| 525899557b | |||
| 03339885ff | |||
| 8fa42206aa | |||
| be12045fa8 | |||
| 7c0aaf5392 | |||
| 55a95889b0 | |||
| 8029f7fb2d | |||
| 2fa4ff2781 | |||
| aadbaab321 | |||
| 54f0d074ba | |||
| 642f2bba6a | |||
| 77cbb1d352 | |||
| 8e686b1ca3 | |||
| d30ceeda6b | |||
| 0168a8b0a5 | |||
| 260b6c3a8a |
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Проверка наличия пользователя в группе 2FA через Active Directory.
|
||||||
|
Использует credentials и настройки из config/config.py.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python check_ad_2fa.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from ldap3 import Server, Connection, ALL, SUBTREE
|
||||||
|
|
||||||
|
# Подтягиваем конфиг
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASES
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
|
|
||||||
|
def find_user(login: str) -> dict | None:
|
||||||
|
"""Найти пользователя в AD по логину (sAMAccountName или mail).
|
||||||
|
Ищет по всем OU из AD_BASES."""
|
||||||
|
filter_login = login.lower().replace("\\", "\\5c").replace("*", "\\2a").replace("(", "\\28").replace(")", "\\29")
|
||||||
|
|
||||||
|
if "@" in filter_login:
|
||||||
|
ldap_filter = "(|{}{})".format("(mail={})".format(filter_login), "(sAMAccountName={})".format(filter_login))
|
||||||
|
else:
|
||||||
|
ldap_filter = "(|{}{})".format("(sAMAccountName={})".format(filter_login), "(mail={})".format(filter_login))
|
||||||
|
|
||||||
|
entries = ad_search_all_bases(ldap_filter, ["cn", "sAMAccountName", "memberOf"])
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
|
||||||
|
entry = entries[0]
|
||||||
|
return {
|
||||||
|
"dn": entry.entry_dn,
|
||||||
|
"cn": str(entry.cn),
|
||||||
|
"sam": str(entry.sAMAccountName),
|
||||||
|
"groups": [str(g) for g in entry.memberOf],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def check_2fa_group(login: str) -> dict:
|
||||||
|
"""Проверить, входит ли пользователь в группу 2FA."""
|
||||||
|
user = find_user(login)
|
||||||
|
if user is None:
|
||||||
|
return {"found": False, "error": f"Пользователь '{login}' не найден в AD."}
|
||||||
|
|
||||||
|
# Ищем группу, содержащую "2FA" в имени
|
||||||
|
groups_2fa = [g for g in user["groups"] if "2FA" in g.upper()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"found": True,
|
||||||
|
"user": user,
|
||||||
|
"in_2fa_group": len(groups_2fa) > 0,
|
||||||
|
"matching_groups": groups_2fa,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(" Проверка пользователя в группе 2FA (Active Directory)")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"\n Сервер AD: {AD_SERVER}")
|
||||||
|
print(f" Базы поиска: {config.AD_BASES}")
|
||||||
|
print(f" Подключено как: {AD_USER}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
login = input(" Введите логин (sAMAccountName или email) или 'q' для выхода:\n > ").strip()
|
||||||
|
if not login or login.lower() == "q":
|
||||||
|
print("\n До свидания!\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
result = check_2fa_group(login)
|
||||||
|
|
||||||
|
if not result["found"]:
|
||||||
|
print(f"\n ❌ {result['error']}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
u = result["user"]
|
||||||
|
print(f"\n ✅ Пользователь найден:")
|
||||||
|
print(f" CN: {u['cn']}")
|
||||||
|
print(f" sAM: {u['sam']}")
|
||||||
|
print(f" DN: {u['dn']}")
|
||||||
|
print(f" Групп: {len(u['groups'])}")
|
||||||
|
|
||||||
|
if result["in_2fa_group"]:
|
||||||
|
print(f"\n 🟢 Пользователь ВХОДИТ в группу 2FA:")
|
||||||
|
for g in result["matching_groups"]:
|
||||||
|
print(f" {g}")
|
||||||
|
else:
|
||||||
|
print(f"\n 🔴 Пользователь НЕ найден в группе 2FA")
|
||||||
|
if u["groups"]:
|
||||||
|
print(f"\n Все группы пользователя ({len(u['groups'])}):")
|
||||||
|
for g in u["groups"]:
|
||||||
|
print(f" {g}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n Прервано пользователем.\n")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n ❌ Ошибка: {e}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Проверочный скрипт: user_id -> CARD_ID -> телефон.
|
||||||
|
|
||||||
|
Интерактивный режим: после каждого запроса спрашивает,
|
||||||
|
хочешь ли продолжить.
|
||||||
|
|
||||||
|
config.py сам подтягивает все секреты из Passwork при импорте.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# config.py при импорте заполняет globals():
|
||||||
|
# AD_USER, AD_PASSWORD, AD_SERVER, AD_BASES
|
||||||
|
# DB_USER, DB_PASSWORD, SQL_SERVER, SQL_DB_NAME
|
||||||
|
# ============================================================
|
||||||
|
sys.path.insert(0, "/opt/trueconf_bot")
|
||||||
|
from config import config
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# AD: user_id -> CARD_ID
|
||||||
|
# ============================================================
|
||||||
|
def get_card_id_from_ad(user_id: str) -> str:
|
||||||
|
"""Ищем пользователя в AD по всем OU из AD_BASES, возвращаем extensionAttribute2 (CARD_ID)."""
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
|
|
||||||
|
entries = search_by_user_id(user_id, ["extensionAttribute2"])
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
raise Exception("Пользователь не найден в AD")
|
||||||
|
|
||||||
|
entry = entries[0]
|
||||||
|
|
||||||
|
if "extensionAttribute2" in entry and entry.extensionAttribute2.value:
|
||||||
|
card_id = str(entry.extensionAttribute2.value).strip()
|
||||||
|
log.info("CARD_ID: {}".format(card_id))
|
||||||
|
return card_id
|
||||||
|
|
||||||
|
raise Exception("extensionAttribute2 пуст для этого пользователя в AD")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# SQL: CARD_ID -> телефон в PR_CARD
|
||||||
|
# ============================================================
|
||||||
|
def get_phone_from_db(card_id: str) -> str:
|
||||||
|
"""Ищем сотрудника в UOV_SELFSERVICE_PR_CARD по CARD_ID."""
|
||||||
|
import pymssql
|
||||||
|
|
||||||
|
conn = pymssql.connect(
|
||||||
|
server=config.SQL_SERVER,
|
||||||
|
user=config.DB_USER,
|
||||||
|
password=config.DB_PASSWORD,
|
||||||
|
database=config.SQL_DB_NAME,
|
||||||
|
charset="cp1251",
|
||||||
|
)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT c.ID, c.PHONE, c.F_NAME, c.L_NAME, c.M_NAME
|
||||||
|
FROM UOV_SELFSERVICE_PR_CARD c
|
||||||
|
WHERE c.ID = %s
|
||||||
|
"""
|
||||||
|
cursor.execute(query, (card_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
raise Exception("CARD_ID {} не найден в PR_CARD".format(card_id))
|
||||||
|
|
||||||
|
card_id_db = row[0]
|
||||||
|
phone = str(row[1]) if row[1] else "Не указан"
|
||||||
|
f_name = str(row[2]) if row[2] else ""
|
||||||
|
l_name = str(row[3]) if row[3] else ""
|
||||||
|
m_name = str(row[4]) if row[4] else ""
|
||||||
|
|
||||||
|
log.info("PR_CARD.ID: {}".format(card_id_db))
|
||||||
|
log.info("ФИО: {} {} {}".format(l_name, f_name, m_name))
|
||||||
|
log.info("PHONE: {}".format(phone))
|
||||||
|
return phone
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# MAIN
|
||||||
|
# ============================================================
|
||||||
|
def run_one():
|
||||||
|
"""Выполнить один запрос: ввод user_id -> CARD_ID -> телефон."""
|
||||||
|
user_id = input("\nВведите user_id (email или sAMAccountName): ").strip()
|
||||||
|
if not user_id:
|
||||||
|
print("user_id пустой, выход.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("[1] Поиск CARD_ID в Active Directory")
|
||||||
|
print(" Сервер: {}".format(config.AD_SERVER))
|
||||||
|
print(" Базы поиска: {}".format(config.AD_BASES))
|
||||||
|
print(" user_id: {}".format(user_id))
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
card_id = get_card_id_from_ad(user_id)
|
||||||
|
except Exception as e:
|
||||||
|
print("Ошибка AD: {}".format(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("[2] Поиск телефона в базе BossCopy")
|
||||||
|
print(" Сервер: {}".format(config.SQL_SERVER))
|
||||||
|
print(" CARD_ID: {}".format(card_id))
|
||||||
|
print()
|
||||||
|
|
||||||
|
try:
|
||||||
|
phone = get_phone_from_db(card_id)
|
||||||
|
except Exception as e:
|
||||||
|
print("Ошибка SQL: {}".format(e))
|
||||||
|
return card_id
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print(" ИТОГ")
|
||||||
|
print("=" * 70)
|
||||||
|
print(" user_id: {}".format(user_id))
|
||||||
|
print(" CARD_ID: {}".format(card_id))
|
||||||
|
print(" Телефон: {}".format(phone))
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
return card_id
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print(" Проверка: user_id -> CARD_ID -> телефон")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
run_one()
|
||||||
|
|
||||||
|
again = input("Продолжить? (y/n): ").strip().lower()
|
||||||
|
if again not in ("y", "yes", "д", "да"):
|
||||||
|
print("Готово.")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+11
-9
@@ -81,20 +81,19 @@ for var_name, card_name in CREDENTIALS_MAP.items():
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
# 1. ГЛОБАЛЬНЫЕ ФЛАГИ МОДУЛЕЙ
|
# 1. ГЛОБАЛЬНЫЕ ФЛАГИ МОДУЛЕЙ
|
||||||
# =========================================================
|
# =========================================================
|
||||||
ENABLE_SERVICE_DESK = True # Модуль создания заявок в техподдержку
|
ENABLE_SERVICE_DESK = False # Модуль создания заявок в техподдержку
|
||||||
ENABLE_PHOTO_BOT = True # Модуль обработки фото/сканов
|
ENABLE_PHOTO_BOT = False # Модуль обработки фото/сканов
|
||||||
ENABLE_TRANSCRIPTION = True # Модуль расшифровки голосовых сообщений (Whisper)
|
ENABLE_TRANSCRIPTION = True # Модуль расшифровки голосовых сообщений (Whisper)
|
||||||
ENABLE_SEARCH_BOT = True # Модуль интеллектуального поиска по регламентам (RAG)
|
ENABLE_SEARCH_BOT = False # Модуль интеллектуального поиска по регламентам (RAG)
|
||||||
|
ENABLE_INSTRUCT = False # Модуль инструкций (Trueconf, Почта)
|
||||||
ENABLE_LK = True # Личный кабинет сотрудника (отпуска, расчетные листки, справки)
|
ENABLE_LK = True # Личный кабинет сотрудника (отпуска, расчетные листки, справки)
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 2. КОНТРОЛЬ ДОСТУПА
|
# 2. КОНТРОЛЬ ДОСТУПА
|
||||||
# =========================================================
|
# =========================================================
|
||||||
ENABLE_WHITELIST = True # Включить режим закрытого бета-тестирования
|
ENABLE_WHITELIST = False # Включить режим закрытого бета-тестирования
|
||||||
ALLOWED_USERS = [ # Список TrueConf ID разрешенных пользователей
|
ALLOWED_USERS = [ # Список TrueConf ID разрешенных пользователей
|
||||||
"ds.krivochenko@tcs.sibcem.ru",
|
|
||||||
"mm.norenberg@tcs.sibcem.ru",
|
|
||||||
"a.kirpichev@tcs.sibcem.ru"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@@ -103,7 +102,10 @@ ALLOWED_USERS = [ # Список TrueConf ID разрешенны
|
|||||||
TC_SERVER = os.getenv("TC_SERVER", "") # Адрес сервера TrueConf
|
TC_SERVER = os.getenv("TC_SERVER", "") # Адрес сервера TrueConf
|
||||||
|
|
||||||
AD_SERVER = "ldap://172.16.20.20" # Адрес контроллера домена Active Directory
|
AD_SERVER = "ldap://172.16.20.20" # Адрес контроллера домена Active Directory
|
||||||
AD_BASE = "OU=-Пользователи,DC=sibcem,DC=ru" # Базовый путь поиска пользователей в AD
|
AD_BASES = ( # Базовые пути поиска пользователей в AD (несколько OU)
|
||||||
|
"OU=-Пользователи,DC=sibcem,DC=ru",
|
||||||
|
"OU=Планшеты,OU=enabled,OU=БезКомпьютеров,DC=sibcem,DC=ru",
|
||||||
|
)
|
||||||
|
|
||||||
SQL_SERVER = "SRVKEM-MOBILEIN.sibcem.ru" # Сервер MS SQL для работы Личного кабинета
|
SQL_SERVER = "SRVKEM-MOBILEIN.sibcem.ru" # Сервер MS SQL для работы Личного кабинета
|
||||||
SQL_DB_NAME = "BossCopy" # База данных с информацией по кадрам и ЗП
|
SQL_DB_NAME = "BossCopy" # База данных с информацией по кадрам и ЗП
|
||||||
@@ -201,7 +203,7 @@ DEBUG_TEXT_LIMIT = 250
|
|||||||
# =========================================================
|
# =========================================================
|
||||||
# 9. ПОДСИСТЕМА МОНИТОРИНГА И СТАТУСА БОТА (HEARTBEAT)
|
# 9. ПОДСИСТЕМА МОНИТОРИНГА И СТАТУСА БОТА (HEARTBEAT)
|
||||||
# =========================================================
|
# =========================================================
|
||||||
ADMIN_CHAT_ID = "744bad93e58c4c498e15468562c6a2da3b610e9c"
|
ADMIN_CHAT_ID = "54966d1f56c2fda8e263a70c3a65ff0fad27ef7d"
|
||||||
ENABLE_HEARTBEAT_MONITOR = True
|
ENABLE_HEARTBEAT_MONITOR = True
|
||||||
HEARTBEAT_CHECK_INTERVAL_SEC = 10.0
|
HEARTBEAT_CHECK_INTERVAL_SEC = 10.0
|
||||||
HEARTBEAT_MAX_FAILURES = 3
|
HEARTBEAT_MAX_FAILURES = 3
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Debug: print the exact LDAP filter that works in lk/handlers.py"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/opt/trueconf_bot")
|
||||||
|
from config.config import AD_USER, AD_PASSWORD, AD_SERVER, AD_BASES
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
|
|
||||||
|
# Simulate lk/handlers.py filter
|
||||||
|
user_id = "ds.krivochenko@tcs.sibcem.ru"
|
||||||
|
search_id = user_id.replace("@tcs.sibcem.ru", "@sibcem.ru")
|
||||||
|
short_username = search_id.split("@")[0]
|
||||||
|
safe_user_id = escape_filter_chars(search_id)
|
||||||
|
|
||||||
|
print(f"user_id: {user_id}")
|
||||||
|
print(f"search_id: {search_id}")
|
||||||
|
print(f"short_username: {short_username}")
|
||||||
|
print(f"safe_user_id: {safe_user_id}")
|
||||||
|
|
||||||
|
# lk/handlers.py filter
|
||||||
|
filter_lk = "(&(objectClass=user)(|(mail={})(userPrincipalName={})(userPrincipalName={}{}{}))({}))".format(
|
||||||
|
safe_user_id,
|
||||||
|
safe_user_id,
|
||||||
|
escape_filter_chars(short_username),
|
||||||
|
"@sibcem.ru",
|
||||||
|
"",
|
||||||
|
escape_filter_chars(short_username),
|
||||||
|
)
|
||||||
|
print(f"\nFilter (lk format):")
|
||||||
|
print(filter_lk)
|
||||||
|
|
||||||
|
# Count parens
|
||||||
|
opens = filter_lk.count('(')
|
||||||
|
closes = filter_lk.count(')')
|
||||||
|
print(f"\nOpen parens: {opens}, Close parens: {closes}")
|
||||||
|
|
||||||
|
# Now test with the connection
|
||||||
|
print(f"\nSearching with filter across all AD_BASES...")
|
||||||
|
entries = ad_search_all_bases(filter_lk, ["cn", "sAMAccountName", "mail", "userPrincipalName"])
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
entry = entries[0]
|
||||||
|
print(f"FOUND: cn={entry.cn}, mail={entry.mail if 'mail' in entry else 'N/A'}, sam={entry.sAMAccountName if 'sAMAccountName' in entry else 'N/A'}")
|
||||||
|
else:
|
||||||
|
print("NOT FOUND in any AD_BASE")
|
||||||
|
|
||||||
|
# Try root
|
||||||
|
root_base = "DC=sibcem,DC=ru"
|
||||||
|
print(f"\nSearching in {root_base}...")
|
||||||
|
from utils.ad_search import search_by_filter
|
||||||
|
entries2 = search_by_filter(filter_lk, ["cn", "sAMAccountName", "mail", "userPrincipalName"])
|
||||||
|
|
||||||
|
if entries2:
|
||||||
|
entry = entries2[0]
|
||||||
|
print(f"FOUND: cn={entry.cn}, mail={entry.mail if 'mail' in entry else 'N/A'}, sam={entry.sAMAccountName if 'sAMAccountName' in entry else 'N/A'}")
|
||||||
|
else:
|
||||||
|
print("NOT FOUND in root")
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from trueconf import Router, Message
|
||||||
|
from trueconf.types import FSInputFile
|
||||||
|
|
||||||
|
from utils.texts import (
|
||||||
|
UNKNOWN_MAIN_CMD_TEXT,
|
||||||
|
EMOJI_DIGITS,
|
||||||
|
INSTRUCT_MAIN_MENU_TEXT,
|
||||||
|
INSTRUCT_TRUECONF_TEXT,
|
||||||
|
INSTRUCT_TRUECONF_WITH_AD,
|
||||||
|
INSTRUCT_EMAIL_TEXT,
|
||||||
|
)
|
||||||
|
from utils.states import get_state, set_state, clear_state
|
||||||
|
from utils.stats_logger import log_menu_stats
|
||||||
|
|
||||||
|
# Безопасный импорт конфигурации
|
||||||
|
try:
|
||||||
|
from config.config import DEFAULT_REQUESTER, SD_URL, SD_TOKEN
|
||||||
|
except ImportError:
|
||||||
|
import importlib.util
|
||||||
|
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config", "config.py")
|
||||||
|
spec = importlib.util.spec_from_file_location("custom_config", config_path)
|
||||||
|
custom_config = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(custom_config)
|
||||||
|
DEFAULT_REQUESTER = custom_config.DEFAULT_REQUESTER
|
||||||
|
SD_URL = custom_config.SD_URL
|
||||||
|
SD_TOKEN = custom_config.SD_TOKEN
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
# Путь к файлу инструкции Trueconf
|
||||||
|
TRUECONF_INSTRUCT_PATH = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
"Проверка_наличия_и_авторизация_на_мобильном_устройстве_Trueconf.docx"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_cn(user_id: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Получить CN пользователя из AD по его user_id (email или логин).
|
||||||
|
user_id из TrueConf — это обычно email, например 'ds.krivochenko@tcs.sibcem.ru'
|
||||||
|
"""
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
search_id = user_id.replace("@tcs.sibcem.ru", "@sibcem.ru")
|
||||||
|
short_username = search_id.split("@")[0] if "@" in search_id else user_id
|
||||||
|
|
||||||
|
logger.info(f"AD lookup CN: user_id={user_id}, search_id={search_id}, short={short_username}")
|
||||||
|
|
||||||
|
entries = search_by_user_id(search_id, ["cn", "sAMAccountName", "mail", "userPrincipalName", "l", "userAccountControl"])
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
entry = entries[0]
|
||||||
|
cn = str(entry.cn)
|
||||||
|
mail = str(entry.mail) if 'mail' in entry else ""
|
||||||
|
sam = str(entry.sAMAccountName) if 'sAMAccountName' in entry else "N/A"
|
||||||
|
upn = str(entry.userPrincipalName) if 'userPrincipalName' in entry else "N/A"
|
||||||
|
logger.info(f"AD found: cn={cn}, mail={mail}, sam={sam}, upn={upn}")
|
||||||
|
return cn
|
||||||
|
|
||||||
|
logger.warning(f"AD not found for user_id={user_id}, search_id={search_id}, short={short_username}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения CN из AD для {user_id}: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ad_user_info(user_id: str) -> dict | None:
|
||||||
|
"""
|
||||||
|
Получить полную информацию о пользователе из AD.
|
||||||
|
Возвращает {cn, mail, city} или None.
|
||||||
|
"""
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
|
|
||||||
|
try:
|
||||||
|
search_id = user_id.replace("@tcs.sibcem.ru", "@sibcem.ru")
|
||||||
|
entries = search_by_user_id(search_id, ["cn", "mail", "l", "userAccountControl"])
|
||||||
|
|
||||||
|
if entries:
|
||||||
|
entry = entries[0]
|
||||||
|
cn = str(entry.cn)
|
||||||
|
raw_mail = str(entry.mail) if 'mail' in entry else ""
|
||||||
|
# mail может быть "[]" (пустой LDAP объект), "" или "N/A" — фильтруем
|
||||||
|
mail = raw_mail if raw_mail and raw_mail not in ("[]", "N/A", "") else None
|
||||||
|
city = str(entry.l) if 'l' in entry and entry.l.value else "Кемерово"
|
||||||
|
return {"cn": cn, "mail": mail, "city": city}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка получения AD info для {user_id}: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_sd_api_request(requester_email: str, subject: str, description: str, city: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Автономная прямая отправка HTTP-запроса в Service Desk API v3.
|
||||||
|
"""
|
||||||
|
# Если запрашивает системный ИИ или не указан email — ищем по ИМЕНИ
|
||||||
|
if requester_email == DEFAULT_REQUESTER or requester_email == "ai@sibcem.ru":
|
||||||
|
requester_payload = {"name": "Искусственный Интеллект"}
|
||||||
|
else:
|
||||||
|
requester_payload = {"email_id": requester_email}
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"request": {
|
||||||
|
"subject": subject,
|
||||||
|
"description": description,
|
||||||
|
"requester": requester_payload,
|
||||||
|
"udf_fields": {
|
||||||
|
"udf_pick_301": city
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{SD_URL.rstrip('/')}/api/v3/requests"
|
||||||
|
headers = {"TECHNICIAN_KEY": SD_TOKEN}
|
||||||
|
data = {"input_data": json.dumps(payload)}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(verify=False) as client:
|
||||||
|
response = await client.post(url, headers=headers, data=data, timeout=15.0)
|
||||||
|
|
||||||
|
if response.status_code in (200, 201):
|
||||||
|
res_json = response.json()
|
||||||
|
ticket_id = res_json.get("request", {}).get("id")
|
||||||
|
return str(ticket_id) if ticket_id else None
|
||||||
|
else:
|
||||||
|
logger.error(f"Service Desk API Error ({response.status_code}): {response.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def create_sd_ticket_for_trueconf(user_id: str, cn: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Создать заявку в Service Desk для запроса доступа к Trueconf.
|
||||||
|
Полностью автономная функция.
|
||||||
|
"""
|
||||||
|
subject = "Запрос доступа к Trueconf"
|
||||||
|
|
||||||
|
# Пытаемся получить информацию из AD
|
||||||
|
ad_info = get_ad_user_info(user_id)
|
||||||
|
logger.info(f"AD search result for SD ticket ({user_id}): {ad_info}")
|
||||||
|
|
||||||
|
if ad_info and ad_info.get("mail"):
|
||||||
|
requester_email = ad_info["mail"]
|
||||||
|
city = ad_info.get("city", "Кемерово")
|
||||||
|
description = (
|
||||||
|
f"Пользователь {cn} ({user_id}) отсутствует группа 2FA."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
requester_email = DEFAULT_REQUESTER
|
||||||
|
city = "Кемерово"
|
||||||
|
logger.info(f"Using DEFAULT_REQUESTER ({requester_email}) for user {user_id} (cn={cn})")
|
||||||
|
|
||||||
|
if ad_info and not ad_info.get("mail"):
|
||||||
|
description = (
|
||||||
|
f"Пользователь {cn} ({user_id}) отсутствует группа 2FA."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
description = (
|
||||||
|
f"Пользователь {cn} ({user_id}) отсутствует группа 2FA."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Calling SD API directly: requester={requester_email}, city={city}")
|
||||||
|
ticket_id = await _send_sd_api_request(requester_email, subject, description, city)
|
||||||
|
if ticket_id:
|
||||||
|
logger.info(f"SD ticket created for {cn}: #{ticket_id} (requester={requester_email})")
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to create SD ticket for {cn}")
|
||||||
|
return ticket_id
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка создания SD заявки для {cn}: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@router.message()
|
||||||
|
async def instruct_router_handler(msg: Message):
|
||||||
|
user_id = msg.from_user.id
|
||||||
|
user_name = getattr(msg.from_user, 'name', 'N/A')
|
||||||
|
user_username = getattr(msg.from_user, 'username', 'N/A')
|
||||||
|
|
||||||
|
logger.info(f"Instruct: user_id={user_id}, name={user_name}, username={user_username}")
|
||||||
|
|
||||||
|
current_state = get_state(user_id)
|
||||||
|
|
||||||
|
if not msg.text:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = msg.text.strip().lower()
|
||||||
|
|
||||||
|
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
||||||
|
if cmd == emoji_num:
|
||||||
|
cmd = raw_num
|
||||||
|
break
|
||||||
|
|
||||||
|
if current_state is None:
|
||||||
|
from utils.menu import MENU_MAP
|
||||||
|
if MENU_MAP.get(cmd) == "INSTRUCT":
|
||||||
|
msg.handled = True
|
||||||
|
set_state(user_id, "INSTRUCT_MODE")
|
||||||
|
await msg.answer(INSTRUCT_MAIN_MENU_TEXT, parse_mode="html")
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
if current_state == "INSTRUCT_MODE":
|
||||||
|
msg.handled = True
|
||||||
|
if cmd == "0":
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Выход в главное меню")
|
||||||
|
clear_state(user_id)
|
||||||
|
from utils.menu import MENU_TEXT
|
||||||
|
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||||
|
elif cmd == "1":
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Trueconf")
|
||||||
|
set_state(user_id, "INSTRUCT_TRUECONF_VIEW")
|
||||||
|
|
||||||
|
# Получаем CN пользователя
|
||||||
|
cn = get_user_cn(user_id)
|
||||||
|
if cn is None:
|
||||||
|
cn = user_id # fallback
|
||||||
|
|
||||||
|
# Проверяем наличие группы 2FA в AD перед созданием заявки
|
||||||
|
from utils.ad_checker import check_group_membership
|
||||||
|
ad_check = check_group_membership(cn, "2FA")
|
||||||
|
|
||||||
|
ticket_id = None
|
||||||
|
# Создаём заявку в SD ТОЛЬКО если у пользователя НЕТ группы 2FA (или он не найден)
|
||||||
|
if not ad_check.get("in_group", False):
|
||||||
|
ticket_id = await create_sd_ticket_for_trueconf(user_id, cn)
|
||||||
|
else:
|
||||||
|
logger.info(f"User {cn} already has 2FA group. SD ticket creation skipped.")
|
||||||
|
|
||||||
|
# Показываем текст с результатом
|
||||||
|
text = INSTRUCT_TRUECONF_WITH_AD(EMOJI_DIGITS, cn, ticket_id)
|
||||||
|
await msg.answer(text, parse_mode="html")
|
||||||
|
|
||||||
|
# Отправляем документ-инструкцию
|
||||||
|
if os.path.isfile(TRUECONF_INSTRUCT_PATH):
|
||||||
|
await msg.answer_document(FSInputFile(TRUECONF_INSTRUCT_PATH))
|
||||||
|
elif cmd == "2":
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Почта")
|
||||||
|
set_state(user_id, "INSTRUCT_EMAIL_VIEW")
|
||||||
|
await msg.answer(INSTRUCT_EMAIL_TEXT, parse_mode="html")
|
||||||
|
else:
|
||||||
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
|
|
||||||
|
elif current_state in ["INSTRUCT_TRUECONF_VIEW", "INSTRUCT_EMAIL_VIEW"]:
|
||||||
|
msg.handled = True
|
||||||
|
if cmd == "0":
|
||||||
|
clear_state(user_id)
|
||||||
|
from utils.menu import MENU_TEXT
|
||||||
|
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||||
|
elif cmd == "9":
|
||||||
|
set_state(user_id, "INSTRUCT_MODE")
|
||||||
|
await msg.answer(INSTRUCT_MAIN_MENU_TEXT, parse_mode="html")
|
||||||
|
else:
|
||||||
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
Binary file not shown.
Binary file not shown.
+56
-31
@@ -57,7 +57,7 @@ EMOJI_DIGITS = {
|
|||||||
"10": "🔟"
|
"10": "🔟"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Список из 7 справок строго по структуру портала
|
# Список из 7 справок строго по структуре портала
|
||||||
DOC_TYPES = {
|
DOC_TYPES = {
|
||||||
"1": "Справка о доходах физического лица (2-НДФЛ)",
|
"1": "Справка о доходах физического лица (2-НДФЛ)",
|
||||||
"2": "Справка об удержаниях за ДМС",
|
"2": "Справка об удержаниях за ДМС",
|
||||||
@@ -74,7 +74,7 @@ LK_DOCS_MENU_TEXT = (
|
|||||||
f"\n\n<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
f"\n\n<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_COOLDOWN_SEC = 1.5
|
USER_COOLDOWN_SEC = 0.5
|
||||||
EMAIL_COOLDOWN_SEC = 10.0
|
EMAIL_COOLDOWN_SEC = 10.0
|
||||||
|
|
||||||
user_last_request = {}
|
user_last_request = {}
|
||||||
@@ -129,19 +129,17 @@ async def handle_lk_error(msg: Message, user_id: str, action: str, error_details
|
|||||||
# ИНТЕГРАЦИЯ С ACTIVE DIRECTORY ЧЕРЕЗ extensionAttribute2
|
# ИНТЕГРАЦИЯ С ACTIVE DIRECTORY ЧЕРЕЗ extensionAttribute2
|
||||||
# =========================================================
|
# =========================================================
|
||||||
def get_card_id_from_ad(user_id: str) -> str:
|
def get_card_id_from_ad(user_id: str) -> str:
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
try:
|
try:
|
||||||
server = Server(config.AD_SERVER, get_info=ALL)
|
search_id = user_id.replace("@tcs.sibcem.ru", "@sibcem.ru")
|
||||||
conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
entries = search_by_user_id(search_id, attributes=['extensionAttribute2'])
|
||||||
safe_user_id = escape_filter_chars(user_id)
|
|
||||||
short_username = user_id.split('@')[0]
|
|
||||||
search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))"
|
|
||||||
|
|
||||||
conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2'])
|
if entries and 'extensionAttribute2' in entries[0] and entries[0].extensionAttribute2.value:
|
||||||
|
return str(entries[0].extensionAttribute2.value).strip()
|
||||||
|
|
||||||
if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value:
|
|
||||||
return str(conn.entries[0].extensionAttribute2.value).strip()
|
|
||||||
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
||||||
except Exception as e: raise Exception(f"AD Card Query Error: {e}")
|
except Exception as e:
|
||||||
|
raise Exception(f"AD Card Query Error: {e}")
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# МОДНОЕ УНИВЕРСАЛЬНОЕ ЯДРО ПАГИНАЦИИ СПИСКОВ ДЛЯ ЧАТА
|
# МОДНОЕ УНИВЕРСАЛЬНОЕ ЯДРО ПАГИНАЦИИ СПИСКОВ ДЛЯ ЧАТА
|
||||||
@@ -206,12 +204,25 @@ async def send_dynamic_lk_main_menu(msg: Message, user_id: str):
|
|||||||
await msg.answer(menu_text, parse_mode="html")
|
await msg.answer(menu_text, parse_mode="html")
|
||||||
|
|
||||||
async def send_tabel_months_menu(msg: Message, user_id: str):
|
async def send_tabel_months_menu(msg: Message, user_id: str):
|
||||||
now = datetime(2026, 6, 3)
|
now = datetime.now()
|
||||||
prev_1 = now.replace(day=1) - timedelta(days=1)
|
m1 = now.replace(day=1) - timedelta(days=1) # Прошлый месяц (M-1)
|
||||||
prev_2 = prev_1.replace(day=1) - timedelta(days=1)
|
m2 = m1.replace(day=1) - timedelta(days=1) # Позапрошлый месяц (M-2)
|
||||||
months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
|
||||||
|
months_ru = {
|
||||||
|
1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь",
|
||||||
|
7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"
|
||||||
|
}
|
||||||
|
|
||||||
|
m1_str = f"{months_ru[m1.month]} {m1.year} г."
|
||||||
|
m2_str = f"{months_ru[m2.month]} {m2.year} г."
|
||||||
|
|
||||||
|
menu_text = (
|
||||||
|
"⏱ <b>Выберите месяц для просмотра табеля:</b>\n\n"
|
||||||
|
f"{EMOJI_DIGITS['1']} — {m1_str}\n"
|
||||||
|
f"{EMOJI_DIGITS['2']} — {m2_str}\n\n"
|
||||||
|
f"<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
menu_text = lk_tabel_months_menu_text(prev_1, prev_2, months_ru, EMOJI_DIGITS)
|
|
||||||
set_state(user_id, "LK_TABEL_MONTHS")
|
set_state(user_id, "LK_TABEL_MONTHS")
|
||||||
await msg.answer(menu_text, parse_mode="html")
|
await msg.answer(menu_text, parse_mode="html")
|
||||||
|
|
||||||
@@ -267,13 +278,22 @@ async def build_confirmation_text(user_id: str, doc: dict) -> str:
|
|||||||
async def lk_router_handler(msg: Message):
|
async def lk_router_handler(msg: Message):
|
||||||
user_id = msg.from_user.id
|
user_id = msg.from_user.id
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if current_time - user_last_request.get(user_id, 0) < USER_COOLDOWN_SEC: return
|
current_state = get_state(user_id)
|
||||||
|
|
||||||
|
# Если пользователь находится внутри меню ЛК, помечаем сообщение обработанным,
|
||||||
|
# чтобы оно не улетало в ServiceDesk / Instruct роутеры
|
||||||
|
if current_state and current_state.startswith("LK_"):
|
||||||
|
msg.handled = True
|
||||||
|
|
||||||
|
# Проверка на слишком частые клики (кулдаун)
|
||||||
|
if current_time - user_last_request.get(user_id, 0) < USER_COOLDOWN_SEC:
|
||||||
|
return
|
||||||
user_last_request[user_id] = current_time
|
user_last_request[user_id] = current_time
|
||||||
|
|
||||||
current_state = get_state(user_id)
|
|
||||||
nav_text = lk_nav_text(EMOJI_DIGITS)
|
nav_text = lk_nav_text(EMOJI_DIGITS)
|
||||||
|
|
||||||
if not msg.text: return
|
if not msg.text:
|
||||||
|
return
|
||||||
cmd = msg.text.strip().lower()
|
cmd = msg.text.strip().lower()
|
||||||
|
|
||||||
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
||||||
@@ -372,7 +392,6 @@ async def lk_router_handler(msg: Message):
|
|||||||
set_state(user_id, "LK_DOCS_MENU")
|
set_state(user_id, "LK_DOCS_MENU")
|
||||||
await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||||
else:
|
else:
|
||||||
# 🎯 ИСПРАВЛЕНО: Вместо спама копии меню ЛК выводим строгое предупреждение
|
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
|
|
||||||
elif current_state == "LK_TABEL_MONTHS":
|
elif current_state == "LK_TABEL_MONTHS":
|
||||||
@@ -385,16 +404,28 @@ async def lk_router_handler(msg: Message):
|
|||||||
set_state(user_id, "LK_MODE")
|
set_state(user_id, "LK_MODE")
|
||||||
await send_dynamic_lk_main_menu(msg, user_id)
|
await send_dynamic_lk_main_menu(msg, user_id)
|
||||||
elif cmd in ["1", "2"]:
|
elif cmd in ["1", "2"]:
|
||||||
log_menu_stats(user_id, "Личный кабинет", "Просмотр табеля за месяц")
|
log_menu_stats(user_id, "Личный кабинет", f"Просмотр табеля (вариант {cmd})")
|
||||||
await msg.answer(lk_tabel_loading(EMOJI_DIGITS), parse_mode="html")
|
await msg.answer(lk_tabel_loading(EMOJI_DIGITS), parse_mode="html")
|
||||||
try:
|
try:
|
||||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||||
now = datetime(2026, 6, 3)
|
now = datetime.now()
|
||||||
target_date = now.replace(day=1) - timedelta(days=1) if cmd == "1" else (now.replace(day=1) - timedelta(days=1)).replace(day=1) - timedelta(days=1)
|
|
||||||
tabel_report = await asyncio.to_thread(get_tabel_report_text, card_id, target_date.year, target_date.month)
|
m1 = now.replace(day=1) - timedelta(days=1)
|
||||||
|
if cmd == "1":
|
||||||
|
target_date = m1 # Прошлый месяц
|
||||||
|
elif cmd == "2":
|
||||||
|
target_date = m1.replace(day=1) - timedelta(days=1) # Позапрошлый месяц
|
||||||
|
|
||||||
|
tabel_report = await asyncio.to_thread(
|
||||||
|
get_tabel_report_text,
|
||||||
|
card_id,
|
||||||
|
target_date.year,
|
||||||
|
target_date.month
|
||||||
|
)
|
||||||
set_state(user_id, "LK_TABEL_VIEW")
|
set_state(user_id, "LK_TABEL_VIEW")
|
||||||
await msg.answer(tabel_report + nav_text, parse_mode="html")
|
await msg.answer(tabel_report + nav_text, parse_mode="html")
|
||||||
except Exception as e: await handle_lk_error(msg, user_id, "Формирование табеля", str(e))
|
except Exception as e:
|
||||||
|
await handle_lk_error(msg, user_id, "Формирование табеля", str(e))
|
||||||
else:
|
else:
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
|
|
||||||
@@ -578,7 +609,6 @@ async def lk_router_handler(msg: Message):
|
|||||||
set_state(user_id, "LK_DOCS_QUANTITY")
|
set_state(user_id, "LK_DOCS_QUANTITY")
|
||||||
await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||||
else:
|
else:
|
||||||
# 🎯 ИСПРАВЛЕНО: Заменен спам списка документов на чистую ошибку ввода
|
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
|
|
||||||
elif current_state == "LK_DOCS_QUANTITY":
|
elif current_state == "LK_DOCS_QUANTITY":
|
||||||
@@ -606,7 +636,6 @@ async def lk_router_handler(msg: Message):
|
|||||||
confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id])
|
confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id])
|
||||||
await msg.answer(confirm_txt, parse_mode="html")
|
await msg.answer(confirm_txt, parse_mode="html")
|
||||||
else:
|
else:
|
||||||
# 🎯 ИСПРАВЛЕНО: Вместо кастомной подсказки выводим единый стандарт
|
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
|
|
||||||
elif current_state == "LK_DOCS_PERIOD":
|
elif current_state == "LK_DOCS_PERIOD":
|
||||||
@@ -619,14 +648,12 @@ async def lk_router_handler(msg: Message):
|
|||||||
if cmd in ["1", "2", "3", "4", "5", "6", "7", "8"]:
|
if cmd in ["1", "2", "3", "4", "5", "6", "7", "8"]:
|
||||||
user_docs_session[user_id]['period'] = f"за {datetime.now().year - (int(cmd) - 1)} год"
|
user_docs_session[user_id]['period'] = f"за {datetime.now().year - (int(cmd) - 1)} год"
|
||||||
else:
|
else:
|
||||||
# 🎯 ИСПРАВЛЕНО: Убран спам меню годов при абракадабре
|
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
return
|
return
|
||||||
elif doc_id == "5":
|
elif doc_id == "5":
|
||||||
if cmd == "1": user_docs_session[user_id]['period'] = "до 1.5 лет"
|
if cmd == "1": user_docs_session[user_id]['period'] = "до 1.5 лет"
|
||||||
elif cmd == "2": user_docs_session[user_id]['period'] = "до 3 лет"
|
elif cmd == "2": user_docs_session[user_id]['period'] = "до 3 лет"
|
||||||
else:
|
else:
|
||||||
# 🎯 ИСПРАВЛЕНО: Убран спам меню возраста при некорректном вводе
|
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
return
|
return
|
||||||
elif doc_id == "3":
|
elif doc_id == "3":
|
||||||
@@ -670,7 +697,6 @@ async def lk_router_handler(msg: Message):
|
|||||||
except Exception as e: await handle_lk_error(msg, user_id, f"Заказ справки {user_docs_session.get(user_id, {}).get('doc_id')}", str(e)); set_state(user_id, "LK_MODE")
|
except Exception as e: await handle_lk_error(msg, user_id, f"Заказ справки {user_docs_session.get(user_id, {}).get('doc_id')}", str(e)); set_state(user_id, "LK_MODE")
|
||||||
finally: user_docs_session.pop(user_id, None)
|
finally: user_docs_session.pop(user_id, None)
|
||||||
else:
|
else:
|
||||||
# 🎯 ИСПРАВЛЕНО: Удален спам-текст подтверждения
|
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
|
|
||||||
elif current_state == "LK_DOCS_SUCCESS":
|
elif current_state == "LK_DOCS_SUCCESS":
|
||||||
@@ -678,5 +704,4 @@ async def lk_router_handler(msg: Message):
|
|||||||
if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||||
elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||||
else:
|
else:
|
||||||
# 🎯 ИСПРАВЛЕНО: Унифицировано под общий шаблон ошибки
|
|
||||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||||
+18
-9
@@ -13,7 +13,10 @@ from ldap3.utils.conv import escape_filter_chars
|
|||||||
TEST_USER = "man.bogov@tcs.sibcem.ru"
|
TEST_USER = "man.bogov@tcs.sibcem.ru"
|
||||||
|
|
||||||
AD_SERVER = "ldap://172.16.20.20"
|
AD_SERVER = "ldap://172.16.20.20"
|
||||||
AD_BASE = "OU=-Пользователи,DC=sibcem,DC=ru"
|
AD_BASES = (
|
||||||
|
"OU=-Пользователи,DC=sibcem,DC=ru",
|
||||||
|
"OU=Планшеты,OU=enabled,OU=БезКомпьютеров,DC=sibcem,DC=ru",
|
||||||
|
)
|
||||||
|
|
||||||
AD_USER = "sdesk-mail"
|
AD_USER = "sdesk-mail"
|
||||||
AD_PASSWORD = "fne?e!q.m8phcrGVAcqr"
|
AD_PASSWORD = "fne?e!q.m8phcrGVAcqr"
|
||||||
@@ -32,7 +35,6 @@ DB_PASSWORD = "Bav:fX#UwH8%atv4"
|
|||||||
def get_phone_from_ad(user_id: str) -> str:
|
def get_phone_from_ad(user_id: str) -> str:
|
||||||
|
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
server = Server(AD_SERVER, get_info=ALL)
|
||||||
|
|
||||||
conn = Connection(
|
conn = Connection(
|
||||||
server,
|
server,
|
||||||
user=AD_USER,
|
user=AD_USER,
|
||||||
@@ -41,22 +43,29 @@ def get_phone_from_ad(user_id: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
safe_user_id = escape_filter_chars(user_id)
|
safe_user_id = escape_filter_chars(user_id)
|
||||||
|
|
||||||
short_username = user_id.split("@")[0]
|
short_username = user_id.split("@")[0]
|
||||||
|
|
||||||
search_filter = (
|
search_filter = (
|
||||||
f"(&(objectClass=user)"
|
"(&(objectClass=user)"
|
||||||
f"(|(mail={safe_user_id})"
|
"(|(mail={})"
|
||||||
f"(userPrincipalName={safe_user_id})"
|
"(userPrincipalName={})"
|
||||||
f"(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})"
|
"(userPrincipalName={}))"
|
||||||
f"(sAMAccountName={escape_filter_chars(short_username)})))"
|
"(sAMAccountName={}))".format(
|
||||||
|
safe_user_id,
|
||||||
|
safe_user_id,
|
||||||
|
escape_filter_chars(short_username) + "@sibcem.ru",
|
||||||
|
escape_filter_chars(short_username),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for base in AD_BASES:
|
||||||
conn.search(
|
conn.search(
|
||||||
AD_BASE,
|
base,
|
||||||
search_filter,
|
search_filter,
|
||||||
attributes=['mobile', 'telephoneNumber']
|
attributes=['mobile', 'telephoneNumber']
|
||||||
)
|
)
|
||||||
|
if conn.entries:
|
||||||
|
break
|
||||||
|
|
||||||
if not conn.entries:
|
if not conn.entries:
|
||||||
raise Exception("Пользователь не найден в AD")
|
raise Exception("Пользователь не найден в AD")
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ log_ram_usage("После импорта transcription_router (Виспер)")
|
|||||||
from search_bot.handlers import router as search_router, SEARCH_MAIN_MENU_TEXT
|
from search_bot.handlers import router as search_router, SEARCH_MAIN_MENU_TEXT
|
||||||
log_ram_usage("После импорта search_router")
|
log_ram_usage("После импорта search_router")
|
||||||
|
|
||||||
|
from instruct.handlers import router as instruct_router
|
||||||
|
log_ram_usage("После импорта instruct_router")
|
||||||
|
|
||||||
from lk.handlers import router as lk_router
|
from lk.handlers import router as lk_router
|
||||||
log_ram_usage("После импорта lk_router (Личный кабинет)")
|
log_ram_usage("После импорта lk_router (Личный кабинет)")
|
||||||
|
|
||||||
@@ -139,6 +142,18 @@ async def main_menu(msg: Message):
|
|||||||
from lk.handlers import send_dynamic_lk_main_menu
|
from lk.handlers import send_dynamic_lk_main_menu
|
||||||
await send_dynamic_lk_main_menu(msg, user_id)
|
await send_dynamic_lk_main_menu(msg, user_id)
|
||||||
return
|
return
|
||||||
|
elif action == "INSTRUCT":
|
||||||
|
USER_MENU_SHOWN[user_id] = True
|
||||||
|
set_state(user_id, "INSTRUCT_MODE")
|
||||||
|
log_menu_stats(user_id, "Инструкции", "Вход")
|
||||||
|
await msg.answer(
|
||||||
|
"📋 <b>Выберите инструкцию:</b>\n\n"
|
||||||
|
"1⃣ — 🖥 Trueconf\n"
|
||||||
|
"2⃣ — 📧 Почта\n\n"
|
||||||
|
"<i>0⃣ — В главное меню</i>",
|
||||||
|
parse_mode="html"
|
||||||
|
)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
if not menu_shown:
|
if not menu_shown:
|
||||||
USER_MENU_SHOWN[user_id] = True
|
USER_MENU_SHOWN[user_id] = True
|
||||||
@@ -156,6 +171,7 @@ async def main():
|
|||||||
dp.include_router(sd_router)
|
dp.include_router(sd_router)
|
||||||
dp.include_router(transcription_router)
|
dp.include_router(transcription_router)
|
||||||
dp.include_router(search_router)
|
dp.include_router(search_router)
|
||||||
|
dp.include_router(instruct_router)
|
||||||
dp.include_router(lk_router)
|
dp.include_router(lk_router)
|
||||||
dp.include_router(main_router)
|
dp.include_router(main_router)
|
||||||
bot = Bot.from_credentials(server=TC_SERVER, username=TC_LOGIN, password=TC_PASSWORD, dispatcher=dp, verify_ssl=False)
|
bot = Bot.from_credentials(server=TC_SERVER, username=TC_LOGIN, password=TC_PASSWORD, dispatcher=dp, verify_ssl=False)
|
||||||
|
|||||||
+15
-13
@@ -5,37 +5,39 @@ from ldap3 import Server, Connection, ALL, MODIFY_REPLACE
|
|||||||
|
|
||||||
# Подтягиваем конфиг
|
# Подтягиваем конфиг
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE
|
from config.config import AD_SERVER, AD_USER, AD_PASSWORD
|
||||||
|
from utils.ad_search import search_by_login as ad_search_all_bases
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def upload_photo_to_ad(target_user: str, photo_path: str) -> dict:
|
def upload_photo_to_ad(target_user: str, photo_path: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Читает готовое фото с диска и записывает его в атрибут thumbnailPhoto в AD.
|
Читает готовое фото с диска и записывает его в атрибут thumbnailPhoto в AD.
|
||||||
|
Ищет пользователя по всем OU из AD_BASES.
|
||||||
"""
|
"""
|
||||||
|
from utils.ad_search import search_by_filter
|
||||||
try:
|
try:
|
||||||
# Читаем сырые байты картинки
|
# Читаем сырые байты картинки
|
||||||
with open(photo_path, "rb") as f:
|
with open(photo_path, "rb") as f:
|
||||||
photo_bytes = f.read()
|
photo_bytes = f.read()
|
||||||
|
|
||||||
# Формируем фильтр для поиска
|
# Ищем пользователя в AD по всем OU
|
||||||
if '@' in target_user:
|
if '@' in target_user:
|
||||||
ldap_filter = f"(mail={target_user})"
|
ldap_filter = "(mail={})".format(target_user)
|
||||||
else:
|
else:
|
||||||
ldap_filter = f"(sAMAccountName={target_user})"
|
ldap_filter = "(sAMAccountName={})".format(target_user)
|
||||||
|
|
||||||
# Подключаемся к AD
|
entries = search_by_filter(ldap_filter, ["cn"])
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return {"success": False, "error": "Пользователь {} не найден в AD.".format(target_user)}
|
||||||
|
|
||||||
|
user_dn = entries[0].entry_dn
|
||||||
|
|
||||||
|
# Подключаемся для модификации
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
server = Server(AD_SERVER, get_info=ALL)
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
||||||
|
|
||||||
# Находим пользователя в дереве (нам нужен его точный путь - DN)
|
|
||||||
conn.search(search_base=AD_BASE, search_filter=ldap_filter, attributes=["cn"])
|
|
||||||
|
|
||||||
if not conn.entries:
|
|
||||||
return {"success": False, "error": f"Пользователь {target_user} не найден в AD."}
|
|
||||||
|
|
||||||
user_dn = conn.entries[0].entry_dn
|
|
||||||
|
|
||||||
# 🪄 МАГИЯ: Перезаписываем атрибут thumbnailPhoto нашими байтами
|
# 🪄 МАГИЯ: Перезаписываем атрибут thumbnailPhoto нашими байтами
|
||||||
conn.modify(user_dn, {'thumbnailPhoto': [(MODIFY_REPLACE, [photo_bytes])]})
|
conn.modify(user_dn, {'thumbnailPhoto': [(MODIFY_REPLACE, [photo_bytes])]})
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from PIL import Image
|
|||||||
|
|
||||||
# Подтягиваем ваши настройки из конфига
|
# Подтягиваем ваши настройки из конфига
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
from config.config import AD_SERVER, AD_USER, AD_PASSWORD, AD_BASE
|
from config.config import AD_SERVER, AD_USER, AD_PASSWORD
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||||
|
|
||||||
@@ -21,22 +22,14 @@ def check_user_photo(target_user: str):
|
|||||||
ldap_filter = f"(sAMAccountName={target_user})"
|
ldap_filter = f"(sAMAccountName={target_user})"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Подключаемся к AD
|
# Подключаемся к AD и ищем по всем OU из AD_BASES
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
entries = ad_search_all_bases(ldap_filter, ["cn", "thumbnailPhoto"])
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
|
||||||
|
|
||||||
# Ищем пользователя и просим вернуть атрибуты: имя (cn) и фото (thumbnailPhoto)
|
if not entries:
|
||||||
conn.search(
|
|
||||||
search_base=AD_BASE,
|
|
||||||
search_filter=ldap_filter,
|
|
||||||
attributes=["cn", "thumbnailPhoto"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not conn.entries:
|
|
||||||
print("❌ Пользователь с такими данными не найден в AD.")
|
print("❌ Пользователь с такими данными не найден в AD.")
|
||||||
return
|
return
|
||||||
|
|
||||||
user = conn.entries[0]
|
user = entries[0]
|
||||||
user_name = user.cn.value if 'cn' in user else target_user
|
user_name = user.cn.value if 'cn' in user else target_user
|
||||||
print(f"👤 Найден сотрудник: {user_name}")
|
print(f"👤 Найден сотрудник: {user_name}")
|
||||||
|
|
||||||
|
|||||||
@@ -72,11 +72,10 @@ sd_sessions = {}
|
|||||||
# ================================================
|
# ================================================
|
||||||
def get_ad_user_sync(login: str):
|
def get_ad_user_sync(login: str):
|
||||||
try:
|
try:
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
from utils.ad_search import search_by_login
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
entries = search_by_login(login, ["displayName", "mail", "l", "userAccountControl"])
|
||||||
conn.search(search_base=AD_BASE, search_filter=f"(sAMAccountName={login})", attributes=["displayName", "mail", "l", "userAccountControl"])
|
if entries:
|
||||||
if conn.entries:
|
user = entries[0]
|
||||||
user = conn.entries[0]
|
|
||||||
uac = user.userAccountControl.value if 'userAccountControl' in user else 0
|
uac = user.userAccountControl.value if 'userAccountControl' in user else 0
|
||||||
return {
|
return {
|
||||||
"name": user.displayName.value if 'displayName' in user else login,
|
"name": user.displayName.value if 'displayName' in user else login,
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Проверка подключения к SQL Server и загрузки данных табеля
|
||||||
|
за последние 5 месяцев для ds.krivochenko@sibcem.ru.
|
||||||
|
|
||||||
|
Цепочка поиска сотрудника (как в LK модуле бота):
|
||||||
|
1. AD: search_by_user_id(email) → extensionAttribute2 = CARD_ID
|
||||||
|
2. SQL: SELECT ID FROM UOV_SELFSERVICE_PR_EMP WHERE CARD_ID = %s → EMP_ID
|
||||||
|
3. SQL: SELECT ... FROM UOV_SELFSERVICE_TB_TABEL WHERE EMP_ID = %s → табель
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
cd /opt/trueconf_bot
|
||||||
|
python3 test_tabel_check.py
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import calendar
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 1. Загрузка .env
|
||||||
|
# ============================================================
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ENV_PATH = os.path.join(BASE_DIR, "config", ".env")
|
||||||
|
|
||||||
|
def load_env(filepath):
|
||||||
|
try:
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if line and not line.startswith("#"):
|
||||||
|
if "=" in line:
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
os.environ[key.strip()] = value.strip(' "\'\r\n')
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"⚠️ Файл {filepath} не найден.")
|
||||||
|
|
||||||
|
load_env(ENV_PATH)
|
||||||
|
|
||||||
|
# Карта соответствия: Имя переменной в боте -> Название карточки в сейфе
|
||||||
|
CREDENTIALS_MAP = {
|
||||||
|
"DB_PASSWORD": os.getenv("PW_ID_SQL", "").strip(' "\'\r\n'),
|
||||||
|
"AD_PASSWORD": os.getenv("PW_ID_AD", "").strip(' "\'\r\n'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 2. Загрузка паролей из Passwork
|
||||||
|
# ============================================================
|
||||||
|
DB_USER = ""
|
||||||
|
DB_PASSWORD = ""
|
||||||
|
AD_USER = ""
|
||||||
|
AD_PASSWORD = ""
|
||||||
|
|
||||||
|
for pw_path in ["/opt/passwork", os.path.join(BASE_DIR, "passwork"), os.path.join(BASE_DIR, "config", "passwork")]:
|
||||||
|
if os.path.isdir(pw_path) and pw_path not in sys.path:
|
||||||
|
sys.path.insert(0, pw_path)
|
||||||
|
if BASE_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, BASE_DIR)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from passwork import get_passwork_secrets
|
||||||
|
print("🔐 Загрузка секретов из Passwork...")
|
||||||
|
required_cards = [name for name in CREDENTIALS_MAP.values() if name]
|
||||||
|
passwork_pool = get_passwork_secrets(required_cards=required_cards)
|
||||||
|
for var_name, card_name in CREDENTIALS_MAP.items():
|
||||||
|
if not card_name:
|
||||||
|
continue
|
||||||
|
clean_card = card_name.strip(' "\'\r\n')
|
||||||
|
card_data = passwork_pool.get(clean_card)
|
||||||
|
if card_data is None:
|
||||||
|
print(f" ⚠️ Карточка '{clean_card}' не найдена")
|
||||||
|
continue
|
||||||
|
globals()[var_name] = card_data["password"].strip(' "\'\r\n')
|
||||||
|
if var_name == "AD_PASSWORD":
|
||||||
|
globals()["AD_USER"] = card_data.get("login", "").strip(' "\'\r\n')
|
||||||
|
elif var_name == "DB_PASSWORD":
|
||||||
|
globals()["DB_USER"] = card_data.get("login", "").strip(' "\'\r\n')
|
||||||
|
print(f" ✓ {var_name} из карточки '{clean_card}'")
|
||||||
|
except ImportError:
|
||||||
|
print("⚠️ Модуль passwork не найден")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Ошибка Passwork: {e}")
|
||||||
|
|
||||||
|
if not DB_PASSWORD:
|
||||||
|
DB_PASSWORD = os.getenv("DB_PASSWORD", "").strip()
|
||||||
|
DB_USER = os.getenv("DB_USER", DB_USER).strip()
|
||||||
|
if not AD_PASSWORD:
|
||||||
|
AD_PASSWORD = os.getenv("AD_PASSWORD", "").strip()
|
||||||
|
AD_USER = os.getenv("AD_USER", AD_USER).strip()
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 3. Настройки подключения
|
||||||
|
# ============================================================
|
||||||
|
DB_SERVER = os.getenv("SQL_SERVER", "SRVKEM-MOBILEIN.sibcem.ru")
|
||||||
|
DB_NAME = os.getenv("SQL_DB_NAME", "BossCopy")
|
||||||
|
AD_SERVER = os.getenv("AD_SERVER", "ldap://172.16.20.20")
|
||||||
|
AD_BASES = (
|
||||||
|
"OU=-Пользователи,DC=sibcem,DC=ru",
|
||||||
|
"OU=Планшеты,OU=enabled,OU=БезКомпьютеров,DC=sibcem,DC=ru",
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Проверка подключения к SQL Server")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f" Сервер : {DB_SERVER}")
|
||||||
|
print(f" База : {DB_NAME}")
|
||||||
|
print(f" Логин : {DB_USER}")
|
||||||
|
print(f" Пароль : {'*' * len(DB_PASSWORD) if DB_PASSWORD else '(не задан)'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 4. Проверка pymssql
|
||||||
|
# ============================================================
|
||||||
|
try:
|
||||||
|
import pymssql
|
||||||
|
print(f"✅ pymssql загружен (версия {pymssql.__version__})")
|
||||||
|
except ImportError:
|
||||||
|
print("❌ pymssql не установлен!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 5. Подключение к SQL Server
|
||||||
|
# ============================================================
|
||||||
|
try:
|
||||||
|
conn = pymssql.connect(
|
||||||
|
server=DB_SERVER,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD,
|
||||||
|
database=DB_NAME,
|
||||||
|
charset='cp1251',
|
||||||
|
login_timeout=10
|
||||||
|
)
|
||||||
|
print("✅ Подключение к SQL Server успешно!")
|
||||||
|
cursor = conn.cursor()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка подключения: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 6. Поиск сотрудника в AD по email → CARD_ID (как в LK модуле)
|
||||||
|
# ============================================================
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Поиск сотрудника в Active Directory")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
TARGET_EMAIL = "ds.krivochenko@sibcem.ru"
|
||||||
|
print(f" Ищем: {TARGET_EMAIL}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ldap3 import Server, Connection, ALL
|
||||||
|
|
||||||
|
server = Server(AD_SERVER, get_info=ALL)
|
||||||
|
ldap_conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
||||||
|
|
||||||
|
# Как в ad_search.py: ищем по mail, userPrincipalName, sAMAccountName
|
||||||
|
short_username = TARGET_EMAIL.split("@")[0] if "@" in TARGET_EMAIL else TARGET_EMAIL
|
||||||
|
|
||||||
|
or_parts = [
|
||||||
|
"(mail={})".format(TARGET_EMAIL),
|
||||||
|
"(userPrincipalName={})".format(TARGET_EMAIL),
|
||||||
|
"(userPrincipalName={})".format(short_username + "@sibcem.ru"),
|
||||||
|
"(sAMAccountName={})".format(short_username),
|
||||||
|
]
|
||||||
|
or_filter = "(|{})".format("".join(or_parts))
|
||||||
|
search_filter = "(&{}{})".format("(objectClass=user)", or_filter)
|
||||||
|
|
||||||
|
print(f" LDAP-фильтр: {search_filter}")
|
||||||
|
|
||||||
|
card_id_from_ad = None
|
||||||
|
|
||||||
|
for base in AD_BASES:
|
||||||
|
ldap_conn.search(
|
||||||
|
search_base=base,
|
||||||
|
search_filter=search_filter,
|
||||||
|
attributes=["cn", "sAMAccountName", "mail", "userPrincipalName", "extensionAttribute2"],
|
||||||
|
)
|
||||||
|
if ldap_conn.entries:
|
||||||
|
entry = ldap_conn.entries[0]
|
||||||
|
print(f" ✅ Найден в OU: {base}")
|
||||||
|
print(f" CN : {entry.cn}")
|
||||||
|
print(f" sAMAccountName : {entry.sAMAccountName}")
|
||||||
|
print(f" mail : {entry.mail}")
|
||||||
|
ext2 = str(entry.extensionAttribute2) if hasattr(entry, 'extensionAttribute2') and entry.extensionAttribute2 else "(не задан)"
|
||||||
|
print(f" extensionAttribute2: {ext2}")
|
||||||
|
card_id_from_ad = ext2
|
||||||
|
break
|
||||||
|
|
||||||
|
if not ldap_conn.entries:
|
||||||
|
print(f" ❌ Сотрудник не найден в AD по email {TARGET_EMAIL}")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("❌ ldap3 не установлен")
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка LDAP: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 7. Поиск EMP_ID по CARD_ID в SQL (как в LK модуле)
|
||||||
|
# ============================================================
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Поиск EMP_ID по CARD_ID в SQL")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
emp_id = None
|
||||||
|
card_id = None
|
||||||
|
|
||||||
|
if card_id_from_ad:
|
||||||
|
card_id = card_id_from_ad
|
||||||
|
print(f" CARD_ID из AD: {card_id}")
|
||||||
|
|
||||||
|
# Как в info_service.py: SELECT e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s
|
||||||
|
cursor.execute("SELECT e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s", (card_id,))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
emp_id = row[0]
|
||||||
|
print(f" ✅ Найден: emp_id={emp_id}")
|
||||||
|
else:
|
||||||
|
print(f" ❌ Сотрудник с CARD_ID={card_id} не найден в UOV_SELFSERVICE_PR_EMP")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 8. Загрузка табеля за последние 5 месяцев
|
||||||
|
# ============================================================
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(" Загрузка табеля за последние 5 месяцев")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
if not emp_id:
|
||||||
|
print(" ❌ Не удалось найти сотрудника — завершение")
|
||||||
|
conn.close()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
months_back = 5
|
||||||
|
|
||||||
|
for i in range(months_back):
|
||||||
|
month = now.month - i
|
||||||
|
year = now.year
|
||||||
|
while month <= 0:
|
||||||
|
month += 12
|
||||||
|
year -= 1
|
||||||
|
|
||||||
|
month_name = calendar.month_name[month]
|
||||||
|
print(f"\n📅 {month_name} {year}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT e.D, e.TDAY_ID, e.H
|
||||||
|
FROM UOV_SELFSERVICE_TB_TABEL e
|
||||||
|
WHERE e.EMP_ID = %s AND YEAR(e.D) = %s AND MONTH(e.D) = %s
|
||||||
|
ORDER BY e.D ASC
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
cursor.execute(query, (emp_id, year, month))
|
||||||
|
records = cursor.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Ошибка запроса: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
print(f" ⚠️ Данные табеля отсутствуют")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f" Записей: {len(records)}")
|
||||||
|
|
||||||
|
total_days = 0
|
||||||
|
total_hours = 0.0
|
||||||
|
codes = {}
|
||||||
|
|
||||||
|
for r in records:
|
||||||
|
tday_id = str(r[1]).strip() if r[1] else "?"
|
||||||
|
hours = float(r[2]) if r[2] is not None else 0.0
|
||||||
|
codes[tday_id] = codes.get(tday_id, 0) + 1
|
||||||
|
if tday_id == 'Я':
|
||||||
|
total_days += 1
|
||||||
|
total_hours += hours
|
||||||
|
|
||||||
|
print(f" Явки (Я): {codes.get('Я', 0)} дн., часов: {total_hours:.1f}")
|
||||||
|
print(f" Коды: {', '.join(f'{k}={v}' for k, v in codes.items())}")
|
||||||
|
|
||||||
|
print(f" Первые 5 записей:")
|
||||||
|
for r in records[:5]:
|
||||||
|
d = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])
|
||||||
|
print(f" {d} | {str(r[1]).strip():>3} | {float(r[2]) if r[2] else 0:.1f} ч.")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Закрытие
|
||||||
|
# ============================================================
|
||||||
|
conn.close()
|
||||||
|
print()
|
||||||
|
print("=" * 60)
|
||||||
|
print(" ✅ Проверка завершена")
|
||||||
|
print("=" * 60)
|
||||||
@@ -45,11 +45,10 @@ ALLOWED_EXTENSIONS = [
|
|||||||
# --- Функция получения Email из Active Directory ---
|
# --- Функция получения Email из Active Directory ---
|
||||||
def get_user_email_sync(login: str) -> str:
|
def get_user_email_sync(login: str) -> str:
|
||||||
try:
|
try:
|
||||||
server = Server(AD_SERVER, get_info=ALL)
|
from utils.ad_search import search_by_login
|
||||||
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True)
|
entries = search_by_login(login, ["mail"])
|
||||||
conn.search(search_base=AD_BASE, search_filter=f"(sAMAccountName={login})", attributes=["mail"])
|
if entries and 'mail' in entries[0] and entries[0].mail.value:
|
||||||
if conn.entries and 'mail' in conn.entries[0] and conn.entries[0].mail.value:
|
return str(entries[0].mail.value)
|
||||||
return str(conn.entries[0].mail.value)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка поиска email в AD: {e}")
|
logger.error(f"Ошибка поиска email в AD: {e}")
|
||||||
return DEFAULT_REQUESTER
|
return DEFAULT_REQUESTER
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# /opt/trueconf_bot/utils/ad_checker.py
|
||||||
|
# Проверка членства пользователя в группах Active Directory
|
||||||
|
|
||||||
|
|
||||||
|
from ldap3 import Server, Connection, ALL
|
||||||
|
from utils.ad_search import search_by_filter as ad_search_all_bases
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_groups(cn: str) -> list[str] | None:
|
||||||
|
"""
|
||||||
|
Найти пользователя по CN и вернуть список групп.
|
||||||
|
Ищет по всем OU из AD_BASES.
|
||||||
|
Возвращает None если пользователь не найден.
|
||||||
|
"""
|
||||||
|
entries = ad_search_all_bases("(cn={})".format(cn), ["cn", "sAMAccountName", "memberOf"])
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
entry = entries[0]
|
||||||
|
return [str(g) for g in entry.memberOf]
|
||||||
|
|
||||||
|
|
||||||
|
def check_group_membership(cn: str, group_pattern: str = "2FA") -> dict:
|
||||||
|
"""
|
||||||
|
Проверить членство пользователя в группе.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cn: CN пользователя (например 'Krivochenko_Denis_Sergeevich')
|
||||||
|
group_pattern: подстрока для поиска в имени группы (по умолчанию '2FA')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict с полями:
|
||||||
|
found: bool — найден ли пользователь
|
||||||
|
cn: str | None — CN пользователя
|
||||||
|
sam: str | None — sAMAccountName
|
||||||
|
in_group: bool — в группе ли
|
||||||
|
groups: list[str] — все группы
|
||||||
|
matching_groups: list[str] — группы с паттерном
|
||||||
|
"""
|
||||||
|
groups = get_user_groups(cn)
|
||||||
|
if groups is None:
|
||||||
|
return {
|
||||||
|
"found": False,
|
||||||
|
"cn": None,
|
||||||
|
"sam": None,
|
||||||
|
"in_group": False,
|
||||||
|
"groups": [],
|
||||||
|
"matching_groups": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
matching = [g for g in groups if group_pattern.upper() in g.upper()]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"found": True,
|
||||||
|
"cn": cn,
|
||||||
|
"sam": groups[0].split(",")[0].split("=")[-1], # грубо из DN
|
||||||
|
"in_group": len(matching) > 0,
|
||||||
|
"groups": groups,
|
||||||
|
"matching_groups": matching,
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# /opt/trueconf_bot/utils/ad_search.py
|
||||||
|
# Универсальный поиск пользователей по нескольким OU в AD.
|
||||||
|
# Каждая функция принимает user_id/email/логин и ищет по всем OU из AD_BASES.
|
||||||
|
# Возвращает первую найденную запись (ldap3 Entry) или None.
|
||||||
|
|
||||||
|
from ldap3 import Server, Connection, ALL
|
||||||
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
|
import config.config as config
|
||||||
|
|
||||||
|
|
||||||
|
def _get_conn():
|
||||||
|
"""Создать соединение с AD."""
|
||||||
|
server = Server(config.AD_SERVER, get_info=ALL)
|
||||||
|
return Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
||||||
|
|
||||||
|
|
||||||
|
def search_by_login(login: str, attributes: list[str] | None = None) -> list:
|
||||||
|
"""
|
||||||
|
Найти пользователя по sAMAccountName во всех OU из AD_BASES.
|
||||||
|
Возвращает список Entry (обычно 0 или 1).
|
||||||
|
"""
|
||||||
|
attrs = attributes or ["cn", "sAMAccountName"]
|
||||||
|
conn = _get_conn()
|
||||||
|
for base in config.AD_BASES:
|
||||||
|
conn.search(
|
||||||
|
search_base=base,
|
||||||
|
search_filter="({}={})".format("sAMAccountName", login),
|
||||||
|
attributes=attrs,
|
||||||
|
)
|
||||||
|
if conn.entries:
|
||||||
|
return list(conn.entries)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def search_by_user_id(user_id: str, attributes: list[str] | None = None) -> list:
|
||||||
|
"""
|
||||||
|
Найти пользователя по email/UPN/sAMAccountName во всех OU из AD_BASES.
|
||||||
|
Возвращает список Entry (обычно 0 или 1).
|
||||||
|
"""
|
||||||
|
attrs = attributes or ["cn", "sAMAccountName", "mail", "userPrincipalName", "extensionAttribute2"]
|
||||||
|
safe_id = escape_filter_chars(user_id)
|
||||||
|
short_username = user_id.split("@")[0] if "@" in user_id else user_id
|
||||||
|
safe_sam = escape_filter_chars(short_username)
|
||||||
|
|
||||||
|
# Строим LDAP filter: (&(objectClass=user)(|(mail=...)(userPrincipalName=...)(sAMAccountName=...)))
|
||||||
|
or_parts = [
|
||||||
|
"(mail={})".format(safe_id),
|
||||||
|
"(userPrincipalName={})".format(safe_id),
|
||||||
|
"(userPrincipalName={})".format(short_username + "@sibcem.ru"),
|
||||||
|
"(sAMAccountName={})".format(safe_sam),
|
||||||
|
]
|
||||||
|
or_filter = "(|{})".format("".join(or_parts))
|
||||||
|
search_filter = "(&{}{})".format("(objectClass=user)", or_filter)
|
||||||
|
|
||||||
|
conn = _get_conn()
|
||||||
|
for base in config.AD_BASES:
|
||||||
|
conn.search(search_base=base, search_filter=search_filter, attributes=attrs)
|
||||||
|
if conn.entries:
|
||||||
|
return list(conn.entries)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def search_by_filter(search_filter: str, attributes: list[str]) -> list:
|
||||||
|
"""
|
||||||
|
Найти по произвольному LDAP-фильтру во всех OU из AD_BASES.
|
||||||
|
Возвращает список Entry.
|
||||||
|
"""
|
||||||
|
conn = _get_conn()
|
||||||
|
for base in config.AD_BASES:
|
||||||
|
conn.search(search_base=base, search_filter=search_filter, attributes=attributes)
|
||||||
|
if conn.entries:
|
||||||
|
return list(conn.entries)
|
||||||
|
return []
|
||||||
@@ -62,6 +62,13 @@ def build_dynamic_menu():
|
|||||||
dynamic_items.append(f"{num_char} — Поиск по регламентам")
|
dynamic_items.append(f"{num_char} — Поиск по регламентам")
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
|
if getattr(config, 'ENABLE_INSTRUCT', False):
|
||||||
|
num_char = emoji_digits.get(counter, str(counter))
|
||||||
|
MENU_MAP[str(counter)] = "INSTRUCT"
|
||||||
|
MENU_MAP[num_char] = "INSTRUCT"
|
||||||
|
dynamic_items.append(f"{num_char} — 📋 Инструкции")
|
||||||
|
counter += 1
|
||||||
|
|
||||||
if counter == 1:
|
if counter == 1:
|
||||||
dynamic_items.append("<i>В данный момент бот находится на техническом обслуживании.</i>")
|
dynamic_items.append("<i>В данный момент бот находится на техническом обслуживании.</i>")
|
||||||
|
|
||||||
|
|||||||
+9
-11
@@ -63,18 +63,16 @@ async def send_otp_via_api(phone: str, code: str) -> tuple:
|
|||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
def get_card_id_from_ad(user_id: str) -> str:
|
def get_card_id_from_ad(user_id: str) -> str:
|
||||||
"""Синхронно вытягивает CARD_ID сотрудника из Active Directory (extensionAttribute2)"""
|
"""Синхронно вытягивает CARD_ID сотрудника из Active Directory (extensionAttribute2).
|
||||||
|
Ищет по всем OU из AD_BASES."""
|
||||||
|
from utils.ad_search import search_by_user_id
|
||||||
try:
|
try:
|
||||||
server = Server(config.AD_SERVER, get_info=ALL)
|
entries = search_by_user_id(user_id, ["extensionAttribute2"])
|
||||||
conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
if not entries:
|
||||||
safe_user_id = escape_filter_chars(user_id)
|
raise Exception("Пользователь не найден в AD.")
|
||||||
short_username = user_id.split('@')[0]
|
entry = entries[0]
|
||||||
|
if 'extensionAttribute2' in entry and entry.extensionAttribute2.value:
|
||||||
search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))"
|
return str(entry.extensionAttribute2.value).strip()
|
||||||
conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2'])
|
|
||||||
|
|
||||||
if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value:
|
|
||||||
return str(conn.entries[0].extensionAttribute2.value).strip()
|
|
||||||
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка получения CARD_ID из AD для {user_id}: {e}")
|
logger.error(f"Ошибка получения CARD_ID из AD для {user_id}: {e}")
|
||||||
|
|||||||
+90
-6
@@ -140,16 +140,14 @@ def _sd_ticket_created(ticket_id: int, subject: str, description: str, emojis: d
|
|||||||
clean_desc = clean_desc[:147] + "..."
|
clean_desc = clean_desc[:147] + "..."
|
||||||
return (
|
return (
|
||||||
f"✅ <b>Заявка #{ticket_id} создана!</b>\n\n"
|
f"✅ <b>Заявка #{ticket_id} создана!</b>\n\n"
|
||||||
|
|
||||||
f"📌 <b>Тема: {subject}</b>\n\n"
|
f"📌 <b>Тема: {subject}</b>\n\n"
|
||||||
f"📝 <b>Текст: {clean_desc}</b>\n\n"
|
f"📝 <b>Текст: {clean_desc}</b>\n\n"
|
||||||
|
|
||||||
f"Если нужно добавить скриншоты или документ — отправьте их сейчас.\n\n"
|
f"Если нужно добавить скриншоты или документ — отправьте их сейчас.\n\n"
|
||||||
f"{_footer(emojis)}"
|
f"{_footer(emojis)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def sd_ticket_created(ticket_id: int, subject: str, description: str) -> str:
|
def _sd_ticket_create_error(emojis: dict) -> str:
|
||||||
return _sd_ticket_created(ticket_id, subject, description, EMOJI_DIGITS)
|
return f"❌ <b>Ошибка создания заявки.</b>{_footer_main_only(emojis)}"
|
||||||
|
|
||||||
def _sd_system_error(emojis: dict) -> str:
|
def _sd_system_error(emojis: dict) -> str:
|
||||||
return f"❌ <b>Системная ошибка.</b> Попробуйте позже.{_footer_main_only(emojis)}"
|
return f"❌ <b>Системная ошибка.</b> Попробуйте позже.{_footer_main_only(emojis)}"
|
||||||
@@ -170,6 +168,9 @@ SD_UNKNOWN_CMD_TEXT = _sd_unknown(EMOJI_DIGITS)
|
|||||||
SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS)
|
SD_TEXT_REQUIRED = _sd_text_required(EMOJI_DIGITS)
|
||||||
SD_CREATING = _sd_creating()
|
SD_CREATING = _sd_creating()
|
||||||
|
|
||||||
|
def sd_ticket_created(ticket_id: int, subject: str, description: str) -> str:
|
||||||
|
return _sd_ticket_created(ticket_id, subject, description, EMOJI_DIGITS)
|
||||||
|
|
||||||
def sd_ticket_create_error() -> str:
|
def sd_ticket_create_error() -> str:
|
||||||
return _sd_ticket_create_error(EMOJI_DIGITS)
|
return _sd_ticket_create_error(EMOJI_DIGITS)
|
||||||
|
|
||||||
@@ -395,7 +396,7 @@ def _lk_payslip_loading(emojis: dict) -> str:
|
|||||||
return "⏳ <i>Формирую архив расчетных листков...</i>"
|
return "⏳ <i>Формирую архив расчетных листков...</i>"
|
||||||
|
|
||||||
def _lk_vacation_loading(emojis: dict) -> str:
|
def _lk_vacation_loading(emojis: dict) -> str:
|
||||||
return "⏳ <i>Формирую справку по отпусках...</i>"
|
return "⏳ <i>Формирую справку по отпускам...</i>"
|
||||||
|
|
||||||
def _lk_sick_loading(emojis: dict) -> str:
|
def _lk_sick_loading(emojis: dict) -> str:
|
||||||
return "⏳ <i>Загружаю историю больничных листов...</i>"
|
return "⏳ <i>Загружаю историю больничных листов...</i>"
|
||||||
@@ -459,6 +460,15 @@ def _lk_docs_quantity_prompt(emojis: dict) -> str:
|
|||||||
def _lk_docs_year_prompt(emojis: dict) -> str:
|
def _lk_docs_year_prompt(emojis: dict) -> str:
|
||||||
return f"📅 <b>Выберите год, за который требуется справка:</b>\n\n<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
return f"📅 <b>Выберите год, за который требуется справка:</b>\n\n<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
|
||||||
|
def _lk_years_menu(current_year: int, emojis: dict) -> str:
|
||||||
|
lines = ["📅 <b>Выберите год запрашиваемого периода:</b>\n"]
|
||||||
|
for i in range(1, 9):
|
||||||
|
year = current_year - (i - 1)
|
||||||
|
emoji = emojis.get(str(i), f"{i}⃣")
|
||||||
|
lines.append(f"{emoji} — {year} год")
|
||||||
|
lines.append(f"\n<i>{emojis.get('9', '9⃣')} — Назад\n{emojis.get('0', '0⃣')} — В главное меню</i>")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
def _lk_docs_avg_salary_prompt(emojis: dict) -> str:
|
def _lk_docs_avg_salary_prompt(emojis: dict) -> str:
|
||||||
return f"Укажите количество месяцев для расчета средней ЗП (например: 3):\n\n<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
return f"Укажите количество месяцев для расчета средней ЗП (например: 3):\n\n<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
|
||||||
@@ -489,6 +499,7 @@ LK_UNKNOWN_CMD_TEXT = (
|
|||||||
f"<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
f"<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def lk_error_handling(emojis: dict) -> str:
|
def lk_error_handling(emojis: dict) -> str:
|
||||||
return _lk_error_handling(emojis)
|
return _lk_error_handling(emojis)
|
||||||
|
|
||||||
@@ -568,4 +579,77 @@ def lk_nav_text(emojis: dict) -> str:
|
|||||||
return _lk_nav_text(emojis)
|
return _lk_nav_text(emojis)
|
||||||
|
|
||||||
def lk_years_menu(current_year: int, emojis: dict) -> str:
|
def lk_years_menu(current_year: int, emojis: dict) -> str:
|
||||||
return lk_years_menu(current_year, emojis)
|
return _lk_years_menu(current_year, emojis)
|
||||||
|
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# INSTRUCT BOT (Инструкции)
|
||||||
|
# =========================================================
|
||||||
|
# Модуль инструкций
|
||||||
|
|
||||||
|
# --- Private functions (Internal logic) ---
|
||||||
|
|
||||||
|
def _instruct_main_menu(emojis: dict) -> str:
|
||||||
|
return (
|
||||||
|
f"📋 <b>Выберите инструкцию:</b>\n\n"
|
||||||
|
f"{emojis['1']} — 🖥 Trueconf\n"
|
||||||
|
f"{emojis['2']} — 📧 Почта\n\n"
|
||||||
|
f"<i>{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _instruct_trueconf(emojis: dict) -> str:
|
||||||
|
"""Статический текст Trueconf — без проверки группы."""
|
||||||
|
return (
|
||||||
|
"🖥 <b>Инструкция по Trueconf</b>\n\n"
|
||||||
|
"Данная функция находится в разработке.\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _instruct_trueconf_with_ad(emojis: dict, cn: str, ticket_id: str = None) -> str:
|
||||||
|
"""
|
||||||
|
Текст Trueconf с проверкой группы 2FA.
|
||||||
|
cn — CN пользователя из AD.
|
||||||
|
ticket_id — номер созданной заявки в SD.
|
||||||
|
"""
|
||||||
|
from utils.ad_checker import check_group_membership
|
||||||
|
|
||||||
|
result = check_group_membership(cn, "2FA")
|
||||||
|
|
||||||
|
if not result["found"]:
|
||||||
|
ticket_part = f"\n\n🎫 <b>Заявка #{ticket_id} создана!</b>" if ticket_id else ""
|
||||||
|
return (
|
||||||
|
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
|
||||||
|
f"Пользователь {cn} не найден в Active Directory.{ticket_part}\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result["in_group"]:
|
||||||
|
body = (
|
||||||
|
f"🖥 <b>Инструкция по Trueconf</b>\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ticket_part = f"\n\n🎫 <b>Заявка #{ticket_id} на предоставление доступа создана в ServiceDesk!</b>" if ticket_id else ""
|
||||||
|
body = (
|
||||||
|
f"❌ <b>Доступ к Trueconf запрещён.</b>\n\n"
|
||||||
|
f"У вас нет группы 2FA в Active Directory.{ticket_part}\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
|
def _instruct_email(emojis: dict) -> str:
|
||||||
|
return (
|
||||||
|
"📧 <b>Инструкция по Почте</b>\n\n"
|
||||||
|
"Данная функция находится в разработке.\n\n"
|
||||||
|
f"<i>{emojis['9']} — Назад\n{emojis['0']} — В главное меню</i>"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Public constants and functions ---
|
||||||
|
|
||||||
|
INSTRUCT_MAIN_MENU_TEXT = _instruct_main_menu(EMOJI_DIGITS)
|
||||||
|
INSTRUCT_TRUECONF_TEXT = _instruct_trueconf(EMOJI_DIGITS)
|
||||||
|
INSTRUCT_TRUECONF_WITH_AD = _instruct_trueconf_with_ad
|
||||||
|
INSTRUCT_EMAIL_TEXT = _instruct_email(EMOJI_DIGITS)
|
||||||
Reference in New Issue
Block a user