├── .gitignore ├── Main.py ├── Procfile ├── README.md ├── Twitter.py ├── example-scripts ├── create-webhook.py └── subscribe-account.py ├── requirements.txt ├── runtime.txt └── www └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | *.pyc 4 | venv 5 | -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from flask import Flask, request, send_from_directory, make_response 3 | from http import HTTPStatus 4 | 5 | import Twitter, hashlib, hmac, base64, os, logging, json 6 | 7 | CONSUMER_SECRET = os.environ.get('CONSUMER_SECRET', None) 8 | CURRENT_USER_ID = os.environ.get('CURRENT_USER_ID', None) 9 | 10 | app = Flask(__name__) 11 | 12 | #generic index route 13 | @app.route('/') 14 | def default_route(): 15 | return send_from_directory('www', 'index.html') 16 | 17 | #The GET method for webhook should be used for the CRC check 18 | #TODO: add header validation (compare_digest https://docs.python.org/3.6/library/hmac.html) 19 | @app.route("/webhook", methods=["GET"]) 20 | def twitterCrcValidation(): 21 | 22 | crc = request.args['crc_token'] 23 | 24 | validation = hmac.new( 25 | key=bytes(CONSUMER_SECRET, 'utf-8'), 26 | msg=bytes(crc, 'utf-8'), 27 | digestmod = hashlib.sha256 28 | ) 29 | digested = base64.b64encode(validation.digest()) 30 | response = { 31 | 'response_token': 'sha256=' + format(str(digested)[2:-1]) 32 | } 33 | print('responding to CRC call') 34 | 35 | return json.dumps(response) 36 | 37 | #The POST method for webhook should be used for all other API events 38 | #TODO: add event-specific behaviours beyond Direct Message and Like 39 | @app.route("/webhook", methods=["POST"]) 40 | def twitterEventReceived(): 41 | 42 | requestJson = request.get_json() 43 | 44 | #dump to console for debugging purposes 45 | print(json.dumps(requestJson, indent=4, sort_keys=True)) 46 | 47 | if 'favorite_events' in requestJson.keys(): 48 | #Tweet Favourite Event, process that 49 | likeObject = requestJson['favorite_events'][0] 50 | userId = likeObject.get('user', {}).get('id') 51 | 52 | #event is from myself so ignore (Favourite event fires when you send a DM too) 53 | if userId == CURRENT_USER_ID: 54 | return ('', HTTPStatus.OK) 55 | 56 | Twitter.processLikeEvent(likeObject) 57 | 58 | elif 'direct_message_events' in requestJson.keys(): 59 | #DM recieved, process that 60 | eventType = requestJson['direct_message_events'][0].get("type") 61 | messageObject = requestJson['direct_message_events'][0].get('message_create', {}) 62 | messageSenderId = messageObject.get('sender_id') 63 | 64 | #event type isnt new message so ignore 65 | if eventType != 'message_create': 66 | return ('', HTTPStatus.OK) 67 | 68 | #message is from myself so ignore (Message create fires when you send a DM too) 69 | if messageSenderId == CURRENT_USER_ID: 70 | return ('', HTTPStatus.OK) 71 | 72 | Twitter.processDirectMessageEvent(messageObject) 73 | 74 | else: 75 | #Event type not supported 76 | return ('', HTTPStatus.OK) 77 | 78 | return ('', HTTPStatus.OK) 79 | 80 | 81 | if __name__ == '__main__': 82 | # Bind to PORT if defined, otherwise default to 65010. 83 | port = int(os.environ.get('PORT', 65010)) 84 | gunicorn_logger = logging.getLogger('gunicorn.error') 85 | app.logger.handlers = gunicorn_logger.handlers 86 | app.logger.setLevel(gunicorn_logger.level) 87 | app.run(host='0.0.0.0', port=port, debug=True) 88 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn Main:app --log-file=- 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitter Webhook Boilerplate Python 2 | 3 | This is a work-in-progress port of a Twitter webhook server to Python 3.x. 4 | Several TODOs are noted in the code, but this should be functional. 5 | 6 | Starter app / scripts for consuming events via Account Activity API. 7 | 8 | The current functionality when setup includes: 9 | 10 | 1. When subscribed user receives a Direct Message that is 'Hello Bot', will reply with 'Hello World' 11 | 2. When a Tweet posted from the subscribed account is liked, the user who liked it's Screen Name will be printed 12 | 13 | ## Dependencies 14 | 15 | * A Twitter app created on [apps.twitter.com](https://apps.twitter.com/) 16 | * [Python](https://www.python.org) 17 | * requires TwitterAPI Python library >= 2.4.8 18 | * [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) (optional) 19 | 20 | ## Create and configure a Twitter app 21 | 22 | 1. Create a Twitter app on [apps.twitter.com](https://apps.twitter.com/) 23 | 24 | 2. On the **Permissions** tab > **Access** section > enable **Read, Write and Access direct messages**. 25 | 26 | 3. On the **Keys and Access Tokens** tab > **Your Access Token** section > click **Create my access token** button. 27 | 28 | 4. On the **Keys and Access Tokens** tab, take note of the `consumer key`, `consumer secret`, `access token` and `access token secret`. 29 | 30 | ## Setup the web app 31 | 32 | 1. Clone this repository: 33 | 34 | ```bash 35 | git clone https://github.com/rickredsix/twitter-webhook-boilerplate-python.git 36 | ``` 37 | 38 | 2. Create a virtual environment 39 | 40 | ```bash 41 | virtualenv venv 42 | ``` 43 | 44 | 3. Activate virtual environment: 45 | 46 | Note that the included venv is Python 2.7 and this current code requires 3.6.x 47 | 48 | ```bash 49 | source venv/bin/activate 50 | ``` 51 | 52 | 4. Install python requirements 53 | 54 | ```bash 55 | pip install -r requirements.txt 56 | ``` 57 | 58 | 5. Define key variables locally using the keys and access tokens noted previously (this is only for local example scripts, replace the text after the =) 59 | 60 | ```bash 61 | export CONSUMER_KEY={INSERT_CONSUMER_KEY} 62 | export CONSUMER_SECRET={INSERT_CONSUMER_SECRET} 63 | export ACCESS_TOKEN={INSERT_ACCESS_TOKEN} 64 | export ACCESS_TOKEN_SECRET={INSERT_ACCESS_TOKEN_SECRET} 65 | export ENVNAME={INSERT_TWITTER_DEV_ENV_NAME} 66 | export WEBHOOK_URL={WEBHOOK_URL_AFTER_DEPLOYMENT} 67 | ``` 68 | 69 | 6. Deploy app. To deploy to Heroku see "Deploy to Heroku" instructions below. 70 | 71 | Take note of your webhook URL. For example: 72 | 73 | ```bash 74 | https://your.app.domain/webhook 75 | ``` 76 | 77 | ## Configure webhook to receive events via the API 78 | 79 | 1. Create webhook config. 80 | 81 | ```bash 82 | python example-scripts/create-webhook.py 83 | ``` 84 | 85 | (Take note of returned `webhook_id`). 86 | 87 | 2. Add user subscription. 88 | 89 | ```bash 90 | python example-scripts/subscribe-account.py 91 | ``` 92 | 93 | Subscription will be created for user the context provided by the access tokens. By default the tokens on the app page are the account that created the app. 94 | 95 | ## Deploy to Heroku (optional) 96 | 97 | 1. Init Heroku app. 98 | 99 | ```bash 100 | heroku create 101 | ``` 102 | 103 | 2. Run locally. (This won't do receive the events as you'll have to configure the webhook URL above as the Heroku URL) 104 | 105 | ```bash 106 | heroku local 107 | ``` 108 | 109 | 3. Configure environment variables. Set up required environmental variables, these will be the keys and access tokens again, plus the Twitter ID of the account that is subscribed. You can find this on the app page listed as Owner ID. See Heroku documentation on [Configuration and Config Vars](https://devcenter.heroku.com/articles/config-vars). 110 | 111 | ```bash 112 | heroku config:set CONSUMER_KEY={INSERT_CONSUMER_KEY} 113 | heroku config:set CONSUMER_SECRET={INSERT_CONSUMER_SECRET} 114 | heroku config:set ACCESS_TOKEN={INSERT_ACCESS_TOKEN} 115 | heroku config:set ACCESS_TOKEN_SECRET={INSERT_ACCESS_TOKEN_SECRET} 116 | heroku config:set CURRENT_USER_ID={INSERT_USER_ID} 117 | ``` 118 | 119 | 4. Deploy to Heroku. 120 | 121 | ```bash 122 | git push heroku master 123 | ``` 124 | 125 | ## Documentation 126 | 127 | * [Direct Message API](https://developer.twitter.com/en/docs/direct-messages/api-features) 128 | * [Account Activity API (All Events)](https://developer.twitter.com/en/docs/accounts-and-users/subscribe-account-activity/api-reference/aaa-premium) 129 | -------------------------------------------------------------------------------- /Twitter.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | import os 4 | 5 | CONSUMER_KEY = os.environ.get('CONSUMER_KEY', None) 6 | CONSUMER_SECRET = os.environ.get('CONSUMER_SECRET', None) 7 | 8 | ACCESS_TOKEN = os.environ.get('ACCESS_TOKEN', None) 9 | ACCESS_TOKEN_SECRET = os.environ.get('ACCESS_TOKEN_SECRET', None) 10 | 11 | 12 | def initApiObject(): 13 | 14 | #user authentication 15 | api = TwitterAPI(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) 16 | 17 | return api 18 | 19 | def processDirectMessageEvent(eventObj): 20 | 21 | messageText = eventObj.get('message_data').get('text') 22 | userID = eventObj.get('sender_id') 23 | 24 | twitterAPI = initApiObject() 25 | 26 | messageReplyJson = '{"event":{"type":"message_create","message_create":{"target":{"recipient_id":"' + userID + '"},"message_data":{"text":"Hello World!"}}}}' 27 | 28 | #ignore casing 29 | if(messageText.lower() == 'hello bot'): 30 | 31 | r = twitterAPI.request('direct_messages/events/new', messageReplyJson) 32 | 33 | return None 34 | 35 | def processLikeEvent(eventObj): 36 | userHandle = eventObj.get('user', {}).get('screen_name') 37 | 38 | print ('This user liked one of your tweets: %s' % userHandle) 39 | 40 | return None 41 | -------------------------------------------------------------------------------- /example-scripts/create-webhook.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | import os 4 | 5 | CONSUMER_KEY = os.environ.get('CONSUMER_KEY', None) 6 | CONSUMER_SECRET = os.environ.get('CONSUMER_SECRET', None) 7 | 8 | ACCESS_TOKEN = os.environ.get('ACCESS_TOKEN', None) 9 | ACCESS_TOKEN_SECRET = os.environ.get('ACCESS_TOKEN_SECRET', None) 10 | 11 | #The environment name for the beta is filled below. Will need changing in future 12 | ENVNAME = os.environ.get('ENVNAME', None) 13 | WEBHOOK_URL = os.environ.get('WEBHOOK_URL', None) 14 | 15 | twitterAPI = TwitterAPI(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) 16 | 17 | r = twitterAPI.request('account_activity/all/:%s/webhooks' % ENVNAME, {'url': WEBHOOK_URL}) 18 | 19 | print (r.status_code) 20 | print (r.text) 21 | -------------------------------------------------------------------------------- /example-scripts/subscribe-account.py: -------------------------------------------------------------------------------- 1 | from TwitterAPI import TwitterAPI 2 | 3 | import os 4 | 5 | CONSUMER_KEY = os.environ.get('CONSUMER_KEY', None) 6 | CONSUMER_SECRET = os.environ.get('CONSUMER_SECRET', None) 7 | 8 | ACCESS_TOKEN = os.environ.get('ACCESS_TOKEN', None) 9 | ACCESS_TOKEN_SECRET = os.environ.get('ACCESS_TOKEN_SECRET', None) 10 | 11 | ENVNAME = os.environ.get('ENVNAME', None) 12 | 13 | twitterAPI = TwitterAPI(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET) 14 | 15 | r = twitterAPI.request('account_activity/all/:%s/subscriptions' % 16 | ENVNAME, None, None, "POST") 17 | 18 | #TODO: check possible status codes and convert to nice messages 19 | print (r.status_code) 20 | 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask>=1.0.0 2 | gunicorn==19.5.0 3 | itsdangerous==0.24 4 | Jinja2>=2.10.1 5 | MarkupSafe==1.0 6 | oauthlib==2.0.6 7 | requests>=2.20.0 8 | requests-oauthlib==0.8.0 9 | TwitterAPI==2.4.8 10 | Werkzeug>=0.15.3 11 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.5 2 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Twitter Webhook Boilerplate 7 | 8 | 9 | 10 | 11 | 12 |

#helloworld

13 | 14 | --------------------------------------------------------------------------------