initial commit
This commit is contained in:
173
examples/pcc-dialin-server/.gitignore
vendored
Normal file
173
examples/pcc-dialin-server/.gitignore
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
.DS_Store
|
||||||
|
server/create_room.py
|
||||||
179
examples/pcc-dialin-server/README.md
Normal file
179
examples/pcc-dialin-server/README.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Handling PSTN/SIP Dial-in on Pipecat cloud
|
||||||
|
|
||||||
|
This repository contains two server implementations for handling
|
||||||
|
the pinless dialin workflow in pipecat cloud. This is the companion to the
|
||||||
|
[pstn/sip pcc starter images](https://github.com/daily-co/pipecat-cloud-images/tree/vr000m-starter-image-dial-in-out/pipecat-starters/pstn_sip).
|
||||||
|
In addition you can use `/api/dial` to trigger dial-out, and
|
||||||
|
eventually, call-transfers.
|
||||||
|
|
||||||
|
1. [Simple Python Server](simple-python/README.md) -
|
||||||
|
A FastAPI implementation that handles PSTN (Public Switched Telephone
|
||||||
|
Network) and SIP (Session Initiation Protocol) calls using the Daily API.
|
||||||
|
|
||||||
|
2. [Vercel Serverless](vercel/readme.md) -
|
||||||
|
A Next.js API implementation designed for deployment on Vercel's
|
||||||
|
serverless platform.
|
||||||
|
|
||||||
|
Both implementations provide:
|
||||||
|
- HMAC signature validation for pinless webhook
|
||||||
|
- Structured logging
|
||||||
|
- Support for dialin and dialout settings
|
||||||
|
- Voicemail detection and call transfer functionality (coming soon)
|
||||||
|
- Test request handling
|
||||||
|
|
||||||
|
## Choosing an Implementation
|
||||||
|
|
||||||
|
- Use the **Simple Python Server** if you:
|
||||||
|
- Need a standalone server
|
||||||
|
- Prefer Python and FastAPI
|
||||||
|
- Want to deploy to traditional hosting platforms
|
||||||
|
|
||||||
|
- Use the **Vercel Serverless** implementation if you:
|
||||||
|
- Want serverless deployment
|
||||||
|
- Prefer JavaScript/TypeScript
|
||||||
|
- Already use Vercel for other projects
|
||||||
|
- Need quick scaling and zero maintenance
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Both implementations require similar environment variables:
|
||||||
|
- `DAILY_API_KEY`: Your Daily API key
|
||||||
|
- `AGENT_NAME`: Your Daily agent name
|
||||||
|
- `PINLESS_HMAC_SECRET`: Your HMAC secret for request verification
|
||||||
|
- `LOG_LEVEL`: (Optional) Logging level (defaults to 'info')
|
||||||
|
|
||||||
|
See the individual README files in each implementation directory for
|
||||||
|
specific setup instructions.
|
||||||
|
|
||||||
|
### ### Phone number setup
|
||||||
|
|
||||||
|
You can buy a phone number through the Pipecat Cloud Dashboard:
|
||||||
|
1. Go to `Settings` > `Telephony`
|
||||||
|
2. Follow the UI to purchase a phone number
|
||||||
|
3. Configure the webhook URL to receive incoming calls
|
||||||
|
|
||||||
|
Or purchase the number using Daily's
|
||||||
|
[PhoneNumbers API](https://docs.daily.co/reference/rest-api/phone-numbers).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url https://api.daily.co/v1/domain-dialin-config \
|
||||||
|
--header 'Authorization: Bearer $TOKEN' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data-raw '{
|
||||||
|
"type": "pinless_dialin",
|
||||||
|
"name_prefix": "Customer1",
|
||||||
|
"phone_number": "+1PURCHASED_NUM",
|
||||||
|
"room_creation_api": "https://example.com/api/dial",
|
||||||
|
"hold_music_url": "https://example.com/static/ringtone.mp3",
|
||||||
|
"timeout_config": {
|
||||||
|
"message": "No agent is available right now"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The API will return a static SIP URI (`sip_uri`) that can be called
|
||||||
|
from other SIP services.
|
||||||
|
|
||||||
|
### `room_creation_api`
|
||||||
|
|
||||||
|
To make and receive calls currently you have to host a server that
|
||||||
|
handles incoming calls. In the coming weeks, incoming calls will be
|
||||||
|
directly handled within Daily and we will expose an endpoint similar
|
||||||
|
to `{service}/start` that will manage this for you.
|
||||||
|
|
||||||
|
In the meantime, the server described below serves as the webhook
|
||||||
|
handler for the `room_creation_api`. Configure your pinless phone
|
||||||
|
number or sip interconnect to the `ngrok` tunnel or
|
||||||
|
the actual server URL, append `/api/dial` to the webhook URL.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Examples curl commands
|
||||||
|
|
||||||
|
Note: Replace `http://localhost:3000` with your actual server URL and
|
||||||
|
phone numbers with valid values for your use case.
|
||||||
|
|
||||||
|
### Dialin Request
|
||||||
|
|
||||||
|
The server will receive a request when a call is received from Daily.
|
||||||
|
|
||||||
|
### Dialout Request
|
||||||
|
|
||||||
|
Dial a number, will use any purchased number
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Dial a number with callerId, which is the UUID of a purchased number.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
"callerId": "purchased_phone_uuid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Dial a number
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
"callerId": "purchased_phone_uuid"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Request with Voicemail Detection
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/dial \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"To": "+1234567890",
|
||||||
|
"From": "+1987654321",
|
||||||
|
"callId": "call-uuid-123",
|
||||||
|
"callDomain": "domain-uuid-456",
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+1234567890",
|
||||||
|
"callerId": "purchased_phone_uuid"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"voicemail_detection": {
|
||||||
|
"testInPrebuilt": true
|
||||||
|
},
|
||||||
|
"call_transfer": {
|
||||||
|
"mode": "dialout",
|
||||||
|
"speakSummary": true,
|
||||||
|
"storeSummary": true,
|
||||||
|
"operatorNumber": "+1234567890",
|
||||||
|
"testInPrebuilt": true
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
82
examples/pcc-dialin-server/simple-python/README.md
Normal file
82
examples/pcc-dialin-server/simple-python/README.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Daily PSTN/SIP Server
|
||||||
|
|
||||||
|
A FastAPI server that handles PSTN (Public Switched Telephone Network) and SIP (Session Initiation Protocol) calls using the Daily API.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- FastAPI
|
||||||
|
- Uvicorn
|
||||||
|
- Python-dotenv
|
||||||
|
- Requests
|
||||||
|
- Pydantic
|
||||||
|
- Loguru
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
3. Copy `.env.example` to `.env`:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
4. Update `.env` with your credentials:
|
||||||
|
- `AGENT_NAME`: Your Daily agent name
|
||||||
|
- `DAILY_API_KEY`: Your Daily API key
|
||||||
|
- `PINLESS_HMAC_SECRET`: Your HMAC secret for request verification
|
||||||
|
|
||||||
|
## Running the Server
|
||||||
|
|
||||||
|
Start the server with:
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
The server will run on `http://localhost:3000`
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /
|
||||||
|
Health check endpoint that returns a "Hello, World!" message.
|
||||||
|
|
||||||
|
### POST /api/dial
|
||||||
|
Initiates a PSTN/SIP call with the following request body format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"To": "+14152251493",
|
||||||
|
"From": "+14158483432",
|
||||||
|
"callId": "string-contains-uuid",
|
||||||
|
"callDomain": "string-contains-uuid",
|
||||||
|
"dialout_settings": [
|
||||||
|
{
|
||||||
|
"phoneNumber": "+14158483432",
|
||||||
|
"callerId": "+14152251493"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"voicemail_detection": {
|
||||||
|
"testInPrebuilt": true
|
||||||
|
},
|
||||||
|
"call_transfer": {
|
||||||
|
"mode": "dialout",
|
||||||
|
"speakSummary": true,
|
||||||
|
"storeSummary": true,
|
||||||
|
"operatorNumber": "+14152250006",
|
||||||
|
"testInPrebuilt": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
Returns a JSON object containing:
|
||||||
|
- `status`: Success/failure status
|
||||||
|
- `data`: Response from Daily API
|
||||||
|
- `room_properties`: Properties of the created Daily room
|
||||||
|
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
- 401: Invalid signature
|
||||||
|
- 500: Server errors (missing API key, network issues)
|
||||||
|
- Other status codes are passed through from the Daily API
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn==0.24.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
requests==2.31.0
|
||||||
|
pydantic==2.10.6
|
||||||
|
loguru==0.7.2
|
||||||
197
examples/pcc-dialin-server/simple-python/server.py
Normal file
197
examples/pcc-dialin-server/simple-python/server.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2025, Daily
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD 2-Clause License
|
||||||
|
#
|
||||||
|
|
||||||
|
# server.py
|
||||||
|
|
||||||
|
|
||||||
|
import base64 # for calculating hmac signature
|
||||||
|
import hmac
|
||||||
|
import os # for accessing environment variables
|
||||||
|
import time # for setting expiration time
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
|
from loguru import logger
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
class RoomRequest(BaseModel):
|
||||||
|
test: Optional[str] = Field(None, alias="Test", description="Test field")
|
||||||
|
To: Optional[str] = Field(None, alias="to", description="Destination phone number")
|
||||||
|
From: Optional[str] = Field(None, alias="from", description="Source phone number")
|
||||||
|
callId: Optional[str] = Field(None, alias="call_id", description="Unique call identifier")
|
||||||
|
callDomain: Optional[str] = Field(
|
||||||
|
None, alias="call_domain", description="Call domain identifier"
|
||||||
|
)
|
||||||
|
dialout_settings: Optional[List[Dict[str, Any]]] = Field(
|
||||||
|
None, description="An array of phone numbers or SIP URIs to dialout to"
|
||||||
|
)
|
||||||
|
voicemail_detection: Optional[Dict[str, Any]] = Field(
|
||||||
|
None, description="A flag to perform voicemail or answeing-machine detection"
|
||||||
|
)
|
||||||
|
call_transfer: Optional[Dict[str, Any]] = Field(None, description="to initiate a call transfer")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
populate_by_name = True
|
||||||
|
alias_generator = None
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
body can contain any fields, but for handling PSTN/SIP,
|
||||||
|
we recommend sending the following custom values:
|
||||||
|
dialin, dialout, voicemail detection, and call transfer
|
||||||
|
|
||||||
|
|
||||||
|
"To": "+14152251493",
|
||||||
|
"From": "+14158483432",
|
||||||
|
"callId": "string-contains-uuid",
|
||||||
|
"callDomain": "string-contains-uuid"
|
||||||
|
These need to be remapped to dialin_settings
|
||||||
|
|
||||||
|
"dialout_settings": [
|
||||||
|
{"phoneNumber": "+14158483432", "callerId": "+14152251493"},
|
||||||
|
{"sipUri": "sip:username@sip.hostname"}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
voicemail_detection:{
|
||||||
|
testInPrebuilt: true
|
||||||
|
},
|
||||||
|
|
||||||
|
"call_transfer": {
|
||||||
|
"mode": "dialout",
|
||||||
|
"speakSummary": true,
|
||||||
|
"storeSummary": true,
|
||||||
|
"operatorNumber": "+14152250006",
|
||||||
|
"testInPrebuilt": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def read_root():
|
||||||
|
return {"message": "Hello, World!"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/dial")
|
||||||
|
async def dial(request: RoomRequest, raw_request: Request):
|
||||||
|
logger.info("Incoming request to /dial:")
|
||||||
|
logger.info(f"Headers: {dict(raw_request.headers)}")
|
||||||
|
raw_body = await raw_request.body()
|
||||||
|
raw_body_str = raw_body.decode()
|
||||||
|
logger.info(f"Raw body: {raw_body_str}")
|
||||||
|
logger.info(f"Parsed body: {request.dict()}")
|
||||||
|
|
||||||
|
# calculate signature and compare/verify
|
||||||
|
hmac_secret = os.getenv("PINLESS_HMAC_SECRET")
|
||||||
|
timestamp = raw_request.headers.get("x-pinless-timestamp")
|
||||||
|
|
||||||
|
if timestamp:
|
||||||
|
signature = timestamp + "." + raw_body_str
|
||||||
|
logger.debug(f"Signature: {signature}")
|
||||||
|
|
||||||
|
base64_decoded_secret = base64.b64decode(hmac_secret)
|
||||||
|
computed_signature = base64.b64encode(
|
||||||
|
hmac.new(base64_decoded_secret, signature.encode(), "sha256").digest()
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
if computed_signature != raw_request.headers.get("x-pinless-signature"):
|
||||||
|
logger.error("Invalid signature")
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid signature")
|
||||||
|
|
||||||
|
if request.test == "test":
|
||||||
|
logger.debug("Test request received")
|
||||||
|
return {"status": "success", "message": "Test request received"}
|
||||||
|
|
||||||
|
dialin_settings = None
|
||||||
|
# these fields are camelCase in the request
|
||||||
|
required_fields = ["To", "From", "callId", "callDomain"]
|
||||||
|
if all(
|
||||||
|
field in request.dict() and request.dict()[field] is not None for field in required_fields
|
||||||
|
):
|
||||||
|
# transform from camelCase to snake_case because daily-python expects snake_case
|
||||||
|
dialin_settings = {
|
||||||
|
"From": request.From,
|
||||||
|
"To": request.To,
|
||||||
|
"call_id": request.callId,
|
||||||
|
"call_domain": request.callDomain,
|
||||||
|
# transform from camelCase to snake_case
|
||||||
|
}
|
||||||
|
logger.debug(f"Populated dialin_settings from request: {dialin_settings}")
|
||||||
|
|
||||||
|
daily_room_properties = {
|
||||||
|
"enable_dialout": request.dialout_settings is not None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if dialin_settings is not None:
|
||||||
|
sip_config = {
|
||||||
|
"display_name": "dialin",
|
||||||
|
"sip_mode": "dial-in",
|
||||||
|
"num_endpoints": 2 if request.call_transfer is not None else 1,
|
||||||
|
}
|
||||||
|
daily_room_properties["sip"] = sip_config
|
||||||
|
|
||||||
|
# Setting default expiry to 5 minutes from now
|
||||||
|
daily_room_properties["exp"] = int(time.time()) + (5 * 60)
|
||||||
|
|
||||||
|
logger.debug(f"Daily room properties: {daily_room_properties}")
|
||||||
|
payload = {
|
||||||
|
"createDailyRoom": True,
|
||||||
|
"dailyRoomProperties": daily_room_properties,
|
||||||
|
"body": {
|
||||||
|
"dialin_settings": dialin_settings,
|
||||||
|
"dialout_settings": request.dialout_settings,
|
||||||
|
"voicemail_detection": request.voicemail_detection,
|
||||||
|
"call_transfer": request.call_transfer,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
daily_api_key = os.getenv("DAILY_API_KEY")
|
||||||
|
agent_name = os.getenv("AGENT_NAME", "my-first-agent")
|
||||||
|
|
||||||
|
if not daily_api_key:
|
||||||
|
raise HTTPException(status_code=500, detail="DAILY_API_KEY environment variable is not set")
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {daily_api_key}", "Content-Type": "application/json"}
|
||||||
|
|
||||||
|
url = f"https://api.pipecat.daily.co/v1/public/{agent_name}/start"
|
||||||
|
|
||||||
|
logger.debug(f"Making API call to Daily: {url} {headers} {payload}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
response_data = response.json()
|
||||||
|
logger.debug(f"Response: {response_data}")
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"data": response_data,
|
||||||
|
"room_properties": daily_room_properties,
|
||||||
|
}
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
# Pass through the status code and error details from the Daily API
|
||||||
|
status_code = e.response.status_code
|
||||||
|
error_detail = e.response.json() if e.response.content else str(e)
|
||||||
|
logger.error(f"HTTP error: {error_detail}")
|
||||||
|
raise HTTPException(status_code=status_code, detail=error_detail)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"Request error: {str(e)}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=3000)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("Server stopped manually")
|
||||||
53
examples/pcc-dialin-server/vercel/.gitignore
vendored
Normal file
53
examples/pcc-dialin-server/vercel/.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# IDE specific files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
4
examples/pcc-dialin-server/vercel/env.local.example
Normal file
4
examples/pcc-dialin-server/vercel/env.local.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
PINLESS_HMAC_SECRET=your_hmac_secret
|
||||||
|
DAILY_API_KEY=your_daily_api_key
|
||||||
|
AGENT_NAME=my-first-agent
|
||||||
|
PORT=8000
|
||||||
8888
examples/pcc-dialin-server/vercel/package-lock.json
generated
Normal file
8888
examples/pcc-dialin-server/vercel/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
examples/pcc-dialin-server/vercel/package.json
Normal file
22
examples/pcc-dialin-server/vercel/package.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "my-daily-app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"next": "^14.0.0",
|
||||||
|
"pino": "^8.15.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8.46.0",
|
||||||
|
"eslint-config-next": "^14.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
174
examples/pcc-dialin-server/vercel/pages/api/dial.js
Normal file
174
examples/pcc-dialin-server/vercel/pages/api/dial.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { logger } from '../../lib/utils';
|
||||||
|
import axios from 'axios';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
const validateSignature = (body, signature, timestamp, secret) => {
|
||||||
|
// Skip if any required fields are missing
|
||||||
|
if (!signature || !timestamp || !secret) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decodedSecret = Buffer.from(secret, 'base64');
|
||||||
|
const hmac = crypto.createHmac('sha256', decodedSecret);
|
||||||
|
const signatureData = `${timestamp}.${body}`;
|
||||||
|
const computedSignature = hmac.update(signatureData).digest('base64');
|
||||||
|
|
||||||
|
logger.debug('Signature validation:', {
|
||||||
|
timestamp,
|
||||||
|
signatureData: signatureData.substring(0, 50) + '...',
|
||||||
|
computedSignature,
|
||||||
|
receivedSignature: signature
|
||||||
|
});
|
||||||
|
|
||||||
|
return computedSignature === signature;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error validating signature:', error);
|
||||||
|
return true; // Allow request to proceed on error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(req, res) {
|
||||||
|
// Only allow POST requests
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('Incoming request to /api/dial:');
|
||||||
|
logger.info(`Headers: ${JSON.stringify(req.headers)}`);
|
||||||
|
|
||||||
|
const rawBody = JSON.stringify(req.body);
|
||||||
|
logger.info(`Raw body: ${rawBody}`);
|
||||||
|
|
||||||
|
const signature = req.headers['x-pinless-signature'];
|
||||||
|
const timestamp = req.headers['x-pinless-timestamp'];
|
||||||
|
|
||||||
|
if (signature && timestamp) {
|
||||||
|
logger.info('Validating HMAC signature');
|
||||||
|
if (!validateSignature(rawBody, signature, timestamp, process.env.PINLESS_HMAC_SECRET)) {
|
||||||
|
logger.error('Invalid HMAC signature', { signature, timestamp });
|
||||||
|
return res.status(401).json({
|
||||||
|
error: 'Invalid signature',
|
||||||
|
message: 'Invalid HMAC signature'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info('Skipping HMAC validation - no signature headers present');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract request data
|
||||||
|
const {
|
||||||
|
Test: test,
|
||||||
|
To,
|
||||||
|
From,
|
||||||
|
callId,
|
||||||
|
callDomain,
|
||||||
|
dialout_settings,
|
||||||
|
voicemail_detection,
|
||||||
|
call_transfer
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Handle test requests when a webhook is configured
|
||||||
|
if (test === 'test') {
|
||||||
|
logger.debug('Test request received');
|
||||||
|
return res.status(200).json({ status: 'success', message: 'Test request received' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process dialin settings
|
||||||
|
let dialin_settings = null;
|
||||||
|
const requiredFields = ['To', 'From', 'callId', 'callDomain'];
|
||||||
|
|
||||||
|
if (requiredFields.every(field => req.body[field] !== undefined && req.body[field] !== null)) {
|
||||||
|
dialin_settings = {
|
||||||
|
// snake_case because pipecat expects this format
|
||||||
|
From,
|
||||||
|
To,
|
||||||
|
call_id: callId,
|
||||||
|
call_domain: callDomain,
|
||||||
|
};
|
||||||
|
logger.debug(`Populated dialin_settings from request: ${JSON.stringify(dialin_settings)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up Daily room properties
|
||||||
|
const daily_room_properties = {
|
||||||
|
enable_dialout: dialout_settings !== undefined && dialout_settings !== null,
|
||||||
|
exp: Math.floor(Date.now() / 1000) + (5 * 60), // 5 minutes from now
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configure SIP if dialin settings are provided
|
||||||
|
if (dialin_settings !== null) {
|
||||||
|
const sip_config = {
|
||||||
|
display_name: 'dialin',
|
||||||
|
sip_mode: 'dial-in',
|
||||||
|
num_endpoints: call_transfer !== null ? 2 : 1,
|
||||||
|
};
|
||||||
|
daily_room_properties.sip = sip_config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare payload for {service}/start API call
|
||||||
|
const payload = {
|
||||||
|
createDailyRoom: true,
|
||||||
|
dailyRoomProperties: daily_room_properties,
|
||||||
|
body: {
|
||||||
|
dialin_settings,
|
||||||
|
dialout_settings,
|
||||||
|
voicemail_detection,
|
||||||
|
call_transfer,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug(`Daily room properties: ${JSON.stringify(daily_room_properties)}`);
|
||||||
|
|
||||||
|
// Get Daily API key and agent name from environment variables
|
||||||
|
const dailyApiKey = process.env.DAILY_API_KEY;
|
||||||
|
const agentName = process.env.AGENT_NAME || 'my-first-agent';
|
||||||
|
|
||||||
|
if (!dailyApiKey) {
|
||||||
|
throw new Error('DAILY_API_KEY environment variable is not set');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up headers for Daily API call
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${dailyApiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `https://api.pipecat.daily.co/v1/public/${agentName}/start`;
|
||||||
|
logger.debug(`Making API call to Daily: ${url} ${JSON.stringify(headers)} ${JSON.stringify(payload)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, payload, { headers });
|
||||||
|
logger.debug(`Response: ${JSON.stringify(response.data)}`);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
status: 'success',
|
||||||
|
data: response.data,
|
||||||
|
room_properties: daily_room_properties,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response) {
|
||||||
|
// Pass through status code and error details from the Daily API
|
||||||
|
const statusCode = error.response.status;
|
||||||
|
const errorDetail = error.response.data || error.message;
|
||||||
|
logger.error(`HTTP error: ${JSON.stringify(errorDetail)}`);
|
||||||
|
return res.status(statusCode).json(errorDetail);
|
||||||
|
} else {
|
||||||
|
logger.error(`Request error: ${error.message}`);
|
||||||
|
return res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Unexpected error: ${error.message}`);
|
||||||
|
return res.status(500).json({ error: 'Internal server error', message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure body parser to preserve raw body text
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: {
|
||||||
|
sizeLimit: '1mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
6
examples/pcc-dialin-server/vercel/pages/api/index.js
Normal file
6
examples/pcc-dialin-server/vercel/pages/api/index.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { logger } from '../../lib/utils';
|
||||||
|
|
||||||
|
export default function handler(req, res) {
|
||||||
|
logger.info('Received request to /api');
|
||||||
|
res.status(200).json({ message: 'Hello, World! from ฅ^•ﻌ•^ฅ ☁' });
|
||||||
|
}
|
||||||
82
examples/pcc-dialin-server/vercel/readme.md
Normal file
82
examples/pcc-dialin-server/vercel/readme.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Daily API Backend
|
||||||
|
|
||||||
|
Next.js API routes for handling Daily Pipecat requests.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- API endpoint for handling Daily Pipecat requests
|
||||||
|
- HMAC signature validation
|
||||||
|
- Structured logging with Pino
|
||||||
|
- Support for dialin and dialout settings
|
||||||
|
- Voicemail detection and call transfer functionality
|
||||||
|
- Test request handling
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
3. Create `.env.local` file with your credentials:
|
||||||
|
```
|
||||||
|
DAILY_API_KEY=your_daily_api_key
|
||||||
|
AGENT_NAME=my-first-agent
|
||||||
|
PINLESS_HMAC_SECRET=your_hmac_secret
|
||||||
|
LOG_LEVEL=info
|
||||||
|
```
|
||||||
|
4. Run the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /api
|
||||||
|
Returns a simple "Hello, World!" message with a cute cat emoji to verify the server is running.
|
||||||
|
|
||||||
|
### POST /api/dial
|
||||||
|
Handles dialin and dialout requests for Daily Pipecat.
|
||||||
|
|
||||||
|
#### Test Requests
|
||||||
|
The endpoint handles test requests when a webhook is configured. Send a request with `"Test": "test"` to verify your setup:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Test": "test"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production Request Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// for dial-in from webhook
|
||||||
|
"To": "+14152251493",
|
||||||
|
"From": "+14158483432",
|
||||||
|
"callId": "string-contains-uuid",
|
||||||
|
"callDomain": "string-contains-uuid",
|
||||||
|
// for making a dial out to a phone or SIP
|
||||||
|
"dialout_settings": [
|
||||||
|
{"phoneNumber": "+14158483432", "callerId": "purchased_phone_uuid"},
|
||||||
|
{"sipUri": "sip:username@sip.hostname.com"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The application is configured for Vercel deployment:
|
||||||
|
|
||||||
|
1. Push your code to a Git repository
|
||||||
|
2. Import your project in Vercel dashboard
|
||||||
|
3. Configure environment variables:
|
||||||
|
- `DAILY_API_KEY`
|
||||||
|
- `AGENT_NAME`
|
||||||
|
- `PINLESS_HMAC_SECRET`
|
||||||
|
- `LOG_LEVEL` (optional, defaults to 'info')
|
||||||
|
4. Deploy!
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- HMAC signature validation for request authentication
|
||||||
|
- Environment variables for sensitive credentials
|
||||||
|
- Method validation (POST only for /dial)
|
||||||
6
examples/pcc-dialin-server/vercel/vercel.js
Normal file
6
examples/pcc-dialin-server/vercel/vercel.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
version: 2,
|
||||||
|
buildCommand: "next build",
|
||||||
|
outputDirectory: ".next",
|
||||||
|
cleanUrls: true
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user