diff --git a/pom.xml b/pom.xml index 5376ff9..61984e2 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,11 @@ spring-boot-starter-webmvc-test test + + com.google.firebase + firebase-admin + 9.3.0 + diff --git a/src/main/java/com/rit/whatsapp/config/FirebaseConfig.java b/src/main/java/com/rit/whatsapp/config/FirebaseConfig.java new file mode 100644 index 0000000..6c884ac --- /dev/null +++ b/src/main/java/com/rit/whatsapp/config/FirebaseConfig.java @@ -0,0 +1,68 @@ +package com.rit.whatsapp.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.firestore.Firestore; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.cloud.FirestoreClient; +import com.google.firebase.cloud.StorageClient; +import com.google.cloud.storage.Bucket; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +import java.io.InputStream; + +@Configuration +public class FirebaseConfig { + + @Value("${firebase.credentials.path:classpath:firebase-service-account.json}") + private String credentialsPath; + + @Value("${firebase.storage.bucket:}") + private String storageBucket; + + private final ResourceLoader resourceLoader; + + public FirebaseConfig(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + private synchronized void initFirebase() { + if (FirebaseApp.getApps().isEmpty()) { + try { + Resource resource = resourceLoader.getResource(credentialsPath); + try (InputStream serviceAccount = resource.getInputStream()) { + FirebaseOptions.Builder optionsBuilder = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)); + + if (storageBucket != null && !storageBucket.isEmpty()) { + optionsBuilder.setStorageBucket(storageBucket); + } + + FirebaseApp.initializeApp(optionsBuilder.build()); + System.out.println("Firebase initialized successfully."); + } + } catch (Exception e) { + throw new RuntimeException("Failed to initialize Firebase", e); + } + } + } + + @Bean + public Firestore firestore() { + initFirebase(); + return FirestoreClient.getFirestore(); + } + + @Bean + public Bucket storageBucket() { + initFirebase(); + if (storageBucket != null && !storageBucket.isEmpty()) { + return StorageClient.getInstance().bucket(); + } + return null; + } +} diff --git a/src/main/java/com/rit/whatsapp/controller/WebhookController.java b/src/main/java/com/rit/whatsapp/controller/WebhookController.java index 46a34e3..f9ddf80 100644 --- a/src/main/java/com/rit/whatsapp/controller/WebhookController.java +++ b/src/main/java/com/rit/whatsapp/controller/WebhookController.java @@ -101,7 +101,6 @@ public class WebhookController { 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"); @@ -111,6 +110,15 @@ public class WebhookController { 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 { + studentMenuHandler.sendMainMenu(fromPhoneNumber); + } } else if ("interactive".equals(messageType)) { JsonNode interactiveNode = messageNode.get("interactive"); String interactiveType = interactiveNode.get("type").asText(); @@ -142,7 +150,6 @@ public class WebhookController { loginHandler.handlePasscodeInput(phoneNumber, messageBody); break; default: - // Trigger the start of the login flow loginHandler.startLoginFlow(phoneNumber); break; } @@ -150,26 +157,40 @@ public class WebhookController { 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.handleGrievanceDescription(phoneNumber, messageBody); + 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 MAIN_MENU: default: - // Fallback to main menu studentMenuHandler.sendMainMenu(phoneNumber); break; } } private void handleInteractiveListSelection(String phoneNumber, String selectedListId, UserState currentState) { - if (selectedListId.startsWith("student_")) { - // They clicked an option from the main menu + 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("sem_")) { - // They clicked an option from the semester selection list studentMenuHandler.handleSemesterSelection(phoneNumber, selectedListId); } else if (selectedListId.startsWith("receipt_") || selectedListId.startsWith("exam_") || selectedListId.startsWith("acad_") || selectedListId.startsWith("cons_")) { - // They clicked a receipt download option studentMenuHandler.handleReceiptSelection(phoneNumber, selectedListId); } else { studentMenuHandler.sendMainMenu(phoneNumber); @@ -223,8 +244,8 @@ public class WebhookController { return; } - if (currentState == UserState.AWAITING_GRIEVANCE_CATEGORY) { - grievanceHandler.handleCategorySelection(phoneNumber, selectedButtonId); + if ("grv_skip_image".equals(selectedButtonId)) { + grievanceHandler.handleSkipImage(phoneNumber); } else { studentMenuHandler.sendMainMenu(phoneNumber); } diff --git a/src/main/java/com/rit/whatsapp/handler/GrievanceHandler.java b/src/main/java/com/rit/whatsapp/handler/GrievanceHandler.java index b5035ce..66c77e4 100644 --- a/src/main/java/com/rit/whatsapp/handler/GrievanceHandler.java +++ b/src/main/java/com/rit/whatsapp/handler/GrievanceHandler.java @@ -1,19 +1,16 @@ package com.rit.whatsapp.handler; +import com.rit.whatsapp.model.GrievanceDraft; import com.rit.whatsapp.model.UserState; +import com.rit.whatsapp.service.FirebaseService; 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.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.springframework.context.annotation.Lazy; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; @Component public class GrievanceHandler { @@ -21,54 +18,270 @@ public class GrievanceHandler { private final WhatsAppMessageService messageService; private final SessionService sessionService; private final StudentMenuHandler studentMenuHandler; + private final FirebaseService firebaseService; @Autowired - public GrievanceHandler(WhatsAppMessageService messageService, SessionService sessionService, @Lazy StudentMenuHandler studentMenuHandler) { + public GrievanceHandler(WhatsAppMessageService messageService, + SessionService sessionService, + @Lazy StudentMenuHandler studentMenuHandler, + FirebaseService firebaseService) { this.messageService = messageService; this.sessionService = sessionService; this.studentMenuHandler = studentMenuHandler; + this.firebaseService = firebaseService; } public void startGrievanceFlow(String phoneNumber) { - sessionService.resetState(phoneNumber); + sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_ACTION); + + List> rows = new ArrayList<>(); + rows.add(createRow("grv_action_new", "Raise New Grievance", "Submit a new issue")); + rows.add(createRow("grv_action_status", "Check Status", "View status of existing grievances")); + rows.add(createRow("grv_action_delete", "Delete Grievance", "Cancel and remove an existing grievance")); + + Map listMsg = WhatsAppMessageBuilder.buildListMessage( + "Grievance Portal", + "Please select an option:", + "Select Action", + rows + ); + messageService.sendInteractiveMessage(phoneNumber, listMsg); + } - String message = "📢 *RIT Grievance Portal Notice*\n\n" + - "Please note that grievances and complaints must be officially registered and tracked using our dedicated mobile application, *Complix-R*.\n\n" + - "Kindly download the app from the Google Play Store to submit your ticket:\n" + - "🔗 https://play.google.com/store/apps/details?id=com.ritchennai.complixr&pcampaignid=web_share"; + public void handleActionSelection(String phoneNumber, String actionId) { + if ("grv_action_new".equals(actionId)) { + sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_TITLE); + GrievanceDraft draft = sessionService.getOrCreateGrievanceDraft(phoneNumber); + + String token = sessionService.getImsToken(phoneNumber); + if (token != null) { + draft.setStudentName(sessionService.getStudentName(phoneNumber, token)); + draft.setImsId(sessionService.getImsId(phoneNumber)); + } - messageService.sendMessage(phoneNumber, message); + messageService.sendMessage(phoneNumber, "📝 Please type a short summary/title of the issue you are facing:"); + } else if ("grv_action_status".equals(actionId)) { + String imsId = sessionService.getImsId(phoneNumber); + List> grievances = firebaseService.getUserGrievances(imsId); + + if (grievances.isEmpty()) { + messageService.sendMessage(phoneNumber, "You have no existing grievances."); + studentMenuHandler.sendStudentMenuDirectly(phoneNumber); + return; + } + + sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_STATUS_SELECTION); + + Collections.sort(grievances, (a, b) -> { + com.google.cloud.Timestamp t1 = (com.google.cloud.Timestamp) a.get("createdAt"); + com.google.cloud.Timestamp t2 = (com.google.cloud.Timestamp) b.get("createdAt"); + if (t1 == null || t2 == null) return 0; + return t2.compareTo(t1); + }); + + List> rows = new ArrayList<>(); + int count = 0; + for (Map g : grievances) { + if (count >= 10) break; + String id = (String) g.get("id"); + String title = (String) g.get("title"); + String status = (String) g.get("status"); + String displayTitle = title != null && title.length() > 24 ? title.substring(0, 21) + "..." : title; + rows.add(createRow("grv_status_" + id, displayTitle, "Status: " + status)); + count++; + } + + Map listMsg = WhatsAppMessageBuilder.buildListMessage( + "Your Grievances", + "Select a grievance to see details:", + "Select Grievance", + rows + ); + messageService.sendInteractiveMessage(phoneNumber, listMsg); + } else if ("grv_action_delete".equals(actionId)) { + String imsId = sessionService.getImsId(phoneNumber); + List> grievances = firebaseService.getUserGrievances(imsId); + + if (grievances.isEmpty()) { + messageService.sendMessage(phoneNumber, "You have no grievances to delete."); + studentMenuHandler.sendStudentMenuDirectly(phoneNumber); + return; + } + + sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_DELETE_SELECTION); + + Collections.sort(grievances, (a, b) -> { + com.google.cloud.Timestamp t1 = (com.google.cloud.Timestamp) a.get("createdAt"); + com.google.cloud.Timestamp t2 = (com.google.cloud.Timestamp) b.get("createdAt"); + if (t1 == null || t2 == null) return 0; + return t2.compareTo(t1); + }); + + List> rows = new ArrayList<>(); + int count = 0; + for (Map g : grievances) { + if (count >= 10) break; + String id = (String) g.get("id"); + String title = (String) g.get("title"); + String displayTitle = title != null && title.length() > 24 ? title.substring(0, 21) + "..." : title; + rows.add(createRow("grv_delete_" + id, displayTitle, "Select to delete")); + count++; + } + + Map listMsg = WhatsAppMessageBuilder.buildListMessage( + "Delete Grievance", + "Select the grievance you want to delete:", + "Select Grievance", + rows + ); + messageService.sendInteractiveMessage(phoneNumber, listMsg); + } + } + + public void handleStatusSelection(String phoneNumber, String selectionId) { + String id = selectionId.replace("grv_status_", ""); + String imsId = sessionService.getImsId(phoneNumber); + List> grievances = firebaseService.getUserGrievances(imsId); + + Map selected = null; + for (Map g : grievances) { + if (id.equals(g.get("id"))) { + selected = g; + break; + } + } + + if (selected != null) { + String msg = "🎫 *Ticket Details*\n\n" + + "📁 *Title:* " + selected.get("title") + "\n" + + "🏢 *Department:* " + selected.get("department") + "\n" + + "📝 *Description:* " + selected.get("description") + "\n" + + "📊 *Status:* " + selected.get("status"); + messageService.sendMessage(phoneNumber, msg); + } else { + messageService.sendMessage(phoneNumber, "Grievance not found."); + } + studentMenuHandler.sendStudentMenuDirectly(phoneNumber); } - public void handleCategorySelection(String phoneNumber, String selectedCategory) { + public void handleDeleteSelection(String phoneNumber, String selectionId) { + String id = selectionId.replace("grv_delete_", ""); + firebaseService.deleteGrievance(id); + messageService.sendMessage(phoneNumber, "✅ Grievance ticket *" + id + "* has been successfully deleted along with any uploaded attachments."); + studentMenuHandler.sendStudentMenuDirectly(phoneNumber); + } + + public void handleTitleInput(String phoneNumber, String title) { + GrievanceDraft draft = sessionService.getOrCreateGrievanceDraft(phoneNumber); + draft.setTitle(title); + + sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_DEPT); + + List deps = firebaseService.getDepartments("RIT"); + List> deptRows = new ArrayList<>(); + + if (deps.isEmpty()) { + deptRows.add(createRow("grv_dept_CSE", "CSE", "Computer Science")); + deptRows.add(createRow("grv_dept_Admin", "Admin", "Admin Office")); + } else { + for (String dep : deps) { + if (deptRows.size() >= 10) break; + String displayTitle = dep != null && dep.length() > 24 ? dep.substring(0, 21) + "..." : dep; + deptRows.add(createRow("grv_dept_" + dep, displayTitle, "Department")); + } + } + + Map listMsg = WhatsAppMessageBuilder.buildListMessage( + "Department Selection", + "Issue: *" + title + "*\n\nPlease select the concerned department:", + "Select Dept", + deptRows + ); + messageService.sendInteractiveMessage(phoneNumber, listMsg); + } + + public void handleDepartmentSelection(String phoneNumber, String deptId) { + String deptName = deptId.replace("grv_dept_", ""); + GrievanceDraft draft = sessionService.getOrCreateGrievanceDraft(phoneNumber); + draft.setDepartment(deptName); + sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_DESC); - messageService.sendMessage(phoneNumber, "You selected " + selectedCategory + ". Please type a brief description of your issue:"); + messageService.sendMessage(phoneNumber, + "Issue: *" + draft.getTitle() + "*\n" + + "Department: *" + deptName + "*\n\n" + + "📝 Please type a detailed description of your grievance:"); } - 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."; + public void handleDescriptionInput(String phoneNumber, String description) { + GrievanceDraft draft = sessionService.getOrCreateGrievanceDraft(phoneNumber); + draft.setDescription(description); - messageService.sendMessage(phoneNumber, confirmationMessage); - - // Reset state back to main menu - sessionService.resetState(phoneNumber); - - // Display student menu + sessionService.setUserState(phoneNumber, UserState.AWAITING_GRIEVANCE_IMAGE); + + List> buttons = new ArrayList<>(); + Map skipBtn = new HashMap<>(); + skipBtn.put("id", "grv_skip_image"); + skipBtn.put("title", "Skip Image"); + buttons.add(skipBtn); + + Map buttonMsg = WhatsAppMessageBuilder.buildButtonMessage( + "📸 (Optional) Please send a photo attachment as supporting evidence for your grievance.\n\n" + + "Or click the button below to submit without an image.", + buttons + ); + messageService.sendInteractiveMessage(phoneNumber, buttonMsg); + } + + public void handleImageInput(String phoneNumber, String mediaId, String mimeType) { + GrievanceDraft draft = sessionService.getOrCreateGrievanceDraft(phoneNumber); + String ticketId = generateTicketId(); + draft.setTicketId(ticketId); + + messageService.sendMessage(phoneNumber, "Uploading image attachment... ⏳"); + + byte[] mediaBytes = messageService.downloadMedia(mediaId); + if (mediaBytes != null) { + String imageUrl = firebaseService.uploadImage(mediaBytes, mimeType, ticketId); + draft.setImageUrl(imageUrl); + } + + finalizeAndSaveGrievance(phoneNumber, draft); + } + + public void handleSkipImage(String phoneNumber) { + GrievanceDraft draft = sessionService.getOrCreateGrievanceDraft(phoneNumber); + draft.setTicketId(generateTicketId()); + finalizeAndSaveGrievance(phoneNumber, draft); + } + + private void finalizeAndSaveGrievance(String phoneNumber, GrievanceDraft draft) { + firebaseService.saveGrievance(draft); + + String confirmationMsg = "✅ *Grievance Submitted Successfully!*\n\n" + + "🎫 *Ticket ID:* `" + draft.getTicketId() + "`\n" + + "📁 *Title:* " + draft.getTitle() + "\n" + + "🏢 *Department:* " + draft.getDepartment() + "\n" + + "📝 *Description:* " + draft.getDescription() + "\n" + + (draft.getImageUrl() != null ? "🖼️ *Attachment:* Uploaded\n" : "") + + "📊 *Status:* pending\n\n" + + "Our administration team will review your ticket shortly."; + + messageService.sendMessage(phoneNumber, confirmationMsg); + + sessionService.clearGrievanceDraft(phoneNumber); studentMenuHandler.sendStudentMenuDirectly(phoneNumber); } - private Map createButton(String id, String title) { - Map btn = new HashMap<>(); - btn.put("id", id); - btn.put("title", title); - return btn; + private String generateTicketId() { + return "TKT-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + } + + private Map createRow(String id, String title, String description) { + Map row = new HashMap<>(); + row.put("id", id); + row.put("title", title); + row.put("description", description); + return row; } } diff --git a/src/main/java/com/rit/whatsapp/handler/sedxyTiNX b/src/main/java/com/rit/whatsapp/handler/sedxyTiNX new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/rit/whatsapp/model/GrievanceDraft.java b/src/main/java/com/rit/whatsapp/model/GrievanceDraft.java new file mode 100644 index 0000000..6d8a63d --- /dev/null +++ b/src/main/java/com/rit/whatsapp/model/GrievanceDraft.java @@ -0,0 +1,37 @@ +package com.rit.whatsapp.model; + +import java.util.Date; + +public class GrievanceDraft { + private String ticketId; + private String phoneNumber; + private String studentName; + private String imsId; + private String title; + private String department; + private String description; + private String imageUrl; + private String status = "pending"; + private Date createdAt = new Date(); + + public String getTicketId() { return ticketId; } + public void setTicketId(String ticketId) { this.ticketId = ticketId; } + public String getPhoneNumber() { return phoneNumber; } + public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } + public String getStudentName() { return studentName; } + public void setStudentName(String studentName) { this.studentName = studentName; } + public String getImsId() { return imsId; } + public void setImsId(String imsId) { this.imsId = imsId; } + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + public String getDepartment() { return department; } + public void setDepartment(String department) { this.department = department; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + public String getImageUrl() { return imageUrl; } + public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public Date getCreatedAt() { return createdAt; } + public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; } +} diff --git a/src/main/java/com/rit/whatsapp/model/UserState.java b/src/main/java/com/rit/whatsapp/model/UserState.java index abaaede..8fba579 100644 --- a/src/main/java/com/rit/whatsapp/model/UserState.java +++ b/src/main/java/com/rit/whatsapp/model/UserState.java @@ -6,6 +6,11 @@ public enum UserState { AWAITING_LOGIN_PASSCODE, AWAITING_SEMESTER_SELECTION, AWAITING_RECEIPT_SELECTION, - AWAITING_GRIEVANCE_CATEGORY, - AWAITING_GRIEVANCE_DESC + AWAITING_GRIEVANCE_ACTION, + AWAITING_GRIEVANCE_TITLE, + AWAITING_GRIEVANCE_DEPT, + AWAITING_GRIEVANCE_DESC, + AWAITING_GRIEVANCE_IMAGE, + AWAITING_GRIEVANCE_STATUS_SELECTION, + AWAITING_GRIEVANCE_DELETE_SELECTION } diff --git a/src/main/java/com/rit/whatsapp/service/FirebaseService.java b/src/main/java/com/rit/whatsapp/service/FirebaseService.java new file mode 100644 index 0000000..9eda85d --- /dev/null +++ b/src/main/java/com/rit/whatsapp/service/FirebaseService.java @@ -0,0 +1,129 @@ +package com.rit.whatsapp.service; + +import com.google.cloud.firestore.Firestore; +import com.google.cloud.firestore.QueryDocumentSnapshot; +import com.google.cloud.firestore.QuerySnapshot; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.rit.whatsapp.model.GrievanceDraft; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import com.google.cloud.Timestamp; + +@Service +public class FirebaseService { + + private final Firestore firestore; + private final Bucket bucket; + + @Autowired + public FirebaseService(@Autowired(required = false) Firestore firestore, + @Autowired(required = false) Bucket bucket) { + this.firestore = firestore; + this.bucket = bucket; + } + + public List getDepartments(String org) { + List deps = new ArrayList<>(); + if (firestore == null) return deps; + try { + QuerySnapshot snapshot = firestore.collection("departments") + .whereEqualTo("org", org) + .get().get(); + for (QueryDocumentSnapshot doc : snapshot.getDocuments()) { + Boolean isDeleted = doc.getBoolean("isDeleted"); + if (isDeleted == null || !isDeleted) { + deps.add(doc.getString("name")); + } + } + } catch (Exception e) { + System.err.println("Error fetching departments: " + e.getMessage()); + } + return deps; + } + + public List> getUserGrievances(String imsId) { + List> list = new ArrayList<>(); + if (firestore == null) return list; + try { + QuerySnapshot snapshot = firestore.collection("grievances") + .whereEqualTo("studentRegNo", imsId) + .get().get(); + for (QueryDocumentSnapshot doc : snapshot.getDocuments()) { + Map data = new HashMap<>(doc.getData()); + data.put("id", doc.getId()); + list.add(data); + } + } catch (Exception e) { + System.err.println("Error fetching grievances: " + e.getMessage()); + } + return list; + } + + public String uploadImage(byte[] imageBytes, String mimeType, String ticketId) { + if (bucket == null || imageBytes == null) return null; + try { + String extension = mimeType != null && mimeType.contains("png") ? ".png" : ".jpg"; + String blobName = "grievances/" + ticketId + "/" + UUID.randomUUID() + extension; + Blob blob = bucket.create(blobName, imageBytes, mimeType != null ? mimeType : "image/jpeg"); + return blob.signUrl(7, TimeUnit.DAYS).toString(); + } catch (Exception e) { + System.err.println("Failed to upload image to Firebase Storage: " + e.getMessage()); + return null; + } + } + + public void saveGrievance(GrievanceDraft draft) { + if (firestore == null) { + System.out.println("Firestore not configured."); + return; + } + try { + Map data = new HashMap<>(); + data.put("userId", "whatsapp_" + draft.getPhoneNumber()); + data.put("title", draft.getTitle()); + data.put("description", draft.getDescription()); + data.put("department", draft.getDepartment()); + data.put("org", "RIT"); + data.put("studentName", draft.getStudentName()); + data.put("studentRegNo", draft.getImsId()); + data.put("status", draft.getStatus()); + data.put("createdAt", Timestamp.now()); + if (draft.getImageUrl() != null) { + data.put("imageUrl", draft.getImageUrl()); + } + firestore.collection("grievances").document(draft.getTicketId()).set(data).get(); + } catch (Exception e) { + System.err.println("Failed to save grievance: " + e.getMessage()); + } + } + + public void deleteGrievance(String ticketId) { + if (firestore != null) { + try { + firestore.collection("grievances").document(ticketId).delete().get(); + System.out.println("Deleted Firestore document: " + ticketId); + } catch (Exception e) { + System.err.println("Failed to delete Firestore document: " + e.getMessage()); + } + } + if (bucket != null) { + try { + Iterable blobs = bucket.list(com.google.cloud.storage.Storage.BlobListOption.prefix("grievances/" + ticketId + "/")).iterateAll(); + for (Blob blob : blobs) { + blob.delete(); + System.out.println("Deleted Storage blob: " + blob.getName()); + } + } catch (Exception e) { + System.err.println("Failed to delete Storage blobs: " + e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/rit/whatsapp/service/SessionService.java b/src/main/java/com/rit/whatsapp/service/SessionService.java index 4044d0a..b297004 100644 --- a/src/main/java/com/rit/whatsapp/service/SessionService.java +++ b/src/main/java/com/rit/whatsapp/service/SessionService.java @@ -2,6 +2,7 @@ package com.rit.whatsapp.service; import com.rit.whatsapp.model.UserEntity; import com.rit.whatsapp.model.UserState; +import com.rit.whatsapp.model.GrievanceDraft; import com.rit.whatsapp.repository.UserRepository; import com.rit.whatsapp.util.EncryptionUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -137,4 +138,30 @@ public class SessionService { public void clearTempLoginId(String phoneNumber) { temporaryLoginIds.remove(phoneNumber); } + + private final Map grievanceDrafts = new ConcurrentHashMap<>(); + + public GrievanceDraft getOrCreateGrievanceDraft(String phoneNumber) { + return grievanceDrafts.computeIfAbsent(phoneNumber, k -> { + GrievanceDraft draft = new GrievanceDraft(); + draft.setPhoneNumber(phoneNumber); + return draft; + }); + } + + public GrievanceDraft getGrievanceDraft(String phoneNumber) { + return grievanceDrafts.get(phoneNumber); + } + + public void clearGrievanceDraft(String phoneNumber) { + grievanceDrafts.remove(phoneNumber); + } + + public String getImsId(String phoneNumber) { + Optional userOpt = userRepository.findById(phoneNumber); + if (userOpt.isPresent()) { + return userOpt.get().getImsId(); + } + return null; + } } diff --git a/src/main/java/com/rit/whatsapp/service/WhatsAppMessageService.java b/src/main/java/com/rit/whatsapp/service/WhatsAppMessageService.java index 3116017..39e9a25 100644 --- a/src/main/java/com/rit/whatsapp/service/WhatsAppMessageService.java +++ b/src/main/java/com/rit/whatsapp/service/WhatsAppMessageService.java @@ -115,4 +115,30 @@ public class WhatsAppMessageService { executePost(payload); } + + public byte[] downloadMedia(String mediaId) { + try { + String cleanToken = apiToken != null ? apiToken.trim() : ""; + String metadataUrl = "https://graph.facebook.com/v19.0/" + mediaId; + + org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders(); + headers.setBearerAuth(cleanToken); + org.springframework.http.HttpEntity request = new org.springframework.http.HttpEntity<>(headers); + + org.springframework.http.ResponseEntity response = restTemplate.exchange(metadataUrl, org.springframework.http.HttpMethod.GET, request, String.class); + com.fasterxml.jackson.databind.JsonNode json = new com.fasterxml.jackson.databind.ObjectMapper().readTree(response.getBody()); + String directUrl = json.get("url").asText(); + + org.springframework.http.HttpHeaders downloadHeaders = new org.springframework.http.HttpHeaders(); + downloadHeaders.setBearerAuth(cleanToken); + downloadHeaders.set("User-Agent", "curl/7.64.1"); + org.springframework.http.HttpEntity downloadRequest = new org.springframework.http.HttpEntity<>(downloadHeaders); + + org.springframework.http.ResponseEntity mediaResponse = restTemplate.exchange(directUrl, org.springframework.http.HttpMethod.GET, downloadRequest, byte[].class); + return mediaResponse.getBody(); + } catch (Exception e) { + System.err.println("Failed to download media with ID " + mediaId + ": " + e.getMessage()); + return null; + } + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 94f79bc..4bb00b7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,4 +18,4 @@ spring.jpa.hibernate.ddl-auto=update # Encryption Secret for Passcodes (Must be 16, 24, or 32 bytes) bot.encryption.secret=12345678901234567890123456789012 -whatsapp.api-token=EAAc3Wjql00oBSfkmo5fYUjw1Wu8vzvG4zbeMgt40cZBKqNJNo56or7IlwV1HYV3Ue6ZB2nZAZALbE4TTtxxZB7r4WhdAcJKzgW7FVSRd4qxAm6EZAIHzZBirogyC4zXyodmsf6hXmaa9oeAiIfOeoiHkTZBZBhv43jFZAu1yJGr9CteScbRRjS1sdt1F0twZCkMYXZCyZCZBzQzptnGoAiEsJhDUIe5DtTWD8k7IkWDvmV7MvsT6DhrjRIcYPDvwIek3EWVZBGAFFsZCFtoyUA6ICgiJ2WEWzE0c +whatsapp.api-token=EAAc3Wjql00oBSdZAyOjO81it8Tnpiqa1mmJq9c3WFzwXmYvjZC1RaXLlJDBe14zrCQ7DA2l5pzAV5y8sinZCBmTzlp9SUXvrcMMzmMDJ7rN58KHBqP6MAIkxcyQZCpewbFqB3mp4xwUHJ89GRZB88rsLpvJFMZABWNbfsaTZAQ0ZCabRcAvL0iJ528UGJJnv98y9F9EMerZCi3KZCGYfvIZBuYBM8tL2XWmesQir8Dlj2ZCjBmtuDLGOEQ59lHZBq1XrEbhbzlWOzH6EdUZBOajYDHn8rcZBdGZC diff --git a/src/main/resources/firebase-service-account.json b/src/main/resources/firebase-service-account.json new file mode 100644 index 0000000..371b523 --- /dev/null +++ b/src/main/resources/firebase-service-account.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "complix-r", + "private_key_id": "6369ede06c0556edcf5999de761bf8212668b4bd", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDBRshj1wH8B2hF\nGM64fBXuZXdMOgyNjmHj6mO0pSqsbauSg3nXbgDhfl5pds/reieuEV67fqE7v5Mf\nrjFLVAsw05rwJ9EcGBgfiEhguLR+Plu1WyV4HXORW2AldByO1xyYYxBFW2Ntyg0T\njZLd15snGg+9rx+jsXNq5cQqBCNYtDqe0Pphyw7e42L6s0BmeKEVr/Iw+WRJSOYh\n6tM5fjuLTmr+3QsTT+FWo6i5mLvtMk1hsULvtVlrfbIRCJnK/bY8xqTVwKl43P+h\nQws2TvyBbU/x20BzeQly4DMvUvoCT4coNitzsnx8ackHlaiHXYYNX0kH4oZ7wYPy\nwtGX4uuLAgMBAAECggEAFpd7//OXZ0rEXONH8peKhrm0hqP4aKl+dsIkkm6F/O66\n0d7tZImWr8TyKzi4mAlIzxd3r8ne9dlwTJEzcN8ribMKVs0Iq9SDKVhoQWoFqwYF\n+T5ktFQY+KIqLGaUmfu5+3QE6F6E0kX+Xk07oBO+LoVk1qK8O4mX+KRP2CtwqEMA\nvxZbDGm/wVGI9Yy93fcYLFjG5jubdBw00W4ZbrBpqSRqKCV9vtR9xP1S0Qh6HR/U\n36Ud0DblIOxw/MoiqlhzUYAR71aSoasTN7yTExCrXSw0CwEywoCq8YVqsnB5jfpd\ngBK0XW4OVjHmPaK6ma5uJkzZmP9Iyd8AUagUe9fuzQKBgQDu00/xwMhcNGJ1pSqW\nkPh9XHgH51IC7LSlIR3mUGSVfUDzS8cmzpAdnMHIttj3+V2ltpGdSwrtn79c4xVI\nqsmX+qFFiS/agzXunPcaHbGWdk0LemVIHPusKlOrUtfrpdnjBUB2TIxGIQH/sYsC\nXh3DSxij86xNk2/IUmoyNnsWlQKBgQDPLO5scKlD5jINB0dIlIUqEy6MxD+LNez3\nAr3TM8uFnua1jfg8lQL7XjaQLfLww0rnv6SJLTSYA6IQXR254C/rMnFzb8V+Q2jp\nlvFNIArP5p1LXEzsd7n8nW+RChL1aoO6RzYwqXuahQldk/ds/FE6OKBgRqkWdwFW\n0KwYtTARnwKBgF+8J1zSmb6UZS+hsOvAW0vtaFRKvw5bEqLrHpA3g8qE9idRJhkB\nTrYAI5Iw8dKdjJOkFsUF0vCkS9KRWPPTOpKIrmMwdhVS6aioFUnLw2d8LrA+NSFu\nNNiz8uxHfjSBPFGLxmZlkwl9q85yvjWJwsaJcOwv/s7uHi4gE1/jhP3tAoGAEl6m\niWD38ThRCcyyamIY6KEoyHyCMeLHLyPEFx0Jl8B+Lmz3LM/9orI43lHkr6pxUtkw\nSqWCe1QfI5pOD3Qsa4teeeDYPxjVDZk/rhoAO2z0V4FIpNL838RJ5y9gfPhD09mK\nmYf9mLbhHLYJA1XgEeyBc+fwCDjnGn6IM+yk5XsCgYBOTHX38h7cIEmFe2IcNOKk\nEq7NSJeYrGgFV5xc4XcGLBucXOBVeRou9TlYqkwCk+B1zVtV2AznICKdltOsOYb6\nsn+Se7LHnG6b2dvRZa3C82BP0XtnFs2yifKxEi+028PoGJ+OMIRGUXYRBHsWLwo9\nOV9DKpNsPx0+4I3coVzQHw==\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-fbsvc@complix-r.iam.gserviceaccount.com", + "client_id": "100217543764222698962", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40complix-r.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +}