Compare commits

..

2 Commits

Author SHA1 Message Date
Dev102030-pixel
cdc98bb1d0 inactivity time configured 2026-08-17 14:16:42 +05:30
Dev102030-pixel
20805c1338 tamil error fixed 2026-08-17 12:36:16 +05:30
8 changed files with 389 additions and 34 deletions

101
fix_menu.py Normal file
View File

@@ -0,0 +1,101 @@
with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "r") as f:
content = f.read()
old_paginated = """ private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) {
int totalOptions = ids.length;
// Loop and send chunks of up to 3 buttons
for (int i = 0; i < totalOptions; i += 3) {
int end = Math.min(i + 3, totalOptions);
int count = end - i;
String[] chunkIds = new String[count];
String[] chunkTitles = new String[count];
for (int j = 0; j < count; j++) {
chunkIds[j] = ids[i + j];
chunkTitles[j] = titles[i + j];
}
String chunkBodyText = (i == 0) ? bodyText : ( "en".equals(state.getLanguagePreference()) ? "More options:" : "மேலும் விருப்பங்கள்:" );
sendInteractiveButtons(chunkBodyText, chunkIds, chunkTitles);
}
}"""
new_methods = """ private void sendInteractiveList(String bodyText, String buttonText, String sectionTitle, String[] ids, String[] titles) {
String phoneNumberId = currentPhoneId.get();
String recipientPhoneNumber = currentFrom.get();
if (phoneNumberId == null || recipientPhoneNumber == null) return;
String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages";
try {
ObjectNode requestBody = objectMapper.createObjectNode();
requestBody.put("messaging_product", "whatsapp");
requestBody.put("recipient_type", "individual");
requestBody.put("to", recipientPhoneNumber);
requestBody.put("type", "interactive");
ObjectNode interactiveNode = objectMapper.createObjectNode();
interactiveNode.put("type", "list");
ObjectNode bodyNode = objectMapper.createObjectNode();
UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null);
if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) {
String restartText = "en".equals(uState.getLanguagePreference())
? "\\n\\n(Press 0 to restart the conversation)"
: "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)";
bodyText += restartText;
}
bodyNode.put("text", bodyText);
interactiveNode.set("body", bodyNode);
ObjectNode actionNode = objectMapper.createObjectNode();
actionNode.put("button", buttonText);
com.fasterxml.jackson.databind.node.ArrayNode sectionsArray = actionNode.putArray("sections");
ObjectNode section = objectMapper.createObjectNode();
section.put("title", sectionTitle);
com.fasterxml.jackson.databind.node.ArrayNode rowsArray = section.putArray("rows");
int limit = Math.min(ids.length, 10);
for (int i = 0; i < limit; i++) {
ObjectNode row = objectMapper.createObjectNode();
row.put("id", ids[i]);
row.put("title", titles[i]);
rowsArray.add(row);
}
sectionsArray.add(section);
interactiveNode.set("action", actionNode);
requestBody.set("interactive", interactiveNode);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
org.springframework.http.HttpEntity<String> entity = new org.springframework.http.HttpEntity<>(requestBody.toString(), headers);
restTemplate.postForEntity(url, entity, String.class);
} catch (org.springframework.web.client.HttpStatusCodeException e) {
logger.error("Error sending interactive list. Status: {}, Response: {}", e.getStatusCode(), e.getResponseBodyAsString(), e);
} catch (Exception e) {
logger.error("Error sending interactive list", e);
}
}
private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) {
if (ids.length <= 3) {
sendInteractiveButtons(bodyText, ids, titles);
} else {
String buttonText = "en".equals(state.getLanguagePreference()) ? "Select Option" : "விருப்பத்தை தேர்ந்தெடுக்கவும்";
String sectionTitle = "en".equals(state.getLanguagePreference()) ? "Options" : "விருப்பங்கள்";
sendInteractiveList(bodyText, buttonText, sectionTitle, ids, titles);
}
}"""
content = content.replace(old_paginated, new_methods)
with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "w") as f:
f.write(content)
print("Replacement complete.")

91
fix_restart.py Normal file
View File

