Files
RIT-nexus/telegram-bot/telegram_bot.py
Shanmuga Krishnan S M 0c86c087e4
Some checks failed
Deploy RIT Freshers Hub to VPS / Deploy to Live VPS (push) Has been cancelled
Integrate new Telegram bot with DevCollab workflow for notifications, actions, and project creation
2026-08-19 09:28:44 +05:30

764 lines
37 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import json
import sqlite3
import threading
import time
import logging
import requests
import asyncio
from typing import Optional
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 env variables from .env if present
env_path = os.path.join(os.path.dirname(__file__), ".env")
if os.path.exists(env_path):
with open(env_path, "r", encoding="utf-8") as env_file:
for line in env_file:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, val = line.split("=", 1)
os.environ[key.strip()] = val.strip()
# 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 = {
"community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"helper_chat_ids": [],
"spring_backend_url": "http://localhost:8085"
}
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()
COMMUNITY_BOT_TOKEN = os.environ.get("COMMUNITY_BOT_TOKEN") or config.get("community_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY")
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") or config.get("telegram_bot_token")
BACKEND_URL = os.environ.get("SPRING_BACKEND_URL") or config.get("spring_backend_url", "http://localhost:8085")
# 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, force_reply: bool = False, reply_markup: dict = None, token: str = None) -> dict:
active_token = token or BOT_TOKEN
url = f"https://api.telegram.org/bot{active_token}/sendMessage"
payload = {
"chat_id": chat_id,
"text": text,
"parse_mode": "Markdown"
}
if reply_markup:
payload["reply_markup"] = reply_markup
elif force_reply:
payload["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 {}
def answer_telegram_callback(callback_query_id: str, text: str = None, token: str = None):
active_token = token or BOT_TOKEN
url = f"https://api.telegram.org/bot{active_token}/answerCallbackQuery"
payload = {"callback_query_id": callback_query_id}
if text:
payload["text"] = text
try:
requests.post(url, json=payload, timeout=5)
except Exception as e:
logging.error(f"Error answering callback query: {e}")
def edit_telegram_message(chat_id: int, message_id: int, text: str, reply_markup: dict = None, token: str = None):
active_token = token or BOT_TOKEN
url = f"https://api.telegram.org/bot{active_token}/editMessageText"
payload = {
"chat_id": chat_id,
"message_id": message_id,
"text": text,
"parse_mode": "Markdown"
}
if reply_markup:
payload["reply_markup"] = reply_markup
try:
requests.post(url, json=payload, timeout=5)
except Exception as e:
logging.error(f"Error editing message: {e}")
TAG_MAP = {
"1": "looking for co-developing a project from scratch",
"2": "looking for beta testers",
"3": "looking for Open-source Collaborators/Contributers"
}
USER_COLLAB_STATE = {}
def parse_collab_text(text: str):
data = {
"authorName": None,
"department": None,
"year": None,
"tag": None,
"projectIdea": None,
"githubLink": None,
"collaboratorsNeeded": None
}
lines = text.splitlines()
if lines and lines[0].lower().startswith("/collab"):
command_line = lines[0]
rest = command_line[7:].strip()
if "|" in rest:
parts = [p.strip() for p in rest.split("|")]
if len(parts) >= 1: data["authorName"] = parts[0]
if len(parts) >= 2: data["department"] = parts[1]
if len(parts) >= 3: data["year"] = parts[2]
if len(parts) >= 4:
t = parts[3].strip()
data["tag"] = TAG_MAP.get(t, t)
if len(parts) >= 5: data["projectIdea"] = parts[4]
if len(parts) >= 6: data["githubLink"] = parts[5]
if len(parts) >= 7 and parts[6].isdigit(): data["collaboratorsNeeded"] = int(parts[6])
return data
lines = lines[1:]
for line in lines:
line_str = line.strip()
if ":" in line_str:
key, val = line_str.split(":", 1)
k = key.strip().lower()
v = val.strip()
if k == "name":
data["authorName"] = v
elif k in ["dept", "department"]:
data["department"] = v
elif k == "year":
data["year"] = v
elif k in ["tag", "tags"]:
data["tag"] = TAG_MAP.get(v, v)
elif k in ["idea", "project", "project idea"]:
data["projectIdea"] = v
elif k in ["github", "github link", "link"]:
data["githubLink"] = v
elif k in ["collaborators", "collaborators needed", "needed"]:
if v.isdigit():
data["collaboratorsNeeded"] = int(v)
else:
if not data["projectIdea"]:
data["projectIdea"] = line_str
return data
# ─── 1. COMMUNITY SENIOR HELPER BOT LONG POLLING THREAD ──────────────────────
def community_bot_polling_thread():
logging.info("Starting Community Senior Helper Bot polling thread...")
offset = 0
while True:
current_config = load_config()
helpers = current_config.get("helper_chat_ids", [])
backend_url = os.environ.get("SPRING_BACKEND_URL") or current_config.get("spring_backend_url")
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
if not comm_token:
time.sleep(5)
continue
url = f"https://api.telegram.org/bot{comm_token}/getUpdates"
params = {"offset": offset, "timeout": 20}
try:
response = requests.get(url, params=params, timeout=25)
data = response.json()
if not data.get("ok"):
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()
if text == "/start":
welcome_text = (
f"👋 *Welcome back, RIT Senior Helper!*\n\n"
f"You are registered as an authorized helper. You will receive new student questions here "
f"and can reply directly to them to post answers to the Q&A board."
)
send_telegram_message(chat_id, welcome_text, force_reply=False, token=comm_token)
continue
reply_to = message.get("reply_to_message")
if reply_to:
if chat_id not in helpers:
logging.warning(f"Unauthorized Q&A reply attempt from chat ID {chat_id}")
send_telegram_message(chat_id, "⚠️ You are not registered as an authorized helper in config.json.", token=comm_token)
continue
original_message_id = reply_to["message_id"]
question_id = get_question_id(chat_id, original_message_id)
if question_id:
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}'")
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 in [200, 201]:
send_telegram_message(chat_id, "✅ *Answer posted successfully to the Q&A board!*", reply_to_message_id=message["message_id"], token=comm_token)
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"], token=comm_token)
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"], token=comm_token)
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"], token=comm_token)
except Exception as e:
logging.error(f"Error in community bot polling loop: {e}")
time.sleep(5)
# ─── 2. 24/7 CHATBOT & DEV COLLAB BOT LONG POLLING THREAD ────────────────────
def telegram_polling_thread():
logging.info("Starting RIT Chatbot 24/7 & Dev Collab Bot polling thread...")
try:
url = f"https://api.telegram.org/bot{BOT_TOKEN}/getMe"
res = requests.get(url, timeout=10).json()
if res.get("ok"):
logging.info(f"Successfully connected to RIT Chatbot 24/7 Bot: @{res['result']['username']} ({res['result']['first_name']})")
else:
logging.error(f"Failed to connect to Telegram Bot. Check token: {res}")
except Exception as e:
logging.error(f"Failed to connect to Telegram API: {e}")
offset = 0
while True:
current_config = load_config()
backend_url = os.environ.get("SPRING_BACKEND_URL") or current_config.get("spring_backend_url")
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or 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"):
error_code = data.get("error_code")
if error_code == 409:
logging.warning("Telegram API 409 Conflict: Another bot instance is active. Sleeping 10s before retrying...")
time.sleep(10)
else:
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
# 1. Callback Queries (Inline buttons)
callback_query = update.get("callback_query")
if callback_query:
cb_id = callback_query["id"]
cb_data = callback_query.get("data", "")
cb_message = callback_query.get("message", {})
cb_chat_id = cb_message.get("chat", {}).get("id")
cb_msg_id = cb_message.get("message_id")
if cb_data.startswith("collab_accept_"):
parts = cb_data.split("_")
app_id = parts[2]
contact = "_".join(parts[3:]) if len(parts) > 3 else "the applicant"
try:
requests.put(f"{backend_url}/api/collab/applications/{app_id}/status?status=ACCEPTED", timeout=5)
except Exception as e:
logging.error(f"Error updating app status: {e}")
answer_telegram_callback(cb_id, "✅ Collaboration Request Accepted!", token=bot_token)
edit_telegram_message(
cb_chat_id, cb_msg_id,
f"✅ *Collaboration Request Accepted!*\n\nYou accepted the collaboration request. Direct contact info: *{contact}*",
token=bot_token
)
continue
elif cb_data.startswith("collab_decline_"):
parts = cb_data.split("_")
app_id = parts[2]
try:
requests.put(f"{backend_url}/api/collab/applications/{app_id}/status?status=REJECTED", timeout=5)
except Exception as e:
logging.error(f"Error updating app status: {e}")
answer_telegram_callback(cb_id, "❌ Collaboration Request Declined.", token=bot_token)
edit_telegram_message(
cb_chat_id, cb_msg_id,
f"❌ *Collaboration Request Declined.*",
token=bot_token
)
continue
elif cb_data.startswith("collab_rm_"):
req_id = cb_data.replace("collab_rm_", "")
try:
requests.delete(f"{backend_url}/api/collab/{req_id}", timeout=5)
answer_telegram_callback(cb_id, "Request Removed!", token=bot_token)
edit_telegram_message(
cb_chat_id, cb_msg_id,
"✅ *Collaboration Request cancelled and removed from RIT Dev Hub.*",
token=bot_token
)
except Exception as e:
logging.error(f"Error removing collab request: {e}")
continue
# Interactive Collab Wizard Callbacks
if cb_data.startswith("cflow_tag_"):
tag_key = cb_data.replace("cflow_tag_", "")
selected_tag = TAG_MAP.get(tag_key, TAG_MAP["1"])
USER_COLLAB_STATE[cb_chat_id] = {
"tag": selected_tag,
"dept": "CSE",
"year": "1st Year",
"collaboratorsNeeded": 1,
"step": "dept"
}
answer_telegram_callback(cb_id, "Tag Selected!", token=bot_token)
dept_keyboard = {
"inline_keyboard": [
[{"text": "CSE", "callback_data": "cflow_dept_CSE"}, {"text": "ECE", "callback_data": "cflow_dept_ECE"}, {"text": "AIML", "callback_data": "cflow_dept_AIML"}],
[{"text": "CSBS", "callback_data": "cflow_dept_CSBS"}, {"text": "MECH", "callback_data": "cflow_dept_MECH"}, {"text": "CIVIL", "callback_data": "cflow_dept_CIVIL"}],
[{"text": "AI & DS", "callback_data": "cflow_dept_AI & DS"}, {"text": "EEE", "callback_data": "cflow_dept_EEE"}, {"text": "IT", "callback_data": "cflow_dept_IT"}]
]
}
edit_telegram_message(
cb_chat_id, cb_msg_id,
f"📌 *Step 2 of 5: Select your Department*\n\nTag: `{selected_tag}`",
reply_markup=dept_keyboard,
token=bot_token
)
continue
if cb_data.startswith("cflow_dept_"):
dept_val = cb_data.replace("cflow_dept_", "")
if cb_chat_id not in USER_COLLAB_STATE:
USER_COLLAB_STATE[cb_chat_id] = {}
USER_COLLAB_STATE[cb_chat_id]["dept"] = dept_val
USER_COLLAB_STATE[cb_chat_id]["step"] = "year"
answer_telegram_callback(cb_id, "Department Selected!", token=bot_token)
yr_keyboard = {
"inline_keyboard": [
[{"text": "1st Year", "callback_data": "cflow_yr_1st Year"}, {"text": "2nd Year", "callback_data": "cflow_yr_2nd Year"}],
[{"text": "3rd Year", "callback_data": "cflow_yr_3rd Year"}, {"text": "4th Year", "callback_data": "cflow_yr_4th Year"}]
]
}
edit_telegram_message(
cb_chat_id, cb_msg_id,
f"📌 *Step 3 of 5: Select your Year*\n\nDepartment: `{dept_val}`",
reply_markup=yr_keyboard,
token=bot_token
)
continue
if cb_data.startswith("cflow_yr_"):
year_val = cb_data.replace("cflow_yr_", "")
if cb_chat_id not in USER_COLLAB_STATE:
USER_COLLAB_STATE[cb_chat_id] = {}
USER_COLLAB_STATE[cb_chat_id]["year"] = year_val
USER_COLLAB_STATE[cb_chat_id]["step"] = "num"
answer_telegram_callback(cb_id, "Year Selected!", token=bot_token)
num_keyboard = {
"inline_keyboard": [
[{"text": "1 Collaborator", "callback_data": "cflow_num_1"}, {"text": "2 Collaborators", "callback_data": "cflow_num_2"}],
[{"text": "3 Collaborators", "callback_data": "cflow_num_3"}, {"text": "4 Collaborators", "callback_data": "cflow_num_4"}]
]
}
edit_telegram_message(
cb_chat_id, cb_msg_id,
f"📌 *Step 4 of 5: How many collaborators are you looking for?*",
reply_markup=num_keyboard,
token=bot_token
)
continue
if cb_data.startswith("cflow_num_"):
num_val = int(cb_data.replace("cflow_num_", ""))
if cb_chat_id not in USER_COLLAB_STATE:
USER_COLLAB_STATE[cb_chat_id] = {}
USER_COLLAB_STATE[cb_chat_id]["collaboratorsNeeded"] = num_val
USER_COLLAB_STATE[cb_chat_id]["step"] = "idea"
answer_telegram_callback(cb_id, "Count Selected!", token=bot_token)
st = USER_COLLAB_STATE[cb_chat_id]
edit_telegram_message(
cb_chat_id, cb_msg_id,
f"📌 *Step 5 of 5: Enter Project Idea & Name*\n\n"
f"🏷️ Tag: `{st.get('tag')}`\n"
f"🏫 Dept: `{st.get('dept')}` | Year: `{st.get('year')}`\n"
f"👥 Collaborators Needed: `{num_val}`\n\n"
f"💬 *Now reply to this chat with your details in this format:*\n"
f"`Name: Your Name`\n"
f"`Idea: Building an AI attendance app`\n"
f"`GitHub: https://github.com/...` (optional)\n",
token=bot_token
)
continue
# 2. Standard Messages
message = update.get("message")
if not message:
continue
chat_id = message["chat"]["id"]
text = message.get("text", "").strip()
logging.info(f"Received message from chat {chat_id}: '{text}'")
# Handle Community Q&A reply if this is a reply to a forwarded question
reply_to = message.get("reply_to_message")
if reply_to:
original_message_id = reply_to["message_id"]
question_id = get_question_id(chat_id, original_message_id)
if question_id:
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}'")
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 in [200, 201]:
send_telegram_message(chat_id, "✅ *Answer posted successfully to the Q&A board!*", reply_to_message_id=message["message_id"], token=bot_token)
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"], token=bot_token)
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"], token=bot_token)
continue
# Handle /remove command in Telegram
if text.lower() in ["/remove", "/cancel"]:
try:
res = requests.get(f"{backend_url}/api/collab/active/telegram/{chat_id}", timeout=5)
if res.status_code == 200:
requests_list = res.json()
if not requests_list:
send_telegram_message(chat_id, "ℹ️ *You have no active collaboration requests to remove.*", token=bot_token)
else:
keyboard = []
for req_item in requests_list:
req_id = req_item["id"]
snippet = req_item.get("projectIdea", "Project")[:28]
keyboard.append([{"text": f"❌ Cancel: {snippet}", "callback_data": f"collab_rm_{req_id}"}])
rm_markup = {"inline_keyboard": keyboard}
send_telegram_message(chat_id, "🗑️ *Your Active Collaboration Requests*\n\nTap a request below to cancel and remove it from RIT Dev Hub:", reply_markup=rm_markup, token=bot_token)
else:
send_telegram_message(chat_id, "❌ Failed to fetch active requests.", token=bot_token)
except Exception as e:
send_telegram_message(chat_id, f"❌ Error: {e}", token=bot_token)
continue
# Handle /collab Command or structured collab post
if text.lower().startswith("/collab") or (chat_id in USER_COLLAB_STATE and USER_COLLAB_STATE[chat_id].get("step") == "idea"):
parsed = parse_collab_text(text)
st = USER_COLLAB_STATE.get(chat_id, {})
author_name = parsed.get("authorName") or st.get("authorName")
dept = parsed.get("department") or st.get("dept") or "CSE"
year = parsed.get("year") or st.get("year") or "1st Year"
tag = parsed.get("tag") or st.get("tag") or TAG_MAP["1"]
num_needed = st.get("collaboratorsNeeded") or parsed.get("collaboratorsNeeded") or 1
idea = parsed.get("projectIdea")
github = parsed.get("githubLink")
if not idea:
tag_keyboard = {
"inline_keyboard": [
[{"text": "🚀 1. Co-develop from scratch", "callback_data": "cflow_tag_1"}],
[{"text": "🧪 2. Beta testers needed", "callback_data": "cflow_tag_2"}],
[{"text": "🌐 3. Open-source contributors", "callback_data": "cflow_tag_3"}]
]
}
help_msg = (
f"🚀 *Post a Collaboration Request to RIT Dev Hub!*\n\n"
f"Tap a button below to select your project tag, or copy & reply with your details:"
)
send_telegram_message(chat_id, help_msg, reply_markup=tag_keyboard, token=bot_token)
continue
user_name = message["from"].get("username")
first_name = message["from"].get("first_name", "")
author_display = author_name or first_name or "Student Developer"
contact_display = f"@{user_name}" if user_name else f"Telegram User #{chat_id}"
payload = {
"authorName": author_display,
"department": dept,
"year": year,
"collaboratorsNeeded": num_needed,
"projectIdea": idea,
"githubLink": github or None,
"tag": tag,
"contactInfo": contact_display,
"telegramChatId": chat_id
}
try:
res = requests.post(f"{backend_url}/api/collab", json=payload, timeout=10)
if res.status_code in [200, 201]:
resp_msg = (
f"🎉 *Collaboration Request Live on RIT Dev Hub!*\n\n"
f"👤 *Author:* {author_display} ({dept}, {year})\n"
f"📌 *Project Idea:* {idea}\n"
f"🏷️ *Tag:* `{tag}`\n"
f"👥 *Collaborators Looking For:* `{num_needed}`\n"
f"📱 *Telegram Contact:* {contact_display}\n\n"
f"When other developers apply on the website, you will receive a Telegram message right here to Accept or Decline!\n"
f"*(Type `/remove` anytime to cancel your active requests)*"
)
send_telegram_message(chat_id, resp_msg, force_reply=False, token=bot_token)
if chat_id in USER_COLLAB_STATE:
del USER_COLLAB_STATE[chat_id]
else:
send_telegram_message(chat_id, f"❌ Failed to save collab request (Status: {res.status_code})", token=bot_token)
except Exception as e:
send_telegram_message(chat_id, f"❌ Error saving collab request: {e}", token=bot_token)
continue
# Welcome command
if text == "/start":
welcome_text = (
f"👋 *Welcome to the RIT Chatbot 24/7!*\n\n"
f"⚠️ *Please note: Answers generated by AI may occasionally be inaccurate or wrong. Always verify critical academic or administrative details with official campus sources.*\n\n"
f"I can help you answer any questions about RIT Chennai — courses, hostels, transport, sports, and more.\n\n"
f"🚀 *Developer Collaboration:* Type `/collab` to post your project idea!\n"
f"🗑️ *Remove Request:* Type `/remove` to cancel your active requests.\n\n"
f"💬 *Or just type your question here!*"
)
send_telegram_message(chat_id, welcome_text, force_reply=False, token=bot_token)
continue
# Direct chat fallback with Go chatbot service
if not text:
continue
logging.info(f"Querying Go chatbot service for user {chat_id}: '{text}'")
chatbot_service_url = "http://localhost:8081/api/chat"
try:
res = requests.post(chatbot_service_url, json={"message": text}, timeout=10)
if res.status_code == 200:
ans_data = res.json()
bot_response = ans_data.get("answer", "I am having trouble processing that question.")
send_telegram_message(chat_id, bot_response, force_reply=False, token=bot_token)
else:
logging.error(f"Go chatbot API returned status code {res.status_code}")
send_telegram_message(chat_id, "⚠️ The RIT Chatbot service is currently experiencing issues. Please try again later.", force_reply=False, token=bot_token)
except Exception as e:
logging.error(f"Failed to connect to Go chatbot service: {e}")
send_telegram_message(chat_id, "⚠️ I cannot connect to the RIT Chatbot database right now. Please make sure the service is online.", force_reply=False, token=bot_token)
except Exception as e:
logging.error(f"Error in 24/7 chatbot polling loop: {e}")
time.sleep(5)
# 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
class CollabApplicationPayload(BaseModel):
collab_id: int
application_id: int
project_idea: str
tag: str
author_name: str
contact_info: Optional[str] = None
telegram_chat_id: Optional[int] = None
applicant_name: str
applicant_dept: str
applicant_year: str
applicant_contact: str
message: Optional[str] = None
@app.post("/send_question")
def send_question(payload: QuestionPayload):
current_config = load_config()
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
# Telegram Broadcast via Senior Helper Community Bot
telegram_helpers = current_config.get("helper_chat_ids", [])
telegram_sent = 0
if telegram_helpers and comm_token:
logging.info(f"Broadcasting question {payload.question_id} to {len(telegram_helpers)} Telegram helpers via Senior Bot.")
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 to the Q&A board.*"
)
for chat_id in telegram_helpers:
res = send_telegram_message(chat_id, formatted_msg, force_reply=True, token=comm_token)
if res.get("ok"):
message_id = res["result"]["message_id"]
save_mapping(chat_id, message_id, payload.question_id)
telegram_sent += 1
return {
"status": "success",
"telegram_delivered_to": telegram_sent
}
@app.post("/send_collab_application")
def send_collab_application(payload: CollabApplicationPayload):
current_config = load_config()
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or current_config.get("telegram_bot_token")
telegram_sent = False
# Telegram Notification strictly to Person A (the project author)
chat_id = payload.telegram_chat_id
if chat_id:
msg_text = (
f"🤝 *New Collaboration Request for your Project!*\n\n"
f"📌 *Project Idea:* {payload.project_idea}\n"
f"🏷️ *Tag:* `{payload.tag}`\n\n"
f"👤 *Applicant:* {payload.applicant_name} ({payload.applicant_dept}, {payload.applicant_year})\n"
f"💬 *Message:* {payload.message or 'No message provided'}\n"
f"📱 *Contact Info:* {payload.applicant_contact}\n\n"
f"Click a button below to respond:"
)
reply_markup = {
"inline_keyboard": [
[
{"text": "✅ Accept Collaboration", "callback_data": f"collab_accept_{payload.application_id}_{payload.applicant_contact}"},
{"text": "❌ Decline Request", "callback_data": f"collab_decline_{payload.application_id}"}
]
]
}
res = send_telegram_message(chat_id, msg_text, reply_markup=reply_markup, token=bot_token)
if res.get("ok"):
telegram_sent = True
else:
logging.info(f"Skipping Telegram notification for collab application {payload.application_id}: No telegram_chat_id provided for author.")
return {
"status": "success",
"telegram_sent": telegram_sent
}
async def run_uvicorn():
config = uvicorn.Config(app, host="0.0.0.0", port=8082, loop="asyncio")
server = uvicorn.Server(config)
await server.serve()
async def main():
current_config = load_config()
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or current_config.get("telegram_bot_token", BOT_TOKEN)
# If separate bot tokens are configured, run dedicated community polling thread
if comm_token and bot_token and comm_token != bot_token:
comm_thread = threading.Thread(target=community_bot_polling_thread, daemon=True)
comm_thread.start()
# Main unified thread handles Dev Collab, Chatbot, and unified Q&A
chat_thread = threading.Thread(target=telegram_polling_thread, daemon=True)
chat_thread.start()
await run_uvicorn()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Shutting down bot server...")