diff --git a/app.yaml b/app.yaml index f11cbea..9d50948 100644 --- a/app.yaml +++ b/app.yaml @@ -6,4 +6,4 @@ threadsafe: true handlers: - url: /.* - script: main_other.app + script: callcenter.app diff --git a/callcenter.py b/callcenter.py new file mode 100644 index 0000000..dbb343d --- /dev/null +++ b/callcenter.py @@ -0,0 +1,171 @@ +import webapp2 +import util +from twilio import twiml +from twilio import rest + +NUMBER = "" + +# Change this to a sub account +ACCOUNT_SID = "" +AUTH_TOKEN = "" +APP_SID = "" + +client = rest.TwilioRestClient(ACCOUNT_SID, AUTH_TOKEN) + +class BossHandler(webapp2.RequestHandler): + + def get(self): + """Enqueue a caller into the entry queue""" + self.response.headers['Content-Type'] = 'application/xml' + + resp = twiml.Response() + d = resp.dial() + d.conference("boss") + + self.response.write(str(resp)) + + +class WaitHandler(webapp2.RequestHandler): + + def get(self): + """Tell a caller information about how long they have waited""" + self.response.headers['Content-Type'] = 'application/xml' + + resp = twiml.Response() + + messages = [ + "You are number %s in line." % self.request.get('QueuePosition'), + "You've been in line for %s seconds." % self.request.get('QueueTime'), + "Average wait time is %s seconds." % self.request.get('AverageQueueTime'), + ] + + for message in messages: + resp.say(message) + + resp.play("http://com.twilio.music.rock.s3.amazonaws.com/nickleus_-_" + "original_guitar_song_200907251723.mp3") + + self.response.out.write(str(resp)) + + +class EnqueueHandler(webapp2.RequestHandler): + + def get(self): + """Enqueue a caller into the entry queue""" + self.response.headers['Content-Type'] = 'application/xml' + + resp = twiml.Response() + resp.say("You are being enqueued now.") + resp.enqueue(self.request.params.get('queue', 'support'), + waitUrl='/twiml/wait', waitMethod='GET') + resp.sms("Thanks for calling into today. How was your call?") + self.response.write(str(resp)) + + +class DequeueHandler(webapp2.RequestHandler): + + def get(self): + """Enqueue a caller into the entry queue""" + self.response.headers['Content-Type'] = 'application/xml' + + resp = twiml.Response() + resp.say("Looking for a caller") + d = resp.dial(record=True) + d.queue(self.request.params.get('queue', 'support'), + url="/twiml/record-consent") + resp.pause(length=10) + resp.redirect() + self.response.write(str(resp)) + + +class ConsentHandler(webapp2.RequestHandler): + + def get(self): + """Inform the caller that the call may be recorded""" + self.response.headers['Content-Type'] = 'application/xml' + + resp = twiml.Response() + resp.say("This call may be recorded for quality purposes") + + self.response.write(str(resp)) + + +class MenuHandler(webapp2.RequestHandler): + + def post(self): + choice = self.request.params('Digits') + + if choice == '1': + self.redirect('/twiml/enqueue?queue=support') + elif choice == '2': + self.redirect('/twiml/enqueue?queue=sales') + elif choice == '3': + self.redirect('/twiml/enqueue?queue=marketing') + else: + self.redirect('/twiml/menu') + + def get(self): + """Let the caller choose what department to queue into""" + self.response.headers['Content-Type'] = 'application/xml' + + resp = twiml.Response() + gather = resp.gather() + gather.say("For support, press 1") + gather.say("For sales, press 2") + gather.say("For marketing, press 3") + resp.redirect() + + self.response.write(str(resp)) + + +class SupportHandler(webapp2.RequestHandler): + + def get(self): + params = { + "token": util.generate_token(ACCOUNT_SID, AUTH_TOKEN, APP_SID) + } + self.response.out.write(render_template("index.html", params)) + + +class CallHandler(webapp2.RequestHandler): + + def get(self): + """Show a list of all the current calls""" + params = { + "calls": client.calls.iter(status="in-progress"), + } + + self.response.out.write(render_template("calls.html", params)) + + def post(self): + sid = self.request.params.get('sid') + + if sid == '': + return + + call = client.calls.get(sid) + call.redirect('/twiml/boss') + + +class FeedbackHandler(webapp2.RequestHandler): + + def get(self): + """Show a list of all the text messages to this number""" + params = { + "messages": client.sms.messages.iter(), + } + + self.response.out.write(render_template("messages.html", params)) + + +app = webapp2.WSGIApplication([ + ('/twiml/boss', BossHandler), + ('/twiml/wait', WaitHandler), + ('/twiml/menu', MenuHandler), + ('/twiml/dequeue', DequeueHandler), + ('/twiml/enqueue', EnqueueHandler), + ('/twiml/record-consent', ConsentHandler), + ('/web/support', SupportHandler), + ('/web/calls', SupportHandler), + ('/web/feedback', FeedbackHandler), +], debug=True) diff --git a/docs/callcenter.rst b/docs/callcenter.rst index 2634f40..89918db 100644 --- a/docs/callcenter.rst +++ b/docs/callcenter.rst @@ -8,3 +8,6 @@ Bridging and Fair Queuing Build a UI to manage queues --------------------------- + +Escalting Calls +--------------- diff --git a/docs/callin.rst b/docs/callin.rst index 11b7b79..92b023a 100644 --- a/docs/callin.rst +++ b/docs/callin.rst @@ -3,8 +3,20 @@ Radio Call In ============= -In this workshop we'll be designing a radio call in application -using Twilio's new functionality. +In this workshop we'll be designing a radio call in application using Twilio's + functionality. While we'll be using a radio show as our target, this +style of queue managment can be used for any phone number where many people may +call at the same time. + +Prerequisties +------------- + +The next sections assume a working knowledge of Twilio. You should be familiar +with TwiML, configuring Twilio phone numbers, and Twilio application model. + +Also, we're assuming you're comfortable writing web applications. For +reference, we'll be developing the application along the way using Python +and Google App Engine. Using the Twilio Helper Libraries --------------------------------- @@ -20,13 +32,14 @@ Reference`_ helpful for this workshop. .. _here: http://www.twilio.com/docs/libraries .. _Queue API Reference: https://twilio-python.readthedocs.org/en/latest/api/rest/resources.html#queues -Using Queue (TwiML) -------------------- +Using +------------- + We'll need two Twilio phone numbers to work with Queue - one for the DJ to -dequeue calls from, and one for the queue that the listener will call into. +dequeue calls from, and one for the listener to call into. -First, we'll enqueue some calls via TwiML. In the example below, we enqueue -to a queue named ``radio-callin-queue``. Note that queues are created on +First, we'll enqueue calls via TwiML. In the example below, we enqueue calls +into a queue named ``radio-callin-queue``. Note that queues are created on if they do not already exist. .. code-block:: xml @@ -48,8 +61,9 @@ We can spice it up by adding some wait music, using the ``waitUrl`` parameter. radio-callin-queue -The ``/wait-loop`` endpoint goes to some TwiML that plays music. The ``waitUrl`` -TwiML document supports a `subset of TwiML verbs`_. +Twilio will request the ``/wait-loop`` and process the TwiML that plays music. +The ``waitUrl`` TwiML document only supports a `subset of TwiML verbs`_, which +includes ```` and ````. .. code-block:: xml @@ -60,10 +74,9 @@ TwiML document supports a `subset of TwiML verbs`_. - -For the DJ dequeuing number, we use some TwiML that bridges the current call -to the queue. Note that ing into a queue represents dequeuing a caller -on the queue, while the only way to get onto a queue is to be d. +For the DJ dequeuing number, we use TwiML that bridges the current call to the +queue. Note that ing into a queue dequeues the front on the queue, while +the only way to get onto a queue is by using the verb. .. code-block:: xml @@ -81,16 +94,29 @@ to the first member on the queue. Dynamic Queue Information ------------------------- + Twilio's Queue exposes dynamic inforrmation about the queue state that you can use to build rich applications. In this section, we'll move past static TwiML applications and start using the data Queue gives you to create dynamic TwiML through a web application. -We'll start by working on our hold music. Wouldn't it be cool if we could -tell users where they were in the queue, how long they've been there, or -even the average wait time for their queue? Twilio exposes `all these -parameters`_ when invoking your application's waiting logic via HTTP so that -you can pass it along in your dynamic TwiML! +We'll start by working on our hold music. Wouldn't it be cool if we could tell +users where they were in the queue, how long they've been there, or even the +average wait time for their queue? Twilio sends these parameters via POST data +when invoking your application's waiting logic via HTTP. + +================ =========== +Parameter Description +================ =========== +QueuePosition The current queue position for the enqueued call. +QueueSid The SID of the Queue that the caller is in. +QueueTime The time in seconds that the caller has been in the queue. +AverageQueueTime An average of how long time the current enqueued callers has been in the queue. +CurrentQueueSize The current number of enqueued calls in this queue. +================ =========== + +Utilizing this information, we can inform our users what position they are in +the queue and how long they can expect to wait before an answer. .. code-block:: python @@ -123,10 +149,9 @@ through the ``action`` parameter when enqueuing. # save to db, ping analytics, whatever you want! -.. _all these parameters: http://www.twilio.com/docs/api/twiml/enqueue#attributes-waiturl-parameters +Handling Long Queue Times +------------------------- -Queue Times Are Too Long! - A Call to Action --------------------------------------------- We can use the ``action`` parameter to collect all sorts of useful metrics on the backend, or even issue hasty apologies for long queue wait times. @@ -135,11 +160,8 @@ let our users know we care. Using the `action URL parameters`_, we can send an SMS apology if the wait time exceeded 30 seconds, or if their call was rejected from a full queue. -You may find the `helper library documentation`_ for your `language of choice`_ -helpful in sending SMS. - Here is some stub code that may help, if you are taking the Python / Google -App Engine route... +App Engine route. .. code-block:: xml @@ -165,17 +187,18 @@ App Engine route... .. _language of choice: http://www.twilio.com/docs/libraries -See You Next Time - Closing Out the Queue ------------------------------------------ -Unfortunately, all good things must come to an end. It's time for our -radio show to close down until next time - but what about the people -still on the waiting queue? +Closing Out the Queue +--------------------- + +Unfortunately, all good things must come to an end. It's time for our radio +show to close down until next time - but what about the people still on the +waiting queue? -We can use `Queue`_ and `Member`_ REST API resources to programmatically -look at all of our account's queues and active members on those queues. +We can use `Queue`_ and `Member`_ REST API resources to programmatically look +at all of our account's queues and active members on those queues. -Let's write a quick script that will find our queue, loop through its -members, and dequeue each of them with a thank you message. +Let's write a quick script that will find our queue, loop through its members, +and dequeue each of them with a thank you message. .. code-block:: python @@ -193,12 +216,12 @@ First, we need to `find our queue`_. my_queue = queue -Then, we can iterate over its members and dequeue with some static thank -you TwiML. Try it yourself! Hint: issuing `an HTTP POST to a Member instance`_ -will dequeue that member. +Then, we can iterate over its members and dequeue with some static thank you +TwiML. Try it yourself! Hint: issuing `an HTTP POST to a Member instance`_ will +dequeue that member. -As a bonus, try allowing the callers being dequeued to record a message -for the DJs to listen to at the beginning of the next show. +As a bonus, try allowing the callers being dequeued to record a message for the +DJs to listen to at the beginning of the next show. Finally, we can delete the queue using a REST API call. diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index d8ce4f8..189cb23 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -17,7 +17,6 @@ Idea Factory custom_twiml static_apps dynamic_apps - deploy voting Unlocking the Box diff --git a/docs/custom_twiml.rst b/docs/custom_twiml.rst index 1884791..15c29e3 100644 --- a/docs/custom_twiml.rst +++ b/docs/custom_twiml.rst @@ -64,6 +64,8 @@ Your final application should look like this. .. image:: _static/twimlbin.png +.. _configure-number: + Configuring your phone number ------------------------------ diff --git a/docs/dynamic_apps.rst b/docs/dynamic_apps.rst index beca7f4..0787e60 100644 --- a/docs/dynamic_apps.rst +++ b/docs/dynamic_apps.rst @@ -256,6 +256,6 @@ deployemnt. It should take less than a minute to deploy. Once it's deployed, take the url for your application and set it as the voice number for your Twilio phone number. Configuring Twilio numbers is covered in -more detail in :ref:`` +more detail in :ref:`configure-number` Now give it a call. You should hear your custom message. Hooray! diff --git a/docs/queue.rst b/docs/queue.rst index 6da5744..5b50127 100644 --- a/docs/queue.rst +++ b/docs/queue.rst @@ -116,3 +116,27 @@ suffices as the initial entry point for our app. Testing Twilio Applications --------------------------- + +Installing and using local tunnel + +https://showoff.io/ +local tunnel +pagekite + +Local tunnel sometimes doesn't work. Many of these services suffer from the +same problems tthat networks can't be awesome. + +So, how do we test if we can't connect to Twilio + +Mocking out Twilio +~~~~~~~~~~~~~~~~~~ + +A simple example showing how, using cUrl, you can actually duplicate many of +Twilio's behaviors + + +Adding a Feedback Loop +---------------------- + +- SMS stuff +- Increasing Engagement diff --git a/templates/index.html b/templates/index.html index 426b52b..591497c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -26,13 +26,13 @@ }); function call() { - Twilio.Device.connect(); + Twilio.evice.connect(); }
Loading pigeons...
diff --git a/templates/messages.html b/templates/messages.html new file mode 100644 index 0000000..9f01102 --- /dev/null +++ b/templates/messages.html @@ -0,0 +1,21 @@ + + + + Radio Call-In Dashboard + + + + + +

Feedback

+
    + {% for msg in messages %} +
  • {{ msg.body }}
  • + {% endfor %} +
+ + diff --git a/util.py b/util.py index aa9f6ba..5468880 100644 --- a/util.py +++ b/util.py @@ -1,6 +1,8 @@ import os +from twilio.util import TwilioCapability from google.appengine.ext.webapp import template + def render_template(rel_path, parameters=None, folder="templates"): """ Takes a path relative to the templates/ folder and @@ -9,3 +11,14 @@ def render_template(rel_path, parameters=None, folder="templates"): parameters = parameters if parameters is not None else {} path = os.path.join(os.path.dirname(__file__), folder, rel_path) return template.render(path, parameters) + + +def generate_token(account_sid, auth_token, application_sid): + """ + Create a capability token given Twilio account credentials + and an application sid. + """ + capability = TwilioCapability(account_sid, auth_token) + # Allow access to the Call-in ApplicationSid we created + capability.allow_client_outgoing(application_sid) + return capability.generate()