Add Telnyx setup wizard, remove PROJECT.md from repo
- setup_telnyx.py: automates fax app creation, number purchase, and config generation via Telnyx API - Gitignore PROJECT.md and CLAUDE.md to keep project files local Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aef5e5283a
commit
0748178355
4 changed files with 288 additions and 10 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,3 +1,7 @@
|
|||
# Project/session files
|
||||
PROJECT.md
|
||||
CLAUDE.md
|
||||
|
||||
# Credentials and config
|
||||
.env
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
Basically, my health insurance claims to cover doulas, however requires a tedious process where one must either fax them or snail mail them the claim. I did both. The fax returned as failure and I haven't seen the claim added even after mailing it a month ago. My coworker has the same issue.
|
||||
|
||||
Build a script (can integrate with cron) to sent an electronic fax to them every hour on the hour, and safely collect the returned failed responses in a directory. I will show this to them or to my companies HR. Also find an electronic fax API to use with a service that is the cheapest for a single month. Assume the doula claim paperwork is about 5 black and white pages.
|
||||
|
||||
You've got a figure out directive, you can ask some clarifying questions in the beginning, but then I want you to go and build the thing. Preferred language is python.
|
||||
30
README.md
30
README.md
|
|
@ -24,24 +24,44 @@ build_windows.bat
|
|||
|
||||
This produces `dist/AutoFax.exe` -- a single self-contained file you can copy to a flash drive.
|
||||
|
||||
## Linux/Server Setup (CLI + cron)
|
||||
## Telnyx Account Setup
|
||||
|
||||
### 1. Telnyx Account
|
||||
### Automated (recommended)
|
||||
|
||||
The setup wizard handles everything -- creates a fax application, buys a number, and writes your config:
|
||||
|
||||
```bash
|
||||
# Interactive
|
||||
python3 setup_telnyx.py
|
||||
|
||||
# Or non-interactive
|
||||
python3 setup_telnyx.py --api-key KEY_xxx --to +18019382102 --area-code 801
|
||||
```
|
||||
|
||||
You just need a Telnyx API key:
|
||||
1. Sign up at [portal.telnyx.com](https://portal.telnyx.com/)
|
||||
2. Add a payment method
|
||||
3. Go to **API Keys** and copy your key
|
||||
|
||||
The wizard creates both `.env` (for CLI) and `autofax_config.json` (for GUI).
|
||||
|
||||
### Manual
|
||||
|
||||
1. Sign up at [portal.telnyx.com](https://portal.telnyx.com/)
|
||||
2. Add a payment method
|
||||
3. Go to **Messaging > Fax** and create a Fax Application
|
||||
3. Go to **Messaging > Fax** and create a Fax Application (note the Connection ID)
|
||||
4. Go to **Numbers** and purchase a fax-enabled number (~$1/month)
|
||||
5. Assign the number to your Fax Application
|
||||
6. Go to **API Keys** and copy your API key
|
||||
|
||||
### 2. Configure
|
||||
7. Configure:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your Telnyx credentials and the destination fax number
|
||||
```
|
||||
|
||||
## Linux/Server Setup (CLI + cron)
|
||||
|
||||
### 3. Add Your Claim
|
||||
|
||||
Place your claim PDF in the `claims/` directory:
|
||||
|
|
|
|||
259
setup_telnyx.py
Executable file
259
setup_telnyx.py
Executable file
|
|
@ -0,0 +1,259 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Telnyx setup wizard - automates account provisioning for AutoFax.
|
||||
|
||||
Run this once after signing up at portal.telnyx.com and getting an API key.
|
||||
It will:
|
||||
1. Create a Fax Application
|
||||
2. Search for and purchase a fax-capable phone number
|
||||
3. Assign the number to the Fax Application
|
||||
4. Write the .env file (or autofax_config.json for the GUI)
|
||||
|
||||
Usage:
|
||||
python setup_telnyx.py # interactive
|
||||
python setup_telnyx.py --api-key KEY_xxx --to +18019382102 # non-interactive
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
API = "https://api.telnyx.com/v2"
|
||||
BASE_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def api(method, path, token, **kwargs):
|
||||
"""Make a Telnyx API call. Raises on HTTP error with details."""
|
||||
resp = requests.request(
|
||||
method,
|
||||
f"{API}{path}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
if not resp.ok:
|
||||
detail = resp.text[:500]
|
||||
print(f" API error ({resp.status_code}): {detail}", file=sys.stderr)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def create_fax_application(token: str, name: str = "AutoFax") -> dict:
|
||||
"""Create a Fax Application. Returns the app data including connection ID."""
|
||||
print(f"Creating Fax Application '{name}'...")
|
||||
data = api("POST", "/fax_applications", token, json={
|
||||
"application_name": name,
|
||||
"webhook_event_url": "https://example.com/fax-status",
|
||||
"active": True,
|
||||
})
|
||||
app = data["data"]
|
||||
print(f" Created: {app['application_name']} (ID: {app['id']})")
|
||||
return app
|
||||
|
||||
|
||||
def search_fax_numbers(token: str, area_code: str = None, limit: int = 5) -> list:
|
||||
"""Search for available fax-capable phone numbers."""
|
||||
print("Searching for fax-capable numbers...")
|
||||
params = {
|
||||
"filter[country_code]": "US",
|
||||
"filter[features]": "fax",
|
||||
"filter[phone_number_type]": "local",
|
||||
"filter[limit]": limit,
|
||||
}
|
||||
if area_code:
|
||||
params["filter[national_destination_code]"] = area_code
|
||||
|
||||
data = api("GET", "/available_phone_numbers", token, params=params)
|
||||
numbers = data.get("data", [])
|
||||
if not numbers:
|
||||
print(" No numbers found. Try a different area code.")
|
||||
return numbers
|
||||
|
||||
|
||||
def order_number(token: str, phone_number: str, connection_id: str) -> dict:
|
||||
"""Purchase a phone number and assign it to the fax application."""
|
||||
print(f"Ordering {phone_number}...")
|
||||
data = api("POST", "/number_orders", token, json={
|
||||
"phone_numbers": [{"phone_number": phone_number}],
|
||||
"connection_id": connection_id,
|
||||
})
|
||||
order = data["data"]
|
||||
print(f" Order ID: {order['id']} | Status: {order.get('status', 'pending')}")
|
||||
|
||||
# Wait for order to complete
|
||||
order_id = order["id"]
|
||||
for attempt in range(12):
|
||||
time.sleep(5)
|
||||
check = api("GET", f"/number_orders/{order_id}", token)
|
||||
status = check["data"].get("status", "pending")
|
||||
if status == "success":
|
||||
print(f" Number purchased successfully!")
|
||||
return check["data"]
|
||||
elif status in ("failed",):
|
||||
print(f" Order failed: {check['data']}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f" Waiting... ({status})")
|
||||
|
||||
print(" Order is still processing. Check the Telnyx portal.")
|
||||
return order
|
||||
|
||||
|
||||
def assign_number_to_app(token: str, phone_number_id: str, connection_id: str):
|
||||
"""Assign a purchased number to the fax application."""
|
||||
print(f"Assigning number to fax application...")
|
||||
api("PATCH", f"/phone_numbers/{phone_number_id}", token, json={
|
||||
"connection_id": connection_id,
|
||||
})
|
||||
print(" Done.")
|
||||
|
||||
|
||||
def write_env(api_key: str, connection_id: str, from_number: str, to_number: str):
|
||||
"""Write the .env file for the CLI workflow."""
|
||||
env_path = BASE_DIR / ".env"
|
||||
content = f"""# Generated by setup_telnyx.py
|
||||
TELNYX_API_KEY={api_key}
|
||||
TELNYX_CONNECTION_ID={connection_id}
|
||||
TELNYX_FROM_NUMBER={from_number}
|
||||
FAX_TO_NUMBER={to_number}
|
||||
|
||||
# ntfy notification URL (optional)
|
||||
# NTFY_URL=https://nt.nevo.engineer/autofax
|
||||
# NTFY_TOKEN=Bearer tk_your_token_here
|
||||
"""
|
||||
env_path.write_text(content)
|
||||
print(f"\n Wrote {env_path}")
|
||||
|
||||
|
||||
def write_gui_config(api_key: str, connection_id: str, from_number: str, to_number: str):
|
||||
"""Write the GUI config file."""
|
||||
config_path = BASE_DIR / "autofax_config.json"
|
||||
config = {
|
||||
"api_key": api_key,
|
||||
"connection_id": connection_id,
|
||||
"from_number": from_number,
|
||||
"to_number": to_number,
|
||||
"pdf_path": "",
|
||||
"ntfy_url": "",
|
||||
"ntfy_token": "",
|
||||
}
|
||||
config_path.write_text(json.dumps(config, indent=2))
|
||||
print(f" Wrote {config_path}")
|
||||
|
||||
|
||||
def pick_number(numbers: list) -> str:
|
||||
"""Let the user pick from available numbers."""
|
||||
print("\nAvailable numbers:")
|
||||
for i, n in enumerate(numbers, 1):
|
||||
pn = n.get("phone_number", "?")
|
||||
region = n.get("region_information", [{}])
|
||||
loc = ""
|
||||
if region:
|
||||
loc = f" ({region[0].get('region_name', '')}, {region[0].get('region_type', '')})"
|
||||
cost = n.get("cost_information", {}).get("monthly_cost", "?")
|
||||
print(f" [{i}] {pn}{loc} - ${cost}/mo")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = input(f"\nSelect a number [1-{len(numbers)}]: ").strip()
|
||||
idx = int(choice) - 1
|
||||
if 0 <= idx < len(numbers):
|
||||
return numbers[idx]["phone_number"]
|
||||
except (ValueError, EOFError):
|
||||
pass
|
||||
print("Invalid choice, try again.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Set up Telnyx for AutoFax")
|
||||
parser.add_argument("--api-key", help="Telnyx API key (starts with KEY_)")
|
||||
parser.add_argument("--to", help="Destination fax number in E.164 format")
|
||||
parser.add_argument("--area-code", help="Preferred area code for your fax number")
|
||||
parser.add_argument("--app-name", default="AutoFax", help="Fax application name")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 50)
|
||||
print(" AutoFax - Telnyx Setup Wizard")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
# Get API key
|
||||
api_key = args.api_key
|
||||
if not api_key:
|
||||
print("You need a Telnyx API key. Get one at:")
|
||||
print(" https://portal.telnyx.com/ -> API Keys")
|
||||
print()
|
||||
api_key = input("Paste your API key: ").strip()
|
||||
|
||||
if not api_key.startswith("KEY"):
|
||||
print("Warning: API key usually starts with 'KEY'. Continuing anyway...")
|
||||
|
||||
# Get destination number
|
||||
to_number = args.to
|
||||
if not to_number:
|
||||
to_number = input("Destination fax number (E.164, e.g. +18019382102): ").strip()
|
||||
|
||||
# Verify API key works
|
||||
print("\nVerifying API key...")
|
||||
try:
|
||||
api("GET", "/balance", api_key)
|
||||
print(" API key is valid.")
|
||||
except Exception:
|
||||
print("ERROR: Invalid API key or account issue.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Step 1: Create fax application
|
||||
print()
|
||||
app = create_fax_application(api_key, args.app_name)
|
||||
connection_id = app["id"]
|
||||
|
||||
# Step 2: Search for a number
|
||||
print()
|
||||
numbers = search_fax_numbers(api_key, area_code=args.area_code)
|
||||
if not numbers:
|
||||
# Try without area code filter
|
||||
numbers = search_fax_numbers(api_key)
|
||||
if not numbers:
|
||||
print("ERROR: No fax numbers available. Try again later.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Step 3: Pick and order a number
|
||||
if args.area_code and len(numbers) == 1:
|
||||
chosen = numbers[0]["phone_number"]
|
||||
print(f"\nAuto-selected: {chosen}")
|
||||
else:
|
||||
chosen = pick_number(numbers)
|
||||
|
||||
print()
|
||||
order_number(api_key, chosen, connection_id)
|
||||
|
||||
# Step 4: Write config files
|
||||
print()
|
||||
print("Writing configuration...")
|
||||
write_env(api_key, connection_id, chosen, to_number)
|
||||
write_gui_config(api_key, connection_id, chosen, to_number)
|
||||
|
||||
print()
|
||||
print("=" * 50)
|
||||
print(" Setup complete!")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print(f" Fax Application: {app['application_name']}")
|
||||
print(f" Connection ID: {connection_id}")
|
||||
print(f" From Number: {chosen}")
|
||||
print(f" To Number: {to_number}")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print(" 1. Place your claim PDF in the claims/ directory")
|
||||
print(" 2. Linux: ./install.sh")
|
||||
print(" 3. Windows: Double-click AutoFax.exe")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue