feat: implement developer collaboration hub with telegram/discord interactive UI

This commit is contained in:
Shanmuga Krishnan S M
2026-07-29 08:27:46 +05:30
parent 2b4a8ee0e9
commit 8bd0718b72
13 changed files with 1625 additions and 129 deletions

View File

@@ -0,0 +1,103 @@
package com.rit.portal.controller;
import com.rit.portal.entity.CollabApplication;
import com.rit.portal.entity.CollabRequest;
import com.rit.portal.repository.CollabApplicationRepository;
import com.rit.portal.repository.CollabRequestRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/collab")
public class CollabController {
@Autowired
private CollabRequestRepository collabRequestRepository;
@Autowired
private CollabApplicationRepository collabApplicationRepository;
private final RestTemplate restTemplate = new RestTemplate();
private static final String BOT_COLLAB_APP_URL = "http://localhost:8082/send_collab_application";
@GetMapping
public List<CollabRequest> getAllCollabRequests() {
return collabRequestRepository.findAllByOrderByCreatedAtDesc();
}
@GetMapping("/{id}")
public ResponseEntity<CollabRequest> getCollabRequestById(@PathVariable Integer id) {
return collabRequestRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public CollabRequest createCollabRequest(@RequestBody CollabRequest request) {
if (request.getStatus() == null) request.setStatus("OPEN");
if (request.getApplicationsCount() == null) request.setApplicationsCount(0);
request.setCreatedAt(LocalDateTime.now());
return collabRequestRepository.save(request);
}
@PostMapping("/{id}/apply")
public ResponseEntity<CollabApplication> applyToCollabRequest(
@PathVariable Integer id,
@RequestBody CollabApplication application) {
return collabRequestRepository.findById(id).map(request -> {
application.setCollabRequest(request);
if (application.getStatus() == null) application.setStatus("PENDING");
application.setCreatedAt(LocalDateTime.now());
CollabApplication savedApp = collabApplicationRepository.save(application);
// Increment count on parent request
request.setApplicationsCount(request.getApplicationsCount() + 1);
collabRequestRepository.save(request);
// Notify Telegram/Discord intermediary bot asynchronously
new Thread(() -> {
try {
Map<String, Object> payload = new HashMap<>();
payload.put("collab_id", request.getId());
payload.put("application_id", savedApp.getId());
payload.put("project_idea", request.getProjectIdea());
payload.put("tag", request.getTag());
payload.put("author_name", request.getAuthorName());
payload.put("contact_info", request.getContactInfo());
payload.put("telegram_chat_id", request.getTelegramChatId());
payload.put("discord_user_id", request.getDiscordUserId());
payload.put("applicant_name", savedApp.getApplicantName());
payload.put("applicant_dept", savedApp.getApplicantDept());
payload.put("applicant_year", savedApp.getApplicantYear());
payload.put("applicant_contact", savedApp.getApplicantContact());
payload.put("message", savedApp.getMessage());
restTemplate.postForEntity(BOT_COLLAB_APP_URL, payload, String.class);
} catch (Exception e) {
System.err.println("Failed to send collab application notification to bot: " + e.getMessage());
}
}).start();
return ResponseEntity.ok(savedApp);
}).orElse(ResponseEntity.notFound().build());
}
@PutMapping("/applications/{applicationId}/status")
public ResponseEntity<CollabApplication> updateApplicationStatus(
@PathVariable Integer applicationId,
@RequestParam String status) {
return collabApplicationRepository.findById(applicationId).map(app -> {
app.setStatus(status.toUpperCase());
CollabApplication updated = collabApplicationRepository.save(app);
return ResponseEntity.ok(updated);
}).orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,48 @@
package com.rit.portal.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "collab_applications")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CollabApplication {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "collab_request_id", nullable = false)
@JsonIgnore
private CollabRequest collabRequest;
@Column(name = "applicant_name", nullable = false)
private String applicantName;
@Column(name = "applicant_dept", nullable = false)
private String applicantDept;
@Column(name = "applicant_year", nullable = false)
private String applicantYear;
@Column(name = "applicant_contact", nullable = false)
private String applicantContact;
@Column(columnDefinition = "TEXT")
private String message;
@Column(name = "status")
@Builder.Default
private String status = "PENDING";
@Column(name = "created_at")
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
}

View File

@@ -0,0 +1,64 @@
package com.rit.portal.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "collab_requests")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CollabRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "author_name", nullable = false)
private String authorName;
@Column(nullable = false)
private String department;
@Column(nullable = false)
private String year;
@Column(name = "project_idea", nullable = false, columnDefinition = "TEXT")
private String projectIdea;
@Column(name = "github_link")
private String githubLink;
@Column(nullable = false)
private String tag;
@Column(name = "contact_info")
private String contactInfo;
@Column(name = "telegram_chat_id")
private Long telegramChatId;
@Column(name = "discord_user_id")
private String discordUserId;
@Column(name = "status")
@Builder.Default
private String status = "OPEN";
@Column(name = "applications_count")
@Builder.Default
private Integer applicationsCount = 0;
@Column(name = "created_at")
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();
@OneToMany(mappedBy = "collabRequest", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@Builder.Default
private List<CollabApplication> applications = new ArrayList<>();
}

View File

@@ -0,0 +1,9 @@
package com.rit.portal.repository;
import com.rit.portal.entity.CollabApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CollabApplicationRepository extends JpaRepository<CollabApplication, Integer> {
}

View File

@@ -0,0 +1,11 @@
package com.rit.portal.repository;
import com.rit.portal.entity.CollabRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface CollabRequestRepository extends JpaRepository<CollabRequest, Integer> {
List<CollabRequest> findAllByOrderByCreatedAtDesc();
}

View File

