diff --git a/src/handlers/admin.py b/src/handlers/admin.py index a58a0da..b71eb2e 100644 --- a/src/handlers/admin.py +++ b/src/handlers/admin.py @@ -1,3 +1,4 @@ +import html from pathlib import Path from telegram import Update from telegram.ext import ContextTypes @@ -22,12 +23,12 @@ async def generate_token(update: Update, context: ContextTypes.DEFAULT_TYPE): label = " ".join(context.args) token = db.generate_token(label) await update.message.reply_text( - f"✅ Token berhasil dibuat untuk *{label}*\n\n" - f"🔑 Token: `{token}`\n\n" + f"✅ Token berhasil dibuat untuk {html.escape(label)}\n\n" + f"🔑 Token: {token}\n\n" f"📋 Instruksi untuk dosen:\n" f"1. Buka bot ini\n" - f"2. Kirim: `/start {token}`", - parse_mode="Markdown" + f"2. Kirim: /start {token}", + parse_mode="HTML" ) @@ -41,11 +42,11 @@ async def list_users(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Belum ada dosen terdaftar.") return - lines = ["👥 *Daftar Dosen Terdaftar:*\n"] + lines = ["👥 Daftar Dosen Terdaftar:\n"] for u in users: status = "✅" if u["is_active"] else "❌" - lines.append(f"{status} *{u['full_name']}* — ID: `{u['telegram_id']}`\n 📅 {u['registered_at'][:10]}") - await update.message.reply_text("\n".join(lines), parse_mode="Markdown") + lines.append(f"{status} {html.escape(u['full_name'])} — ID: {u['telegram_id']}\n 📅 {u['registered_at'][:10]}") + await update.message.reply_text("\n".join(lines), parse_mode="HTML") async def list_tokens(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -58,11 +59,11 @@ async def list_tokens(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Belum ada token dibuat.") return - lines = ["🔑 *Daftar Token:*\n"] + lines = ["🔑 Daftar Token:\n"] for t in tokens: - used = f"✅ Terpakai (`{t['used_by_telegram_id']}`)" if t["used_by_telegram_id"] else "⏳ Belum dipakai" - lines.append(f"• `{t['token']}` — {t['label']}\n {used}") - await update.message.reply_text("\n".join(lines), parse_mode="Markdown") + used = f"✅ Terpakai ({t['used_by_telegram_id']})" if t["used_by_telegram_id"] else "⏳ Belum dipakai" + lines.append(f"• {t['token']} — {html.escape(t['label'])}\n {used}") + await update.message.reply_text("\n".join(lines), parse_mode="HTML") async def revoke_user(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -80,12 +81,11 @@ async def revoke_user(update: Update, context: ContextTypes.DEFAULT_TYPE): return success = db.revoke_user(target_id) - msg = f"✅ Akses `{target_id}` berhasil dicabut." if success else "❌ User tidak ditemukan." - await update.message.reply_text(msg, parse_mode="Markdown") + msg = f"✅ Akses {target_id} berhasil dicabut." if success else "❌ User tidak ditemukan." + await update.message.reply_text(msg, parse_mode="HTML") async def upload_global(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Admin upload dokumen ke knowledge base global — semua dosen bisa mengaksesnya.""" if not is_admin(update.effective_user.id): await update.message.reply_text("⛔ Akses ditolak.") return @@ -93,8 +93,8 @@ async def upload_global(update: Update, context: ContextTypes.DEFAULT_TYPE): doc = update.message.document if not doc: await update.message.reply_text( - "📎 Kirim file PDF, DOCX, atau TXT dengan caption `/upload_global`", - parse_mode="Markdown" + "📎 Kirim file PDF, DOCX, atau TXT dengan caption /upload_global", + parse_mode="HTML" ) return @@ -125,7 +125,7 @@ async def upload_global(update: Update, context: ContextTypes.DEFAULT_TYPE): await rag.add_document_chunks(user_id=0, doc_id=doc_id, chunks=chunks, scope="global") await update.message.reply_text( - f"✅ {doc.file_name} berhasil ditambahkan ke knowledge base global!\n" + f"✅ {html.escape(doc.file_name)} berhasil ditambahkan ke knowledge base global!\n" f"📄 {len(chunks)} bagian tersimpan — semua dosen dapat mengaksesnya via /ask." ) @@ -140,10 +140,10 @@ async def list_global_docs(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Belum ada dokumen global.") return - lines = ["🌐 *Dokumen Knowledge Base Global:*\n"] + lines = ["🌐 Dokumen Knowledge Base Global:\n"] for d in docs: - lines.append(f"• *{d['original_name']}* ({d['chunk_count']} bagian)\n 📅 {d['uploaded_at'][:16]}") - await update.message.reply_text("\n".join(lines), parse_mode="Markdown") + lines.append(f"• {html.escape(d['original_name'])} ({d['chunk_count']} bagian)\n 📅 {d['uploaded_at'][:16]}") + await update.message.reply_text("\n".join(lines), parse_mode="HTML") async def help_admin(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -152,14 +152,14 @@ async def help_admin(update: Update, context: ContextTypes.DEFAULT_TYPE): return text = ( - "👑 *Perintah Admin:*\n\n" - "*Manajemen Akses:*\n" - "/generate_token `Nama` — Buat token untuk dosen baru\n" + "👑 Perintah Admin:\n\n" + "Manajemen Akses:\n" + "/generate_token Nama — Buat token untuk dosen baru\n" "/list_tokens — Lihat semua token\n" "/list_users — Lihat semua dosen terdaftar\n" - "/revoke `ID` — Cabut akses dosen\n\n" - "*Knowledge Base Global:*\n" + "/revoke ID — Cabut akses dosen\n\n" + "Knowledge Base Global:\n" "/list_global_docs — Lihat dokumen global\n" - "📎 Kirim file + caption `/upload_global` — Upload dokumen global" + "📎 Kirim file + caption /upload_global — Upload dokumen global" ) - await update.message.reply_text(text, parse_mode="Markdown") + await update.message.reply_text(text, parse_mode="HTML") diff --git a/src/handlers/user.py b/src/handlers/user.py index 1346775..303f6b7 100644 --- a/src/handlers/user.py +++ b/src/handlers/user.py @@ -1,3 +1,4 @@ +import html from pathlib import Path from telegram import Update from telegram.ext import ContextTypes @@ -24,17 +25,18 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): existing = db.get_user(user.id) if existing: + name = html.escape(existing['full_name']) await update.message.reply_text( - f"👋 Selamat datang kembali, *{existing['full_name']}*!\n" + f"👋 Selamat datang kembali, {name}!\n" f"🧠 Second Brain Anda aktif. Gunakan /help untuk panduan.", - parse_mode="Markdown" + parse_mode="HTML" ) return if user.id == ADMIN_TELEGRAM_ID: await update.message.reply_text( - "👑 *Mode Admin Aktif*\n\nGunakan /help_admin untuk melihat semua perintah admin.", - parse_mode="Markdown" + "👑 Mode Admin Aktif\n\nGunakan /help_admin untuk melihat semua perintah admin.", + parse_mode="HTML" ) return @@ -42,8 +44,8 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text( "👋 Halo! Bot ini khusus untuk dosen dengan token registrasi.\n\n" "Hubungi admin untuk mendapatkan token, lalu kirim:\n" - "`/start TOKEN_ANDA`", - parse_mode="Markdown" + "/start TOKEN_ANDA", + parse_mode="HTML" ) return @@ -56,11 +58,11 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): full_name = user.full_name or user.username or "Dosen" db.register_user(user.id, full_name, user.username or "", token) await update.message.reply_text( - f"✅ *Registrasi berhasil!*\n\n" - f"👤 {full_name}\n" + f"✅ Registrasi berhasil!\n\n" + f"👤 {html.escape(full_name)}\n" f"🧠 Second Brain Anda telah aktif.\n\n" f"Gunakan /help untuk panduan lengkap.", - parse_mode="Markdown" + parse_mode="HTML" ) @@ -76,7 +78,10 @@ async def remember(update: Update, context: ContextTypes.DEFAULT_TYPE): content = " ".join(context.args) mem_id = db.save_memory_meta(user_data["id"], content, scope="private") await rag.add_memory(user_data["id"], mem_id, content, scope="private") - await update.message.reply_text(f"✅ Tersimpan di Second Brain:\n_{content}_", parse_mode="Markdown") + await update.message.reply_text( + f"✅ Tersimpan di Second Brain:\n{html.escape(content)}", + parse_mode="HTML" + ) async def recall(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -86,18 +91,16 @@ async def recall(update: Update, context: ContextTypes.DEFAULT_TYPE): return if not context.args: - # Tampilkan 10 catatan terbaru memories = db.get_recent_memories(user_data["id"]) if not memories: await update.message.reply_text("Second Brain Anda masih kosong. Gunakan /remember untuk menyimpan catatan.") return - lines = ["🧠 *Catatan Terbaru:*\n"] + lines = ["🧠 Catatan Terbaru:\n"] for m in memories: - lines.append(f"• {m['content'][:150]}\n _{m['created_at'][:16]}_") - await update.message.reply_text("\n".join(lines), parse_mode="Markdown") + lines.append(f"• {html.escape(m['content'][:150])}\n {m['created_at'][:16]}") + await update.message.reply_text("\n".join(lines), parse_mode="HTML") return - # Pencarian semantik query = " ".join(context.args) await update.message.chat.send_action("typing") results = await rag.search(user_data["id"], query) @@ -106,13 +109,12 @@ async def recall(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Tidak ada hasil yang relevan ditemukan.") return - lines = [f"🔍 Hasil pencarian '{query}':\n"] + lines = [f"🔍 Hasil pencarian '{html.escape(query)}':\n"] for i, r in enumerate(results, 1): scope_label = "🌐 Global" if r["scope"] == "global" else "🔒 Pribadi" - # cosine distance: 0 = identik, 2 = berlawanan → konversi ke persentase relevance = round(max(0, (1 - r["distance"])) * 100, 1) - lines.append(f"{i}. [{scope_label}] {r['content'][:200]}\n {relevance}% relevan\n") - await update.message.reply_text("\n".join(lines), parse_mode="Markdown") + lines.append(f"{i}. [{scope_label}] {html.escape(r['content'][:200])}\n {relevance}% relevan\n") + await update.message.reply_text("\n".join(lines), parse_mode="HTML") async def ask(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -127,12 +129,10 @@ async def ask(update: Update, context: ContextTypes.DEFAULT_TYPE): question = " ".join(context.args) await update.message.chat.send_action("typing") - # Guard: hanya izinkan topik akademik if not await is_academic_topic(question, _client, DEEPSEEK_MODEL): await update.message.reply_text(REJECT_MESSAGE) return - # Cari konteks relevan dari Second Brain relevant = await rag.search(user_data["id"], question, n_results=5) context_text = "" if relevant: @@ -169,7 +169,6 @@ async def ask(update: Update, context: ContextTypes.DEFAULT_TYPE): async def upload_doc(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Upload dokumen ke Second Brain pribadi.""" user_data = db.get_user(update.effective_user.id) if not user_data: await update.message.reply_text("⛔ Belum terdaftar.") @@ -178,8 +177,8 @@ async def upload_doc(update: Update, context: ContextTypes.DEFAULT_TYPE): doc = update.message.document if not doc: await update.message.reply_text( - "📎 Kirim file PDF, DOCX, atau TXT dengan caption `/upload`", - parse_mode="Markdown" + "📎 Kirim file PDF, DOCX, atau TXT dengan caption /upload", + parse_mode="HTML" ) return @@ -210,7 +209,7 @@ async def upload_doc(update: Update, context: ContextTypes.DEFAULT_TYPE): await rag.add_document_chunks(user_id=user_data["id"], doc_id=doc_id, chunks=chunks, scope="private") await update.message.reply_text( - f"✅ {doc.file_name} berhasil diproses!\n" + f"✅ {html.escape(doc.file_name)} berhasil diproses!\n" f"📄 {len(chunks)} bagian tersimpan di Second Brain Anda.\n" f"Gunakan /ask untuk bertanya tentang isi dokumen ini." ) @@ -236,16 +235,16 @@ async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): return text = ( - "🧠 *Second Brain Bot — Panduan*\n\n" - "*Catatan:*\n" - "/remember `teks` — Simpan catatan baru\n" + "🧠 Second Brain Bot — Panduan\n\n" + "Catatan:\n" + "/remember teks — Simpan catatan baru\n" "/recall — Lihat 10 catatan terbaru\n" - "/recall `kata kunci` — Cari dengan AI (semantik)\n\n" - "*Dokumen:*\n" - "📎 Kirim file + caption `/upload` — Upload PDF/DOCX/TXT\n\n" - "*Tanya AI:*\n" - "/ask `pertanyaan` — Tanya AI dengan konteks Second Brain\n" + "/recall kata kunci — Cari dengan AI (semantik)\n\n" + "Dokumen:\n" + "📎 Kirim file + caption /upload — Upload PDF/DOCX/TXT\n\n" + "Tanya AI:\n" + "/ask pertanyaan — Tanya AI dengan konteks Second Brain\n" "/clear — Hapus riwayat percakapan\n\n" - "_Bot ini khusus untuk topik akademik dan perkuliahan._" + "Bot ini khusus untuk topik akademik dan perkuliahan." ) - await update.message.reply_text(text, parse_mode="Markdown") + await update.message.reply_text(text, parse_mode="HTML")