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/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..9ff8c74 100644 --- a/rickroll.py +++ b/rickroll.py @@ -2,6 +2,19 @@ app = Flask(__name__) from twilio import twiml +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) + +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/" @@ -68,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 @@ -105,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) @@ -153,5 +174,55 @@ def original(): return str(play_tune(tune)) +@app.route("/sms", methods=["POST"]) +def sms(): + """Adds an incoming SMS to a queue, to reply to later.""" + bucket.put_object( + 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/'): + _, 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_key, + ) + 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=their_number, + from_=our_number, + body=messages[next_msg], + ) + + # Record which message we just sent in S3 + bucket.put_object( + Key=state_key, + Body=str(next_msg), + Expires=datetime.now() + timedelta(days=7), + ) + + queue_entry.delete() + if __name__ == "__main__": app.run() 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)" + }] + } +}