feat: implement developer collaboration hub with telegram/discord interactive UI
This commit is contained in:
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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<>();
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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 ────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
771
src/pages/DevCollab/DevCollab.tsx
Normal file
771
src/pages/DevCollab/DevCollab.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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.
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,87 +439,227 @@ 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 to the RIT Chatbot 24/7!*\n\n"
|
||||||
f"👋 *Welcome back, RIT Senior Helper!*\n\n"
|
f"I can help you answer any questions about RIT Chennai — courses, hostels, transport, sports, and more.\n\n"
|
||||||
f"You are registered as an authorized helper. You will receive new student questions here "
|
f"🚀 *Developer Collaboration:* Type `/collab` to post your project idea and find co-developers!\n\n"
|
||||||
f"and can reply directly to them to post answers to the Q&A board."
|
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 not text:
|
||||||
if reply_to:
|
continue
|
||||||
# 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"]
|
logging.info(f"Querying Go chatbot service for user {chat_id}: '{text}'")
|
||||||
question_id = get_question_id(chat_id, original_message_id)
|
chatbot_service_url = "http://localhost:8081/api/chat"
|
||||||
|
try:
|
||||||
if question_id:
|
res = requests.post(chatbot_service_url, json={"message": text}, timeout=10)
|
||||||
# Extract author name
|
if res.status_code == 200:
|
||||||
first_name = message["from"].get("first_name", "")
|
ans_data = res.json()
|
||||||
last_name = message["from"].get("last_name", "")
|
bot_response = ans_data.get("answer", "I am having trouble processing that question.")
|
||||||
author_name = f"{first_name} {last_name}".strip() or "Senior Helper"
|
send_telegram_message(chat_id, bot_response, force_reply=False, token=bot_token)
|
||||||
|
|
||||||
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:
|
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)
|
logging.error(f"Go chatbot API returned status code {res.status_code}")
|
||||||
else:
|
send_telegram_message(chat_id, "⚠️ The RIT Chatbot service is currently experiencing issues. Please try again later.", force_reply=False, token=bot_token)
|
||||||
# Direct chat with the chatbot service
|
except Exception as e:
|
||||||
if not text:
|
logging.error(f"Failed to connect to Go chatbot service: {e}")
|
||||||
continue
|
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)
|
||||||
|
|
||||||
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:
|
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,43 +686,19 @@ 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
|
return
|
||||||
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
|
|
||||||
|
|
||||||
# Direct query fallback to Go chatbot service
|
# Direct query fallback to Go chatbot service
|
||||||
if not content.strip():
|
if not content.strip():
|
||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user