Removed helpers to make routes self contained.
This commit is contained in:
parent
01b2299472
commit
e7786593e1
4 changed files with 94 additions and 103 deletions
|
|
@ -1,99 +0,0 @@
|
||||||
import random, time
|
|
||||||
import phonenumbers
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from config import YEASTAR_WEBHOOK_URL, YEASTAR_SECRET
|
|
||||||
|
|
||||||
def generate_signature(body: str) -> str:
|
|
||||||
signature = hmac.new(YEASTAR_SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
|
|
||||||
return f"sha256={signature}"
|
|
||||||
|
|
||||||
def to_e164(number):
|
|
||||||
try:
|
|
||||||
parsed = phonenumbers.parse(number, "US")
|
|
||||||
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(f"Failed to normalize phone number {number}: {e}")
|
|
||||||
return number
|
|
||||||
|
|
||||||
def create_yeastar_payload(data):
|
|
||||||
now_iso = datetime.now().astimezone().isoformat()
|
|
||||||
message_id = str(int(datetime.utcnow().timestamp()))
|
|
||||||
payload = {
|
|
||||||
"data": {
|
|
||||||
"event_type": "message.received",
|
|
||||||
"payload": {
|
|
||||||
"id": message_id,
|
|
||||||
"from": {
|
|
||||||
"phone_number": data["from"]
|
|
||||||
},
|
|
||||||
"to": [
|
|
||||||
{"phone_number": num} for num in data["to"]
|
|
||||||
],
|
|
||||||
"text": data["text"],
|
|
||||||
"record_type": "message",
|
|
||||||
"received_at": now_iso
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
media_urls = data.get("media_urls", [])
|
|
||||||
if media_urls:
|
|
||||||
payload["data"]["payload"].pop("text", None)
|
|
||||||
payload["data"]["payload"]["media"] = [{"url": url} for url in media_urls]
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
# routes/voipms.py
|
# routes/voipms.py
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import hmac, hashlib
|
||||||
|
import phonenumbers
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from config import YEASTAR_WEBHOOK_URL, YEASTAR_SECRET, ENDPOINT_OBSCURITY
|
from config import YEASTAR_WEBHOOK_URL, YEASTAR_SECRET, ENDPOINT_OBSCURITY
|
||||||
from routes.helpers import to_e164, create_yeastar_payload, generate_signature
|
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
@ -84,3 +85,43 @@ def voipms_inbound():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[{request_id}] Error in voipms_inbound: {e}", exc_info=True)
|
logging.error(f"[{request_id}] Error in voipms_inbound: {e}", exc_info=True)
|
||||||
return jsonify({"error": "internal server error"}), 500
|
return jsonify({"error": "internal server error"}), 500
|
||||||
|
|
||||||
|
def generate_signature(body: str) -> str:
|
||||||
|
signature = hmac.new(YEASTAR_SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
|
||||||
|
return f"sha256={signature}"
|
||||||
|
|
||||||
|
def to_e164(number):
|
||||||
|
try:
|
||||||
|
parsed = phonenumbers.parse(number, "US")
|
||||||
|
return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Failed to normalize phone number {number}: {e}")
|
||||||
|
return number
|
||||||
|
|
||||||
|
def create_yeastar_payload(data):
|
||||||
|
now_iso = datetime.now().astimezone().isoformat()
|
||||||
|
message_id = str(int(datetime.utcnow().timestamp()))
|
||||||
|
payload = {
|
||||||
|
"data": {
|
||||||
|
"event_type": "message.received",
|
||||||
|
"payload": {
|
||||||
|
"id": message_id,
|
||||||
|
"from": {
|
||||||
|
"phone_number": data["from"]
|
||||||
|
},
|
||||||
|
"to": [
|
||||||
|
{"phone_number": num} for num in data["to"]
|
||||||
|
],
|
||||||
|
"text": data["text"],
|
||||||
|
"record_type": "message",
|
||||||
|
"received_at": now_iso
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
media_urls = data.get("media_urls", [])
|
||||||
|
if media_urls:
|
||||||
|
payload["data"]["payload"].pop("text", None)
|
||||||
|
payload["data"]["payload"]["media"] = [{"url": url} for url in media_urls]
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
|
import random, time
|
||||||
import requests
|
import requests
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from config import VOIPMS_ENDPOINT, ENDPOINT_OBSCURITY
|
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 = Blueprint('yeastar', __name__)
|
||||||
|
|
||||||
|
|
@ -119,3 +119,53 @@ def handle_yeastar_outbound():
|
||||||
"detail": f"VOIP.ms returned status: {status or 'unknown'}"
|
"detail": f"VOIP.ms returned status: {status or 'unknown'}"
|
||||||
}]
|
}]
|
||||||
}), 400
|
}), 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()
|
||||||
|
|
@ -27,7 +27,6 @@
|
||||||
<Compile Include="app.py" />
|
<Compile Include="app.py" />
|
||||||
<Compile Include="config.py" />
|
<Compile Include="config.py" />
|
||||||
<Compile Include="config.template.py" />
|
<Compile Include="config.template.py" />
|
||||||
<Compile Include="routes\helpers.py" />
|
|
||||||
<Compile Include="routes\threecx.py" />
|
<Compile Include="routes\threecx.py" />
|
||||||
<Compile Include="routes\voipms.py" />
|
<Compile Include="routes\voipms.py" />
|
||||||
<Compile Include="routes\yeastar.py" />
|
<Compile Include="routes\yeastar.py" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue