[asterisk-commits] rmudgett: trunk r393631 - in /trunk: ./ apps/ channels/ configs/ include/aste...
SVN commits to the Asterisk project
asterisk-commits at lists.digium.com
Wed Jul 3 18:55:55 CDT 2013
Author: rmudgett
Date: Wed Jul 3 18:55:53 2013
New Revision: 393631
URL: http://svnview.digium.com/svn/asterisk?view=rev&rev=393631
Log:
Add BUGBUG note for ASTERISK-22009
Added:
trunk/apps/app_agent_pool.c (with props)
Removed:
trunk/channels/chan_agent.c
Modified:
trunk/CHANGES
trunk/UPGRADE.txt
trunk/configs/agents.conf.sample
trunk/configs/queues.conf.sample
trunk/include/asterisk/bridging.h
trunk/include/asterisk/config_options.h
trunk/include/asterisk/stasis_channels.h
trunk/main/bridging.c
trunk/main/config_options.c
trunk/main/stasis_channels.c
Modified: trunk/CHANGES
URL: http://svnview.digium.com/svn/asterisk/trunk/CHANGES?view=diff&rev=393631&r1=393630&r2=393631
==============================================================================
--- trunk/CHANGES (original)
+++ trunk/CHANGES Wed Jul 3 18:55:53 2013
@@ -14,10 +14,19 @@
Applications
------------------
+AgentLogin
+------------------
+ * The application no longer does agent authentication. The dialplan needs to
+ perform this function before running AgentLogin. If the agent is already
+ logged in, dialplan will continue with the AGENT_STATUS channel variable
+ set to ALREADY_LOGGED_IN.
+
AgentMonitorOutgoing
------------------
* The 'c' option has been removed. It is not possible to modify the name of a
channel involved in a CDR.
+ * Application removed. It was a holdover from when AgentCallbackLogin was
+ removed.
ForkCDR
------------------
@@ -244,8 +253,8 @@
of "CallerID" and "ConnectedID" to avoid confusion with similarly named
parameters in the channel snapshot.
- * The "Agentlogin" and "Agentlogoff" events have been renamed "AgentLogin" and
- "AgentLogoff" respectively.
+ * The AMI events "Agentlogin" and "Agentlogoff" have been renamed
+ "AgentLogin" and "AgentLogoff" respectively.
* The "Channel" key used in the "AlarmClear", "Alarm", and "DNDState" has been
renamed "DAHDIChannel" since it does not convey an Asterisk channel name.
@@ -423,6 +432,21 @@
and pretending otherwise helps no one.
* The AGENTUPDATECDR channel variable has also been removed, for the same
reason as the updatecdr option.
+ * The driver is no longer a Data retrieval API data provider for the
+ AMI DataGet action.
+ * The endcall and enddtmf configuration options are removed. Use the
+ dialplan function CHANNEL(dtmf-features) to set DTMF features on the agent
+ channel before calling AgentLogin.
+ * chan_agent is removed and replaced with AgentLogin and AgentRequest dialplan
+ applications. Agents are connected with callers using the new AgentRequest
+ dialplan application. The Agents:<agent-id> device state is available to
+ monitor the status of an agent. See agents.conf.sample for valid
+ configuration options.
+
+chan_bridge
+------------------
+ * chan_bridge is removed and its functionality is incorporated into ConfBridge
+ itself.
chan_local
------------------
Modified: trunk/UPGRADE.txt
URL: http://svnview.digium.com/svn/asterisk/trunk/UPGRADE.txt?view=diff&rev=393631&r1=393630&r2=393631
==============================================================================
--- trunk/UPGRADE.txt (original)
+++ trunk/UPGRADE.txt Wed Jul 3 18:55:53 2013
@@ -24,6 +24,8 @@
AgentMonitorOutgoing
- The 'c' option has been removed. It is not possible to modify the name of a
channel involved in a CDR.
+ - Application removed. It was a holdover from when AgentCallbackLogin was
+ removed.
NoCDR:
- This application is deprecated. Please use the CDR_PROP function instead.
@@ -124,6 +126,15 @@
and pretending otherwise helps no one.
- The AGENTUPDATECDR channel variable has also been removed, for the same
reason as the updatecdr option.
+ - chan_agent is removed and replaced with AgentLogin and AgentRequest dialplan
+ applications. Agents are connected with callers using the new AgentRequest
+ dialplan application. The Agents:<agent-id> device state is available to
+ monitor the status of an agent. See agents.conf.sample for valid
+ configuration options.
+
+chan_bridge
+ - chan_bridge is removed and its functionality is incorporated into ConfBridge
+ itself.
chan_dahdi:
- Analog port dialing and deferred DTMF dialing for PRI now distinguishes
Added: trunk/apps/app_agent_pool.c
URL: http://svnview.digium.com/svn/asterisk/trunk/apps/app_agent_pool.c?view=auto&rev=393631
==============================================================================
--- trunk/apps/app_agent_pool.c (added)
+++ trunk/apps/app_agent_pool.c Wed Jul 3 18:55:53 2013
@@ -1,0 +1,2486 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2013 Digium, Inc.
+ *
+ * Richard Mudgett <rmudgett at digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*!
+ * \file
+ * \brief Call center agent pool.
+ *
+ * \author Richard Mudgett <rmudgett at digium.com>
+ *
+ * See Also:
+ * \arg \ref AstCREDITS
+ * \arg \ref Config_agent
+ */
+/*** MODULEINFO
+ <support_level>core</support_level>
+ ***/
+
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/cli.h"
+#include "asterisk/app.h"
+#include "asterisk/pbx.h"
+#include "asterisk/module.h"
+#include "asterisk/channel.h"
+#include "asterisk/bridging.h"
+#include "asterisk/bridging_basic.h"
+#include "asterisk/config_options.h"
+#include "asterisk/features_config.h"
+#include "asterisk/astobj2.h"
+#include "asterisk/stringfields.h"
+#include "asterisk/stasis_channels.h"
+
+/*** DOCUMENTATION
+ <application name="AgentLogin" language="en_US">
+ <synopsis>
+ Login an agent.
+ </synopsis>
+ <syntax argsep=",">
+ <parameter name="AgentId" required="true" />
+ <parameter name="options">
+ <optionlist>
+ <option name="s">
+ <para>silent login - do not announce the login ok segment after
+ agent logged on.</para>
+ </option>
+ </optionlist>
+ </parameter>
+ </syntax>
+ <description>
+ <para>Login an agent to the system. Any agent authentication is assumed to
+ already be done by dialplan. While logged in, the agent can receive calls
+ and will hear a configurable <literal>beep</literal> sound when a new call
+ comes in for the agent. Login failures will continue in the dialplan
+ with <variable>AGENT_STATUS</variable> set.</para>
+ <para>Before logging in, you can setup on the real agent channel the
+ CHANNEL(dtmf-features) an agent will have when talking to a caller
+ and you can setup on the channel running this application the
+ CONNECTEDLINE() information the agent will see while waiting for a
+ caller.</para>
+ <para><variable>AGENT_STATUS</variable> enumeration values:</para>
+ <enumlist>
+ <enum name = "INVALID"><para>The specified agent is invalid.</para></enum>
+ <enum name = "ALREADY_LOGGED_IN"><para>The agent is already logged in.</para></enum>
+ </enumlist>
+ <note><para>The Agents:<replaceable>AgentId</replaceable> device state is
+ available to monitor the status of the agent.</para></note>
+ </description>
+ <see-also>
+ <ref type="application">Authenticate</ref>
+ <ref type="application">Queue</ref>
+ <ref type="application">AddQueueMember</ref>
+ <ref type="application">RemoveQueueMember</ref>
+ <ref type="application">PauseQueueMember</ref>
+ <ref type="application">UnpauseQueueMember</ref>
+ <ref type="function">AGENT</ref>
+ <ref type="function">CHANNEL(dtmf-features)</ref>
+ <ref type="function">CONNECTEDLINE()</ref>
+ <ref type="filename">agents.conf</ref>
+ <ref type="filename">queues.conf</ref>
+ </see-also>
+ </application>
+ <application name="AgentRequest" language="en_US">
+ <synopsis>
+ Request an agent to connect with the channel.
+ </synopsis>
+ <syntax argsep=",">
+ <parameter name="AgentId" required="true" />
+ </syntax>
+ <description>
+ <para>Request an agent to connect with the channel. Failure to find and
+ alert an agent will continue in the dialplan with <variable>AGENT_STATUS</variable> set.</para>
+ <para><variable>AGENT_STATUS</variable> enumeration values:</para>
+ <enumlist>
+ <enum name = "INVALID"><para>The specified agent is invalid.</para></enum>
+ <enum name = "NOT_LOGGED_IN"><para>The agent is not available.</para></enum>
+ <enum name = "BUSY"><para>The agent is on another call.</para></enum>
+ <enum name = "ERROR"><para>Alerting the agent failed.</para></enum>
+ </enumlist>
+ </description>
+ <see-also>
+ <ref type="application">AgentLogin</ref>
+ </see-also>
+ </application>
+ <function name="AGENT" language="en_US">
+ <synopsis>
+ Gets information about an Agent
+ </synopsis>
+ <syntax argsep=":">
+ <parameter name="AgentId" required="true" />
+ <parameter name="item">
+ <para>The valid items to retrieve are:</para>
+ <enumlist>
+ <enum name="status">
+ <para>(default) The status of the agent (LOGGEDIN | LOGGEDOUT)</para>
+ </enum>
+ <enum name="password">
+ <para>Deprecated. The dialplan handles any agent authentication.</para>
+ </enum>
+ <enum name="name">
+ <para>The name of the agent</para>
+ </enum>
+ <enum name="mohclass">
+ <para>MusicOnHold class</para>
+ </enum>
+ <enum name="channel">
+ <para>The name of the active channel for the Agent (AgentLogin)</para>
+ </enum>
+ <enum name="fullchannel">
+ <para>The untruncated name of the active channel for the Agent (AgentLogin)</para>
+ </enum>
+ </enumlist>
+ </parameter>
+ </syntax>
+ <description></description>
+ </function>
+ <manager name="Agents" language="en_US">
+ <synopsis>
+ Lists agents and their status.
+ </synopsis>
+ <syntax>
+ <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+ </syntax>
+ <description>
+ <para>Will list info about all defined agents.</para>
+ </description>
+ </manager>
+ <manager name="AgentLogoff" language="en_US">
+ <synopsis>
+ Sets an agent as no longer logged in.
+ </synopsis>
+ <syntax>
+ <xi:include xpointer="xpointer(/docs/manager[@name='Login']/syntax/parameter[@name='ActionID'])" />
+ <parameter name="Agent" required="true">
+ <para>Agent ID of the agent to log off.</para>
+ </parameter>
+ <parameter name="Soft">
+ <para>Set to <literal>true</literal> to not hangup existing calls.</para>
+ </parameter>
+ </syntax>
+ <description>
+ <para>Sets an agent as no longer logged in.</para>
+ </description>
+ </manager>
+ <configInfo name="app_agent_pool" language="en_US">
+ <synopsis>Agent pool applications</synopsis>
+ <description>
+ <note><para>Option changes take effect on agent login or after an agent
+ disconnects from a call.</para></note>
+ </description>
+ <configFile name="agents.conf">
+ <configObject name="global">
+ <synopsis>Unused, but reserved.</synopsis>
+ </configObject>
+ <configObject name="agent-id">
+ <synopsis>Configure an agent for the pool.</synopsis>
+ <description>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ <configOption name="ackcall">
+ <synopsis>Enable to require the agent to acknowledge a call.</synopsis>
+ <description>
+ <para>Enable to require the agent to give a DTMF acknowledgement
+ when the agent receives a call.</para>
+ <note><para>The option is overridden by <variable>AGENTACKCALL</variable> on agent login.</para></note>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ <configOption name="acceptdtmf">
+ <synopsis>DTMF key sequence the agent uses to acknowledge a call.</synopsis>
+ <description>
+ <note><para>The option is overridden by <variable>AGENTACCEPTDTMF</variable> on agent login.</para></note>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ <configOption name="autologoff">
+ <synopsis>Time the agent has to acknowledge a call before being logged off.</synopsis>
+ <description>
+ <para>Set how many seconds a call for the agent has to wait for the
+ agent to acknowledge the call before the agent is automatically
+ logged off. If set to zero then the call will wait forever for
+ the agent to acknowledge.</para>
+ <note><para>The option is overridden by <variable>AGENTAUTOLOGOFF</variable> on agent login.</para></note>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ <configOption name="wrapuptime">
+ <synopsis>Minimum time the agent has between calls.</synopsis>
+ <description>
+ <para>Set the minimum amount of time in milliseconds after
+ disconnecting a call before the agent can receive a new call.</para>
+ <note><para>The option is overridden by <variable>AGENTWRAPUPTIME</variable> on agent login.</para></note>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ <configOption name="musiconhold">
+ <synopsis>Music on hold class the agent listens to between calls.</synopsis>
+ <description>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ <configOption name="recordagentcalls">
+ <synopsis>Enable to automatically record calls the agent takes.</synopsis>
+ <description>
+ <para>Enable recording calls the agent takes automatically by
+ invoking the automixmon DTMF feature when the agent connects
+ to a caller. See <filename>features.conf.sample</filename> for information about
+ the automixmon feature.</para>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ <configOption name="custom_beep">
+ <synopsis>Sound file played to alert the agent when a call is present.</synopsis>
+ <description>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ <configOption name="fullname">
+ <synopsis>A friendly name for the agent used in log messages.</synopsis>
+ <description>
+ <xi:include xpointer="xpointer(/docs/configInfo[@name='app_agent_pool']/description/note)" />
+ </description>
+ </configOption>
+ </configObject>
+ </configFile>
+ </configInfo>
+ ***/
+
+/* ------------------------------------------------------------------- */
+
+#define AST_MAX_BUF 256
+
+/*! Maximum wait time (in ms) for the custom_beep file to play announcing the caller. */
+#define CALLER_SAFETY_TIMEOUT_TIME (2 * 60 * 1000)
+
+/*! Number of seconds to wait for local channel optimizations to complete. */
+#define LOGIN_WAIT_TIMEOUT_TIME 5
+
+static const char app_agent_login[] = "AgentLogin";
+static const char app_agent_request[] = "AgentRequest";
+
+/*! Agent config parameters. */
+struct agent_cfg {
+ AST_DECLARE_STRING_FIELDS(
+ /*! Identification of the agent. (agents config container key) */
+ AST_STRING_FIELD(username);
+ /*! Name of agent for logging and querying purposes */
+ AST_STRING_FIELD(full_name);
+
+ /*!
+ * \brief DTMF string for an agent to accept a call.
+ *
+ * \note The channel variable AGENTACCEPTDTMF overrides on login.
+ */
+ AST_STRING_FIELD(dtmf_accept);
+ /*! Beep sound file to use. Alert the agent a call is waiting. */
+ AST_STRING_FIELD(beep_sound);
+ /*! MOH class to use while agent waiting for call. */
+ AST_STRING_FIELD(moh);
+ );
+ /*!
+ * \brief Number of seconds for agent to ack a call before being logged off.
+ *
+ * \note The channel variable AGENTAUTOLOGOFF overrides on login.
+ * \note If zero then timer is disabled.
+ */
+ unsigned int auto_logoff;
+ /*!
+ * \brief Time after a call in ms before the agent can get a new call.
+ *
+ * \note The channel variable AGENTWRAPUPTIME overrides on login.
+ */
+ unsigned int wrapup_time;
+ /*!
+ * \brief TRUE if agent needs to ack a call to accept it.
+ *
+ * \note The channel variable AGENTACKCALL overrides on login.
+ */
+ int ack_call;
+ /*! TRUE if agent calls are automatically recorded. */
+ int record_agent_calls;
+};
+
+/*!
+ * \internal
+ * \brief Agent config ao2 container sort function.
+ * \since 12.0.0
+ *
+ * \param obj_left pointer to the (user-defined part) of an object.
+ * \param obj_right pointer to the (user-defined part) of an object.
+ * \param flags flags from ao2_callback()
+ * OBJ_POINTER - if set, 'obj_right', is an object.
+ * OBJ_KEY - if set, 'obj_right', is a search key item that is not an object.
+ * OBJ_PARTIAL_KEY - if set, 'obj_right', is a partial search key item that is not an object.
+ *
+ * \retval <0 if obj_left < obj_right
+ * \retval =0 if obj_left == obj_right
+ * \retval >0 if obj_left > obj_right
+ */
+static int agent_cfg_sort_cmp(const void *obj_left, const void *obj_right, int flags)
+{
+ const struct agent_cfg *cfg_left = obj_left;
+ const struct agent_cfg *cfg_right = obj_right;
+ const char *right_key = obj_right;
+ int cmp;
+
+ switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+ default:
+ case OBJ_POINTER:
+ right_key = cfg_right->username;
+ /* Fall through */
+ case OBJ_KEY:
+ cmp = strcmp(cfg_left->username, right_key);
+ break;
+ case OBJ_PARTIAL_KEY:
+ cmp = strncmp(cfg_left->username, right_key, strlen(right_key));
+ break;
+ }
+ return cmp;
+}
+
+static void agent_cfg_destructor(void *vdoomed)
+{
+ struct agent_cfg *doomed = vdoomed;
+
+ ast_string_field_free_memory(doomed);
+}
+
+static void *agent_cfg_alloc(const char *name)
+{
+ struct agent_cfg *cfg;
+
+ cfg = ao2_alloc_options(sizeof(*cfg), agent_cfg_destructor,
+ AO2_ALLOC_OPT_LOCK_NOLOCK);
+ if (!cfg || ast_string_field_init(cfg, 64)) {
+ return NULL;
+ }
+ ast_string_field_set(cfg, username, name);
+ return cfg;
+}
+
+static void *agent_cfg_find(struct ao2_container *agents, const char *username)
+{
+ return ao2_find(agents, username, OBJ_KEY);
+}
+
+/*! Agents configuration */
+struct agents_cfg {
+ /*! Master configured agents container. */
+ struct ao2_container *agents;
+};
+
+static struct aco_type agent_type = {
+ .type = ACO_ITEM,
+ .name = "agent-id",
+ .category_match = ACO_BLACKLIST,
+ .category = "^(general|agents)$",
+ .item_alloc = agent_cfg_alloc,
+ .item_find = agent_cfg_find,
+ .item_offset = offsetof(struct agents_cfg, agents),
+};
+
+static struct aco_type *agent_types[] = ACO_TYPES(&agent_type);
+
+/* The general category is reserved, but unused */
+static struct aco_type general_type = {
+ .type = ACO_GLOBAL,
+ .name = "global",
+ .category_match = ACO_WHITELIST,
+ .category = "^general$",
+};
+
+static struct aco_file agents_conf = {
+ .filename = "agents.conf",
+ .types = ACO_TYPES(&general_type, &agent_type),
+};
+
+static AO2_GLOBAL_OBJ_STATIC(cfg_handle);
+
+static void agents_cfg_destructor(void *vdoomed)
+{
+ struct agents_cfg *doomed = vdoomed;
+
+ ao2_cleanup(doomed->agents);
+ doomed->agents = NULL;
+}
+
+/*!
+ * \internal
+ * \brief Create struct agents_cfg object.
+ * \since 12.0.0
+ *
+ * \note A lock is not needed for the object or any secondary
+ * created cfg objects. These objects are immutable after the
+ * config is loaded and applied.
+ *
+ * \retval New struct agents_cfg object.
+ * \retval NULL on error.
+ */
+static void *agents_cfg_alloc(void)
+{
+ struct agents_cfg *cfg;
+
+ cfg = ao2_alloc_options(sizeof(*cfg), agents_cfg_destructor,
+ AO2_ALLOC_OPT_LOCK_NOLOCK);
+ if (!cfg) {
+ return NULL;
+ }
+ cfg->agents = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_NOLOCK,
+ AO2_CONTAINER_ALLOC_OPT_DUPS_REJECT, agent_cfg_sort_cmp, NULL);
+ if (!cfg->agents) {
+ ao2_ref(cfg, -1);
+ cfg = NULL;
+ }
+ return cfg;
+}
+
+static void agents_post_apply_config(void);
+
+CONFIG_INFO_STANDARD(cfg_info, cfg_handle, agents_cfg_alloc,
+ .files = ACO_FILES(&agents_conf),
+ .post_apply_config = agents_post_apply_config,
+);
+
+static void destroy_config(void)
+{
+ ao2_global_obj_release(cfg_handle);
+ aco_info_destroy(&cfg_info);
+}
+
+static int load_config(void)
+{
+ if (aco_info_init(&cfg_info)) {
+ return -1;
+ }
+
+ /* Agent options */
+ aco_option_register(&cfg_info, "ackcall", ACO_EXACT, agent_types, "no", OPT_BOOL_T, 1, FLDSET(struct agent_cfg, ack_call));
+ aco_option_register(&cfg_info, "acceptdtmf", ACO_EXACT, agent_types, "#", OPT_STRINGFIELD_T, 1, STRFLDSET(struct agent_cfg, dtmf_accept));
+ aco_option_register(&cfg_info, "autologoff", ACO_EXACT, agent_types, "0", OPT_UINT_T, 0, FLDSET(struct agent_cfg, auto_logoff));
+ aco_option_register(&cfg_info, "wrapuptime", ACO_EXACT, agent_types, "0", OPT_UINT_T, 0, FLDSET(struct agent_cfg, wrapup_time));
+ aco_option_register(&cfg_info, "musiconhold", ACO_EXACT, agent_types, "default", OPT_STRINGFIELD_T, 0, STRFLDSET(struct agent_cfg, moh));
+ aco_option_register(&cfg_info, "recordagentcalls", ACO_EXACT, agent_types, "no", OPT_BOOL_T, 1, FLDSET(struct agent_cfg, record_agent_calls));
+ aco_option_register(&cfg_info, "custom_beep", ACO_EXACT, agent_types, "beep", OPT_STRINGFIELD_T, 1, STRFLDSET(struct agent_cfg, beep_sound));
+ aco_option_register(&cfg_info, "fullname", ACO_EXACT, agent_types, NULL, OPT_STRINGFIELD_T, 0, STRFLDSET(struct agent_cfg, full_name));
+
+ if (aco_process_config(&cfg_info, 0) == ACO_PROCESS_ERROR) {
+ goto error;
+ }
+
+ return 0;
+
+error:
+ destroy_config();
+ return -1;
+}
+
+enum agent_state {
+ /*! The agent is defined but an agent is not present. */
+ AGENT_STATE_LOGGED_OUT,
+ /*! Forced initial login wait to allow any local channel optimizations to happen. */
+ AGENT_STATE_PROBATION_WAIT,
+ /*! The agent is ready for a call. */
+ AGENT_STATE_READY_FOR_CALL,
+ /*! The agent has a call waiting to connect. */
+ AGENT_STATE_CALL_PRESENT,
+ /*! The agent needs to ack the call. */
+ AGENT_STATE_CALL_WAIT_ACK,
+ /*! The agent is connected with a call. */
+ AGENT_STATE_ON_CALL,
+ /*! The agent is resting between calls. */
+ AGENT_STATE_CALL_WRAPUP,
+ /*! The agent is being kicked out. */
+ AGENT_STATE_LOGGING_OUT,
+};
+
+/*! Agent config option override flags. */
+enum agent_override_flags {
+ AGENT_FLAG_ACK_CALL = (1 << 0),
+ AGENT_FLAG_DTMF_ACCEPT = (1 << 1),
+ AGENT_FLAG_AUTO_LOGOFF = (1 << 2),
+ AGENT_FLAG_WRAPUP_TIME = (1 << 3),
+};
+
+/*! \brief Structure representing an agent. */
+struct agent_pvt {
+ AST_DECLARE_STRING_FIELDS(
+ /*! Identification of the agent. (agents container key) */
+ AST_STRING_FIELD(username);
+ /*! Login override DTMF string for an agent to accept a call. */
+ AST_STRING_FIELD(override_dtmf_accept);
+ );
+ /*! Connected line information to send when reentering the holding bridge. */
+ struct ast_party_connected_line waiting_colp;
+ /*! Flags show if settings were overridden by channel vars. */
+ unsigned int flags;
+ /*! Login override number of seconds for agent to ack a call before being logged off. */
+ unsigned int override_auto_logoff;
+ /*! Login override time after a call in ms before the agent can get a new call. */
+ unsigned int override_wrapup_time;
+ /*! Login override if agent needs to ack a call to accept it. */
+ unsigned int override_ack_call:1;
+
+ /*! TRUE if the agent is requested to logoff when the current call ends. */
+ unsigned int deferred_logoff:1;
+
+ /*! Mark and sweep config update to determine if an agent is dead. */
+ unsigned int the_mark:1;
+ /*!
+ * \brief TRUE if the agent is no longer configured and is being destroyed.
+ *
+ * \note Agents cannot log in if they are dead.
+ */
+ unsigned int dead:1;
+
+ /*! Agent control state variable. */
+ enum agent_state state;
+ /*! Custom device state of agent. */
+ enum ast_device_state devstate;
+
+ /*! When agent first logged in */
+ time_t login_start;
+ /*! When agent login probation started. */
+ time_t probation_start;
+ /*! When call started */
+ time_t call_start;
+ /*! When ack timer started */
+ struct timeval ack_time;
+ /*! When last disconnected */
+ struct timeval last_disconnect;
+
+ /*! Caller is waiting in this bridge for agent to join. (Holds ref) */
+ struct ast_bridge *caller_bridge;
+ /*! Agent is logged in with this channel. (Holds ref) (NULL if not logged in.) */
+ struct ast_channel *logged;
+ /*! Active config values from config file. (Holds ref) */
+ struct agent_cfg *cfg;
+};
+
+/*! Container of defined agents. */
+static struct ao2_container *agents;
+
+/*!
+ * \brief Lock the agent.
+ *
+ * \param agent Agent to lock
+ *
+ * \return Nothing
+ */
+#define agent_lock(agent) _agent_lock(agent, __FILE__, __PRETTY_FUNCTION__, __LINE__, #agent)
+static inline void _agent_lock(struct agent_pvt *agent, const char *file, const char *function, int line, const char *var)
+{
+ __ao2_lock(agent, AO2_LOCK_REQ_MUTEX, file, function, line, var);
+}
+
+/*!
+ * \brief Unlock the agent.
+ *
+ * \param agent Agent to unlock
+ *
+ * \return Nothing
+ */
+#define agent_unlock(agent) _agent_unlock(agent, __FILE__, __PRETTY_FUNCTION__, __LINE__, #agent)
+static inline void _agent_unlock(struct agent_pvt *agent, const char *file, const char *function, int line, const char *var)
+{
+ __ao2_unlock(agent, file, function, line, var);
+}
+
+/*!
+ * \internal
+ * \brief Obtain the agent logged in channel lock if it exists.
+ * \since 12.0.0
+ *
+ * \param agent Pointer to the LOCKED agent_pvt.
+ *
+ * \note Assumes the agent lock is already obtained.
+ *
+ * \return Nothing
+ */
+static struct ast_channel *agent_lock_logged(struct agent_pvt *agent)
+{
+ struct ast_channel *logged;
+
+ for (;;) {
+ if (!agent->logged) { /* No owner. Nothing to do. */
+ return NULL;
+ }
+
+ /* If we don't ref the logged, it could be killed when we unlock the agent. */
+ logged = ast_channel_ref(agent->logged);
+
+ /* Locking logged requires us to lock channel, then agent. */
+ agent_unlock(agent);
+ ast_channel_lock(logged);
+ agent_lock(agent);
+
+ /* Check if logged changed during agent unlock period */
+ if (logged != agent->logged) {
+ /* Channel changed. Unref and do another pass. */
+ ast_channel_unlock(logged);
+ ast_channel_unref(logged);
+ } else {
+ /* Channel stayed the same. Return it. */
+ return logged;
+ }
+ }
+}
+
+/*!
+ * \internal
+ * \brief Get the Agent:agent_id device state.
+ * \since 12.0.0
+ *
+ * \param agent_id Username of the agent.
+ *
+ * \details
+ * Search the agents container for the agent and return the
+ * current state.
+ *
+ * \return Device state of the agent.
+ */
+static enum ast_device_state agent_pvt_devstate_get(const char *agent_id)
+{
+ RAII_VAR(struct agent_pvt *, agent, ao2_find(agents, agent_id, OBJ_KEY), ao2_cleanup);
+
+ if (agent) {
+ return agent->devstate;
+ }
+ return AST_DEVICE_INVALID;
+}
+
+/*!
+ * \internal
+ * \brief Request an agent device state be updated.
+ * \since 12.0.0
+ *
+ * \param agent_id Which agent needs the device state updated.
+ *
+ * \return Nothing
+ */
+static void agent_devstate_changed(const char *agent_id)
+{
+ ast_devstate_changed(AST_DEVICE_UNKNOWN, AST_DEVSTATE_CACHABLE, "Agent:%s", agent_id);
+}
+
+static void agent_pvt_destructor(void *vdoomed)
+{
+ struct agent_pvt *doomed = vdoomed;
+
+ /* Make sure device state reflects agent destruction. */
+ if (!ast_strlen_zero(doomed->username)) {
+ ast_debug(1, "Agent %s: Destroyed.\n", doomed->username);
+ agent_devstate_changed(doomed->username);
+ }
+
+ ast_party_connected_line_free(&doomed->waiting_colp);
+ if (doomed->caller_bridge) {
+ ast_bridge_destroy(doomed->caller_bridge);
+ doomed->caller_bridge = NULL;
+ }
+ if (doomed->logged) {
+ doomed->logged = ast_channel_unref(doomed->logged);
+ }
+ ao2_cleanup(doomed->cfg);
+ doomed->cfg = NULL;
+ ast_string_field_free_memory(doomed);
+}
+
+static struct agent_pvt *agent_pvt_new(struct agent_cfg *cfg)
+{
+ struct agent_pvt *agent;
+
+ agent = ao2_alloc(sizeof(*agent), agent_pvt_destructor);
+ if (!agent) {
+ return NULL;
+ }
+ if (ast_string_field_init(agent, 32)) {
+ ao2_ref(agent, -1);
+ return NULL;
+ }
+ ast_string_field_set(agent, username, cfg->username);
+ ast_party_connected_line_init(&agent->waiting_colp);
+ ao2_ref(cfg, +1);
+ agent->cfg = cfg;
+ agent->devstate = AST_DEVICE_UNAVAILABLE;
+ return agent;
+}
+
+/*!
+ * \internal
+ * \brief Agents ao2 container sort function.
+ * \since 12.0.0
+ *
+ * \param obj_left pointer to the (user-defined part) of an object.
+ * \param obj_right pointer to the (user-defined part) of an object.
+ * \param flags flags from ao2_callback()
+ * OBJ_POINTER - if set, 'obj_right', is an object.
+ * OBJ_KEY - if set, 'obj_right', is a search key item that is not an object.
+ * OBJ_PARTIAL_KEY - if set, 'obj_right', is a partial search key item that is not an object.
+ *
+ * \retval <0 if obj_left < obj_right
+ * \retval =0 if obj_left == obj_right
+ * \retval >0 if obj_left > obj_right
+ */
+static int agent_pvt_sort_cmp(const void *obj_left, const void *obj_right, int flags)
+{
+ const struct agent_pvt *agent_left = obj_left;
+ const struct agent_pvt *agent_right = obj_right;
+ const char *right_key = obj_right;
+ int cmp;
+
+ switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+ default:
+ case OBJ_POINTER:
+ right_key = agent_right->username;
+ /* Fall through */
+ case OBJ_KEY:
+ cmp = strcmp(agent_left->username, right_key);
+ break;
+ case OBJ_PARTIAL_KEY:
+ cmp = strncmp(agent_left->username, right_key, strlen(right_key));
+ break;
+ }
+ return cmp;
+}
+
+/*!
+ * \internal
+ * \brief ao2_find() callback function.
+ * \since 12.0.0
+ *
+ * Usage:
+ * found = ao2_find(agents, agent, OBJ_POINTER);
+ * found = ao2_find(agents, "agent-id", OBJ_KEY);
+ * found = ao2_find(agents, agent->logged, 0);
+ */
+static int agent_pvt_cmp(void *obj, void *arg, int flags)
+{
+ const struct agent_pvt *agent = obj;
+ int cmp;
+
+ switch (flags & (OBJ_POINTER | OBJ_KEY | OBJ_PARTIAL_KEY)) {
+ case OBJ_POINTER:
+ case OBJ_KEY:
+ case OBJ_PARTIAL_KEY:
+ cmp = CMP_MATCH;
+ break;
+ default:
+ if (agent->logged == arg) {
+ cmp = CMP_MATCH;
+ } else {
+ cmp = 0;
+ }
+ break;
+ }
+ return cmp;
+}
+
+static int agent_mark(void *obj, void *arg, int flags)
+{
+ struct agent_pvt *agent = obj;
+
+ agent_lock(agent);
+ agent->the_mark = 1;
+ agent_unlock(agent);
+ return 0;
+}
+
+static void agents_mark(void)
+{
+ ao2_callback(agents, 0, agent_mark, NULL);
+}
+
+static int agent_sweep(void *obj, void *arg, int flags)
+{
+ struct agent_pvt *agent = obj;
+ int cmp = 0;
+
+ agent_lock(agent);
+ if (agent->the_mark) {
+ agent->the_mark = 0;
+ agent->dead = 1;
+ /* Unlink dead agents immediately. */
+ cmp = CMP_MATCH;
+ }
+ agent_unlock(agent);
+ return cmp;
+}
+
+static void agents_sweep(void)
+{
+ struct ao2_iterator *iter;
+ struct agent_pvt *agent;
+ struct ast_channel *logged;
+
+ iter = ao2_callback(agents, OBJ_MULTIPLE | OBJ_UNLINK, agent_sweep, NULL);
+ if (!iter) {
+ return;
+ }
+ for (; (agent = ao2_iterator_next(iter)); ao2_ref(agent, -1)) {
+ agent_lock(agent);
+ if (agent->logged) {
+ logged = ast_channel_ref(agent->logged);
+ } else {
+ logged = NULL;
+ }
+ agent_unlock(agent);
+ if (!logged) {
+ continue;
+ }
+ ast_log(LOG_NOTICE,
+ "Forced logoff of agent %s(%s). Agent no longer configured.\n",
+ agent->username, ast_channel_name(logged));
+ ast_softhangup(logged, AST_SOFTHANGUP_EXPLICIT);
+ ast_channel_unref(logged);
+ }
+ ao2_iterator_destroy(iter);
+}
+
+static void agents_post_apply_config(void)
+{
+ struct ao2_iterator iter;
+ struct agent_cfg *cfg;
+ RAII_VAR(struct agents_cfg *, cfgs, ao2_global_obj_ref(cfg_handle), ao2_cleanup);
+
+ ast_assert(cfgs != NULL);
+
+ agents_mark();
+ iter = ao2_iterator_init(cfgs->agents, 0);
+ for (; (cfg = ao2_iterator_next(&iter)); ao2_ref(cfg, -1)) {
+ RAII_VAR(struct agent_pvt *, agent, ao2_find(agents, cfg->username, OBJ_KEY), ao2_cleanup);
+
+ if (agent) {
+ agent_lock(agent);
+ agent->the_mark = 0;
+ if (!agent->logged) {
+ struct agent_cfg *cfg_old;
+
+ /* Replace the config of agents not logged in. */
+ cfg_old = agent->cfg;
+ ao2_ref(cfg, +1);
+ agent->cfg = cfg;
+ ao2_cleanup(cfg_old);
+ }
+ agent_unlock(agent);
+ continue;
+ }
+ agent = agent_pvt_new(cfg);
+ if (!agent) {
+ continue;
+ }
+ ao2_link(agents, agent);
+ ast_debug(1, "Agent %s: Created.\n", agent->username);
+ agent_devstate_changed(agent->username);
+ }
+ ao2_iterator_destroy(&iter);
+ agents_sweep();
+}
+
+static int agent_logoff_request(const char *agent_id, int soft)
+{
+ struct ast_channel *logged;
+ RAII_VAR(struct agent_pvt *, agent, ao2_find(agents, agent_id, OBJ_KEY), ao2_cleanup);
+
+ if (!agent) {
+ return -1;
+ }
+
+ agent_lock(agent);
+ logged = agent_lock_logged(agent);
+ if (logged) {
+ if (soft) {
+ agent->deferred_logoff = 1;
+ } else {
+ ast_softhangup(logged, AST_SOFTHANGUP_EXPLICIT);
+ }
+ ast_channel_unlock(logged);
+ ast_channel_unref(logged);
+ }
+ agent_unlock(agent);
+ return 0;
+}
+
+/*! Agent holding bridge instance holder. */
+static AO2_GLOBAL_OBJ_STATIC(agent_holding);
+
+/*! Agent holding bridge deferred creation lock. */
+AST_MUTEX_DEFINE_STATIC(agent_holding_lock);
+
+/*!
+ * \internal
+ * \brief Connect the agent with the waiting caller.
+ * \since 12.0.0
+ *
+ * \param bridge_channel Agent channel connecting to the caller.
+ * \param agent Which agent is connecting to the caller.
+ *
+ * \note The agent is locked on entry and not locked on exit.
+ *
+ * \return Nothing
+ */
+static void agent_connect_caller(struct ast_bridge_channel *bridge_channel, struct agent_pvt *agent)
+{
+ struct ast_bridge *caller_bridge;
+ int record_agent_calls;
+ int res;
+
+ record_agent_calls = agent->cfg->record_agent_calls;
+ caller_bridge = agent->caller_bridge;
+ agent->caller_bridge = NULL;
+ agent->state = AGENT_STATE_ON_CALL;
+ time(&agent->call_start);
+ agent_unlock(agent);
+
+ if (!caller_bridge) {
+ /* Reset agent. */
+ ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_END);
+ return;
+ }
+ res = ast_bridge_move(caller_bridge, bridge_channel->bridge, bridge_channel->chan,
+ NULL, 0);
+ if (res) {
+ /* Reset agent. */
+ ast_bridge_destroy(caller_bridge);
+ ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_END);
+ return;
+ }
+ ast_bridge_channel_write_control_data(bridge_channel, AST_CONTROL_ANSWER, NULL, 0);
+
+ if (record_agent_calls) {
+ struct ast_bridge_features_automixmonitor options = {
+ .start_stop = AUTO_MONITOR_START,
+ };
+
+ /*
+ * The agent is in the new bridge so we can invoke the
+ * mixmonitor hook to only start recording.
+ */
+ ast_bridge_features_do(AST_BRIDGE_BUILTIN_AUTOMIXMON, caller_bridge,
+ bridge_channel, &options);
+ }
+}
+
+static int bridge_agent_hold_ack(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt)
+{
+ struct agent_pvt *agent = hook_pvt;
+
+ agent_lock(agent);
+ switch (agent->state) {
+ case AGENT_STATE_CALL_WAIT_ACK:
+ /* Connect to caller now. */
+ ast_debug(1, "Agent %s: Acked call.\n", agent->username);
+ agent_connect_caller(bridge_channel, agent);/* Will unlock agent. */
+ return 0;
+ default:
+ break;
+ }
+ agent_unlock(agent);
+ return 0;
+}
+
+static int bridge_agent_hold_heartbeat(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt)
+{
+ struct agent_pvt *agent = hook_pvt;
+ int probation_timedout = 0;
+ int ack_timedout = 0;
+ int wrapup_timedout = 0;
+ int deferred_logoff;
+ unsigned int wrapup_time;
+ unsigned int auto_logoff;
+
+ agent_lock(agent);
+ deferred_logoff = agent->deferred_logoff;
+ if (deferred_logoff) {
+ agent->state = AGENT_STATE_LOGGING_OUT;
+ }
+
+ switch (agent->state) {
+ case AGENT_STATE_PROBATION_WAIT:
+ probation_timedout =
+ LOGIN_WAIT_TIMEOUT_TIME <= (time(NULL) - agent->probation_start);
+ if (probation_timedout) {
+ /* Now ready for a caller. */
+ agent->state = AGENT_STATE_READY_FOR_CALL;
+ agent->devstate = AST_DEVICE_NOT_INUSE;
+ }
+ break;
+ case AGENT_STATE_CALL_WAIT_ACK:
+ /* Check ack call time. */
+ auto_logoff = agent->cfg->auto_logoff;
+ if (ast_test_flag(agent, AGENT_FLAG_AUTO_LOGOFF)) {
+ auto_logoff = agent->override_auto_logoff;
+ }
+ if (auto_logoff) {
+ auto_logoff *= 1000;
+ ack_timedout = ast_tvdiff_ms(ast_tvnow(), agent->ack_time) > auto_logoff;
+ if (ack_timedout) {
+ agent->state = AGENT_STATE_LOGGING_OUT;
+ }
+ }
+ break;
+ case AGENT_STATE_CALL_WRAPUP:
+ /* Check wrapup time. */
+ wrapup_time = agent->cfg->wrapup_time;
+ if (ast_test_flag(agent, AGENT_FLAG_WRAPUP_TIME)) {
+ wrapup_time = agent->override_wrapup_time;
+ }
+ wrapup_timedout = ast_tvdiff_ms(ast_tvnow(), agent->last_disconnect) > wrapup_time;
+ if (wrapup_timedout) {
+ agent->state = AGENT_STATE_READY_FOR_CALL;
+ agent->devstate = AST_DEVICE_NOT_INUSE;
+ }
+ break;
+ default:
+ break;
+ }
+ agent_unlock(agent);
+
+ if (deferred_logoff) {
+ ast_debug(1, "Agent %s: Deferred logoff.\n", agent->username);
+ ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_END);
+ } else if (probation_timedout) {
+ ast_debug(1, "Agent %s: Login complete.\n", agent->username);
+ agent_devstate_changed(agent->username);
+ } else if (ack_timedout) {
+ ast_debug(1, "Agent %s: Ack call timeout.\n", agent->username);
+ ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_END);
+ } else if (wrapup_timedout) {
+ ast_debug(1, "Agent %s: Wrapup timeout. Ready for new call.\n", agent->username);
+ agent_devstate_changed(agent->username);
+ }
+
+ return 0;
+}
+
+static void agent_after_bridge_cb(struct ast_channel *chan, void *data);
+static void agent_after_bridge_cb_failed(enum ast_after_bridge_cb_reason reason, void *data);
+
+/*!
+ * \internal
+ * \brief ast_bridge agent_hold push method.
+ * \since 12.0.0
+ *
+ * \param self Bridge to operate upon.
+ * \param bridge_channel Bridge channel to push.
+ * \param swap Bridge channel to swap places with if not NULL.
+ *
+ * \note On entry, self is already locked.
+ *
+ * \retval 0 on success
+ * \retval -1 on failure
+ */
+static int bridge_agent_hold_push(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel, struct ast_bridge_channel *swap)
+{
+ int res = 0;
+ unsigned int wrapup_time;
+ char dtmf[AST_FEATURE_MAX_LEN];
+ struct ast_channel *chan;
+ const char *moh_class;
+ RAII_VAR(struct agent_pvt *, agent, NULL, ao2_cleanup);
+
+ chan = bridge_channel->chan;
+
+ agent = ao2_find(agents, swap ? swap->chan : chan, 0);
+ if (!agent) {
+ /* Could not find the agent. */
+ return -1;
+ }
+
+ /* Setup agent entertainment */
+ agent_lock(agent);
+ moh_class = ast_strdupa(agent->cfg->moh);
+ agent_unlock(agent);
+ res |= ast_channel_add_bridge_role(chan, "holding_participant");
+ res |= ast_channel_set_bridge_role_option(chan, "holding_participant", "idle_mode", "musiconhold");
+ res |= ast_channel_set_bridge_role_option(chan, "holding_participant", "moh_class", moh_class);
+
+ /* Add DTMF acknowledge hook. */
+ dtmf[0] = '\0';
+ agent_lock(agent);
+ if (ast_test_flag(agent, AGENT_FLAG_ACK_CALL)
+ ? agent->override_ack_call : agent->cfg->ack_call) {
+ const char *dtmf_accept;
+
+ dtmf_accept = ast_test_flag(agent, AGENT_FLAG_DTMF_ACCEPT)
+ ? agent->override_dtmf_accept : agent->cfg->dtmf_accept;
[... 2164 lines stripped ...]
More information about the asterisk-commits
mailing list