yeastar-voipms-webhook-proxy/routes/yeastar.py
2025-07-13 22:10:11 -04:00

121 lines
No EOL
3.8 KiB
Python

from flask import Blueprint, request, jsonify
import requests
import xml.etree.ElementTree as ET
from datetime import datetime
from config import VOIPMS_ENDPOINT, ENDPOINT_OBSCURITY
from routes.helpers import generate_message_id, parse_bearer_token, build_soap_envelope
bp = Blueprint('yeastar', __name__)
@bp.route(f"/{ENDPOINT_OBSCURITY}/yeastar-outbound", methods=['POST'])
@bp.route(f"/{ENDPOINT_OBSCURITY}/yeastar-outbound/verbose", methods=['POST'])
def handle_yeastar_outbound():
verbose = request.path.endswith('/verbose')
ip = request.headers.get('X-Forwarded-For', request.remote_addr)
try:
data = request.get_json(force=True)
except Exception:
return jsonify({
"errors": [{
"code": "10002",
"title": "Invalid JSON",
"detail": "The request body must be valid JSON."
}]
}), 400
from_number = data.get('from')
to_number = data.get('to')
text = data.get('text', '')
media_urls = data.get('media_urls', [])
is_mms = len(media_urls) > 0 or len(text) > 160
if not media_urls and len(text) > 160:
is_mms = True
auth_header = request.headers.get('Authorization', '')
username, password = parse_bearer_token(auth_header)
if not username or not password:
return jsonify({
"errors": [{
"code": "10004",
"title": "Invalid Authorization",
"detail": "Bearer token is missing or invalid."
}]
}), 400
envelope = build_soap_envelope(username, password, from_number, to_number, text, media_urls if is_mms else [])
if verbose:
print(f"===== Yeastar Verbose Request =====")
print(f"IP: {ip}")
print(f"Type: {'MMS' if is_mms else 'SMS'}")
print(f"From: {from_number} ? To: {to_number}")
print(f"Text: {text}")
print(f"Media: {media_urls}")
print(f"Envelope:\n{envelope}")
print("==============================")
response = requests.post(
VOIPMS_ENDPOINT,
data=envelope.encode('utf-8'),
headers={'Content-Type': 'text/xml'}
)
if verbose:
print(f"SOAP Raw Response: {response.text}")
# Robust SOAP status parse
status = None
try:
root = ET.fromstring(response.text)
for item in root.iter():
if item.tag.endswith('item'):
key = item.find('./key')
if key is not None and key.text == 'status':
value = item.find('./value')
if value is not None:
status = value.text
break
except Exception as e:
if verbose:
print(f"Failed to parse SOAP response: {e}")
# === Determine pass/fail ===
result = "PASS" if status == "success" else "FAIL"
if verbose:
print(f"Parsed SOAP Status: {status}")
print(f"Result: {result}")
msg_id = generate_message_id()
# Basic always-on log
print(f"[{datetime.now().isoformat()}] [{result}] IP:{ip} From Yeastar | {'MMS' if is_mms else 'SMS'} | From:{from_number} ? To:{to_number} | Images:{len(media_urls)}")
# === Error code mapping ===
error_code_map = {
"invalid_number": "10001",
"invalid_param": "10002",
"unsupported_media": "10003",
"auth_fail": "10004",
"no_permission": "10005",
"too_many_requests": "10006",
"service_unavailable": "10007",
"media_toolong": "10008",
}
if status == "success":
return jsonify({"data": {"id": msg_id}}), 200
else:
code = error_code_map.get(status, "10002")
return jsonify({
"errors": [{
"code": code,
"title": "Message Failed",
"detail": f"VOIP.ms returned status: {status or 'unknown'}"
}]
}), 400