From cccf6fdfd7562a45a0b739609d278e3b32905eb4 Mon Sep 17 00:00:00 2001 From: Adam Brenecki Date: Wed, 9 Aug 2017 14:07:46 +0930 Subject: [PATCH 1/6] Add Sentry to help with debugging --- requirements.txt | 5 ++++- rickroll.py | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f25d35..45e70e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,17 @@ Flask==0.11.1 Jinja2==2.8 MarkupSafe==0.23 PyYAML==3.12 -Unidecode==0.04.19 +Unidecode==0.4.19 Werkzeug==0.11.11 argparse==1.2.1 base58==0.2.4 +blinker==1.4 boto3==1.4.1 botocore==1.4.80 certifi==2016.9.26 cffi==1.9.1 click==6.6 +contextlib2==0.5.5 cryptography==1.5.3 docutils==0.12 enum34==1.1.6 @@ -30,6 +32,7 @@ pycparser==2.17 python-dateutil==2.6.0 python-slugify==1.2.1 pytz==2016.7 +raven==6.1.0 requests==2.12.3 s3transfer==0.1.9 six==1.10.0 diff --git a/rickroll.py b/rickroll.py index 0a5239d..34d1605 100644 --- a/rickroll.py +++ b/rickroll.py @@ -2,6 +2,8 @@ app = Flask(__name__) from twilio import twiml +from raven.contrib.flask import Sentry +sentry = Sentry(app) # Where we're storing all our audio files. url_base = "https://s3-us-west-2.amazonaws.com/true-commitment/" From 4c46443419c2996e1b2984d93ed6113fe495b7cc Mon Sep 17 00:00:00 2001 From: Adam Brenecki Date: Wed, 9 Aug 2017 14:10:16 +0930 Subject: [PATCH 2/6] Enqueue numbers that send incoming SMSes in S3 --- rickroll.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rickroll.py b/rickroll.py index 34d1605..c97994e 100644 --- a/rickroll.py +++ b/rickroll.py @@ -2,9 +2,13 @@ app = Flask(__name__) from twilio import twiml +import boto3 +import os from raven.contrib.flask import Sentry sentry = Sentry(app) +s3 = boto3.resource('s3') +bucket = s3.Bucket(os.environ['data_bucket']) # Where we're storing all our audio files. url_base = "https://s3-us-west-2.amazonaws.com/true-commitment/" @@ -155,5 +159,14 @@ def original(): return str(play_tune(tune)) +@app.route("/sms", methods=["POST"]) +def sms(): + from_number = request.form.get('From') + bucket.put_object( + Key='queue/{}'.format(from_number), + Body='', + ) + return "Hello world!" + if __name__ == "__main__": app.run() From d46e77794b0cea6b17ef2d9f174924de1cb58929 Mon Sep 17 00:00:00 2001 From: Adam Brenecki Date: Wed, 9 Aug 2017 16:42:20 +0930 Subject: [PATCH 3/6] Reply to SMS messages with the first line of the song --- rickroll.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/rickroll.py b/rickroll.py index c97994e..1c1c921 100644 --- a/rickroll.py +++ b/rickroll.py @@ -2,6 +2,8 @@ app = Flask(__name__) from twilio import twiml +from twilio.rest import TwilioRestClient + import boto3 import os from raven.contrib.flask import Sentry @@ -9,6 +11,9 @@ s3 = boto3.resource('s3') bucket = s3.Bucket(os.environ['data_bucket']) +twilio_client = TwilioRestClient(os.environ['twilio_sid'], + os.environ['twilio_token']) + # Where we're storing all our audio files. url_base = "https://s3-us-west-2.amazonaws.com/true-commitment/" @@ -168,5 +173,15 @@ def sms(): ) return "Hello world!" +def send_sms(): + for queue_entry in bucket.objects.filter(Prefix='queue/'): + number = queue_entry.key[6:] + twilio_client.messages.create( + to=number, + from_="+61476856860", + body="We're no strangers to love.", + ) + queue_entry.delete() + if __name__ == "__main__": app.run() From 89c8b6b642c9922bb53c2e18c633a3bba400aa72 Mon Sep 17 00:00:00 2001 From: Adam Brenecki Date: Thu, 10 Aug 2017 16:14:18 +0930 Subject: [PATCH 4/6] Track which SMS messages have already been sent --- rickroll.py | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/rickroll.py b/rickroll.py index 1c1c921..7aad2b2 100644 --- a/rickroll.py +++ b/rickroll.py @@ -5,7 +5,9 @@ from twilio.rest import TwilioRestClient import boto3 +import botocore import os +from datetime import datetime, timedelta from raven.contrib.flask import Sentry sentry = Sentry(app) @@ -79,6 +81,14 @@ _original ] +messages = [ + "We're no strangers to love.", + "You know the rules, and so do I.", + "A full commitment's what I'm thinking of.", + "You wouldn't get this from any other guy.", + "Call me?", +] + # Menu generation. I'd love to put this in its own function to be clean and # tidy, but if I put that at the end Python gets grumpy and I'm not sure how to # forward-declare. I could put it into a separate file and include that, but @@ -176,11 +186,39 @@ def sms(): def send_sms(): for queue_entry in bucket.objects.filter(Prefix='queue/'): number = queue_entry.key[6:] - twilio_client.messages.create( - to=number, - from_="+61476856860", - body="We're no strangers to love.", + + # Find out which message we sent last, if any + # Error handling taken from https://stackoverflow.com/a/33843019 + try: + state_obj = s3.Object( + os.environ['data_bucket'], + 'state/{}'.format(number), + ) + state_obj.load() + except botocore.exceptions.ClientError as e: + if e.response['Error']['Code'] == "404": + last_msg = -1 + else: + raise + else: + last_msg = int(state_obj.get()['Body'].read().decode('utf8')) + + # Send the next one + next_msg = last_msg + 1 + if next_msg < len(messages): + twilio_client.messages.create( + to=number, + from_="+61476856860", + body=messages[next_msg], + ) + + # Record which message we just sent in S3 + bucket.put_object( + Key='state/{}'.format(number), + Body=str(next_msg), + Expires=datetime.now() + timedelta(days=7), ) + queue_entry.delete() if __name__ == "__main__": From 7b81007ed5a32aa4b986577e8933c94233e3e747 Mon Sep 17 00:00:00 2001 From: Adam Brenecki Date: Sun, 13 Aug 2017 16:49:36 +0930 Subject: [PATCH 5/6] Add support for multiple incoming numbers --- rickroll.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/rickroll.py b/rickroll.py index 7aad2b2..9ff8c74 100644 --- a/rickroll.py +++ b/rickroll.py @@ -126,7 +126,7 @@ def play_tune(tune): gather = response.gather(numDigits=1, timeout=10) gather.play(tune['url']) gather.say(menu) - + # Our goodbye triggers after gather times out. response.say(goodbye) @@ -176,30 +176,33 @@ def original(): @app.route("/sms", methods=["POST"]) def sms(): - from_number = request.form.get('From') + """Adds an incoming SMS to a queue, to reply to later.""" bucket.put_object( - Key='queue/{}'.format(from_number), + Key='queue/{to}/{from_}'.format(to=request.form['To'], + from_=request.form['From']), Body='', ) return "Hello world!" def send_sms(): + """Empties the queue of incoming SMSes, replying to each one.""" for queue_entry in bucket.objects.filter(Prefix='queue/'): - number = queue_entry.key[6:] + _, our_number, their_number = queue_entry.key.split("/") + state_key = "state/{}/{}".format(our_number, their_number) # Find out which message we sent last, if any # Error handling taken from https://stackoverflow.com/a/33843019 try: state_obj = s3.Object( os.environ['data_bucket'], - 'state/{}'.format(number), + state_key, ) state_obj.load() except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] == "404": last_msg = -1 else: - raise + raise else: last_msg = int(state_obj.get()['Body'].read().decode('utf8')) @@ -207,18 +210,18 @@ def send_sms(): next_msg = last_msg + 1 if next_msg < len(messages): twilio_client.messages.create( - to=number, - from_="+61476856860", + to=their_number, + from_=our_number, body=messages[next_msg], ) # Record which message we just sent in S3 bucket.put_object( - Key='state/{}'.format(number), + Key=state_key, Body=str(next_msg), Expires=datetime.now() + timedelta(days=7), ) - + queue_entry.delete() if __name__ == "__main__": From b1d56b40ccf4eee5d5ecd6f094bb94463195798e Mon Sep 17 00:00:00 2001 From: Adam Brenecki Date: Sun, 13 Aug 2017 16:50:55 +0930 Subject: [PATCH 6/6] Update documentation for SMS endpoint --- README.md | 37 ++++++++++++++++++++++++++++++------- zappa_settings.example.json | 24 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 zappa_settings.example.json diff --git a/README.md b/README.md index 4afee9e..e229648 100644 --- a/README.md +++ b/README.md @@ -43,14 +43,38 @@ and you've built your own hotline! The main service runs using [AWS Lambda](https://aws.amazon.com/documentation/lambda/), a scalable, serverless platform which removes the overhead of needing to maintain an underlying server. The [Zappa documentation](https://github.com/Miserlou/Zappa#zappa---serverless-python-web-services) -provides detailed set-up instructions, but if you have your `~/.aws/credentials` file in order, -it should be as simple as: +provides detailed set-up instructions, but the process should be as simple as: - $ . venv/bin/activate - $ zappa init - $ zappa deploy +1. [Create a private S3 bucket](https://s3.console.aws.amazon.com/s3/home). The Rick Astley Hotline will store the state of its SMS conversations here. Take a note of its name. +2. [Create an IAM user](https://console.aws.amazon.com/iam/home?region=us-east-1#/users$new?step=details) that has access to the S3 bucket. If your bucket from the previous step was called `rick-astley-data`, the IAM policy to grant access should look something like this: + + { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "s3:*", + "Effect": "Allow", + "Resource": [ + "arn:aws:s3:::rick-astley-data", + "arn:aws:s3:::rick-astley-data/*" + ] + } + ] + } + + Take a note of the Access Key ID and Secret Access Key of your new IAM user. + +3. Sign up for [Twilio](https://www.twilio.com), create a new project, and take a note of its SID and access token. +4. Copy `zappa_settings.example.json` to `zappa_settings.json`. Copy the IAM details, S3 bucket name and Twilio details into the appropriate places. +5. Deploy your project to AWS Lambda: -Record the URL it spits out, connect it as a webhook to your own phone number with Twilio, and you have your own serverless Rick Astley Hotline! + $ . venv/bin/activate + $ zappa init + $ zappa deploy + +6. Record the URL it spits out, connect it as the incoming call webhook to your own phone number with Twilio. Set the incoming SMS webhook to the same URL, but with `/sms` on the end. (You can do this for more than one number.) + +Congratulations! You now have your own serverless Rick Astley Hotline! ## How much do you spend bringing joy to Rick Astley fans? @@ -58,4 +82,3 @@ As of the end of 2016, number and connection costs were averaging about $150 USD worth it for the joy it brings others. If you want to defray my hosting costs, there's always [bitcoin](https://blockchain.info/address/18pgvfqWGs2CvurmNvq58h499RRTPCh3mz) and [Patreon](https://www.patreon.com/_pjf). - diff --git a/zappa_settings.example.json b/zappa_settings.example.json new file mode 100644 index 0000000..ad1f1a6 --- /dev/null +++ b/zappa_settings.example.json @@ -0,0 +1,24 @@ +{ + "dev": { + "app_function": "rickroll.app", + // Put a random S3 bucket name that doesn't already exist here. Zappa + // will create it and put your Lambda code in it when you run + // 'zappa init'. + "s3_bucket": "insert-bucket-name-here", + "aws_region": "us-east-1", + "environment_variables": { + // This should be a bucket WITHOUT public read access. Phone numbers + // will get stored in here. + "data_bucket": "rick-astley-data", + // These should be set to an IAM user with access to your S3 bucket + "aws_access_key_id": "INSERT_ACCESS_KEY_HERE", + "aws_secret_access_key": "insert-secret-access-key-here", + "twilio_sid": "insert-twilio-sid-here", + "twilio_token": "insert-twilio-token-here" + }, + "events": [{ + "function": "rickroll.send_sms", + "expression": "rate(4 minutes)" + }] + } +}