@@ -0,0 +1,91 @@
with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "r") as f:
content = f.read()
# 1. Modify sendInteractiveButtons to fetch state and append restart text
old_method_start = """ private void sendInteractiveButtons(String bodyText, String[] ids, String[] titles) {
String phoneNumberId = currentPhoneId.get();
String recipientPhoneNumber = currentFrom.get();"""
new_method_start = """ private void sendInteractiveButtons(String bodyText, String[] ids, String[] titles) {
String phoneNumberId = currentPhoneId.get();
String recipientPhoneNumber = currentFrom.get();
UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null);
if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) {
String restartText = "en".equals(uState.getLanguagePreference())
? "\\n\\n(Press 0 to restart the conversation)"
: "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)";
bodyText += restartText;
}"""
content = content.replace(old_method_start, new_method_start)
# 2. Modify sendPaginatedButtons to not append restart text itself
old_paginated = """ private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) {
int totalOptions = ids.length;
String restartText = "en".equals(state.getLanguagePreference())
? "\\n\\n(Send 0 to go back to the main menu / Restart the conversation)"
: "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)";
// Loop and send chunks of up to 3 buttons
for (int i = 0; i < totalOptions; i += 3) {
int end = Math.min(i + 3, totalOptions);
int count = end - i;
String[] chunkIds = new String[count];
String[] chunkTitles = new String[count];
for (int j = 0; j < count; j++) {
chunkIds[j] = ids[i + j];
chunkTitles[j] = titles[i + j];
}
String chunkBodyText = (i == 0) ? bodyText : ( "en".equals(state.getLanguagePreference()) ? "More options:" : "மேலும் விருப்பங்கள்:" );
if (end == totalOptions) {
chunkBodyText += restartText;
}
sendInteractiveButtons(chunkBodyText, chunkIds, chunkTitles);
}
}"""
new_paginated = """ private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) {
int totalOptions = ids.length;
// Loop and send chunks of up to 3 buttons
for (int i = 0; i < totalOptions; i += 3) {
int end = Math.min(i + 3, totalOptions);
int count = end - i;
String[] chunkIds = new String[count];
String[] chunkTitles = new String[count];
for (int j = 0; j < count; j++) {
chunkIds[j] = ids[i + j];
chunkTitles[j] = titles[i + j];
}
String chunkBodyText = (i == 0) ? bodyText : ( "en".equals(state.getLanguagePreference()) ? "More options:" : "மேலும் விருப்பங்கள்:" );
sendInteractiveButtons(chunkBodyText, chunkIds, chunkTitles);
}
}"""
content = content.replace(old_paginated, new_paginated)
# 3. Change processMessage to say "Press 0 to restart the conversation" instead of "(Send 0 to go back to the main menu / Restart the conversation)" for text responses
old_process_text = """ if (response != null && !response.isEmpty() && !response.startsWith("INTERACTIVE:") && !userState.getState().equals("MAIN_MENU") && !userState.getState().equals("LANGUAGE_SELECTION")) {
response += "\\n\\n(Send 0 to go back to the main menu / Restart the conversation)";
}"""
new_process_text = """ if (response != null && !response.isEmpty() && !response.startsWith("INTERACTIVE:") && !userState.getState().equals("MAIN_MENU") && !userState.getState().equals("LANGUAGE_SELECTION")) {
String rt = "en".equals(userState.getLanguagePreference())
? "\\n\\n(Press 0 to restart the conversation)"
: "\\n\\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)";
response += rt;
}"""
content = content.replace(old_process_text, new_process_text)
with open("src/main/java/com/example/whatsappautomation/service/ChatbotService.java", "w") as f:
f.write(content)
print("Replacement complete.")

View File

