This commit is contained in:
groovecoder 2019-12-21 23:53:47 -06:00
Родитель a3e2d47baf
Коммит 32764c95d1
18 изменённых файлов: 289 добавлений и 1 удалений

Просмотреть файл

@ -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
phones/__init__.py Normal file
Просмотреть файл

10
phones/admin.py Normal file
Просмотреть файл

@ -0,0 +1,10 @@
from django.contrib import admin
from .models import Session
class SessionAdmin(admin.ModelAdmin):
pass
admin.site.register(Session, SessionAdmin)

5
phones/apps.py Normal file
Просмотреть файл

@ -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),
),
]

Просмотреть файл

9
phones/models.py Normal file
Просмотреть файл

@ -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

3
phones/tests.py Normal file
Просмотреть файл

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

9
phones/urls.py Normal file
Просмотреть файл

@ -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),
]

140
phones/views.py Normal file
Просмотреть файл

@ -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