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