initial commit
This commit is contained in:
13
src/main/java/com/rit/whatsapp/WhatsappBotApplication.java
Normal file
13
src/main/java/com/rit/whatsapp/WhatsappBotApplication.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.rit.whatsapp;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class WhatsappBotApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(WhatsappBotApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
178
src/main/java/com/rit/whatsapp/controller/WebhookController.java
Normal file
178
src/main/java/com/rit/whatsapp/controller/WebhookController.java
Normal file
@@ -0,0 +1,178 @@
|
||||
package com.rit.whatsapp.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.rit.whatsapp.handler.GrievanceHandler;
|
||||
import com.rit.whatsapp.handler.LoginHandler;
|
||||
import com.rit.whatsapp.handler.StudentMenuHandler;
|
||||
import com.rit.whatsapp.model.UserState;
|
||||
import com.rit.whatsapp.service.SessionService;
|
||||
import com.rit.whatsapp.service.WhatsAppMessageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/webhook")
|
||||
public class WebhookController {
|
||||
|
||||
@Value("${whatsapp.verify-token:my_secure_verify_token}")
|
||||
private String verifyToken;
|
||||
|
||||
private final WhatsAppMessageService messageService;
|
||||
private final SessionService sessionService;
|
||||
private final StudentMenuHandler studentMenuHandler;
|
||||
private final GrievanceHandler grievanceHandler;
|
||||
private final LoginHandler loginHandler;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
public WebhookController(WhatsAppMessageService messageService,
|
||||
SessionService sessionService,
|
||||
StudentMenuHandler studentMenuHandler,
|
||||
GrievanceHandler grievanceHandler,
|
||||
LoginHandler loginHandler) {
|
||||
this.messageService = messageService;
|
||||
this.sessionService = sessionService;
|
||||
this.studentMenuHandler = studentMenuHandler;
|
||||
this.grievanceHandler = grievanceHandler;
|
||||
this.loginHandler = loginHandler;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<String> verifyWebhook(
|
||||
@RequestParam(name = "hub.mode", required = false) String mode,
|
||||
@RequestParam(name = "hub.verify_token", required = false) String token,
|
||||
@RequestParam(name = "hub.challenge", required = false) String challenge) {
|
||||
|
||||
if ("subscribe".equals(mode) && verifyToken.equals(token)) {
|
||||
return ResponseEntity.ok(challenge);
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<String> receiveMessage(@RequestBody String payload) {
|
||||
System.out.println("Received Webhook Payload: " + payload);
|
||||
try {
|
||||
JsonNode rootNode = objectMapper.readTree(payload);
|
||||
|
||||
if (rootNode.has("entry") && rootNode.get("entry").isArray()) {
|
||||
JsonNode entryNode = rootNode.get("entry").get(0);
|
||||
|
||||
if (entryNode.has("changes") && entryNode.get("changes").isArray()) {
|
||||
JsonNode changesNode = entryNode.get("changes").get(0);
|
||||
JsonNode valueNode = changesNode.get("value");
|
||||
|
||||
if (valueNode != null && valueNode.has("messages") && valueNode.get("messages").isArray()) {
|
||||
JsonNode messageNode = valueNode.get("messages").get(0);
|
||||
|
||||
String fromPhoneNumber = messageNode.get("from").asText();
|
||||
String messageType = messageNode.get("type").asText();
|
||||
|
||||
UserState currentState = sessionService.getUserState(fromPhoneNumber);
|
||||
boolean isAuthenticated = sessionService.isAuthenticated(fromPhoneNumber);
|
||||
|
||||
// Global reset command
|
||||
if ("text".equals(messageType) && messageNode.get("text").get("body").asText().trim().equalsIgnoreCase("reset")) {
|
||||
sessionService.resetState(fromPhoneNumber);
|
||||
if (isAuthenticated) {
|
||||
studentMenuHandler.sendMainMenu(fromPhoneNumber);
|
||||
} else {
|
||||
loginHandler.startLoginFlow(fromPhoneNumber);
|
||||
}
|
||||
return ResponseEntity.ok("EVENT_RECEIVED");
|
||||
}
|
||||
|
||||
// Route unauthenticated users strictly to Login Flow
|
||||
if (!isAuthenticated) {
|
||||
if ("text".equals(messageType)) {
|
||||
String body = messageNode.get("text").get("body").asText();
|
||||
handleUnauthenticatedUser(fromPhoneNumber, body, currentState);
|
||||
} else {
|
||||
// If they send a sticker/image/etc before logging in, force login
|
||||
handleUnauthenticatedUser(fromPhoneNumber, "", currentState);
|
||||
}
|
||||
return ResponseEntity.ok("EVENT_RECEIVED");
|
||||
}
|
||||
|
||||
// Authenticated users flow
|
||||
if ("text".equals(messageType)) {
|
||||
String messageBody = messageNode.get("text").get("body").asText();
|
||||
handleAuthenticatedTextMessage(fromPhoneNumber, messageBody, currentState);
|
||||
} else if ("interactive".equals(messageType)) {
|
||||
JsonNode interactiveNode = messageNode.get("interactive");
|
||||
String interactiveType = interactiveNode.get("type").asText();
|
||||
|
||||
if ("list_reply".equals(interactiveType)) {
|
||||
String selectedListId = interactiveNode.get("list_reply").get("id").asText();
|
||||
handleInteractiveListSelection(fromPhoneNumber, selectedListId, currentState);
|
||||
} else if ("button_reply".equals(interactiveType)) {
|
||||
String selectedButtonId = interactiveNode.get("button_reply").get("id").asText();
|
||||
handleInteractiveButtonSelection(fromPhoneNumber, selectedButtonId, currentState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error processing webhook payload: " + e.getMessage());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("EVENT_RECEIVED");
|
||||
}
|
||||
|
||||
private void handleUnauthenticatedUser(String phoneNumber, String messageBody, UserState currentState) {
|
||||
switch (currentState) {
|
||||
case AWAITING_LOGIN_ID:
|
||||
loginHandler.handleLoginIdInput(phoneNumber, messageBody);
|
||||
break;
|
||||
case AWAITING_LOGIN_PASSCODE:
|
||||
loginHandler.handlePasscodeInput(phoneNumber, messageBody);
|
||||
break;
|
||||
default:
|
||||
// Trigger the start of the login flow
|
||||
loginHandler.startLoginFlow(phoneNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleAuthenticatedTextMessage(String phoneNumber, String messageBody, UserState currentState) {
|
||||
switch (currentState) {
|
||||
case AWAITING_GRIEVANCE_DESC:
|
||||
grievanceHandler.handleGrievanceDescription(phoneNumber, messageBody);
|
||||
break;
|
||||
case MAIN_MENU:
|
||||
default:
|
||||
// Fallback to main menu
|
||||
studentMenuHandler.sendMainMenu(phoneNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInteractiveListSelection(String phoneNumber, String selectedListId, UserState currentState) {
|
||||
if (currentState == UserState.MAIN_MENU) {
|
||||
studentMenuHandler.handleMenuSelection(phoneNumber, selectedListId);
|
||||
} else {
|
||||
studentMenuHandler.sendMainMenu(phoneNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInteractiveButtonSelection(String phoneNumber, String selectedButtonId, UserState currentState) {
|
||||
if ("student_logout".equals(selectedButtonId)) {
|
||||
sessionService.logoutUser(phoneNumber);
|
||||
messageService.sendMessage(phoneNumber, "You have been logged out successfully. Type 'Hi' to log in again.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentState == UserState.AWAITING_GRIEVANCE_CATEGORY) {
|
||||
grievanceHandler.handleCategorySelection(phoneNumber, selectedButtonId);
|
||||
} else {
|
||||
studentMenuHandler.sendMainMenu(phoneNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/main/java/com/rit/whatsapp/handler/GrievanceHandler.java
Normal file
71
src/main/java/com/rit/whatsapp/handler/GrievanceHandler.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.rit.whatsapp.handler;
|
||||
|
||||
import com.rit.whatsapp.model.UserState;
|
||||
import com.rit.whatsapp.service.SessionService;
|
||||
import com.rit.whatsapp.service.WhatsAppMessageBuilder;
|
||||
import com.rit.whatsapp.service.WhatsAppMessageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class GrievanceHandler {
|
||||
|
||||
private final WhatsAppMessageService messageService;
|
||||
private final SessionService sessionService;
|
||||
|
||||
@Autowired
|
||||
public GrievanceHandler(WhatsAppMessageService messageService, SessionService sessionService) {
|
||||
this.messageService = messageService;
|
||||
this.sessionService = sessionService;
|
||||
}
|
||||
|
||||
public void startGrievanceFlow(String phoneNumber) {
|
||||
sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_CATEGORY);
|
||||
|
||||
List<Map<String, String>> buttons = new ArrayList<>();
|
||||
buttons.add(createButton("grievance_academic", "Academic"));
|
||||
buttons.add(createButton("grievance_hostel", "Hostel"));
|
||||
buttons.add(createButton("grievance_other", "Other"));
|
||||
|
||||
Map<String, Object> buttonMessage = WhatsAppMessageBuilder.buildButtonMessage(
|
||||
"Please select the category for your grievance:",
|
||||
buttons
|
||||
);
|
||||
|
||||
messageService.sendInteractiveMessage(phoneNumber, buttonMessage);
|
||||
}
|
||||
|
||||
public void handleCategorySelection(String phoneNumber, String selectedCategory) {
|
||||
sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_DESC);
|
||||
messageService.sendMessage(phoneNumber, "You selected " + selectedCategory + ". Please type a brief description of your issue:");
|
||||
}
|
||||
|
||||
public void handleGrievanceDescription(String phoneNumber, String description) {
|
||||
// Generate a mock ticket ID
|
||||
String ticketId = "TKT-" + UUID.randomUUID().toString().substring(0, 6).toUpperCase();
|
||||
|
||||
String confirmationMessage = "Thank you. Your grievance has been recorded.\n\n" +
|
||||
"Ticket ID: *" + ticketId + "*\n" +
|
||||
"Department Notified: Yes\n" +
|
||||
"Status: Open\n\n" +
|
||||
"You can check the status of this ticket anytime using the main menu.";
|
||||
|
||||
messageService.sendMessage(phoneNumber, confirmationMessage);
|
||||
|
||||
// Reset state back to main menu
|
||||
sessionService.resetState(phoneNumber);
|
||||
}
|
||||
|
||||
private Map<String, String> createButton(String id, String title) {
|
||||
Map<String, String> btn = new HashMap<>();
|
||||
btn.put("id", id);
|
||||
btn.put("title", title);
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
64
src/main/java/com/rit/whatsapp/handler/LoginHandler.java
Normal file
64
src/main/java/com/rit/whatsapp/handler/LoginHandler.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package com.rit.whatsapp.handler;
|
||||
|
||||
import com.rit.whatsapp.model.UserState;
|
||||
import com.rit.whatsapp.service.ImsApiService;
|
||||
import com.rit.whatsapp.service.SessionService;
|
||||
import com.rit.whatsapp.service.WhatsAppMessageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class LoginHandler {
|
||||
|
||||
private final WhatsAppMessageService messageService;
|
||||
private final SessionService sessionService;
|
||||
private final ImsApiService imsApiService;
|
||||
private final StudentMenuHandler studentMenuHandler;
|
||||
|
||||
@Autowired
|
||||
public LoginHandler(WhatsAppMessageService messageService,
|
||||
SessionService sessionService,
|
||||
ImsApiService imsApiService,
|
||||
StudentMenuHandler studentMenuHandler) {
|
||||
this.messageService = messageService;
|
||||
this.sessionService = sessionService;
|
||||
this.imsApiService = imsApiService;
|
||||
this.studentMenuHandler = studentMenuHandler;
|
||||
}
|
||||
|
||||
public void startLoginFlow(String phoneNumber) {
|
||||
sessionService.setUserState(phoneNumber, UserState.AWAITING_LOGIN_ID);
|
||||
messageService.sendMessage(phoneNumber, "Welcome to RIT Digital Assistant! 🎓\n\nPlease enter your IMS User ID to login:");
|
||||
}
|
||||
|
||||
public void handleLoginIdInput(String phoneNumber, String imsId) {
|
||||
sessionService.setTempLoginId(phoneNumber, imsId);
|
||||
sessionService.setUserState(phoneNumber, UserState.AWAITING_LOGIN_PASSCODE);
|
||||
messageService.sendMessage(phoneNumber, "Thanks. Now, please enter your IMS Passcode:");
|
||||
}
|
||||
|
||||
public void handlePasscodeInput(String phoneNumber, String passcode) {
|
||||
String imsId = sessionService.getTempLoginId(phoneNumber);
|
||||
|
||||
messageService.sendMessage(phoneNumber, "Authenticating with IMS... ⏳");
|
||||
|
||||
String imsSessionToken = imsApiService.authenticate(imsId, passcode);
|
||||
|
||||
if (imsSessionToken != null) {
|
||||
// Authentication successful
|
||||
sessionService.authenticateUser(phoneNumber, imsId, passcode, imsSessionToken);
|
||||
sessionService.clearTempLoginId(phoneNumber);
|
||||
sessionService.resetState(phoneNumber); // Reset the flow state
|
||||
|
||||
messageService.sendMessage(phoneNumber, "✅ Login Successful!");
|
||||
|
||||
// Show the main menu
|
||||
studentMenuHandler.sendMainMenu(phoneNumber);
|
||||
} else {
|
||||
// Authentication failed
|
||||
sessionService.clearTempLoginId(phoneNumber);
|
||||
sessionService.setUserState(phoneNumber, UserState.AWAITING_LOGIN_ID);
|
||||
messageService.sendMessage(phoneNumber, "❌ Login Failed. Invalid ID or Passcode. Please try again.\n\nEnter your IMS User ID:");
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/main/java/com/rit/whatsapp/handler/StudentMenuHandler.java
Normal file
117
src/main/java/com/rit/whatsapp/handler/StudentMenuHandler.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package com.rit.whatsapp.handler;
|
||||
|
||||
import com.rit.whatsapp.model.UserState;
|
||||
import com.rit.whatsapp.service.ImsApiService;
|
||||
import com.rit.whatsapp.service.SessionService;
|
||||
import com.rit.whatsapp.service.WhatsAppMessageBuilder;
|
||||
import com.rit.whatsapp.service.WhatsAppMessageService;
|
||||
import com.rit.whatsapp.util.MessageFormatter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class StudentMenuHandler {
|
||||
|
||||
private final WhatsAppMessageService messageService;
|
||||
private final SessionService sessionService;
|
||||
private final GrievanceHandler grievanceHandler;
|
||||
private final ImsApiService imsApiService;
|
||||
|
||||
@Autowired
|
||||
public StudentMenuHandler(WhatsAppMessageService messageService,
|
||||
SessionService sessionService,
|
||||
GrievanceHandler grievanceHandler,
|
||||
ImsApiService imsApiService) {
|
||||
this.messageService = messageService;
|
||||
this.sessionService = sessionService;
|
||||
this.grievanceHandler = grievanceHandler;
|
||||
this.imsApiService = imsApiService;
|
||||
}
|
||||
|
||||
public void sendMainMenu(String phoneNumber) {
|
||||
sessionService.setUserState(phoneNumber, UserState.MAIN_MENU);
|
||||
|
||||
List<Map<String, String>> rows = new ArrayList<>();
|
||||
rows.add(createRow("student_attendance", "Attendance", "Overall % and Subject-wise"));
|
||||
rows.add(createRow("student_timetable", "Timetable", "Today's classes and Weekly"));
|
||||
rows.add(createRow("student_internal_marks", "Internal marks", "CAT 1, CAT 2, Overall"));
|
||||
rows.add(createRow("student_semester_results", "Semester results", "Latest result and CGPA"));
|
||||
rows.add(createRow("student_fee_details", "Fee details & receipt", "Fee paid, Pending, Due date"));
|
||||
rows.add(createRow("student_academic_calendar", "Academic calendar", "Holidays and Important dates"));
|
||||
rows.add(createRow("student_exam_schedule", "Exam schedule", "Upcoming exams and Hall details"));
|
||||
rows.add(createRow("student_circulars", "Circulars & Notices", "Department circulars"));
|
||||
rows.add(createRow("student_raise_grievance", "Raise a grievance \u2B50", "Submit an issue ticket"));
|
||||
rows.add(createRow("student_contact_department", "Contact department", "Faculty mentor, HOD, etc."));
|
||||
|
||||
Map<String, Object> interactiveList = WhatsAppMessageBuilder.buildListMessage(
|
||||
"RIT Digital Assistant",
|
||||
"Welcome to the RITz Student Portal! Please select an option from the menu below.",
|
||||
"Student Menu",
|
||||
rows
|
||||
);
|
||||
|
||||
messageService.sendInteractiveMessage(phoneNumber, interactiveList);
|
||||
|
||||
// Send a separate Logout button since the list is maxed out at 10 items
|
||||
List<Map<String, String>> logoutBtn = new ArrayList<>();
|
||||
Map<String, String> btn = new HashMap<>();
|
||||
btn.put("id", "student_logout");
|
||||
btn.put("title", "Logout");
|
||||
logoutBtn.add(btn);
|
||||
|
||||
Map<String, Object> logoutMessage = WhatsAppMessageBuilder.buildButtonMessage(
|
||||
"Need to switch accounts?",
|
||||
logoutBtn
|
||||
);
|
||||
messageService.sendInteractiveMessage(phoneNumber, logoutMessage);
|
||||
}
|
||||
|
||||
public void handleMenuSelection(String phoneNumber, String selectedOptionId) {
|
||||
String token = sessionService.getImsToken(phoneNumber);
|
||||
if (token == null) {
|
||||
messageService.sendMessage(phoneNumber, "Your session has expired. Please type 'Reset' to login again.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (selectedOptionId) {
|
||||
case "student_attendance":
|
||||
String rawAttendance = imsApiService.getAttendance(token);
|
||||
messageService.sendMessage(phoneNumber, MessageFormatter.formatAttendance(rawAttendance));
|
||||
break;
|
||||
|
||||
case "student_timetable":
|
||||
String rawTimetable = imsApiService.getTimetable(token);
|
||||
messageService.sendMessage(phoneNumber, MessageFormatter.formatTimetable(rawTimetable));
|
||||
break;
|
||||
|
||||
case "student_semester_results":
|
||||
// Defaulting to semester 1 for now
|
||||
String rawResults = imsApiService.getResults(token, 1);
|
||||
messageService.sendMessage(phoneNumber, MessageFormatter.formatResults(rawResults, 1));
|
||||
break;
|
||||
|
||||
case "student_raise_grievance":
|
||||
grievanceHandler.startGrievanceFlow(phoneNumber);
|
||||
break;
|
||||
|
||||
default:
|
||||
messageService.sendMessage(phoneNumber, "You selected an option that is currently under development. Please check back later!");
|
||||
// Resend main menu
|
||||
sendMainMenu(phoneNumber);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> createRow(String id, String title, String description) {
|
||||
Map<String, String> row = new HashMap<>();
|
||||
row.put("id", id);
|
||||
row.put("title", title);
|
||||
row.put("description", description);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
50
src/main/java/com/rit/whatsapp/model/UserEntity.java
Normal file
50
src/main/java/com/rit/whatsapp/model/UserEntity.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.rit.whatsapp.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class UserEntity {
|
||||
|
||||
@Id
|
||||
private String phoneNumber;
|
||||
|
||||
private String imsId;
|
||||
|
||||
private String encryptedPasscode;
|
||||
|
||||
public UserEntity() {
|
||||
}
|
||||
|
||||
public UserEntity(String phoneNumber, String imsId, String encryptedPasscode) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.imsId = imsId;
|
||||
this.encryptedPasscode = encryptedPasscode;
|
||||
}
|
||||
|
||||
public String getPhoneNumber() {
|
||||
return phoneNumber;
|
||||
}
|
||||
|
||||
public void setPhoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
}
|
||||
|
||||
public String getImsId() {
|
||||
return imsId;
|
||||
}
|
||||
|
||||
public void setImsId(String imsId) {
|
||||
this.imsId = imsId;
|
||||
}
|
||||
|
||||
public String getEncryptedPasscode() {
|
||||
return encryptedPasscode;
|
||||
}
|
||||
|
||||
public void setEncryptedPasscode(String encryptedPasscode) {
|
||||
this.encryptedPasscode = encryptedPasscode;
|
||||
}
|
||||
}
|
||||
9
src/main/java/com/rit/whatsapp/model/UserState.java
Normal file
9
src/main/java/com/rit/whatsapp/model/UserState.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package com.rit.whatsapp.model;
|
||||
|
||||
public enum UserState {
|
||||
MAIN_MENU,
|
||||
AWAITING_LOGIN_ID,
|
||||
AWAITING_LOGIN_PASSCODE,
|
||||
AWAITING_GRIEVANCE_CATEGORY,
|
||||
AWAITING_GRIEVANCE_DESC
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.rit.whatsapp.repository;
|
||||
|
||||
import com.rit.whatsapp.model.UserEntity;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<UserEntity, String> {
|
||||
}
|
||||
105
src/main/java/com/rit/whatsapp/service/ImsApiService.java
Normal file
105
src/main/java/com/rit/whatsapp/service/ImsApiService.java
Normal file
@@ -0,0 +1,105 @@
|
||||
package com.rit.whatsapp.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class ImsApiService {
|
||||
|
||||
private final String BASE_URL = "https://ims-api.sidharthprabhu.co.in";
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ImsApiService() {
|
||||
this.restTemplate = new RestTemplate();
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates the user against the real IMS backend.
|
||||
* @param imsId The user ID
|
||||
* @param passcode The passcode
|
||||
* @return The API session token if successful, null otherwise.
|
||||
*/
|
||||
public String authenticate(String imsId, String passcode) {
|
||||
String url = BASE_URL + "/api/auth/login";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
Map<String, String> requestBody = new HashMap<>();
|
||||
requestBody.put("username", imsId);
|
||||
requestBody.put("password", passcode);
|
||||
|
||||
HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
JsonNode rootNode = objectMapper.readTree(response.getBody());
|
||||
if (rootNode.has("success") && rootNode.get("success").asBoolean()) {
|
||||
if (rootNode.has("session")) {
|
||||
return rootNode.get("session").asText();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to authenticate with IMS API: " + e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String fetchFromIms(String endpoint, String token) {
|
||||
String url = BASE_URL + endpoint;
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(token);
|
||||
|
||||
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, org.springframework.http.HttpMethod.GET, request, String.class);
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
return response.getBody();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to fetch from IMS API (" + endpoint + "): " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getAttendance(String token) {
|
||||
return fetchFromIms("/api/student/attendance", token);
|
||||
}
|
||||
|
||||
public String getTimetable(String token) {
|
||||
return fetchFromIms("/api/student/timetable", token);
|
||||
}
|
||||
|
||||
public String getResults(String token, int semester) {
|
||||
return fetchFromIms("/api/student/results?semester=" + semester, token);
|
||||
}
|
||||
|
||||
public void logout(String token) {
|
||||
String url = BASE_URL + "/api/auth/logout";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(token);
|
||||
|
||||
HttpEntity<String> request = new HttpEntity<>(headers);
|
||||
|
||||
try {
|
||||
restTemplate.postForEntity(url, request, String.class);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to logout from IMS API: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/main/java/com/rit/whatsapp/service/SessionService.java
Normal file
107
src/main/java/com/rit/whatsapp/service/SessionService.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package com.rit.whatsapp.service;
|
||||
|
||||
import com.rit.whatsapp.model.UserEntity;
|
||||
import com.rit.whatsapp.model.UserState;
|
||||
import com.rit.whatsapp.repository.UserRepository;
|
||||
import com.rit.whatsapp.util.EncryptionUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class SessionService {
|
||||
|
||||
private final Map<String, UserState> userStates = new ConcurrentHashMap<>();
|
||||
|
||||
// In-memory cache for API session tokens
|
||||
private final Map<String, String> sessionTokens = new ConcurrentHashMap<>();
|
||||
private final Map<String, String> temporaryLoginIds = new ConcurrentHashMap<>();
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final EncryptionUtils encryptionUtils;
|
||||
private final ImsApiService imsApiService;
|
||||
|
||||
@Autowired
|
||||
public SessionService(UserRepository userRepository,
|
||||
EncryptionUtils encryptionUtils,
|
||||
ImsApiService imsApiService) {
|
||||
this.userRepository = userRepository;
|
||||
this.encryptionUtils = encryptionUtils;
|
||||
this.imsApiService = imsApiService;
|
||||
}
|
||||
|
||||
public UserState getUserState(String phoneNumber) {
|
||||
return userStates.getOrDefault(phoneNumber, UserState.MAIN_MENU);
|
||||
}
|
||||
|
||||
public void setUserState(String phoneNumber, UserState state) {
|
||||
userStates.put(phoneNumber, state);
|
||||
}
|
||||
|
||||
public void resetState(String phoneNumber) {
|
||||
userStates.remove(phoneNumber);
|
||||
}
|
||||
|
||||
public boolean isAuthenticated(String phoneNumber) {
|
||||
if (sessionTokens.containsKey(phoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check database for persistent session
|
||||
Optional<UserEntity> userOpt = userRepository.findById(phoneNumber);
|
||||
if (userOpt.isPresent()) {
|
||||
UserEntity user = userOpt.get();
|
||||
String decryptedPasscode = encryptionUtils.decrypt(user.getEncryptedPasscode());
|
||||
|
||||
// Silent background login
|
||||
String token = imsApiService.authenticate(user.getImsId(), decryptedPasscode);
|
||||
if (token != null) {
|
||||
sessionTokens.put(phoneNumber, token);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void authenticateUser(String phoneNumber, String imsId, String passcode, String token) {
|
||||
sessionTokens.put(phoneNumber, token);
|
||||
|
||||
// Persist to database
|
||||
String encryptedPasscode = encryptionUtils.encrypt(passcode);
|
||||
UserEntity user = new UserEntity(phoneNumber, imsId, encryptedPasscode);
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public String getImsToken(String phoneNumber) {
|
||||
// Ensure they are authenticated (which populates the token cache if needed)
|
||||
if (isAuthenticated(phoneNumber)) {
|
||||
return sessionTokens.get(phoneNumber);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void logoutUser(String phoneNumber) {
|
||||
String token = sessionTokens.remove(phoneNumber);
|
||||
if (token != null) {
|
||||
imsApiService.logout(token);
|
||||
}
|
||||
userRepository.deleteById(phoneNumber);
|
||||
resetState(phoneNumber);
|
||||
}
|
||||
|
||||
public void setTempLoginId(String phoneNumber, String tempId) {
|
||||
temporaryLoginIds.put(phoneNumber, tempId);
|
||||
}
|
||||
|
||||
public String getTempLoginId(String phoneNumber) {
|
||||
return temporaryLoginIds.get(phoneNumber);
|
||||
}
|
||||
|
||||
public void clearTempLoginId(String phoneNumber) {
|
||||
temporaryLoginIds.remove(phoneNumber);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.rit.whatsapp.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class WhatsAppMessageBuilder {
|
||||
|
||||
public static Map<String, Object> buildListMessage(String header, String body, String buttonText, List<Map<String, String>> rows) {
|
||||
Map<String, Object> interactive = new HashMap<>();
|
||||
interactive.put("type", "list");
|
||||
|
||||
if (header != null && !header.isEmpty()) {
|
||||
Map<String, Object> headerMap = new HashMap<>();
|
||||
headerMap.put("type", "text");
|
||||
headerMap.put("text", header);
|
||||
interactive.put("header", headerMap);
|
||||
}
|
||||
|
||||
Map<String, Object> bodyMap = new HashMap<>();
|
||||
bodyMap.put("text", body);
|
||||
interactive.put("body", bodyMap);
|
||||
|
||||
Map<String, Object> action = new HashMap<>();
|
||||
action.put("button", buttonText);
|
||||
|
||||
List<Map<String, Object>> sections = new ArrayList<>();
|
||||
Map<String, Object> section = new HashMap<>();
|
||||
section.put("title", "Options"); // Default section title
|
||||
section.put("rows", rows);
|
||||
sections.add(section);
|
||||
|
||||
action.put("sections", sections);
|
||||
interactive.put("action", action);
|
||||
|
||||
return interactive;
|
||||
}
|
||||
|
||||
public static Map<String, Object> buildButtonMessage(String body, List<Map<String, String>> buttons) {
|
||||
Map<String, Object> interactive = new HashMap<>();
|
||||
interactive.put("type", "button");
|
||||
|
||||
Map<String, Object> bodyMap = new HashMap<>();
|
||||
bodyMap.put("text", body);
|
||||
interactive.put("body", bodyMap);
|
||||
|
||||
Map<String, Object> action = new HashMap<>();
|
||||
List<Map<String, Object>> actionButtons = new ArrayList<>();
|
||||
|
||||
for (Map<String, String> btn : buttons) {
|
||||
Map<String, Object> buttonWrapper = new HashMap<>();
|
||||
buttonWrapper.put("type", "reply");
|
||||
Map<String, String> reply = new HashMap<>();
|
||||
reply.put("id", btn.get("id"));
|
||||
reply.put("title", btn.get("title"));
|
||||
buttonWrapper.put("reply", reply);
|
||||
actionButtons.add(buttonWrapper);
|
||||
}
|
||||
|
||||
action.put("buttons", actionButtons);
|
||||
interactive.put("action", action);
|
||||
|
||||
return interactive;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.rit.whatsapp.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class WhatsAppMessageService {
|
||||
|
||||
@Value("${whatsapp.api-token}")
|
||||
private String apiToken;
|
||||
|
||||
@Value("${whatsapp.phone-number-id}")
|
||||
private String phoneNumberId;
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public WhatsAppMessageService() {
|
||||
this.restTemplate = new RestTemplate();
|
||||
}
|
||||
|
||||
public void sendMessage(String toPhoneNumber, String messageText) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("messaging_product", "whatsapp");
|
||||
payload.put("to", toPhoneNumber);
|
||||
payload.put("type", "text");
|
||||
|
||||
Map<String, String> textPayload = new HashMap<>();
|
||||
textPayload.put("body", messageText);
|
||||
payload.put("text", textPayload);
|
||||
|
||||
executePost(payload);
|
||||
}
|
||||
|
||||
public void sendInteractiveMessage(String toPhoneNumber, Map<String, Object> interactiveObject) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("messaging_product", "whatsapp");
|
||||
payload.put("to", toPhoneNumber);
|
||||
payload.put("type", "interactive");
|
||||
payload.put("interactive", interactiveObject);
|
||||
|
||||
executePost(payload);
|
||||
}
|
||||
|
||||
private void executePost(Map<String, Object> payload) {
|
||||
String url = "https://graph.facebook.com/v19.0/" + phoneNumberId + "/messages";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(apiToken);
|
||||
|
||||
HttpEntity<Map<String, Object>> request = new HttpEntity<>(payload, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
|
||||
System.out.println("Message sent successfully. Response: " + response.getBody());
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to send message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/main/java/com/rit/whatsapp/util/EncryptionUtils.java
Normal file
43
src/main/java/com/rit/whatsapp/util/EncryptionUtils.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package com.rit.whatsapp.util;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
@Component
|
||||
public class EncryptionUtils {
|
||||
|
||||
@Value("${bot.encryption.secret}")
|
||||
private String secretKey;
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
|
||||
public String encrypt(String value) {
|
||||
try {
|
||||
SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
byte[] encryptedValue = cipher.doFinal(value.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encryptedValue);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error encrypting value", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(String value) {
|
||||
try {
|
||||
SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
||||
byte[] decodedValue = Base64.getDecoder().decode(value);
|
||||
byte[] decryptedValue = cipher.doFinal(decodedValue);
|
||||
return new String(decryptedValue, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error decrypting value", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/main/java/com/rit/whatsapp/util/MessageFormatter.java
Normal file
104
src/main/java/com/rit/whatsapp/util/MessageFormatter.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.rit.whatsapp.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class MessageFormatter {
|
||||
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public static String formatAttendance(String rawJson) {
|
||||
if (rawJson == null) return "❌ Failed to retrieve attendance data.";
|
||||
|
||||
try {
|
||||
JsonNode root = mapper.readTree(rawJson);
|
||||
// Assuming the JSON has an array of subjects or an overall percentage
|
||||
// Since we don't have the exact schema, this is a best-effort template.
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\ud83d\udcca *Your Attendance Report*\n\n");
|
||||
|
||||
if (root.has("overall_percentage")) {
|
||||
sb.append("*Overall Attendance:* ").append(root.get("overall_percentage").asText()).append("% \ud83d\udfe2\n\n");
|
||||
} else {
|
||||
sb.append("*Overall Attendance:* Data unavailable\n\n");
|
||||
}
|
||||
|
||||
sb.append("*Subject-wise Breakdown:*\n");
|
||||
if (root.has("data") && root.get("data").isArray()) {
|
||||
for (JsonNode subject : root.get("data")) {
|
||||
String name = subject.has("course_name") ? subject.get("course_name").asText() : "Unknown";
|
||||
String percentage = subject.has("percentage") ? subject.get("percentage").asText() : "0";
|
||||
sb.append("- ").append(name).append(": ").append(percentage).append("%\n");
|
||||
}
|
||||
} else {
|
||||
sb.append("- Details not available in this format.\n");
|
||||
}
|
||||
|
||||
sb.append("\n_Keep up the good work!_");
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "⚠️ Error formatting attendance data.";
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatTimetable(String rawJson) {
|
||||
if (rawJson == null) return "❌ Failed to retrieve timetable data.";
|
||||
|
||||
try {
|
||||
JsonNode root = mapper.readTree(rawJson);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\ud83d\uddd3\ufe0f *Today's Classes*\n\n");
|
||||
|
||||
if (root.has("data") && root.get("data").isArray()) {
|
||||
for (JsonNode classNode : root.get("data")) {
|
||||
String time = classNode.has("time") ? classNode.get("time").asText() : "TBA";
|
||||
String course = classNode.has("course_name") ? classNode.get("course_name").asText() : "Unknown";
|
||||
String room = classNode.has("room") ? classNode.get("room").asText() : "Unknown";
|
||||
|
||||
sb.append("*").append(time).append(":* ").append(course).append(" (Room ").append(room).append(")\n");
|
||||
}
|
||||
} else {
|
||||
sb.append("No classes scheduled for today.\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "⚠️ Error formatting timetable data.";
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatResults(String rawJson, int semester) {
|
||||
if (rawJson == null) return "❌ Failed to retrieve semester results.";
|
||||
|
||||
try {
|
||||
JsonNode root = mapper.readTree(rawJson);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\ud83c\udf93 *Semester ").append(semester).append(" Results*\n\n");
|
||||
|
||||
boolean passedAll = true;
|
||||
|
||||
if (root.has("data") && root.get("data").isArray()) {
|
||||
for (JsonNode courseNode : root.get("data")) {
|
||||
String name = courseNode.has("course_name") ? courseNode.get("course_name").asText() : "Unknown";
|
||||
String total = courseNode.has("total_mark") ? courseNode.get("total_mark").asText() : "0";
|
||||
String grade = courseNode.has("grade") ? courseNode.get("grade").asText() : "N/A";
|
||||
String result = courseNode.has("result") ? courseNode.get("result").asText() : "N/A";
|
||||
|
||||
if (!"PASS".equalsIgnoreCase(result)) {
|
||||
passedAll = false;
|
||||
}
|
||||
|
||||
sb.append("*").append(name).append(":* ").append(total).append("/100 (Grade: ").append(grade).append(")\n");
|
||||
}
|
||||
} else {
|
||||
sb.append("No results found for this semester.\n");
|
||||
}
|
||||
|
||||
sb.append("\n*Result:* ").append(passedAll ? "PASS \ud83c\udf89" : "Arrears \u26a0\ufe0f");
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
return "⚠️ Error formatting results data.";
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/main/resources/application.properties
Normal file
22
src/main/resources/application.properties
Normal file
@@ -0,0 +1,22 @@
|
||||
spring.application.name=whatsapp-bot
|
||||
server.port=8080
|
||||
|
||||
# WhatsApp Verification Token (You provide this to Meta during webhook setup)
|
||||
whatsapp.verify-token=rit_bot_secret_token_123
|
||||
|
||||
# WhatsApp Graph API Token (You get this from the Meta developer console)
|
||||
whatsapp.api-token=EAAc3Wjql00oBSZAZAWjbSjOOoX3dZBPZAb4VHcQqg1oZAYqPQXvd9fpoNb3bOoPeHtR6irmZB5zQtBApBctcS72eMRgIMa0TOqbP9huKHFoeKq3RJVgcLEtMRJGus4yDrSM23YJpkEbolrT2HDIxahm75fI3oetVZC27jmtoqkdTnVEAOO5wbhZAF7ZAg05nMqFWwMBvpd6cf4Regtfljj0ZBCZBfDaUkOSAf4ZC6D7VFU7djl6RY0qfbRRMRuf0F2D9gV7JzF8WZBZBcvepFixU7PAd6SUsCrxQZDZD
|
||||
# WhatsApp Phone Number ID (You get this from the Meta developer console)
|
||||
whatsapp.phone-number-id=1236658909534007
|
||||
|
||||
# H2 Database Configuration
|
||||
spring.datasource.url=jdbc:h2:file:./db/whatsappbot
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=password
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.h2.console.enabled=true
|
||||
|
||||
# Encryption Secret for Passcodes (Must be 16, 24, or 32 bytes)
|
||||
bot.encryption.secret=12345678901234567890123456789012
|
||||
Reference in New Issue
Block a user