diff --git a/.env b/.env new file mode 100644 index 0000000..2722a93 --- /dev/null +++ b/.env @@ -0,0 +1,10 @@ +# --- VoIP.ms API Credentials --- +# Find these in your VoIP.ms customer portal under Main Menu > SOAP and REST/JSON API +VOIPMS_API_USERNAME="eddwinn@gmail.com" +VOIPMS_API_PASSWORD="!Gofionago123catdog" + +# --- Target DID and Routing Destinations --- +TARGET_DID="5084268800" +TARGET_SIP_ACCOUNT="407323_auburnoil@washington2.voip.ms" # e.g., 123456_myhome@newyork1.voip.ms +TARGET_CELLPHONE_1="7743342638" # Use E.164 format (country code + number) +TARGET_CELLPHONE_2="9143306100" # Use E.164 format \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile.dev b/Dockerfile.dev index 508ae4b..34e3f1f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -6,9 +6,11 @@ ENV PYTHONUNBUFFERED=1 ENV MODE="DEVELOPMENT" WORKDIR /app +ENV PYTHONPATH=/app + COPY requirements.txt requirements.txt RUN pip install -r requirements.txt COPY . . -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/__pycache__/config.cpython-39.pyc b/__pycache__/config.cpython-39.pyc new file mode 100644 index 0000000..cf11b1a Binary files /dev/null and b/__pycache__/config.cpython-39.pyc differ diff --git a/__pycache__/settings_dev.cpython-313.pyc b/__pycache__/settings_dev.cpython-313.pyc new file mode 100644 index 0000000..c883983 Binary files /dev/null and b/__pycache__/settings_dev.cpython-313.pyc differ diff --git a/__pycache__/settings_dev.cpython-39.pyc b/__pycache__/settings_dev.cpython-39.pyc new file mode 100644 index 0000000..dc11585 Binary files /dev/null and b/__pycache__/settings_dev.cpython-39.pyc differ diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b1ec948 Binary files /dev/null and b/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/__pycache__/__init__.cpython-39.pyc b/app/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..3fec030 Binary files /dev/null and b/app/__pycache__/__init__.cpython-39.pyc differ diff --git a/app/__pycache__/database.cpython-39.pyc b/app/__pycache__/database.cpython-39.pyc new file mode 100644 index 0000000..fab5932 Binary files /dev/null and b/app/__pycache__/database.cpython-39.pyc differ diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..7ae76e4 Binary files /dev/null and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/__pycache__/main.cpython-39.pyc b/app/__pycache__/main.cpython-39.pyc new file mode 100644 index 0000000..9d04cd2 Binary files /dev/null and b/app/__pycache__/main.cpython-39.pyc differ diff --git a/app/__pycache__/models.cpython-39.pyc b/app/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000..daaed73 Binary files /dev/null and b/app/__pycache__/models.cpython-39.pyc differ diff --git a/app/__pycache__/voipms_client.cpython-39.pyc b/app/__pycache__/voipms_client.cpython-39.pyc new file mode 100644 index 0000000..0ec02ff Binary files /dev/null and b/app/__pycache__/voipms_client.cpython-39.pyc differ diff --git a/app/config.py b/app/config.py deleted file mode 100644 index d4d46fd..0000000 --- a/app/config.py +++ /dev/null @@ -1,22 +0,0 @@ -from pydantic_settings import BaseSettings, SettingsConfigDict - -class Settings(BaseSettings): - # Load settings from the .env file - model_config = SettingsConfigDict(env_file="../.env", env_file_encoding='utf-8') - - # VoIP.ms Credentials - voipms_api_username: str - voipms_api_password: str - - # Target DID and Destinations - target_did: str - target_sip_account: str - target_cellphone_1: str - target_cellphone_2: str - - # VoIP.ms API endpoint - voipms_api_url: str = "https://voip.ms/api/v1/rest.php" - - -# Create a single instance of the settings to be used throughout the app -settings = Settings() \ No newline at end of file diff --git a/app/main.py b/app/main.py index 2a19f37..5acf70a 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,10 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from settings_dev import settings +import requests from fastapi import FastAPI, HTTPException, status -from .config import settings -from .voipms_client import update_did_routing +from .voipms_client import update_did_routing, get_forwardings from .database import Session from .models import Call @@ -9,9 +13,33 @@ app = FastAPI( description="An API to manage routing for a VoIP.ms DID.", version="1.0.0", ) - - - +@app.get("/test/forwardings") +def test_get_forwardings(): + try: + result = get_forwardings(phone_number=settings.target_cellphone_1) + return { + "message": f"Forwarding entry for {settings.target_cellphone_1}", + "voipms_response": result + } + except HTTPException as e: + raise e +@app.get("/test/did") +def test_did_info(): + params = { + "api_username": settings.voipms_api_username, + "api_password": settings.voipms_api_password, + "method": "getDIDsInfo", # Correct: plural "DIDs" + "did": settings.target_did, # Filters to one DID + } + try: + response = requests.get(settings.voipms_api_url, params=params) + print(f"Test Request URL: {response.request.url}") + print(f"Test Response: {response.json()}") + return response.json() + except requests.exceptions.RequestException as e: + return {"error": f"Failed to connect to VoIP.ms API: {str(e)}", "params": params} + except Exception as e: + return {"error": f"Unexpected error: {str(e)}", "params": params} @app.get("/", tags=["General"]) def read_root(): """A simple root endpoint to confirm the API is running.""" @@ -23,10 +51,12 @@ def route_to_sip_account(): """ Routes the target DID to the pre-configured SIP account. """ - routing_string = f"sip:{settings.target_sip_account}" try: + # Use sub-account ID from TARGET_SIP_ACCOUNT + sip_account_id = settings.target_sip_account.split('@')[0] # Extract '407323_auburnoil' + routing_string = f"account:{sip_account_id}" # e.g., 'account:407323_auburnoil' result = update_did_routing(did=settings.target_did, routing=routing_string) - target_phone = routing_string.split(':')[1] + target_phone = sip_account_id db = Session() db.add(Call(current_phone=target_phone)) db.commit() @@ -36,7 +66,6 @@ def route_to_sip_account(): "voipms_response": result } except HTTPException as e: - # Re-raise the exception to let FastAPI handle the response raise e @@ -45,10 +74,18 @@ def route_to_cellphone_1(): """ Routes the target DID to the pre-configured Cellphone #1. """ - routing_string = f"fwd:{settings.target_cellphone_1}" try: + # Get the forwarding entry for the phone number + forwarding = get_forwardings(phone_number=settings.target_cellphone_1) + forwarding_id = forwarding.get("forwarding") # e.g., '998692' + if not forwarding_id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No forwarding ID found for phone number {settings.target_cellphone_1}" + ) + routing_string = f"fwd:{forwarding_id}" # e.g., 'fwd:998692' result = update_did_routing(did=settings.target_did, routing=routing_string) - target_phone = routing_string.split(':')[1] + target_phone = settings.target_cellphone_1 db = Session() db.add(Call(current_phone=target_phone)) db.commit() @@ -60,16 +97,23 @@ def route_to_cellphone_1(): except HTTPException as e: raise e - @app.post("/route/cellphone2", status_code=status.HTTP_200_OK, tags=["DID Routing"]) def route_to_cellphone_2(): """ Routes the target DID to the pre-configured Cellphone #2. """ - routing_string = f"fwd:{settings.target_cellphone_2}" try: + # Get the forwarding entry for the phone number + forwarding = get_forwardings(phone_number=settings.target_cellphone_2) + forwarding_id = forwarding.get("forwarding") # e.g., ID for 9143306100 + if not forwarding_id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No forwarding ID found for phone number {settings.target_cellphone_2}" + ) + routing_string = f"fwd:{forwarding_id}" # e.g., 'fwd:' result = update_did_routing(did=settings.target_did, routing=routing_string) - target_phone = routing_string.split(':')[1] + target_phone = settings.target_cellphone_2 db = Session() db.add(Call(current_phone=target_phone)) db.commit() @@ -79,4 +123,4 @@ def route_to_cellphone_2(): "voipms_response": result } except HTTPException as e: - raise e + raise e \ No newline at end of file diff --git a/app/voipms_client.py b/app/voipms_client.py index 446cf4f..3b38f71 100644 --- a/app/voipms_client.py +++ b/app/voipms_client.py @@ -1,7 +1,53 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +from settings_dev import settings + import requests from fastapi import HTTPException, status -from .config import settings +def get_did_info(did: str): + """ + Calls the VoIP.ms API to get DID information. + + Args: + did (str): The phone number (DID) to query. + + Raises: + HTTPException: If the API call fails or returns an error. + + Returns: + dict: The JSON response from the VoIP.ms API on success. + """ + params = { + "api_username": settings.voipms_api_username, + "api_password": settings.voipms_api_password, + "method": "getDIDInfo", + "did": did, + } + + try: + response = requests.get(settings.voipms_api_url, params=params) + response.raise_for_status() + data = response.json() + if data.get("status") != "success": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"VoIP.ms API Error: {data.get('status')}. Full response: {data}. Request params: {params}" + ) + return data + except requests.exceptions.RequestException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Failed to connect to VoIP.ms API: {e}" + ) + except Exception as e: + if isinstance(e, HTTPException): + raise e + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An unexpected error occurred: {e}" + ) def update_did_routing(did: str, routing: str): """ @@ -20,37 +66,84 @@ def update_did_routing(did: str, routing: str): params = { "api_username": settings.voipms_api_username, "api_password": settings.voipms_api_password, - "method": "setDIDInfo", + "method": "setDIDRouting", # Correct method for routing updates "did": did, - "routing": routing, + "routing": routing, # e.g., 'fwd:7743342638' + "pop": "75", # Match the DID's current POP (from getDIDsInfo) } try: response = requests.get(settings.voipms_api_url, params=params) - response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx) - + print(f"Request URL: {response.request.url}") # Debug + print(f"Request Params: {params}") # Debug + response.raise_for_status() data = response.json() + print(f"VoIP.ms API Response: {data}") # Debug - # VoIP.ms API has its own status field in the JSON response if data.get("status") != "success": raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"VoIP.ms API Error: {data.get('status')}" + detail=f"VoIP.ms API Error: {data.get('status')} - Full response: {data}" ) return data except requests.exceptions.RequestException as e: - # Handle network errors, timeouts, etc. raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail=f"Failed to connect to VoIP.ms API: {e}" + detail=f"Failed to connect to VoIP.ms API: {e} - Request params: {params}" ) except Exception as e: - # Catch any other exceptions, including the ones we raised manually if isinstance(e, HTTPException): - raise e # Re-raise if it's already an HTTPException + raise e raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"An unexpected error occurred: {e}" + detail=f"An unexpected error occurred: {e} - Request params: {params}" + ) + +def get_forwardings(phone_number: str = None): + """ + Retrieves call forwarding entries from VoIP.ms. + + Args: + phone_number (str, optional): Filter by phone number (e.g., '7743342638'). + + Returns: + dict: The JSON response from the VoIP.ms API, including forwarding IDs. + """ + params = { + "api_username": settings.voipms_api_username, + "api_password": settings.voipms_api_password, + "method": "getForwardings", + } + try: + response = requests.get(settings.voipms_api_url, params=params) + print(f"Get Forwardings Request URL: {response.request.url}") + print(f"Get Forwardings Response: {response.json()}") + response.raise_for_status() + data = response.json() + if data.get("status") != "success": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"VoIP.ms API Error: {data.get('status')} - Full response: {data}" + ) + # Filter by phone_number if provided + if phone_number and "forwardings" in data: + for forwarding in data["forwardings"]: + if forwarding.get("phone_number") == phone_number: + return forwarding + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No forwarding entry found for phone number {phone_number}" + ) + return data + except requests.exceptions.RequestException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"Failed to connect to VoIP.ms API: {e} - Request params: {params}" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"An unexpected error occurred: {e} - Request params: {params}" ) \ No newline at end of file diff --git a/config.py b/config.py index ad92505..4f22694 100644 --- a/config.py +++ b/config.py @@ -22,5 +22,5 @@ def load_config(mode=os.environ.get('MODE')): pass except ImportError: - from settings_local import ApplicationConfig + from settings_dev import ApplicationConfig return ApplicationConfig \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c58e901..96ffb4f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ fastapi uvicorn[standard] pydantic-settings -requests \ No newline at end of file +python-dotenv +requests +sqlalchemy +psycopg2-binary \ No newline at end of file diff --git a/settings_dev.py b/settings_dev.py index f9cbf65..9bab9fe 100644 --- a/settings_dev.py +++ b/settings_dev.py @@ -1,5 +1,11 @@ +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + class ApplicationConfig: """ Basic Configuration for a generic User @@ -33,3 +39,20 @@ class ApplicationConfig: # # Authorize.net credentials (Sandbox Test Credentials) # API_LOGIN_ID = '5KP3u95bQpv' # TRANSACTION_KEY = '346HZ32z3fP4hTG2' + + # VoIP.ms Credentials and Settings + voipms_api_username = os.environ.get('VOIPMS_API_USERNAME') + voipms_api_password = os.environ.get('VOIPMS_API_PASSWORD') + + # Target DID and Destinations + target_did = os.environ.get('TARGET_DID') + target_sip_account = os.environ.get('TARGET_SIP_ACCOUNT') + target_cellphone_1 = os.environ.get('TARGET_CELLPHONE_1') + target_cellphone_2 = os.environ.get('TARGET_CELLPHONE_2') + + # VoIP.ms API endpoint + voipms_api_url = os.environ.get('VOIPMS_API_URL', "https://voip.ms/api/v1/rest.php") + + +# Create a single instance of the settings to be used throughout the app +settings = ApplicationConfig() diff --git a/settings_local.py b/settings_local.py index e249762..a256824 100644 --- a/settings_local.py +++ b/settings_local.py @@ -1,5 +1,11 @@ +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + class ApplicationConfig: """ Basic Configuration for a generic User @@ -26,5 +32,20 @@ class ApplicationConfig: "http://192.168.1.204:9614", "http://192.168.1.204:9612", "http://192.168.1.204:9611", -] + ] + # VoIP.ms Credentials and Settings + voipms_api_username = os.environ.get('VOIPMS_API_USERNAME') + voipms_api_password = os.environ.get('VOIPMS_API_PASSWORD') + + # Target DID and Destinations + target_did = os.environ.get('TARGET_DID') + target_sip_account = os.environ.get('TARGET_SIP_ACCOUNT') + target_cellphone_1 = os.environ.get('TARGET_CELLPHONE_1') + target_cellphone_2 = os.environ.get('TARGET_CELLPHONE_2') + + # VoIP.ms API endpoint + voipms_api_url = os.environ.get('VOIPMS_API_URL', "https://voip.ms/api/v1/rest.php") + +# Create a single instance of the settings to be used throughout the app +settings = ApplicationConfig() diff --git a/settings_prod.py b/settings_prod.py index f2dbbc8..cec7fe6 100644 --- a/settings_prod.py +++ b/settings_prod.py @@ -1,3 +1,10 @@ + +import os +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + class ApplicationConfig: """ Basic Configuration for a generic User @@ -19,4 +26,19 @@ class ApplicationConfig: origins = [ "https://oil.edwineames.com", "https://apiauto.edwineames.com", -] + ] + # VoIP.ms Credentials and Settings + voipms_api_username = os.environ.get('VOIPMS_API_USERNAME') + voipms_api_password = os.environ.get('VOIPMS_API_PASSWORD') + + # Target DID and Destinations + target_did = os.environ.get('TARGET_DID') + target_sip_account = os.environ.get('TARGET_SIP_ACCOUNT') + target_cellphone_1 = os.environ.get('TARGET_CELLPHONE_1') + target_cellphone_2 = os.environ.get('TARGET_CELLPHONE_2') + + # VoIP.ms API endpoint + voipms_api_url = os.environ.get('VOIPMS_API_URL', "https://voip.ms/api/v1/rest.php") + +# Create a single instance of the settings to be used throughout the app +settings = ApplicationConfig()