171 lines
No EOL
5.3 KiB
Python
171 lines
No EOL
5.3 KiB
Python
from flask import Blueprint, request, jsonify
|
|
import random, time
|
|
import requests
|
|
import xml.etree.ElementTree as ET
|
|
from datetime import datetime
|
|
|
|
from config import VOIPMS_ENDPOINT, ENDPOINT_OBSCURITY
|
|
|
|
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
|
|
|
|
def generate_message_id():
|
|
return f"{random.randint(100000, 999999)}{int(time.time())}"
|
|
|
|
def parse_bearer_token(auth_header):
|
|
"""
|
|
Expect: Authorization: Bearer username|||password
|
|
"""
|
|
if not auth_header.startswith("Bearer "):
|
|
return None, None
|
|
token = auth_header.split("Bearer ")[1].strip()
|
|
parts = token.split("|||")
|
|
if len(parts) == 2:
|
|
return parts[0], parts[1]
|
|
return None, None
|
|
|
|
def build_soap_envelope(
|
|
username, password, from_number, to_number,
|
|
message_text, media_urls=None
|
|
):
|
|
media_urls = media_urls or []
|
|
is_mms = len(media_urls) > 0
|
|
soap_method = "sendMMS" if is_mms else "sendSMS"
|
|
|
|
envelope = f"""
|
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
|
xmlns:wsd="https://voip.ms/api/wsdl"
|
|
xmlns:xsd1="https://voip.ms/api/schema">
|
|
<soapenv:Header/>
|
|
<soapenv:Body>
|
|
<wsd:{soap_method}>
|
|
<wsd:params>
|
|
<xsd1:api_username>{username}</xsd1:api_username>
|
|
<xsd1:api_password>{password}</xsd1:api_password>
|
|
<xsd1:did>{from_number}</xsd1:did>
|
|
<xsd1:dst>{to_number}</xsd1:dst>
|
|
<xsd1:message>{message_text}</xsd1:message>
|
|
"""
|
|
|
|
# Add media tags if MMS, max 3 allowed
|
|
if is_mms:
|
|
for i, url in enumerate(media_urls[:3]):
|
|
envelope += f" <xsd1:media{i+1}>{url}</xsd1:media{i+1}>\n"
|
|
|
|
envelope += f""" </wsd:params>
|
|
</wsd:{soap_method}>
|
|
</soapenv:Body>
|
|
</soapenv:Envelope>"""
|
|
|
|
return envelope.strip() |