<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> 
<base href="https://wiki.asterisk.org/wiki" /> 
<title>Message Title</title>  
<style type="text/css">@media only screen and (max-device-width: 480px) {.mobile-only {
        width: auto !important;
        height: auto !important;
        overflow: visible !important;
        line-height: normal !important;
        font-size: inherit !important;
        mso-hide: all;
}

.desktop-only {
        display: none !important;
}

/* iPhone 3GS fix for unwanted 20px right margin */
body { min-width: 100% !important; padding: 0; margin: 0; }

#center-content-table { max-width: none; !important; }
#header-pattern-container { padding: 10px 10px 10px 10px !important; line-height: 20px !important; }
#header-avatar-image-container { padding-right: 8px !important; }
#email-content-container { padding: 0 !important; }
.mobile-expand { border-radius: 0 !important; border-left: 0 !important; border-right: 0 !important; padding-left: 26px !important;}
.mobile-resize-text { font-size: 16px !important; line-height: 22px !important; }
#page-title-pattern-header { font-size: 20px !important; line-height: 28px !important; }
#page-title-pattern-icon-image-container-cell { padding-top: 7px !important; }
#inline-user-pattern { display: block !important; }
#inline-user-pattern-avatar { padding-top: 3px !important; }
.contextual-area-pattern { border-bottom: 1px solid #ccc !important; padding: 15px 10px 0 10px !important;}
.users-involved-pattern-column-table { width: 100% !important;  }
.users-involved-pattern-avatar-table-cell { padding: 3px 5px 5px 0 !important; }
.users-involved-pattern-column-container { padding-right: 0 !important; }
.contextual-excerpt-pattern, #users-involved-pattern { border: 0 !important; }

/** Aui Typography upsized for mobile **/
#content-excerpt-pattern-container, #contextual-excerpt-pattern-text-container { font-size: 16px !important; line-height: 22px !important; }
#content-excerpt-pattern-container h1, #contextual-excerpt-pattern-text-container h1 { font-size: 24px !important; line-height: 28px !important; }
#content-excerpt-pattern-container h2, #contextual-excerpt-pattern-text-container h2 { font-size: 20px !important; line-height: 28px !important; }
#content-excerpt-pattern-container h3, #contextual-excerpt-pattern-text-container h3 { font-size: 18px !important; line-height: 24px !important; }
#content-excerpt-pattern-container h4, #contextual-excerpt-pattern-text-container h4 { font-size: 16px !important; line-height: 22px !important; }
#content-excerpt-pattern-container h5, #contextual-excerpt-pattern-text-container h5 { font-size: 14px !important; line-height: 20px !important; }
#content-excerpt-pattern-container h6, #contextual-excerpt-pattern-text-container h6 { font-size: 14px !important; line-height: 20px !important; }
.user-mention { line-height: 18px !important; }
/** Aui Typography end **/

/* Show appropriate footer logo on mobile, display links vertically */
#footer-pattern { padding: 15px 10px !important; }
#footer-pattern-logo-desktop-container { padding: 0 !important; }
#footer-pattern-logo-desktop { width: 0 !important; height: 0 !important; }
#footer-pattern-logo-mobile {
    padding-top: 10px !important;
    width: 30px !important;
    height: 27px !important;
    display: inline !important;
}
#footer-pattern-text {
    display: block !important;
}
#footer-pattern-links-container { line-height: 0 !important;}
.footer-pattern-links.mobile-resize-text,
.footer-pattern-links.mobile-resize-text,
#footer-pattern-text.mobile-resize-text,
#footer-pattern-links-container.no-footer-links {
    font-size: 14px !important;
    line-height: 20px !important;
}
.footer-link { display: block !important; }
#footer-pattern-links-container table { display: inline-block !important; float: none !important; }
#footer-pattern-links-container, #footer-pattern-text { text-align: center !important; }
#footer-pattern-links { padding-bottom: 5px !important; }

/** Team Calendar overrides, these should be removed when notifications are updated in Team Calendars. For now CSS
    overrides are being used because the structure of the content can't change without rereleasing the plugin */
