├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── app.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg* 2 | *.py[co] 3 | *.swp 4 | *.swo 5 | .DS_Store 6 | *~ 7 | *.db 8 | *.log 9 | assets 10 | tmp/ 11 | setenvs.sh 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matthew Makai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:app -b 0.0.0.0:$PORT -w 3 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi Channel Call Center with Twilio TaskRouter 2 | A call center that supports both inbound voice calls and SMS messages through 3 | Twilio's TaskRouter API. 4 | 5 | 6 | ## Deploy to Heroku 7 | Press the big ol' purple button below. Input your credentials into the Heroku 8 | set up page and click deploy. 9 | 10 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/makaimc/taskrouter-multi-channel-support-desk) 11 | 12 | 13 | ## Environment variables 14 | Ensure you've set the following environment variables locally or via the 15 | Heroku deploy button: 16 | 17 | * ``TWILIO_ACCOUNT_SID``: Account SID found on 18 | [Twilio dashboard](https://www.twilio.com/user/account/voice-messaging) 19 | under API credentials 20 | 21 | * ``TWILIO_AUTH_TOKEN``: Secret authentication token also found on 22 | [Twilio dashboard](https://www.twilio.com/user/account/voice-messaging) 23 | under API credentials 24 | 25 | * ``WORKSPACE_SID``: Found on the 26 | [TaskRouter workspaces dashboard](https://www.twilio.com/user/account/taskrouter/workspaces) 27 | after [creating a workspace](https://www.twilio.com/user/account/taskrouter/workspaces/create) 28 | 29 | * ``WORKFLOW_SID``: Found on the TaskRouter workflow page for a given 30 | workspace 31 | 32 | * ``SUPPORT_DESK_NUMBER``: Twilio phone number used for the support desk, 33 | in the +12025551234 format 34 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Multi Channel Support Desk", 3 | "description": "A Flask app that handles inbound calling and SMS. Uses TaskRouter to coordinate handing off calls and messages to support desk agents.", 4 | "keywords": [ 5 | "twilio", 6 | "call center", 7 | "taskrouter", 8 | "flask", 9 | "python" 10 | ], 11 | "website": "https://www.twilio.com", 12 | "repository": "https://github.com/makaimc/taskrouter-multi-channel-support-desk", 13 | "logo": "https://www.twilio.com/bundles/marketing/img/logos/wordmark-red.svg", 14 | "env": { 15 | "TWILIO_ACCOUNT_SID": { 16 | "description": "Your Twilio account's unique identifier, found here: https://www.twilio.com/user/account", 17 | "value": "ACxxxxxxxxxxxxxx" 18 | }, 19 | "TWILIO_AUTH_TOKEN": { 20 | "description": "Your Twilio account's secret key, found here: https://www.twilio.com/user/account", 21 | "value": "yyyyyyyyyyyyyyyyyy" 22 | }, 23 | "WORKSPACE_SID": { 24 | "description": "Twilio Workspace SID for workspace you want to use for this support desk", 25 | "value": "WSzzzzzzzzzzzzzzzzzzz" 26 | }, 27 | "WORKFLOW_SID": { 28 | "description": "Twilio Workflow SID (different from Workspace) for workspace you want to use for this support desk", 29 | "value": "WWaaaaaaaaaaaaaaaaaaa" 30 | }, 31 | "SUPPORT_DESK_NUMBER": { 32 | "description": "Twilio phone number used for the support desk", 33 | "value": "+12025551234" 34 | } 35 | }, 36 | "success_url": "/" 37 | } 38 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from flask import Flask, Response, request 4 | from twilio import twiml 5 | from twilio.rest import TwilioRestClient, TwilioTaskRouterClient 6 | 7 | 8 | ACCOUNT_SID = os.environ.get('TWILIO_ACCOUNT_SID', '') 9 | AUTH_TOKEN = os.environ.get('TWILIO_AUTH_TOKEN', '') 10 | SUPPORT_DESK_NUMBER = os.environ.get('SUPPORT_DESK_NUMBER', '') 11 | WORKSPACE_SID = os.environ.get('WORKSPACE_SID', '') 12 | WORKFLOW_SID = os.environ.get('WORKFLOW_SID', '') 13 | 14 | client = TwilioRestClient(account=ACCOUNT_SID, token=AUTH_TOKEN) 15 | tr_client = TwilioTaskRouterClient(account=ACCOUNT_SID, token=AUTH_TOKEN) 16 | app = Flask(__name__) 17 | 18 | 19 | @app.route('/') 20 | def working(): 21 | return "Service desk up and running!" 22 | 23 | @app.route('/call', methods=['GET', 'POST']) 24 | def call(): 25 | r = twiml.Response() 26 | r.enqueue('', workflowSid=WORKFLOW_SID) 27 | return Response(str(r), content_type='application/xml') 28 | 29 | 30 | @app.route('/assign', methods=['POST']) 31 | def assign(): 32 | task_attrs = json.loads(request.form['TaskAttributes']) 33 | if 'training' in task_attrs and task_attrs['training'] == 'sms': 34 | number = json.loads(request.form['WorkerAttributes'])['phone_number'] 35 | instruction = {"instruction": "accept"} 36 | client.messages.create(from_=SUPPORT_DESK_NUMBER, to=number, 37 | body='Text {0} asking "{1}"'.format(task_attrs['phone_number'], 38 | task_attrs['body'])) 39 | return Response(json.dumps(instruction), 40 | content_type='application/json') 41 | # defaults to voice call 42 | number = json.loads(request.form['WorkerAttributes'])['phone_number'] 43 | instruction = { 44 | "instruction": "dequeue", 45 | "to": number, 46 | "from": SUPPORT_DESK_NUMBER 47 | } 48 | return Response(json.dumps(instruction), content_type='application/json') 49 | 50 | 51 | @app.route('/message', methods=['POST']) 52 | def message(): 53 | # check if one of our workers is completing a task 54 | if request.form['Body'] == 'DONE': 55 | from_number = request.form['From'] 56 | for w in tr_client.workers(WORKSPACE_SID).list(): 57 | if from_number == json.loads(w.attributes)['phone_number']: 58 | # update worker status back to idle 59 | for activity in tr_client.activities(WORKSPACE_SID).list(): 60 | if activity.friendly_name == 'Idle': 61 | w.update(activity_sid=activity.sid) 62 | break 63 | r = twiml.Response() 64 | r.message("Ticket closed.") 65 | return Response(str(r), content_type='application/xml') 66 | 67 | task_attributes = { 68 | "training" : "sms", 69 | "phone_number" : request.form['From'], 70 | "body": request.form['Body'] 71 | } 72 | tasks = tr_client.tasks(WORKSPACE_SID).create(json.dumps(task_attributes), 73 | WORKFLOW_SID) 74 | r = twiml.Response() 75 | r.message("Thanks. You'll hear back from us soon.") 76 | return Response(str(r), content_type='application/xml') 77 | 78 | 79 | if __name__ == '__main__': 80 | # first attempt to get the PORT environment variable, 81 | # otherwise default to port 5000 82 | port = int(os.environ.get("PORT", 5000)) 83 | if port == 5000: 84 | app.debug = True 85 | app.run(host='0.0.0.0', port=port) 86 | 87 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | 3 | twilio==3.7.0 4 | 5 | # the following requirement is just for Heroku deploy button 6 | gunicorn==19.2.1 7 | --------------------------------------------------------------------------------