Add project files with proper line endings
This commit is contained in:
parent
1d27083a4b
commit
66d789436b
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env_*
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
webhook.log
|
||||||
|
node_red.log
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.bak1
|
||||||
|
bak/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
# Temporary/cache files
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Upload files
|
||||||
|
whpload.json
|
||||||
|
|
||||||
|
# Certificates (if sensitive)
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
414
alert.py
Normal file
414
alert.py
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import argparse
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import requests # Added for voice calls
|
||||||
|
import telnyx # For SMS
|
||||||
|
from kombu import Connection, Exchange, Queue
|
||||||
|
from kombu.exceptions import KombuError, OperationalError
|
||||||
|
from kombu.simple import SimpleQueue # Using SimpleQueue for get/put ease
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# --- Configuration Loading ---
|
||||||
|
# Load environment variables from $HOME/.env then ./.env (current dir overrides home)
|
||||||
|
dotenv_path_home = os.path.join(os.environ.get("HOME", "."), '.env')
|
||||||
|
load_dotenv(dotenv_path=dotenv_path_home)
|
||||||
|
dotenv_path_cwd = os.path.join(os.getcwd(), '.env')
|
||||||
|
load_dotenv(dotenv_path=dotenv_path_cwd, override=True)
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
# General Telnyx
|
||||||
|
TELNYX_API_KEY = os.environ.get("TELNYX_API_KEY")
|
||||||
|
TELNYX_MESSAGING_PROFILE_ID = os.environ.get("TELNYX_MESSAGING_PROFILE_ID")
|
||||||
|
|
||||||
|
# SMS Specific
|
||||||
|
TELNYX_SENDER_ID = os.environ.get("TELNYX_SENDER_ID") # Sending SMS *FROM* this E.164 number
|
||||||
|
TELNYX_SENDER_ID_ALPHA = os.environ.get("TELNYX_SENDER_ID_ALPHA") # Sending SMS/Voice *FROM* this Alpha Sender ID
|
||||||
|
|
||||||
|
# Voice Call Specific
|
||||||
|
TELNYX_CONNECTION_ID_VOICE = os.environ.get("TELNYX_CONNECTION_ID_VOICE","2671409623596009055")
|
||||||
|
TELNYX_WEBHOOK_URL_VOICE = os.environ.get("TELNYX_WEBHOOK_URL_VOICE", "http://your-webhook-server.com/telnyx-webhook")
|
||||||
|
|
||||||
|
# RabbitMQ Configuration
|
||||||
|
raw_rabbitmq_url = os.environ.get("RABBITMQ_URL", "localhost")
|
||||||
|
if raw_rabbitmq_url == "localhost":
|
||||||
|
RABBITMQ_URL = "amqp://guest:guest@localhost:5672//"
|
||||||
|
else:
|
||||||
|
RABBITMQ_URL = raw_rabbitmq_url
|
||||||
|
RABBITMQ_ALERTS_QNAME = os.environ.get("RABBITMQ_ALERTS_QNAME", "alerts")
|
||||||
|
|
||||||
|
MOSQUITTO_PASSWORD_FILE = os.environ.get('MOSQUITTO_PASSWORD_FILE', '/etc/mosquitto/passwd')
|
||||||
|
MOSQUITTO_ACL_FILE = os.environ.get('MOSQUITTO_ACL_FILE', '/etc/mosquitto/acl')
|
||||||
|
|
||||||
|
# Kombu Exchange and Queue definitions
|
||||||
|
exchange = Exchange("", type='direct')
|
||||||
|
alert_queue_obj = Queue(RABBITMQ_ALERTS_QNAME, exchange=exchange, routing_key=RABBITMQ_ALERTS_QNAME, durable=True)
|
||||||
|
|
||||||
|
# --- Setup Logging ---
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(name)s - [%(funcName)s] - %(message)s",
|
||||||
|
stream=sys.stdout,
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("MessengerTool")
|
||||||
|
|
||||||
|
# --- Helper Functions ---
|
||||||
|
def check_env_vars(require_telnyx_sms=False, require_telnyx_voice=False, require_rabbitmq=False):
|
||||||
|
missing_vars = []
|
||||||
|
if require_rabbitmq:
|
||||||
|
if not RABBITMQ_URL: missing_vars.append("RABBITMQ_URL")
|
||||||
|
if not RABBITMQ_ALERTS_QNAME: missing_vars.append("RABBITMQ_ALERTS_QNAME")
|
||||||
|
|
||||||
|
if require_telnyx_sms or require_telnyx_voice:
|
||||||
|
if not TELNYX_API_KEY: missing_vars.append("TELNYX_API_KEY")
|
||||||
|
|
||||||
|
if require_telnyx_sms:
|
||||||
|
if not TELNYX_SENDER_ID: missing_vars.append("TELNYX_SENDER_ID (for numeric SMS sender)")
|
||||||
|
# TELNYX_SENDER_ID_ALPHA is needed if 'alpha' or 'auto' for non-US SMS is used
|
||||||
|
if not TELNYX_SENDER_ID_ALPHA: logger.debug("TELNYX_SENDER_ID_ALPHA not set (optional for SMS if only numeric is used or for US destinations in auto mode).")
|
||||||
|
|
||||||
|
|
||||||
|
if require_telnyx_voice:
|
||||||
|
if not TELNYX_CONNECTION_ID_VOICE: missing_vars.append("TELNYX_CONNECTION_ID_VOICE")
|
||||||
|
if not TELNYX_WEBHOOK_URL_VOICE: missing_vars.append("TELNYX_WEBHOOK_URL_VOICE")
|
||||||
|
if not TELNYX_SENDER_ID: missing_vars.append("TELNYX_SENDER_ID (for numeric voice caller ID)")
|
||||||
|
if not TELNYX_SENDER_ID_ALPHA: missing_vars.append("TELNYX_SENDER_ID_ALPHA (for alphanumeric voice caller ID)")
|
||||||
|
|
||||||
|
if missing_vars:
|
||||||
|
logger.error(f"Missing required environment variables for this operation: {', '.join(missing_vars)}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def normalize_phone_number(phone_number_str: str) -> str:
|
||||||
|
if not phone_number_str: return ""
|
||||||
|
cleaned_number = "".join(filter(lambda char: char.isdigit() or char == '+', phone_number_str))
|
||||||
|
if not cleaned_number.startswith('+'):
|
||||||
|
if cleaned_number.startswith('1') and len(cleaned_number) >= 11:
|
||||||
|
cleaned_number = '+' + cleaned_number
|
||||||
|
else:
|
||||||
|
cleaned_number = '+' + cleaned_number
|
||||||
|
return cleaned_number
|
||||||
|
|
||||||
|
# --- Telnyx SMS Sending Function ---
|
||||||
|
def setup_telnyx_sms_client():
|
||||||
|
check_env_vars(require_telnyx_sms=True)
|
||||||
|
try:
|
||||||
|
telnyx.api_key = TELNYX_API_KEY
|
||||||
|
logger.info("Telnyx client for SMS configured.")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to configure Telnyx client for SMS: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_telnyx_sms(recipient_phone: str, message_body: str, caller_id_type: str = "auto") -> bool:
|
||||||
|
"""Sends an SMS using the Telnyx API, with dynamic 'from_' based on caller_id_type."""
|
||||||
|
if not setup_telnyx_sms_client():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not recipient_phone or not message_body:
|
||||||
|
logger.error("Cannot send Telnyx SMS: Recipient phone and message are required.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
recipient_phone = normalize_phone_number(recipient_phone)
|
||||||
|
from_id = TELNYX_SENDER_ID # Default to numeric
|
||||||
|
|
||||||
|
if caller_id_type == "alpha":
|
||||||
|
if TELNYX_SENDER_ID_ALPHA:
|
||||||
|
from_id = TELNYX_SENDER_ID_ALPHA
|
||||||
|
else:
|
||||||
|
logger.warning("SMS Caller ID type 'alpha' requested, but TELNYX_SENDER_ID_ALPHA is not set. Falling back to numeric.")
|
||||||
|
elif caller_id_type == "numeric":
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
elif caller_id_type == "auto":
|
||||||
|
if recipient_phone.startswith("+1"): # US/Canada
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
elif TELNYX_SENDER_ID_ALPHA: # Other international, try Alpha if available
|
||||||
|
from_id = TELNYX_SENDER_ID_ALPHA
|
||||||
|
else: # Fallback to numeric if Alpha not set for international
|
||||||
|
logger.warning("SMS Caller ID type 'auto' for non-US/Canada destination, but TELNYX_SENDER_ID_ALPHA not set. Using numeric.")
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
else: # Should not happen with argparse choices
|
||||||
|
logger.warning(f"Invalid caller_id_type '{caller_id_type}' for SMS. Defaulting to numeric.")
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
|
||||||
|
|
||||||
|
logger.info(f"Attempting to send Telnyx SMS from '{from_id}' to '{recipient_phone}'")
|
||||||
|
try:
|
||||||
|
message_create_params = {
|
||||||
|
"from_": from_id,
|
||||||
|
"to": recipient_phone,
|
||||||
|
"text": message_body,
|
||||||
|
"messaging_profile_id": TELNYX_MESSAGING_PROFILE_ID,
|
||||||
|
"type": "sms"
|
||||||
|
}
|
||||||
|
if not message_create_params["messaging_profile_id"]:
|
||||||
|
del message_create_params["messaging_profile_id"]
|
||||||
|
|
||||||
|
response = telnyx.Message.create(**message_create_params)
|
||||||
|
logger.info(f"SMS submitted successfully. Message ID: {response.id}")
|
||||||
|
return True
|
||||||
|
except telnyx.error.TelnyxError as e:
|
||||||
|
logger.error(f"Telnyx API Error sending SMS to {recipient_phone} from '{from_id}': {e}")
|
||||||
|
if hasattr(e, 'json_body') and e.json_body and 'errors' in e.json_body:
|
||||||
|
for err in e.json_body['errors']:
|
||||||
|
logger.error(f" - Code: {err.get('code')}, Title: {err.get('title')}, Detail: {err.get('detail')}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error sending Telnyx SMS to {recipient_phone}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# --- Telnyx Voice Call Function ---
|
||||||
|
def make_telnyx_voice_call(to_phone: str, tts_message: str = None, audio_url: str = None,
|
||||||
|
connection_id_override: str = None, webhook_url_override: str = None,
|
||||||
|
caller_id_type: str = "auto", amd_mode: str = "disabled",
|
||||||
|
extra_custom_headers: list = None):
|
||||||
|
check_env_vars(require_telnyx_voice=True)
|
||||||
|
api_url = "https://api.telnyx.com/v2/calls"
|
||||||
|
to_phone = normalize_phone_number(to_phone)
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
|
||||||
|
if caller_id_type == "alpha":
|
||||||
|
if TELNYX_SENDER_ID_ALPHA:
|
||||||
|
from_id = TELNYX_SENDER_ID_ALPHA
|
||||||
|
else:
|
||||||
|
logger.warning("Voice Caller ID type 'alpha' requested, but TELNYX_SENDER_ID_ALPHA is not set. Falling back to numeric.")
|
||||||
|
elif caller_id_type == "numeric":
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
elif caller_id_type == "auto":
|
||||||
|
if to_phone.startswith("+1"):
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
elif TELNYX_SENDER_ID_ALPHA:
|
||||||
|
from_id = TELNYX_SENDER_ID_ALPHA
|
||||||
|
else:
|
||||||
|
logger.warning("Voice Caller ID type 'auto' for non-US/Canada destination, but TELNYX_SENDER_ID_ALPHA not set. Using numeric.")
|
||||||
|
from_id = TELNYX_SENDER_ID
|
||||||
|
|
||||||
|
telnyx_connection_id = connection_id_override or TELNYX_CONNECTION_ID_VOICE
|
||||||
|
telnyx_webhook_url = webhook_url_override or TELNYX_WEBHOOK_URL_VOICE
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {TELNYX_API_KEY}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
payload_custom_headers = []
|
||||||
|
if tts_message: payload_custom_headers.append({"name": "X-TTS-Payload", "value": tts_message})
|
||||||
|
if audio_url: payload_custom_headers.append({"name": "X-Audio-Url", "value": audio_url})
|
||||||
|
if extra_custom_headers:
|
||||||
|
for header_item in extra_custom_headers:
|
||||||
|
if "=" in header_item:
|
||||||
|
name, value = header_item.split("=", 1)
|
||||||
|
payload_custom_headers.append({"name": name.strip(), "value": value.strip()})
|
||||||
|
else:
|
||||||
|
logger.warning(f"Skipping malformed custom header: {header_item}. Expected NAME=VALUE")
|
||||||
|
|
||||||
|
api_payload = {
|
||||||
|
"to": to_phone, "from": from_id, "connection_id": telnyx_connection_id,
|
||||||
|
"webhook_url": telnyx_webhook_url, "webhook_url_method": "POST",
|
||||||
|
"answering_machine_detection": amd_mode
|
||||||
|
}
|
||||||
|
if payload_custom_headers: api_payload["custom_headers"] = payload_custom_headers
|
||||||
|
|
||||||
|
logger.info(f"Initiating Telnyx voice call from '{from_id}' to '{to_phone}' using connection '{telnyx_connection_id}'")
|
||||||
|
logger.debug(f"Voice Call API Payload: {json.dumps(api_payload, indent=2)}")
|
||||||
|
try:
|
||||||
|
response = requests.post(api_url, headers=headers, json=api_payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
call_data = response.json().get("data", {})
|
||||||
|
logger.info("Voice call initiated successfully!")
|
||||||
|
logger.info(f" Call Control ID: {call_data.get('call_control_id')}, Session ID: {call_data.get('call_session_id')}, Leg ID: {call_data.get('call_leg_id')}")
|
||||||
|
return True
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
logger.error(f"Telnyx API HTTP Error initiating voice call: {e}")
|
||||||
|
if e.response is not None:
|
||||||
|
logger.error(f"Status Code: {e.response.status_code}")
|
||||||
|
try:
|
||||||
|
error_details = e.response.json()
|
||||||
|
if "errors" in error_details:
|
||||||
|
for err in error_details["errors"]: logger.error(f" - Code: {err.get('code')}, Title: {err.get('title')}, Detail: {err.get('detail')}")
|
||||||
|
else: logger.error(f"Response Body: {json.dumps(error_details, indent=2)}")
|
||||||
|
except json.JSONDecodeError: logger.error(f"Response Body (raw): {e.response.text}")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Network error initiating voice call: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error initiating voice call: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# --- RabbitMQ Interaction Functions ---
|
||||||
|
def publish_to_rmq(payload: dict):
|
||||||
|
check_env_vars(require_rabbitmq=True)
|
||||||
|
try:
|
||||||
|
with Connection(RABBITMQ_URL) as connection:
|
||||||
|
logger.info(f"Connecting to RabbitMQ at {connection.as_uri(hide_password=True)} to publish...")
|
||||||
|
producer = connection.Producer(serializer='json')
|
||||||
|
producer.publish(payload, exchange=exchange, routing_key=RABBITMQ_ALERTS_QNAME, declare=[alert_queue_obj], retry=True, retry_policy={'interval_start': 0, 'interval_step': 2, 'interval_max': 30, 'max_retries': 3}, delivery_mode='persistent')
|
||||||
|
logger.info(f"Message published successfully to queue '{RABBITMQ_ALERTS_QNAME}'.")
|
||||||
|
return True
|
||||||
|
except (KombuError, OperationalError, ConnectionRefusedError, Exception) as e:
|
||||||
|
logger.error(f"Failed to publish message to RabbitMQ ({RABBITMQ_URL}): {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def peek_messages_rmq(limit=5):
|
||||||
|
check_env_vars(require_rabbitmq=True)
|
||||||
|
messages_peeked = []
|
||||||
|
try:
|
||||||
|
with Connection(RABBITMQ_URL) as connection:
|
||||||
|
logger.info(f"Connecting to RabbitMQ at {connection.as_uri(hide_password=True)} to peek...")
|
||||||
|
alert_queue_obj(connection.channel()).declare(passive=True)
|
||||||
|
queue = connection.SimpleQueue(RABBITMQ_ALERTS_QNAME)
|
||||||
|
logger.info(f"Peeking up to {limit} messages from queue '{RABBITMQ_ALERTS_QNAME}'...")
|
||||||
|
count = 0
|
||||||
|
while count < limit:
|
||||||
|
try:
|
||||||
|
message = queue.get(block=False)
|
||||||
|
print("-" * 20 + f" Message {count + 1} " + "-" * 20)
|
||||||
|
print(json.dumps(message.payload, indent=2))
|
||||||
|
messages_peeked.append(message.payload)
|
||||||
|
message.requeue()
|
||||||
|
count += 1
|
||||||
|
except queue.Empty: logger.info("Queue is empty or no more messages."); break
|
||||||
|
except Exception as get_err:
|
||||||
|
logger.error(f"Error during message get/requeue: {get_err}")
|
||||||
|
if 'message' in locals() and hasattr(message, 'delivery_tag') and message.delivery_tag:
|
||||||
|
try: message.requeue()
|
||||||
|
except Exception as req_err: logger.error(f"Failed to requeue msg after error: {req_err}")
|
||||||
|
break
|
||||||
|
queue.close()
|
||||||
|
if not messages_peeked: logger.info(f"No messages found in queue '{RABBITMQ_ALERTS_QNAME}'.")
|
||||||
|
return messages_peeked
|
||||||
|
except (KombuError, OperationalError, ConnectionRefusedError) as e: logger.error(f"Failed to peek from RMQ: {e}"); return []
|
||||||
|
except Exception as e: logger.error(f"Unexpected error peeking: {e}"); return []
|
||||||
|
|
||||||
|
def get_and_process_one_rmq_sms(target_phone: str):
|
||||||
|
check_env_vars(require_rabbitmq=True, require_telnyx_sms=True)
|
||||||
|
message = None
|
||||||
|
try:
|
||||||
|
with Connection(RABBITMQ_URL) as connection:
|
||||||
|
logger.info(f"Connecting to RMQ for get_and_process_one_rmq_sms...")
|
||||||
|
alert_queue_obj(connection.channel()).declare(passive=True)
|
||||||
|
queue = connection.SimpleQueue(RABBITMQ_ALERTS_QNAME)
|
||||||
|
try:
|
||||||
|
message = queue.get(block=True, timeout=1)
|
||||||
|
logger.info(f"Got message from queue (Tag: {message.delivery_tag}).")
|
||||||
|
payload = message.payload
|
||||||
|
if not isinstance(payload, dict): raise TypeError("Msg payload not dict.")
|
||||||
|
body = payload.get("body")
|
||||||
|
if not body: raise ValueError("Msg payload missing 'body'.")
|
||||||
|
# For this function, caller_id_type for SMS from RMQ will be 'auto' by default
|
||||||
|
# as it's not specified in the RMQ message itself.
|
||||||
|
sms_sent = send_telnyx_sms(recipient_phone=target_phone, message_body=body, caller_id_type="auto")
|
||||||
|
if sms_sent:
|
||||||
|
logger.info(f"Telnyx SMS sent. Acking RMQ msg (Tag: {message.delivery_tag}).")
|
||||||
|
message.ack()
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to send SMS. Rejecting RMQ msg (Tag: {message.delivery_tag}).")
|
||||||
|
message.reject(requeue=False)
|
||||||
|
queue.close()
|
||||||
|
return sms_sent
|
||||||
|
except queue.Empty: logger.info(f"Queue '{RABBITMQ_ALERTS_QNAME}' empty."); queue.close(); return False
|
||||||
|
except (KombuError, OperationalError, ConnectionRefusedError, TypeError, ValueError, json.JSONDecodeError) as e:
|
||||||
|
logger.error(f"RMQ/Payload error: {e}.")
|
||||||
|
if message and hasattr(message, 'delivery_tag'):
|
||||||
|
try: message.reject(requeue=False)
|
||||||
|
except Exception as reject_e: logger.error(f"Failed to reject msg after error: {reject_e}")
|
||||||
|
return False
|
||||||
|
except Exception as e: logger.error(f"Unexpected error in get_and_process_one_rmq_sms: {e}"); return False
|
||||||
|
|
||||||
|
def clear_rmq_messages(mode='one'):
|
||||||
|
check_env_vars(require_rabbitmq=True)
|
||||||
|
try:
|
||||||
|
with Connection(RABBITMQ_URL) as connection:
|
||||||
|
logger.info(f"Connecting to RMQ to clear messages (mode: {mode})...")
|
||||||
|
channel = connection.channel()
|
||||||
|
alert_queue_obj(channel).declare(passive=True)
|
||||||
|
if mode == 'one':
|
||||||
|
method_frame, _, _ = channel.basic_get(RABBITMQ_ALERTS_QNAME)
|
||||||
|
if method_frame: channel.basic_ack(method_frame.delivery_tag); logger.info("One msg acked.")
|
||||||
|
else: logger.info(f"Queue '{RABBITMQ_ALERTS_QNAME}' empty.")
|
||||||
|
elif mode == 'all':
|
||||||
|
confirm = input("PURGE ALL messages from queue? (yes/no): ").lower()
|
||||||
|
if confirm == 'yes': msg_count = channel.queue_purge(RABBITMQ_ALERTS_QNAME); logger.info(f"Queue purged. Approx {msg_count} msgs removed.")
|
||||||
|
else: logger.info("Purge cancelled.")
|
||||||
|
else: logger.error(f"Invalid clear mode '{mode}'.")
|
||||||
|
channel.close(); return True
|
||||||
|
except (KombuError, OperationalError, ConnectionRefusedError) as e: logger.error(f"Failed to clear RMQ: {e}"); return False
|
||||||
|
except Exception as e: logger.error(f"Unexpected error clearing RMQ: {e}"); return False
|
||||||
|
|
||||||
|
# --- Main Execution ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Messenger Tool: Send SMS/Voice via Telnyx and interact with RabbitMQ.", formatter_class=argparse.RawTextHelpFormatter)
|
||||||
|
subparsers = parser.add_subparsers(dest='command', help='Available actions', required=True)
|
||||||
|
|
||||||
|
# --- Subparser: sms ---
|
||||||
|
parser_sms = subparsers.add_parser('sms', help='Send an SMS directly using Telnyx.')
|
||||||
|
parser_sms.add_argument('--to', required=True, help='Recipient phone number (E.164 format, e.g., +14155552671).')
|
||||||
|
parser_sms.add_argument('--message', required=True, help='The content of the SMS message.')
|
||||||
|
parser_sms.add_argument( # ADDED for SMS
|
||||||
|
'--caller-id-type', choices=['auto', 'alpha', 'numeric'], default='auto',
|
||||||
|
help="Choose SMS caller ID type: 'auto' (numeric for +1, alpha otherwise), 'alpha', or 'numeric'. Default: auto."
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Subparser: voice-tts-call ---
|
||||||
|
parser_voice = subparsers.add_parser('voice-tts-call', help='Initiate a voice call with TTS or audio playback using Telnyx.', description=("Initiates a voice call. Your webhook needs to be configured in Telnyx\nto handle `call.answered` and issue `speak_text` or `play_audio`."))
|
||||||
|
parser_voice.add_argument('--to', default='+14082397258', help='Recipient phone for voice call (default: +14082397258).')
|
||||||
|
parser_voice.add_argument('--message', default='Test message from Telnyx.', help='TTS message for call (default: "Test message from Telnyx."). Sent via X-TTS-Payload.')
|
||||||
|
parser_voice.add_argument('--audio-url', default=None, help='URL of audio file (MP3/WAV) to play. Sent via X-Audio-Url.')
|
||||||
|
parser_voice.add_argument('--connection-id', default=None, help='Override Telnyx Connection ID for the call.')
|
||||||
|
parser_voice.add_argument('--webhook-url', default=None, help='Override webhook URL for call events.')
|
||||||
|
parser_voice.add_argument('--caller-id-type', choices=['auto', 'alpha', 'numeric'], default='auto', help="Choose voice caller ID type: 'auto', 'alpha', or 'numeric'. Default: auto.")
|
||||||
|
parser_voice.add_argument('--amd', choices=['disabled', 'detect', 'detect_beep', 'detect_words', 'greeting_end'], default='disabled', help="Answering Machine Detection mode (default: disabled).")
|
||||||
|
parser_voice.add_argument('--custom-header', action='append', help='Add custom header. Format: NAME=VALUE. Multiple allowed.')
|
||||||
|
|
||||||
|
# --- Subparser: send-rmq ---
|
||||||
|
parser_rmq = subparsers.add_parser('send-rmq', help="Send formatted message to RabbitMQ 'alerts' queue.")
|
||||||
|
parser_rmq.add_argument('--method', choices=['sms', 'email', 'phone'], default='sms', help='Method for consumer (default: sms).')
|
||||||
|
parser_rmq.add_argument('--to', required=True, help='Destination (phone/email).')
|
||||||
|
parser_rmq.add_argument('--body', required=True, help='Main content of message.')
|
||||||
|
parser_rmq.add_argument('--subject', default='Notification', help='Subject line.')
|
||||||
|
parser_rmq.add_argument('--timestamp', default=None, help='ISO 8601 timestamp (optional, defaults to now UTC).')
|
||||||
|
|
||||||
|
# --- Subparser: peek-rmq ---
|
||||||
|
parser_peek = subparsers.add_parser('peek-rmq', help="View messages in 'alerts' queue.")
|
||||||
|
parser_peek.add_argument('--limit', type=int, default=5, help='Max messages to peek (default: 5).')
|
||||||
|
|
||||||
|
# --- Subparser: process-one ---
|
||||||
|
parser_process = subparsers.add_parser('process-one', help="Get message from RMQ, send via Telnyx SMS to specified number, then ACK.")
|
||||||
|
parser_process.add_argument('--to', required=True, help='Phone number (E.164) to send SMS TO.')
|
||||||
|
|
||||||
|
# --- Subparser: clear-rmq ---
|
||||||
|
parser_clear = subparsers.add_parser('clear-rmq', help="Remove messages from 'alerts' queue.")
|
||||||
|
parser_clear.add_argument('--mode', choices=['one', 'all'], required=True, help="'one': next message, 'all': ALL messages (confirm).")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
success = False
|
||||||
|
|
||||||
|
if args.command == 'sms':
|
||||||
|
logger.info("Action: Send Telnyx SMS")
|
||||||
|
success = send_telnyx_sms(recipient_phone=args.to, message_body=args.message, caller_id_type=args.caller_id_type) # Pass new arg
|
||||||
|
elif args.command == 'voice-tts-call':
|
||||||
|
logger.info("Action: Initiate Telnyx Voice Call")
|
||||||
|
success = make_telnyx_voice_call(to_phone=args.to, tts_message=args.message, audio_url=args.audio_url, connection_id_override=args.connection_id, webhook_url_override=args.webhook_url, caller_id_type=args.caller_id_type, amd_mode=args.amd, extra_custom_headers=args.custom_header)
|
||||||
|
elif args.command == 'send-rmq':
|
||||||
|
logger.info("Action: Send Message to RabbitMQ")
|
||||||
|
timestamp = args.timestamp if args.timestamp else datetime.now(timezone.utc).isoformat()
|
||||||
|
payload = {"send_method": args.method, "subject": args.subject, "body": args.body, "timestamp": timestamp, "destination": args.to}
|
||||||
|
success = publish_to_rmq(payload)
|
||||||
|
elif args.command == 'peek-rmq':
|
||||||
|
logger.info("Action: Peek RabbitMQ Messages")
|
||||||
|
peek_messages_rmq(limit=args.limit); success = True
|
||||||
|
elif args.command == 'process-one':
|
||||||
|
logger.info("Action: Process One RMQ Message as Telnyx SMS")
|
||||||
|
success = get_and_process_one_rmq_sms(target_phone=args.to)
|
||||||
|
elif args.command == 'clear-rmq':
|
||||||
|
logger.info(f"Action: Clear RabbitMQ Messages (Mode: {args.mode})")
|
||||||
|
success = clear_rmq_messages(mode=args.mode)
|
||||||
|
else:
|
||||||
|
logger.error(f"Unknown command: {args.command}"); parser.print_help(); sys.exit(1)
|
||||||
|
|
||||||
|
if success: logger.info(f"Command '{args.command}' completed successfully."); sys.exit(0)
|
||||||
|
else: logger.error(f"Command '{args.command}' failed."); sys.exit(1)
|
||||||
60
terms.html
Normal file
60
terms.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Wellnuo Terms and Conditions</title>
|
||||||
|
<style>body { font-family: sans-serif; line-height: 1.6; padding: 20px; } h1, h2 { color: #333; }</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Wellnuo Terms and Conditions</h1>
|
||||||
|
<p><strong>Last Updated:</strong> April 18, 2025</p>
|
||||||
|
|
||||||
|
<h2>1. Agreement to Terms</h2>
|
||||||
|
<p>These Terms and Conditions ("Terms") constitute a legally binding agreement made between you ("you," "user") and Wellnuo ("we," "us," "our") concerning your access to and use of the Wellnuo home monitoring service, including associated hardware, software, mobile applications, and SMS notifications (collectively, the "Service"). By accessing or using the Service, you agree to be bound by these Terms. If you do not agree with all of these Terms, then you are expressly prohibited from using the Service and must discontinue use immediately.</p>
|
||||||
|
|
||||||
|
<h2>2. Service Description</h2>
|
||||||
|
<p>Wellnuo provides a Service utilizing IoT sensors to monitor home environments and generate notifications and alerts based on user-defined parameters and detected events (e.g., presence, potential falls, air quality changes). Alerts may be delivered via the mobile application and/or SMS text messages.</p>
|
||||||
|
|
||||||
|
<h2>3. User Accounts and Responsibilities</h2>
|
||||||
|
<p>You may need to register for an account to use certain features. You agree to provide accurate, current, and complete information during registration and keep it updated. You are responsible for safeguarding your account password and for all activities that occur under your account. You must notify us immediately of any unauthorized use.</p>
|
||||||
|
|
||||||
|
<h2>4. SMS Messaging Terms</h2>
|
||||||
|
<p>By opting into Wellnuo SMS alerts (e.g., by texting the keyword **WELLNUOJOIN** to [Your 10DLC Number], or through account settings when available), you expressly consent to receive automated recurring text messages from Wellnuo at the phone number provided. These messages relate to your account status, system alerts, and notifications based on events detected by your configured sensors.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Consent Not Required for Purchase:</strong> Your consent to receive SMS messages is not a condition of purchasing any goods or services from Wellnuo.</li>
|
||||||
|
<li><strong>Frequency Varies:</strong> Message frequency depends on the number and type of events detected by your sensors and your notification settings.</li>
|
||||||
|
<li><strong>Rates May Apply:</strong> Message and data rates may apply to messages sent and received, as determined by your mobile carrier plan. Wellnuo is not responsible for these charges.</li>
|
||||||
|
<li><strong>Opt-Out:</strong> You can opt-out of receiving SMS messages at any time by replying **STOP** to any message you receive from us. You will receive one final confirmation message confirming your opt-out.</li>
|
||||||
|
<li><strong>Help:</strong> For help regarding SMS messages, reply **HELP** to any message or contact our support team via methods listed below.</li>
|
||||||
|
<li><strong>Supported Carriers:</strong> [Optional: List major supported carriers like AT&T, Verizon, T-Mobile, etc., or state "Supported carriers may vary"]</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>5. Privacy Policy</h2>
|
||||||
|
<p>Your use of the Service is also governed by our Privacy Policy, which is incorporated herein by reference. Please review our Privacy Policy at [Link to your privacy-policy.html] to understand our practices regarding your personal information, including specific rights for California residents.</p>
|
||||||
|
|
||||||
|
<h2>6. Service Limitations and Disclaimers</h2>
|
||||||
|
<p>THE SERVICE IS PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS. WELLNUO MAKES NO WARRANTIES, EXPRESS OR IMPLIED, REGARDING THE SERVICE, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WE DO NOT GUARANTEE THAT THE SERVICE WILL BE UNINTERRUPTED, ERROR-FREE, SECURE, OR THAT ALERTS WILL BE DELIVERED ACCURATELY OR IN A TIMELY MANNER. The accuracy and timeliness of alerts depend on various factors, including sensor functionality, internet connectivity, mobile network availability, and third-party service providers (like SMS gateways).</p>
|
||||||
|
<p>Wellnuo is not a replacement for emergency services (like 911). The Service is intended for informational purposes only and should not be relied upon for life-safety or critical-care monitoring.</p>
|
||||||
|
|
||||||
|
<h2>7. Limitation of Liability</h2>
|
||||||
|
<p>TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL WELLNUO, ITS AFFILIATES, DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE FOR ANY INDIRECT, PUNITIVE, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES, INCLUDING WITHOUT LIMITATION DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA, OR OTHER INTANGIBLE LOSSES, ARISING OUT OF OR RELATING TO THE USE OF, OR INABILITY TO USE, THE SERVICE.</p>
|
||||||
|
<p>[**Note:** Liability limitations need careful legal review, especially regarding consumer services in California.]</p>
|
||||||
|
|
||||||
|
<h2>8. Governing Law and Dispute Resolution</h2>
|
||||||
|
<p>These Terms shall be governed by and construed in accordance with the laws of the State of California, without regard to its conflict of law principles.</p>
|
||||||
|
<p>Any dispute arising out of or relating to these Terms or the Service shall be resolved through binding arbitration administered by [Choose an Arbitration Body, e.g., the American Arbitration Association (AAA)] in accordance with its [Specify Rules, e.g., Consumer Arbitration Rules], conducted in [Specify County, e.g., the county of your office in California], California. YOU AGREE TO WAIVE YOUR RIGHT TO A JURY TRIAL AND TO PARTICIPATE IN CLASS ACTION LAWSUITS OR CLASS-WIDE ARBITRATION.</p>
|
||||||
|
<p>[**Note:** Arbitration clauses and class action waivers require careful legal drafting and review for enforceability.]</p>
|
||||||
|
|
||||||
|
<h2>9. Modifications to Terms</h2>
|
||||||
|
<p>We reserve the right, in our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide reasonable notice prior to any new terms taking effect (e.g., via email or in-app notification). What constitutes a material change will be determined at our sole discretion. By continuing to access or use our Service after those revisions become effective, you agree to be bound by the revised terms.</p>
|
||||||
|
|
||||||
|
<h2>10. Contact Us</h2>
|
||||||
|
<p>If you have any questions about these Terms, please contact us:</p>
|
||||||
|
<ul>
|
||||||
|
<li>By email: bernhard@wellnuo.com</li>
|
||||||
|
<li>By visiting this page on our website: https://www.wellnuo.com/contact</li>
|
||||||
|
<li>By mail: 14585 Big Basin Way, Saratoga, CA, US</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
tst_voice
Normal file
31
tst_voice
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# This script demonstrates how to make a voice call using the Telnyx API.
|
||||||
|
curl -X POST https://api.telnyx.com/v2/calls \
|
||||||
|
-H "Authorization: Bearer KEY0196087A75998434A30FA637CE4FDAFF_ZljGj9KBSAQL0zXx4Sb5eW" \
|
||||||
|
-H 'Accept: application/json' \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"to": "+14082397258",
|
||||||
|
"from": "+16505820706",
|
||||||
|
"connection_id": "2671409623596009055",
|
||||||
|
"custom_headers": [
|
||||||
|
{
|
||||||
|
"name": "X-TTS-Payload",
|
||||||
|
"value": "This is a test call TTS message"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
|
||||||
|
#curl --location 'https://api.telnyx.com/v2/calls' \
|
||||||
|
#--header 'Content-Type: application/json' \
|
||||||
|
#--header 'Accept: application/json' \
|
||||||
|
#--header 'Authorization: Bearer YOUR_API_KEY' \
|
||||||
|
#--data '{
|
||||||
|
# "to": "+15551234567",
|
||||||
|
# "from": "+16505820706",
|
||||||
|
# "connection_id": "YOUR_CONNECTION_ID",
|
||||||
|
# "audio_url": "https://example.com/your-audio-file.mp3",
|
||||||
|
# "webhook_url": "https://your-webhook.url/events"
|
||||||
|
#}'
|
||||||
|
|
||||||
|
|
||||||
4050
well-alerts.py
Normal file
4050
well-alerts.py
Normal file
File diff suppressed because it is too large
Load Diff
347
well-svc-msg.py
Normal file
347
well-svc-msg.py
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import telnyx
|
||||||
|
from sendgrid import SendGridAPIClient
|
||||||
|
from sendgrid.helpers.mail import Mail, Email, To, Content
|
||||||
|
from kombu import Connection, Exchange, Queue # Remove Consumer import here
|
||||||
|
# Import Consumer where it's used or rely on connection.Consumer
|
||||||
|
from kombu.exceptions import KombuError # Import specific exception for connection errors
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# --- Configuration Loading ---
|
||||||
|
# Load environment variables from from $HOME/.env and ./.env files
|
||||||
|
dotenv_path = os.path.join(os.environ.get("HOME", "."), '.env')
|
||||||
|
load_dotenv(dotenv_path=dotenv_path, override=True) # Allow overriding if $HOME/.env exists
|
||||||
|
dotenv_path = os.path.join(os.getcwd(), '.env')
|
||||||
|
load_dotenv(dotenv_path=dotenv_path)
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
TELNYX_API_KEY = os.environ.get("TELNYX_API_KEY")
|
||||||
|
TELNYX_SENDER_ID = os.environ.get("TELNYX_SENDER_ID", "+16505820706")
|
||||||
|
TELNYX_CONNECTION_ID = os.environ.get("TELNYX_CONNECTION_ID") # For Voice/TTS Calls
|
||||||
|
messaging_profile_id = os.environ.get("TELNYX_MESSAGING_PROFILE_ID") # Optional but good practice
|
||||||
|
|
||||||
|
# RabbitMQ Configuration using Kombu URL format
|
||||||
|
# Default to standard local guest user if only hostname 'localhost' is provided
|
||||||
|
raw_rabbitmq_url = os.environ.get("RABBITMQ_URL", "localhost")
|
||||||
|
if raw_rabbitmq_url == "localhost":
|
||||||
|
RABBITMQ_URL = "amqp://guest:guest@localhost:5672//"
|
||||||
|
# If your RabbitMQ needs different user/pass/vhost, provide the full URL
|
||||||
|
# Example: "amqp://user:password@host:port/vhost"
|
||||||
|
else:
|
||||||
|
# If the URL is not localhost, assume it's a full URL
|
||||||
|
RABBITMQ_URL = raw_rabbitmq_url
|
||||||
|
RABBITMQ_ALERTS_QNAME = os.environ.get("RABBITMQ_ALERTS_QNAME", "alerts")
|
||||||
|
|
||||||
|
# Define the exchange (default direct exchange)
|
||||||
|
exchange = Exchange("", type='direct')
|
||||||
|
|
||||||
|
# Define the queue, binding it to the default exchange using its name as routing key
|
||||||
|
# Make sure queue is durable to survive broker restarts
|
||||||
|
alert_queue = Queue(RABBITMQ_ALERTS_QNAME, exchange=exchange, routing_key=RABBITMQ_ALERTS_QNAME, durable=True)
|
||||||
|
|
||||||
|
# SendGrid configuration
|
||||||
|
SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY")
|
||||||
|
SENDGRID_FROM_EMAIL = os.environ.get("SENDGRID_FROM_EMAIL") # Verified sender email
|
||||||
|
|
||||||
|
# --- Setup Logging ---
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||||
|
stream=sys.stdout,
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# --- Log Initial Configuration ---
|
||||||
|
logger.info("=============================================")
|
||||||
|
logger.info(" Initial Service Configuration ")
|
||||||
|
logger.info("---------------------------------------------")
|
||||||
|
logger.info(f"RABBITMQ_URL = {RABBITMQ_URL}")
|
||||||
|
logger.info(f"RABBITMQ_ALERTS_QNAME = {RABBITMQ_ALERTS_QNAME}")
|
||||||
|
logger.info(f"TELNYX_SENDER_ID = {TELNYX_SENDER_ID}")
|
||||||
|
logger.info(f"TELNYX_CONNECTION_ID (TTS) = {TELNYX_CONNECTION_ID if TELNYX_CONNECTION_ID else 'Not Set'}")
|
||||||
|
logger.info(f"SENDGRID_FROM_EMAIL (Email)= {SENDGRID_FROM_EMAIL if SENDGRID_FROM_EMAIL else 'Not Set'}")
|
||||||
|
logger.info(f"TELNYX_API_KEY = {'Set' if TELNYX_API_KEY else 'Not Set'}")
|
||||||
|
logger.info(f"SENDGRID_API_KEY = {'Set' if SENDGRID_API_KEY else 'Not Set'}")
|
||||||
|
logger.info("=============================================")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helper Functions ---
|
||||||
|
|
||||||
|
def check_env_vars():
|
||||||
|
"""Checks if required environment variables are set based on needs."""
|
||||||
|
required_vars = ["TELNYX_API_KEY", "TELNYX_SENDER_ID", "RABBITMQ_URL", "RABBITMQ_ALERTS_QNAME"]
|
||||||
|
missing_vars = [var for var in required_vars if not os.environ.get(var)]
|
||||||
|
|
||||||
|
# Check conditional variables needed for specific methods
|
||||||
|
# Assume we might get *any* type, so check all potential optional vars
|
||||||
|
if not TELNYX_CONNECTION_ID:
|
||||||
|
logger.warning("Optional environment variable TELNYX_CONNECTION_ID is missing (required for phone/TTS)")
|
||||||
|
if not SENDGRID_API_KEY:
|
||||||
|
logger.warning("Optional environment variable SENDGRID_API_KEY is missing (required for email)")
|
||||||
|
if not SENDGRID_FROM_EMAIL:
|
||||||
|
logger.warning("Optional environment variable SENDGRID_FROM_EMAIL is missing (required for email)")
|
||||||
|
|
||||||
|
if any(v in missing_vars for v in ["TELNYX_API_KEY", "TELNYX_SENDER_ID", "RABBITMQ_URL", "RABBITMQ_ALERTS_QNAME"]):
|
||||||
|
logger.error(f"Missing critical environment variables: {', '.join(missing_vars)}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
logger.info("Core environment variables appear to be set.")
|
||||||
|
|
||||||
|
|
||||||
|
def setup_telnyx():
|
||||||
|
"""Configures the Telnyx client."""
|
||||||
|
if not TELNYX_API_KEY:
|
||||||
|
logger.error("Cannot configure Telnyx client: TELNYX_API_KEY not set.")
|
||||||
|
return False
|
||||||
|
telnyx.api_key = TELNYX_API_KEY
|
||||||
|
logger.info("Telnyx client configured.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# --- Sending Functions (Keep these identical) ---
|
||||||
|
|
||||||
|
def send_telnyx_sms(recipient: str, message_body: str) -> bool:
|
||||||
|
"""Sends an SMS using the Telnyx API."""
|
||||||
|
if not recipient:
|
||||||
|
logger.error("Cannot send SMS: Recipient phone number is missing.")
|
||||||
|
return False
|
||||||
|
if not TELNYX_SENDER_ID:
|
||||||
|
logger.error("Cannot send SMS: TELNYX_SENDER_ID is not set.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"Attempting to send SMS from '{TELNYX_SENDER_ID}' to '{recipient}'")
|
||||||
|
try:
|
||||||
|
message_create_params = {
|
||||||
|
"from_": TELNYX_SENDER_ID,
|
||||||
|
"to": recipient,
|
||||||
|
"text": message_body,
|
||||||
|
"messaging_profile_id": messaging_profile_id, # Optional but good practice
|
||||||
|
"type": "sms" # Explicitly set type to SMS
|
||||||
|
}
|
||||||
|
response = telnyx.Message.create(**message_create_params)
|
||||||
|
logger.info(f"SMS submitted successfully to Telnyx. Message ID: {response.id}")
|
||||||
|
return True
|
||||||
|
except telnyx.error.TelnyxError as e:
|
||||||
|
logger.error(f"Telnyx API Error sending SMS to {recipient}: {e}")
|
||||||
|
if hasattr(e, 'http_body') and e.http_body:
|
||||||
|
logger.error(f"Telnyx Error Details: {e.http_body}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error sending SMS to {recipient}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_email(recipient_email: str, subject: str, body: str) -> bool:
|
||||||
|
"""Sends an email using SendGrid."""
|
||||||
|
if not recipient_email:
|
||||||
|
logger.error("Cannot send email: Recipient email address is missing.")
|
||||||
|
return False
|
||||||
|
if not SENDGRID_API_KEY or not SENDGRID_FROM_EMAIL:
|
||||||
|
logger.error("Cannot send email: SENDGRID_API_KEY or SENDGRID_FROM_EMAIL not configured.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"Attempting to send email via SendGrid from '{SENDGRID_FROM_EMAIL}' to '{recipient_email}'")
|
||||||
|
message = Mail(
|
||||||
|
from_email=Email(SENDGRID_FROM_EMAIL), # Use Email object
|
||||||
|
to_emails=To(recipient_email), # Use To object
|
||||||
|
subject=subject if subject else 'Alert Notification', # Default subject
|
||||||
|
plain_text_content=Content("text/plain", body) # Use Content object
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
sg = SendGridAPIClient(SENDGRID_API_KEY)
|
||||||
|
response = sg.send(message)
|
||||||
|
# SendGrid returns 2xx status codes on success
|
||||||
|
if 200 <= response.status_code < 300:
|
||||||
|
logger.info(f"Email submitted successfully to SendGrid for {recipient_email}. Status: {response.status_code}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"SendGrid API Error sending email to {recipient_email}. Status: {response.status_code}, Body: {response.body}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
# Log the full exception details from SendGrid/http client if possible
|
||||||
|
logger.exception(f"Unexpected error sending email via SendGrid to {recipient_email}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def make_telnyx_tts_call(recipient: str, tts_body: str) -> bool:
|
||||||
|
"""Initiates a Telnyx Call Control V2 call to speak TTS."""
|
||||||
|
if not recipient:
|
||||||
|
logger.error("Cannot initiate TTS call: Recipient phone number is missing.")
|
||||||
|
return False
|
||||||
|
if not TELNYX_SENDER_ID:
|
||||||
|
logger.error("Cannot initiate TTS call: TELNYX_SENDER_ID is not set.")
|
||||||
|
return False
|
||||||
|
if not TELNYX_CONNECTION_ID:
|
||||||
|
logger.error("Cannot initiate TTS call: TELNYX_CONNECTION_ID (Call Control Application) is not set.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.info(f"Attempting to initiate TTS call from '{TELNYX_SENDER_ID}' to '{recipient}' via Connection ID '{TELNYX_CONNECTION_ID}'")
|
||||||
|
try:
|
||||||
|
call_request = {
|
||||||
|
"to": recipient,
|
||||||
|
"from_": TELNYX_SENDER_ID,
|
||||||
|
"connection_id": TELNYX_CONNECTION_ID,
|
||||||
|
"custom_headers": [
|
||||||
|
{"name": "X-TTS-Payload", "value": tts_body}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
response = telnyx.Call.create(**call_request)
|
||||||
|
|
||||||
|
logger.info(f"Telnyx call initiation request successful for {recipient}. Call Control ID: {response.call_control_id}, Call Leg ID: {response.call_leg_id}")
|
||||||
|
logger.warning(f"Reminder: A Call Control Application webhook MUST handle call events for Connection ID '{TELNYX_CONNECTION_ID}' and issue the 'speak_text' command using the provided body: '{tts_body[:50]}...'")
|
||||||
|
return True
|
||||||
|
except telnyx.error.TelnyxError as e:
|
||||||
|
logger.error(f"Telnyx API Error initiating call to {recipient}: {e}")
|
||||||
|
if hasattr(e, 'http_body') and e.http_body:
|
||||||
|
logger.error(f"Telnyx Error Details: {e.http_body}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error initiating call to {recipient}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# --- Kombu Callback ---
|
||||||
|
|
||||||
|
def handle_message(body, message):
|
||||||
|
"""Callback function for Kombu to process messages."""
|
||||||
|
delivery_tag = message.delivery_tag
|
||||||
|
logger.info(f"Received message via Kombu (Delivery Tag: {delivery_tag})")
|
||||||
|
try:
|
||||||
|
# Decode JSON payload
|
||||||
|
if isinstance(body, bytes):
|
||||||
|
message_data = json.loads(body.decode('utf-8'))
|
||||||
|
elif isinstance(body, str):
|
||||||
|
message_data = json.loads(body)
|
||||||
|
elif isinstance(body, dict):
|
||||||
|
message_data = body # Already decoded
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Unexpected message body type: {type(body)}")
|
||||||
|
|
||||||
|
logger.debug(f"Decoded message data: {message_data}")
|
||||||
|
|
||||||
|
send_method = message_data.get('send_method')
|
||||||
|
destination = message_data.get('destination')
|
||||||
|
message_body = message_data.get('body')
|
||||||
|
subject = message_data.get('subject', '') # Subject primarily for email
|
||||||
|
|
||||||
|
if not all([send_method, destination, message_body]):
|
||||||
|
logger.error(f"Invalid message format: Missing 'send_method', 'destination', or 'body'. Message: {body}")
|
||||||
|
message.reject(requeue=False) # Reject and discard malformed message
|
||||||
|
logger.warning(f"Message rejected (tag: {delivery_tag}) due to invalid format.")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Processing alert - Method: {send_method}, Destination: {destination[:15]}...")
|
||||||
|
|
||||||
|
success = False
|
||||||
|
if send_method == "sms":
|
||||||
|
success = send_telnyx_sms(recipient=destination, message_body=message_body)
|
||||||
|
elif send_method == "email":
|
||||||
|
# Check if email vars are present before calling
|
||||||
|
if not SENDGRID_API_KEY or not SENDGRID_FROM_EMAIL:
|
||||||
|
logger.error(f"Cannot send email for tag {delivery_tag}: SendGrid config missing.")
|
||||||
|
success = False # Mark as failure for this specific task
|
||||||
|
else:
|
||||||
|
success = send_email(recipient_email=destination, subject=subject, body=message_body)
|
||||||
|
elif send_method == "phone":
|
||||||
|
# Check if TTS var is present before calling
|
||||||
|
if not TELNYX_CONNECTION_ID:
|
||||||
|
logger.error(f"Cannot make TTS call for tag {delivery_tag}: TELNYX_CONNECTION_ID missing.")
|
||||||
|
success = False # Mark as failure
|
||||||
|
else:
|
||||||
|
success = make_telnyx_tts_call(recipient=destination, tts_body=message_body)
|
||||||
|
else:
|
||||||
|
logger.error(f"Unsupported send_method: '{send_method}'. Discarding message (tag: {delivery_tag}).")
|
||||||
|
message.reject(requeue=False) # Reject and discard
|
||||||
|
return
|
||||||
|
|
||||||
|
# Acknowledge or Requeue/Reject based on success
|
||||||
|
if success:
|
||||||
|
logger.info(f"Successfully submitted '{send_method}' task for tag {delivery_tag}. Acknowledging.")
|
||||||
|
message.ack()
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to submit '{send_method}' task for tag {delivery_tag}. Rejecting message (requeue=False).")
|
||||||
|
message.reject(requeue=False)
|
||||||
|
|
||||||
|
except (json.JSONDecodeError, TypeError) as decode_err:
|
||||||
|
logger.error(f"Failed to decode or process message body (Tag: {delivery_tag}): {decode_err}. Body sample: {str(body)[:100]}. Rejecting (requeue=False).")
|
||||||
|
message.reject(requeue=False) # Reject malformed/unexpected message type
|
||||||
|
except Exception as e:
|
||||||
|
# Catch-all for unexpected errors during processing
|
||||||
|
logger.exception(f"Unexpected error processing message (Tag: {delivery_tag}): {e}. Rejecting message (requeue=False).")
|
||||||
|
try:
|
||||||
|
message.reject(requeue=False) # Try to reject even after an exception
|
||||||
|
except Exception as reject_e:
|
||||||
|
logger.error(f"Further error trying to reject message (Tag: {delivery_tag}): {reject_e}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Main Service Logic using Kombu (Corrected) ---
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to start the service using Kombu."""
|
||||||
|
logger.info("Starting Alert Service using Kombu...")
|
||||||
|
check_env_vars()
|
||||||
|
if not setup_telnyx():
|
||||||
|
sys.exit("Failed to configure Telnyx client. Exiting.")
|
||||||
|
|
||||||
|
logger.info(f"Attempting to connect to RabbitMQ via Kombu at {RABBITMQ_URL}")
|
||||||
|
|
||||||
|
# Connection loop
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Establish connection using a context manager
|
||||||
|
with Connection(RABBITMQ_URL, connect_timeout=10, heartbeat=60) as connection:
|
||||||
|
logger.info("Kombu connection established.")
|
||||||
|
|
||||||
|
# *** FIX: Create the Consumer *using* the connection object ***
|
||||||
|
# This ensures the consumer is properly bound to the connection/channel.
|
||||||
|
consumer = connection.Consumer(
|
||||||
|
queues=[alert_queue], # List of queues to consume from
|
||||||
|
callbacks=[handle_message], # List of callbacks
|
||||||
|
accept=['json', 'text/plain'],# Define acceptable content-types
|
||||||
|
prefetch_count=1 # Process one message at a time
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start consuming messages. This declares queues, sets QoS etc.
|
||||||
|
consumer.consume()
|
||||||
|
logger.info(f"Kombu consumer started. Waiting for messages on '{RABBITMQ_ALERTS_QNAME}'. To exit press CTRL+C")
|
||||||
|
|
||||||
|
# Inner loop to process messages using drain_events
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Process messages indefinitely, checking for events every second
|
||||||
|
connection.drain_events(timeout=1)
|
||||||
|
except socket.timeout:
|
||||||
|
# No events within the timeout, connection likely still fine. Continue loop.
|
||||||
|
# You could add a check here connection.connected if needed.
|
||||||
|
pass
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("KeyboardInterrupt received during drain_events. Shutting down...")
|
||||||
|
consumer.cancel() # Stop consuming
|
||||||
|
logger.info("Kombu consumer cancelled.")
|
||||||
|
# Exit the inner loop to allow the 'with Connection' block to close
|
||||||
|
raise # Re-raise KeyboardInterrupt to exit outer loop/program
|
||||||
|
except (KombuError, ConnectionResetError, ConnectionAbortedError) as e: # Catch specific connection/channel errors
|
||||||
|
logger.error(f"Kombu connection/channel error during drain_events: {e}. Breaking inner loop to reconnect...")
|
||||||
|
consumer.cancel() # Try to cancel consumer cleanly
|
||||||
|
break # Exit inner loop to force reconnection via outer loop
|
||||||
|
except Exception as e: # Catch other unexpected errors in consumer loop
|
||||||
|
logger.exception(f"Unexpected error in Kombu drain_events loop: {e}. Breaking inner loop to reconnect...")
|
||||||
|
consumer.cancel() # Try to cancel consumer cleanly
|
||||||
|
break # Exit inner loop
|
||||||
|
|
||||||
|
# Handle outer loop exceptions (connection failures, final KeyboardInterrupt)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("KeyboardInterrupt received during connection attempt or after exit from inner loop. Exiting.")
|
||||||
|
break # Exit the main while loop
|
||||||
|
except (ConnectionRefusedError, KombuError, Exception) as conn_err:
|
||||||
|
logger.error(f"Failed to connect or catastrophic failure ({type(conn_err).__name__}): {conn_err}. Retrying in 10 seconds...")
|
||||||
|
# Consumer is already cancelled or wasn't created if connection failed initially
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
logger.info("Alert Service stopped.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
147
well-svc-webhook.py
Normal file
147
well-svc-webhook.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from flask import Flask, request, jsonify, Response
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from dotenv import load_dotenv # Import load_dotenv
|
||||||
|
|
||||||
|
# --- Configuration Loading ---
|
||||||
|
# Load environment variables from from $HOME/.env and ./.env files
|
||||||
|
# $HOME/.env allows for user-specific settings, ./.env for project-specific settings.
|
||||||
|
# Values in ./.env might override $HOME/.env if load_dotenv is called again without override=False
|
||||||
|
# or if the variable exists in both and override=True is used on the second call.
|
||||||
|
# Current approach: Load $HOME/.env first, then load ./.env allowing ./.env to add or override.
|
||||||
|
logging.info("Loading environment variables...")
|
||||||
|
home_dotenv_path = os.path.join(os.environ.get("HOME", "."), '.env')
|
||||||
|
project_dotenv_path = os.path.join(os.getcwd(), '.env')
|
||||||
|
|
||||||
|
if os.path.exists(home_dotenv_path):
|
||||||
|
logging.info(f"Loading environment variables from: {home_dotenv_path}")
|
||||||
|
load_dotenv(dotenv_path=home_dotenv_path)
|
||||||
|
else:
|
||||||
|
logging.info(f"Optional environment file not found: {home_dotenv_path}")
|
||||||
|
|
||||||
|
if os.path.exists(project_dotenv_path):
|
||||||
|
logging.info(f"Loading environment variables from: {project_dotenv_path}")
|
||||||
|
# load_dotenv will not override existing variables by default.
|
||||||
|
# Use override=True if you want ./.env to forcefully replace variables from $HOME/.env
|
||||||
|
load_dotenv(dotenv_path=project_dotenv_path, override=True)
|
||||||
|
else:
|
||||||
|
logging.info(f"Project environment file not found: {project_dotenv_path}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
# Now loaded from environment variables defined in the .env files
|
||||||
|
TELNYX_API_KEY = os.environ.get("TELNYX_API_KEY") # Used for making API calls (e.g., sending messages)
|
||||||
|
TELNYX_SENDER_ID = os.environ.get("TELNYX_SENDER_ID") # Default 'from' number if sending messages via API
|
||||||
|
TELNYX_CONNECTION_ID = os.environ.get("TELNYX_CONNECTION_ID") # Used for Voice/TTS Calls via API
|
||||||
|
TELNYX_MESSAGING_PROFILE_ID = os.environ.get("TELNYX_MESSAGING_PROFILE_ID") # Associated messaging profile
|
||||||
|
TELNYX_PUBLIC_KEY = os.environ.get("TELNYX_PUBLIC_KEY") # REQUIRED if enabling signature validation
|
||||||
|
|
||||||
|
# --- Other Configurations ---
|
||||||
|
# Keyword can also be moved to .env if preferred
|
||||||
|
OPT_IN_KEYWORD = os.environ.get("OPT_IN_KEYWORD", "WELLNUOJOIN") # Load from env or default
|
||||||
|
|
||||||
|
# Configure logging (after potentially loading log level from env)
|
||||||
|
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
|
||||||
|
logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stdout) # Log to stdout for containers/systemd
|
||||||
|
|
||||||
|
|
||||||
|
# --- Verification ---
|
||||||
|
if not TELNYX_PUBLIC_KEY:
|
||||||
|
logging.warning("TELNYX_PUBLIC_KEY is not set in environment variables. Signature validation will be skipped.")
|
||||||
|
# Add checks for other essential variables if needed for specific functions
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# --- Database Interaction Placeholder ---
|
||||||
|
# (Keep your existing database functions here)
|
||||||
|
def add_opted_in_number(phone_number):
|
||||||
|
logging.info(f"Adding phone number to opt-in list: {phone_number}")
|
||||||
|
print(f"DATABASE STUB: Adding {phone_number} to opt-ins.")
|
||||||
|
# Replace with your actual DB logic
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_opted_in(phone_number):
|
||||||
|
logging.info(f"Checking opt-in status for: {phone_number}")
|
||||||
|
print(f"DATABASE STUB: Checking if {phone_number} is opted in.")
|
||||||
|
# Replace with your actual DB logic
|
||||||
|
return False
|
||||||
|
# --------------------------------------
|
||||||
|
|
||||||
|
@app.route('/webhook', methods=['POST'])
|
||||||
|
def telnyx_webhook():
|
||||||
|
"""Handles incoming webhooks from Telnyx."""
|
||||||
|
logging.debug("Webhook received") # Use debug level for noisy logs
|
||||||
|
|
||||||
|
# --- Telnyx Signature Validation (Using loaded TELNYX_PUBLIC_KEY) ---
|
||||||
|
# UNCOMMENT this section if you have set TELNYX_PUBLIC_KEY in your .env file
|
||||||
|
# and installed the 'telnyx' library.
|
||||||
|
# ---
|
||||||
|
# if not TELNYX_PUBLIC_KEY:
|
||||||
|
# logging.warning("Skipping signature validation: TELNYX_PUBLIC_KEY not configured.")
|
||||||
|
# else:
|
||||||
|
# signature = request.headers.get("Telnyx-Signature-Ed25519")
|
||||||
|
# timestamp = request.headers.get("Telnyx-Timestamp")
|
||||||
|
# raw_payload = request.data # Get raw bytes
|
||||||
|
# if not signature or not timestamp:
|
||||||
|
# logging.warning("Missing Telnyx signature headers")
|
||||||
|
# return jsonify({"error": "Missing signature headers"}), 400
|
||||||
|
# try:
|
||||||
|
# # Requires 'telnyx' library: pip install telnyx
|
||||||
|
# from telnyx.webhook import Webhook
|
||||||
|
# Webhook.construct_event(raw_payload, signature, timestamp, TELNYX_PUBLIC_KEY, tolerance=600) # 10 min tolerance
|
||||||
|
# logging.info("Telnyx signature validated successfully.")
|
||||||
|
# except Exception as e:
|
||||||
|
# logging.error(f"Invalid Telnyx signature: {e}")
|
||||||
|
# return jsonify({"error": "Invalid signature"}), 401
|
||||||
|
# --- End Signature Validation Section ---
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = request.json
|
||||||
|
# Log the full payload only at DEBUG level to avoid excessive logging
|
||||||
|
logging.debug(f"Payload received: {json.dumps(payload, indent=2)}")
|
||||||
|
|
||||||
|
event_type = payload.get('data', {}).get('event_type')
|
||||||
|
record_type = payload.get('data', {}).get('record_type')
|
||||||
|
|
||||||
|
if event_type == 'message.received' and record_type == 'message':
|
||||||
|
message_payload = payload.get('data', {}).get('payload', {})
|
||||||
|
direction = message_payload.get('direction')
|
||||||
|
|
||||||
|
if direction == 'inbound':
|
||||||
|
from_number = message_payload.get('from', {}).get('phone_number')
|
||||||
|
# Use the OPT_IN_KEYWORD loaded from env or default
|
||||||
|
message_body = message_payload.get('text', '').strip().upper()
|
||||||
|
|
||||||
|
logging.info(f"Received inbound message from {from_number}: '{message_body[:50]}'...") # Log truncated message
|
||||||
|
|
||||||
|
if message_body == OPT_IN_KEYWORD:
|
||||||
|
logging.info(f"Opt-in keyword '{OPT_IN_KEYWORD}' detected from {from_number}")
|
||||||
|
if not is_opted_in(from_number):
|
||||||
|
if add_opted_in_number(from_number):
|
||||||
|
logging.info(f"Successfully processed opt-in for {from_number}")
|
||||||
|
else:
|
||||||
|
logging.error(f"Failed to add {from_number} to opt-in database.")
|
||||||
|
else:
|
||||||
|
logging.info(f"{from_number} is already opted in.")
|
||||||
|
else:
|
||||||
|
logging.info(f"Message from {from_number} did not match opt-in keyword.")
|
||||||
|
else:
|
||||||
|
logging.debug(f"Ignoring non-inbound message: direction={direction}") # Use debug for less important ignores
|
||||||
|
else:
|
||||||
|
logging.debug(f"Ignoring non-message.received event: type={event_type}, record={record_type}") # Use debug
|
||||||
|
|
||||||
|
# Acknowledge receipt to Telnyx
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Error processing webhook") # Logs the full exception traceback
|
||||||
|
return jsonify({"error": "Internal server error"}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.info("Starting Flask development server for local testing...")
|
||||||
|
# Bind to 0.0.0.0 to be accessible externally if needed for local testing with ngrok/similar
|
||||||
|
app.run(host='0.0.0.0', port=8002, debug=True) # Debug=True is NOT for production
|
||||||
49
well-svc-whookSMSrcv.py
Normal file
49
well-svc-whookSMSrcv.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from flask import Flask, request, Response
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Configure basic logging to print to console
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stdout)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
listen_port = 1998 # The port specified
|
||||||
|
|
||||||
|
@app.route('/sms_receive_test', methods=['POST']) # Define the path Telnyx will call
|
||||||
|
def handle_incoming_sms():
|
||||||
|
"""Listens for and logs incoming webhook POST requests."""
|
||||||
|
logging.info(f"Received request on /sms_receive_test from {request.remote_addr}")
|
||||||
|
try:
|
||||||
|
# Check if the content type is JSON, though Telnyx should always send JSON
|
||||||
|
if request.is_json:
|
||||||
|
payload = request.get_json()
|
||||||
|
logging.info("Received JSON payload:")
|
||||||
|
# Log the received payload nicely formatted
|
||||||
|
logging.info(json.dumps(payload, indent=2))
|
||||||
|
|
||||||
|
# --- TODO: Add logic here if needed (e.g., save to DB) ---
|
||||||
|
# For now, just logging is sufficient proof of receipt.
|
||||||
|
|
||||||
|
# Acknowledge receipt to Telnyx with 204 No Content
|
||||||
|
return Response(status=204)
|
||||||
|
else:
|
||||||
|
# Log if unexpected content type is received
|
||||||
|
logging.warning(f"Received non-JSON request. Content-Type: {request.content_type}")
|
||||||
|
# Still acknowledge, but maybe log the raw data if needed
|
||||||
|
# raw_data = request.get_data(as_text=True)
|
||||||
|
# logging.info(f"Raw data: {raw_data}")
|
||||||
|
return Response(status=204) # Acknowledge anyway
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception("Error processing incoming webhook")
|
||||||
|
# Return 500 Internal Server Error to Telnyx if processing fails
|
||||||
|
# Telnyx *might* retry the webhook later in case of 5xx errors.
|
||||||
|
return Response(status=500)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.info(f"Starting simple webhook listener on port {listen_port}...")
|
||||||
|
# Run on 0.0.0.0 to be accessible externally (within firewall limits)
|
||||||
|
# DO NOT use debug=True in production environments
|
||||||
|
app.run(host='0.0.0.0', port=listen_port, debug=False)
|
||||||
Loading…
x
Reference in New Issue
Block a user