Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- from flask import Flask, request, Response, jsonify, render_template, send_from_directory
- from twilio.twiml.voice_response import VoiceResponse
- from twilio.rest import Client
- from twilio.jwt.access_token import AccessToken
- from twilio.jwt.access_token.grants import VoiceGrant
- import os
- import base64
- import json
- from dotenv import load_dotenv
- load_dotenv()
- app = Flask(__name__, template_folder="templates")
- # ─── Twilio ENV ────────────────────────────────────────────────────────────────
- TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
- TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
- TWILIO_API_KEY = os.getenv("TWILIO_API_KEY_SID")
- TWILIO_API_SECRET = os.getenv("TWILIO_API_KEY_SECRET")
- TWILIO_TWIML_APP_SID = os.getenv("TWILIO_TWIML_APP_SID")
- TWILIO_CLIENT_IDENTITY = os.getenv("TWILIO_CLIENT_IDENTITY", "browser")
- TWILIO_NUMBER = os.getenv("TWILIO_NUMBER") or os.getenv("TWILIO_PHONE_NUMBER")
- twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
- def _b64url_decode(data):
- padding = '=' * (-len(data) % 4)
- return base64.urlsafe_b64decode(data + padding)
- def decode_jwt_noverify(token):
- """Decode JWT header/payload without verification (debug only)."""
- try:
- parts = token.split('.')
- if len(parts) != 3:
- return {"error": "not a jwt"}
- header = json.loads(_b64url_decode(parts[0]).decode('utf-8'))
- payload = json.loads(_b64url_decode(parts[1]).decode('utf-8'))
- return {"header": header, "payload": payload}
- except Exception as exc: # debug only
- return {"error": str(exc)}
- # ─── Routes ────────────────────────────────────────────────────────────────────
- @app.route("/")
- def index():
- return render_template("client-example.html")
- # ───────────────────────────────────────────────────────────────────────────────
- # TOKEN (Voice SDK ONLY — no legacy)
- # ───────────────────────────────────────────────────────────────────────────────
- def build_token(identity):
- ttl_seconds = 3600
- access_token = AccessToken(
- account_sid = TWILIO_ACCOUNT_SID,
- signing_key_sid = TWILIO_API_KEY,
- secret =TWILIO_API_SECRET,
- identity=identity,
- ttl=ttl_seconds
- )
- voice_grant = VoiceGrant(
- outgoing_application_sid=TWILIO_TWIML_APP_SID,
- incoming_allow=True,
- )
- access_token.add_grant(voice_grant)
- jwt = access_token.to_jwt()
- if isinstance(jwt, bytes):
- jwt = jwt.decode("utf-8")
- return jwt
- @app.route("/token", methods=["GET"])
- def token():
- identity = request.args.get("identity", TWILIO_CLIENT_IDENTITY)
- jwt = build_token(identity)
- print(f"Issued AccessToken for {identity}")
- return jsonify({
- "token": jwt,
- "identity": identity,
- "type": "access",
- })
- @app.route("/token_raw", methods=["GET"])
- def token_raw():
- """Debug endpoint: returns token plus decoded header/payload (no verify)."""
- identity = request.args.get("identity", TWILIO_CLIENT_IDENTITY)
- jwt = build_token(identity)
- decoded = decode_jwt_noverify(jwt)
- token_resp = {
- "token": jwt,
- "identity": identity,
- "type": "access",
- }
- token_resp.update({
- "decoded": decoded,
- "config": {
- "account_sid": TWILIO_ACCOUNT_SID,
- "api_key": TWILIO_API_KEY,
- "twiml_app_sid": TWILIO_TWIML_APP_SID,
- "identity": identity,
- }
- })
- return jsonify(token_resp)
- # ───────────────────────────────────────────────────────────────────────────────
- # INCOMING CALL HANDLER (PSTN → Browser)
- # ───────────────────────────────────────────────────────────────────────────────
- @app.route("/voice", methods=["POST"])
- def voice():
- print("Voice request:", dict(request.form))
- to = request.form.get("To")
- response = VoiceResponse()
- # If the call is NOT coming from a client, it's an incoming PSTN call to our number
- # We want to route it to our browser client
- if not request.form.get("From", "").startswith("client:"):
- dial = response.dial(
- timeout=30,
- answer_on_bridge=True,
- action="/call-status",
- )
- dial.client(TWILIO_CLIENT_IDENTITY)
- # If there is a 'To' number provided, it's an outgoing call from our client
- elif to:
- dial = response.dial(caller_id=TWILIO_NUMBER)
- # Check if we are calling a client or a number
- if to.startswith("client:"):
- dial.client(to.replace("client:", ""))
- else:
- dial.number(to)
- else:
- response.say("No destination provided.")
- return Response(str(response), mimetype="text/xml")
- @app.route("/call-status", methods=["POST"])
- def call_status():
- print("📡 Call status:", dict(request.form))
- # Twilio expects valid TwiML; return an empty <Response/> to avoid 12100 parse errors.
- vr = VoiceResponse()
- return Response(str(vr), mimetype="text/xml")
- # ───────────────────────────────────────────────────────────────────────────────
- # DEBUG ROUTES
- # ───────────────────────────────────────────────────────────────────────────────
- @app.route("/env_check")
- def env_check():
- def mask(v):
- if not v:
- return None
- return v[:2] + "..." + v[-2:]
- return jsonify({
- "TWILIO_ACCOUNT_SID": mask(TWILIO_ACCOUNT_SID),
- "TWILIO_AUTH_TOKEN": mask(TWILIO_AUTH_TOKEN),
- "TWILIO_API_KEY": mask(TWILIO_API_KEY),
- "TWILIO_API_SECRET": mask(TWILIO_API_SECRET),
- "TWILIO_TWIML_APP_SID": mask(TWILIO_TWIML_APP_SID),
- "TWILIO_PHONE_NUMBER": TWILIO_NUMBER,
- "TWILIO_CLIENT_IDENTITY": TWILIO_CLIENT_IDENTITY,
- })
- # ───────────────────────────────────────────────────────────────────────────────
- # LOCAL SDK SERVING (NO CDN)
- # ───────────────────────────────────────────────────────────────────────────────
- @app.route("/static/vendor/twilio-voice.min.js")
- def twilio_sdk():
- base = os.path.dirname(os.path.abspath(__file__))
- vendor = os.path.join(base, "src", "static", "vendor")
- return send_from_directory(
- vendor,
- "twilio-voice.min.js",
- mimetype="application/javascript",
- )
- @app.route("/send_link", methods=["POST"])
- def send_link():
- data = request.get_json(silent=True) or {}
- recipient = data.get("phone")
- print(f"[send_link] placeholder send to: {recipient}")
- return jsonify({"status": "ok", "recipient": recipient})
- # ───────────────────────────────────────────────────────────────────────────────
- # OPERATOR DASHBOARD API ENDPOINTS
- # ───────────────────────────────────────────────────────────────────────────────
- @app.route("/api/transcript", methods=["POST"])
- def receive_transcript():
- """Receive real-time transcription from caller session"""
- data = request.get_json(silent=True) or {}
- call_sid = data.get("call_sid")
- speaker = data.get("speaker", "Caller")
- text = data.get("text")
- confidence = data.get("confidence")
- print(f"[Transcript] {call_sid} - {speaker}: {text}")
- # In production: store in database, broadcast via WebSocket, etc.
- return jsonify({
- "status": "ok",
- "timestamp": data.get("timestamp"),
- "speaker": speaker
- })
- @app.route("/api/location", methods=["POST"])
- def receive_location():
- """Receive GPS location from caller session"""
- data = request.get_json(silent=True) or {}
- call_sid = data.get("call_sid")
- lat = data.get("lat")
- lng = data.get("lng")
- accuracy = data.get("accuracy")
- address = data.get("address")
- print(f"[Location] {call_sid} - ({lat}, {lng}) - {address}")
- # In production: store in database, geocode if needed, update incident record
- return jsonify({
- "status": "ok",
- "lat": lat,
- "lng": lng,
- "address": address
- })
- @app.route("/api/photo", methods=["POST"])
- def receive_photo():
- """Receive photo upload from caller session"""
- call_sid = request.form.get("call_sid")
- if "photo" not in request.files:
- return jsonify({"error": "No photo provided"}), 400
- photo = request.files["photo"]
- if photo.filename == "":
- return jsonify({"error": "Empty filename"}), 400
- # In production: save to storage (S3, local disk, etc.), create thumbnail
- # For now, just acknowledge receipt
- print(f"[Photo] {call_sid} - Received photo: {photo.filename} ({photo.content_type})")
- # Simulate storing and returning a URL
- photo_url = f"/uploads/{call_sid}/{photo.filename}"
- return jsonify({
- "status": "ok",
- "photo_url": photo_url,
- "thumbnail_url": photo_url,
- "timestamp": request.form.get("timestamp")
- })
- @app.route("/api/incident/<call_sid>", methods=["GET"])
- def get_incident_details(call_sid):
- """Get incident details for a specific call"""
- # In production: fetch from database
- return jsonify({
- "call_sid": call_sid,
- "status": "active",
- "type": "Medical Emergency",
- "priority": "P1",
- "caller": {
- "phone": "+15551234567",
- "location": None
- },
- "transcript": [],
- "media": [],
- "notes": "",
- "units": []
- })
- @app.route("/api/incident/<call_sid>/notes", methods=["POST"])
- def update_notes(call_sid):
- """Update operator notes for an incident"""
- data = request.get_json(silent=True) or {}
- notes = data.get("notes", "")
- print(f"[Notes] {call_sid} - Updated notes: {notes[:50]}...")
- # In production: save to database
- return jsonify({"status": "ok", "call_sid": call_sid})
- @app.route("/api/units", methods=["GET"])
- def get_units():
- """Get available/dispatched units"""
- # In production: fetch from CAD system or database
- return jsonify({
- "units": [
- {
- "id": "car-22",
- "type": "police",
- "name": "Car 22",
- "status": "dispatched",
- "eta": 4,
- "distance": 1.2,
- "location": {"lat": 39.78, "lng": -89.65}
- },
- {
- "id": "engine-7",
- "type": "fire",
- "name": "Engine 7",
- "status": "enroute",
- "eta": 5,
- "distance": 1.8,
- "location": {"lat": 39.79, "lng": -89.64}
- }
- ]
- })
- # ───────────────────────────────────────────────────────────────────────────────
- if __name__ == "__main__":
- app.run(host="0.0.0.0", port=int(os.getenv("PORT", 5000)), debug=True)
Add Comment
Please, Sign In to add comment