166 lines
6.6 KiB
Python
166 lines
6.6 KiB
Python
import logging
|
|
from telegram import Update
|
|
from telegram.ext import Application, CommandHandler, CallbackQueryHandler, MessageHandler, filters
|
|
from config import BOT_TOKEN, PATRICK_ID
|
|
from models.session import get_state, ATTENTE_PHOTO_FIN, ATTENTE_CONTENU_SAV, ATTENTE_CONTENU_MATERIEL, SAISIE_MANUELLE_CHANTIER, ATTENTE_FAQ
|
|
from handlers.menu import start, button_handler
|
|
from handlers.fin_chantier import handle_photo
|
|
from handlers.chantier import handle_saisie_manuelle
|
|
from handlers.sav import handle_sav
|
|
from handlers.materiel import handle_materiel
|
|
from handlers.faq import cmd_faq, handle_faq
|
|
from handlers.resolu import cmd_resolu
|
|
from services.tickets import init_db
|
|
from scripts.fiches_victron import verifier_et_indexer
|
|
|
|
logging.basicConfig(
|
|
format="%(asctime)s — %(name)s — %(levelname)s — %(message)s",
|
|
level=logging.INFO,
|
|
)
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
async def error_handler(update: object, context) -> None:
|
|
log.error("Erreur sur update %s : %s", update, context.error, exc_info=context.error)
|
|
|
|
|
|
async def chatid(update: Update, context) -> None:
|
|
chat = update.effective_chat
|
|
await update.effective_message.reply_text(
|
|
f"Chat ID : `{chat.id}`\nNom : {chat.title or chat.full_name}",
|
|
parse_mode="Markdown",
|
|
)
|
|
|
|
|
|
async def ls_command(update: Update, context) -> None:
|
|
"""Liste les fichiers/dossiers d'un chemin Nextcloud. Réservé à Patrick."""
|
|
if update.effective_user.id != PATRICK_ID:
|
|
return
|
|
folder = " ".join(context.args) if context.args else ""
|
|
from services.nextcloud import NEXTCLOUD_URL, _auth
|
|
import httpx
|
|
from urllib.parse import unquote
|
|
from xml.etree import ElementTree as ET
|
|
url = f"{NEXTCLOUD_URL}/{folder}/" if folder else f"{NEXTCLOUD_URL}/"
|
|
try:
|
|
async with httpx.AsyncClient(auth=_auth, timeout=15) as client:
|
|
r = await client.request("PROPFIND", url, headers={"Depth": "1"},
|
|
content=b'<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/><d:resourcetype/></d:prop></d:propfind>')
|
|
if r.status_code == 404:
|
|
await update.message.reply_text(f"❌ Chemin introuvable : `{folder or '/'}`", parse_mode="Markdown")
|
|
return
|
|
root = ET.fromstring(r.text)
|
|
ns = {"d": "DAV:"}
|
|
items = []
|
|
for resp in root.findall("d:response", ns):
|
|
href = resp.find("d:href", ns).text
|
|
name = unquote(href.rstrip("/").split("/")[-1])
|
|
if not name:
|
|
continue
|
|
is_dir = resp.find(".//d:collection", ns) is not None
|
|
items.append(("📁" if is_dir else "📄") + f" {name}")
|
|
items_text = "\n".join(items) if items else "(vide)"
|
|
await update.message.reply_text(f"`/{folder or ''}`\n\n{items_text}", parse_mode="Markdown")
|
|
except Exception as exc:
|
|
await update.message.reply_text(f"❌ Erreur : {exc}")
|
|
|
|
|
|
async def sync_docs_command(update: Update, context) -> None:
|
|
"""Indexe tous les PDFs du dossier Nextcloud Doc/ dans la FAQ SAV. Réservé à Patrick."""
|
|
if update.effective_user.id != PATRICK_ID:
|
|
return
|
|
folder = context.args[0] if context.args else "Doc"
|
|
status = await update.message.reply_text(f"🔄 Scan de `{folder}/` en cours...", parse_mode="Markdown")
|
|
|
|
from services.nextcloud import list_pdfs, download_file
|
|
from services.faq_service import faq_service, extract_pdf_text
|
|
|
|
pdfs = await list_pdfs(folder)
|
|
if not pdfs:
|
|
await status.edit_text(f"❌ Aucun PDF trouvé dans `{folder}/`.", parse_mode="Markdown")
|
|
return
|
|
|
|
await status.edit_text(f"📚 {len(pdfs)} PDF(s) trouvé(s). Indexation en cours...", parse_mode="Markdown")
|
|
|
|
from services.pdf_indexer import indexer_pdf
|
|
import os as _os
|
|
docs_dir = f"./data/docs/{folder.replace('/', '_')}"
|
|
_os.makedirs(docs_dir, exist_ok=True)
|
|
|
|
ok, skipped, total_sections = 0, [], 0
|
|
for filename in pdfs:
|
|
pdf_bytes = await download_file(folder, filename)
|
|
if not pdf_bytes:
|
|
skipped.append(filename)
|
|
continue
|
|
# Sauvegarder localement pour permettre la réindexation offline
|
|
local_path = f"{docs_dir}/{filename}"
|
|
_os.makedirs(_os.path.dirname(local_path), exist_ok=True)
|
|
with open(local_path, "wb") as f:
|
|
f.write(pdf_bytes)
|
|
try:
|
|
n = indexer_pdf(faq_service, local_path)
|
|
if n == 0:
|
|
# Fallback pypdf si pymupdf n'extrait rien (PDF scanné)
|
|
texte = extract_pdf_text(pdf_bytes)
|
|
n = faq_service.indexer_document(texte, filename) if texte.strip() else 0
|
|
total_sections += n
|
|
ok += 1
|
|
except Exception as exc:
|
|
log.error("indexer_pdf %s : %s", filename, exc)
|
|
skipped.append(filename)
|
|
|
|
lines = [f"✅ {ok}/{len(pdfs)} PDF(s) indexés — {total_sections} sections au total."]
|
|
if skipped:
|
|
lines.append("⚠️ Non indexés :\n" + "\n".join(f" • {s}" for s in skipped))
|
|
await status.edit_text("\n\n".join(lines), parse_mode="Markdown")
|
|
|
|
|
|
async def dispatch_message(update: Update, context) -> None:
|
|
user_id = update.effective_user.id if update.effective_user else None
|
|
if not user_id:
|
|
return
|
|
|
|
state = get_state(user_id)
|
|
log.debug("user=%d state=%s", user_id, state)
|
|
|
|
if state == ATTENTE_PHOTO_FIN:
|
|
await handle_photo(update, context)
|
|
elif state == SAISIE_MANUELLE_CHANTIER:
|
|
await handle_saisie_manuelle(update, context)
|
|
elif state == ATTENTE_FAQ:
|
|
await handle_faq(update, context)
|
|
elif state == ATTENTE_CONTENU_SAV:
|
|
await handle_sav(update, context)
|
|
elif state == ATTENTE_CONTENU_MATERIEL:
|
|
await handle_materiel(update, context)
|
|
else:
|
|
await start(update, context)
|
|
|
|
|
|
def main() -> None:
|
|
init_db()
|
|
from services.faq_service import faq_service
|
|
verifier_et_indexer(faq_service)
|
|
app = Application.builder().token(BOT_TOKEN).build()
|
|
|
|
app.add_handler(CommandHandler("start", start))
|
|
app.add_handler(CommandHandler("chatid", chatid))
|
|
app.add_handler(CommandHandler("sync_docs", sync_docs_command))
|
|
app.add_handler(CommandHandler("ls", ls_command))
|
|
app.add_handler(CommandHandler("faq", cmd_faq))
|
|
app.add_handler(CommandHandler("resolu", cmd_resolu))
|
|
app.add_handler(CallbackQueryHandler(button_handler))
|
|
app.add_error_handler(error_handler)
|
|
app.add_handler(MessageHandler(
|
|
(filters.TEXT | filters.PHOTO) & ~filters.COMMAND,
|
|
dispatch_message,
|
|
))
|
|
|
|
log.info("ETM Bot démarré.")
|
|
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|