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; @Value("${whatsapp.phone-number-id:}") private String configuredPhoneId; private final WhatsAppMessageService messageService; private final SessionService sessionService; private final StudentMenuHandler studentMenuHandler; private final GrievanceHandler grievanceHandler; private final LoginHandler loginHandler; private final ObjectMapper objectMapper; private final com.rit.whatsapp.handler.FacultyMenuHandler facultyMenuHandler; @Autowired public WebhookController(WhatsAppMessageService messageService, SessionService sessionService, StudentMenuHandler studentMenuHandler, GrievanceHandler grievanceHandler, LoginHandler loginHandler, com.rit.whatsapp.handler.FacultyMenuHandler facultyMenuHandler) { this.messageService = messageService; this.sessionService = sessionService; this.studentMenuHandler = studentMenuHandler; this.grievanceHandler = grievanceHandler; this.loginHandler = loginHandler; this.facultyMenuHandler = facultyMenuHandler; this.objectMapper = new ObjectMapper(); } @GetMapping public ResponseEntity 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 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) { if (valueNode.has("metadata")) { JsonNode metadataNode = valueNode.get("metadata"); if (metadataNode.has("phone_number_id")) { String incomingPhoneId = metadataNode.get("phone_number_id").asText(); if (configuredPhoneId != null && !configuredPhoneId.trim().isEmpty() && !incomingPhoneId.trim().equals(configuredPhoneId.trim())) { System.out.println("Incoming phone ID " + incomingPhoneId + " does not match configured ID " + configuredPhoneId + ". Ignoring webhook payload."); return ResponseEntity.ok("EVENT_IGNORED"); } } } } 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 isInMemory = sessionService.isSessionCached(fromPhoneNumber); boolean hasDbRecord = sessionService.hasSavedCredentials(fromPhoneNumber); if (!isInMemory && hasDbRecord) { messageService.sendMessage(fromPhoneNumber, "Authenticating User with IMS... ⏳"); } 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) { if (sessionService.isFaculty(fromPhoneNumber)) { facultyMenuHandler.sendMainMenu(fromPhoneNumber); } else { studentMenuHandler.sendMainMenu(fromPhoneNumber); } } else { loginHandler.startLoginFlow(fromPhoneNumber); } return ResponseEntity.ok("EVENT_RECEIVED"); } // Global erase command if ("text".equals(messageType) && messageNode.get("text").get("body").asText().trim().equalsIgnoreCase("erase")) { sessionService.deleteUser(fromPhoneNumber); messageService.sendMessage(fromPhoneNumber, "Your IMS credentials have been deleted from our records. Type 'Hi' to start again."); 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 { 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 ("image".equals(messageType)) { if (currentState == UserState.AWAITING_GRIEVANCE_IMAGE) { JsonNode imageNode = messageNode.get("image"); String mediaId = imageNode.get("id").asText(); String mimeType = imageNode.has("mime_type") ? imageNode.get("mime_type").asText() : "image/jpeg"; grievanceHandler.handleImageInput(fromPhoneNumber, mediaId, mimeType); } else { if (sessionService.isFaculty(fromPhoneNumber)) { facultyMenuHandler.sendMainMenu(fromPhoneNumber); } else { studentMenuHandler.sendMainMenu(fromPhoneNumber); } } } 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: loginHandler.startLoginFlow(phoneNumber); break; } } private void handleAuthenticatedTextMessage(String phoneNumber, String messageBody, UserState currentState) { switch (currentState) { case AWAITING_GRIEVANCE_TITLE: grievanceHandler.handleTitleInput(phoneNumber, messageBody); break; case AWAITING_GRIEVANCE_DESC: grievanceHandler.handleDescriptionInput(phoneNumber, messageBody); break; case AWAITING_GRIEVANCE_IMAGE: if (messageBody.trim().equalsIgnoreCase("skip")) { grievanceHandler.handleSkipImage(phoneNumber); } else { messageService.sendMessage(phoneNumber, "Please send a photo or type 'skip' / click 'Skip Image' button."); } break; case AWAITING_ABSENTEES: facultyMenuHandler.handleAbsenteesInput(phoneNumber, messageBody); break; case MAIN_MENU: default: if (sessionService.isFaculty(phoneNumber)) { facultyMenuHandler.sendMainMenu(phoneNumber); } else { studentMenuHandler.sendMainMenu(phoneNumber); } break; } } private void handleInteractiveListSelection(String phoneNumber, String selectedListId, UserState currentState) { if (selectedListId.startsWith("grv_action_")) { grievanceHandler.handleActionSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("grv_dept_")) { grievanceHandler.handleDepartmentSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("grv_status_")) { grievanceHandler.handleStatusSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("grv_delete_")) { grievanceHandler.handleDeleteSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("student_")) { studentMenuHandler.handleMenuSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("faculty_")) { facultyMenuHandler.handleMenuSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("sem_")) { studentMenuHandler.handleSemesterSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("receipt_") || selectedListId.startsWith("exam_") || selectedListId.startsWith("acad_") || selectedListId.startsWith("cons_")) { studentMenuHandler.handleReceiptSelection(phoneNumber, selectedListId); } else { if (sessionService.isFaculty(phoneNumber)) { facultyMenuHandler.sendMainMenu(phoneNumber); } else { studentMenuHandler.sendMainMenu(phoneNumber); } } } private void handleInteractiveButtonSelection(String phoneNumber, String selectedButtonId, UserState currentState) { if (selectedButtonId.startsWith("faculty_")) { facultyMenuHandler.handleButtonSelection(phoneNumber, selectedButtonId); return; } 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 ("student_weekly_timetable".equals(selectedButtonId)) { studentMenuHandler.handleWeeklyTimetable(phoneNumber); return; } if ("student_show_menu".equals(selectedButtonId)) { studentMenuHandler.sendStudentMenuDirectly(phoneNumber); return; } if ("student_exam_fees".equals(selectedButtonId)) { studentMenuHandler.handleExamFees(phoneNumber); return; } if ("student_cat1_marks".equals(selectedButtonId)) { studentMenuHandler.handleCat1Marks(phoneNumber); return; } if ("student_cat2_marks".equals(selectedButtonId)) { studentMenuHandler.handleCat2Marks(phoneNumber); return; } if ("student_cat3_marks".equals(selectedButtonId)) { studentMenuHandler.handleCat3Marks(phoneNumber); return; } if ("student_assignment_marks".equals(selectedButtonId)) { studentMenuHandler.handleAssignmentMarks(phoneNumber); return; } if ("student_payment_history".equals(selectedButtonId)) { studentMenuHandler.handlePaymentHistory(phoneNumber); return; } if ("grv_skip_image".equals(selectedButtonId)) { grievanceHandler.handleSkipImage(phoneNumber); } else { if (sessionService.isFaculty(phoneNumber)) { facultyMenuHandler.sendMainMenu(phoneNumber); } else { studentMenuHandler.sendMainMenu(phoneNumber); } } } }