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