feat: init development branch with permanent access token and menu handlers

This commit is contained in:
RIT Services
2026-08-29 07:22:13 +00:00
parent 7d6d019b85
commit a0fbe836c4
11 changed files with 1014 additions and 36 deletions

View File

@@ -27,18 +27,21 @@ public class WebhookController {
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) {
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();
}
@@ -88,13 +91,24 @@ public class WebhookController {
if ("text".equals(messageType) && messageNode.get("text").get("body").asText().trim().equalsIgnoreCase("reset")) {
sessionService.resetState(fromPhoneNumber);
if (isAuthenticated) {
studentMenuHandler.sendMainMenu(fromPhoneNumber);
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)) {
@@ -117,7 +131,11 @@ public class WebhookController {
String mimeType = imageNode.has("mime_type") ? imageNode.get("mime_type").asText() : "image/jpeg";
grievanceHandler.handleImageInput(fromPhoneNumber, mediaId, mimeType);
} else {
studentMenuHandler.sendMainMenu(fromPhoneNumber);
if (sessionService.isFaculty(fromPhoneNumber)) {
facultyMenuHandler.sendMainMenu(fromPhoneNumber);
} else {
studentMenuHandler.sendMainMenu(fromPhoneNumber);
}
}
} else if ("interactive".equals(messageType)) {
JsonNode interactiveNode = messageNode.get("interactive");
@@ -170,9 +188,16 @@ public class WebhookController {
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:
studentMenuHandler.sendMainMenu(phoneNumber);
if (sessionService.isFaculty(phoneNumber)) {
facultyMenuHandler.sendMainMenu(phoneNumber);
} else {
studentMenuHandler.sendMainMenu(phoneNumber);
}
break;
}
}
@@ -188,16 +213,27 @@ public class WebhookController {
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 {
studentMenuHandler.sendMainMenu(phoneNumber);
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.");
@@ -247,7 +283,11 @@ public class WebhookController {
if ("grv_skip_image".equals(selectedButtonId)) {
grievanceHandler.handleSkipImage(phoneNumber);
} else {
studentMenuHandler.sendMainMenu(phoneNumber);
if (sessionService.isFaculty(phoneNumber)) {
facultyMenuHandler.sendMainMenu(phoneNumber);
} else {
studentMenuHandler.sendMainMenu(phoneNumber);
}
}
}
}

View File

@@ -0,0 +1,442 @@
package com.rit.whatsapp.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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.*;
@Component
public class FacultyMenuHandler {
private final WhatsAppMessageService messageService;
private final SessionService sessionService;
private final ImsApiService imsApiService;
private final ObjectMapper objectMapper = new ObjectMapper();
@Autowired
public FacultyMenuHandler(WhatsAppMessageService messageService,
SessionService sessionService,
ImsApiService imsApiService) {
this.messageService = messageService;
this.sessionService = sessionService;
this.imsApiService = imsApiService;
}
public void sendMainMenu(String phoneNumber) {
sessionService.setUserState(phoneNumber, UserState.MAIN_MENU);
String token = sessionService.getImsToken(phoneNumber);
String name = sessionService.getStudentName(phoneNumber, token);
String text = "Welcome back, *" + name + "*! 👨‍🏫\n\nWhat would you like to do today?";
java.util.Map<String, Object> interactiveList = WhatsAppMessageBuilder.buildListMessage(
"RIT Digital Assistant",
text,
"Faculty Menu",
getFacultyMenuRows()
);
messageService.sendInteractiveMessage(phoneNumber, interactiveList);
}
public void sendFacultyMenuDirectly(String phoneNumber) {
sendMainMenu(phoneNumber);
}
public void handleMenuSelection(String phoneNumber, String selectedListId) {
if (selectedListId.startsWith("faculty_markatt_select_")) {
try {
int index = Integer.parseInt(selectedListId.substring("faculty_markatt_select_".length()));
handleClassSelected(phoneNumber, index);
} catch (Exception e) {
sendResponseWithMenu(phoneNumber, "❌ Invalid selection.");
}
return;
}
switch (selectedListId) {
case "faculty_timetable":
handleTimetable(phoneNumber);
break;
case "faculty_subjects":
handleSubjects(phoneNumber);
break;
case "faculty_attendance":
handleAttendanceSubjects(phoneNumber);
break;
case "faculty_logout":
sessionService.logoutUser(phoneNumber);
messageService.sendMessage(phoneNumber, "You have been logged out successfully. Type 'Hi' to log in again.");
break;
default:
sendMainMenu(phoneNumber);
break;
}
}
public void handleTimetable(String phoneNumber) {
String token = sessionService.getImsToken(phoneNumber);
if (token == null) {
messageService.sendMessage(phoneNumber, "Your session has expired. Please type 'Reset' to login again.");
return;
}
sendTimetableMenu(phoneNumber, token);
}
public void sendTimetableMenu(String phoneNumber, String token) {
String timetableJson = imsApiService.fetchFromIms("/api/faculty/timetable", token);
if (timetableJson == null) {
sendResponseWithMenu(phoneNumber, "❌ Failed to fetch timetable. Please try again later.");
return;
}
String todaySchedule = MessageFormatter.formatFacultyTimetable(timetableJson);
if (todaySchedule.length() > 1020) {
todaySchedule = todaySchedule.substring(0, 1017) + "...";
}
List<Map<String, String>> buttons = new ArrayList<>();
Map<String, String> weeklyBtn = new HashMap<>();
weeklyBtn.put("id", "faculty_weekly_timetable");
weeklyBtn.put("title", "Weekly Timetable");
buttons.add(weeklyBtn);
Map<String, String> menuBtn = new HashMap<>();
menuBtn.put("id", "faculty_show_menu");
menuBtn.put("title", "Faculty Menu");
buttons.add(menuBtn);
Map<String, Object> buttonMessage = WhatsAppMessageBuilder.buildButtonMessage(todaySchedule, buttons);
messageService.sendInteractiveMessage(phoneNumber, buttonMessage);
sendMainMenu(phoneNumber);
}
public void handleWeeklyTimetable(String phoneNumber) {
String token = sessionService.getImsToken(phoneNumber);
if (token == null) {
messageService.sendMessage(phoneNumber, "Your session has expired. Please type 'Reset' to login again.");
return;
}
String rawTimetable = imsApiService.fetchFromIms("/api/faculty/timetable", token);
String weeklyTimetable = MessageFormatter.formatFacultyWeeklyTimetable(rawTimetable);
messageService.sendMessage(phoneNumber, weeklyTimetable);
sendTimetableMenu(phoneNumber, token);
}
public void handleSubjects(String phoneNumber) {
String token = sessionService.getImsToken(phoneNumber);
String subjectsJson = imsApiService.fetchFromIms("/api/faculty/subjects", token);
if (subjectsJson != null) {
String formattedSubjects = MessageFormatter.formatFacultySubjects(subjectsJson);
sendResponseWithMenu(phoneNumber, formattedSubjects);
} else {
sendResponseWithMenu(phoneNumber, "❌ Failed to fetch subjects. Please try again later.");
}
}
public void handleAttendanceSubjects(String phoneNumber) {
sendResponseWithMenu(phoneNumber, "🚧 *Feature under development*");
}
public void handleClassSelected(String phoneNumber, int index) {
Map<String, Object> params = sessionService.getAttendanceSubjectParams(phoneNumber, index);
if (params == null) {
sendResponseWithMenu(phoneNumber, "❌ Failed to retrieve class details. Please try again.");
return;
}
sessionService.setActiveAttendanceParams(phoneNumber, params);
String subjectName = (String) params.get("subjectName");
Object periodsObj = params.get("periods");
Object classesObj = params.get("classes");
String periodStr = (periodsObj instanceof List) ? String.join(", ", (List<String>) periodsObj) : String.valueOf(periodsObj);
String classStr = (classesObj instanceof List) ? String.join(", ", (List<String>) classesObj) : String.valueOf(classesObj);
String body = "Selected Class:\n📚 *" + subjectName + "*\n📅 Period: *" + periodStr + "*\n🏫 Class: *" + classStr + "*\n\nChoose an action:";
List<Map<String, String>> buttons = new ArrayList<>();
Map<String, String> presentBtn = new HashMap<>();
presentBtn.put("id", "faculty_mark_present");
presentBtn.put("title", "Mark All Present");
buttons.add(presentBtn);
Map<String, String> absBtn = new HashMap<>();
absBtn.put("id", "faculty_mark_absentees");
absBtn.put("title", "Enter Absentees");
buttons.add(absBtn);
Map<String, Object> buttonMessage = WhatsAppMessageBuilder.buildButtonMessage(body, buttons);
messageService.sendInteractiveMessage(phoneNumber, buttonMessage);
}
public void handleButtonSelection(String phoneNumber, String selectedButtonId) {
switch (selectedButtonId) {
case "faculty_mark_present":
handleMarkAllPresent(phoneNumber);
break;
case "faculty_mark_absentees":
handleStartAbsenteesFlow(phoneNumber);
break;
case "faculty_weekly_timetable":
handleWeeklyTimetable(phoneNumber);
break;
case "faculty_show_menu":
sendMainMenu(phoneNumber);
break;
}
}
private Map<String, Object> mapToApiParams(Map<String, Object> params) {
Map<String, Object> apiParams = new HashMap<>();
apiParams.put("date", params.get("date"));
apiParams.put("period", params.get("periods"));
apiParams.put("subject", params.get("subjectId"));
apiParams.put("staff", params.get("staffId"));
apiParams.put("classes", params.get("classes"));
apiParams.put("special", params.get("specialSub") != null ? params.get("specialSub") : 0);
apiParams.put("regular", params.get("regularSub") != null ? params.get("regularSub") : 1);
apiParams.put("batch", "");
return apiParams;
}
public void handleMarkAllPresent(String phoneNumber) {
String token = sessionService.getImsToken(phoneNumber);
Map<String, Object> activeParams = sessionService.getActiveAttendanceParams(phoneNumber);
if (activeParams == null) {
sendResponseWithMenu(phoneNumber, "❌ Session expired or invalid class. Please try again.");
return;
}
messageService.sendMessage(phoneNumber, "Retrieving student list... ⏳");
String rawList = imsApiService.getStudentListForAttendance(token, mapToApiParams(activeParams));
if (rawList == null) {
sendResponseWithMenu(phoneNumber, "❌ Failed to retrieve student list from IMS.");
return;
}
try {
JsonNode root = objectMapper.readTree(rawList);
if (root.has("success") && !root.get("success").asBoolean()) {
String errorMsg = root.has("message") ? root.get("message").asText() : "Unknown IMS error";
sendResponseWithMenu(phoneNumber, "❌ IMS Error: " + errorMsg);
return;
}
} catch (Exception e) {
// Proceed to parse HTML if not JSON error
}
List<MessageFormatter.StudentAttendanceInfo> students = MessageFormatter.parseStudents(rawList);
if (students.isEmpty()) {
sendResponseWithMenu(phoneNumber, "❌ No students found in this class.");
return;
}
Map<String, Object> postData = new HashMap<>();
postData.put("date", activeParams.get("date"));
postData.put("period", activeParams.get("periods"));
postData.put("subject", activeParams.get("subjectId"));
List<Map<String, String>> studentStatusList = new ArrayList<>();
for (MessageFormatter.StudentAttendanceInfo s : students) {
Map<String, String> sStatus = new HashMap<>();
sStatus.put("id", s.id);
sStatus.put("status", "present");
studentStatusList.add(sStatus);
}
postData.put("students", studentStatusList);
messageService.sendMessage(phoneNumber, "Submitting attendance to IMS... ⏳");
String submitRes = imsApiService.submitAttendance(token, postData);
boolean isSuccess = false;
String errorMsg = "Unknown error";
try {
if (submitRes != null) {
JsonNode root = objectMapper.readTree(submitRes);
if (root.has("success") && root.get("success").asBoolean()) {
isSuccess = true;
} else if (root.has("message")) {
errorMsg = root.get("message").asText();
}
}
} catch (Exception e) {
// fallback
}
if (isSuccess) {
sendResponseWithMenu(phoneNumber, "✅ Attendance marked successfully! (100% Present)");
} else {
sendResponseWithMenu(phoneNumber, "❌ Failed to submit attendance: " + errorMsg);
}
sessionService.clearActiveAttendanceParams(phoneNumber);
}
public void handleStartAbsenteesFlow(String phoneNumber) {
String token = sessionService.getImsToken(phoneNumber);
Map<String, Object> activeParams = sessionService.getActiveAttendanceParams(phoneNumber);
if (activeParams == null) {
sendResponseWithMenu(phoneNumber, "❌ Session expired or invalid class. Please try again.");
return;
}
messageService.sendMessage(phoneNumber, "Retrieving student list... ⏳");
String rawList = imsApiService.getStudentListForAttendance(token, mapToApiParams(activeParams));
if (rawList == null) {
sendResponseWithMenu(phoneNumber, "❌ Failed to retrieve student list from IMS.");
return;
}
try {
JsonNode root = objectMapper.readTree(rawList);
if (root.has("success") && !root.get("success").asBoolean()) {
String errorMsg = root.has("message") ? root.get("message").asText() : "Unknown IMS error";
sendResponseWithMenu(phoneNumber, "❌ IMS Error: " + errorMsg);
return;
}
} catch (Exception e) {
// Proceed to parse HTML if not JSON error
}
List<MessageFormatter.StudentAttendanceInfo> students = MessageFormatter.parseStudents(rawList);
if (students.isEmpty()) {
sendResponseWithMenu(phoneNumber, "❌ No students found in this class.");
return;
}
sessionService.storeActiveStudentList(phoneNumber, students);
sessionService.setUserState(phoneNumber, UserState.AWAITING_ABSENTEES);
StringBuilder sb = new StringBuilder();
sb.append("📋 *Student List*\n");
sb.append("Please reply with the Serial Numbers of the ABSENT students separated by commas (e.g., 2, 5).\n");
sb.append("Type *'none'* if everyone is present.\n━━━━━━━━━━━━━━\n\n");
for (int i = 0; i < students.size(); i++) {
MessageFormatter.StudentAttendanceInfo s = students.get(i);
sb.append(i + 1).append(". `").append(s.regNo).append("` - ").append(s.name).append("\n");
}
messageService.sendMessage(phoneNumber, sb.toString());
}
public void handleAbsenteesInput(String phoneNumber, String messageBody) {
String token = sessionService.getImsToken(phoneNumber);
Map<String, Object> activeParams = sessionService.getActiveAttendanceParams(phoneNumber);
List<MessageFormatter.StudentAttendanceInfo> students = sessionService.getActiveStudentList(phoneNumber);
if (activeParams == null || students == null) {
sessionService.resetState(phoneNumber);
sendResponseWithMenu(phoneNumber, "❌ Session expired. Please restart marking attendance.");
return;
}
Set<Integer> absenteeIndices = new HashSet<>();
String input = messageBody.trim().toLowerCase();
if (!input.equals("none") && !input.equals("nil") && !input.equals("no one")) {
String[] parts = input.split(",");
for (String p : parts) {
try {
int val = Integer.parseInt(p.trim());
if (val >= 1 && val <= students.size()) {
absenteeIndices.add(val - 1);
}
} catch (NumberFormatException e) {
// Ignore non-numeric inputs
}
}
}
Map<String, Object> postData = new HashMap<>();
postData.put("date", activeParams.get("date"));
postData.put("period", activeParams.get("periods"));
postData.put("subject", activeParams.get("subjectId"));
List<Map<String, String>> studentStatusList = new ArrayList<>();
int absentCount = 0;
for (int i = 0; i < students.size(); i++) {
MessageFormatter.StudentAttendanceInfo s = students.get(i);
Map<String, String> sStatus = new HashMap<>();
sStatus.put("id", s.id);
if (absenteeIndices.contains(i)) {
sStatus.put("status", "absent");
absentCount++;
} else {
sStatus.put("status", "present");
}
studentStatusList.add(sStatus);
}
postData.put("students", studentStatusList);
messageService.sendMessage(phoneNumber, "Submitting attendance to IMS... ⏳");
String submitRes = imsApiService.submitAttendance(token, postData);
boolean isSuccess = false;
String errorMsg = "Unknown error";
try {
if (submitRes != null) {
JsonNode root = objectMapper.readTree(submitRes);
if (root.has("success") && root.get("success").asBoolean()) {
isSuccess = true;
} else if (root.has("message")) {
errorMsg = root.get("message").asText();
}
}
} catch (Exception e) {
// fallback
}
sessionService.resetState(phoneNumber);
sessionService.clearActiveAttendanceParams(phoneNumber);
if (isSuccess) {
sendResponseWithMenu(phoneNumber, "✅ Attendance marked successfully! (" + (students.size() - absentCount) + " Present, " + absentCount + " Absent)");
} else {
sendResponseWithMenu(phoneNumber, "❌ Failed to submit attendance: " + errorMsg);
}
}
public void sendResponseWithMenu(String phoneNumber, String bodyText) {
if (bodyText.length() > 1024) {
bodyText = bodyText.substring(0, 1021) + "...";
}
java.util.Map<String, Object> interactiveList = WhatsAppMessageBuilder.buildListMessage(
null,
bodyText,
"Faculty Menu",
getFacultyMenuRows()
);
messageService.sendInteractiveMessage(phoneNumber, interactiveList);
}
private java.util.List<java.util.Map<String, String>> getFacultyMenuRows() {
java.util.List<java.util.Map<String, String>> rows = new java.util.ArrayList<>();
rows.add(createRow("faculty_timetable", "📅 View Timetable", "Check your daily schedule"));
rows.add(createRow("faculty_subjects", "📚 View Subjects", "Subjects you are teaching"));
rows.add(createRow("faculty_attendance", "📝 Mark Attendance", "Mark student attendance"));
rows.add(createRow("faculty_logout", "🚪 Logout", "Sign out of your account"));
return rows;
}
private java.util.Map<String, String> createRow(String id, String title, String description) {
java.util.Map<String, String> row = new java.util.HashMap<>();
row.put("id", id);
row.put("title", title);
row.put("description", description);
return row;
}
}

View File

@@ -15,15 +15,19 @@ public class LoginHandler {
private final ImsApiService imsApiService;
private final StudentMenuHandler studentMenuHandler;
private final FacultyMenuHandler facultyMenuHandler;
@Autowired
public LoginHandler(WhatsAppMessageService messageService,
SessionService sessionService,
ImsApiService imsApiService,
StudentMenuHandler studentMenuHandler) {
StudentMenuHandler studentMenuHandler,
FacultyMenuHandler facultyMenuHandler) {
this.messageService = messageService;
this.sessionService = sessionService;
this.imsApiService = imsApiService;
this.studentMenuHandler = studentMenuHandler;
this.facultyMenuHandler = facultyMenuHandler;
}
public void startLoginFlow(String phoneNumber) {
@@ -42,18 +46,22 @@ public class LoginHandler {
messageService.sendMessage(phoneNumber, "Authenticating with IMS... ⏳");
String imsSessionToken = imsApiService.authenticate(imsId, passcode);
ImsApiService.AuthResult result = imsApiService.authenticate(imsId, passcode);
if (imsSessionToken != null) {
if (result != null && result.token != null) {
// Authentication successful
sessionService.authenticateUser(phoneNumber, imsId, passcode, imsSessionToken);
sessionService.authenticateUser(phoneNumber, imsId, passcode, result);
sessionService.clearTempLoginId(phoneNumber);
sessionService.resetState(phoneNumber); // Reset the flow state
messageService.sendMessage(phoneNumber, "✅ Login Successful!");
// Show the main menu
studentMenuHandler.sendMainMenu(phoneNumber);
if (result.isFaculty) {
facultyMenuHandler.sendMainMenu(phoneNumber);
} else {
studentMenuHandler.sendMainMenu(phoneNumber);
}
} else {
// Authentication failed
sessionService.clearTempLoginId(phoneNumber);

View File

@@ -1,24 +1,38 @@
package com.rit.whatsapp.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
@Table(name = "students")
public class UserEntity {
@Id
private String phoneNumber;
@Column(name = "roll_no")
private String imsId;
@Column(name = "mobile_no")
private String phoneNumber;
@Column(name = "encrypted_passcode")
private String encryptedPasscode;
@Column(name = "is_faculty", columnDefinition = "boolean default false")
private Boolean isFaculty = false;
public UserEntity() {
}
public UserEntity(String phoneNumber, String imsId, String encryptedPasscode) {
public UserEntity(String imsId, String phoneNumber, String encryptedPasscode) {
this.imsId = imsId;
this.phoneNumber = phoneNumber;
this.encryptedPasscode = encryptedPasscode;
}
// Keep secondary constructor for backward compatibility
public UserEntity(String phoneNumber, String imsId, String encryptedPasscode, boolean legacyOrder) {
this.phoneNumber = phoneNumber;
this.imsId = imsId;
this.encryptedPasscode = encryptedPasscode;
@@ -47,4 +61,12 @@ public class UserEntity {
public void setEncryptedPasscode(String encryptedPasscode) {
this.encryptedPasscode = encryptedPasscode;
}
public boolean isFaculty() {
return isFaculty != null && isFaculty;
}
public void setFaculty(Boolean faculty) {
isFaculty = faculty;
}
}

View File

@@ -12,5 +12,6 @@ public enum UserState {
AWAITING_GRIEVANCE_DESC,
AWAITING_GRIEVANCE_IMAGE,
AWAITING_GRIEVANCE_STATUS_SELECTION,
AWAITING_GRIEVANCE_DELETE_SELECTION
AWAITING_GRIEVANCE_DELETE_SELECTION,
AWAITING_ABSENTEES
}

View File

@@ -3,7 +3,10 @@ package com.rit.whatsapp.repository;
import com.rit.whatsapp.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<UserEntity, String> {
Optional<UserEntity> findByPhoneNumber(String phoneNumber);
boolean existsByPhoneNumber(String phoneNumber);
}

View File

@@ -24,13 +24,22 @@ public class ImsApiService {
this.objectMapper = new ObjectMapper();
}
public static class AuthResult {
public final String token;
public final boolean isFaculty;
public AuthResult(String token, boolean isFaculty) {
this.token = token;
this.isFaculty = isFaculty;
}
}
/**
* 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.
* @return AuthResult if successful, null otherwise.
*/
public String authenticate(String imsId, String passcode) {
public AuthResult authenticate(String imsId, String passcode) {
String url = BASE_URL + "/api/auth/login";
HttpHeaders headers = new HttpHeaders();
@@ -48,7 +57,12 @@ public class ImsApiService {
JsonNode rootNode = objectMapper.readTree(response.getBody());
if (rootNode.has("success") && rootNode.get("success").asBoolean()) {
if (rootNode.has("session")) {
return rootNode.get("session").asText();
String token = rootNode.get("session").asText();
boolean isFaculty = false;
if (rootNode.has("role")) {
isFaculty = "staff".equals(rootNode.get("role").asText());
}
return new AuthResult(token, isFaculty);
}
}
}
@@ -59,7 +73,7 @@ public class ImsApiService {
return null;
}
private String fetchFromIms(String endpoint, String token) {
public String fetchFromIms(String endpoint, String token) {
String url = BASE_URL + endpoint;
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
@@ -250,4 +264,65 @@ public class ImsApiService {
}
return null;
}
public String getFacultyProfile(String token) {
return fetchFromIms("/api/faculty/profile", token);
}
public String getFacultyAttendanceSubjects(String token) {
return fetchFromIms("/api/faculty/attendance-subjects", token);
}
public String getFacultyName(String token) {
String rawJson = getFacultyProfile(token);
if (rawJson == null) return "Faculty";
try {
JsonNode root = objectMapper.readTree(rawJson);
if (root.has("data")) {
JsonNode data = root.get("data");
if (data.has("name")) return data.get("name").asText();
}
} catch (Exception e) {
System.err.println("Failed to parse faculty profile JSON: " + e.getMessage());
}
return "Faculty";
}
public String getStudentListForAttendance(String token, Map<String, Object> params) {
String url = BASE_URL + "/api/faculty/attendance/list";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(params, headers);
try {
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
return response.getBody();
} catch (org.springframework.web.client.HttpStatusCodeException e) {
System.err.println("Failed to fetch student list (HTTP error): " + e.getResponseBodyAsString());
return e.getResponseBodyAsString();
} catch (Exception e) {
System.err.println("Failed to fetch student list: " + e.getMessage());
return "{\"success\":false,\"message\":\"" + e.getMessage() + "\"}";
}
}
public String submitAttendance(String token, Map<String, Object> postData) {
String url = BASE_URL + "/api/faculty/attendance/take_periods";
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(postData, headers);
try {
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
return response.getBody();
} catch (org.springframework.web.client.HttpStatusCodeException e) {
System.err.println("Failed to submit attendance (HTTP error): " + e.getResponseBodyAsString());
return e.getResponseBodyAsString();
} catch (Exception e) {
System.err.println("Failed to submit attendance: " + e.getMessage());
return "{\"success\":false,\"message\":\"" + e.getMessage() + "\"}";
}
}
}

View File

@@ -8,6 +8,7 @@ import com.rit.whatsapp.util.EncryptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@@ -48,7 +49,7 @@ public class SessionService {
}
public boolean hasSavedCredentials(String phoneNumber) {
return userRepository.existsById(phoneNumber);
return userRepository.existsByPhoneNumber(phoneNumber);
}
public void resetState(String phoneNumber) {
@@ -61,15 +62,22 @@ public class SessionService {
}
// Check database for persistent session
Optional<UserEntity> userOpt = userRepository.findById(phoneNumber);
Optional<UserEntity> userOpt = userRepository.findByPhoneNumber(phoneNumber);
if (userOpt.isPresent()) {
UserEntity user = userOpt.get();
if (user.getEncryptedPasscode() == null) {
return false;
}
String decryptedPasscode = encryptionUtils.decrypt(user.getEncryptedPasscode());
// Silent background login
String token = imsApiService.authenticate(user.getImsId(), decryptedPasscode);
if (token != null) {
sessionTokens.put(phoneNumber, token);
ImsApiService.AuthResult result = imsApiService.authenticate(user.getImsId(), decryptedPasscode);
if (result != null) {
sessionTokens.put(phoneNumber, result.token);
if (user.isFaculty() != result.isFaculty) {
user.setFaculty(result.isFaculty);
userRepository.save(user);
}
return true;
}
}
@@ -77,12 +85,21 @@ public class SessionService {
return false;
}
public void authenticateUser(String phoneNumber, String imsId, String passcode, String token) {
sessionTokens.put(phoneNumber, token);
public void authenticateUser(String phoneNumber, String imsId, String passcode, ImsApiService.AuthResult result) {
sessionTokens.put(phoneNumber, result.token);
// Persist to database
String encryptedPasscode = encryptionUtils.encrypt(passcode);
UserEntity user = new UserEntity(phoneNumber, imsId, encryptedPasscode);
// Persist to database (students table)
Optional<UserEntity> userOpt = userRepository.findById(imsId);
UserEntity user;
if (userOpt.isPresent()) {
user = userOpt.get();
user.setPhoneNumber(phoneNumber);
user.setEncryptedPasscode(encryptionUtils.encrypt(passcode));
} else {
String encryptedPasscode = encryptionUtils.encrypt(passcode);
user = new UserEntity(imsId, phoneNumber, encryptedPasscode);
}
user.setFaculty(result.isFaculty);
userRepository.save(user);
}
@@ -99,7 +116,12 @@ public class SessionService {
if (token != null) {
imsApiService.logout(token);
}
userRepository.deleteById(phoneNumber);
Optional<UserEntity> userOpt = userRepository.findByPhoneNumber(phoneNumber);
if (userOpt.isPresent()) {
UserEntity user = userOpt.get();
user.setEncryptedPasscode(null);
userRepository.save(user);
}
studentNames.remove(phoneNumber);
resetState(phoneNumber);
}
@@ -108,11 +130,53 @@ public class SessionService {
if (studentNames.containsKey(phoneNumber)) {
return studentNames.get(phoneNumber);
}
String name = imsApiService.getStudentName(token);
String name;
if (isFaculty(phoneNumber)) {
name = imsApiService.getFacultyName(token);
} else {
name = imsApiService.getStudentName(token);
}
studentNames.put(phoneNumber, name);
return name;
}
private final Map<String, List<Map<String, Object>>> attendanceSubjectParams = new ConcurrentHashMap<>();
private final Map<String, Map<String, Object>> activeAttendanceParams = new ConcurrentHashMap<>();
private final Map<String, List<com.rit.whatsapp.util.MessageFormatter.StudentAttendanceInfo>> activeStudentLists = new ConcurrentHashMap<>();
public void storeAttendanceSubjectParams(String phoneNumber, List<Map<String, Object>> paramsList) {
attendanceSubjectParams.put(phoneNumber, paramsList);
}
public Map<String, Object> getAttendanceSubjectParams(String phoneNumber, int index) {
List<Map<String, Object>> paramsList = attendanceSubjectParams.get(phoneNumber);
if (paramsList != null && index >= 0 && index < paramsList.size()) {
return paramsList.get(index);
}
return null;
}
public void setActiveAttendanceParams(String phoneNumber, Map<String, Object> params) {
activeAttendanceParams.put(phoneNumber, params);
}
public Map<String, Object> getActiveAttendanceParams(String phoneNumber) {
return activeAttendanceParams.get(phoneNumber);
}
public void clearActiveAttendanceParams(String phoneNumber) {
activeAttendanceParams.remove(phoneNumber);
activeStudentLists.remove(phoneNumber);
}
public void storeActiveStudentList(String phoneNumber, List<com.rit.whatsapp.util.MessageFormatter.StudentAttendanceInfo> list) {
activeStudentLists.put(phoneNumber, list);
}
public List<com.rit.whatsapp.util.MessageFormatter.StudentAttendanceInfo> getActiveStudentList(String phoneNumber) {
return activeStudentLists.get(phoneNumber);
}
private final Map<String, Map<String, String>> receiptIdMappings = new ConcurrentHashMap<>();
public void storeReceiptIdMappings(String phoneNumber, Map<String, String> mappings) {
@@ -158,10 +222,26 @@ public class SessionService {
}
public String getImsId(String phoneNumber) {
Optional<UserEntity> userOpt = userRepository.findById(phoneNumber);
Optional<UserEntity> userOpt = userRepository.findByPhoneNumber(phoneNumber);
if (userOpt.isPresent()) {
return userOpt.get().getImsId();
}
return null;
}
public boolean isFaculty(String phoneNumber) {
Optional<UserEntity> userOpt = userRepository.findByPhoneNumber(phoneNumber);
if (userOpt.isPresent()) {
return userOpt.get().isFaculty();
}
return false;
}
public void deleteUser(String phoneNumber) {
logoutUser(phoneNumber);
Optional<UserEntity> userOpt = userRepository.findByPhoneNumber(phoneNumber);
if (userOpt.isPresent()) {
userRepository.delete(userOpt.get());
}
}
}

View File

@@ -1244,4 +1244,310 @@ public class MessageFormatter {
if (text.length() <= length) return text;
return text.substring(0, length);
}
public static String formatFacultyTimetable(String rawJson) {
if (rawJson == null) return "❌ Failed to retrieve timetable data.";
try {
JsonNode root = mapper.readTree(rawJson);
if (!root.has("data") || !root.get("data").isObject() || !root.get("data").has("schedule")) {
return "❌ Timetable details not available in this format.";
}
JsonNode schedule = root.get("data").get("schedule");
String day = java.time.LocalDate.now().getDayOfWeek().name().toLowerCase();
StringBuilder sb = new StringBuilder();
sb.append("📅 *Today's Classes (Faculty)*\n");
sb.append("━━━━━━━━━━━━━━\n\n");
if (schedule.has(day)) {
JsonNode daySchedule = schedule.get(day);
int maxPeriod = 7;
java.util.Iterator<String> fieldNames = daySchedule.fieldNames();
while (fieldNames.hasNext()) {
String fName = fieldNames.next();
try {
int p = Integer.parseInt(fName);
if (p > maxPeriod) maxPeriod = p;
} catch (NumberFormatException e) {}
}
for (int period = 1; period <= maxPeriod; period++) {
String periodKey = String.valueOf(period);
sb.append(getNumberEmoji(period)).append(" ");
if (daySchedule.has(periodKey)) {
JsonNode periodNode = daySchedule.get(periodKey);
java.util.List<String> entries = new java.util.ArrayList<>();
if (periodNode.isArray()) {
for (JsonNode node : periodNode) {
String sName = node.has("subjectName") ? node.get("subjectName").asText() : "";
String sCode = node.has("subjectCode") ? node.get("subjectCode").asText() : "";
String cName = node.has("className") ? node.get("className").asText() : "";
entries.add("*" + sName + "*\n `" + sCode + "` - " + cName);
}
}
if (!entries.isEmpty()) {
sb.append(String.join(" / ", entries)).append("\n\n");
} else {
sb.append("*Free Period*\n\n");
}
} else {
sb.append("*Free Period*\n\n");
}
}
} else {
sb.append("No classes scheduled for today.\n\n");
}
sb.append("━━━━━━━━━━━━━━");
return sb.toString();
} catch (Exception e) {
System.err.println("Error formatting faculty timetable: " + e.getMessage());
return "⚠️ Error formatting timetable data.";
}
}
public static String formatFacultySubjects(String rawJson) {
if (rawJson == null) return "❌ Failed to retrieve subjects data.";
try {
JsonNode root = mapper.readTree(rawJson);
if (!root.has("data") || !root.get("data").isObject() || !root.get("data").has("subjects")) {
return "❌ Subjects details not available in this format.";
}
JsonNode subjects = root.get("data").get("subjects");
StringBuilder sb = new StringBuilder();
sb.append("📚 *Your Subjects*\n");
sb.append("━━━━━━━━━━━━━━\n\n");
if (subjects.isArray() && subjects.size() > 0) {
for (JsonNode node : subjects) {
String sName = node.has("subjectName") ? node.get("subjectName").asText() : "";
String sCode = node.has("subjectCode") ? node.get("subjectCode").asText() : "";
String cName = node.has("className") ? node.get("className").asText() : "";
sb.append("🔹 *").append(sName).append("*\n");
sb.append(" Code: `").append(sCode).append("`\n");
sb.append(" Class: ").append(cName).append("\n\n");
}
} else {
sb.append("No subjects assigned currently.\n\n");
}
sb.append("━━━━━━━━━━━━━━");
return sb.toString();
} catch (Exception e) {
System.err.println("Error formatting faculty subjects: " + e.getMessage());
return "⚠️ Error formatting subjects data.";
}
}
public static String formatFacultyAttendanceSubjects(String rawJson) {
if (rawJson == null) return "❌ Failed to retrieve attendance subjects data.";
try {
JsonNode root = mapper.readTree(rawJson);
if (!root.has("data") || !root.get("data").isObject()) {
return "❌ Attendance details not available in this format.";
}
JsonNode data = root.get("data");
StringBuilder sb = new StringBuilder();
sb.append("📝 *Attendance Pending*\n");
sb.append("━━━━━━━━━━━━━━\n\n");
boolean found = false;
if (data.has("regularSubjects")) {
JsonNode regular = data.get("regularSubjects");
if (regular.isArray() && regular.size() > 0) {
sb.append("*Regular Subjects:*\n");
for (JsonNode node : regular) {
String sName = node.has("subject") ? node.get("subject").asText() : "";
String pName = node.has("allotedPeriod") ? node.get("allotedPeriod").asText() : "";
String cName = node.has("className") ? node.get("className").asText() : "";
sb.append("🔹 Period ").append(pName).append(": *").append(sName).append("*\n");
sb.append(" Class: ").append(cName).append("\n\n");
found = true;
}
}
}
if (!found) {
sb.append("No attendance pending to mark today.\n\n");
}
sb.append("━━━━━━━━━━━━━━");
return sb.toString();
} catch (Exception e) {
System.err.println("Error formatting faculty attendance subjects: " + e.getMessage());
return "⚠️ Error formatting attendance subjects data.";
}
}
public static class StudentAttendanceInfo {
public String id;
public String regNo;
public String name;
public StudentAttendanceInfo(String id, String regNo, String name) {
this.id = id;
this.regNo = regNo;
this.name = name;
}
}
public static java.util.List<StudentAttendanceInfo> parseStudentList(String html) {
java.util.List<StudentAttendanceInfo> list = new java.util.ArrayList<>();
String cleanHtml = html.replaceAll("\\s+", " ");
java.util.regex.Matcher rowMatcher = java.util.regex.Pattern.compile("<tr[^>]*>(.*?)</tr>").matcher(cleanHtml);
int serial = 1;
while (rowMatcher.find()) {
String rowContent = rowMatcher.group(1);
java.util.regex.Matcher idMatcher = java.util.regex.Pattern.compile("<input[^>]+value=\"(\\d+)\"[^>]*name=\"student_id\\[\\]\"").matcher(rowContent);
if (!idMatcher.find()) {
idMatcher = java.util.regex.Pattern.compile("<input[^>]+name=\"student_id\\[\\]\"[^>]+value=\"(\\d+)\"").matcher(rowContent);
}
if (idMatcher.find()) {
String id = idMatcher.group(1);
java.util.List<String> cells = new java.util.ArrayList<>();
java.util.regex.Matcher cellMatcher = java.util.regex.Pattern.compile("<td[^>]*>(.*?)</td>").matcher(rowContent);
while (cellMatcher.find()) {
cells.add(cellMatcher.group(1).replaceAll("<[^>]*>", "").trim());
}
String regNo = cells.size() > 1 ? cells.get(1) : "";
String name = cells.size() > 2 ? cells.get(2) : "";
if (regNo.isEmpty()) {
regNo = "Roll " + serial;
}
if (name.isEmpty()) {
name = "Student " + serial;
}
list.add(new StudentAttendanceInfo(id, regNo, name));
serial++;
}
}
return list;
}
public static java.util.List<StudentAttendanceInfo> parseStudents(String rawJsonOrHtml) {
java.util.List<StudentAttendanceInfo> list = new java.util.ArrayList<>();
if (rawJsonOrHtml == null) return list;
try {
JsonNode root = mapper.readTree(rawJsonOrHtml);
JsonNode dataNode = root.has("data") ? root.get("data") : root;
if (dataNode.isArray()) {
for (JsonNode student : dataNode) {
String id = student.has("id") ? student.get("id").asText() : "";
String name = student.has("name") ? student.get("name").asText() :
(student.has("student_name") ? student.get("student_name").asText() : "");
String regNo = student.has("roll_no") ? student.get("roll_no").asText() :
(student.has("register_number") ? student.get("register_number").asText() :
(student.has("reg_no") ? student.get("reg_no").asText() : ""));
if (!id.isEmpty()) {
list.add(new StudentAttendanceInfo(id, regNo, name));
}
}
return list;
} else if (dataNode.isTextual()) {
return parseStudentList(dataNode.asText());
}
} catch (Exception e) {
return parseStudentList(rawJsonOrHtml);
}
return list;
}
public static String formatFacultyWeeklyTimetable(String rawJson) {
if (rawJson == null) return "❌ Failed to retrieve weekly timetable data.";
try {
JsonNode root = mapper.readTree(rawJson);
if (!root.has("data") || !root.get("data").isObject() || !root.get("data").has("schedule")) {
return "❌ Weekly timetable details not available in the response.";
}
JsonNode schedule = root.get("data").get("schedule");
StringBuilder sb = new StringBuilder();
sb.append("📅 *Weekly Timetable (Faculty)*\n\n");
String[] days = {"monday", "tuesday", "wednesday", "thursday", "friday", "saturday"};
String[] dayLabels = {"MON", "TUE", "WED", "THU", "FRI", "SAT"};
boolean hasAnyClasses = false;
for (int i = 0; i < days.length; i++) {
String day = days[i];
String label = dayLabels[i];
if (schedule.has(day)) {
JsonNode daySchedule = schedule.get(day);
StringBuilder daySb = new StringBuilder();
daySb.append("📅 *").append(label).append("*\n");
boolean dayHasClasses = false;
int maxPeriod = 7;
java.util.Iterator<String> fieldNames = daySchedule.fieldNames();
while (fieldNames.hasNext()) {
try {
int p = Integer.parseInt(fieldNames.next());
if (p > maxPeriod) {
maxPeriod = p;
}
} catch (NumberFormatException e) {}
}
for (int period = 1; period <= maxPeriod; period++) {
String periodKey = String.valueOf(period);
if (daySchedule.has(periodKey)) {
JsonNode details = daySchedule.get(periodKey);
java.util.Set<String> subjects = new java.util.LinkedHashSet<>();
if (details.isArray()) {
for (JsonNode entry : details) {
String name = entry.has("subjectName") ? entry.get("subjectName").asText() : "";
String code = entry.has("subjectCode") ? entry.get("subjectCode").asText() : "";
String cName = entry.has("className") ? entry.get("className").asText() : "";
if (!name.trim().isEmpty()) {
String entryStr = name.trim() + " (" + code.trim() + ")";
if (!cName.isEmpty()) {
entryStr += " - " + cName.trim();
}
subjects.add(entryStr);
}
}
}
if (!subjects.isEmpty()) {
daySb.append(" ").append(getNumberEmoji(period)).append(" ").append(String.join(" / ", subjects)).append("\n");
dayHasClasses = true;
hasAnyClasses = true;
}
}
}
if (dayHasClasses) {
sb.append(daySb).append("\n");
}
}
}
if (!hasAnyClasses) {
return "📅 *Weekly Timetable*\n\nNo classes scheduled for this week.";
}
return sb.toString().trim();
} catch (Exception e) {
return "⚠️ Error formatting weekly timetable data.";
}
}
}