-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain.py
More file actions
251 lines (203 loc) · 8.84 KB
/
main.py
File metadata and controls
251 lines (203 loc) · 8.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import os
import random
from datetime import datetime, timedelta
from fastapi import FastAPI, Depends, Form
from typing import Optional
from fastapi.responses import Response
from sqlalchemy.orm import Session
from twilio.twiml.messaging_response import MessagingResponse
from database import Base, engine, get_db, init_db
from dotenv import load_dotenv
import smtplib
from email.message import EmailMessage
import re
from models import User, Rides
from utils import create_ride_and_try_match, cancel_active_ride # 👈 import from utils
load_dotenv() # loads variables from a .env file into os.environ
# -----------------------
# DB setup: create tables
# -----------------------
Base.metadata.create_all(bind=engine)
app = FastAPI()
@app.on_event("startup")
def on_startup():
init_db()
# -----------------------
# Helpers
# -----------------------
TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID")
TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN")
TWILIO_WHATSAPP_NUMBER = os.getenv("TWILIO_WHATSAPP_NUMBER") # e.g. "whatsapp:+1415xxxxxxx"
def generate_otp() -> str:
"""Generate a 6-digit zero-padded OTP as a string."""
return f"{random.randint(0, 999999):06d}"
def send_verification_email(emory_email: str, code: str):
"""
Send the verification code to the user's Emory email using SMTP.
Expects these env vars to be set (e.g. in a .env file):
SMTP_HOST - e.g. "smtp.gmail.com"
SMTP_PORT - e.g. "587"
SMTP_USER - the login username (e.g. your email)
SMTP_PASS - the SMTP/app password
FROM_EMAIL - "From" address (often same as SMTP_USER)
"""
smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
smtp_port = int(os.getenv("SMTP_PORT", "587"))
smtp_user = os.getenv("SMTP_USER")
smtp_pass = os.getenv("SMTP_PASS")
from_email = os.getenv("FROM_EMAIL", smtp_user)
print(
"[EMAIL-DEBUG] Runtime env:",
"SMTP_USER:", repr(smtp_user),
"SMTP_PASS set?", bool(smtp_pass),
"FROM_EMAIL:", repr(from_email),
)
if not smtp_user or not smtp_pass:
# Fail gracefully in dev if not configured
print(
f"[EMAIL-DEBUG] Missing SMTP_USER/SMTP_PASS; "
f"would have sent code {code} to {emory_email}"
)
return
msg = EmailMessage()
msg["Subject"] = "Your RideBuddy Verification Code"
msg["From"] = from_email
msg["To"] = emory_email
msg.set_content(
f"""Hi,
Your RideBuddy verification code is: {code}
Enter this code in WhatsApp to complete verification.
If you did not request this, you can ignore this email.
Thanks,
RideBuddy
"""
)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
print(f"[EMAIL] Sent verification code to {emory_email}")
# -----------------------
# Twilio SMS webhook
# -----------------------
@app.post("/sms")
async def sms_webhook(
From: str = Form(...), # Twilio sends "From" as the sender's phone number
Body: str = Form(""), # Twilio sends "Body" as the message text
NumMedia: int = Form(0), # Number of media items from Twilio
MediaUrl0: Optional[str] = Form(None), # URL of the first media item
db: Session = Depends(get_db),
):
from_number = From.strip()
body = (Body or "").strip()
resp = MessagingResponse()
body = "" # Initialize body as an empty string
# --- Speech-to-Text & Body Handling ---
# Check if a voice message was sent.
if NumMedia > 0 and MediaUrl0:
from utils import transcribe_audio_with_elevenlabs
print(f"Received voice message. Transcribing from {MediaUrl0}...")
transcribed_text = transcribe_audio_with_elevenlabs(MediaUrl0)
if transcribed_text:
body = transcribed_text.strip()
else:
# Handle transcription failure by sending a message back to the user.
resp.message("Sorry, I had trouble understanding your voice message. Could you please try sending a text message instead?")
return Response(content=str(resp), media_type="application/xml")
elif Body is not None:
# Fallback to the text message body if no media is present.
body = Body.strip()
else:
body = "" # Ensure body is a string if both are None
# 1) Get or create user by phone number.
user = db.query(User).filter(User.phone_number == from_number).one_or_none()
if user is None:
user = User(
phone_number=from_number,
is_verified=False,
emory_email=None,
otp_code=None,
)
db.add(user)
db.commit()
db.refresh(user)
# 2) Onboarding / verification flow
if not user.is_verified:
# STEP 1: Ask for their full name first
if user.full_name is None:
name = body.strip()
# If this doesn't look like a full name yet (no space, too short),
# just treat this as the initial ping ("hi", "hey", etc.)
# and prompt them for their full name.
if " " not in name or len(name) < 3:
resp.message(
"Welcome to RideBuddy! 🚕\n\n"
"To get started, please reply with your full name "
"(for example: 'Akhil Arularasu')."
)
return Response(content=str(resp), media_type="application/xml")
# Looks like a real name → save it
user.full_name = name
db.commit()
resp.message(
f"Nice to meet you, {name}! 🎉\n\n"
"Now please reply with your Emory email ending in @emory.edu."
)
return Response(content=str(resp), media_type="application/xml")
# STEP 2: We know their name but not their email → treat message as email step
if user.emory_email is None:
em_raw = body.strip()
em = em_raw.lower()
# 1) If it doesn't even look like an email → instructions
if "@" not in em or "." not in em.split("@")[-1]:
resp.message(
"Please reply with your Emory email ending in @emory.edu.\n\n"
"Example: akhil.arularasu@emory.edu"
)
return Response(content=str(resp), media_type="application/xml")
# 2) Allowed domain: Emory only (Gatech allowed silently for your testing)
if not em.endswith(("@emory.edu", "@gmail.com")):
resp.message(
"The RideBuddy service is currently only available to Emory students.\n\n"
"Please reply with a valid Emory email ending in @emory.edu."
)
return Response(content=str(resp), media_type="application/xml")
# 3) Valid email → save & send OTP
user.emory_email = em
code = generate_otp()
user.otp_code = code
db.commit()
send_verification_email(user.emory_email, code)
resp.message(
f"Thanks {user.full_name}! We sent a 6-digit code to {user.emory_email}. "
"Reply with that code here to verify your account."
)
return Response(content=str(resp), media_type="application/xml")
# STEP 3: We know name + email → expect OTP in this message
if body.strip() == (user.otp_code or ""):
user.is_verified = True
user.otp_code = None
db.commit()
resp.message(
f"You're verified ✅, {user.full_name}!\n\n"
"From now on, just send your ride requests like:\n"
"'8:30 am 11/17 emory to airport'.\n\n"
"You can cancel your ride at any time by replying 'cancel'."
)
return Response(content=str(resp), media_type="application/xml")
else:
resp.message(
"That code is incorrect. Please reply with the 6-digit code we sent "
f"to {user.emory_email}."
)
return Response(content=str(resp), media_type="application/xml")
# 3) User is verified at this point
# If they type "cancel" -> cancel active ride instead of creating a new one
if body.strip().lower() == "cancel":
msg = cancel_active_ride(db, user)
resp.message(msg)
return Response(content=str(resp), media_type="application/xml")
# Otherwise treat message as a ride request
response_text = create_ride_and_try_match(db, user, body)
resp.message(response_text)
return Response(content=str(resp), media_type="application/xml")