Skip to content

Commit e12cbc9

Browse files
committed
feat: dockerfile for twilio shim
1 parent ee2861e commit e12cbc9

File tree

4 files changed

+156
-17
lines changed

4 files changed

+156
-17
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
x-sms-env: &sms_env
2+
_APP_SMS_PROVIDER: ${_APP_SMS_PROVIDER:-sms://twilio:shim@twilio}
3+
_APP_SMS_FROM: ${_APP_SMS_FROM:-+15555555555}
4+
5+
networks:
6+
appwrite_net:
7+
external: true
8+
name: appwrite
9+
10+
volumes:
11+
fake_twilio_certs: {}
12+
php_extra_conf: {}
13+
14+
services:
15+
# one-shot: write php ini into the named volume
16+
php-ini-init:
17+
image: busybox:1.36
18+
volumes:
19+
- php_extra_conf:/out
20+
command: >
21+
sh -lc 'printf "%s\n%s\n"
22+
"curl.cainfo=/usr/local/share/ca-certificates/dev-rootCA.pem"
23+
"openssl.cafile=/usr/local/share/ca-certificates/dev-rootCA.pem"
24+
> /out/zz-dev-ca.ini'
25+
26+
twilio-shim:
27+
image: syntaxsdev/twilio-shim:latest
28+
volumes:
29+
- fake_twilio_certs:/certs
30+
ports: ["4443:443"] # dev-only host access
31+
networks:
32+
appwrite_net:
33+
aliases: [api.twilio.com]
34+
restart: unless-stopped
35+
healthcheck:
36+
test: ["CMD","curl","-k","https://localhost:443/"]
37+
interval: 5s
38+
timeout: 3s
39+
retries: 5
40+
41+
appwrite:
42+
depends_on:
43+
php-ini-init: { condition: service_completed_successfully }
44+
environment:
45+
<<: *sms_env
46+
PHP_INI_SCAN_DIR: /usr/local/etc/php/conf.d:/opt/php-extra-conf.d
47+
volumes:
48+
- fake_twilio_certs:/usr/local/share/ca-certificates:ro
49+
- php_extra_conf:/opt/php-extra-conf.d:ro
50+
networks: [appwrite_net]
51+
52+
appwrite-worker-messaging:
53+
depends_on:
54+
php-ini-init: { condition: service_completed_successfully }
55+
twilio-shim: { condition: service_healthy }
56+
environment:
57+
<<: *sms_env
58+
PHP_INI_SCAN_DIR: /usr/local/etc/php/conf.d:/opt/php-extra-conf.d
59+
volumes:
60+
- fake_twilio_certs:/usr/local/share/ca-certificates:ro
61+
- php_extra_conf:/opt/php-extra-conf.d:ro
62+
networks: [appwrite_net]

twilio-shim/Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
# deps
6+
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
# mkcert (static binary)
10+
RUN curl -L -o /usr/local/bin/mkcert \
11+
https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 \
12+
&& chmod +x /usr/local/bin/mkcert
13+
14+
# app
15+
COPY requirements.txt .
16+
RUN pip install --no-cache-dir -r requirements.txt
17+
COPY main.py ./main.py
18+
19+
# entrypoint that generates certs then runs HTTPS
20+
COPY entrypoint.sh /entrypoint.sh
21+
RUN chmod +x /entrypoint.sh
22+
23+
EXPOSE 443
24+
ENTRYPOINT ["/entrypoint.sh"]

twilio-shim/entrypoint.sh

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
CERT_DIR=${CERT_DIR:-/certs}
5+
CAROOT="${CERT_DIR}/ca"
6+
mkdir -p "${CAROOT}"
7+
8+
# Generate CA + leaf if missing
9+
if [ ! -f "${CERT_DIR}/api.twilio.com.crt" ] || [ ! -f "${CERT_DIR}/api.twilio.com.key" ]; then
10+
echo ">> Generating dev CA and leaf cert..."
11+
export CAROOT
12+
mkcert -install >/dev/null 2>&1 || true
13+
mkcert -key-file "${CERT_DIR}/api.twilio.com.key" \
14+
-cert-file "${CERT_DIR}/api.twilio.com.crt" \
15+
"api.twilio.com"
16+
cp "${CAROOT}/rootCA.pem" "${CERT_DIR}/dev-rootCA.pem"
17+
fi
18+
19+
echo ">> Starting Twilio shim (HTTPS)…"
20+
exec uvicorn main:app \
21+
--host 0.0.0.0 --port 443 \
22+
--ssl-keyfile "${CERT_DIR}/api.twilio.com.key" \
23+
--ssl-certfile "${CERT_DIR}/api.twilio.com.crt"

twilio-shim/main.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
from fastapi import FastAPI, Body, Form
1+
from fastapi import FastAPI, Body, Form, Request
22
from fastapi.responses import HTMLResponse, JSONResponse
33
from typing import List, Dict
44
from datetime import datetime
55

66
app = FastAPI()
77
MESSAGES: List[Dict] = []
88

9+
910
@app.post("/inbox")
1011
async def inbox(payload: Dict = Body(...)):
1112
payload["ts"] = datetime.utcnow().isoformat() + "Z"
@@ -14,32 +15,61 @@ async def inbox(payload: Dict = Body(...)):
1415
del MESSAGES[:-100]
1516
return {"ok": True}
1617

18+
1719
@app.get("/inbox")
1820
def list_inbox():
1921
return {"messages": MESSAGES}
2022

23+
2124
# Twilio-compatible endpoint
2225
@app.post("/2010-04-01/Accounts/{sid}/Messages.json")
23-
async def send_sms(sid: str, To: str = Form(...), From: str = Form(...), Body: str = Form(...)):
24-
print(f"[FAKE-TWILIO] SID={sid} To={To} From={From} Body={Body}")
25-
MESSAGES.append({
26-
"to": To,
27-
"frm": From,
28-
"body": Body,
29-
"ts": datetime.utcnow().isoformat() + "Z"
30-
})
31-
return JSONResponse(status_code=201, content={
32-
"sid": "SM_fake123",
33-
"status": "queued",
34-
"to": To,
35-
"from": From,
36-
"body": Body,
37-
})
26+
async def send_sms(sid: str, request: Request):
27+
form = await request.form()
28+
data = dict(form) # includes any extra fields
29+
to = data.get("To")
30+
body = data.get("Body")
31+
from_ = data.get("From") # optional
32+
mssid = data.get("MessagingServiceSid") # optional
33+
34+
# Log everything we got to see what Appwrite is actually posting
35+
print(f"[FAKE-TWILIO] sid={sid} payload={data}")
36+
MESSAGES.append(
37+
{
38+
"to": to,
39+
"frm": from_,
40+
"body": body,
41+
"ts": datetime.utcnow().isoformat() + "Z",
42+
}
43+
)
44+
45+
# Minimal Twilio-style validation: need To + Body + (From or MessagingServiceSid)
46+
if not to or not body or not (from_ or mssid):
47+
return JSONResponse(
48+
status_code=400,
49+
content={
50+
"code": 21601,
51+
"message": "Missing required parameter: 'To'/'Body' or sender",
52+
},
53+
)
54+
55+
# Return Twilio-like success
56+
return JSONResponse(
57+
status_code=201,
58+
content={
59+
"sid": "SM_fake123",
60+
"status": "queued",
61+
"to": to,
62+
"from": from_,
63+
"messaging_service_sid": mssid,
64+
"body": body,
65+
},
66+
)
67+
3868

3969
@app.get("/", response_class=HTMLResponse)
4070
def home():
4171
rows = "".join(
42-
f"<tr><td>{m.get('to','')}</td><td>{m.get('frm','')}</td><td>{m.get('body','')}</td><td>{m.get('ts','')}</td></tr>"
72+
f"<tr><td>{m.get('to', '')}</td><td>{m.get('frm', '')}</td><td>{m.get('body', '')}</td><td>{m.get('ts', '')}</td></tr>"
4373
for m in reversed(MESSAGES)
4474
)
4575
return f"""

0 commit comments

Comments
 (0)