11#!/usr/bin/env python3
22"""
33SMS Notification Module for URL Watcher
4- Sends SMS notifications via AWS SNS when URL changes are detected
4+ Sends SMS notifications via TextBelt API when URL changes are detected
55"""
66
77import os
8- import boto3
8+ import requests
99import logging
1010from datetime import datetime
1111from typing import Optional , Dict , Any
12- from botocore .exceptions import ClientError , NoCredentialsError
1312
1413
1514class SMSNotifier :
16- """Handles SMS notifications via AWS SNS """
15+ """Handles SMS notifications via TextBelt API """
1716
1817 def __init__ (
1918 self ,
20- topic_arn : str = None ,
21- aws_access_key_id : str = None ,
22- aws_secret_access_key : str = None ,
23- region_name : str = "us-east-1" ,
19+ phone_number : str = None ,
20+ api_key : str = None ,
2421 ):
2522 """
2623 Initialize SMS notifier
2724
2825 Args:
29- topic_arn: SNS topic ARN for sending SMS
30- aws_access_key_id: AWS access key ID
31- aws_secret_access_key: AWS secret access key
32- region_name: AWS region name (default: us-east-1)
26+ phone_number: Phone number to send SMS to (e.g., "+1234567890")
27+ api_key: TextBelt API key
3328 """
34- self .topic_arn = topic_arn or os .getenv ("SNS_TOPIC_ARN" )
35- self .region_name = region_name
36-
37- # Initialize boto3 client
38- try :
39- if aws_access_key_id and aws_secret_access_key :
40- self .sns_client = boto3 .client (
41- "sns" ,
42- aws_access_key_id = aws_access_key_id ,
43- aws_secret_access_key = aws_secret_access_key ,
44- region_name = region_name ,
45- )
46- else :
47- # Use environment variables or IAM role
48- self .sns_client = boto3 .client ("sns" , region_name = region_name )
49-
50- except (NoCredentialsError , Exception ) as e :
51- logging .error (f"Failed to initialize AWS SNS client: { e } " )
52- self .sns_client = None
29+ self .phone_number = phone_number or os .getenv ("SMS_PHONE_NUMBER" )
30+ self .api_key = api_key or os .getenv ("TEXTBELT_API_KEY" )
31+ self .api_url = "https://textbelt.com/text"
5332
5433 def is_configured (self ) -> bool :
5534 """Check if SMS notifications are properly configured"""
56- return self .sns_client is not None and self .topic_arn is not None
35+ return self .phone_number is not None and self .api_key is not None
5736
5837 def send_notification (self , url : str , message : str , subject : str = None ) -> bool :
5938 """
@@ -62,7 +41,7 @@ def send_notification(self, url: str, message: str, subject: str = None) -> bool
6241 Args:
6342 url: The URL that changed
6443 message: Change description/diff
65- subject: Optional subject line
44+ subject: Optional subject line (not used with TextBelt)
6645
6746 Returns:
6847 bool: True if notification sent successfully, False otherwise
@@ -72,33 +51,54 @@ def send_notification(self, url: str, message: str, subject: str = None) -> bool
7251 return False
7352
7453 try :
75- # Prepare message
54+ # Prepare simple message without URLs or diffs to avoid TextBelt restrictions
7655 timestamp = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
77- sms_message = f"URL CHANGE DETECTED\n "
56+ sms_message = f"WEBSITE CHANGE DETECTED\n "
7857 sms_message += f"Time: { timestamp } \n "
79- sms_message += f"URL: { url } \n \n "
58+
59+ # Extract and simplify domain from URL
60+ try :
61+ from urllib .parse import urlparse
62+ domain = urlparse (url ).netloc
63+ # Further simplify domain to avoid detection
64+ domain_parts = domain .split ('.' )
65+ if len (domain_parts ) > 1 :
66+ simplified = f"{ domain_parts [0 ]} site"
67+ else :
68+ simplified = "monitored site"
69+ sms_message += f"Site: { simplified } \n \n "
70+ except Exception :
71+ sms_message += f"Site: monitored website\n \n "
72+
73+ # Use simple change notification instead of diff content
74+ sms_message += "Content changes detected. Check your monitoring dashboard for details."
75+
76+ # Debug: Log the message being sent
77+ logging .info (f"Sending SMS message: { repr (sms_message )} " )
78+
79+ # Send message via TextBelt API
80+ payload = {
81+ "phone" : self .phone_number ,
82+ "message" : sms_message ,
83+ "key" : self .api_key ,
84+ }
8085
81- # Truncate message for SMS limits (160 chars for single SMS)
82- if len (message ) > 100 :
83- sms_message += f"Changes: { message [:100 ]} ..."
86+ response = requests .post (self .api_url , data = payload , timeout = 30 )
87+ response .raise_for_status ()
88+
89+ result = response .json ()
90+
91+ if result .get ("success" ):
92+ text_id = result .get ("textId" )
93+ logging .info (f"SMS notification sent successfully. TextId: { text_id } " )
94+ return True
8495 else :
85- sms_message += f"Changes: { message } "
86-
87- # Send message
88- response = self .sns_client .publish (
89- TopicArn = self .topic_arn ,
90- Message = sms_message ,
91- Subject = subject or f"URL Change: { url [:50 ]} ..." ,
92- )
93-
94- message_id = response .get ("MessageId" )
95- logging .info (f"SMS notification sent successfully. MessageId: { message_id } " )
96- return True
97-
98- except ClientError as e :
99- error_code = e .response ["Error" ]["Code" ]
100- error_message = e .response ["Error" ]["Message" ]
101- logging .error (f"AWS SNS error ({ error_code } ): { error_message } " )
96+ error_msg = result .get ("error" , "Unknown error" )
97+ logging .error (f"TextBelt API error: { error_msg } " )
98+ return False
99+
100+ except requests .exceptions .RequestException as e :
101+ logging .error (f"HTTP error sending SMS: { e } " )
102102 return False
103103
104104 except Exception as e :
@@ -117,48 +117,75 @@ def test_notification(self) -> Dict[str, Any]:
117117 "success" : False ,
118118 "error" : "SMS notifications not configured" ,
119119 "details" : {
120- "sns_client " : self .sns_client is not None ,
121- "topic_arn " : self .topic_arn is not None ,
120+ "phone_number " : self .phone_number is not None ,
121+ "api_key " : self .api_key is not None ,
122122 },
123123 }
124124
125125 try :
126126 test_message = f"Test notification from URL Watcher at { datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )} "
127127
128- response = self .sns_client .publish (
129- TopicArn = self .topic_arn , Message = test_message , Subject = "URL Watcher Test"
130- )
131-
132- return {
133- "success" : True ,
134- "message_id" : response .get ("MessageId" ),
135- "details" : {"topic_arn" : self .topic_arn , "region" : self .region_name },
128+ payload = {
129+ "phone" : self .phone_number ,
130+ "message" : test_message ,
131+ "key" : self .api_key ,
136132 }
137133
138- except ClientError as e :
139- return {
140- "success" : False ,
141- "error" : f"AWS SNS error: { e .response ['Error' ]['Message' ]} " ,
142- "error_code" : e .response ["Error" ]["Code" ],
143- }
134+ response = requests .post (self .api_url , data = payload , timeout = 30 )
135+ response .raise_for_status ()
136+
137+ result = response .json ()
138+
139+ if result .get ("success" ):
140+ return {
141+ "success" : True ,
142+ "text_id" : result .get ("textId" ),
143+ "details" : {
144+ "phone_number" : self .phone_number ,
145+ "api_url" : self .api_url ,
146+ },
147+ }
148+ else :
149+ return {
150+ "success" : False ,
151+ "error" : f"TextBelt API error: { result .get ('error' , 'Unknown error' )} " ,
152+ }
153+
154+ except requests .exceptions .RequestException as e :
155+ return {"success" : False , "error" : f"HTTP error: { str (e )} " }
144156
145157 except Exception as e :
146158 return {"success" : False , "error" : f"Unexpected error: { str (e )} " }
147159
148160
149- def create_notifier_from_env () -> SMSNotifier :
161+ def create_notifier_from_env (load_dotenv : bool = True ) -> SMSNotifier :
150162 """
151163 Create SMS notifier using environment variables
152164
153165 Expected environment variables:
154- - SNS_TOPIC_ARN: ARN of the SNS topic
155- - AWS_ACCESS_KEY_ID: AWS access key ID
156- - AWS_SECRET_ACCESS_KEY: AWS secret access key
157- - AWS_REGION: AWS region (optional, defaults to us-east-1)
166+ - SMS_PHONE_NUMBER: Phone number to send SMS to (e.g., "+1234567890")
167+ - TEXTBELT_API_KEY: TextBelt API key
168+
169+ Args:
170+ load_dotenv: Whether to load .env file (default: True)
158171 """
172+ # Try to load .env file only if requested (for testing flexibility)
173+ if load_dotenv :
174+ env_file = ".env"
175+ if os .path .exists (env_file ):
176+ try :
177+ with open (env_file , "r" ) as f :
178+ for line in f :
179+ line = line .strip ()
180+ if line and not line .startswith ("#" ) and "=" in line :
181+ key , value = line .split ("=" , 1 )
182+ # Only set if not already in environment (allows test override)
183+ if key .strip () not in os .environ :
184+ os .environ [key .strip ()] = value .strip ()
185+ except Exception as e :
186+ logging .warning (f"Failed to load .env file: { e } " )
187+
159188 return SMSNotifier (
160- topic_arn = os .getenv ("SNS_TOPIC_ARN" ),
161- aws_access_key_id = os .getenv ("AWS_ACCESS_KEY_ID" ),
162- aws_secret_access_key = os .getenv ("AWS_SECRET_ACCESS_KEY" ),
163- region_name = os .getenv ("AWS_REGION" , "us-east-1" ),
189+ phone_number = os .getenv ("SMS_PHONE_NUMBER" ),
190+ api_key = os .getenv ("TEXTBELT_API_KEY" ),
164191 )
0 commit comments