feat: enhance chatbot Telegram/Discord integrations, add /remove commands and Q&A reply handling

This commit is contained in:
Shanmuga Krishnan S M
2026-08-03 08:28:47 +05:30
parent 8bd0718b72
commit d51d538ee9
20 changed files with 1243 additions and 99 deletions

View File

@@ -2,8 +2,10 @@ package com.rit.portal;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class PortalApplication {
public static void main(String[] args) {

View File

@@ -43,6 +43,10 @@ public class CollabController {
public CollabRequest createCollabRequest(@RequestBody CollabRequest request) {
if (request.getStatus() == null) request.setStatus("OPEN");
if (request.getApplicationsCount() == null) request.setApplicationsCount(0);
if (request.getCollaboratorsNeeded() == null || request.getCollaboratorsNeeded() < 1) {
request.setCollaboratorsNeeded(1);
}
if (request.getAcceptedCount() == null) request.setAcceptedCount(0);
request.setCreatedAt(LocalDateTime.now());
return collabRequestRepository.save(request);
}
@@ -52,6 +56,10 @@ public class CollabController {
@PathVariable Integer id,
@RequestBody CollabApplication application) {
return collabRequestRepository.findById(id).map(request -> {
if ("CLOSED".equalsIgnoreCase(request.getStatus()) || "CANCELLED".equalsIgnoreCase(request.getStatus())) {
return ResponseEntity.badRequest().<CollabApplication>build();
}
application.setCollabRequest(request);
if (application.getStatus() == null) application.setStatus("PENDING");
application.setCreatedAt(LocalDateTime.now());
@@ -95,9 +103,44 @@ public class CollabController {
@PathVariable Integer applicationId,
@RequestParam String status) {
return collabApplicationRepository.findById(applicationId).map(app -> {
app.setStatus(status.toUpperCase());
String newStatus = status.toUpperCase();
String oldStatus = app.getStatus();
app.setStatus(newStatus);
CollabApplication updated = collabApplicationRepository.save(app);
// Delete request entirely when accepted count meets requested collaborators count
if ("ACCEPTED".equals(newStatus) && !"ACCEPTED".equals(oldStatus)) {
CollabRequest parentReq = app.getCollabRequest();
if (parentReq != null) {
int accepted = (parentReq.getAcceptedCount() == null ? 0 : parentReq.getAcceptedCount()) + 1;
parentReq.setAcceptedCount(accepted);
int needed = parentReq.getCollaboratorsNeeded() == null ? 1 : parentReq.getCollaboratorsNeeded();
if (accepted >= needed) {
collabRequestRepository.delete(parentReq);
} else {
collabRequestRepository.save(parentReq);
}
}
}
return ResponseEntity.ok(updated);
}).orElse(ResponseEntity.notFound().build());
}
@GetMapping("/active/telegram/{chatId}")
public List<CollabRequest> getActiveRequestsByTelegram(@PathVariable Long chatId) {
return collabRequestRepository.findByTelegramChatIdAndStatusOrderByCreatedAtDesc(chatId, "OPEN");
}
@GetMapping("/active/discord/{userId}")
public List<CollabRequest> getActiveRequestsByDiscord(@PathVariable String userId) {
return collabRequestRepository.findByDiscordUserIdAndStatusOrderByCreatedAtDesc(userId, "OPEN");
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrCancelCollabRequest(@PathVariable Integer id) {
return collabRequestRepository.findById(id).map(req -> {
collabRequestRepository.delete(req);
return ResponseEntity.noContent().<Void>build();
}).orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,63 @@
package com.rit.portal.controller;
import com.rit.portal.entity.LeetcodeProfile;
import com.rit.portal.service.LeetcodeService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/leetcode")
@CrossOrigin(originPatterns = "*")
@RequiredArgsConstructor
public class LeetcodeController {
private final LeetcodeService leetcodeService;
@GetMapping("/leaderboard")
public List<LeetcodeProfile> getLeaderboard() {
return leetcodeService.getAllRankedProfiles();
}
@PostMapping("/register")
public ResponseEntity<?> registerProfile(@RequestBody RegisterRequest request) {
if (request.getLeetcodeUsername() == null || request.getLeetcodeUsername().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "LeetCode username is required"));
}
if (request.getStudentName() == null || request.getStudentName().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Student name is required"));
}
try {
LeetcodeProfile profile = leetcodeService.registerOrUpdateStudent(
request.getStudentName(),
request.getLeetcodeUsername(),
request.getDepartment() != null ? request.getDepartment() : "CSE",
request.getYear() != null ? request.getYear() : "1st Year"
);
return ResponseEntity.ok(profile);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "Failed to register profile: " + e.getMessage()));
}
}
@PostMapping("/sync")
public ResponseEntity<?> triggerSpacedSync() {
leetcodeService.syncAllProfilesWithSpacing();
return ResponseEntity.ok(Map.of("message", "24h background sync initiated with 3-second request spacing between users."));
}
@Data
public static class RegisterRequest {
private String studentName;
private String leetcodeUsername;
private String department;
private String year;
}
}

View File

@@ -54,6 +54,14 @@ public class CollabRequest {
@Builder.Default
private Integer applicationsCount = 0;
@Column(name = "collaborators_needed")
@Builder.Default
private Integer collaboratorsNeeded = 1;
@Column(name = "accepted_count")
@Builder.Default
private Integer acceptedCount = 0;
@Column(name = "created_at")
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();

View File

@@ -0,0 +1,67 @@
package com.rit.portal.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "leetcode_profiles")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LeetcodeProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "student_name", nullable = false)
private String studentName;
@Column(name = "leetcode_username", nullable = false, unique = true)
private String leetcodeUsername;
@Column(nullable = false)
private String department;
@Column(nullable = false)
private String year;
@Column(name = "total_solved")
@Builder.Default
private Integer totalSolved = 0;
@Column(name = "easy_solved")
@Builder.Default
private Integer easySolved = 0;
@Column(name = "medium_solved")
@Builder.Default
private Integer mediumSolved = 0;
@Column(name = "hard_solved")
@Builder.Default
private Integer hardSolved = 0;
@Column(name = "ranking")
@Builder.Default
private Integer ranking = 0;
@Column(name = "reputation")
@Builder.Default
private Integer reputation = 0;
@Column(name = "last_updated")
private LocalDateTime lastUpdated;
@Column(name = "created_at")
private LocalDateTime createdAt;
@PrePersist
protected void onCreate() {
if (createdAt == null) createdAt = LocalDateTime.now();
if (lastUpdated == null) lastUpdated = LocalDateTime.now();
}
}

View File

@@ -8,4 +8,6 @@ import java.util.List;
@Repository
public interface CollabRequestRepository extends JpaRepository<CollabRequest, Integer> {
List<CollabRequest> findAllByOrderByCreatedAtDesc();
List<CollabRequest> findByTelegramChatIdAndStatusOrderByCreatedAtDesc(Long telegramChatId, String status);
List<CollabRequest> findByDiscordUserIdAndStatusOrderByCreatedAtDesc(String discordUserId, String status);
}

View File

@@ -0,0 +1,15 @@
package com.rit.portal.repository;
import com.rit.portal.entity.LeetcodeProfile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface LeetcodeProfileRepository extends JpaRepository<LeetcodeProfile, Integer> {
Optional<LeetcodeProfile> findByLeetcodeUsernameIgnoreCase(String leetcodeUsername);
List<LeetcodeProfile> findAllByOrderByTotalSolvedDescRankingAsc();
boolean existsByLeetcodeUsernameIgnoreCase(String leetcodeUsername);
}

View File

@@ -0,0 +1,153 @@
package com.rit.portal.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.rit.portal.entity.LeetcodeProfile;
import com.rit.portal.repository.LeetcodeProfileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.*;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.util.*;
@Service
@RequiredArgsConstructor
@Slf4j
public class LeetcodeService {
private final LeetcodeProfileRepository repository;
private final ObjectMapper objectMapper = new ObjectMapper();
private final RestTemplate restTemplate = new RestTemplate();
private static final String LEETCODE_GRAPHQL_URL = "https://leetcode.com/graphql";
public List<LeetcodeProfile> getAllRankedProfiles() {
return repository.findAllByOrderByTotalSolvedDescRankingAsc();
}
public LeetcodeProfile registerOrUpdateStudent(String studentName, String leetcodeUsername, String department, String year) {
String cleanUsername = leetcodeUsername.trim();
LeetcodeProfile profile = repository.findByLeetcodeUsernameIgnoreCase(cleanUsername)
.orElse(LeetcodeProfile.builder()
.studentName(studentName.trim())
.leetcodeUsername(cleanUsername)
.department(department.trim())
.year(year.trim())
.build());
profile.setStudentName(studentName.trim());
profile.setDepartment(department.trim());
profile.setYear(year.trim());
// Fetch fresh stats immediately for registration
boolean success = fetchAndUpdateProfile(profile);
if (!success) {
throw new IllegalArgumentException("LeetCode username '" + cleanUsername + "' could not be found on LeetCode.");
}
return repository.save(profile);
}
public boolean fetchAndUpdateProfile(LeetcodeProfile profile) {
try {
log.info("Fetching LeetCode GraphQL stats for: {}", profile.getLeetcodeUsername());
String graphqlQuery = "{\"query\":\"query getUserProfile($username: String!) { matchedUser(username: $username) { username submitStats: submitStatsGlobal { acSubmissionNum { difficulty count } } profile { ranking reputation } } }\",\"variables\":{\"username\":\"" + profile.getLeetcodeUsername() + "\"}}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headers.set("Referer", "https://leetcode.com/");
HttpEntity<String> request = new HttpEntity<>(graphqlQuery, headers);
ResponseEntity<String> response = restTemplate.postForEntity(LEETCODE_GRAPHQL_URL, request, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
JsonNode root = objectMapper.readTree(response.getBody());
JsonNode matchedUser = root.path("data").path("matchedUser");
if (matchedUser.isMissingNode() || matchedUser.isNull()) {
log.warn("LeetCode username not found: {}", profile.getLeetcodeUsername());
return false;
}
// Extract ranking and reputation
JsonNode profileNode = matchedUser.path("profile");
if (!profileNode.isMissingNode()) {
profile.setRanking(profileNode.path("ranking").asInt(0));
profile.setReputation(profileNode.path("reputation").asInt(0));
}
// Extract problem submission counts
JsonNode acSubmissions = matchedUser.path("submitStats").path("acSubmissionNum");
if (acSubmissions.isArray()) {
for (JsonNode item : acSubmissions) {
String diff = item.path("difficulty").asText("");
int count = item.path("count").asInt(0);
if ("All".equalsIgnoreCase(diff)) {
profile.setTotalSolved(count);
} else if ("Easy".equalsIgnoreCase(diff)) {
profile.setEasySolved(count);
} else if ("Medium".equalsIgnoreCase(diff)) {
profile.setMediumSolved(count);
} else if ("Hard".equalsIgnoreCase(diff)) {
profile.setHardSolved(count);
}
}
}
profile.setLastUpdated(LocalDateTime.now());
log.info("Successfully updated LeetCode profile for {}: Total Solved = {}", profile.getLeetcodeUsername(), profile.getTotalSolved());
return true;
}
} catch (Exception e) {
log.error("Failed to fetch LeetCode data for {}: {}", profile.getLeetcodeUsername(), e.getMessage());
}
return false;
}
/**
* Automatic 24-hour scheduled job running at 2:00 AM every day.
* Uses spaced requests (3-second delay between each user) to ensure ZERO IP blocking.
*/
@Scheduled(cron = "0 0 2 * * ?")
public void scheduledDailySyncWithSpacedRequests() {
log.info("Starting scheduled 24h LeetCode sync job...");
syncAllProfilesWithSpacing();
}
/**
* Manual or scheduled trigger to sync all profiles with a 3-second delay between requests.
*/
public void syncAllProfilesWithSpacing() {
List<LeetcodeProfile> profiles = repository.findAll();
log.info("Queued {} LeetCode profiles for spaced synchronization.", profiles.size());
new Thread(() -> {
for (int i = 0; i < profiles.size(); i++) {
LeetcodeProfile profile = profiles.get(i);
try {
fetchAndUpdateProfile(profile);
repository.save(profile);
// Space out requests by 3 seconds if not the last item
if (i < profiles.size() - 1) {
log.info("Waiting 3 seconds before fetching next profile to space out requests...");
Thread.sleep(5000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Spaced sync job interrupted.");
break;
} catch (Exception e) {
log.error("Error during spaced sync for {}: {}", profile.getLeetcodeUsername(), e.getMessage());
}
}
log.info("Finished spaced sync for all LeetCode profiles.");
}).start();
}
}

View File

@@ -1,26 +0,0 @@
com\rit\portal\controller\BusLocationController.class
com\rit\portal\config\DataInitializer$1.class
com\rit\portal\repository\CommunityAnswerRepository.class
com\rit\portal\entity\CommunityAnswer$CommunityAnswerBuilder.class
com\rit\portal\entity\CommunityQuestion.class
com\rit\portal\controller\BusRouteController.class
com\rit\portal\entity\NotePyq.class
com\rit\portal\entity\CommunityAnswer.class
com\rit\portal\controller\CommunityQuestionController.class
com\rit\portal\dto\DriverLocationUpdate.class
com\rit\portal\config\WebConfig.class
com\rit\portal\repository\BusStopRepository.class
com\rit\portal\config\DataInitializer.class
com\rit\portal\entity\BusStop.class
com\rit\portal\entity\BusStop$BusStopBuilder.class
com\rit\portal\repository\BusRouteRepository.class
com\rit\portal\entity\CommunityQuestion$CommunityQuestionBuilder.class
com\rit\portal\entity\NotePyq$NotePyqBuilder.class
com\rit\portal\entity\BusRoute$BusRouteBuilder.class
com\rit\portal\controller\NotePyqController.class
com\rit\portal\service\BusLocationService.class
com\rit\portal\repository\NotePyqRepository.class
com\rit\portal\model\BusLocation.class
com\rit\portal\entity\BusRoute.class
com\rit\portal\repository\CommunityQuestionRepository.class
com\rit\portal\PortalApplication.class

View File

@@ -1,20 +0,0 @@
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\config\DataInitializer.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\config\WebConfig.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\BusLocationController.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\BusRouteController.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\CommunityQuestionController.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\controller\NotePyqController.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\dto\DriverLocationUpdate.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\BusRoute.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\BusStop.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\CommunityAnswer.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\CommunityQuestion.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\entity\NotePyq.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\model\BusLocation.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\PortalApplication.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\BusRouteRepository.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\BusStopRepository.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\CommunityAnswerRepository.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\CommunityQuestionRepository.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\repository\NotePyqRepository.java
F:\fresher\Freshers-Hub\backend\src\main\java\com\rit\portal\service\BusLocationService.java