@@ -10,6 +10,7 @@ import Events from '@/pages/Events/Events';
import Community from '@/pages/Community/Community'; import Community from '@/pages/Community/Community';
import BusRoutes from '@/pages/BusRoutes/BusRoutes'; import BusRoutes from '@/pages/BusRoutes/BusRoutes';
import Faculty from '@/pages/Faculty/Faculty'; import Faculty from '@/pages/Faculty/Faculty';
import DevCollab from '@/pages/DevCollab/DevCollab';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -45,6 +46,7 @@ function App() {
<Route path="/faculty" element={<Faculty />} /> <Route path="/faculty" element={<Faculty />} />
<Route path="/events" element={<Events />} /> <Route path="/events" element={<Events />} />
<Route path="/community" element={<Community />} /> <Route path="/community" element={<Community />} />
<Route path="/collab" element={<DevCollab />} />
</Route> </Route>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@@ -10,6 +10,7 @@ const quickLinks = [
{ label: 'Faculty Directory', path: '/faculty' }, { label: 'Faculty Directory', path: '/faculty' },
{ label: 'Events', path: '/events' }, { label: 'Events', path: '/events' },
{ label: 'Community', path: '/community' }, { label: 'Community', path: '/community' },
{ label: 'Dev Collab', path: '/collab' },
]; ];
const resources = [ const resources = [

View File

@@ -10,6 +10,7 @@ export const NAV_LINKS = [
{ label: 'Faculty Directory', path: '/faculty' }, { label: 'Faculty Directory', path: '/faculty' },
{ label: 'Clubs', path: '/events' }, { label: 'Clubs', path: '/events' },
{ label: 'Community', path: '/community' }, { label: 'Community', path: '/community' },
{ label: 'Dev Collab', path: '/collab' },
]; ];
// ─── Stats ──────────────────────────────────────────────────────────────────── // ─── Stats ────────────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,771 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Code2, Plus, GitBranch, Tag, User, Send, CheckCircle2,
Search, Filter, Sparkles, X, ExternalLink, Layers,
ChevronRight, MessageSquare, Clock, Users, ArrowUpRight
} from 'lucide-react';
import { Link } from 'react-router-dom';
import { getBackendUrl } from '@/lib/utils';
export interface CollabRequestItem {
id: number;
authorName: string;
department: string;
year: string;
projectIdea: string;
githubLink?: string;
tag: string;
contactInfo?: string;
telegramChatId?: number;
status: string;
applicationsCount: number;
createdAt: string;
}
const TAG_OPTIONS = [
'looking for co-developing a project from scratch',
'looking for beta testers',
'looking for Open-source Collaborators/Contributers',
];
const DEPARTMENTS = ['CSE', 'CSBS', 'AIML', 'ECE', 'MECH', 'CIVIL', 'AI & DS', 'EEE', 'IT', 'Other'];
const YEARS = ['1st Year', '2nd Year', '3rd Year', '4th Year'];
const INITIAL_MOCK_REQUESTS: CollabRequestItem[] = [
{
id: 101,
authorName: 'Rohan Sharma',
department: 'CSE',
year: '3rd Year',
projectIdea: 'Building an automated AI Attendance & Proxy Detection system using OpenCV and Python for college labs.',
githubLink: 'https://github.com/example/rit-ai-attendance',
tag: 'looking for co-developing a project from scratch',
contactInfo: '@rohan_sharma_rit',
status: 'OPEN',
applicationsCount: 3,
createdAt: new Date(Date.now() - 3600000 * 5).toISOString(),
},
{
id: 102,
authorName: 'Ananya V.',
department: 'AIML',
year: '2nd Year',
projectIdea: 'Need beta testers for our web-based RIT Bus Tracking & Live ETA PWA before publishing to campus app store.',
githubLink: 'https://github.com/example/rit-bus-live',
tag: 'looking for beta testers',
contactInfo: '@ananya_rit_dev',
status: 'OPEN',
applicationsCount: 7,
createdAt: new Date(Date.now() - 3600000 * 24).toISOString(),
},
{
id: 103,
authorName: 'Karthik N.',
department: 'ECE',
year: '4th Year',
projectIdea: 'Open-source IoT Smart Canteen Pre-order Hardware & Mobile App. Looking for React Native & ESP32 contributors!',
githubLink: 'https://github.com/example/rit-smart-canteen',
tag: 'looking for Open-source Collaborators/Contributers',
contactInfo: '@karthik_ece_rit',
status: 'OPEN',
applicationsCount: 5,
createdAt: new Date(Date.now() - 3600000 * 48).toISOString(),
},
];
export default function DevCollab() {
const [requests, setRequests] = useState<CollabRequestItem[]>(INITIAL_MOCK_REQUESTS);
const [loading, setLoading] = useState(false);
const [activeTagFilter, setActiveTagFilter] = useState('All');
const [searchTerm, setSearchTerm] = useState('');
// Modals
const [showPostModal, setShowPostModal] = useState(false);
const [selectedCollab, setSelectedCollab] = useState<CollabRequestItem | null>(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
// New Request Form
const [newAuthorName, setNewAuthorName] = useState('');
const [newDept, setNewDept] = useState('CSE');
const [newYear, setNewYear] = useState('1st Year');
const [newTag, setNewTag] = useState(TAG_OPTIONS[0]);
const [newIdea, setNewIdea] = useState('');
const [newGithub, setNewGithub] = useState('');
const [newContact, setNewContact] = useState('');
const [submittingPost, setSubmittingPost] = useState(false);
// Application Form
const [appApplicantName, setAppApplicantName] = useState('');
const [appDept, setAppDept] = useState('CSE');
const [appYear, setAppYear] = useState('1st Year');
const [appContact, setAppContact] = useState('');
const [appMessage, setAppMessage] = useState('');
const [submittingApp, setSubmittingApp] = useState(false);
const fetchCollabRequests = () => {
setLoading(true);
fetch(getBackendUrl('/api/collab'))
.then((res) => {
if (!res.ok) throw new Error('Backend offline');
return res.json();
})
.then((data) => {
if (Array.isArray(data) && data.length > 0) {
setRequests(data);
}
})
.catch(() => {
// Fallback to mock data if backend not available
})
.finally(() => setLoading(false));
};
useEffect(() => {
fetchCollabRequests();
}, []);
const showToast = (msg: string) => {
setToastMessage(msg);
setTimeout(() => setToastMessage(null), 4000);
};
const handlePostRequest = (e: React.FormEvent) => {
e.preventDefault();
if (!newAuthorName.trim() || !newIdea.trim()) return;
setSubmittingPost(true);
const payload = {
authorName: newAuthorName.trim(),
department: newDept,
year: newYear,
tag: newTag,
projectIdea: newIdea.trim(),
githubLink: newGithub.trim() || null,
contactInfo: newContact.trim() || undefined,
};
fetch(getBackendUrl('/api/collab'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
.then((res) => res.json())
.then((saved) => {
setRequests((prev) => [saved, ...prev]);
showToast('🎉 Collaboration request posted successfully!');
setShowPostModal(false);
setNewAuthorName('');
setNewIdea('');
setNewGithub('');
setNewContact('');
})
.catch(() => {
// Local state fallback
const mockSaved: CollabRequestItem = {
id: Date.now(),
authorName: payload.authorName,
department: payload.department,
year: payload.year,
tag: payload.tag,
projectIdea: payload.projectIdea,
githubLink: payload.githubLink || undefined,
contactInfo: payload.contactInfo || '@student_rit',
status: 'OPEN',
applicationsCount: 0,
createdAt: new Date().toISOString(),
};
setRequests((prev) => [mockSaved, ...prev]);
showToast('🎉 Collaboration request posted to live view!');
setShowPostModal(false);
})
.finally(() => setSubmittingPost(false));
};
const handleApply = (e: React.FormEvent) => {
e.preventDefault();
if (!selectedCollab || !appApplicantName.trim() || !appContact.trim()) return;
setSubmittingApp(true);
const payload = {
applicantName: appApplicantName.trim(),
applicantDept: appDept,
applicantYear: appYear,
applicantContact: appContact.trim(),
message: appMessage.trim(),
};
fetch(getBackendUrl(`/api/collab/${selectedCollab.id}/apply`), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
.then((res) => res.json())
.then(() => {
showToast(`🚀 Collaboration request sent to ${selectedCollab.authorName} via Telegram!`);
// Increment count locally
setRequests((prev) =>
prev.map((item) =>
item.id === selectedCollab.id ? { ...item, applicationsCount: item.applicationsCount + 1 } : item
)
);
setSelectedCollab(null);
setAppApplicantName('');
setAppContact('');
setAppMessage('');
})
.catch(() => {
showToast(`🚀 Application submitted! Sent notification to ${selectedCollab.authorName}.`);
setRequests((prev) =>
prev.map((item) =>
item.id === selectedCollab.id ? { ...item, applicationsCount: item.applicationsCount + 1 } : item
)
);
setSelectedCollab(null);
})
.finally(() => setSubmittingApp(false));
};
// Tag Pill Styling Helper
const getTagColor = (tag: string) => {
if (tag.includes('scratch')) return { bg: 'bg-amber-50 text-amber-700 border-amber-200', dot: 'bg-amber-500' };
if (tag.includes('beta')) return { bg: 'bg-[#F5F3FF] text-[#8B5CF6] border-[#DDD6FE]', dot: 'bg-[#8B5CF6]' };
return { bg: 'bg-emerald-50 text-emerald-700 border-emerald-200', dot: 'bg-emerald-500' };
};
const filteredRequests = requests.filter((req) => {
const matchTag = activeTagFilter === 'All' || req.tag === activeTagFilter;
const matchSearch =
req.projectIdea.toLowerCase().includes(searchTerm.toLowerCase()) ||
req.authorName.toLowerCase().includes(searchTerm.toLowerCase()) ||
req.department.toLowerCase().includes(searchTerm.toLowerCase()) ||
req.tag.toLowerCase().includes(searchTerm.toLowerCase());
return matchTag && matchSearch;
});
return (
<div className="min-h-screen relative overflow-hidden bg-[#FAFAFA]">
{/* Background Orbs */}
<div className="absolute inset-0 pointer-events-none overflow-hidden">
<div className="absolute -top-40 -left-40 w-96 h-96 bg-orange-200/30 rounded-full blur-3xl opacity-50" />
<div className="absolute top-1/3 -right-40 w-96 h-96 bg-purple-200/30 rounded-full blur-3xl opacity-40" />
</div>
{/* Toast Notification */}
<AnimatePresence>
{toastMessage && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="fixed top-20 left-1/2 -translate-x-1/2 z-50 bg-[#1E293B] text-white px-6 py-3.5 rounded-2xl shadow-2xl border border-slate-700 flex items-center gap-3 text-sm font-semibold"
>
<Sparkles className="w-4 h-4 text-[#F97316]" />
<span>{toastMessage}</span>
</motion.div>
)}
</AnimatePresence>
<div className="relative z-10">
{/* Header */}
<div className="bg-white border-b border-[#E5E7EB] py-10">
<div className="container-custom">
<div className="flex items-center gap-2 text-xs text-[#94A3B8] mb-3">
<Link to="/" className="hover:text-[#F97316]">Home</Link>
<ChevronRight className="w-3 h-3" />
<span className="text-[#F97316]">Developer Collab Hub</span>
</div>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<div className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-orange-50 border border-orange-200 text-[#F97316] text-xs font-semibold mb-3">
<Code2 className="w-4 h-4" /> RIT Developers Environment
</div>
<h1 className="text-3xl md:text-4xl font-bold text-[#1E293B] mb-2" style={{ fontFamily: 'Playfair Display, serif' }}>
Find Project{' '}
<span style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
Collaborators
</span>
</h1>
<p className="text-[#475569] text-sm max-w-2xl" style={{ fontFamily: 'Inter, sans-serif' }}>
Post your open-source projects, search for co-developers, or find beta testers. Connect instantly via Telegram & Discord bot integrations!
</p>
</div>
<motion.button
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={() => setShowPostModal(true)}
className="flex items-center gap-2.5 px-6 py-3.5 rounded-2xl text-white font-semibold text-sm shadow-lg shadow-orange-500/25 shrink-0"
style={{ background: 'linear-gradient(135deg, #F97316, #FB923C)', fontFamily: 'Poppins, sans-serif' }}
>
<Plus className="w-4.5 h-4.5" />
Post Collaboration Request
</motion.button>
</div>
</div>
</div>
{/* Main Content Container */}
<div className="container-custom py-8 space-y-8">
{/* Telegram / Discord Bot Banner Callout */}
<div className="bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 rounded-3xl p-6 text-white border border-slate-700/80 shadow-xl flex flex-col md:flex-row items-start md:items-center justify-between gap-6 relative overflow-hidden">
<div className="flex items-start gap-4 relative z-10">
<div className="w-12 h-12 rounded-2xl bg-orange-500/20 border border-orange-500/30 flex items-center justify-center shrink-0">
<Send className="w-6 h-6 text-[#F97316]" />
</div>
<div>
<h3 className="text-lg font-bold text-white mb-1" style={{ fontFamily: 'Poppins, sans-serif' }}>
🤖 Post directly via Telegram or Discord Bot!
</h3>
<p className="text-xs text-slate-300 max-w-xl leading-relaxed" style={{ fontFamily: 'Inter, sans-serif' }}>
Send <code className="px-2 py-0.5 rounded bg-slate-800 text-orange-400 font-mono border border-slate-700">/collab</code> to our 24/7 Telegram bot or Discord bot to submit project requests directly from your chat!
</p>
</div>
</div>
<div className="flex items-center gap-3 shrink-0 relative z-10">
<a
href="https://t.me/Ritchatbot_bot"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2.5 rounded-xl bg-[#229ED9]/20 hover:bg-[#229ED9]/30 text-[#229ED9] border border-[#229ED9]/40 text-xs font-semibold flex items-center gap-2 transition-all"
>
Telegram Bot <ExternalLink className="w-3.5 h-3.5" />
</a>
</div>
</div>
{/* Search & Tag Filter Bar */}
<div className="bg-white rounded-3xl p-4 border border-[#E5E7EB] shadow-xs flex flex-col lg:flex-row gap-4 justify-between">
{/* Search Input */}
<div className="relative flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4.5 h-4.5 text-[#94A3B8]" />
<input
type="text"
placeholder="Search by idea, author, department, or tag..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-11 pr-4 py-3 rounded-2xl bg-[#F8FAFC] border-none text-sm text-[#1E293B] placeholder-[#94A3B8] focus:ring-2 focus:ring-[#F97316]/20 focus:outline-none transition-all"
style={{ fontFamily: 'Inter, sans-serif' }}
/>
</div>
{/* Tag Filter Pills */}
<div className="flex flex-wrap items-center gap-2">
{['All', ...TAG_OPTIONS].map((tag) => {
const isActive = activeTagFilter === tag;
return (
<button
key={tag}
onClick={() => setActiveTagFilter(tag)}
className={`px-3.5 py-2 rounded-xl text-xs font-semibold transition-all cursor-pointer border ${
isActive
? 'bg-[#1E293B] text-white border-[#1E293B] shadow-sm'
: 'bg-[#F8FAFC] text-[#475569] border-[#E5E7EB] hover:bg-orange-50 hover:text-[#F97316] hover:border-orange-200'
}`}
style={{ fontFamily: 'Inter, sans-serif' }}
>
{tag === 'All' ? 'All Tags' : tag.replace('looking for ', '')}
</button>
);
})}
</div>
</div>
{/* Cards Grid */}
{filteredRequests.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 pb-12">
{filteredRequests.map((item) => {
const tagStyle = getTagColor(item.tag);
return (
<motion.div
key={item.id}
whileHover={{ y: -4, boxShadow: '0 20px 40px -10px rgba(0,0,0,0.08)' }}
className="bg-white rounded-3xl border border-[#E8ECF4] p-6 flex flex-col justify-between relative shadow-xs transition-all"
>
<div>
{/* Top Bar: Tag Badge */}
<div className="flex items-start justify-between gap-2 mb-4">
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[11px] font-semibold border ${tagStyle.bg}`}>
<span className={`w-1.5 h-1.5 rounded-full ${tagStyle.dot}`} />
{item.tag}
</span>
</div>
{/* Project Idea */}
<h3 className="text-base font-bold text-[#1E293B] leading-snug mb-3" style={{ fontFamily: 'Poppins, sans-serif' }}>
{item.projectIdea}
</h3>
</div>
<div className="mt-4 pt-4 border-t border-[#F1F5F9] space-y-4">
{/* GitHub link if present */}
{item.githubLink && (
<a
href={item.githubLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-xs font-semibold text-slate-700 hover:text-[#F97316] bg-slate-100 hover:bg-orange-50 px-3 py-1.5 rounded-xl border border-slate-200 hover:border-orange-200 transition-all w-fit"
>
<GitBranch className="w-3.5 h-3.5" />
View Repository
<ArrowUpRight className="w-3 h-3 text-slate-400" />
</a>
)}
{/* Author Info */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-9 h-9 rounded-xl bg-orange-100 flex items-center justify-center text-[#F97316] font-bold text-xs">
{item.authorName.charAt(0)}
</div>
<div>
<div className="text-xs font-bold text-[#1E293B]">{item.authorName}</div>
<div className="text-[11px] text-[#64748B]">
{item.department} {item.year}
</div>
</div>
</div>
<div className="text-right">
<div className="text-[11px] font-semibold text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full">
{item.applicationsCount} Requests
</div>
</div>
</div>
{/* Primary Action Button */}
<button
onClick={() => setSelectedCollab(item)}
className="w-full py-2.5 rounded-xl bg-[#FFF7ED] hover:bg-[#F97316] text-[#F97316] hover:text-white border border-[#FED7AA] hover:border-transparent text-xs font-semibold transition-all flex items-center justify-center gap-2 cursor-pointer shadow-xs"
style={{ fontFamily: 'Poppins, sans-serif' }}
>
<MessageSquare className="w-3.5 h-3.5" />
Send Collaboration Request
</button>
</div>
</motion.div>
);
})}
</div>
) : (
<div className="py-20 text-center flex flex-col items-center justify-center bg-white rounded-3xl border border-[#E8ECF4] shadow-xs">
<Code2 className="w-12 h-12 text-[#CBD5E1] mb-4" />
<h3 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
No collaboration requests found
</h3>
<p className="text-[#64748B] text-xs mt-1 mb-6" style={{ fontFamily: 'Inter, sans-serif' }}>
Be the first developer to post a request or try adjusting your filters!
</p>
<button
onClick={() => setShowPostModal(true)}
className="px-5 py-2.5 rounded-xl bg-[#F97316] text-white text-xs font-semibold hover:bg-[#EA580C] transition-all"
>
Post First Collaboration Request
</button>
</div>
)}
</div>
</div>
{/* ─── POST COLLABORATION REQUEST MODAL ───────────────────────────────────── */}
<AnimatePresence>
{showPostModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowPostModal(false)}
className="absolute inset-0 bg-slate-900/60 backdrop-blur-xs"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative w-full max-w-xl bg-white rounded-3xl shadow-2xl p-6 sm:p-8 max-h-[90vh] overflow-y-auto z-10"
>
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
Post Collaboration Request
</h2>
<p className="text-xs text-[#64748B] mt-0.5">
Share your idea to find co-developers, beta testers, or contributors.
</p>
</div>
<button
onClick={() => setShowPostModal(false)}
className="p-2 rounded-full hover:bg-slate-100 text-slate-400 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handlePostRequest} className="space-y-4">
{/* 1. Name */}
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">
1) Your Name <span className="text-red-500">*</span>
</label>
<input
type="text"
required
placeholder="e.g., Priyan Sharma"
value={newAuthorName}
onChange={(e) => setNewAuthorName(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all"
/>
</div>
{/* 2 & 3. Dept & Year */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">
2) Dept <span className="text-red-500">*</span>
</label>
<select
value={newDept}
onChange={(e) => setNewDept(e.target.value)}
className="w-full px-3 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all cursor-pointer"
>
{DEPARTMENTS.map((d) => (
<option key={d} value={d}>{d}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">
3) Year <span className="text-red-500">*</span>
</label>
<select
value={newYear}
onChange={(e) => setNewYear(e.target.value)}
className="w-full px-3 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all cursor-pointer"
>
{YEARS.map((y) => (
<option key={y} value={y}>{y}</option>
))}
</select>
</div>
</div>
{/* 4. Tag */}
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1.5">
4) Choose a Tag <span className="text-red-500">*</span>
</label>
<div className="space-y-2">
{TAG_OPTIONS.map((tagOption) => {
const isSelected = newTag === tagOption;
return (
<div
key={tagOption}
onClick={() => setNewTag(tagOption)}
className={`p-3 rounded-xl border text-xs font-medium cursor-pointer transition-all flex items-center justify-between ${
isSelected
? 'border-[#F97316] bg-orange-50 text-[#F97316] font-semibold'
: 'border-[#E5E7EB] bg-[#F8FAFC] text-slate-700 hover:bg-slate-50'
}`}
>
<span>{tagOption}</span>
{isSelected && <CheckCircle2 className="w-4 h-4 text-[#F97316] shrink-0" />}
</div>
);
})}
</div>
</div>
{/* 5. Project Idea */}
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">
5) Project Idea <span className="text-red-500">*</span>
</label>
<textarea
required
rows={3}
placeholder="Describe your project idea, stack, and what role you need..."
value={newIdea}
onChange={(e) => setNewIdea(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all"
/>
</div>
{/* Optional GitHub & Contact */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">
GitHub Link <span className="text-slate-400 font-normal">(Optional)</span>
</label>
<input
type="url"
placeholder="https://github.com/username/repo"
value={newGithub}
onChange={(e) => setNewGithub(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all"
/>
</div>
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">
Telegram Username / Contact <span className="text-slate-400 font-normal">(Optional)</span>
</label>
<input
type="text"
placeholder="@username or phone"
value={newContact}
onChange={(e) => setNewContact(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all"
/>
</div>
</div>
<div className="pt-4 flex justify-end gap-3">
<button
type="button"
onClick={() => setShowPostModal(false)}
className="px-5 py-2.5 rounded-xl text-xs font-semibold text-slate-600 bg-slate-100 hover:bg-slate-200 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={submittingPost}
className="px-6 py-2.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-[#F97316] to-[#FB923C] hover:brightness-105 transition-all shadow-md disabled:opacity-50"
>
{submittingPost ? 'Posting...' : 'Post Request'}
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
{/* ─── APPLY / COLLABORATE MODAL ─────────────────────────────────────────── */}
<AnimatePresence>
{selectedCollab && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedCollab(null)}
className="absolute inset-0 bg-slate-900/60 backdrop-blur-xs"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative w-full max-w-lg bg-white rounded-3xl shadow-2xl p-6 sm:p-8 z-10"
>
<div className="flex items-center justify-between mb-4">
<div>
<span className="text-[10px] uppercase font-bold tracking-wider text-[#F97316]">
Collaborate Request
</span>
<h2 className="text-lg font-bold text-[#1E293B]" style={{ fontFamily: 'Poppins, sans-serif' }}>
Join {selectedCollab.authorName}'s Project
</h2>
</div>
<button
onClick={() => setSelectedCollab(null)}
className="p-2 rounded-full hover:bg-slate-100 text-slate-400 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-3.5 bg-slate-50 rounded-2xl border border-slate-200 mb-5 text-xs text-slate-700 leading-relaxed">
<strong>Project Idea:</strong> {selectedCollab.projectIdea}
</div>
<form onSubmit={handleApply} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">Your Name *</label>
<input
type="text"
required
placeholder="Enter your name"
value={appApplicantName}
onChange={(e) => setAppApplicantName(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">Dept *</label>
<select
value={appDept}
onChange={(e) => setAppDept(e.target.value)}
className="w-full px-3 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all cursor-pointer"
>
{DEPARTMENTS.map((d) => (
<option key={d} value={d}>{d}</option>
))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">Year *</label>
<select
value={appYear}
onChange={(e) => setAppYear(e.target.value)}
className="w-full px-3 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all cursor-pointer"
>
{YEARS.map((y) => (
<option key={y} value={y}>{y}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">
Your Contact Info (Telegram @username / Phone / Email) *
</label>
<input
type="text"
required
placeholder="@username or phone number"
value={appContact}
onChange={(e) => setAppContact(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all"
/>
</div>
<div>
<label className="block text-xs font-semibold text-[#1E293B] mb-1">Message / Experience (Optional)</label>
<textarea
rows={2}
placeholder="Briefly state your skills or why you want to contribute..."
value={appMessage}
onChange={(e) => setAppMessage(e.target.value)}
className="w-full px-4 py-2.5 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] text-sm text-[#1E293B] focus:bg-white focus:border-[#F97316] focus:outline-none transition-all"
/>
</div>
<div className="pt-4 flex justify-end gap-3">
<button
type="button"
onClick={() => setSelectedCollab(null)}
className="px-5 py-2.5 rounded-xl text-xs font-semibold text-slate-600 bg-slate-100 hover:bg-slate-200 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={submittingApp}
className="px-6 py-2.5 rounded-xl text-xs font-semibold text-white bg-gradient-to-r from-[#F97316] to-[#FB923C] hover:brightness-105 transition-all shadow-md disabled:opacity-50 flex items-center gap-2"
>
<Send className="w-3.5 h-3.5" />
{submittingApp ? 'Sending...' : 'Send Request'}
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -1,9 +1,5 @@
# 🤖 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. 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 ## 🚀 How It Works
1. **Student submits a question** on the web portal. 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`. 2. **Spring Boot Backend** saves the question and triggers an HTTP POST request to the Python bot: `/send_question`.
@@ -11,7 +7,7 @@ This is a lightweight Python microservice that acts as an intermediary between t
4. **Helpers reply** directly to the Telegram message. 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`). 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 ## 🛠️ Installation & Setup

Binary file not shown.

View File

@@ -1,7 +1,8 @@
{ {
"community_bot_token": "",
"telegram_bot_token": "", "telegram_bot_token": "",
"helper_chat_ids": [971749136,5567776672], "helper_chat_ids": [971749136, 5567776672],
"discord_bot_token": "", "discord_bot_token": "",
"discord_helper_user_ids": [789393727641878568], "discord_helper_user_ids": [789393727641878568],
"spring_backend_url": "http://localhost:8085" "spring_backend_url": "http://localhost:8080"
} }

View File

@@ -7,6 +7,7 @@ import logging
import requests import requests
import asyncio import asyncio
import discord import discord
from typing import Optional
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel from pydantic import BaseModel
@@ -37,7 +38,8 @@ CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
def load_config(): def load_config():
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
default_config = { default_config = {
"telegram_bot_token": "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo", "community_bot_token": "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo",
"telegram_bot_token": "8913773505:AAHASuKLLOto3Ax573_dxg8bnvQy2ML6yLk",
"helper_chat_ids": [], "helper_chat_ids": [],
"spring_backend_url": "http://localhost:8080" "spring_backend_url": "http://localhost:8080"
} }
@@ -49,6 +51,7 @@ def load_config():
return json.load(f) return json.load(f)
config = load_config() config = load_config()
COMMUNITY_BOT_TOKEN = os.environ.get("COMMUNITY_BOT_TOKEN") or config.get("community_bot_token", "8859374355:AAH0dhwstkTBhRerRTjzmb2RG2fjPbigzvo")
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") or config.get("telegram_bot_token") BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN") or config.get("telegram_bot_token")
DISCORD_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") or config.get("discord_bot_token") DISCORD_TOKEN = os.environ.get("DISCORD_BOT_TOKEN") or config.get("discord_bot_token")
BACKEND_URL = os.environ.get("SPRING_BACKEND_URL") or config.get("spring_backend_url", "http://localhost:8080") BACKEND_URL = os.environ.get("SPRING_BACKEND_URL") or config.get("spring_backend_url", "http://localhost:8080")
@@ -94,7 +97,7 @@ def get_question_id(chat_id: int, message_id: int) -> int:
return row[0] if row else None return row[0] if row else None
# Telegram API Helpers # Telegram API Helpers
def send_telegram_message(chat_id: int, text: str, reply_to_message_id: int = None, force_reply: bool = True, token: str = None) -> dict: 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 active_token = token or BOT_TOKEN
url = f"https://api.telegram.org/bot{active_token}/sendMessage" url = f"https://api.telegram.org/bot{active_token}/sendMessage"
payload = { payload = {
@@ -102,7 +105,9 @@ def send_telegram_message(chat_id: int, text: str, reply_to_message_id: int = No
"text": text, "text": text,
"parse_mode": "Markdown" "parse_mode": "Markdown"
} }
if force_reply: if reply_markup:
payload["reply_markup"] = reply_markup
elif force_reply:
payload["reply_markup"] = {"force_reply": True, "selective": True} payload["reply_markup"] = {"force_reply": True, "selective": True}
if reply_to_message_id: if reply_to_message_id:
payload["reply_to_message_id"] = reply_to_message_id payload["reply_to_message_id"] = reply_to_message_id
@@ -114,14 +119,181 @@ def send_telegram_message(chat_id: int, text: str, reply_to_message_id: int = No
logging.error(f"Error sending Telegram message to {chat_id}: {e}") logging.error(f"Error sending Telegram message to {chat_id}: {e}")
return {} return {}
# Background Long Polling for Telegram Updates 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
}
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]
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
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
# Handle senior helper replies to questions
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(): def telegram_polling_thread():
logging.info("Starting Telegram long polling thread...") logging.info("Starting RIT Chatbot 24/7 & Dev Collab Bot polling thread...")
try: try:
url = f"https://api.telegram.org/bot{BOT_TOKEN}/getMe" url = f"https://api.telegram.org/bot{BOT_TOKEN}/getMe"
res = requests.get(url, timeout=10).json() res = requests.get(url, timeout=10).json()
if res.get("ok"): if res.get("ok"):
logging.info(f"Successfully connected to Telegram Bot: @{res['result']['username']} ({res['result']['first_name']})") logging.info(f"Successfully connected to RIT Chatbot 24/7 Bot: @{res['result']['username']} ({res['result']['first_name']})")
else: else:
logging.error(f"Failed to connect to Telegram Bot. Check token: {res}") logging.error(f"Failed to connect to Telegram Bot. Check token: {res}")
except Exception as e: except Exception as e:
@@ -129,9 +301,7 @@ def telegram_polling_thread():
offset = 0 offset = 0
while True: while True:
# Re-load config dynamic updates
current_config = load_config() 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") 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") bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") or current_config.get("telegram_bot_token")
@@ -149,6 +319,117 @@ def telegram_polling_thread():
for update in updates: for update in updates:
offset = update["update_id"] + 1 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=DECLINED", 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
# 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",
"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 4: 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 4: 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"] = "idea"
answer_telegram_callback(cb_id, "Year Selected!", token=bot_token)
st = USER_COLLAB_STATE[cb_chat_id]
edit_telegram_message(
cb_chat_id, cb_msg_id,
f"📌 *Step 4 of 4: Enter Project Idea & Name*\n\n"
f"🏷️ Tag: `{st.get('tag')}`\n"
f"🏫 Dept: `{st.get('dept')}` | Year: `{year_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") message = update.get("message")
if not message: if not message:
continue continue
@@ -158,64 +439,81 @@ def telegram_polling_thread():
logging.info(f"Received message from chat {chat_id}: '{text}'") logging.info(f"Received message from chat {chat_id}: '{text}'")
# 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"]
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,
"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"📱 *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!"
)
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 # Welcome command
if text == "/start": if text == "/start":
if chat_id in helpers:
welcome_text = ( welcome_text = (
f"👋 *Welcome back, RIT Senior Helper!*\n\n" f"👋 *Welcome to the RIT Chatbot 24/7!*\n\n"
f"You are registered as an authorized helper. You will receive new student questions here " f"I can help you answer any questions about RIT Chennai — courses, hostels, transport, sports, and more.\n\n"
f"and can reply directly to them to post answers to the Q&A board." f"🚀 *Developer Collaboration:* Type `/collab` to post your project idea and find co-developers!\n\n"
) f"💬 *Or just type your question here!*"
else:
welcome_text = (
f"👋 *Welcome to the RIT Student Assistant Bot!*\n\n"
f"I can help you answer any questions about Rajalakshmi Institute of Technology (RIT Chennai) — "
f"from courses, placements, and hostels, to transport, library hours, and sports.\n\n"
f"💬 *Just type your question here!* (e.g., _What courses are offered?_ or _How do I pay fees online?_)\n\n"
f"_(For Senior Helpers: To receive student Q&A broadcasts here, register your Chat ID `{chat_id}` in config.json)_"
) )
send_telegram_message(chat_id, welcome_text, force_reply=False, token=bot_token) send_telegram_message(chat_id, welcome_text, force_reply=False, token=bot_token)
continue continue
# Process reply messages # Direct chat fallback with Go chatbot service
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.", token=bot_token)
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"], 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)
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=bot_token)
else:
# Direct chat with the chatbot service
if not text: if not text:
continue continue
@@ -235,10 +533,133 @@ def telegram_polling_thread():
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) 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: except Exception as e:
logging.error(f"Error in long polling loop: {e}") logging.error(f"Error in 24/7 chatbot polling loop: {e}")
time.sleep(5) time.sleep(5)
# Telegram polling thread will be started inside main() # Discord Action View for Collaboration Applications
class DiscordCollabActionView(discord.ui.View):
def __init__(self, application_id: int, applicant_contact: str):
super().__init__(timeout=86400)
self.application_id = application_id
self.applicant_contact = applicant_contact
@discord.ui.button(label="Accept Collaboration", style=discord.ButtonStyle.success, emoji="")
async def accept_button(self, interaction: discord.Interaction, button: discord.ui.Button):
try:
requests.put(f"{BACKEND_URL}/api/collab/applications/{self.application_id}/status?status=ACCEPTED", timeout=5)
await interaction.response.send_message(
f"✅ **Collaboration Request Accepted!**\nDirect contact details: **{self.applicant_contact}**",
ephemeral=False
)
for item in self.children:
item.disabled = True
await interaction.message.edit(view=self)
except Exception as e:
await interaction.response.send_message(f"❌ Error updating status: {e}", ephemeral=True)
@discord.ui.button(label="Decline Request", style=discord.ButtonStyle.danger, emoji="")
async def decline_button(self, interaction: discord.Interaction, button: discord.ui.Button):
try:
requests.put(f"{BACKEND_URL}/api/collab/applications/{self.application_id}/status?status=DECLINED", timeout=5)
await interaction.response.send_message("❌ **Collaboration Request Declined.**", ephemeral=False)
for item in self.children:
item.disabled = True
await interaction.message.edit(view=self)
except Exception as e:
await interaction.response.send_message(f"❌ Error updating status: {e}", ephemeral=True)
async def broadcast_discord_collab_application(application_id: int, project_idea: str, tag: str, applicant_name: str, applicant_dept: str, applicant_year: str, applicant_contact: str, message: str, user_ids: list):
formatted_msg = (
f"🤝 **New Collaboration Request for your Project!**\n\n"
f"📌 **Project Idea:** {project_idea}\n"
f"🏷️ **Tag:** `{tag}`\n\n"
f"👤 **Applicant:** {applicant_name} ({applicant_dept}, {applicant_year})\n"
f"💬 **Message:** {message or 'No message provided'}\n"
f"📱 **Contact Info:** {applicant_contact}\n\n"
f"Click a button below to respond:"
)
view = DiscordCollabActionView(application_id, applicant_contact)
for user_id_val in user_ids:
try:
user_id = int(user_id_val)
user = await discord_client.fetch_user(user_id)
if user:
await user.send(content=formatted_msg, view=view)
logging.info(f"Sent Discord collab notification DM to user {user_id}")
except Exception as e:
logging.error(f"Failed to send Discord collab DM to {user_id_val}: {e}")
# Discord Native Interactive UI Components (Modal & Dropdown View)
class CollabModal(discord.ui.Modal, title="Post Collaboration Request"):
author_name = discord.ui.TextInput(label="1) Your Name", placeholder="e.g. Priyan Sharma", required=True)
department = discord.ui.TextInput(label="2) Department", placeholder="e.g. CSE / ECE / AIML", required=True, default="CSE")
year = discord.ui.TextInput(label="3) Year", placeholder="e.g. 1st Year / 2nd Year", required=True, default="1st Year")
project_idea = discord.ui.TextInput(label="4) Project Idea & Details", style=discord.TextStyle.paragraph, placeholder="Describe your project idea and what help you need...", required=True)
github_link = discord.ui.TextInput(label="5) GitHub Link (Optional)", placeholder="https://github.com/...", required=False)
def __init__(self, tag: str):
super().__init__()
self.selected_tag = tag
async def on_submit(self, interaction: discord.Interaction):
payload = {
"authorName": self.author_name.value,
"department": self.department.value,
"year": self.year.value,
"tag": self.selected_tag,
"projectIdea": self.project_idea.value,
"githubLink": self.github_link.value or None,
"contactInfo": f"Discord: {interaction.user.name}",
"discordUserId": str(interaction.user.id)
}
try:
res = requests.post(f"{BACKEND_URL}/api/collab", json=payload, timeout=10)
if res.status_code in [200, 201]:
await interaction.response.send_message(
f"🎉 **Collaboration Request posted live to RIT Dev Hub!**\n"
f"👤 **Author:** {self.author_name.value} ({self.department.value}, {self.year.value})\n"
f"📌 **Project:** {self.project_idea.value}\n"
f"🏷️ **Tag:** `{self.selected_tag}`",
ephemeral=False
)
else:
await interaction.response.send_message(f"❌ Failed to save request (Status: {res.status_code})", ephemeral=True)
except Exception as e:
await interaction.response.send_message(f"❌ Error connecting to backend: {e}", ephemeral=True)
class CollabTagSelect(discord.ui.Select):
def __init__(self):
options = [
discord.SelectOption(
label="Co-developing from scratch",
value="looking for co-developing a project from scratch",
description="Build a brand new project from scratch together",
emoji="🚀"
),
discord.SelectOption(
label="Beta Testers needed",
value="looking for beta testers",
description="Test early builds and provide user feedback",
emoji="🧪"
),
discord.SelectOption(
label="Open-Source Contributors",
value="looking for Open-source Collaborators/Contributers",
description="Open repository seeking PRs and contributors",
emoji="🌐"
),
]
super().__init__(placeholder="Scroll down to select a tag for your request...", min_values=1, max_values=1, options=options)
async def callback(self, interaction: discord.Interaction):
selected_tag = self.values[0]
modal = CollabModal(tag=selected_tag)
await interaction.response.send_modal(modal)
class CollabView(discord.ui.View):
def __init__(self):
super().__init__(timeout=180)
self.add_item(CollabTagSelect())
# Discord Bot Client Setup # Discord Bot Client Setup
intents = discord.Intents.default() intents = discord.Intents.default()
@@ -265,42 +686,18 @@ async def on_message(message):
content = message.content content = message.content
if is_mention: if is_mention:
# Strip out bot mention tags
mention_str = f"<@{discord_client.user.id}>" mention_str = f"<@{discord_client.user.id}>"
mention_nick_str = f"<@!{discord_client.user.id}>" mention_nick_str = f"<@!{discord_client.user.id}>"
content = content.replace(mention_str, "").replace(mention_nick_str, "").strip() content = content.replace(mention_str, "").replace(mention_nick_str, "").strip()
# Process DM helper replies to active questions # Discord Interactive /collab command
if is_dm and message.reference and message.reference.message_id: if content.lower().startswith("/collab"):
current_config = load_config() view = CollabView()
discord_helpers = current_config.get("discord_helper_user_ids", []) await message.reply(
author_id = message.author.id "🚀 **Post a Collaboration Request to RIT Dev Hub!**\n"
"Please select a tag from the scroll-down dropdown menu below to open the submission form:",
# Check if the author is a registered helper view=view
if author_id in [int(x) for x in discord_helpers if str(x).isdigit()]: )
original_message_id = message.reference.message_id
question_id = get_question_id(author_id, original_message_id)
if question_id:
author_name = message.author.name
logging.info(f"Submitting Discord answer for question {question_id} by helper '{author_name}'")
# Post answer to Spring Boot backend
backend_url = os.environ.get("SPRING_BACKEND_URL") or current_config.get("spring_backend_url")
backend_endpoint = f"{backend_url}/api/questions/{question_id}/answers"
answer_payload = {
"body": content,
"author": author_name
}
try:
res = requests.post(backend_endpoint, json=answer_payload, timeout=10)
if res.status_code in [200, 201]:
await message.reply("✅ *Answer posted successfully to the Q&A board!*")
else:
await message.reply(f"❌ *Failed to post answer to backend.* (Status: {res.status_code})")
except Exception as e:
logging.error(f"Error calling backend endpoint {backend_endpoint}: {e}")
await message.reply(f"❌ *Connection error to backend.* ({e})")
return return
# Direct query fallback to Go chatbot service # Direct query fallback to Go chatbot service
@@ -313,7 +710,6 @@ async def on_message(message):
def call_chatbot(): def call_chatbot():
return requests.post(chatbot_service_url, json={"message": content}, timeout=10) return requests.post(chatbot_service_url, json={"message": content}, timeout=10)
# Run requests.post in executor to keep the Discord loop non-blocking
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
res = await loop.run_in_executor(None, call_chatbot) res = await loop.run_in_executor(None, call_chatbot)
@@ -373,24 +769,40 @@ class QuestionPayload(BaseModel):
body: str body: str
author: 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
discord_user_id: Optional[str] = None
applicant_name: str
applicant_dept: str
applicant_year: str
applicant_contact: str
message: Optional[str] = None
@app.post("/send_question") @app.post("/send_question")
def send_question(payload: QuestionPayload): def send_question(payload: QuestionPayload):
current_config = load_config() current_config = load_config()
comm_token = os.environ.get("COMMUNITY_BOT_TOKEN") or current_config.get("community_bot_token", COMMUNITY_BOT_TOKEN)
# 1. Telegram Broadcast # 1. Telegram Broadcast via Senior Helper Community Bot
telegram_helpers = current_config.get("helper_chat_ids", []) telegram_helpers = current_config.get("helper_chat_ids", [])
telegram_sent = 0 telegram_sent = 0
if telegram_helpers: if telegram_helpers and comm_token:
logging.info(f"Broadcasting question {payload.question_id} to {len(telegram_helpers)} Telegram helpers.") logging.info(f"Broadcasting question {payload.question_id} to {len(telegram_helpers)} Telegram helpers via Senior Bot.")
formatted_msg = ( formatted_msg = (
f"❓ *New Student Question!*\n\n" f"❓ *New Student Question!*\n\n"
f"👤 *Author:* {payload.author}\n" f"👤 *Author:* {payload.author}\n"
f"📌 *Topic:* {payload.title}\n" f"📌 *Topic:* {payload.title}\n"
f"📝 *Details:* {payload.body}\n\n" f"📝 *Details:* {payload.body}\n\n"
f"💬 *Reply to this message directly to submit your answer.*" f"💬 *Reply to this message directly to submit your answer to the Q&A board.*"
) )
for chat_id in telegram_helpers: for chat_id in telegram_helpers:
res = send_telegram_message(chat_id, formatted_msg) res = send_telegram_message(chat_id, formatted_msg, force_reply=True, token=comm_token)
if res.get("ok"): if res.get("ok"):
message_id = res["result"]["message_id"] message_id = res["result"]["message_id"]
save_mapping(chat_id, message_id, payload.question_id) save_mapping(chat_id, message_id, payload.question_id)
@@ -419,15 +831,92 @@ def send_question(payload: QuestionPayload):
"discord_queued_for": discord_sent "discord_queued_for": discord_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
discord_sent = False
# 1. Telegram Broadcast via 24/7 Chatbot & Collab Bot
chat_id = payload.telegram_chat_id
if not chat_id:
helpers = current_config.get("helper_chat_ids", [])
if helpers:
chat_id = helpers[0]
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
# 2. Discord Broadcast
discord_target_users = []
if payload.discord_user_id:
discord_target_users.append(payload.discord_user_id)
config_discord_helpers = current_config.get("discord_helper_user_ids", [])
for dh in config_discord_helpers:
if str(dh) not in [str(x) for x in discord_target_users]:
discord_target_users.append(dh)
if discord_target_users and DISCORD_TOKEN and discord_loop:
try:
asyncio.run_coroutine_threadsafe(
broadcast_discord_collab_application(
payload.application_id,
payload.project_idea,
payload.tag,
payload.applicant_name,
payload.applicant_dept,
payload.applicant_year,
payload.applicant_contact,
payload.message or "",
discord_target_users
),
discord_loop
)
discord_sent = True
except Exception as e:
logging.error(f"Error scheduling Discord collab broadcast: {e}")
return {
"status": "success",
"telegram_sent": telegram_sent,
"discord_sent": discord_sent
}
async def run_uvicorn(): async def run_uvicorn():
config = uvicorn.Config(app, host="0.0.0.0", port=8082, loop="asyncio") config = uvicorn.Config(app, host="0.0.0.0", port=8082, loop="asyncio")
server = uvicorn.Server(config) server = uvicorn.Server(config)
await server.serve() await server.serve()
async def main(): async def main():
# Start Telegram long polling thread # Start Senior Helper Community Bot thread
polling_thread = threading.Thread(target=telegram_polling_thread, daemon=True) comm_thread = threading.Thread(target=community_bot_polling_thread, daemon=True)
polling_thread.start() comm_thread.start()
# Start 24/7 Chatbot & Collab Bot thread
chat_thread = threading.Thread(target=telegram_polling_thread, daemon=True)
chat_thread.start()
tasks = [] tasks = []
if DISCORD_TOKEN: if DISCORD_TOKEN: