feat: implement Q&A database persistence and Telegram bot integration

This commit is contained in:
Devesh S
2026-07-22 12:30:21 +05:30
parent 2bf7985f90
commit 8cacd1f23e
16 changed files with 750 additions and 219 deletions

50
telegram-bot/README.md Normal file
View File

@@ -0,0 +1,50 @@
# 🤖 RIT Freshers Hub - Telegram Q&A Intermediary Bot
This is a lightweight Python microservice that acts as an intermediary between the student Q&A forum and registered helper accounts (Seniors/Staff) on Telegram.
---
## 🚀 How It Works
1. **Student submits a question** on the web portal.
2. **Spring Boot Backend** saves the question and triggers an HTTP POST request to the Python bot: `/send_question`.
3. **Telegram Bot** broadcasts the question with a `force_reply` prompt to all configured helper accounts.
4. **Helpers reply** directly to the Telegram message.
5. **Telegram Bot** captures the reply, maps it back to the original question ID, and pushes the answer back to the Spring Boot REST endpoint (`POST /api/questions/{id}/answers`).
---
## 🛠️ Installation & Setup
### 1. Install Dependencies
Run this in the `telegram-bot/` directory:
```bash
pip install -r requirements.txt
```
### 2. Configure the Bot
Update [config.json](config.json):
```json
{
"telegram_bot_token": "YOUR_TELEGRAM_BOT_TOKEN",
"helper_chat_ids": [
971749136
],
"spring_backend_url": "http://localhost:8080"
}
```
#### How to find your Telegram Chat ID:
1. Search for the bot username on Telegram and click **Start** (or send `/start`).
2. The bot will automatically reply with your exact **Chat ID**.
3. Add this ID to the `helper_chat_ids` array in `config.json`. The bot dynamically reloads configuration changes on the fly!
---
## 🏃 Running the Bot
Start the server:
```bash
python telegram_bot.py
```
* **Bot Server Port**: `8082`
* **Local Mappings Store**: `bot_mappings.db` (SQLite) is created automatically to persist message mappings so that replies continue to map to the correct questions even if the bot is restarted.

5
telegram-bot/config.json Normal file
View File

@@ -0,0 +1,5 @@
{
"telegram_bot_token": "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo",
"helper_chat_ids": [971749136],
"spring_backend_url": "http://localhost:8080"
}

View File

@@ -0,0 +1,4 @@
requests>=2.28.0
fastapi>=0.95.0
uvicorn>=0.20.0
pydantic>=2.0

View File

@@ -0,0 +1,238 @@
import os
import json
import sqlite3
import threading
import time
import logging
import requests
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uvicorn
# Configure Logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.StreamHandler()
]
)
# Load Configuration
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
def load_config():
if not os.path.exists(CONFIG_PATH):
default_config = {
"telegram_bot_token": "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo",
"helper_chat_ids": [],
"spring_backend_url": "http://localhost:8080"
}
with open(CONFIG_PATH, "w") as f:
json.dump(default_config, f, indent=2)
return default_config
with open(CONFIG_PATH, "r") as f:
return json.load(f)
config = load_config()
BOT_TOKEN = config.get("telegram_bot_token")
BACKEND_URL = config.get("spring_backend_url")
# Database Setup
DB_PATH = os.path.join(os.path.dirname(__file__), "bot_mappings.db")
def init_db():
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS question_mappings (
chat_id INTEGER,
message_id INTEGER,
question_id INTEGER,
PRIMARY KEY (chat_id, message_id)
)
""")
conn.commit()
conn.close()
init_db()
def save_mapping(chat_id: int, message_id: int, question_id: int):
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"INSERT OR REPLACE INTO question_mappings (chat_id, message_id, question_id) VALUES (?, ?, ?)",
(chat_id, message_id, question_id)
)
conn.commit()
conn.close()
def get_question_id(chat_id: int, message_id: int) -> int:
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute(
"SELECT question_id FROM question_mappings WHERE chat_id = ? AND message_id = ?",
(chat_id, message_id)
)
row = cursor.fetchone()
conn.close()
return row[0] if row else None
# Telegram API Helpers
def send_telegram_message(chat_id: int, text: str, reply_to_message_id: int = None) -> dict:
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
payload = {
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown",
"reply_markup": {"force_reply": True, "selective": True}
}
if reply_to_message_id:
payload["reply_to_message_id"] = reply_to_message_id
try:
response = requests.post(url, json=payload, timeout=10)
return response.json()
except Exception as e:
logging.error(f"Error sending Telegram message to {chat_id}: {e}")
return {}
# Background Long Polling for Telegram Updates
def telegram_polling_thread():
logging.info("Starting Telegram long polling thread...")
offset = 0
while True:
# Re-load config dynamic updates
current_config = load_config()
helpers = current_config.get("helper_chat_ids", [])
backend_url = current_config.get("spring_backend_url")
bot_token = current_config.get("telegram_bot_token")
url = f"https://api.telegram.org/bot{bot_token}/getUpdates"
params = {"offset": offset, "timeout": 20}
try:
response = requests.get(url, params=params, timeout=25)
data = response.json()
if not data.get("ok"):
logging.error(f"Telegram API getUpdates error: {data}")
time.sleep(5)
continue
updates = data.get("result", [])
for update in updates:
offset = update["update_id"] + 1
message = update.get("message")
if not message:
continue
chat_id = message["chat"]["id"]
text = message.get("text", "").strip()
# Help helper find their Chat ID
if text == "/start":
welcome_text = (
f"👋 *Welcome to RIT Freshers Hub Intermediary Bot!*\n\n"
f"To configure this helper, register this Chat ID in the `config.json` file:\n"
f"`{chat_id}`\n\n"
f"Once registered, you will receive new student questions here and can reply directly to them."
)
send_telegram_message(chat_id, welcome_text)
continue
# Process reply messages
reply_to = message.get("reply_to_message")
if reply_to:
# Verify if helper is authorized
if chat_id not in helpers:
logging.warning(f"Unauthorized message from chat ID {chat_id}")
send_telegram_message(chat_id, "⚠️ You are not registered as an authorized helper in config.json.")
continue
original_message_id = reply_to["message_id"]
question_id = get_question_id(chat_id, original_message_id)
if question_id:
# Extract author name
first_name = message["from"].get("first_name", "")
last_name = message["from"].get("last_name", "")
author_name = f"{first_name} {last_name}".strip() or "Senior Helper"
logging.info(f"Submitting answer for question {question_id} by helper '{author_name}'")
# Post answer to Spring Boot backend
backend_endpoint = f"{backend_url}/api/questions/{question_id}/answers"
answer_payload = {
"body": text,
"author": author_name
}
try:
res = requests.post(backend_endpoint, json=answer_payload, timeout=10)
if res.status_code == 200 or res.status_code == 201:
send_telegram_message(chat_id, "✅ *Answer posted successfully to the Q&A board!*", reply_to_message_id=message["message_id"])
else:
send_telegram_message(chat_id, f"❌ *Failed to post answer to backend.* (Status: {res.status_code})\nResponse: {res.text[:100]}", reply_to_message_id=message["message_id"])
except Exception as e:
logging.error(f"Error calling backend endpoint {backend_endpoint}: {e}")
send_telegram_message(chat_id, f"❌ *Connection error to backend.* ({e})", reply_to_message_id=message["message_id"])
else:
send_telegram_message(chat_id, "❓ This message does not correspond to any active question or the mapping has expired.", reply_to_message_id=message["message_id"])
except Exception as e:
logging.error(f"Error in long polling loop: {e}")
time.sleep(5)
# Start Polling Thread
polling_thread = threading.Thread(target=telegram_polling_thread, daemon=True)
polling_thread.start()
# FastAPI Web Server Setup
app = FastAPI(title="RIT Telegram Intermediary Bot HTTP Server")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class QuestionPayload(BaseModel):
question_id: int
title: str
body: str
author: str
@app.post("/send_question")
def send_question(payload: QuestionPayload):
current_config = load_config()
helpers = current_config.get("helper_chat_ids", [])
if not helpers:
logging.warning("No helper chat IDs registered in config.json.")
return {"status": "ignored", "reason": "No helpers registered"}
logging.info(f"Broadcasting question {payload.question_id} to {len(helpers)} helpers.")
formatted_msg = (
f"❓ *New Student Question!*\n\n"
f"👤 *Author:* {payload.author}\n"
f"📌 *Topic:* {payload.title}\n"
f"📝 *Details:* {payload.body}\n\n"
f"💬 *Reply to this message directly to submit your answer.*"
)
sent_count = 0
for chat_id in helpers:
res = send_telegram_message(chat_id, formatted_msg)
if res.get("ok"):
message_id = res["result"]["message_id"]
save_mapping(chat_id, message_id, payload.question_id)
sent_count += 1
return {"status": "success", "delivered_to": sent_count}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8082)