Guest User

Untitled

a guest
Feb 5th, 2026
6
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.61 KB | None | 0 0
  1. from flask import Flask, request, Response, jsonify, render_template, send_from_directory
  2. from twilio.twiml.voice_response import VoiceResponse
  3. from twilio.rest import Client
  4. from twilio.jwt.access_token import AccessToken
  5. from twilio.jwt.access_token.grants import VoiceGrant
  6. import os
  7. import base64
  8. import json
  9. from dotenv import load_dotenv
  10.  
  11. load_dotenv()
  12.  
  13. app = Flask(__name__, template_folder="templates")
  14.  
  15. # ─── Twilio ENV ────────────────────────────────────────────────────────────────
  16. TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
  17. TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
  18. TWILIO_API_KEY = os.getenv("TWILIO_API_KEY_SID")
  19. TWILIO_API_SECRET = os.getenv("TWILIO_API_KEY_SECRET")
  20. TWILIO_TWIML_APP_SID = os.getenv("TWILIO_TWIML_APP_SID")
  21. TWILIO_CLIENT_IDENTITY = os.getenv("TWILIO_CLIENT_IDENTITY", "browser")
  22. TWILIO_NUMBER = os.getenv("TWILIO_NUMBER") or os.getenv("TWILIO_PHONE_NUMBER")
  23.  
  24. twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
  25.  
  26.  
  27. def _b64url_decode(data):
  28. padding = '=' * (-len(data) % 4)
  29. return base64.urlsafe_b64decode(data + padding)
  30.  
  31.  
  32. def decode_jwt_noverify(token):
  33. """Decode JWT header/payload without verification (debug only)."""
  34. try:
  35. parts = token.split('.')
  36. if len(parts) != 3:
  37. return {"error": "not a jwt"}
  38. header = json.loads(_b64url_decode(parts[0]).decode('utf-8'))
  39. payload = json.loads(_b64url_decode(parts[1]).decode('utf-8'))
  40. return {"header": header, "payload": payload}
  41. except Exception as exc: # debug only
  42. return {"error": str(exc)}
  43.  
  44. # ─── Routes ────────────────────────────────────────────────────────────────────
  45.  
  46. @app.route("/")
  47. def index():
  48. return render_template("client-example.html")
  49.  
  50.  
  51. # ───────────────────────────────────────────────────────────────────────────────
  52. # TOKEN (Voice SDK ONLY — no legacy)
  53. # ───────────────────────────────────────────────────────────────────────────────
  54. def build_token(identity):
  55. ttl_seconds = 3600
  56. access_token = AccessToken(
  57. account_sid = TWILIO_ACCOUNT_SID,
  58. signing_key_sid = TWILIO_API_KEY,
  59. secret =TWILIO_API_SECRET,
  60. identity=identity,
  61. ttl=ttl_seconds
  62. )
  63.  
  64. voice_grant = VoiceGrant(
  65. outgoing_application_sid=TWILIO_TWIML_APP_SID,
  66. incoming_allow=True,
  67. )
  68.  
  69. access_token.add_grant(voice_grant)
  70.  
  71. jwt = access_token.to_jwt()
  72. if isinstance(jwt, bytes):
  73. jwt = jwt.decode("utf-8")
  74. return jwt
  75.  
  76.  
  77. @app.route("/token", methods=["GET"])
  78. def token():
  79. identity = request.args.get("identity", TWILIO_CLIENT_IDENTITY)
  80. jwt = build_token(identity)
  81. print(f"Issued AccessToken for {identity}")
  82. return jsonify({
  83. "token": jwt,
  84. "identity": identity,
  85. "type": "access",
  86. })
  87.  
  88.  
  89. @app.route("/token_raw", methods=["GET"])
  90. def token_raw():
  91. """Debug endpoint: returns token plus decoded header/payload (no verify)."""
  92. identity = request.args.get("identity", TWILIO_CLIENT_IDENTITY)
  93. jwt = build_token(identity)
  94. decoded = decode_jwt_noverify(jwt)
  95. token_resp = {
  96. "token": jwt,
  97. "identity": identity,
  98. "type": "access",
  99. }
  100. token_resp.update({
  101. "decoded": decoded,
  102. "config": {
  103. "account_sid": TWILIO_ACCOUNT_SID,
  104. "api_key": TWILIO_API_KEY,
  105. "twiml_app_sid": TWILIO_TWIML_APP_SID,
  106. "identity": identity,
  107. }
  108. })
  109. return jsonify(token_resp)
  110.  
  111.  
  112. # ───────────────────────────────────────────────────────────────────────────────
  113. # INCOMING CALL HANDLER (PSTN → Browser)
  114. # ───────────────────────────────────────────────────────────────────────────────
  115. @app.route("/voice", methods=["POST"])
  116. def voice():
  117. print("Voice request:", dict(request.form))
  118.  
  119. to = request.form.get("To")
  120. response = VoiceResponse()
  121.  
  122. # If the call is NOT coming from a client, it's an incoming PSTN call to our number
  123. # We want to route it to our browser client
  124. if not request.form.get("From", "").startswith("client:"):
  125. dial = response.dial(
  126. timeout=30,
  127. answer_on_bridge=True,
  128. action="/call-status",
  129. )
  130. dial.client(TWILIO_CLIENT_IDENTITY)
  131. # If there is a 'To' number provided, it's an outgoing call from our client
  132. elif to:
  133. dial = response.dial(caller_id=TWILIO_NUMBER)
  134. # Check if we are calling a client or a number
  135. if to.startswith("client:"):
  136. dial.client(to.replace("client:", ""))
  137. else:
  138. dial.number(to)
  139. else:
  140. response.say("No destination provided.")
  141.  
  142. return Response(str(response), mimetype="text/xml")
  143.  
  144.  
  145. @app.route("/call-status", methods=["POST"])
  146. def call_status():
  147. print("📡 Call status:", dict(request.form))
  148. # Twilio expects valid TwiML; return an empty <Response/> to avoid 12100 parse errors.
  149. vr = VoiceResponse()
  150. return Response(str(vr), mimetype="text/xml")
  151.  
  152.  
  153. # ───────────────────────────────────────────────────────────────────────────────
  154. # DEBUG ROUTES
  155. # ───────────────────────────────────────────────────────────────────────────────
  156. @app.route("/env_check")
  157. def env_check():
  158. def mask(v):
  159. if not v:
  160. return None
  161. return v[:2] + "..." + v[-2:]
  162.  
  163. return jsonify({
  164. "TWILIO_ACCOUNT_SID": mask(TWILIO_ACCOUNT_SID),
  165. "TWILIO_AUTH_TOKEN": mask(TWILIO_AUTH_TOKEN),
  166. "TWILIO_API_KEY": mask(TWILIO_API_KEY),
  167. "TWILIO_API_SECRET": mask(TWILIO_API_SECRET),
  168. "TWILIO_TWIML_APP_SID": mask(TWILIO_TWIML_APP_SID),
  169. "TWILIO_PHONE_NUMBER": TWILIO_NUMBER,
  170. "TWILIO_CLIENT_IDENTITY": TWILIO_CLIENT_IDENTITY,
  171. })
  172.  
  173.  
  174. # ───────────────────────────────────────────────────────────────────────────────
  175. # LOCAL SDK SERVING (NO CDN)
  176. # ───────────────────────────────────────────────────────────────────────────────
  177. @app.route("/static/vendor/twilio-voice.min.js")
  178. def twilio_sdk():
  179. base = os.path.dirname(os.path.abspath(__file__))
  180. vendor = os.path.join(base, "src", "static", "vendor")
  181. return send_from_directory(
  182. vendor,
  183. "twilio-voice.min.js",
  184. mimetype="application/javascript",
  185. )
  186.  
  187.  
  188. @app.route("/send_link", methods=["POST"])
  189. def send_link():
  190. data = request.get_json(silent=True) or {}
  191. recipient = data.get("phone")
  192. print(f"[send_link] placeholder send to: {recipient}")
  193. return jsonify({"status": "ok", "recipient": recipient})
  194.  
  195.  
  196. # ───────────────────────────────────────────────────────────────────────────────
  197. # OPERATOR DASHBOARD API ENDPOINTS
  198. # ───────────────────────────────────────────────────────────────────────────────
  199.  
  200. @app.route("/api/transcript", methods=["POST"])
  201. def receive_transcript():
  202. """Receive real-time transcription from caller session"""
  203. data = request.get_json(silent=True) or {}
  204. call_sid = data.get("call_sid")
  205. speaker = data.get("speaker", "Caller")
  206. text = data.get("text")
  207. confidence = data.get("confidence")
  208.  
  209. print(f"[Transcript] {call_sid} - {speaker}: {text}")
  210.  
  211. # In production: store in database, broadcast via WebSocket, etc.
  212.  
  213. return jsonify({
  214. "status": "ok",
  215. "timestamp": data.get("timestamp"),
  216. "speaker": speaker
  217. })
  218.  
  219.  
  220. @app.route("/api/location", methods=["POST"])
  221. def receive_location():
  222. """Receive GPS location from caller session"""
  223. data = request.get_json(silent=True) or {}
  224. call_sid = data.get("call_sid")
  225. lat = data.get("lat")
  226. lng = data.get("lng")
  227. accuracy = data.get("accuracy")
  228. address = data.get("address")
  229.  
  230. print(f"[Location] {call_sid} - ({lat}, {lng}) - {address}")
  231.  
  232. # In production: store in database, geocode if needed, update incident record
  233.  
  234. return jsonify({
  235. "status": "ok",
  236. "lat": lat,
  237. "lng": lng,
  238. "address": address
  239. })
  240.  
  241.  
  242. @app.route("/api/photo", methods=["POST"])
  243. def receive_photo():
  244. """Receive photo upload from caller session"""
  245. call_sid = request.form.get("call_sid")
  246.  
  247. if "photo" not in request.files:
  248. return jsonify({"error": "No photo provided"}), 400
  249.  
  250. photo = request.files["photo"]
  251.  
  252. if photo.filename == "":
  253. return jsonify({"error": "Empty filename"}), 400
  254.  
  255. # In production: save to storage (S3, local disk, etc.), create thumbnail
  256. # For now, just acknowledge receipt
  257.  
  258. print(f"[Photo] {call_sid} - Received photo: {photo.filename} ({photo.content_type})")
  259.  
  260. # Simulate storing and returning a URL
  261. photo_url = f"/uploads/{call_sid}/{photo.filename}"
  262.  
  263. return jsonify({
  264. "status": "ok",
  265. "photo_url": photo_url,
  266. "thumbnail_url": photo_url,
  267. "timestamp": request.form.get("timestamp")
  268. })
  269.  
  270.  
  271. @app.route("/api/incident/<call_sid>", methods=["GET"])
  272. def get_incident_details(call_sid):
  273. """Get incident details for a specific call"""
  274. # In production: fetch from database
  275.  
  276. return jsonify({
  277. "call_sid": call_sid,
  278. "status": "active",
  279. "type": "Medical Emergency",
  280. "priority": "P1",
  281. "caller": {
  282. "phone": "+15551234567",
  283. "location": None
  284. },
  285. "transcript": [],
  286. "media": [],
  287. "notes": "",
  288. "units": []
  289. })
  290.  
  291.  
  292. @app.route("/api/incident/<call_sid>/notes", methods=["POST"])
  293. def update_notes(call_sid):
  294. """Update operator notes for an incident"""
  295. data = request.get_json(silent=True) or {}
  296. notes = data.get("notes", "")
  297.  
  298. print(f"[Notes] {call_sid} - Updated notes: {notes[:50]}...")
  299.  
  300. # In production: save to database
  301.  
  302. return jsonify({"status": "ok", "call_sid": call_sid})
  303.  
  304.  
  305. @app.route("/api/units", methods=["GET"])
  306. def get_units():
  307. """Get available/dispatched units"""
  308. # In production: fetch from CAD system or database
  309.  
  310. return jsonify({
  311. "units": [
  312. {
  313. "id": "car-22",
  314. "type": "police",
  315. "name": "Car 22",
  316. "status": "dispatched",
  317. "eta": 4,
  318. "distance": 1.2,
  319. "location": {"lat": 39.78, "lng": -89.65}
  320. },
  321. {
  322. "id": "engine-7",
  323. "type": "fire",
  324. "name": "Engine 7",
  325. "status": "enroute",
  326. "eta": 5,
  327. "distance": 1.8,
  328. "location": {"lat": 39.79, "lng": -89.64}
  329. }
  330. ]
  331. })
  332.  
  333.  
  334. # ───────────────────────────────────────────────────────────────────────────────
  335. if __name__ == "__main__":
  336. app.run(host="0.0.0.0", port=int(os.getenv("PORT", 5000)), debug=True)
Add Comment
Please, Sign In to add comment