You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

527 regels
20 KiB

  1. import json
  2. import subprocess
  3. import logging
  4. import ipaddress
  5. #import api_utils.carddav_util as carddav_util
  6. #from .api_utils import carddav_util
  7. from enum import Enum
  8. from typing import Any, Dict, List, Optional
  9. # import wave
  10. import os
  11. import shutil
  12. import re
  13. # import enviroment variables
  14. import config_env
  15. #other
  16. from pathlib import Path
  17. from tempfile import NamedTemporaryFile
  18. from typing import Callable
  19. import base64
  20. from datetime import datetime, timedelta
  21. import csv
  22. from api_utils import api_utils, coerce_methods
  23. from collections import OrderedDict
  24. from pydantic import BaseModel, Field
  25. from fastapi import Depends, FastAPI, HTTPException, File, UploadFile, Query
  26. from fastapi.encoders import jsonable_encoder
  27. from fastapi.openapi.docs import get_swagger_ui_html
  28. from fastapi.openapi.utils import get_openapi
  29. from starlette.status import HTTP_403_FORBIDDEN
  30. from starlette.responses import RedirectResponse, Response, JSONResponse
  31. from starlette.requests import Request
  32. from starlette.middleware.cors import CORSMiddleware
  33. from starlette.responses import FileResponse
  34. from starlette.types import ASGIApp, Message, Receive, Scope, Send
  35. from models.cellular_hardware import cellularHardware
  36. from models.cellular_hardwares import cellularHardwares
  37. from models.call import call, post_call
  38. from models.calls import calls
  39. from models.httpresponse import httpResponse400, httpResponse200, httpResponse500
  40. from schemas.reslevis import CalibrationMetadata
  41. from fastapi_login import LoginManager
  42. from core.security import manager, NotAuthenticatedException
  43. from security import get_current_user
  44. from routes import auth as _auth
  45. auth_router = _auth.router
  46. from routes import user as _user
  47. user_router = _user.router
  48. #from routes.posts import router as posts_router
  49. from routes import majornet as _majornet
  50. majornet_router = _majornet.router
  51. from routes import presence as _presence
  52. presence_router = _presence.router
  53. from routes import contacts as _contacts
  54. contacts_router = _contacts.router
  55. from routes import reslevis as _reslevis
  56. reslevis_router = _reslevis.router
  57. #security
  58. from fastapi import FastAPI, Security
  59. from fastapi.security import OAuth2AuthorizationCodeBearer
  60. #Proxy al CORE ResLevis
  61. import httpx
  62. from mqtt_gateway_monitor import MqttGatewayMonitor
  63. AUTH_URL = config_env.KEYCLOAK_AUTH_URL
  64. TOKEN_URL = config_env.KEYCLOAK_TOKEN_URL
  65. oauth2 = OAuth2AuthorizationCodeBearer(
  66. authorizationUrl=AUTH_URL,
  67. tokenUrl=TOKEN_URL,
  68. scopes={"items:read": "Read items", "items:write": "Write items"},
  69. )
  70. log = logging.getLogger(__name__) # pylint: disable=invalid-name
  71. DEBUG = True
  72. app = FastAPI(title="MajorNet API", redoc_url=None, docs_url=None, openapi_url=None)
  73. ####DEV
  74. ##app = FastAPI(title=PROJECT_NAME)
  75. app.debug = True
  76. #logging
  77. from audit import AuditMiddleware
  78. app.add_middleware(AuditMiddleware)
  79. logging.basicConfig(filename='/data/var/log/FastAPI/AuthenticatedAPI.log', filemode='w', format='%(name)s - %(levelname)s - %(message)s')
  80. logging.basicConfig(
  81. level=logging.INFO,
  82. format="%(levelname)s:%(name)s:%(message)s"
  83. )
  84. #ResLevis CORE Proxying
  85. CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/")
  86. HOP_BY_HOP = {
  87. "connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
  88. "te", "trailers", "transfer-encoding", "upgrade",
  89. }
  90. def _filter_headers(headers: dict) -> dict:
  91. return {k: v for k, v in headers.items() if k.lower() not in HOP_BY_HOP}
  92. async def _forward_to_core(request: Request, body: bytes) -> Response:
  93. url = f"{CORE_BASE_URL}{request.url.path}"
  94. async with httpx.AsyncClient(timeout=30.0) as client:
  95. resp = await client.request(
  96. request.method,
  97. url,
  98. params=request.query_params,
  99. content=body,
  100. headers=_filter_headers(dict(request.headers)),
  101. )
  102. return Response(
  103. content=resp.content,
  104. status_code=resp.status_code,
  105. headers=_filter_headers(dict(resp.headers)),
  106. media_type=resp.headers.get("content-type"),
  107. )
  108. ALLOWED_HOSTS = ["*"]
  109. app.add_middleware(
  110. CORSMiddleware,
  111. allow_origins=ALLOWED_HOSTS,
  112. allow_credentials=True,
  113. allow_methods=["*"],
  114. allow_headers=["*"],
  115. )
  116. # MQTT gateway monitor
  117. mqtt_monitor = MqttGatewayMonitor()
  118. @app.on_event("startup")
  119. async def start_mqtt_monitor():
  120. await mqtt_monitor.start()
  121. @app.on_event("shutdown")
  122. async def stop_mqtt_monitor():
  123. await mqtt_monitor.stop()
  124. #ResLevis CORE middleware
  125. @app.middleware("http")
  126. async def local_then_core(request: Request, call_next):
  127. # only proxy CRUD for Reslevis (change prefix or methods if needed)
  128. if request.url.path.startswith("/reslevis/") and request.method in {"POST", "PUT", "DELETE", "PATCH"}:
  129. body = await request.body() # raw body preserved
  130. local_resp = await call_next(request) # local storage runs here
  131. if local_resp.status_code >= 400:
  132. return local_resp # stop if local failed
  133. try:
  134. core_resp = await _forward_to_core(request, body)
  135. except httpx.RequestError:
  136. return local_resp
  137. if core_resp.status_code >= 400:
  138. return local_resp
  139. return core_resp
  140. return await call_next(request)
  141. @app.exception_handler(NotAuthenticatedException)
  142. def auth_exception_handler(request: Request, exc: NotAuthenticatedException):
  143. """
  144. Redirect the user to the login page if not logged in
  145. """
  146. return RedirectResponse(url='/login')
  147. # Nasconde i dettagli degli errori 5xx
  148. @app.exception_handler(HTTPException)
  149. async def http_exception_handler(request: Request, exc: HTTPException):
  150. if exc.status_code >= 500:
  151. log.exception("HTTP %s: %s", exc.status_code, exc.detail)
  152. return JSONResponse(status_code=exc.status_code, content={"detail": "Internal Server Error"})
  153. return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
  154. @app.exception_handler(Exception)
  155. async def unhandled_exception_handler(request: Request, exc: Exception):
  156. log.exception("Unhandled exception")
  157. return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
  158. app.include_router(auth_router)
  159. app.include_router(user_router)
  160. #app.include_router(posts_router)
  161. app.include_router(presence_router)
  162. app.include_router(majornet_router)
  163. app.include_router(contacts_router)
  164. app.include_router(reslevis_router, prefix="/reslevis", tags=["Reslevis"])
  165. META_FILE_PATTERN = re.compile(r"^meta_(-?\d+)\.json$")
  166. def _model_to_dict(model: Any) -> Dict[str, Any]:
  167. if hasattr(model, "model_dump"):
  168. return model.model_dump()
  169. return model.dict()
  170. def _validate_metadata(payload: Dict[str, Any]) -> Dict[str, Any]:
  171. if hasattr(CalibrationMetadata, "model_validate"):
  172. return _model_to_dict(CalibrationMetadata.model_validate(payload))
  173. return _model_to_dict(CalibrationMetadata.parse_obj(payload))
  174. def _read_ble_ai_metadata(path: str) -> Dict[str, Any]:
  175. with open(path) as f:
  176. payload = json.load(f)
  177. return _validate_metadata(payload)
  178. @app.get("/")
  179. async def root():
  180. #return {"url": "/docs"}
  181. return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
  182. @app.get("/ble-ai/infer", tags=["BLE-AI"], dependencies=[Depends(get_current_user)])
  183. async def get_ble_ai_infer(mac: Optional[List[str]] = Query(default=None)):
  184. path = config_env.BLE_AI_INFER_CSV
  185. if not os.path.isfile(path):
  186. raise HTTPException(status_code=404, detail="CSV not found")
  187. mac_filter = None
  188. if mac:
  189. mac_filter = set()
  190. for item in mac:
  191. if not item:
  192. continue
  193. for part in str(item).split(","):
  194. part = part.strip()
  195. if part:
  196. mac_filter.add(part.lower())
  197. items = []
  198. with open(path, newline="") as f:
  199. reader = csv.DictReader(f, delimiter=";")
  200. for row in reader:
  201. row_mac = row.get("mac")
  202. if mac_filter is not None:
  203. if not row_mac or row_mac.lower() not in mac_filter:
  204. continue
  205. try:
  206. items.append(
  207. {
  208. "mac": row_mac,
  209. "z": int(row["z"]) if row.get("z") not in (None, "") else None,
  210. "x": int(row["x"]) if row.get("x") not in (None, "") else None,
  211. "y": int(row["y"]) if row.get("y") not in (None, "") else None,
  212. }
  213. )
  214. except (KeyError, ValueError):
  215. continue
  216. return {"items": items, "count": len(items)}
  217. @app.get("/ble-ai/metadata", tags=["BLE-AI"], dependencies=[Depends(get_current_user)])
  218. async def get_ble_ai_metadata(floor: Optional[int] = Query(default=None)):
  219. meta_dir = config_env.BLE_AI_META_DIR
  220. if not os.path.isdir(meta_dir):
  221. raise HTTPException(status_code=404, detail="Metadata directory not found")
  222. if floor is not None:
  223. file_path = os.path.join(meta_dir, f"meta_{floor}.json")
  224. if not os.path.isfile(file_path):
  225. raise HTTPException(status_code=404, detail="Metadata file not found")
  226. try:
  227. metadata = _read_ble_ai_metadata(file_path)
  228. except Exception:
  229. log.exception("Invalid metadata file: %s", file_path)
  230. raise HTTPException(status_code=500, detail="Invalid metadata file")
  231. return {"floor": floor, "metadata": metadata}
  232. items = []
  233. for file_name in os.listdir(meta_dir):
  234. match = META_FILE_PATTERN.match(file_name)
  235. if not match:
  236. continue
  237. file_floor = int(match.group(1))
  238. file_path = os.path.join(meta_dir, file_name)
  239. if not os.path.isfile(file_path):
  240. continue
  241. try:
  242. metadata = _read_ble_ai_metadata(file_path)
  243. except Exception:
  244. log.exception("Invalid metadata file: %s", file_path)
  245. raise HTTPException(status_code=500, detail="Invalid metadata file")
  246. items.append({"floor": file_floor, "metadata": metadata})
  247. items.sort(key=lambda item: item["floor"])
  248. return {"items": items, "count": len(items)}
  249. @app.get("/openapi.json/", tags=["Documentation"])
  250. async def get_open_api_endpoint():
  251. #async def get_open_api_endpoint(current_user: User = Depends(get_current_active_user)):
  252. return JSONResponse(get_openapi(title="MajorNet APIs", version="1.0", routes=app.routes))
  253. @app.get("/docs/", tags=["Documentation"])
  254. #async def get_documentation(current_user: User = Depends(get_current_active_user)):
  255. async def get_documentation():
  256. if DEBUG: print("SONO IN /DOCS")
  257. return get_swagger_ui_html(openapi_url="/openapi.json", title="docs")
  258. @app.post("/majortel/call/", tags=["Majortel"])
  259. async def route_call(active_user=Depends(manager),callerNumber=None, calledNumber=None):
  260. try:
  261. if DEBUG: print("Entro nel TRY")
  262. # Check if the callerNumber sent is an ip address and retrieve the associated number
  263. ipaddress.ip_address(callerNumber)
  264. callerNumberRetrieved = os.popen('asterisk -rx "sip show peers" | grep ' + callerNumber).read()
  265. callerNumberRetrieved = callerNumberRetrieved.split("/")
  266. callerNumber = callerNumberRetrieved[0]
  267. except:
  268. if DEBUG: print("EXCEPT")
  269. mode = "callFromTo"
  270. from_ = callerNumber
  271. to_ = calledNumber
  272. if DEBUG: print("DATI ARRIVATI: ")
  273. if DEBUG: print(callerNumber)
  274. if DEBUG: print(calledNumber)
  275. subprocess.Popen(['perl', '/usr/local/bin/ast/voice.pl', mode, from_, to_], stdout=subprocess.PIPE)
  276. return
  277. @app.post("/majortel/ring/", tags=["Majortel"])
  278. async def route_ring(active_user=Depends(manager),calledNumber=None, calledId=None, ringTime=None):
  279. try:
  280. if DEBUG: print("Entro nel TRY")
  281. # Check if the callerNumber sent is an ip address and retrieve the associated number
  282. ipaddress.ip_address(calledNumber)
  283. calledNumberRetrieved = os.popen('asterisk -rx "sip show peers" | grep ' + calledNumber).read()
  284. calledNumberRetrieved = calledNumberRetrieved.split("/")
  285. calledNumber = calledNumberRetrieved[0]
  286. except:
  287. if DEBUG: print("EXCEPT")
  288. mode = "ringAlert"
  289. to_ = calledNumber
  290. calledId_ = calledId
  291. ringTime_ = ringTime
  292. if DEBUG: print("DATI ARRIVATI: ")
  293. if DEBUG: print(calledNumber)
  294. if DEBUG: print(calledId)
  295. if DEBUG: print(ringTime)
  296. subprocess.Popen(['perl', '/usr/local/bin/ast/voice.pl', mode, to_, calledId_, ringTime_], stdout=subprocess.PIPE)
  297. return
  298. @app.get("/majortel/hardware/", response_model=cellularHardwares, tags=["Majortel"])
  299. #async def majortel_hardware_get(current_user: User = Depends(get_current_active_user)):
  300. async def majortel_hardware_get():
  301. gsm_temp_list = "GSM span1: Provisioned, Up, Active"
  302. response = {"CellularHardwares": []}
  303. hardware_dict = {}
  304. myCmd = os.popen('asterisk -rx "gsm show spans"').read()
  305. myCmd = myCmd.split("\n")
  306. # cancello l'ultimo item della lista poichè, risultando vuoto dopo lo split,
  307. # genera errore "index out of range" nei successivi accessi alla lista
  308. if DEBUG: print("spans: ")
  309. if DEBUG: print(myCmd)
  310. myCmd = os.popen('asterisk -rx "dongle show devices"').read()
  311. myCmd = myCmd.split("\n")
  312. # cancello il primo item della lista poichè contiene la legenda
  313. del myCmd[0]
  314. del myCmd[-1]
  315. # costruisco la response
  316. for device in myCmd:
  317. device = device.split()
  318. hardware_id = device[0]
  319. current_device = os.popen('asterisk -rx "dongle show device state ' + hardware_id + '"').read()
  320. current_device = current_device.split("\n")
  321. # cancello il primo e gli ultimi item della lista poichè, risultando vuoti dopo lo split,
  322. # generano errore "index out of range" nei successivi accessi alla lista
  323. del current_device[0]
  324. del current_device[-1]
  325. del current_device[-1]
  326. # costruisco un dizionario a partire dall'output della system call
  327. for row in current_device:
  328. row = row.split(":")
  329. row[0] = row[0].strip()
  330. row[1] = row[1].strip()
  331. hardware_dict[row[0]] = row[1]
  332. hardware_id = hardware_dict["Device"]
  333. status = hardware_dict["State"]
  334. signal = hardware_dict["RSSI"]
  335. signal = int(signal[0:2])
  336. operator = hardware_dict["Provider Name"]
  337. device_obj = {"id": hardware_id, "description": "description", "status": status, "signal_level": signal,
  338. "registered_number": "To Do", "operator": operator}
  339. response["CellularHardwares"].append(device_obj)
  340. return response
  341. @app.get("/majortel/hardware/{item_id}", response_model=cellularHardware, tags=["Majortel"], responses={400: {"model": httpResponse400}})
  342. #async def majortel_hardware_id_get(item_id: str, current_user: User = Depends(get_current_active_user)):
  343. async def majortel_hardware_id_get(item_id: str,active_user=Depends(manager)):
  344. hardware_response = {}
  345. hardware_dict = {}
  346. current_device = os.popen('asterisk -rx "dongle show device state ' + item_id + '"').read()
  347. current_device = current_device.split("\n")
  348. # cancello il primo e gli ultimi item della lista poichè, risultando vuoti dopo lo split,
  349. # generano errore "index out of range" nei successivi accessi alla lista
  350. del current_device[0]
  351. if (current_device[0]):
  352. del current_device[-1]
  353. del current_device[-1]
  354. # costruisco un dizionario a partire dall'output della system call
  355. for row in current_device:
  356. row = row.split(":")
  357. row[0] = row[0].strip()
  358. row[1] = row[1].strip()
  359. hardware_dict[row[0]] = row[1]
  360. hardware_id = hardware_dict["Device"]
  361. status = hardware_dict["State"]
  362. signal = hardware_dict["RSSI"]
  363. signal = int(signal[0:2])
  364. operator = hardware_dict["Provider Name"]
  365. hardware_response = {"id": hardware_id, "description": "description", "status": status, "signal_level": signal,
  366. "registered_number": "To Do", "operator": operator}
  367. return hardware_response
  368. else:
  369. return JSONResponse(status_code=404, content={"message": "Device not found"})
  370. @app.get("/majortel/calls/", response_model=calls, tags=["Majortel"])
  371. #async def majortel_calls_get(current_user: User = Depends(get_current_active_user)):
  372. async def majortel_calls_get(active_user=Depends(manager)):
  373. response = {"Calls": []}
  374. p = subprocess.Popen(['perl', '/opt/api_project_python/addCallQueue.pl', "get_calls"], stdout=subprocess.PIPE)
  375. calls = str(p.stdout.read())
  376. calls = calls.split("'")
  377. response1 = calls
  378. calls = calls[1].split("||")
  379. # cancello l'ultimo item della lista poichè, risultando vuoto dopo lo split,
  380. # genera errore "index out of range" nei successivi accessi alla lista
  381. del calls[-1]
  382. for call in calls:
  383. call = call.split("|")
  384. call_id = call[0]
  385. status = call[1]
  386. history = call[2]
  387. ntel = call[3]
  388. ackid = call[4]
  389. call_obj = {"id": call_id, "status": status, "history": history, "called_number": ntel, "ack_id": ackid}
  390. response["Calls"].append(call_obj)
  391. return response
  392. @app.get("/majortel/calls/{call_id}/", response_model=call, tags=["Majortel"])
  393. async def majortel_calls_id_get(call_id,active_user=Depends(manager)):
  394. p = subprocess.Popen(['perl', '/var/opt/FastAPI/addCallQueue.pl', "call_id", call_id],
  395. stdout=subprocess.PIPE)
  396. call = str(p.stdout.read())
  397. call = call.split("|")
  398. call_id = call[0].split("'")[1]
  399. status = call[1]
  400. # history = call[2]
  401. ntel = call[3]
  402. ackid = call[4]
  403. ackid = int(ackid.split("'")[0])
  404. response = {"id": call_id, "status": status, "called_number": ntel, "ack_id": ackid}
  405. return response
  406. # response_model=post_call,
  407. @app.post("/majortel/calls", response_model=post_call, tags=["Majortel"])
  408. async def majortel_calls_post(hw_id, ack_id, called_number, text_message="codice di riscontro ", timeout=30,
  409. retry=1, file: UploadFile = File(None),active_user=Depends(manager)):
  410. # controllo se l'hw_id corrisponde ai devices connessi
  411. current_device = os.popen('asterisk -rx "dongle show device state ' + hw_id + '"').read()
  412. current_device = current_device.split("\n")
  413. del current_device[0]
  414. if (current_device[0]):
  415. curr_time = datetime.now()
  416. call_id = str(curr_time.strftime('%y')) + str(curr_time.strftime('%m')) + str(curr_time.strftime('%d')) + str(
  417. curr_time.strftime('%H')) + str(curr_time.strftime('%M')) + str(curr_time.strftime('%S')) + str(
  418. curr_time.strftime('%f'))
  419. file_path = ""
  420. if (file):
  421. nomefile = file.filename
  422. extension = nomefile.split(".")[1]
  423. if (extension == "wav"):
  424. file_path = "/data/service/voip.majornet/ivr/" + call_id
  425. renamed_file = file_path + ".wav"
  426. with open(renamed_file, "wb") as f:
  427. shutil.copyfileobj(file.file, f)
  428. f.close()
  429. # p = subprocess.Popen(['(cd /tmp/audioCalls;/usr/local/bin/sox_wav2gsm)'],stdout=subprocess.PIPE)
  430. # os.popen('/usr/local/bin/sox_wav2gsm 1>/dev/null 2>/dev/null')
  431. subprocess.Popen(
  432. ['perl', '/opt/api_project_python/addCallQueue.pl', "voicegateway", "digiovine@afasystems.it", call_id,
  433. called_number, ack_id, text_message, "retry message here", timeout, retry, "yes", "notification_to",
  434. "notification_bcc", "setup", file_path, "fromemailssss", hw_id], stdout=subprocess.PIPE)
  435. response = {"id": call_id}
  436. return response
  437. else:
  438. return JSONResponse(status_code=404, content={"message": "Device not found"})
  439. @app.delete("/majortel/calls/", tags=["Majortel"],
  440. responses={200: {"model": httpResponse200}, 400: {"model": httpResponse400},
  441. 500: {"model": httpResponse500}})
  442. #async def majortel_calls_id_delete(call_id, current_user: User = Depends(get_current_active_user)):
  443. async def majortel_calls_id_delete(call_id,active_user=Depends(manager)):
  444. p = subprocess.Popen(['perl', '/opt/api_project_python/addCallQueue.pl', "delete_call", call_id],
  445. stdout=subprocess.PIPE)
  446. response = str(p.stdout.read())
  447. response = response.split("'")[1]
  448. if (response == "Success"):
  449. return JSONResponse(status_code=200, content={"message": "Success"})
  450. elif (response == "Not found"):
  451. return JSONResponse(status_code=404, content={"message": "Call not found"})
  452. else:
  453. return JSONResponse(status_code=500, content={"message": "Server error"})