etm-terrain/main.py

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()