@@ -2,8 +2,10 @@ package com.example.whatsappautomation;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableScheduling
public class WhatsAppAutomationApplication { public class WhatsAppAutomationApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -11,6 +11,7 @@ public class UserState {
@Id @Id
private String phoneNumber; private String phoneNumber;
private String phoneNumberId;
private String patientId; private String patientId;
private String state; private String state;
private String category; private String category;
@@ -22,6 +23,7 @@ public class UserState {
private String bookingTime; private String bookingTime;
private String languagePreference = "en"; private String languagePreference = "en";
private int menuPage = 0; private int menuPage = 0;
private Long lastActiveTime = System.currentTimeMillis();
public UserState() {} public UserState() {}
@@ -41,6 +43,19 @@ public class UserState {
this.bookingDate = null; this.bookingDate = null;
this.bookingTime = null; this.bookingTime = null;
this.menuPage = 0; this.menuPage = 0;
this.lastActiveTime = System.currentTimeMillis();
}
public void expire() {
this.state = "EXPIRED";
this.category = null;
this.subCategory = null;
this.selectedItem = null;
this.price = null;
this.address = null;
this.bookingDate = null;
this.bookingTime = null;
this.menuPage = 0;
} }
public String getPhoneNumber() { public String getPhoneNumber() {
@@ -141,4 +156,20 @@ public class UserState {
public void setMenuPage(int menuPage) { public void setMenuPage(int menuPage) {
this.menuPage = menuPage; this.menuPage = menuPage;
} }
public Long getLastActiveTime() {
return lastActiveTime;
}
public void setLastActiveTime(Long lastActiveTime) {
this.lastActiveTime = lastActiveTime;
}
public String getPhoneNumberId() {
return phoneNumberId;
}
public void setPhoneNumberId(String phoneNumberId) {
this.phoneNumberId = phoneNumberId;
}
} }

View File

@@ -4,6 +4,9 @@ import com.example.whatsappautomation.entity.UserState;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.List;
@Repository @Repository
public interface UserStateRepository extends JpaRepository<UserState, String> { public interface UserStateRepository extends JpaRepository<UserState, String> {
List<UserState> findByStateNotAndLastActiveTimeBefore(String state, Long cutoffTime);
} }

View File

@@ -35,6 +35,9 @@ public class ChatbotService {
@Value("${whatsapp.access-token}") @Value("${whatsapp.access-token}")
private String accessToken; private String accessToken;
@Value("${conversation.inactivity.timeout:120}")
private long inactivityTimeoutSeconds;
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
@@ -55,18 +58,29 @@ public class ChatbotService {
currentFrom.set(from); currentFrom.set(from);
try { try {
UserState userState = userStateRepository.findById(from).orElse(new UserState(from)); UserState userState = userStateRepository.findById(from).orElse(new UserState(from));
userState.setPhoneNumberId(phoneNumberId);
String lowerInput = messageText.trim().toLowerCase();
// Global Reset: Handling basic greetings // Global Reset: Handling basic greetings
String lowerInput = messageText.trim().toLowerCase();
if (lowerInput.equals("hi") || lowerInput.equals("hello") || lowerInput.equals("hey") || lowerInput.equals("0")) { if (lowerInput.equals("hi") || lowerInput.equals("hello") || lowerInput.equals("hey") || lowerInput.equals("0")) {
userState.reset(); userState.reset();
} else if ("EXPIRED".equals(userState.getState())) {
// If expired and not a greeting, ignore or return without processing
return;
} }
// Refresh activity timer
userState.setLastActiveTime(System.currentTimeMillis());
String response = handleState(userState, messageText.trim()); String response = handleState(userState, messageText.trim());
// If not in MAIN_MENU or LANGUAGE_SELECTION, append restart hint // If not in MAIN_MENU or LANGUAGE_SELECTION, append restart hint
if (response != null && !response.isEmpty() && !response.startsWith("INTERACTIVE:") && !userState.getState().equals("MAIN_MENU") && !userState.getState().equals("LANGUAGE_SELECTION")) { if (response != null && !response.isEmpty() && !response.startsWith("INTERACTIVE:") && !userState.getState().equals("MAIN_MENU") && !userState.getState().equals("LANGUAGE_SELECTION")) {
response += "\n\n(Send 0 to go back to the main menu / Restart the conversation)"; String rt = "en".equals(userState.getLanguagePreference())
? "\n\n(Press 0 to restart the conversation)"
: "\n\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)";
response += rt;
} }
userStateRepository.save(userState); userStateRepository.save(userState);
@@ -1052,6 +1066,14 @@ public class ChatbotService {
private void sendInteractiveButtons(String bodyText, String[] ids, String[] titles) { private void sendInteractiveButtons(String bodyText, String[] ids, String[] titles) {
String phoneNumberId = currentPhoneId.get(); String phoneNumberId = currentPhoneId.get();
String recipientPhoneNumber = currentFrom.get(); String recipientPhoneNumber = currentFrom.get();
UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null);
if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) {
String restartText = "en".equals(uState.getLanguagePreference())
? "\n\n(Press 0 to restart the conversation)"
: "\n\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)";
bodyText += restartText;
}
if (phoneNumberId == null || recipientPhoneNumber == null) return; if (phoneNumberId == null || recipientPhoneNumber == null) return;
String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages"; String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages";
try { try {
@@ -1097,41 +1119,94 @@ public class ChatbotService {
} }
} }
private void sendInteractiveList(String bodyText, String buttonText, String sectionTitle, String[] ids, String[] titles) {
String phoneNumberId = currentPhoneId.get();
String recipientPhoneNumber = currentFrom.get();
if (phoneNumberId == null || recipientPhoneNumber == null) return;
String url = "https://graph.facebook.com/v20.0/" + phoneNumberId + "/messages";
try {
ObjectNode requestBody = objectMapper.createObjectNode();
requestBody.put("messaging_product", "whatsapp");
requestBody.put("recipient_type", "individual");
requestBody.put("to", recipientPhoneNumber);
requestBody.put("type", "interactive");
ObjectNode interactiveNode = objectMapper.createObjectNode();
interactiveNode.put("type", "list");
ObjectNode bodyNode = objectMapper.createObjectNode();
UserState uState = userStateRepository.findById(recipientPhoneNumber).orElse(null);
if (uState != null && !"MAIN_MENU".equals(uState.getState()) && !"LANGUAGE_SELECTION".equals(uState.getState())) {
String restartText = "en".equals(uState.getLanguagePreference())
? "\n\n(Press 0 to restart the conversation)"
: "\n\n(உரையாடலை மீண்டும் தொடங்க 0 ஐ அழுத்தவும்)";
bodyText += restartText;
}
bodyNode.put("text", bodyText);
interactiveNode.set("body", bodyNode);
ObjectNode actionNode = objectMapper.createObjectNode();
actionNode.put("button", buttonText);
com.fasterxml.jackson.databind.node.ArrayNode sectionsArray = actionNode.putArray("sections");
ObjectNode section = objectMapper.createObjectNode();
section.put("title", sectionTitle);
com.fasterxml.jackson.databind.node.ArrayNode rowsArray = section.putArray("rows");
int limit = Math.min(ids.length, 10);
for (int i = 0; i < limit; i++) {
ObjectNode row = objectMapper.createObjectNode();
row.put("id", ids[i]);
row.put("title", titles[i]);
rowsArray.add(row);
}
sectionsArray.add(section);
interactiveNode.set("action", actionNode);
requestBody.set("interactive", interactiveNode);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON);
headers.setBearerAuth(accessToken);
org.springframework.http.HttpEntity<String> entity = new org.springframework.http.HttpEntity<>(requestBody.toString(), headers);
restTemplate.postForEntity(url, entity, String.class);
} catch (org.springframework.web.client.HttpStatusCodeException e) {
logger.error("Error sending interactive list. Status: {}, Response: {}", e.getStatusCode(), e.getResponseBodyAsString(), e);
} catch (Exception e) {
logger.error("Error sending interactive list", e);
}
}
private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) { private void sendPaginatedButtons(UserState state, String bodyText, String[] ids, String[] titles) {
int totalOptions = ids.length; if (ids.length <= 3) {
if (totalOptions <= 3) {
sendInteractiveButtons(bodyText, ids, titles); sendInteractiveButtons(bodyText, ids, titles);
return; } else {
} String buttonText = "en".equals(state.getLanguagePreference()) ? "Select Option" : "தேர்ந்தெடு";
String sectionTitle = "en".equals(state.getLanguagePreference()) ? "Options" : "விருப்பங்கள்";
int page = state.getMenuPage(); sendInteractiveList(bodyText, buttonText, sectionTitle, ids, titles);
int maxItemsPerPage = 2; }
int startIndex = page * maxItemsPerPage; }
if (startIndex >= totalOptions) { @org.springframework.scheduling.annotation.Scheduled(fixedDelay = 30000) // Runs every 30 seconds
startIndex = 0; public void pollInactiveSessions() {
state.setMenuPage(0); long cutoffTime = System.currentTimeMillis() - (inactivityTimeoutSeconds * 1000);
} List<UserState> inactiveUsers = userStateRepository.findByStateNotAndLastActiveTimeBefore("EXPIRED", cutoffTime);
int endIndex = Math.min(startIndex + maxItemsPerPage, totalOptions); for (UserState user : inactiveUsers) {
boolean hasMore = (endIndex < totalOptions) || (totalOptions > 3); user.expire();
userStateRepository.save(user);
int buttonCount = endIndex - startIndex;
if (hasMore) buttonCount++; // Send fallback message
String fallbackText = "en".equals(user.getLanguagePreference())
String[] pageIds = new String[buttonCount]; ? "It looks like your previous conversation has ended.\n👋 To start a new conversation, please type Hi or Hello."
String[] pageTitles = new String[buttonCount]; : "உங்கள் முந்தைய உரையாடல் முடிவடைந்துவிட்டது.\n👋 புதிய உரையாடலைத் தொடங்க Hi அல்லது Hello என டைப் செய்யவும்.";
for (int i = 0; i < endIndex - startIndex; i++) { if (user.getPhoneNumberId() != null) {
pageIds[i] = ids[startIndex + i]; sendMessage(user.getPhoneNumberId(), user.getPhoneNumber(), fallbackText);
pageTitles[i] = titles[startIndex + i]; }
} }
if (hasMore) {
pageIds[buttonCount - 1] = "NEXT_PAGE";
pageTitles[buttonCount - 1] = getMessage("pagination.next", state);
}
sendInteractiveButtons(bodyText, pageIds, pageTitles);
} }
} }

View File

@@ -1,7 +1,7 @@
spring.application.name=whatsapp-automation spring.application.name=whatsapp-automation
server.port=8085 server.port=8085
whatsapp.access-token=EAAOZCONMSXCkBSHaYF1PqnnZAVjcmLqoOOJZCcbQGVWfWxILu9n0hQZBnUVGJG8WZBnLiZASM7etogv1xGj2PZCCmQPhFYyAKrhagFeYoi9fUZCfTryKonhStH3OU3RnHuZCxIWhnVIkIKe4byuOwqOv47SmM815ZCHUZA33fEhxPe9Onr1lg4tOnDeJMHoRYyDtcpWPbpc1eDbZBcy3vXwLslaBwx94xovhSKPpkDNgttTWsNBMnhbT7rKmnXEZAqDwWSg5FhITKePQS9HQ5troiyrXb whatsapp.access-token=EAAOZCONMSXCkBSL3CC4fKfY9X4ZAV8VzgQbiVx3qc05mdTSPgyy32jxSVVRDA5bGN3tcNmBmU5fWrqeRYYZCZApsobi4lZCWBrWBXJjEDQrxdTvZCdDZCjv7GZBgidEqZBboKbi5ZA9Yi864jE8mYmZAtjitliIKJ1nDsHpJpY0tZB2kpaZAKnnfHf9RcZBixr8owGaGHUvok4LO1SVNNZBJ4ZAr7JrZC5DY3vi402sGPOnY9n8QXGzIRKrcmFij74xAWKhybZAgvHIZBAIVsuicybPiDfVy8ZBN
whatsapp.verify-token=my_whatsapp_verify_token_123 whatsapp.verify-token=my_whatsapp_verify_token_123
spring.datasource.url=jdbc:postgresql://localhost:5432/rmc_bot spring.datasource.url=jdbc:postgresql://localhost:5432/rmc_bot
@@ -15,3 +15,6 @@ spring.jpa.show-sql=true
spring.messages.encoding=UTF-8 spring.messages.encoding=UTF-8
# Conversation timeout in seconds
conversation.inactivity.timeout=120

49
test_restart.py Normal file
View File

@@ -0,0 +1,49 @@
import requests
payload = {
"object": "whatsapp_business_account",
"entry": [
{
"id": "1068191009070265",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "15551914329",
"phone_number_id": "1236658909534007"
},
"contacts": [
{
"profile": {
"name": "Dev"
},
"wa_id": "919994834737",
"user_id": "IN.1458184416042595"
}
],
"messages": [
{
"from": "919994834737",
"from_user_id": "IN.1458184416042595",
"id": "wamid.HBgMOTE2Mzc5NzgzOTExFQIAEhgWM0VCMDU3MDk2N0Q2ODk0MzE2N0U4NwA=",
"timestamp": "1786945902",
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "MENU_SERVICES",
"title": "My Services"
}
},
"type": "interactive"
}
]
},
"field": "messages"
}
]
}
]
}
requests.post('http://localhost:8085/webhook', json=payload)