initial commit

This commit is contained in:
2026-08-19 11:03:29 +05:30
commit 3914e4f304
23 changed files with 3050 additions and 0 deletions

View 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);
}
}

View 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);
}
}
}

View 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;
}
}

View 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:");
}
}
}

View 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;
}
}

View 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;
}
}

View 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
}

View File

@@ -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> {
}

View 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());
}
}
}

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View 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);
}
}
}

View 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.";
}
}
}