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