.mail-calendar-container .day-header + table tr td:first-child {
    vertical-align: top !important;
    padding-top: 5px !important;
}}
@media (min-width: 900px) {#center-content-table { width: 900px; }}
@media all {#outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
/* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
body{-webkit-text-size-adjust:100%; -ms-text-size-adjust:100%;}
.ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
#background-table {margin:0; padding:0; width:100% !important; }
/* Needed to override highlighting on date and time links in iOS */
.grey a {color: #707070; text-decoration: none; }}
</style> 
</head>
<body>
<table id="background-table" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; background-color: #f5f5f5"> 
<tbody> 
<tr> 
<td id="header-pattern-container" style="padding: 0px; border-collapse: collapse; padding: 10px 20px"> 
<table id="header-pattern" cellspacing="0" cellpadding="0" border="0" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333"> 
<tbody> 
<tr> 
<td id="header-avatar-image-container" valign="top" style="padding: 0px; border-collapse: collapse; vertical-align: top; width: 32px; padding-right: 9px"><a href="https://wiki.asterisk.org/wiki/display/~mjordan?src=email" style="color: #3b73af; text-decoration: none"><img id="header-avatar-image" class="image_fix" src="cid:avatar_ce51dcf276530e4a4b00548e2a6d0905" height="32" width="32" border="0" style="border-radius: 3px; vertical-align: top" /></a></td>
<td id="header-text-container" valign="middle" style="padding: 0px; border-collapse: collapse; vertical-align: middle; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 1px">Matt Jordan <strong>created</strong> a page</td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
<!-- End Header pattern --> 
<tr> 
<td id="email-content-container" style="padding: 0px; border-collapse: collapse; padding: 0 20px"> 
<table id="email-content-table" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; border-spacing: 0; border-collapse: separate"> 
<tbody> 
<tr> 
<td class="email-content-rounded-top mobile-expand" style="padding: 0px; border-collapse: collapse; color: #fff; padding: 0 15px 0 16px; height: 15px; background-color: #fff; border-left: 1px solid #ccc; border-top: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 0; border-top-right-radius: 5px; border-top-left-radius: 5px"> </td> 
</tr> 
<tr> 
<td class="email-content-main mobile-expand" style="padding: 0px; border-collapse: collapse; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-top: 0; border-bottom: 0; padding: 0 15px 15px 16px; background-color: #fff"> 
<table id="page-title-pattern" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333"> 
<tbody> 
<tr> 
<td id="page-title-pattern-icon-image-container" valign="top" style="padding: 0px; border-collapse: collapse; width: 16px; vertical-align: top"> 
<table cellspacing="0" cellpadding="0" border="0" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333"> 
<tbody> 
<tr> 
<td id="page-title-pattern-icon-image-container-cell" style="padding: 0px; border-collapse: collapse; width: 16px; padding: 9px 8px 0px 0px; mso-text-raise: 5px; mso-line-height-rule: exactly"><a href="https://wiki.asterisk.org/wiki/display/AST/ARI+and+Channels%3A+Handling+DTMF?src=email" title="page icon" style="vertical-align: top;; color: #3b73af; text-decoration: none"><img style="vertical-align: top; display: block;" src="cid:page-icon" alt="page icon" title="page icon" height="16" width="16" border="0" /></a></td> 
</tr> 
</tbody> 
</table> </td>
<td style="vertical-align: top;; padding: 0px; border-collapse: collapse; padding-right: 5px; font-size: 20px; line-height: 30px; mso-line-height-rule: exactly" id="page-title-pattern-header-container"><span id="page-title-pattern-header" style="font-family: Arial, sans-serif; padding: 0; font-size: 20px; line-height: 30px; mso-text-raise: 2px; mso-line-height-rule: exactly; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/display/AST/ARI+and+Channels%3A+Handling+DTMF?src=email" title="ARI and Channels: Handling DTMF" style="color: #3b73af; text-decoration: none">ARI and Channels: Handling DTMF</a></span></td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
<tr> 
<td class="email-content-main mobile-expand" style="padding: 0px; border-collapse: collapse; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-top: 0; border-bottom: 0; padding: 0 15px 15px 16px; background-color: #fff"> 
<table class="content-excerpt-pattern" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 1px"> 
<tbody> 
<tr> 
<td class="content-excerpt-pattern-container mobile-resize-text " style="padding: 0px; border-collapse: collapse; padding: 0 0 0 24px"> 
<div class="sectionColumnWrapper"> 
<div class="sectionMacro"> 
<div class="sectionMacroRow"> 
<div class="columnMacro"> 
<h1 id="ARIandChannels:HandlingDTMF-HandlingDTMFevents" style="margin: 10px 0 0 0; margin-top: 0; font-size: 24px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0; margin-top: 0">Handling DTMF events</h1> 
<p style="margin: 10px 0 0 0">DTMF events are conveyed via the <a href="https://wiki.asterisk.org/wiki/display/AST/Asterisk+12+REST+Data+Models#Asterisk12RESTDataModels-ChannelDtmfReceived" rel="nofollow" style="color: #3b73af; text-decoration: none"><code style="font-family: monospace">ChannelDtmfReceived</code></a> event. The event contains the channel that pressed the DTMF key, the digit that was pressed, and the duration of the digit.</p> 
<p style="margin: 10px 0 0 0">While this concept is relatively straight forward, handling DTMF is quite common in applications, as it is the primary mechanism that phones have to inform a server to perform some action. This includes manipulating media, initiating call features, performing transfers, dialling, and just about every thing in between. As such, the examples on this page focus less on simply handling the event and more on using the DTMF in a relatively realistic fashion.</p> 
</div> 
<div class="columnMacro" style="width:40%;min-width:40%;max-width:40%;"> 
<div class="panel" style="border-width: 1px;"> 
<div class="panelHeader" style="border-bottom-width: 1px;"> 
<b>On This Page</b> 
</div> 
<div class="panelContent"> 
<p style="margin: 10px 0 0 0; margin-top: 0"> <style type="text/css">/**/
div.rbtoc1408640290733 {padding: 0px;}
div.rbtoc1408640290733 ul {list-style: disc;margin-left: 0px;}
div.rbtoc1408640290733 li {margin-left: 0px;padding-left: 0px;}

/**/</style> </p> 
<div class="toc-macro rbtoc1408640290733" style="padding: 0px; padding: 0px"> 
<ul class="toc-indentation" style="margin: 10px 0 0 0; margin-top: 0; list-style: disc; margin-left: 0px; list-style: disc; margin-left: 0px"> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-HandlingDTMFevents" style="color: #3b73af; text-decoration: none">Handling DTMF events</a> </li> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-Example:Asimpleautomatedattendant" style="color: #3b73af; text-decoration: none">Example: A simple automated attendant</a> 
<ul class="toc-indentation" style="margin: 10px 0 0 0; list-style: disc; margin-left: 0px; list-style: disc; margin-left: 0px"> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-Dialplan" style="color: #3b73af; text-decoration: none">Dialplan</a> </li> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-Python" style="color: #3b73af; text-decoration: none">Python</a> 
<ul class="toc-indentation" style="margin: 10px 0 0 0; list-style: disc; margin-left: 0px; list-style: disc; margin-left: 0px"> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-Playingthemenu" style="color: #3b73af; text-decoration: none">Playing the menu</a> 
<ul class="toc-indentation" style="margin: 10px 0 0 0; list-style: disc; margin-left: 0px; list-style: disc; margin-left: 0px"> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-Cancellingthemenu" style="color: #3b73af; text-decoration: none">Cancelling the menu</a> </li> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-Timingout" style="color: #3b73af; text-decoration: none">Timing out</a> </li> 
</ul> </li> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-HandlingtheDTMFoptions" style="color: #3b73af; text-decoration: none">Handling the DTMF options</a> </li> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-channel-aa.py" style="color: #3b73af; text-decoration: none">channel-aa.py</a> </li> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-channel-aa.pyinaction" style="color: #3b73af; text-decoration: none">channel-aa.py in action</a> </li> 
</ul> </li> 
<li style="margin-left: 0px; padding-left: 0px; margin-left: 0px; padding-left: 0px"> <a href="#ARIandChannels:HandlingDTMF-JavaScript(Node.js)" style="color: #3b73af; text-decoration: none">JavaScript (Node.js)</a> </li> 
</ul> </li> 
</ul> 
</div> 
<p style="margin: 10px 0 0 0"></p> 
</div> 
</div> 
</div> 
</div> 
</div> 
</div> <h1 id="ARIandChannels:HandlingDTMF-Example:Asimpleautomatedattendant" style="margin: 10px 0 0 0; font-size: 24px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0">Example: A simple automated attendant</h1> <p style="margin: 10px 0 0 0">This example mimics the <a href="https://wiki.asterisk.org/wiki/display/AST/Handling+Special+Extensions" rel="nofollow" style="color: #3b73af; text-decoration: none">automated attendant/IVR dialplan example</a>. It does the following:</p> 
<ul style="margin: 10px 0 0 0"> 
<li>Plays a menu to the user which is cancelled when the user takes some action.</li> 
<li>If the user presses 1 or 2, the digit is repeated to the user and the menu restarted.</li> 
<li>If the user presses an invalid digit, a prompt informing the user that the digit was invalid is played to the user and the menu restarted.</li> 
<li>If the user fails to press anything within some period of time, a prompt asking the user if they are still present is played to the user and the menu restarted.</li> 
</ul> <p style="margin: 10px 0 0 0"> </p> 
<div class="aui-message success shadowed information-macro"> 
<span class="aui-icon icon-success">Icon</span> 
<div class="message-content"> 
<p style="margin: 10px 0 0 0; margin-top: 0">For this example, you will need the following:</p> 
<ol style="margin: 10px 0 0 0"> 
<li>The <strong>extra</strong> sound package from Asterisk. You can install this using the <code style="font-family: monospace">menuselect</code> tool.</li> 
<li>If using the Python example, <code style="font-family: monospace">ari-py</code>version 0.1.3 or later.</li> 
</ol> 
</div> 
</div> <h2 id="ARIandChannels:HandlingDTMF-Dialplan" style="margin: 10px 0 0 0; font-size: 20px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0">Dialplan</h2> <p style="margin: 10px 0 0 0">As usual, a very simple dialplan is sufficient for this example. The dialplan takes the channel and places it into the Stasis application <code style="font-family: monospace">channel-aa</code>.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeHeader panelHeader pdl" style="border-bottom-width: 1px;"> 
<b>extensions.conf</b> 
</div> 
<div class="codeContent panelContent pdl"> 
<pre class="theme: Confluence; brush: java; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">exten => 1000,1,NoOp()
 same =>      n,Stasis(channel-aa)
 same =>      n,Hangup()</pre> 
</div> 
</div> <h2 id="ARIandChannels:HandlingDTMF-Python" style="margin: 10px 0 0 0; font-size: 20px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0">Python</h2> <p style="margin: 10px 0 0 0">As this example is a bit larger, how the code is written and structured is broken up into two phases:</p> 
<ol style="margin: 10px 0 0 0"> 
<li>Constructing the menu and handling its state as the user presses buttons.</li> 
<li>Actually handling the button presses from the user.</li> 
</ol> <p style="margin: 10px 0 0 0">The full source code for this example immediately follows the walk through.</p> <h3 id="ARIandChannels:HandlingDTMF-Playingthemenu" style="margin: 10px 0 0 0; font-size: 16px; line-height: 25px; margin: 30px 0 0 0">Playing the menu</h3> <p style="margin: 10px 0 0 0">Unlike Playback, which can chain multiple sounds together and play them back in one continuous operation, ARI treats all sound files being played as separate operations. It will queue each sound file up to be played on the channel, and hand back the caller an object to control the operation of that single sound file. The menu announcement for the attendant has the following requirements:</p> 
<ol style="margin: 10px 0 0 0"> 
<li>Playback the options for the user</li> 
<li>If the user presses a DTMF key, cancel the playback of the options and handle the request</li> 
<li>If the user presses an invalid DTMF key, let them know and restart the menu</li> 
<li>If the user doesn't press anything, wait 10 seconds, ask them if they are still present, and restart the menu</li> 
</ol> <p style="margin: 10px 0 0 0">The second requirement makes this a bit more challenging: when the user presses a DTMF key, we want to cancel whatever sound file is currently being played back and immediately handle their request. We thus have to maintain some state in our application about what sound file is currently being played so that we can cancel the correct playback. We also don't want to queue up all of the sounds immediately - we'd have to walk through all of the queued up sounds and cancel each one - that'd be annoying! Instead, we only want to start the next sound in our prompt when the previous has completed.</p> <p style="margin: 10px 0 0 0">To start, we'll define in a list at the top of our scripts the sounds that make up the initial menu prompt:</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 12; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">sounds = ['press-1', 'or', 'press-2']</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0">Since we'll want to maintain some state, we'll create a small object to do that for us. In Python, tuples are immutable - and we'll want to mutate the state in callbacks when certain operations happen. As such, it makes sense to use a small class for this with two properties:</p> 
<ol style="margin: 10px 0 0 0"> 
<li>The current sound being played</li> 
<li>Whether or not we should consider the menu complete</li> 
</ol> <p style="margin: 10px 0 0 0">It's useful to have both pieces of data, as we may cancel the menu half-way through and want to take one set of actions, or we may complete the menu and all the sounds and start a different set of actions.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 16; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">class MenuState(object):
    """A small tracking object for the channel in the menu"""

    def __init__(self, current_sound, complete):
        self.current_sound = current_sound
        self.complete = complete</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0">To start, we'll write a function, <code style="font-family: monospace">play_intro_menu</code>, that starts the menu on a channel. It will simply initialize the state of the menu, and get the ball rolling on the channel by calling <code style="font-family: monospace">queue_up_sound</code>.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 24; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">def play_intro_menu(channel):
    """Play our intro menu to the specified channel
    Since we want to interrupt the playback of the menu when the user presses
    a DTMF key, we maintain the state of the menu via the MenuState object.
    A menu completes in one of two ways:
    (1) The user hits a key
    (2) The menu finishes to completion
    In the case of (2), a timer is started for the channel. If the timer pops,
    a prompt is played back and the menu restarted.
    Keyword Arguments:
    channel  The channel in the IVR
    """
    menu_state = MenuState(0, False)

    def queue_up_sound(channel, menu_state):
        ...

    queue_up_sound(channel, menu_state)</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0"> <code style="font-family: monospace">queue_up_sound</code> will be responsible for starting the next sound file on the channel and handling the manipulation of that sound file. Since there's a fair amount of checking that goes into this, we'll put the actual act of starting the sound in <code style="font-family: monospace">play_next_sound</code>, which will return the <code style="font-family: monospace">Playback</code> object from ARI. We'll prep the <code style="font-family: monospace">menu_state</code> object for the next sound file playback, and pass it to the <code style="font-family: monospace">PlaybackFinished</code> handler for the current sound being played back to the channel.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 70; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">    def queue_up_sound(channel, menu_state):
        """Start up the next sound and handle whatever happens

        Keywords Arguments:
        channel    The channel in the IVR
        menu_state The current state of the menu
        """

        current_playback = play_next_sound(menu_state)

        if not current_playback:
            return
        menu_state.current_sound += 1
        current_playback.on_event('PlaybackFinished', on_playback_finished,
                                  callback_args=[menu_state])
</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0"> <code style="font-family: monospace">play_next_sound</code> will do two things:</p> 
<ol style="margin: 10px 0 0 0"> 
<li>If we shouldn't play another sound - either because we've run out of sounds to play or because the menu is now "complete", we bail and return None.</li> 
<li>If we should play back a sound, start it up on the channel and return the <code style="font-family: monospace">Playback</code> object.</li> 
</ol> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 42; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">    def play_next_sound(menu_state):
        """Play the next sound, if we should

        Keyword Arguments:
        menu_state The current state of the IVR

        Returns:
        None if no playback should occur
        A playback object if a playback was started
        """
        if (menu_state.current_sound == len(sounds) or menu_state.complete):
            return None
        try:
            current_playback = channel.play(media='sound:%s' % sounds[menu_state.current_sound])
        except:
            current_playback = None
        return current_playback</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0">Our playback finished handler is very simple: since we've already incremented the state of the menu, we just call <code style="font-family: monospace">queue_up_sound</code> again:</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 60; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">    def on_playback_finished(playback, ev, menu_state):
        """Callback handler for when a playback is finished
        Keyword Arguments:
        playback   The playback object that finished
        ev         The PlaybackFinished event
        menu_state The current state of the menu
        """
        queue_up_sound(channel, menu_state)</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0">To recap, our <code style="font-family: monospace">play_intro_menu</code> function has three nested functions:</p> 
<ol style="margin: 10px 0 0 0"> 
<li> <code style="font-family: monospace">queue_up_sound</code> - starts a sound on a channel, increments the state of the menu, and subscribes for the <code style="font-family: monospace">PlaybackFinished</code> event.</li> 
<li> <code style="font-family: monospace">play_next_sound</code> - if possible, actually starts the sound. Called from <code style="font-family: monospace">queue_up_sound</code>.</li> 
<li> <code style="font-family: monospace">on_playback_finished</code> - called when <code style="font-family: monospace">PlaybackFinished</code> is received for the current playback, and call <code style="font-family: monospace">queue_up_sound</code> to start the next sound in the menu.</li> 
</ol> <p style="margin: 10px 0 0 0">This will play back the menu sounds, but it doesn't handle cancelling the menu, time-outs, or other conditions. To do that, we're going to need more information from Asterisk.</p> <h4 id="ARIandChannels:HandlingDTMF-Cancellingthemenu" style="margin: 10px 0 0 0; font-size: 14px; line-height: 20px; margin: 20px 0 0 0">Cancelling the menu</h4> <p style="margin: 10px 0 0 0">When the user presses a DTMF key, we want to stop the current playback and end the menu. To do that, we'll need to subscribe for DTMF events from the channel. We'll define a new handler function, <code style="font-family: monospace">cancel_menu</code>, and tell <code style="font-family: monospace">ari-py</code> to call it when a DTMF key is received via the <code style="font-family: monospace">ChannelDtmfReceived</code> event. We don't really care about the digit here - we just want to cancel the menu. In the handler function, we'll set <code style="font-family: monospace">menu_state.complete</code> to <code style="font-family: monospace">True</code>, then tell the <code style="font-family: monospace">current_playback</code> to stop.</p> <p style="margin: 10px 0 0 0">We should also stop the menu when the channel is hung up. Since the <code style="font-family: monospace">cancel_menu</code> , so we'll subscribe to the <code style="font-family: monospace">StasisEnd</code> event here and call <code style="font-family: monospace">cancel_menu</code> from it as well:</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 70; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">    def queue_up_sound(channel, menu_state):
        """Start up the next sound and handle whatever happens

        Keywords Arguments:
        channel    The channel in the IVR
        menu_state The current state of the menu
        """

        current_playback = play_next_sound(menu_state)

        def cancel_menu(channel, ev, current_playback, menu_state):
            """Cancel the menu, as the user did something"""
            menu_state.complete = True
            try:
                current_playback.stop()
            except:
                pass
            return

        if not current_playback:
            return
        menu_state.current_sound += 1
        current_playback.on_event('PlaybackFinished', on_playback_finished,
                                  callback_args=[menu_state])

        # If the user hits a key or hangs up, cancel the menu operations
        channel.on_event('ChannelDtmfReceived', cancel_menu,
                         callback_args=[current_playback, menu_state])
        channel.on_event('StasisEnd', cancel_menu,
                         callback_args=[current_playback, menu_state])</pre> 
</div> 
</div> <h4 id="ARIandChannels:HandlingDTMF-Timingout" style="margin: 10px 0 0 0; font-size: 14px; line-height: 20px; margin: 20px 0 0 0">Timing out</h4> <p style="margin: 10px 0 0 0">Now we can cancel the menu, but we also need to restart it if the user doesn't do anything. We can use a Python timer to start a timer if we're finished playing sounds <em>and</em> we got to the end of the sound prompt list. We don't want to start the timer if the user pressed a DTMF key - in that case, we would have stopped the menu early and we should be off handling their DTMF key press. The timer will call <code style="font-family: monospace">menu_timeout</code>, which will play back a "are you still there?" prompt, then restart the menu.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 70; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">    def queue_up_sound(channel, menu_state):
        """Start up the next sound and handle whatever happens
        Keywords Arguments:
        channel    The channel in the IVR
        menu_state The current state of the menu
        """

        def menu_timeout(channel):
            """Callback called by a timer when the menu times out"""
            channel.play(media='sound:are-you-still-there')
            play_intro_menu(channel)

        def cancel_menu(channel, ev, current_playback, menu_state):
            """Cancel the menu, as the user did something"""
            menu_state.complete = True
            try:
                current_playback.stop()
            except:
                pass
            return

        current_playback = play_next_sound(menu_state)
        if not current_playback:
            if menu_state.current_sound == len(sounds):
                # Menu played, start a timer!
                timer = threading.Timer(10, menu_timeout, [channel])
                channel_timers[channel.id] = timer
                timer.start()
            return

        menu_state.current_sound += 1
        current_playback.on_event('PlaybackFinished', on_playback_finished,
                                  callback_args=[menu_state])

        # If the user hits a key or hangs up, cancel the menu operations
        channel.on_event('ChannelDtmfReceived', cancel_menu,
                         callback_args=[current_playback, menu_state])
        channel.on_event('StasisEnd', cancel_menu,
                         callback_args=[current_playback, menu_state])</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0">Now that we've introduced timers, we know we're going to need to stop them if the user does something. We'll store the timers in a dictionary indexed by channel ID, so we can get them from various parts of the script:</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 14; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">channel_timers = {}</pre> 
</div> 
</div> <h3 id="ARIandChannels:HandlingDTMF-HandlingtheDTMFoptions" style="margin: 10px 0 0 0; font-size: 16px; line-height: 25px; margin: 30px 0 0 0">Handling the DTMF options</h3> <p style="margin: 10px 0 0 0">While we now have code that plays back the menu to the user, we actually have to implement the attendant menu still. This is slightly easier than playing the menu. We can register for the <code style="font-family: monospace">ChannelDtmfReceived</code> event in the <code style="font-family: monospace">StasisStart</code> event handler. In that callback, we need to do the following:</p> 
<ol style="margin: 10px 0 0 0"> 
<li>Cancel any timers associated with the channel. Note that we don't need to stop the playback of the menu, as the menu function <code style="font-family: monospace">queue_up_sound</code> already registers a handler for that event and cancels the menu when it gets any digit.</li> 
<li>Actually handle the digit, if the digit is a <code style="font-family: monospace">1</code> or a <code style="font-family: monospace">2</code>.</li> 
<li>If the digit isn't supported, play a prompt informing the user that their option was invalid, and re-play the menu.</li> 
</ol> <p style="margin: 10px 0 0 0">The following implements these three items, deferring processing of the valid options to separate functions.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 150; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">def on_dtmf_received(channel, ev):
    """Our main DTMF handler for a channel in the IVR

    Keyword Arguments:
    channel The channel in the IVR
    digit   The DTMF digit that was pressed
    """

    # Since they pressed something, cancel the timeout timer
    cancel_timeout(channel)
    digit = int(ev.get('digit'))

    print 'Channel %s entered %d' % (channel.json.get('name'), digit)
    if digit == 1:
        handle_extension_one(channel)
    elif digit == 2:
        handle_extension_two(channel)
    else:
        channel.play(media='sound:option-is-invalid')
        play_intro_menu(channel)


def stasis_start_cb(channel_obj, ev):
    """Handler for StasisStart event"""

    channel = channel_obj.get('channel')
    print "Channel %s has entered the application" % channel.json.get('name')

    channel.on_event('ChannelDtmfReceived', on_dtmf_received)
    play_intro_menu(channel)</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0">Cancelling the timer is done in a fashion similar to other examples. If the channel has a Python timer associated with it, we cancel the timer and remove it from the dictionary.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 138; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">def cancel_timeout(channel):
    """Cancel the timeout timer for the channel

    Keyword Arguments:
    channel The channel in the IVR
    """
    timer = channel_timers.get(channel.id)
    if timer:
        timer.cancel()
        del channel_timers[channel.id]</pre> 
</div> 
</div> <p style="margin: 10px 0 0 0">Finally, we need to actually do <em>something</em> when the user presses a <code style="font-family: monospace">1</code> or a <code style="font-family: monospace">2</code>. We could do anything here - but in our case, we're merely going to play back the number that they pressed and restart the menu.</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeContent panelContent pdl"> 
<pre class="first-line: 114; theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">def handle_extension_one(channel):
    """Handler for a channel pressing '1'

    Keyword Arguments:
    channel The channel in the IVR
    """
    print 'Channel %s pressed 1' % channel.json.get('name')
    channel.play(media='sound:you-entered')
    channel.play(media='digits:1')
    play_intro_menu(channel)


def handle_extension_two(channel):
    """Handler for a channel pressing '2'

    Keyword Arguments:
    channel The channel in the IVR
    """
    print 'Channel %s pressed 2' % channel.json.get('name')
    channel.play(media='sound:you-entered')
    channel.play(media='digits:2')
    play_intro_menu(channel)</pre> 
</div> 
</div> <h3 id="ARIandChannels:HandlingDTMF-channel-aa.py" style="margin: 10px 0 0 0; font-size: 16px; line-height: 25px; margin: 30px 0 0 0">channel-aa.py</h3> <p style="margin: 10px 0 0 0">The full source for <code style="font-family: monospace">channel-aa.py</code> is shown below:</p> 
<div class="code panel pdl" style="border-width: 1px;"> 
<div class="codeHeader panelHeader pdl" style="border-bottom-width: 1px;"> 
<b>channel-aa.py</b> 
</div> 
<div class="codeContent panelContent pdl"> 
<pre class="theme: Confluence; brush: py; gutter: true" style="font-size:12px;; margin: 10px 0 0 0; margin-top: 0">#!/usr/bin/env python

import ari
import logging
import threading

logging.basicConfig(level=logging.ERROR)

client = ari.connect('http://localhost:8088', 'asterisk', 'asterisk')

# Note: this uses the 'extra' sounds package
sounds = ['press-1', 'or', 'press-2']

channel_timers = {}

class MenuState(object):
    """A small tracking object for the channel in the menu"""

    def __init__(self, current_sound, complete):
        self.current_sound = current_sound
        self.complete = complete


def play_intro_menu(channel):
    """Play our intro menu to the specified channel

    Since we want to interrupt the playback of the menu when the user presses
    a DTMF key, we maintain the state of the menu via the MenuState object.
    A menu completes in one of two ways:
    (1) The user hits a key
    (2) The menu finishes to completion

    In the case of (2), a timer is started for the channel. If the timer pops,
    a prompt is played back and the menu restarted.

    Keyword Arguments:
    channel  The channel in the IVR
    """

    menu_state = MenuState(0, False)

    def play_next_sound(menu_state):
        """Play the next sound, if we should

        Keyword Arguments:
        menu_state The current state of the IVR

        Returns:
        None if no playback should occur
        A playback object if a playback was started
        """
        if (menu_state.current_sound == len(sounds) or menu_state.complete):
            return None
        try:
            current_playback = channel.play(media='sound:%s' % sounds[menu_state.current_sound])
        except:
            current_playback = None
        return current_playback

    def on_playback_finished(playback, ev, menu_state):
        """Callback handler for when a playback is finished

        Keyword Arguments:
        playback   The playback object that finished
        ev         The PlaybackFinished event
        menu_state The current state of the menu
        """
        queue_up_sound(channel, menu_state)

    def queue_up_sound(channel, menu_state):
        """Start up the next sound and handle whatever happens

        Keywords Arguments:
        channel    The channel in the IVR
        menu_state The current state of the menu
        """

        def menu_timeout(channel):
            """Callback called by a timer when the menu times out"""
            print 'Channel %s stopped paying attention...' % channel.json.get('name')
            channel.play(media='sound:are-you-still-there')
            play_intro_menu(channel)

        def cancel_menu(channel, ev, current_playback, menu_state):
            """Cancel the menu, as the user did something"""
            menu_state.complete = True
            try:
                current_playback.stop()
            except:
                pass
            return

        current_playback = play_next_sound(menu_state)
        if not current_playback:
            if menu_state.current_sound == len(sounds):
                # Menu played, start a timer!
                timer = threading.Timer(10, menu_timeout, [channel])
                channel_timers[channel.id] = timer
                timer.start()
            return

        menu_state.current_sound += 1
        current_playback.on_event('PlaybackFinished', on_playback_finished,
                                  callback_args=[menu_state])

        # If the user hits a key or hangs up, cancel the menu operations
        channel.on_event('ChannelDtmfReceived', cancel_menu,
                         callback_args=[current_playback, menu_state])
        channel.on_event('StasisEnd', cancel_menu,
                         callback_args=[current_playback, menu_state])

    queue_up_sound(channel, menu_state)


def handle_extension_one(channel):
    """Handler for a channel pressing '1'

    Keyword Arguments:
    channel The channel in the IVR
    """
    channel.play(media='sound:you-entered')
    channel.play(media='digits:1')
    play_intro_menu(channel)


def handle_extension_two(channel):
    """Handler for a channel pressing '2'

    Keyword Arguments:
    channel The channel in the IVR
    """
    channel.play(media='sound:you-entered')
    channel.play(media='digits:2')
    play_intro_menu(channel)


def cancel_timeout(channel):
    """Cancel the timeout timer for the channel

    Keyword Arguments:
    channel The channel in the IVR
    """
    timer = channel_timers.get(channel.id)
    if timer:
        timer.cancel()
        del channel_timers[channel.id]

    
def on_dtmf_received(channel, ev):
    """Our main DTMF handler for a channel in the IVR

    Keyword Arguments:
    channel The channel in the IVR
    digit   The DTMF digit that was pressed
    """

    # Since they pressed something, cancel the timeout timer
    cancel_timeout(channel)
    digit = int(ev.get('digit'))

    print 'Channel %s entered %d' % (channel.json.get('name'), digit)
    if digit == 1:
        handle_extension_one(channel)
    elif digit == 2:
        handle_extension_two(channel)
    else:
        print 'Channel %s entered an invalid option!' % channel.json.get('name')
        channel.play(media='sound:option-is-invalid')
        play_intro_menu(channel)


def stasis_start_cb(channel_obj, ev):
    """Handler for StasisStart event"""

    channel = channel_obj.get('channel')
    print "Channel %s has entered the application" % channel.json.get('name')

    channel.on_event('ChannelDtmfReceived', on_dtmf_received)
    play_intro_menu(channel)


def stasis_end_cb(channel, ev):
    """Handler for StasisEnd event"""

    print "%s has left the application" % channel.json.get('name')
    cancel_timeout(channel)


client.on_channel_event('StasisStart', stasis_start_cb)
client.on_channel_event('StasisEnd', stasis_end_cb)

client.run(apps='channel-aa')

</pre> 
</div> 
</div> <h3 id="ARIandChannels:HandlingDTMF-channel-aa.pyinaction" style="margin: 10px 0 0 0; font-size: 16px; line-height: 25px; margin: 30px 0 0 0">channel-aa.py in action</h3> <p style="margin: 10px 0 0 0"> </p> <h2 id="ARIandChannels:HandlingDTMF-JavaScript(Node.js)" style="margin: 10px 0 0 0; font-size: 20px; font-weight: normal; line-height: 30px; margin: 40px 0 0 0">JavaScript (Node.js)</h2> <p style="margin: 10px 0 0 0"> </p> </td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
<tr> 
<td class="email-content-main mobile-expand action-padding last-row-padding" style="padding: 0px; border-collapse: collapse; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-top: 0; border-bottom: 0; padding: 0 15px 15px 16px; background-color: #fff; padding-bottom: 10px; padding-bottom: 10px"> 
<table id="actions-pattern" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 1px"> 
<tbody> 
<tr> 
<td id="actions-pattern-container" valign="middle" style="padding: 0px; border-collapse: collapse; padding: 15px 0 0 24px; vertical-align: middle"> 
<table align="left" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333"> 
<tbody> 
<tr> 
<td class="actions-pattern-action-icon-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 0px; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/display/AST/ARI+and+Channels%3A+Handling+DTMF?src=email" title="View page" style="color: #3b73af; text-decoration: none"><img class="actions-pattern-action-icon-image" src="cid:confluence.mail.templates.view.page" alt="View page-icon" title="View page-icon" height="16" width="16" border="0" style="vertical-align: middle" /></a></td>
<td class="actions-pattern-action-text-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; padding-left: 5px; white-space: nowrap"><a href="https://wiki.asterisk.org/wiki/display/AST/ARI+and+Channels%3A+Handling+DTMF?src=email" title="View page" style="color: #3b73af; text-decoration: none">View page</a></td>
<td class="actions-pattern-action-bull" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; color: #999; padding: 0 5px">•</td> 
</tr> 
</tbody> 
</table> 
<table align="left" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333"> 
<tbody> 
<tr> 
<td class="actions-pattern-action-icon-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 0px; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/display/AST/ARI+and+Channels%3A+Handling+DTMF?showComments=true&showCommentArea=true#addcomment" title="Add comment" style="color: #3b73af; text-decoration: none"><img class="actions-pattern-action-icon-image" src="cid:confluence.mail.templates.add.comment" alt="Add comment-icon" title="Add comment-icon" height="16" width="16" border="0" style="vertical-align: middle" /></a></td>
<td class="actions-pattern-action-text-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; padding-left: 5px; white-space: nowrap"><a href="https://wiki.asterisk.org/wiki/display/AST/ARI+and+Channels%3A+Handling+DTMF?showComments=true&showCommentArea=true#addcomment" title="Add comment" style="color: #3b73af; text-decoration: none">Add comment</a></td>
<td class="actions-pattern-action-bull" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; color: #999; padding: 0 5px">•</td> 
</tr> 
</tbody> 
</table> 
<table style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333"> 
<tbody> 
<tr> 
<td class="actions-pattern-action-icon-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 0px; vertical-align: middle"><a href="https://wiki.asterisk.org/wiki/plugins/likes/like.action?contentId=29395612&src=email" title="Like" style="color: #3b73af; text-decoration: none"><img class="actions-pattern-action-icon-image" src="cid:likes.like" alt="Like-icon" title="Like-icon" height="16" width="16" border="0" style="vertical-align: middle" /></a></td>
<td class="actions-pattern-action-text-container" style="padding: 0px; border-collapse: collapse; font-family: Arial, sans-serif; font-size: 14px; line-height: 20px; mso-line-height-rule: exactly; mso-text-raise: 4px; padding-left: 5px; white-space: nowrap"><a href="https://wiki.asterisk.org/wiki/plugins/likes/like.action?contentId=29395612&src=email" title="Like" style="color: #3b73af; text-decoration: none">Like</a></td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
<tr> 
<td class="email-content-rounded-bottom mobile-expand" style="padding: 0px; border-collapse: collapse; color: #fff; height: 5px; line-height: 5px; padding: 0 15px 0 16px; background-color: #fff; border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; border-top: 0; border-left: 1px solid #ccc; border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; mso-line-height-rule: exactly"> </td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
<tr> 
<td id="footer-pattern" style="padding: 0px; border-collapse: collapse; padding: 12px 20px"> 
<table id="footer-pattern-container" cellspacing="0" cellpadding="0" border="0" width="100%" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333"> 
<tbody> 
<tr> 
<td id="footer-pattern-links-container" width="100%" style="padding: 0px; border-collapse: collapse; color: #999; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px"> 
<table align="left" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px"> 
<tbody> 
<tr> 
<td class="footer-pattern-links mobile-resize-text" style="padding: 0px; border-collapse: collapse"><a href="https://wiki.asterisk.org/wiki/users/removespacenotification.action?spaceKey=AST&src=email" title="" style="color: #3b73af; text-decoration: none">Stop watching space</a></td>
<td class="footer-pattern-links-bull" style="padding: 0px; border-collapse: collapse; padding: 0 5px; color: #999">•</td> 
</tr> 
</tbody> 
</table> 
<table style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px"> 
<tbody> 
<tr> 
<td class="footer-pattern-links mobile-resize-text" style="padding: 0px; border-collapse: collapse"><a href="https://wiki.asterisk.org/wiki/users/editmyemailsettings.action?src=email" title="" style="color: #3b73af; text-decoration: none">Manage notifications</a></td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
<tr> 
<td id="footer-pattern-text" class="mobile-resize-text" width="100%" style="padding: 0px; border-collapse: collapse; color: #999; font-size: 12px; line-height: 18px; font-family: Arial, sans-serif; mso-line-height-rule: exactly; mso-text-raise: 2px; display: none">This message was sent by Atlassian Confluence 5.4.3</td> 
</tr> 
</tbody> 
</table> </td> 
</tr> 
</tbody> 
</table> 
<table id="sealed-section" border="0" cellpadding="0" cellspacing="0" width="0" style="border-collapse: collapse; mso-table-lspace: 0pt; mso-table-rspace: 0pt; color: #333; display: none"> 
<tbody> 
<tr> 
<td style="padding: 0px; border-collapse: collapse; border: 0; font-size: 0px; line-height: 0; mso-line-height-rule: exactly"></td> 
</tr> 
</tbody> 
</table>
</body>
</html>