Skip to content

Commit a943e03

Browse files
committed
Enhance WhatsApp message handling: Add message timestamp extraction and message-age condition
1 parent 00b329a commit a943e03

File tree

2 files changed

+88
-15
lines changed

2 files changed

+88
-15
lines changed

src/ansari/app/main_whatsapp.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ async def main_webhook(request: Request, background_tasks: BackgroundTasks) -> R
102102
incoming_msg_type,
103103
incoming_msg_body,
104104
message_id,
105+
message_unix_time,
105106
) = await presenter.extract_relevant_whatsapp_message_details(data)
106107
except Exception as e:
107108
logger.error(f"Error extracting message details: {e}")
@@ -117,22 +118,24 @@ async def main_webhook(request: Request, background_tasks: BackgroundTasks) -> R
117118
# Terminate if whatsapp is not enabled (i.e., via .env configurations, etc)
118119
if not whatsapp_enabled:
119120
# Create a temporary user-specific presenter just to send the message
120-
temp_presenter = WhatsAppPresenter.create_user_specific_presenter(presenter, from_whatsapp_number, None, None, None)
121+
temp_presenter = WhatsAppPresenter.create_user_specific_presenter(
122+
presenter, from_whatsapp_number, None, None, None, None
123+
)
121124
background_tasks.add_task(
122125
temp_presenter.send_whatsapp_message,
123126
"Ansari for WhatsApp is down for maintenance, please try again later or visit our website at https://ansari.chat.",
124127
)
125128
return Response(status_code=200)
126129

127-
# Workaround while locally developing:
128-
# don't process other dev's whatsapp recepient phone nums coming from staging env.
129-
# (as both stage Meta app / local-.env-file have same testing number)
130-
dev_num_sub_str = "YOUR_DEV_PHONE_NUM"
131-
if get_settings().DEV_MODE and dev_num_sub_str not in from_whatsapp_number:
132-
logger.debug(
133-
f"Incoming message from {from_whatsapp_number} (doesn't have this sub-str: {dev_num_sub_str}). \
134-
Therefore, will not process it as it's not cur. dev."
135-
)
130+
# Temporarycorner case while locally developing:
131+
# Since the staging server is always running,
132+
# and since we currently have the same testing number for both staging and local testing,
133+
# therefore we need an indicator that a message is meant for a dev who's testing locally now
134+
# and not for the staging server.
135+
# This is done by prefixing the message with "!d " (e.g., "!d what is ansari?")
136+
# NOTE: Obviously, this temp. solution will be removed when we get a dedicated testing number for staging testing.
137+
if get_settings().DEPLOYMENT_TYPE == "staging" and incoming_msg_body.get("body", "").startswith("!d "):
138+
logger.debug("Incoming message is meant for a dev who's testing locally now, so will not process it in staging...")
136139
return Response(status_code=200)
137140

138141
# Create a user-specific presenter for this message
@@ -142,6 +145,7 @@ async def main_webhook(request: Request, background_tasks: BackgroundTasks) -> R
142145
incoming_msg_type,
143146
incoming_msg_body,
144147
message_id,
148+
message_unix_time,
145149
)
146150

147151
# Start the typing indicator loop that will continue until message is processed
@@ -159,6 +163,22 @@ async def main_webhook(request: Request, background_tasks: BackgroundTasks) -> R
159163
)
160164
return Response(status_code=200)
161165

166+
# Check if there are more than 24 hours have passed from the user's message to the current time
167+
# If so, send a message to the user and return
168+
if user_presenter.is_message_too_old():
169+
response_msg = "Sorry, your message "
170+
user_msg_start = " ".join(incoming_msg_body.get("body", "").split(" ")[:5])
171+
if user_msg_start:
172+
response_msg_cont = ' "' + user_msg_start + '" '
173+
else:
174+
response_msg_cont = " "
175+
response_msg = f"Sorry, your message{response_msg_cont}is too old. Please send a new message."
176+
background_tasks.add_task(
177+
user_presenter.send_whatsapp_message,
178+
response_msg,
179+
)
180+
return Response(status_code=200)
181+
162182
# Check if the incoming message is a location
163183
if incoming_msg_type == "location":
164184
# NOTE: Currently, will not handle location messages

src/ansari/presenters/whatsapp_presenter.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(
3232
incoming_msg_type: str | None = None,
3333
incoming_msg_body: dict | None = None,
3434
message_id: str | None = None,
35+
message_unix_time: int | None = None,
3536
):
3637
if agent:
3738
self.settings = agent.settings
@@ -48,6 +49,7 @@ def __init__(
4849
self.incoming_msg_type = incoming_msg_type
4950
self.incoming_msg_body = incoming_msg_body
5051
self.message_id = message_id
52+
self.message_unix_time = message_unix_time
5153
self.typing_indicator_task = None
5254
self.first_indicator_time = None
5355

@@ -59,6 +61,7 @@ def create_user_specific_presenter(
5961
incoming_msg_type: str,
6062
incoming_msg_body: dict,
6163
message_id: str,
64+
message_unix_time: int | None = None,
6265
):
6366
"""Creates a user-specific presenter instance from a general presenter."""
6467
return cls(
@@ -69,20 +72,22 @@ def create_user_specific_presenter(
6972
incoming_msg_type=incoming_msg_type,
7073
incoming_msg_body=incoming_msg_body,
7174
message_id=message_id,
75+
message_unix_time=message_unix_time,
7276
)
7377

7478
async def extract_relevant_whatsapp_message_details(
7579
self,
7680
body: dict[str, Any],
77-
) -> tuple[bool, str | None, str | None, dict | None, str | None]:
81+
) -> tuple[bool, str | None, str | None, dict | None, str | None, int | None]:
7882
"""Extracts relevant whatsapp message details from the incoming webhook payload.
7983
8084
Args:
8185
body (Dict[str, Any]): The JSON body of the incoming request.
8286
8387
Returns:
84-
tuple[bool, Optional[str], Optional[str], Optional[dict], Optional[str]]:
85-
A tuple of (is_status, user_whatsapp_number, incoming_msg_type, incoming_msg_body, message_id)
88+
tuple[bool, Optional[str], Optional[str], Optional[dict], Optional[str], Optional[int]]:
89+
A tuple of:
90+
(is_status, user_whatsapp_number, incoming_msg_type, incoming_msg_body, message_id, message_unix_time)
8691
8792
Raises:
8893
Exception: If the payload structure is invalid or unsupported.
@@ -109,7 +114,7 @@ async def extract_relevant_whatsapp_message_details(
109114
# logger.debug(
110115
# f"WhatsApp status update received:\n({status} at {timestamp}.)",
111116
# )
112-
return True, None, None, None, None
117+
return True, None, None, None, None, None
113118
else:
114119
is_status = False
115120

@@ -127,6 +132,9 @@ async def extract_relevant_whatsapp_message_details(
127132
message_id = incoming_msg.get("id")
128133
# Extract the phone number of the WhatsApp sender
129134
user_whatsapp_number = incoming_msg["from"]
135+
# Extract timestamp from message (in Unix time format) and convert to int if present
136+
message_unix_time_str = incoming_msg.get("timestamp")
137+
message_unix_time = int(message_unix_time_str) if message_unix_time_str is not None else None
130138
# Meta API note: Meta sends "errors" key when receiving unsupported message types
131139
# (e.g., video notes, gifs sent from giphy, or polls)
132140
incoming_msg_type = incoming_msg["type"] if incoming_msg["type"] in incoming_msg.keys() else "errors"
@@ -135,7 +143,7 @@ async def extract_relevant_whatsapp_message_details(
135143

136144
logger.info(f"Received a supported whatsapp message from {user_whatsapp_number}: {incoming_msg_body}")
137145

138-
return (is_status, user_whatsapp_number, incoming_msg_type, incoming_msg_body, message_id)
146+
return (is_status, user_whatsapp_number, incoming_msg_type, incoming_msg_body, message_id, message_unix_time)
139147

140148
async def check_and_register_user(self) -> bool:
141149
"""
@@ -795,5 +803,50 @@ async def handle_unsupported_message(
795803
f"Sorry, I can't process {msg_type} yet. Please send me a text message.",
796804
)
797805

806+
def is_message_too_old(self) -> bool:
807+
"""
808+
Checks if the incoming message is older than the allowed threshold (24 hours).
809+
810+
Uses the message_unix_time attribute (timestamp in Unix time format - seconds since epoch)
811+
extracted during message processing to determine if the message is too old.
812+
813+
Returns:
814+
bool: True if the message is older than 24 hours, False otherwise
815+
"""
816+
# Define the too old threshold (24 hours in seconds)
817+
TOO_OLD_THRESHOLD = 24 * 60 * 60 # 24 hours in seconds
818+
819+
# If there's no timestamp, message can't be verified as too old
820+
if not self.message_unix_time:
821+
logger.debug("No timestamp available, cannot determine message age")
822+
return False
823+
824+
# Convert the Unix timestamp to a datetime object
825+
try:
826+
msg_time = datetime.fromtimestamp(self.message_unix_time, tz=timezone.utc)
827+
# Get the current time in UTC
828+
current_time = datetime.now(timezone.utc)
829+
# Calculate time difference in seconds
830+
time_diff = (current_time - msg_time).total_seconds()
831+
832+
# Log the message age for debugging
833+
if time_diff < 60:
834+
age_logging = f"{time_diff:.1f} seconds"
835+
elif time_diff < 3600:
836+
age_logging = f"{time_diff / 60:.1f} minutes"
837+
elif time_diff < 86400:
838+
age_logging = f"{time_diff / 3600:.1f} hours"
839+
else:
840+
age_logging = f"{time_diff / 86400:.1f} days"
841+
842+
logger.debug(f"Message age: {age_logging}")
843+
844+
# Return True if the message is older than the threshold
845+
return time_diff > TOO_OLD_THRESHOLD
846+
847+
except (ValueError, TypeError) as e:
848+
logger.error(f"Error parsing message timestamp: {e}")
849+
return False
850+
798851
def present(self):
799852
pass

0 commit comments

Comments
 (0)