initial twilio proxy code
This commit is contained in:
Родитель
a3e2d47baf
Коммит
32764c95d1
|
@ -2,3 +2,6 @@ FXA_OAUTH_ENDPOINT=https://oauth-stable.dev.lcip.org/v1
|
|||
FXA_PROFILE_ENDPOINT=https://stable.dev.lcip.org/profile/v1
|
||||
SECRET_KEY=
|
||||
SENDGRID_API_KEY=
|
||||
TWILIO_ACCOUNT_SID=
|
||||
TWILIO_AUTH_TOKEN=
|
||||
TWILIO_SERVICE_ID=
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import Session
|
||||
|
||||
|
||||
class SessionAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(Session, SessionAdmin)
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PhonesConfig(AppConfig):
|
||||
name = 'phones'
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 2.2.9 on 2019-12-22 19:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('twilio_sid', models.CharField(max_length=34, unique=True)),
|
||||
('initiating_proxy_number', models.CharField(max_length=20)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.9 on 2019-12-22 20:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('phones', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='session',
|
||||
name='initiating_real_number',
|
||||
field=models.CharField(default='', max_length=20),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 2.2.9 on 2019-12-23 15:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('phones', '0002_session_initiating_real_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='session',
|
||||
name='initiating_participant_sid',
|
||||
field=models.CharField(default='', max_length=20),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2.9 on 2019-12-23 18:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('phones', '0003_session_initiating_participant_sid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='session',
|
||||
name='status',
|
||||
field=models.CharField(default='', max_length=20),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='session',
|
||||
name='initiating_participant_sid',
|
||||
field=models.CharField(max_length=34),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,9 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
twilio_sid = models.CharField(max_length=34, unique=True, blank=False)
|
||||
initiating_proxy_number = models.CharField(max_length=20, blank=False)
|
||||
initiating_real_number = models.CharField(max_length=20, blank=False)
|
||||
initiating_participant_sid = models.CharField(max_length=34, blank=False)
|
||||
status = models.CharField(max_length=20, blank=False)
|
|
@ -0,0 +1,17 @@
|
|||
sequenceDiagram
|
||||
User->>Relay Phone Number: TXT {minutes}
|
||||
Relay Phone Number->>Proxy Number: Creates {minutes} session
|
||||
Relay Phone Number->>User: TXT {proxy number}
|
||||
User->>3rd Party: give {proxy number} as phone number
|
||||
loop proxied TXT conversation
|
||||
3rd Party->>Proxy Number: TXT
|
||||
Proxy Number->>User: TXT
|
||||
User->>Proxy Number: TXT
|
||||
Proxy Number->>3rd Party: TXT
|
||||
end
|
||||
opt TODO: manual stop
|
||||
User->>Proxy Number: TXT "stop"
|
||||
end
|
||||
opt manual reset
|
||||
User->>Relay Phone Number: TXT "reset"
|
||||
end
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
После Ширина: | Высота: | Размер: 14 KiB |
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('main-twilio-webhook', views.main_twilio_webhook),
|
||||
path('twilio-proxy-out-of-session', views.twilio_proxy_out_of_session),
|
||||
]
|
|
@ -0,0 +1,140 @@
|
|||
from decouple import config
|
||||
from phonenumbers import parse, format_number
|
||||
from phonenumbers import PhoneNumberFormat
|
||||
from twilio.rest import Client
|
||||
from twilio.twiml.messaging_response import MessagingResponse
|
||||
|
||||
from django.core.exceptions import MultipleObjectsReturned
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from .models import Session
|
||||
|
||||
|
||||
account_sid = config('TWILIO_ACCOUNT_SID', None)
|
||||
auth_token = config('TWILIO_AUTH_TOKEN', None)
|
||||
client = Client(account_sid, auth_token)
|
||||
service = client.proxy.services(config('TWILIO_SERVICE_ID'))
|
||||
|
||||
|
||||
PROMPT_MESSAGE = "For how many minutes do you need a relay number?"
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def main_twilio_webhook(request):
|
||||
resp = MessagingResponse()
|
||||
from_num = request.POST['From']
|
||||
body = request.POST['Body'].lower()
|
||||
|
||||
if body == 'reset':
|
||||
from_num_sessions = Session.objects.filter(
|
||||
initiating_real_number=from_num
|
||||
)
|
||||
for session in from_num_sessions:
|
||||
service.sessions(session.twilio_sid).update(status='closed')
|
||||
from_num_sessions.delete()
|
||||
resp.message(
|
||||
"Relay session reset. \n%s" % PROMPT_MESSAGE
|
||||
)
|
||||
return HttpResponse(resp)
|
||||
|
||||
# TODO: remove this check; allow users to have multiple sessions at once?
|
||||
if body != 'reset':
|
||||
existing_sessions = Session.objects.filter(
|
||||
initiating_real_number=from_num
|
||||
)
|
||||
if existing_sessions:
|
||||
pretty_proxy = format_number(
|
||||
parse(existing_sessions[0].initiating_proxy_number),
|
||||
PhoneNumberFormat.NATIONAL
|
||||
)
|
||||
resp.message(
|
||||
"You already have a relay number: \n%s. \n"
|
||||
"Reply 'reset' to reset it." % pretty_proxy
|
||||
)
|
||||
return HttpResponse(resp)
|
||||
|
||||
try:
|
||||
ttl_minutes = int(body)
|
||||
except ValueError:
|
||||
resp.message(PROMPT_MESSAGE)
|
||||
return HttpResponse(resp)
|
||||
|
||||
session = service.sessions.create(ttl=ttl_minutes*60,)
|
||||
participant = service.sessions(session.sid).participants.create(
|
||||
identifier=from_num
|
||||
)
|
||||
proxy_num = participant.proxy_identifier
|
||||
|
||||
# store this half-way open session in our local DB,
|
||||
# so when the next number texts the proxy number,
|
||||
# we can add them as the 2nd participant and open the session
|
||||
Session.objects.create(
|
||||
twilio_sid = session.sid,
|
||||
initiating_proxy_number=proxy_num,
|
||||
initiating_real_number=from_num,
|
||||
initiating_participant_sid=participant.sid,
|
||||
status='waiting-for-party',
|
||||
)
|
||||
|
||||
# reply back with the number and minutes it will live
|
||||
pretty_from = format_number(parse(from_num), PhoneNumberFormat.NATIONAL)
|
||||
pretty_proxy = format_number(parse(proxy_num), PhoneNumberFormat.NATIONAL)
|
||||
resp.message(
|
||||
'%s will forward to this number for %s minutes' %
|
||||
(pretty_proxy, ttl_minutes)
|
||||
)
|
||||
return HttpResponse(resp)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def twilio_proxy_out_of_session(request):
|
||||
"""
|
||||
By design, Relay doesn't know who the 2nd participant is before they text
|
||||
the 1st participant.
|
||||
|
||||
Twilio requires both participants be added to a session before either can
|
||||
communicate with the other. When the 2nd participant sends their first text
|
||||
to a 1st participant, it triggers an "out-of-session" hook.
|
||||
|
||||
So, we use this to add the 2nd participant to the existing session, relay
|
||||
the 2nd participant's message, and the 2 parties can begin communicating
|
||||
thru the proxy number.
|
||||
|
||||
TODO: detect real out-of-session messages - i.e., not first messages
|
||||
"""
|
||||
resp = MessagingResponse()
|
||||
|
||||
try:
|
||||
db_session = Session.objects.get(
|
||||
initiating_proxy_number=request.POST['To'],
|
||||
status='waiting-for-party',
|
||||
)
|
||||
except (Session.DoesNotExist, MultipleObjectsReturned) as e:
|
||||
print(e)
|
||||
resp.message('The person you are trying to reach is unavailable.')
|
||||
return HttpResponse(resp)
|
||||
|
||||
twilio_session = service.sessions(db_session.twilio_sid).fetch()
|
||||
if (twilio_session.status in ['closed', 'failed', 'unknown']):
|
||||
error_message = ('Twilio session %s status: %s' %
|
||||
(db_session.twilio_sid, twilio_session.status))
|
||||
print(error_message)
|
||||
return HttpResponseNotFound(error_message)
|
||||
|
||||
from_num = request.POST['From']
|
||||
new_participant = service.sessions(twilio_session.sid).participants.create(
|
||||
identifier=from_num
|
||||
)
|
||||
db_session.status = 'connected-to-party'
|
||||
db_session.save()
|
||||
# Now that we've added the 2nd participant,
|
||||
# send their first message to the 1st participant
|
||||
message = (
|
||||
service.sessions(twilio_session.sid)
|
||||
.participants(db_session.initiating_participant_sid)
|
||||
.message_interactions.create(body=request.POST['Body'])
|
||||
)
|
||||
|
||||
return HttpResponse(status=201, content="Created")
|
|
@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
|||
'allauth.socialaccount.providers.fxa',
|
||||
|
||||
'emails.apps.EmailsConfig',
|
||||
'phones.apps.PhonesConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
|
|
@ -24,5 +24,6 @@ urlpatterns = [
|
|||
path('admin/', admin.site.urls),
|
||||
path('accounts/', include('allauth.urls')),
|
||||
path('emails/', include('emails.urls')),
|
||||
path('phones/', include('phones.urls')),
|
||||
path('', views.home),
|
||||
]
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
Django==2.2.2
|
||||
Django==2.2.9
|
||||
django-allauth==0.39.1
|
||||
django-csp==3.5
|
||||
django-heroku==0.3.1
|
||||
django-referrer-policy==1.0
|
||||
gunicorn==19.9.0
|
||||
phonenumbers==8.11.1
|
||||
python-decouple==3.1
|
||||
sendgrid==6.0.5
|
||||
twilio==6.35.1
|
||||
|
|
Загрузка…
Ссылка в новой задаче