From d43b17a872e8227aa8a9905a21f90bd48f9d5348 Mon Sep 17 00:00:00 2001 From: Richard Mudgett Date: Mon, 15 Jul 2013 23:20:55 +0000 Subject: [PATCH] Replace chan_agent with app_agent_pool. The ill conceived chan_agent is no more. It is now replaced by app_agent_pool. Agents login using the AgentLogin() application as before. The AgentLogin() application no longer does any authentication. Authentication is now the responsibility of the dialplan. (Besides, the authentication done by chan_agent did not match what the voice prompts asked for.) Sample extensions.conf [login] ; Sample agent 1001 login ; Set COLP for in between calls so the agent does not see the last caller COLP. exten => 1001,1,Set(CONNECTEDLINE(all)="Agent Waiting" <1001>) ; Give the agent DTMF transfer and disconnect features when connected to a caller. same => n,Set(CHANNEL(dtmf-features)=TX) same => n,AgentLogin(1001) same => n,NoOp(AGENT_STATUS is ${AGENT_STATUS}) same => n,Hangup() [caller] ; Sample caller direct connect to agent 1001 exten => 800,1,AgentRequest(1001) same => n,NoOp(AGENT_STATUS is ${AGENT_STATUS}) same => n,Hangup() ; Sample caller going through a Queue to agent 1001 exten => 900,1,Queue(agent_q) same => n,Hangup() Sample queues.conf [agent_q] member => Local/800@caller,,SuperAgent,Agent:1001 Under the hood operation overview: 1) Logged in agents wait for callers in an agents holding bridge. 2) Caller requests an agent using AgentRequest() 3) A basic bridge is created, the agent is notified, and caller joins the basic bridge to wait for the agent. 4) The agent is either automatically connected to the caller or must ack the call to connect. 5) The agent is moved from the agents holding bridge to the basic bridge. 6) The agent and caller talk. 7) The connection is ended by either party. 8) The agent goes back to the agents holding bridge. To avoid some locking issues with the agent holding bridge, I needed to make some changes to the after bridge callback support. The after bridge callback is now a list of requested callbacks with the last to be added the only active callback. The after bridge callback for failed callbacks will always happen in the channel thread when the channel leaves the bridging system or is destroyed. (closes issue ASTERISK-21554) Reported by: Matt Jordan Review: https://reviewboard.asterisk.org/r/2657/ git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@394417 65c4cc65-6c06-0410-ace0-fbb531ad65f3 --- CHANGES | 30 +- UPGRADE.txt | 13 +- apps/app_agent_pool.c | 2597 ++++++++++++++++++++++++++++ channels/chan_agent.c | 2568 --------------------------- configs/agents.conf.sample | 132 +- configs/queues.conf.sample | 15 +- include/asterisk/bridging.h | 8 +- include/asterisk/config_options.h | 2 +- include/asterisk/stasis_channels.h | 16 + main/bridging.c | 230 ++- main/stasis_channels.c | 77 + 11 files changed, 2959 insertions(+), 2729 deletions(-) create mode 100644 apps/app_agent_pool.c delete mode 100644 channels/chan_agent.c diff --git a/CHANGES b/CHANGES index 5819d7098d..d1dfd5f41a 100644 --- a/CHANGES +++ b/CHANGES @@ -14,10 +14,17 @@ 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 ------------------ @@ -260,8 +267,8 @@ AMI (Asterisk Manager Interface) 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. @@ -453,6 +460,21 @@ chan_agent 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: 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 ------------------ diff --git a/UPGRADE.txt b/UPGRADE.txt index bcb2de4db1..f5e473bc5a 100644 --- a/UPGRADE.txt +++ b/UPGRADE.txt @@ -22,8 +22,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. @@ -140,6 +140,15 @@ chan_agent: 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: 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 diff --git a/apps/app_agent_pool.c b/apps/app_agent_pool.c new file mode 100644 index 0000000000..92209e1eb2 --- /dev/null +++ b/apps/app_agent_pool.c @@ -0,0 +1,2597 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2013 Digium, Inc. + * + * Richard Mudgett + * + * 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 + * + * See Also: + * \arg \ref AstCREDITS + * \arg \ref Config_agent + */ +/*** MODULEINFO + core + ***/ + + +#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 + + + Login an agent. + + + + + + + + + + + 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 the sound file specified by the config option custom_beep + when a new call comes in for the agent. Login failures will continue in + the dialplan with AGENT_STATUS set. + 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. + AGENT_STATUS enumeration values: + + The specified agent is invalid. + The agent is already logged in. + + The Agents:AgentId device state is + available to monitor the status of the agent. + + + Authenticate + Queue + AddQueueMember + RemoveQueueMember + PauseQueueMember + UnpauseQueueMember + AGENT + CHANNEL(dtmf-features) + CONNECTEDLINE() + agents.conf + queues.conf + + + + + Request an agent to connect with the channel. + + + + + + Request an agent to connect with the channel. Failure to find and + alert an agent will continue in the dialplan with AGENT_STATUS set. + AGENT_STATUS enumeration values: + + The specified agent is invalid. + The agent is not available. + The agent is on another call. + Alerting the agent failed. + + + + AgentLogin + + + + + Gets information about an Agent + + + + + The valid items to retrieve are: + + + (default) The status of the agent (LOGGEDIN | LOGGEDOUT) + + + Deprecated. The dialplan handles any agent authentication. + + + The name of the agent + + + MusicOnHold class + + + The name of the active channel for the Agent (AgentLogin) + + + The untruncated name of the active channel for the Agent (AgentLogin) + + + + + + + + + Lists agents and their status. + + + + + + Will list info about all defined agents. + + + Agents + AgentsComplete + + + + + + Response event in a series to the Agents AMI action containing + information about a defined agent. + + + + Agent ID of the agent. + + + User friendly name of the agent. + + + Current status of the agent. + The valid values are: + + + + + + + + BRIDGEPEER value on agent channel. + Present if Status value is AGENT_ONCALL. + + + Epoche time when the agent started talking with the caller. + Present if Status value is AGENT_ONCALL. + + + Epoche time when the agent logged in. + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + + Present if Status value is AGENT_IDLE or AGENT_ONCALL. + + + + + Agents + + + + + + + Final response event in a series of events to the Agents AMI action. + + + + + + Agents + + + + + + Sets an agent as no longer logged in. + + + + + Agent ID of the agent to log off. + + + Set to true to not hangup existing calls. + + + + Sets an agent as no longer logged in. + + + + Agent pool applications + + Option changes take effect on agent login or after an agent + disconnects from a call. + + + + Unused, but reserved. + + + Configure an agent for the pool. + + + + + Enable to require the agent to acknowledge a call. + + Enable to require the agent to give a DTMF acknowledgement + when the agent receives a call. + The option is overridden by AGENTACKCALL on agent login. + + + + + DTMF key sequence the agent uses to acknowledge a call. + + The option is overridden by AGENTACCEPTDTMF on agent login. + The option is ignored unless the ackcall option is enabled. + + + + + Time the agent has to acknowledge a call before being logged off. + + 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. + The option is overridden by AGENTAUTOLOGOFF on agent login. + The option is ignored unless the ackcall option is enabled. + + + + + Minimum time the agent has between calls. + + Set the minimum amount of time in milliseconds after + disconnecting a call before the agent can receive a new call. + The option is overridden by AGENTWRAPUPTIME on agent login. + + + + + Music on hold class the agent listens to between calls. + + + + + + Enable to automatically record calls the agent takes. + + Enable recording calls the agent takes automatically by + invoking the automixmon DTMF feature when the agent connects + to a caller. See features.conf.sample for information about + the automixmon feature. + + + + + Sound file played to alert the agent when a call is present. + + + + + + A friendly name for the agent used in log messages. + + + + + + + + ***/ + +/* ------------------------------------------------------------------- */ + +#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. + * + * \note Defined locking order is channel lock then agent lock. + * + * \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; + ast_copy_string(dtmf, dtmf_accept, sizeof(dtmf)); + } + agent_unlock(agent); + if (!ast_strlen_zero(dtmf)) { + ao2_ref(agent, +1); + if (ast_bridge_dtmf_hook(bridge_channel->features, dtmf, bridge_agent_hold_ack, + agent, __ao2_cleanup, AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + ao2_ref(agent, -1); + res = -1; + } + } + + /* Add heartbeat interval hook. */ + ao2_ref(agent, +1); + if (ast_bridge_interval_hook(bridge_channel->features, 1000, + bridge_agent_hold_heartbeat, agent, __ao2_cleanup, AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + ao2_ref(agent, -1); + res = -1; + } + + res |= ast_bridge_base_v_table.push(self, bridge_channel, swap); + if (res) { + ast_channel_remove_bridge_role(chan, "holding_participant"); + return -1; + } + + if (swap) { + res = ast_after_bridge_callback_set(chan, agent_after_bridge_cb, + agent_after_bridge_cb_failed, chan); + if (res) { + ast_channel_remove_bridge_role(chan, "holding_participant"); + return -1; + } + + agent_lock(agent); + ast_channel_unref(agent->logged); + agent->logged = ast_channel_ref(chan); + agent_unlock(agent); + + /* + * Kick the channel out so it can come back in fully controlled. + * Otherwise, the after bridge callback will linger and the + * agent will have some slightly different behavior in corner + * cases. + */ + ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_END); + return 0; + } + + agent_lock(agent); + switch (agent->state) { + case AGENT_STATE_LOGGED_OUT: + /*! + * \todo XXX the login probation time should be only if it is needed. + * + * Need to determine if there are any local channels that can + * optimize and wait until they actually do before leaving the + * AGENT_STATE_PROBATION_WAIT state. For now, the blind + * timer of LOGIN_WAIT_TIMEOUT_TIME will do. + */ + /* + * Start the login probation timer. + * + * We cannot handle an agent local channel optimization when the + * agent is on a call. The optimization may kick the agent + * channel we know about out of the call without our being able + * to switch to the replacement channel. Get any agent local + * channel optimization out of the way while the agent is in the + * holding bridge. + */ + time(&agent->probation_start); + agent->state = AGENT_STATE_PROBATION_WAIT; + agent_unlock(agent); + break; + case AGENT_STATE_PROBATION_WAIT: + /* Restart the probation timer. */ + time(&agent->probation_start); + agent_unlock(agent); + break; + case AGENT_STATE_READY_FOR_CALL: + /* + * Likely someone manually kicked us out of the holding bridge + * and we came right back in. + */ + agent_unlock(agent); + break; + default: + /* Unexpected agent state. */ + ast_assert(0); + /* Fall through */ + case AGENT_STATE_CALL_PRESENT: + case AGENT_STATE_CALL_WAIT_ACK: + agent->state = AGENT_STATE_READY_FOR_CALL; + agent->devstate = AST_DEVICE_NOT_INUSE; + agent_unlock(agent); + ast_debug(1, "Agent %s: Call abort recovery complete.\n", agent->username); + agent_devstate_changed(agent->username); + break; + case AGENT_STATE_ON_CALL: + case AGENT_STATE_CALL_WRAPUP: + wrapup_time = agent->cfg->wrapup_time; + if (ast_test_flag(agent, AGENT_FLAG_WRAPUP_TIME)) { + wrapup_time = agent->override_wrapup_time; + } + if (wrapup_time) { + agent->state = AGENT_STATE_CALL_WRAPUP; + } else { + agent->state = AGENT_STATE_READY_FOR_CALL; + agent->devstate = AST_DEVICE_NOT_INUSE; + } + agent_unlock(agent); + if (!wrapup_time) { + /* No wrapup time. */ + ast_debug(1, "Agent %s: Ready for new call.\n", agent->username); + agent_devstate_changed(agent->username); + } + break; + } + + return 0; +} + +/*! + * \internal + * \brief ast_bridge agent_hold pull method. + * + * \param self Bridge to operate upon. + * \param bridge_channel Bridge channel to pull. + * + * \details + * Remove any channel hooks controlled by the bridge. Release + * any resources held by bridge_channel->bridge_pvt and release + * bridge_channel->bridge_pvt. + * + * \note On entry, self is already locked. + * + * \return Nothing + */ +static void bridge_agent_hold_pull(struct ast_bridge *self, struct ast_bridge_channel *bridge_channel) +{ + ast_channel_remove_bridge_role(bridge_channel->chan, "holding_participant"); + ast_bridge_base_v_table.pull(self, bridge_channel); +} + +/*! + * \brief The bridge is being dissolved. + * + * \param self Bridge to operate upon. + * + * \details + * The bridge is being dissolved. Remove any external + * references to the bridge so it can be destroyed. + * + * \note On entry, self must NOT be locked. + * + * \return Nothing + */ +static void bridge_agent_hold_dissolving(struct ast_bridge *self) +{ + ao2_global_obj_replace_unref(agent_holding, NULL); + ast_bridge_base_v_table.dissolving(self); +} + +static struct ast_bridge_methods bridge_agent_hold_v_table; + +static struct ast_bridge *bridge_agent_hold_new(void) +{ + struct ast_bridge *bridge; + + bridge = ast_bridge_alloc(sizeof(struct ast_bridge), &bridge_agent_hold_v_table); + bridge = ast_bridge_base_init(bridge, AST_BRIDGE_CAPABILITY_HOLDING, + AST_BRIDGE_FLAG_MERGE_INHIBIT_TO | AST_BRIDGE_FLAG_MERGE_INHIBIT_FROM + | AST_BRIDGE_FLAG_SWAP_INHIBIT_FROM | AST_BRIDGE_FLAG_TRANSFER_PROHIBITED); + bridge = ast_bridge_register(bridge); + return bridge; +} + +static void bridging_init_agent_hold(void) +{ + /* Setup bridge agent_hold subclass v_table. */ + bridge_agent_hold_v_table = ast_bridge_base_v_table; + bridge_agent_hold_v_table.name = "agent_hold"; + bridge_agent_hold_v_table.dissolving = bridge_agent_hold_dissolving; + bridge_agent_hold_v_table.push = bridge_agent_hold_push; + bridge_agent_hold_v_table.pull = bridge_agent_hold_pull; +} + +static int bridge_agent_hold_deferred_create(void) +{ + RAII_VAR(struct ast_bridge *, holding, ao2_global_obj_ref(agent_holding), ao2_cleanup); + + if (!holding) { + ast_mutex_lock(&agent_holding_lock); + holding = ao2_global_obj_ref(agent_holding); + if (!holding) { + holding = bridge_agent_hold_new(); + ao2_global_obj_replace_unref(agent_holding, holding); + } + ast_mutex_unlock(&agent_holding_lock); + if (!holding) { + ast_log(LOG_ERROR, "Could not create agent holding bridge.\n"); + return -1; + } + } + return 0; +} + +static void send_agent_login(struct ast_channel *chan, const char *agent) +{ + RAII_VAR(struct ast_json *, blob, NULL, ast_json_unref); + + ast_assert(agent != NULL); + + blob = ast_json_pack("{s: s}", + "agent", agent); + if (!blob) { + return; + } + + ast_channel_publish_blob(chan, ast_channel_agent_login_type(), blob); +} + +static void send_agent_logoff(struct ast_channel *chan, const char *agent, long logintime) +{ + RAII_VAR(struct ast_json *, blob, NULL, ast_json_unref); + + ast_assert(agent != NULL); + + blob = ast_json_pack("{s: s, s: i}", + "agent", agent, + "logintime", logintime); + if (!blob) { + return; + } + + ast_channel_publish_blob(chan, ast_channel_agent_logoff_type(), blob); +} + +/*! + * \internal + * \brief Logout the agent. + * \since 12.0.0 + * + * \param agent Which agent logging out. + * + * \note On entry agent is already locked. On exit it is no longer locked. + * + * \return Nothing + */ +static void agent_logout(struct agent_pvt *agent) +{ + struct ast_channel *logged; + struct ast_bridge *caller_bridge; + long time_logged_in; + + time_logged_in = time(NULL) - agent->login_start; + logged = agent->logged; + agent->logged = NULL; + caller_bridge = agent->caller_bridge; + agent->caller_bridge = NULL; + agent->state = AGENT_STATE_LOGGED_OUT; + agent->devstate = AST_DEVICE_UNAVAILABLE; + ast_clear_flag(agent, AST_FLAGS_ALL); + agent_unlock(agent); + agent_devstate_changed(agent->username); + + if (caller_bridge) { + ast_bridge_destroy(caller_bridge); + } + + send_agent_logoff(logged, agent->username, time_logged_in); + ast_verb(2, "Agent '%s' logged out. Logged in for %ld seconds.\n", + agent->username, time_logged_in); + ast_channel_unref(logged); +} + +/*! + * \internal + * \brief Agent driver loop. + * \since 12.0.0 + * + * \param agent Which agent. + * \param logged The logged in channel. + * + * \return Nothing + */ +static void agent_run(struct agent_pvt *agent, struct ast_channel *logged) +{ + struct ast_bridge_features features; + + if (ast_bridge_features_init(&features)) { + goto agent_run_cleanup; + } + for (;;) { + struct agents_cfg *cfgs; + struct agent_cfg *cfg_new; + struct agent_cfg *cfg_old; + struct ast_bridge *holding; + struct ast_bridge *caller_bridge; + + holding = ao2_global_obj_ref(agent_holding); + if (!holding) { + ast_debug(1, "Agent %s: Someone destroyed the agent holding bridge.\n", + agent->username); + break; + } + + /* + * When the agent channel leaves the bridging system we usually + * want to put the agent back into the holding bridge for the + * next caller. + */ + ast_bridge_join(holding, logged, NULL, &features, NULL, 1); + if (logged != agent->logged) { + /* This channel is no longer the logged in agent. */ + break; + } + + if (agent->dead) { + /* The agent is no longer configured. */ + break; + } + + /* Update the agent's config before rejoining the holding bridge. */ + cfgs = ao2_global_obj_ref(cfg_handle); + if (!cfgs) { + /* There is no agent configuration. All agents were destroyed. */ + break; + } + cfg_new = ao2_find(cfgs->agents, agent->username, OBJ_KEY); + ao2_ref(cfgs, -1); + if (!cfg_new) { + /* The agent is no longer configured. */ + break; + } + agent_lock(agent); + cfg_old = agent->cfg; + agent->cfg = cfg_new; + + agent->last_disconnect = ast_tvnow(); + + /* Clear out any caller bridge before rejoining the holding bridge. */ + caller_bridge = agent->caller_bridge; + agent->caller_bridge = NULL; + agent_unlock(agent); + ao2_ref(cfg_old, -1); + if (caller_bridge) { + ast_bridge_destroy(caller_bridge); + } + + if (agent->state == AGENT_STATE_LOGGING_OUT + || agent->deferred_logoff + || ast_check_hangup_locked(logged)) { + /* The agent was requested to logout or hungup. */ + break; + } + + /* + * It is safe to access agent->waiting_colp without a lock. It + * is only setup on agent login and not changed. + */ + ast_channel_update_connected_line(logged, &agent->waiting_colp, NULL); + } + ast_bridge_features_cleanup(&features); + +agent_run_cleanup: + agent_lock(agent); + if (logged != agent->logged) { + /* + * We are no longer the agent channel because of local channel + * optimization. + */ + agent_unlock(agent); + ast_debug(1, "Agent %s: Channel %s is no longer the agent.\n", + agent->username, ast_channel_name(logged)); + return; + } + agent_logout(agent); +} + +static void agent_after_bridge_cb(struct ast_channel *chan, void *data) +{ + struct agent_pvt *agent; + + agent = ao2_find(agents, chan, 0); + if (!agent) { + return; + } + + ast_debug(1, "Agent %s: New agent channel %s.\n", + agent->username, ast_channel_name(chan)); + agent_run(agent, chan); + ao2_ref(agent, -1); +} + +static void agent_after_bridge_cb_failed(enum ast_after_bridge_cb_reason reason, void *data) +{ + struct ast_channel *chan = data; + struct agent_pvt *agent; + + agent = ao2_find(agents, chan, 0); + if (!agent) { + return; + } + ast_log(LOG_WARNING, "Agent %s: Forced logout. Lost control of %s because: %s\n", + agent->username, ast_channel_name(chan), + ast_after_bridge_cb_reason_string(reason)); + agent_lock(agent); + agent_logout(agent); + ao2_ref(agent, -1); +} + +/*! + * \internal + * \brief Get the lock on the agent bridge_channel and return it. + * \since 12.0.0 + * + * \param agent Whose bridge_channel to get. + * + * \retval bridge_channel on success (Reffed and locked). + * \retval NULL on error. + */ +static struct ast_bridge_channel *agent_bridge_channel_get_lock(struct agent_pvt *agent) +{ + struct ast_channel *logged; + struct ast_bridge_channel *bc; + + for (;;) { + agent_lock(agent); + logged = agent->logged; + if (!logged) { + agent_unlock(agent); + return NULL; + } + ast_channel_ref(logged); + agent_unlock(agent); + + ast_channel_lock(logged); + bc = ast_channel_get_bridge_channel(logged); + ast_channel_unlock(logged); + ast_channel_unref(logged); + if (!bc) { + if (agent->logged != logged) { + continue; + } + return NULL; + } + + ast_bridge_channel_lock(bc); + if (bc->chan != logged || agent->logged != logged) { + ast_bridge_channel_unlock(bc); + ao2_ref(bc, -1); + continue; + } + return bc; + } +} + +static void caller_abort_agent(struct agent_pvt *agent) +{ + struct ast_bridge_channel *logged; + + logged = agent_bridge_channel_get_lock(agent); + if (!logged) { + struct ast_bridge *caller_bridge; + + ast_debug(1, "Agent '%s' no longer logged in.\n", agent->username); + + agent_lock(agent); + caller_bridge = agent->caller_bridge; + agent->caller_bridge = NULL; + agent_unlock(agent); + if (caller_bridge) { + ast_bridge_destroy(caller_bridge); + } + return; + } + + /* Kick the agent out of the holding bridge to reset it. */ + ast_bridge_change_state_nolock(logged, AST_BRIDGE_CHANNEL_STATE_END); + ast_bridge_channel_unlock(logged); +} + +static int caller_safety_timeout(struct ast_bridge *bridge, struct ast_bridge_channel *bridge_channel, void *hook_pvt) +{ + struct agent_pvt *agent = hook_pvt; + + if (agent->state == AGENT_STATE_CALL_PRESENT) { + ast_verb(3, "Agent '%s' did not respond. Safety timeout.\n", agent->username); + ast_bridge_change_state(bridge_channel, AST_BRIDGE_CHANNEL_STATE_END); + caller_abort_agent(agent); + } + + return -1; +} + +static void agent_alert(struct ast_bridge_channel *bridge_channel, const void *payload, size_t payload_size) +{ + const char *agent_id = payload; + const char *playfile; + RAII_VAR(struct agent_pvt *, agent, NULL, ao2_cleanup); + + agent = ao2_find(agents, agent_id, OBJ_KEY); + if (!agent) { + ast_debug(1, "Agent '%s' does not exist. Where did it go?\n", agent_id); + return; + } + + /* Alert the agent. */ + agent_lock(agent); + playfile = ast_strdupa(agent->cfg->beep_sound); + agent_unlock(agent); + ast_stream_and_wait(bridge_channel->chan, playfile, AST_DIGIT_NONE); + + agent_lock(agent); + switch (agent->state) { + case AGENT_STATE_CALL_PRESENT: + if (ast_test_flag(agent, AGENT_FLAG_ACK_CALL) + ? agent->override_ack_call : agent->cfg->ack_call) { + agent->state = AGENT_STATE_CALL_WAIT_ACK; + agent->ack_time = ast_tvnow(); + break; + } + + /* Connect to caller now. */ + ast_debug(1, "Agent %s: Immediately connecting to call.\n", agent->username); + agent_connect_caller(bridge_channel, agent);/* Will unlock agent. */ + return; + default: + break; + } + agent_unlock(agent); +} + +static int send_alert_to_agent(struct ast_bridge_channel *bridge_channel, const char *agent_id) +{ + return ast_bridge_channel_queue_callback(bridge_channel, agent_alert, agent_id, + strlen(agent_id) + 1); +} + +static int send_colp_to_agent(struct ast_bridge_channel *bridge_channel, struct ast_party_connected_line *connected) +{ + struct ast_set_party_connected_line update = { + .id.name = 1, + .id.number = 1, + .id.subaddress = 1, + }; + unsigned char data[1024]; /* This should be large enough */ + size_t datalen; + + datalen = ast_connected_line_build_data(data, sizeof(data), connected, &update); + if (datalen == (size_t) -1) { + return 0; + } + + return ast_bridge_channel_queue_control_data(bridge_channel, + AST_CONTROL_CONNECTED_LINE, data, datalen); +} + +/*! + * \brief Dialplan AgentRequest application to locate an agent to talk with. + * + * \param chan Channel wanting to talk with an agent. + * \param data Application parameters + * + * \retval 0 To continue in dialplan. + * \retval -1 To hangup. + */ +static int agent_request_exec(struct ast_channel *chan, const char *data) +{ + struct ast_bridge *caller_bridge; + struct ast_bridge_channel *logged; + char *parse; + int res; + struct ast_bridge_features caller_features; + struct ast_party_connected_line connected; + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(agent_id); + AST_APP_ARG(other); /* Any remaining unused arguments */ + ); + + RAII_VAR(struct agent_pvt *, agent, NULL, ao2_cleanup); + + if (bridge_agent_hold_deferred_create()) { + return -1; + } + + parse = ast_strdupa(data); + AST_STANDARD_APP_ARGS(args, parse); + + if (ast_strlen_zero(args.agent_id)) { + ast_log(LOG_WARNING, "AgentRequest requires an AgentId\n"); + return -1; + } + + /* Find the agent. */ + agent = ao2_find(agents, args.agent_id, OBJ_KEY); + if (!agent) { + ast_verb(3, "Agent '%s' does not exist.\n", args.agent_id); + pbx_builtin_setvar_helper(chan, "AGENT_STATUS", "INVALID"); + return 0; + } + + if (ast_bridge_features_init(&caller_features)) { + return -1; + } + + /* Add safety timeout hook. */ + ao2_ref(agent, +1); + if (ast_bridge_interval_hook(&caller_features, CALLER_SAFETY_TIMEOUT_TIME, + caller_safety_timeout, agent, __ao2_cleanup, AST_BRIDGE_HOOK_REMOVE_ON_PULL)) { + ao2_ref(agent, -1); + ast_bridge_features_cleanup(&caller_features); + return -1; + } + + caller_bridge = ast_bridge_basic_new(); + if (!caller_bridge) { + ast_bridge_features_cleanup(&caller_features); + return -1; + } + + /* Get COLP for agent. */ + ast_party_connected_line_init(&connected); + ast_channel_lock(chan); + ast_connected_line_copy_from_caller(&connected, ast_channel_caller(chan)); + ast_channel_unlock(chan); + + agent_lock(agent); + switch (agent->state) { + case AGENT_STATE_LOGGED_OUT: + case AGENT_STATE_LOGGING_OUT: + agent_unlock(agent); + ast_party_connected_line_free(&connected); + ast_bridge_destroy(caller_bridge); + ast_bridge_features_cleanup(&caller_features); + ast_verb(3, "Agent '%s' not logged in.\n", agent->username); + pbx_builtin_setvar_helper(chan, "AGENT_STATUS", "NOT_LOGGED_IN"); + return 0; + case AGENT_STATE_READY_FOR_CALL: + ao2_ref(caller_bridge, +1); + agent->caller_bridge = caller_bridge; + agent->state = AGENT_STATE_CALL_PRESENT; + agent->devstate = AST_DEVICE_INUSE; + break; + default: + agent_unlock(agent); + ast_party_connected_line_free(&connected); + ast_bridge_destroy(caller_bridge); + ast_bridge_features_cleanup(&caller_features); + ast_verb(3, "Agent '%s' is busy.\n", agent->username); + pbx_builtin_setvar_helper(chan, "AGENT_STATUS", "BUSY"); + return 0; + } + agent_unlock(agent); + agent_devstate_changed(agent->username); + + logged = agent_bridge_channel_get_lock(agent); + if (!logged) { + ast_party_connected_line_free(&connected); + ast_bridge_destroy(caller_bridge); + ast_bridge_features_cleanup(&caller_features); + ast_verb(3, "Agent '%s' not logged in.\n", agent->username); + pbx_builtin_setvar_helper(chan, "AGENT_STATUS", "NOT_LOGGED_IN"); + caller_abort_agent(agent); + return 0; + } + + send_colp_to_agent(logged, &connected); + ast_party_connected_line_free(&connected); + + res = send_alert_to_agent(logged, agent->username); + ast_bridge_channel_unlock(logged); + ao2_ref(logged, -1); + if (res) { + ast_bridge_destroy(caller_bridge); + ast_bridge_features_cleanup(&caller_features); + ast_verb(3, "Agent '%s': Failed to alert the agent.\n", agent->username); + pbx_builtin_setvar_helper(chan, "AGENT_STATUS", "ERROR"); + caller_abort_agent(agent); + return 0; + } + + ast_indicate(chan, AST_CONTROL_RINGING); + ast_bridge_join(caller_bridge, chan, NULL, &caller_features, NULL, 1); + ast_bridge_features_cleanup(&caller_features); + + return -1; +} + +/*! + * \internal + * \brief Get agent config values from the login channel. + * \since 12.0.0 + * + * \param agent What to setup channel config values on. + * \param chan Channel logging in as an agent. + * + * \return Nothing + */ +static void agent_login_channel_config(struct agent_pvt *agent, struct ast_channel *chan) +{ + struct ast_flags opts = { 0 }; + struct ast_party_connected_line connected; + unsigned int override_ack_call = 0; + unsigned int override_auto_logoff = 0; + unsigned int override_wrapup_time = 0; + const char *override_dtmf_accept = NULL; + const char *var; + + ast_party_connected_line_init(&connected); + + /* Get config values from channel. */ + ast_channel_lock(chan); + ast_party_connected_line_copy(&connected, ast_channel_connected(chan)); + + var = pbx_builtin_getvar_helper(chan, "AGENTACKCALL"); + if (!ast_strlen_zero(var)) { + override_ack_call = ast_true(var) ? 1 : 0; + ast_set_flag(&opts, AGENT_FLAG_ACK_CALL); + } + + var = pbx_builtin_getvar_helper(chan, "AGENTACCEPTDTMF"); + if (!ast_strlen_zero(var)) { + override_dtmf_accept = ast_strdupa(var); + ast_set_flag(&opts, AGENT_FLAG_DTMF_ACCEPT); + } + + var = pbx_builtin_getvar_helper(chan, "AGENTAUTOLOGOFF"); + if (!ast_strlen_zero(var)) { + if (sscanf(var, "%u", &override_auto_logoff) == 1) { + ast_set_flag(&opts, AGENT_FLAG_AUTO_LOGOFF); + } + } + + var = pbx_builtin_getvar_helper(chan, "AGENTWRAPUPTIME"); + if (!ast_strlen_zero(var)) { + if (sscanf(var, "%u", &override_wrapup_time) == 1) { + ast_set_flag(&opts, AGENT_FLAG_WRAPUP_TIME); + } + } + ast_channel_unlock(chan); + + /* Set config values on agent. */ + agent_lock(agent); + ast_party_connected_line_free(&agent->waiting_colp); + agent->waiting_colp = connected; + + ast_string_field_set(agent, override_dtmf_accept, override_dtmf_accept); + ast_copy_flags(agent, &opts, AST_FLAGS_ALL); + agent->override_auto_logoff = override_auto_logoff; + agent->override_wrapup_time = override_wrapup_time; + agent->override_ack_call = override_ack_call; + agent_unlock(agent); +} + +enum AGENT_LOGIN_OPT_FLAGS { + OPT_SILENT = (1 << 0), +}; +AST_APP_OPTIONS(agent_login_opts, BEGIN_OPTIONS + AST_APP_OPTION('s', OPT_SILENT), +END_OPTIONS); + +/*! + * \brief Dialplan AgentLogin application to log in an agent. + * + * \param chan Channel attempting to login as an agent. + * \param data Application parameters + * + * \retval 0 To continue in dialplan. + * \retval -1 To hangup. + */ +static int agent_login_exec(struct ast_channel *chan, const char *data) +{ + char *parse; + struct ast_flags opts; + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(agent_id); + AST_APP_ARG(options); + AST_APP_ARG(other); /* Any remaining unused arguments */ + ); + + RAII_VAR(struct agent_pvt *, agent, NULL, ao2_cleanup); + + if (bridge_agent_hold_deferred_create()) { + return -1; + } + + if (ast_channel_state(chan) != AST_STATE_UP && ast_answer(chan)) { + return -1; + } + + parse = ast_strdupa(data); + AST_STANDARD_APP_ARGS(args, parse); + + if (ast_strlen_zero(args.agent_id)) { + ast_log(LOG_WARNING, "AgentLogin requires an AgentId\n"); + return -1; + } + + if (ast_app_parse_options(agent_login_opts, &opts, NULL, args.options)) { + /* General invalid option syntax. */ + return -1; + } + + /* Find the agent. */ + agent = ao2_find(agents, args.agent_id, OBJ_KEY); + if (!agent) { + ast_verb(3, "Agent '%s' does not exist.\n", args.agent_id); + pbx_builtin_setvar_helper(chan, "AGENT_STATUS", "INVALID"); + return 0; + } + + /* Has someone already logged in as this agent already? */ + agent_lock(agent); + if (agent->logged) { + agent_unlock(agent); + ast_verb(3, "Agent '%s' already logged in.\n", agent->username); + pbx_builtin_setvar_helper(chan, "AGENT_STATUS", "ALREADY_LOGGED_IN"); + return 0; + } + agent->logged = ast_channel_ref(chan); + agent->last_disconnect = ast_tvnow(); + time(&agent->login_start); + agent_unlock(agent); + + agent_login_channel_config(agent, chan); + + if (!ast_test_flag(&opts, OPT_SILENT) + && !ast_streamfile(chan, "agent-loginok", ast_channel_language(chan))) { + ast_waitstream(chan, ""); + } + + ast_verb(2, "Agent '%s' logged in (format %s/%s)\n", agent->username, + ast_getformatname(ast_channel_readformat(chan)), + ast_getformatname(ast_channel_writeformat(chan))); + send_agent_login(chan, agent->username); + + agent_run(agent, chan); + return -1; +} + +static int agent_function_read(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) +{ + char *parse; + struct agent_pvt *agent; + struct ast_channel *logged; + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(agentid); + AST_APP_ARG(item); + ); + + buf[0] = '\0'; + + parse = ast_strdupa(data ?: ""); + AST_NONSTANDARD_APP_ARGS(args, parse, ':'); + + if (ast_strlen_zero(args.agentid)) { + ast_log(LOG_WARNING, "The AGENT function requires an argument - agentid!\n"); + return -1; + } + if (!args.item) { + args.item = "status"; + } + + agent = ao2_find(agents, args.agentid, OBJ_KEY); + if (!agent) { + ast_log(LOG_WARNING, "Agent '%s' not found!\n", args.agentid); + return -1; + } + + agent_lock(agent); + if (!strcasecmp(args.item, "status")) { + const char *status; + + if (agent->logged) { + status = "LOGGEDIN"; + } else { + status = "LOGGEDOUT"; + } + ast_copy_string(buf, status, len); + } else if (!strcasecmp(args.item, "name")) { + ast_copy_string(buf, agent->cfg->full_name, len); + } else if (!strcasecmp(args.item, "mohclass")) { + ast_copy_string(buf, agent->cfg->moh, len); + } else if (!strcasecmp(args.item, "channel")) { + logged = agent_lock_logged(agent); + if (logged) { + char *pos; + + ast_copy_string(buf, ast_channel_name(logged), len); + ast_channel_unlock(logged); + ast_channel_unref(logged); + + pos = strrchr(buf, '-'); + if (pos) { + *pos = '\0'; + } + } + } else if (!strcasecmp(args.item, "fullchannel")) { + logged = agent_lock_logged(agent); + if (logged) { + ast_copy_string(buf, ast_channel_name(logged), len); + ast_channel_unlock(logged); + ast_channel_unref(logged); + } + } + agent_unlock(agent); + ao2_ref(agent, -1); + + return 0; +} + +static struct ast_custom_function agent_function = { + .name = "AGENT", + .read = agent_function_read, +}; + +struct agent_complete { + /*! Nth match to return. */ + int state; + /*! Which match currently on. */ + int which; +}; + +static int complete_agent_search(void *obj, void *arg, void *data, int flags) +{ + struct agent_complete *search = data; + + if (++search->which > search->state) { + return CMP_MATCH; + } + return 0; +} + +static char *complete_agent(const char *word, int state) +{ + char *ret; + struct agent_pvt *agent; + struct agent_complete search = { + .state = state, + }; + + agent = ao2_callback_data(agents, ast_strlen_zero(word) ? 0 : OBJ_PARTIAL_KEY, + complete_agent_search, (char *) word, &search); + if (!agent) { + return NULL; + } + ret = ast_strdup(agent->username); + ao2_ref(agent, -1); + return ret; +} + +static int complete_agent_logoff_search(void *obj, void *arg, void *data, int flags) +{ + struct agent_pvt *agent = obj; + struct agent_complete *search = data; + + if (!agent->logged) { + return 0; + } + if (++search->which > search->state) { + return CMP_MATCH; + } + return 0; +} + +static char *complete_agent_logoff(const char *word, int state) +{ + char *ret; + struct agent_pvt *agent; + struct agent_complete search = { + .state = state, + }; + + agent = ao2_callback_data(agents, ast_strlen_zero(word) ? 0 : OBJ_PARTIAL_KEY, + complete_agent_logoff_search, (char *) word, &search); + if (!agent) { + return NULL; + } + ret = ast_strdup(agent->username); + ao2_ref(agent, -1); + return ret; +} + +static void agent_show_requested(struct ast_cli_args *a, int online_only) +{ +#define FORMAT_HDR "%-8s %-20s %-11s %-30s %s\n" +#define FORMAT_ROW "%-8s %-20s %-11s %-30s %s\n" + + struct ao2_iterator iter; + struct agent_pvt *agent; + struct ast_str *out = ast_str_alloca(512); + unsigned int agents_total = 0; + unsigned int agents_logged_in = 0; + unsigned int agents_talking = 0; + + ast_cli(a->fd, FORMAT_HDR, "Agent-ID", "Name", "State", "Channel", "Talking with"); + iter = ao2_iterator_init(agents, 0); + for (; (agent = ao2_iterator_next(&iter)); ao2_ref(agent, -1)) { + struct ast_channel *logged; + + ++agents_total; + + agent_lock(agent); + logged = agent_lock_logged(agent); + if (logged) { + const char *talking_with; + + ++agents_logged_in; + + talking_with = pbx_builtin_getvar_helper(logged, "BRIDGEPEER"); + if (!ast_strlen_zero(talking_with)) { + ++agents_talking; + } else { + talking_with = ""; + } + ast_str_set(&out, 0, FORMAT_ROW, agent->username, agent->cfg->full_name, + ast_devstate_str(agent->devstate), ast_channel_name(logged), talking_with); + ast_channel_unlock(logged); + ast_channel_unref(logged); + } else { + ast_str_set(&out, 0, FORMAT_ROW, agent->username, agent->cfg->full_name, + ast_devstate_str(agent->devstate), "", ""); + } + agent_unlock(agent); + + if (!online_only || logged) { + ast_cli(a->fd, "%s", ast_str_buffer(out)); + } + } + ao2_iterator_destroy(&iter); + + ast_cli(a->fd, "\nDefined agents: %u, Logged in: %u, Talking: %u\n", + agents_total, agents_logged_in, agents_talking); + +#undef FORMAT_HDR +#undef FORMAT_ROW +} + +static char *agent_handle_show_online(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + switch (cmd) { + case CLI_INIT: + e->command = "agent show online"; + e->usage = + "Usage: agent show online\n" + " Provides summary information for logged in agents.\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + agent_show_requested(a, 1); + + return CLI_SUCCESS; +} + +static char *agent_handle_show_all(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + switch (cmd) { + case CLI_INIT: + e->command = "agent show all"; + e->usage = + "Usage: agent show all\n" + " Provides summary information for all agents.\n"; + return NULL; + case CLI_GENERATE: + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + agent_show_requested(a, 0); + + return CLI_SUCCESS; +} + +static char *agent_handle_show_specific(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + struct agent_pvt *agent; + struct ast_channel *logged; + struct ast_str *out = ast_str_alloca(4096); + + switch (cmd) { + case CLI_INIT: + e->command = "agent show"; + e->usage = + "Usage: agent show \n" + " Show information about the agent\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 2) { + return complete_agent(a->word, a->n); + } + return NULL; + } + + if (a->argc != 3) { + return CLI_SHOWUSAGE; + } + + agent = ao2_find(agents, a->argv[2], OBJ_KEY); + if (!agent) { + ast_cli(a->fd, "Agent '%s' not found\n", a->argv[2]); + return CLI_SUCCESS; + } + + agent_lock(agent); + logged = agent_lock_logged(agent); + ast_str_set(&out, 0, "Id: %s\n", agent->username); + ast_str_append(&out, 0, "Name: %s\n", agent->cfg->full_name); + ast_str_append(&out, 0, "Beep: %s\n", agent->cfg->beep_sound); + ast_str_append(&out, 0, "MOH: %s\n", agent->cfg->moh); + ast_str_append(&out, 0, "RecordCalls: %s\n", AST_CLI_YESNO(agent->cfg->record_agent_calls)); + ast_str_append(&out, 0, "State: %s\n", ast_devstate_str(agent->devstate)); + if (logged) { + const char *talking_with; + + ast_str_append(&out, 0, "LoggedInChannel: %s\n", ast_channel_name(logged)); + ast_str_append(&out, 0, "LoggedInTime: %ld\n", (long) agent->login_start); + talking_with = pbx_builtin_getvar_helper(logged, "BRIDGEPEER"); + if (!ast_strlen_zero(talking_with)) { + ast_str_append(&out, 0, "TalkingWith: %s\n", talking_with); + ast_str_append(&out, 0, "CallStarted: %ld\n", (long) agent->call_start); + } + ast_channel_unlock(logged); + ast_channel_unref(logged); + } + agent_unlock(agent); + ao2_ref(agent, -1); + + ast_cli(a->fd, "%s", ast_str_buffer(out)); + + return CLI_SUCCESS; +} + +static char *agent_handle_logoff_cmd(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) +{ + switch (cmd) { + case CLI_INIT: + e->command = "agent logoff"; + e->usage = + "Usage: agent logoff [soft]\n" + " Sets an agent as no longer logged in.\n" + " If 'soft' is specified, do not hangup existing calls.\n"; + return NULL; + case CLI_GENERATE: + if (a->pos == 2) { + return complete_agent_logoff(a->word, a->n); + } else if (a->pos == 3 && a->n == 0 + && (ast_strlen_zero(a->word) + || !strncasecmp("soft", a->word, strlen(a->word)))) { + return ast_strdup("soft"); + } + return NULL; + } + + if (a->argc < 3 || 4 < a->argc) { + return CLI_SHOWUSAGE; + } + if (a->argc == 4 && strcasecmp(a->argv[3], "soft")) { + return CLI_SHOWUSAGE; + } + + if (!agent_logoff_request(a->argv[2], a->argc == 4)) { + ast_cli(a->fd, "Logging out %s\n", a->argv[2]); + } + + return CLI_SUCCESS; +} + +static struct ast_cli_entry cli_agents[] = { + AST_CLI_DEFINE(agent_handle_show_online, "Show status of online agents"), + AST_CLI_DEFINE(agent_handle_show_all, "Show status of all agents"), + AST_CLI_DEFINE(agent_handle_show_specific, "Show information about an agent"), + AST_CLI_DEFINE(agent_handle_logoff_cmd, "Sets an agent offline"), +}; + +static int action_agents(struct mansession *s, const struct message *m) +{ + const char *id = astman_get_header(m, "ActionID"); + char id_text[AST_MAX_BUF]; + struct ao2_iterator iter; + struct agent_pvt *agent; + struct ast_str *out = ast_str_alloca(4096); + + if (!ast_strlen_zero(id)) { + snprintf(id_text, sizeof(id_text), "ActionID: %s\r\n", id); + } else { + id_text[0] = '\0'; + } + astman_send_ack(s, m, "Agents will follow"); + + iter = ao2_iterator_init(agents, 0); + for (; (agent = ao2_iterator_next(&iter)); ao2_ref(agent, -1)) { + struct ast_channel *logged; + + agent_lock(agent); + logged = agent_lock_logged(agent); + + /* + * Status Values: + * AGENT_LOGGEDOFF - Agent isn't logged in + * AGENT_IDLE - Agent is logged in, and waiting for call + * AGENT_ONCALL - Agent is logged in, and on a call + * AGENT_UNKNOWN - Don't know anything about agent. Shouldn't ever get this. + */ + ast_str_set(&out, 0, "Agent: %s\r\n", agent->username); + ast_str_append(&out, 0, "Name: %s\r\n", agent->cfg->full_name); + + if (logged) { + const char *talking_to_chan; + struct ast_str *logged_headers; + RAII_VAR(struct ast_channel_snapshot *, logged_snapshot, ast_channel_snapshot_create(logged), ao2_cleanup); + + if (!logged_snapshot + || !(logged_headers = + ast_manager_build_channel_state_string(logged_snapshot))) { + ast_channel_unlock(logged); + ast_channel_unref(logged); + agent_unlock(agent); + continue; + } + + talking_to_chan = pbx_builtin_getvar_helper(logged, "BRIDGEPEER"); + if (!ast_strlen_zero(talking_to_chan)) { + ast_str_append(&out, 0, "Status: %s\r\n", "AGENT_ONCALL"); + ast_str_append(&out, 0, "TalkingToChan: %s\r\n", talking_to_chan); + ast_str_append(&out, 0, "CallStarted: %ld\n", (long) agent->call_start); + } else { + ast_str_append(&out, 0, "Status: %s\r\n", "AGENT_IDLE"); + } + ast_str_append(&out, 0, "LoggedInTime: %ld\r\n", (long) agent->login_start); + ast_str_append(&out, 0, "%s", ast_str_buffer(logged_headers)); + ast_channel_unlock(logged); + ast_channel_unref(logged); + ast_free(logged_headers); + } else { + ast_str_append(&out, 0, "Status: %s\r\n", "AGENT_LOGGEDOFF"); + } + + agent_unlock(agent); + + astman_append(s, "Event: Agents\r\n" + "%s%s\r\n", + ast_str_buffer(out), id_text); + } + ao2_iterator_destroy(&iter); + + astman_append(s, "Event: AgentsComplete\r\n" + "%s" + "\r\n", id_text); + return 0; +} + +static int action_agent_logoff(struct mansession *s, const struct message *m) +{ + const char *agent = astman_get_header(m, "Agent"); + const char *soft_s = astman_get_header(m, "Soft"); /* "true" is don't hangup */ + + if (ast_strlen_zero(agent)) { + astman_send_error(s, m, "No agent specified"); + return 0; + } + + if (!agent_logoff_request(agent, ast_true(soft_s))) { + astman_send_ack(s, m, "Agent logged out"); + } else { + astman_send_error(s, m, "No such agent"); + } + + return 0; +} + +static int unload_module(void) +{ + struct ast_bridge *holding; + + /* Unregister dialplan applications */ + ast_unregister_application(app_agent_login); + ast_unregister_application(app_agent_request); + + /* Unregister dialplan functions */ + ast_custom_function_unregister(&agent_function); + + /* Unregister manager command */ + ast_manager_unregister("Agents"); + ast_manager_unregister("AgentLogoff"); + + /* Unregister CLI commands */ + ast_cli_unregister_multiple(cli_agents, ARRAY_LEN(cli_agents)); + + ast_devstate_prov_del("Agent"); + + /* Destroy agent holding bridge. */ + holding = ao2_global_obj_replace(agent_holding, NULL); + if (holding) { + ast_bridge_destroy(holding); + } + + destroy_config(); + ao2_ref(agents, -1); + agents = NULL; + return 0; +} + +static int load_module(void) +{ + int res = 0; + + agents = ao2_container_alloc_rbtree(AO2_ALLOC_OPT_LOCK_MUTEX, + AO2_CONTAINER_ALLOC_OPT_DUPS_REPLACE, agent_pvt_sort_cmp, agent_pvt_cmp); + if (!agents) { + return AST_MODULE_LOAD_FAILURE; + } + if (load_config()) { + ast_log(LOG_ERROR, "Unable to load config. Not loading module.\n"); + ao2_ref(agents, -1); + agents = NULL; + return AST_MODULE_LOAD_DECLINE; + } + + /* Init agent holding bridge v_table. */ + bridging_init_agent_hold(); + + /* Setup to provide Agent:agent-id device state. */ + res |= ast_devstate_prov_add("Agent", agent_pvt_devstate_get); + + /* CLI Commands */ + res |= ast_cli_register_multiple(cli_agents, ARRAY_LEN(cli_agents)); + + /* Manager commands */ + res |= ast_manager_register_xml("Agents", EVENT_FLAG_AGENT, action_agents); + res |= ast_manager_register_xml("AgentLogoff", EVENT_FLAG_AGENT, action_agent_logoff); + + /* Dialplan Functions */ + res |= ast_custom_function_register(&agent_function); + + /* Dialplan applications */ + res |= ast_register_application_xml(app_agent_login, agent_login_exec); + res |= ast_register_application_xml(app_agent_request, agent_request_exec); + + if (res) { + unload_module(); + return AST_MODULE_LOAD_FAILURE; + } + return AST_MODULE_LOAD_SUCCESS; +} + +static int reload(void) +{ + if (aco_process_config(&cfg_info, 1) == ACO_PROCESS_ERROR) { + /* Just keep the config we already have in place. */ + return -1; + } + return 0; +} + +AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Call center agent pool applications", + .load = load_module, + .unload = unload_module, + .reload = reload, + .load_pri = AST_MODPRI_DEVSTATE_PROVIDER, +); diff --git a/channels/chan_agent.c b/channels/chan_agent.c deleted file mode 100644 index 57f0914cfd..0000000000 --- a/channels/chan_agent.c +++ /dev/null @@ -1,2568 +0,0 @@ -/* - * Asterisk -- An open source telephony toolkit. - * - * Copyright (C) 1999 - 2012, Digium, Inc. - * - * Mark Spencer - * - * 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 Implementation of Agents (proxy channel) - * - * \author Mark Spencer - * - * This file is the implementation of Agents modules. - * It is a dynamic module that is loaded by Asterisk. - * \par See also - * \arg \ref Config_agent - * - * \ingroup channel_drivers - */ -/*** MODULEINFO - res_monitor - core - ***/ - -#include "asterisk.h" - -ASTERISK_FILE_VERSION(__FILE__, "$Revision$") - -#include -#include -#include -#include -#include -#include - -#include "asterisk/lock.h" -#include "asterisk/channel.h" -#include "asterisk/config.h" -#include "asterisk/module.h" -#include "asterisk/pbx.h" -#include "asterisk/sched.h" -#include "asterisk/io.h" -#include "asterisk/acl.h" -#include "asterisk/callerid.h" -#include "asterisk/file.h" -#include "asterisk/cli.h" -#include "asterisk/app.h" -#include "asterisk/musiconhold.h" -#include "asterisk/manager.h" -#include "asterisk/features.h" -#include "asterisk/utils.h" -#include "asterisk/causes.h" -#include "asterisk/astdb.h" -#include "asterisk/devicestate.h" -#include "asterisk/monitor.h" -#include "asterisk/stringfields.h" -#include "asterisk/event.h" -#include "asterisk/data.h" - -/*** DOCUMENTATION - - - Call agent login. - - - - - - - - - - - Asks the agent to login to the system. Always returns -1. - While logged in, the agent can receive calls and will hear a beep - when a new call comes in. The agent can dump the call by pressing the star key. - - - Queue - AddQueueMember - RemoveQueueMember - PauseQueueMember - UnpauseQueueMember - AGENT - agents.conf - queues.conf - - - - - Record agent's outgoing call. - - - - - - - - - - - Tries to figure out the id of the agent who is placing outgoing call based on - comparison of the callerid of the current interface and the global variable - placed by the AgentCallbackLogin application. That's why it should be used only - with the AgentCallbackLogin app. Uses the monitoring functions in chan_agent - instead of Monitor application. That has to be configured in the - agents.conf file. - Normally the app returns 0 unless the options are passed. - - - agents.conf - - - - - Gets information about an Agent - - - - - The valid items to retrieve are: - - - (default) The status of the agent (LOGGEDIN | LOGGEDOUT) - - - The password of the agent - - - The name of the agent - - - MusicOnHold class - - - The name of the active channel for the Agent (AgentLogin) - - - The untruncated name of the active channel for the Agent (AgentLogin) - - - - - - - - - Lists agents and their status. - - - - - - Will list info about all possible agents. - - - - - Sets an agent as no longer logged in. - - - - - Agent ID of the agent to log off. - - - Set to true to not hangup existing calls. - - - - Sets an agent as no longer logged in. - - - ***/ - -static const char tdesc[] = "Call Agent Proxy Channel"; -static const char config[] = "agents.conf"; - -static const char app[] = "AgentLogin"; -static const char app3[] = "AgentMonitorOutgoing"; - -static char moh[80] = "default"; - -#define AST_MAX_AGENT 80 /*!< Agent ID or Password max length */ -#define AST_MAX_BUF 256 -#define AST_MAX_FILENAME_LEN 256 - -static const char pa_family[] = "Agents"; /*!< Persistent Agents astdb family */ -#define PA_MAX_LEN 2048 /*!< The maximum length of each persistent member agent database entry */ - -#define DEFAULT_ACCEPTDTMF '#' -#define DEFAULT_ENDDTMF '*' - -static ast_group_t group; -static int autologoff; -static int wrapuptime; -static int ackcall; -static int endcall; -static int autologoffunavail = 0; -static char acceptdtmf = DEFAULT_ACCEPTDTMF; -static char enddtmf = DEFAULT_ENDDTMF; - -static int maxlogintries = 3; -static char agentgoodbye[AST_MAX_FILENAME_LEN] = "vm-goodbye"; - -static int recordagentcalls = 0; -static char recordformat[AST_MAX_BUF] = ""; -static char recordformatext[AST_MAX_BUF] = ""; -static char urlprefix[AST_MAX_BUF] = ""; -static char savecallsin[AST_MAX_BUF] = ""; -static char beep[AST_MAX_BUF] = "beep"; - -#define GETAGENTBYCALLERID "AGENTBYCALLERID" - -enum { - AGENT_FLAG_ACKCALL = (1 << 0), - AGENT_FLAG_AUTOLOGOFF = (1 << 1), - AGENT_FLAG_WRAPUPTIME = (1 << 2), - AGENT_FLAG_ACCEPTDTMF = (1 << 3), - AGENT_FLAG_ENDDTMF = (1 << 4), -}; - -/*! \brief Structure representing an agent. */ -struct agent_pvt { - ast_mutex_t lock; /*!< Channel private lock */ - int dead; /*!< Poised for destruction? */ - int pending; /*!< Not a real agent -- just pending a match */ - int abouttograb; /*!< About to grab */ - int autologoff; /*!< Auto timeout time */ - int ackcall; /*!< ackcall */ - int deferlogoff; /*!< Defer logoff to hangup */ - char acceptdtmf; - char enddtmf; - time_t loginstart; /*!< When agent first logged in (0 when logged off) */ - time_t start; /*!< When call started */ - struct timeval lastdisc; /*!< When last disconnected */ - int wrapuptime; /*!< Wrapup time in ms */ - ast_group_t group; /*!< Group memberships */ - int acknowledged; /*!< Acknowledged */ - char moh[80]; /*!< Which music on hold */ - char agent[AST_MAX_AGENT]; /*!< Agent ID */ - char password[AST_MAX_AGENT]; /*!< Password for Agent login */ - char name[AST_MAX_AGENT]; - int app_lock_flag; - ast_cond_t app_complete_cond; - ast_cond_t login_wait_cond; - int app_sleep_cond; /*!< Non-zero if the login app should sleep. */ - struct ast_channel *owner; /*!< Agent */ - struct ast_channel *chan; /*!< Channel we use */ - unsigned int flags; /*!< Flags show if settings were applied with channel vars */ - AST_LIST_ENTRY(agent_pvt) list;/*!< Next Agent in the linked list. */ -}; - -#define DATA_EXPORT_AGENT(MEMBER) \ - MEMBER(agent_pvt, autologoff, AST_DATA_INTEGER) \ - MEMBER(agent_pvt, ackcall, AST_DATA_BOOLEAN) \ - MEMBER(agent_pvt, deferlogoff, AST_DATA_BOOLEAN) \ - MEMBER(agent_pvt, wrapuptime, AST_DATA_MILLISECONDS) \ - MEMBER(agent_pvt, acknowledged, AST_DATA_BOOLEAN) \ - MEMBER(agent_pvt, name, AST_DATA_STRING) \ - MEMBER(agent_pvt, password, AST_DATA_PASSWORD) \ - MEMBER(agent_pvt, acceptdtmf, AST_DATA_CHARACTER) - -AST_DATA_STRUCTURE(agent_pvt, DATA_EXPORT_AGENT); - -static AST_LIST_HEAD_STATIC(agents, agent_pvt); /*!< Holds the list of agents (loaded form agents.conf). */ - -#define CHECK_FORMATS(ast, p) do { \ - if (p->chan) {\ - if (!(ast_format_cap_identical(ast_channel_nativeformats(ast), ast_channel_nativeformats(p->chan)))) { \ - char tmp1[256], tmp2[256]; \ - ast_debug(1, "Native formats changing from '%s' to '%s'\n", ast_getformatname_multiple(tmp1, sizeof(tmp1), ast_channel_nativeformats(ast)), ast_getformatname_multiple(tmp2, sizeof(tmp2), ast_channel_nativeformats(p->chan))); \ - /* Native formats changed, reset things */ \ - ast_format_cap_copy(ast_channel_nativeformats(ast), ast_channel_nativeformats(p->chan)); \ - ast_debug(1, "Resetting read to '%s' and write to '%s'\n", ast_getformatname(ast_channel_readformat(ast)), ast_getformatname(ast_channel_writeformat(ast)));\ - ast_set_read_format(ast, ast_channel_readformat(ast)); \ - ast_set_write_format(ast, ast_channel_writeformat(ast)); \ - } \ - if ((ast_format_cmp(ast_channel_readformat(p->chan), ast_channel_rawreadformat(ast)) != AST_FORMAT_CMP_EQUAL) && !ast_channel_generator(p->chan)) \ - ast_set_read_format(p->chan, ast_channel_rawreadformat(ast)); \ - if ((ast_format_cmp(ast_channel_writeformat(p->chan), ast_channel_rawwriteformat(ast)) != AST_FORMAT_CMP_EQUAL) && !ast_channel_generator(p->chan)) \ - ast_set_write_format(p->chan, ast_channel_rawwriteformat(ast)); \ - } \ -} while(0) - -/*! \brief Cleanup moves all the relevant FD's from the 2nd to the first, but retains things - properly for a timingfd XXX This might need more work if agents were logged in as agents or other - totally impractical combinations XXX */ - -#define CLEANUP(ast, p) do { \ - int x; \ - if (p->chan) { \ - for (x = 0; x < AST_MAX_FDS; x++) { \ - if (x != AST_TIMING_FD) { \ - ast_channel_set_fd(ast, x, ast_channel_fd(p->chan, x)); \ - } \ - } \ - ast_channel_set_fd(ast, AST_AGENT_FD, ast_channel_fd(p->chan, AST_TIMING_FD)); \ - } \ -} while(0) - -/*--- Forward declarations */ -static struct ast_channel *agent_request(const char *type, struct ast_format_cap *cap, const struct ast_channel *requestor, const char *data, int *cause); -static int agent_devicestate(const char *data); -static int agent_digit_begin(struct ast_channel *ast, char digit); -static int agent_digit_end(struct ast_channel *ast, char digit, unsigned int duration); -static int agent_call(struct ast_channel *ast, const char *dest, int timeout); -static int agent_hangup(struct ast_channel *ast); -static int agent_answer(struct ast_channel *ast); -static struct ast_frame *agent_read(struct ast_channel *ast); -static int agent_write(struct ast_channel *ast, struct ast_frame *f); -static int agent_sendhtml(struct ast_channel *ast, int subclass, const char *data, int datalen); -static int agent_sendtext(struct ast_channel *ast, const char *text); -static int agent_indicate(struct ast_channel *ast, int condition, const void *data, size_t datalen); -static int agent_fixup(struct ast_channel *oldchan, struct ast_channel *newchan); -static struct ast_channel *agent_bridgedchannel(struct ast_channel *chan, struct ast_channel *bridge); -static char *complete_agent_logoff_cmd(const char *line, const char *word, int pos, int state); -static struct ast_channel* agent_get_base_channel(struct ast_channel *chan); -static int agent_logoff(const char *agent, int soft); - -/* BUGBUG This channel driver is totally hosed until it is rewritten. */ -/*! \brief Channel interface description for PBX integration */ -static struct ast_channel_tech agent_tech = { - .type = "Agent", - .description = tdesc, - .requester = agent_request, - .devicestate = agent_devicestate, - .send_digit_begin = agent_digit_begin, - .send_digit_end = agent_digit_end, - .call = agent_call, - .hangup = agent_hangup, - .answer = agent_answer, - .read = agent_read, - .write = agent_write, - .write_video = agent_write, - .send_html = agent_sendhtml, - .send_text = agent_sendtext, - .exception = agent_read, - .indicate = agent_indicate, - .fixup = agent_fixup, - .bridged_channel = agent_bridgedchannel, - .get_base_channel = agent_get_base_channel, -}; - -/*! - * \brief Locks the owning channel for a LOCKED pvt while obeying locking order. The pvt - * must enter this function locked and will be returned locked, but this function will - * unlock the pvt for a short time, so it can't be used while expecting the pvt to remain - * static. If function returns a non NULL channel, it will need to be unlocked and - * unrefed once it is no longer needed. - * - * \param pvt Pointer to the LOCKED agent_pvt for which the owner is needed - * locked channel which owns the pvt at the time of completion. NULL if not available. - */ -static struct ast_channel *agent_lock_owner(struct agent_pvt *pvt) -{ - struct ast_channel *owner; - - for (;;) { - if (!pvt->owner) { /* No owner. Nothing to do. */ - return NULL; - } - - /* If we don't ref the owner, it could be killed when we unlock the pvt. */ - owner = ast_channel_ref(pvt->owner); - - /* Locking order requires us to lock channel, then pvt. */ - ast_mutex_unlock(&pvt->lock); - ast_channel_lock(owner); - ast_mutex_lock(&pvt->lock); - - /* Check if owner changed during pvt unlock period */ - if (owner != pvt->owner) { /* Channel changed. Unref and do another pass. */ - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } else { /* Channel stayed the same. Return it. */ - return owner; - } - } -} - -/*! - * \internal - * \brief Destroy an agent pvt struct. - * - * \param doomed Agent pvt to destroy. - * - * \return Nothing - */ -static void agent_pvt_destroy(struct agent_pvt *doomed) -{ - ast_mutex_destroy(&doomed->lock); - ast_cond_destroy(&doomed->app_complete_cond); - ast_cond_destroy(&doomed->login_wait_cond); - ast_free(doomed); -} - -/*! - * Adds an agent to the global list of agents. - * - * \param agent A string with the username, password and real name of an agent. As defined in agents.conf. Example: "13,169,John Smith" - * \param pending If it is pending or not. - * @return The just created agent. - * \sa agent_pvt, agents. - */ -static struct agent_pvt *add_agent(const char *agent, int pending) -{ - char *parse; - AST_DECLARE_APP_ARGS(args, - AST_APP_ARG(agt); - AST_APP_ARG(password); - AST_APP_ARG(name); - ); - char *password = NULL; - char *name = NULL; - char *agt = NULL; - struct agent_pvt *p; - - parse = ast_strdupa(agent); - - /* Extract username (agt), password and name from agent (args). */ - AST_STANDARD_APP_ARGS(args, parse); - - if(args.argc == 0) { - ast_log(LOG_WARNING, "A blank agent line!\n"); - return NULL; - } - - if(ast_strlen_zero(args.agt) ) { - ast_log(LOG_WARNING, "An agent line with no agentid!\n"); - return NULL; - } else - agt = args.agt; - - if(!ast_strlen_zero(args.password)) { - password = args.password; - while (*password && *password < 33) password++; - } - if(!ast_strlen_zero(args.name)) { - name = args.name; - while (*name && *name < 33) name++; - } - - if (!pending) { - /* Are we searching for the agent here ? To see if it exists already ? */ - AST_LIST_TRAVERSE(&agents, p, list) { - if (!strcmp(p->agent, agt)) { - break; - } - } - } else { - p = NULL; - } - if (!p) { - // Build the agent. - if (!(p = ast_calloc(1, sizeof(*p)))) - return NULL; - ast_copy_string(p->agent, agt, sizeof(p->agent)); - ast_mutex_init(&p->lock); - ast_cond_init(&p->app_complete_cond, NULL); - ast_cond_init(&p->login_wait_cond, NULL); - p->app_lock_flag = 0; - p->app_sleep_cond = 1; - p->group = group; - p->pending = pending; - AST_LIST_INSERT_TAIL(&agents, p, list); - } - - ast_copy_string(p->password, password ? password : "", sizeof(p->password)); - ast_copy_string(p->name, name ? name : "", sizeof(p->name)); - ast_copy_string(p->moh, moh, sizeof(p->moh)); - if (!ast_test_flag(p, AGENT_FLAG_ACKCALL)) { - p->ackcall = ackcall; - } - if (!ast_test_flag(p, AGENT_FLAG_AUTOLOGOFF)) { - p->autologoff = autologoff; - } - if (!ast_test_flag(p, AGENT_FLAG_ACCEPTDTMF)) { - p->acceptdtmf = acceptdtmf; - } - if (!ast_test_flag(p, AGENT_FLAG_ENDDTMF)) { - p->enddtmf = enddtmf; - } - - /* If someone reduces the wrapuptime and reloads, we want it - * to change the wrapuptime immediately on all calls */ - if (!ast_test_flag(p, AGENT_FLAG_WRAPUPTIME) && p->wrapuptime > wrapuptime) { - struct timeval now = ast_tvnow(); - /* XXX check what is this exactly */ - - /* We won't be pedantic and check the tv_usec val */ - if (p->lastdisc.tv_sec > (now.tv_sec + wrapuptime/1000)) { - p->lastdisc.tv_sec = now.tv_sec + wrapuptime/1000; - p->lastdisc.tv_usec = now.tv_usec; - } - } - p->wrapuptime = wrapuptime; - - if (pending) - p->dead = 1; - else - p->dead = 0; - return p; -} - -/*! - * Deletes an agent after doing some clean up. - * Further documentation: How safe is this function ? What state should the agent be to be cleaned. - * - * \warning XXX This function seems to be very unsafe. - * Potential for double free and use after free among other - * problems. - * - * \param p Agent to be deleted. - * \returns Always 0. - */ -static int agent_cleanup(struct agent_pvt *p) -{ - struct ast_channel *chan; - - ast_mutex_lock(&p->lock); - chan = p->owner; - p->owner = NULL; - /* Release ownership of the agent to other threads (presumably running the login app). */ - p->app_sleep_cond = 1; - p->app_lock_flag = 0; - ast_cond_signal(&p->app_complete_cond); - if (chan) { - ast_channel_tech_pvt_set(chan, NULL); - chan = ast_channel_release(chan); - } - if (p->dead) { - ast_mutex_unlock(&p->lock); - agent_pvt_destroy(p); - } else { - ast_mutex_unlock(&p->lock); - } - return 0; -} - -static int agent_answer(struct ast_channel *ast) -{ - ast_log(LOG_WARNING, "Huh? Agent is being asked to answer?\n"); - return -1; -} - -static int __agent_start_monitoring(struct ast_channel *ast, struct agent_pvt *p, int needlock) -{ - char tmp[AST_MAX_BUF], tmp2[AST_MAX_BUF], *pointer; - char filename[AST_MAX_BUF]; - int res = -1; - if (!p) - return -1; - if (!ast_channel_monitor(ast)) { - snprintf(filename, sizeof(filename), "agent-%s-%s",p->agent, ast_channel_uniqueid(ast)); - /* substitute . for - */ - if ((pointer = strchr(filename, '.'))) - *pointer = '-'; - snprintf(tmp, sizeof(tmp), "%s%s", savecallsin, filename); - ast_monitor_start(ast, recordformat, tmp, needlock, X_REC_IN | X_REC_OUT); - ast_monitor_setjoinfiles(ast, 1); - snprintf(tmp2, sizeof(tmp2), "%s%s.%s", urlprefix, filename, recordformatext); -#if 0 - ast_verbose("name is %s, link is %s\n",tmp, tmp2); -#endif - ast_cdr_setuserfield(ast_channel_name(ast), tmp2); - res = 0; - } else - ast_log(LOG_ERROR, "Recording already started on that call.\n"); - return res; -} - -static int agent_start_monitoring(struct ast_channel *ast, int needlock) -{ - return __agent_start_monitoring(ast, ast_channel_tech_pvt(ast), needlock); -} - -static struct ast_frame *agent_read(struct ast_channel *ast) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - struct ast_frame *f = NULL; - static struct ast_frame answer_frame = { AST_FRAME_CONTROL, { AST_CONTROL_ANSWER } }; - int cur_time = time(NULL); - struct ast_channel *owner; - - ast_mutex_lock(&p->lock); - owner = agent_lock_owner(p); - - CHECK_FORMATS(ast, p); - if (!p->start) { - p->start = cur_time; - } - if (p->chan) { - ast_copy_flags(ast_channel_flags(p->chan), ast_channel_flags(ast), AST_FLAG_EXCEPTION); - ast_channel_fdno_set(p->chan, (ast_channel_fdno(ast) == AST_AGENT_FD) ? AST_TIMING_FD : ast_channel_fdno(ast)); - f = ast_read(p->chan); - ast_channel_fdno_set(ast, -1); - } else - f = &ast_null_frame; - if (f) { - /* if acknowledgement is not required, and the channel is up, we may have missed - an AST_CONTROL_ANSWER (if there was one), so mark the call acknowledged anyway */ - if (!p->ackcall && !p->acknowledged && p->chan && (ast_channel_state(p->chan) == AST_STATE_UP)) { - p->acknowledged = 1; - } - - if (!p->acknowledged) { - int howlong = cur_time - p->start; - if (p->autologoff && (howlong >= p->autologoff)) { - ast_log(LOG_NOTICE, "Agent '%s' didn't answer/confirm within %d seconds (waited %d)\n", p->name, p->autologoff, howlong); - if (owner || p->chan) { - if (owner) { - ast_softhangup(owner, AST_SOFTHANGUP_EXPLICIT); - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - - while (p->chan && ast_channel_trylock(p->chan)) { - DEADLOCK_AVOIDANCE(&p->lock); - } - if (p->chan) { - ast_softhangup(p->chan, AST_SOFTHANGUP_EXPLICIT); - ast_channel_unlock(p->chan); - } - } - } - } - switch (f->frametype) { - case AST_FRAME_CONTROL: - if (f->subclass.integer == AST_CONTROL_ANSWER) { - if (p->ackcall) { - ast_verb(3, "%s answered, waiting for '%c' to acknowledge\n", ast_channel_name(p->chan), p->acceptdtmf); - /* Don't pass answer along */ - ast_frfree(f); - f = &ast_null_frame; - } else { - p->acknowledged = 1; - /* Use the builtin answer frame for the - recording start check below. */ - ast_frfree(f); - f = &answer_frame; - } - } - break; - case AST_FRAME_DTMF_BEGIN: - /*ignore DTMF begin's as it can cause issues with queue announce files*/ - if((!p->acknowledged && f->subclass.integer == p->acceptdtmf) || (f->subclass.integer == p->enddtmf && endcall)){ - ast_frfree(f); - f = &ast_null_frame; - } - break; - case AST_FRAME_DTMF_END: - if (!p->acknowledged && (f->subclass.integer == p->acceptdtmf)) { - if (p->chan) { - ast_verb(3, "%s acknowledged\n", ast_channel_name(p->chan)); - } - p->acknowledged = 1; - ast_frfree(f); - f = &answer_frame; - } else if (f->subclass.integer == p->enddtmf && endcall) { - /* terminates call */ - ast_frfree(f); - f = NULL; - } - break; - case AST_FRAME_VOICE: - case AST_FRAME_VIDEO: - /* don't pass voice or video until the call is acknowledged */ - if (!p->acknowledged) { - ast_frfree(f); - f = &ast_null_frame; - } - default: - /* pass everything else on through */ - break; - } - } - - if (owner) { - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - - CLEANUP(ast,p); - if (p->chan && !ast_channel_internal_bridged_channel(p->chan)) { - if (strcasecmp(ast_channel_tech(p->chan)->type, "Local")) { - ast_channel_internal_bridged_channel_set(p->chan, ast); - ast_debug(1, "Bridge on '%s' being set to '%s' (3)\n", ast_channel_name(p->chan), ast_channel_name(ast_channel_internal_bridged_channel(p->chan))); - } - } - ast_mutex_unlock(&p->lock); - if (recordagentcalls && f == &answer_frame) - agent_start_monitoring(ast,0); - return f; -} - -static int agent_sendhtml(struct ast_channel *ast, int subclass, const char *data, int datalen) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - int res = -1; - ast_mutex_lock(&p->lock); - if (p->chan) - res = ast_channel_sendhtml(p->chan, subclass, data, datalen); - ast_mutex_unlock(&p->lock); - return res; -} - -static int agent_sendtext(struct ast_channel *ast, const char *text) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - int res = -1; - ast_mutex_lock(&p->lock); - if (p->chan) - res = ast_sendtext(p->chan, text); - ast_mutex_unlock(&p->lock); - return res; -} - -static int agent_write(struct ast_channel *ast, struct ast_frame *f) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - int res = -1; - CHECK_FORMATS(ast, p); - ast_mutex_lock(&p->lock); - if (!p->chan) - res = 0; - else { - if ((f->frametype != AST_FRAME_VOICE) || - (f->frametype != AST_FRAME_VIDEO) || - (ast_format_cmp(&f->subclass.format, ast_channel_writeformat(p->chan)) != AST_FORMAT_CMP_NOT_EQUAL)) { - res = ast_write(p->chan, f); - } else { - ast_debug(1, "Dropping one incompatible %s frame on '%s' to '%s'\n", - f->frametype == AST_FRAME_VOICE ? "audio" : "video", - ast_channel_name(ast), ast_channel_name(p->chan)); - res = 0; - } - } - CLEANUP(ast, p); - ast_mutex_unlock(&p->lock); - return res; -} - -static int agent_fixup(struct ast_channel *oldchan, struct ast_channel *newchan) -{ - struct agent_pvt *p = ast_channel_tech_pvt(newchan); - ast_mutex_lock(&p->lock); - if (p->owner != oldchan) { - ast_log(LOG_WARNING, "old channel wasn't %p but was %p\n", oldchan, p->owner); - ast_mutex_unlock(&p->lock); - return -1; - } - p->owner = newchan; - ast_mutex_unlock(&p->lock); - return 0; -} - -static int agent_indicate(struct ast_channel *ast, int condition, const void *data, size_t datalen) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - int res = -1; - - ast_mutex_lock(&p->lock); - if (p->chan && !ast_check_hangup(p->chan)) { - ast_channel_unlock(ast); - ast_channel_lock(p->chan); - res = ast_channel_tech(p->chan)->indicate - ? ast_channel_tech(p->chan)->indicate(p->chan, condition, data, datalen) - : -1; - ast_channel_unlock(p->chan); - ast_mutex_unlock(&p->lock); - ast_channel_lock(ast); - } else { - ast_mutex_unlock(&p->lock); - res = 0; - } - return res; -} - -static int agent_digit_begin(struct ast_channel *ast, char digit) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - ast_mutex_lock(&p->lock); - if (p->chan) { - ast_senddigit_begin(p->chan, digit); - } - ast_mutex_unlock(&p->lock); - return 0; -} - -static int agent_digit_end(struct ast_channel *ast, char digit, unsigned int duration) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - ast_mutex_lock(&p->lock); - if (p->chan) { - ast_senddigit_end(p->chan, digit, duration); - } - ast_mutex_unlock(&p->lock); - return 0; -} - -static int agent_call(struct ast_channel *ast, const char *dest, int timeout) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - int res; - int newstate=0; - - ast_mutex_lock(&p->lock); - p->acknowledged = 0; - - if (p->pending) { - ast_log(LOG_DEBUG, "Pretending to dial on pending agent\n"); - ast_mutex_unlock(&p->lock); - ast_setstate(ast, AST_STATE_DIALING); - return 0; - } - - ast_assert(p->chan != NULL); - ast_verb(3, "agent_call, call to agent '%s' call on '%s'\n", p->agent, ast_channel_name(p->chan)); - ast_debug(3, "Playing beep, lang '%s'\n", ast_channel_language(p->chan)); - - ast_mutex_unlock(&p->lock); - - res = ast_streamfile(p->chan, beep, ast_channel_language(p->chan)); - ast_debug(3, "Played beep, result '%d'\n", res); - if (!res) { - res = ast_waitstream(p->chan, ""); - ast_debug(3, "Waited for stream, result '%d'\n", res); - } - - ast_mutex_lock(&p->lock); - - if (!res) { - struct ast_format tmpfmt; - res = ast_set_read_format_from_cap(p->chan, ast_channel_nativeformats(p->chan)); - ast_debug(3, "Set read format, result '%d'\n", res); - if (res) - ast_log(LOG_WARNING, "Unable to set read format to %s\n", ast_getformatname(&tmpfmt)); - } - - if (!res) { - struct ast_format tmpfmt; - res = ast_set_write_format_from_cap(p->chan, ast_channel_nativeformats(p->chan)); - ast_debug(3, "Set write format, result '%d'\n", res); - if (res) - ast_log(LOG_WARNING, "Unable to set write format to %s\n", ast_getformatname(&tmpfmt)); - } - if(!res) { - /* Call is immediately up, or might need ack */ - if (p->ackcall) { - newstate = AST_STATE_RINGING; - } else { - newstate = AST_STATE_UP; - if (recordagentcalls) - agent_start_monitoring(ast, 0); - p->acknowledged = 1; - } - } - CLEANUP(ast, p); - ast_mutex_unlock(&p->lock); - if (newstate) - ast_setstate(ast, newstate); - return res ? -1 : 0; -} - -/*! \brief return the channel or base channel if one exists. This function assumes the channel it is called on is already locked */ -struct ast_channel* agent_get_base_channel(struct ast_channel *chan) -{ - struct agent_pvt *p; - struct ast_channel *base = chan; - - /* chan is locked by the calling function */ - if (!chan || !ast_channel_tech_pvt(chan)) { - ast_log(LOG_ERROR, "whoa, you need a channel (0x%ld) with a tech_pvt (0x%ld) to get a base channel.\n", (long)chan, (chan)?(long)ast_channel_tech_pvt(chan):(long)NULL); - return NULL; - } - p = ast_channel_tech_pvt(chan); - if (p->chan) - base = p->chan; - return base; -} - -static int agent_hangup(struct ast_channel *ast) -{ - struct agent_pvt *p = ast_channel_tech_pvt(ast); - struct ast_channel *indicate_chan = NULL; - char *tmp_moh; /* moh buffer for indicating after unlocking p */ - - if (p->pending) { - AST_LIST_LOCK(&agents); - AST_LIST_REMOVE(&agents, p, list); - AST_LIST_UNLOCK(&agents); - } - - ast_mutex_lock(&p->lock); - p->owner = NULL; - ast_channel_tech_pvt_set(ast, NULL); - p->acknowledged = 0; - - /* if they really are hung up then set start to 0 so the test - * later if we're called on an already downed channel - * doesn't cause an agent to be logged out like when - * agent_request() is followed immediately by agent_hangup() - * as in apps/app_chanisavail.c:chanavail_exec() - */ - - ast_debug(1, "Hangup called for state %s\n", ast_state2str(ast_channel_state(ast))); - p->start = 0; - if (p->chan) { - ast_channel_internal_bridged_channel_set(p->chan, NULL); - /* If they're dead, go ahead and hang up on the agent now */ - if (p->dead) { - ast_softhangup(p->chan, AST_SOFTHANGUP_EXPLICIT); - } else if (p->loginstart) { - indicate_chan = ast_channel_ref(p->chan); - tmp_moh = ast_strdupa(p->moh); - } - } - ast_mutex_unlock(&p->lock); - - if (indicate_chan) { - ast_indicate_data(indicate_chan, AST_CONTROL_HOLD, - S_OR(tmp_moh, NULL), - !ast_strlen_zero(tmp_moh) ? strlen(tmp_moh) + 1 : 0); - indicate_chan = ast_channel_unref(indicate_chan); - } - - ast_mutex_lock(&p->lock); - if (p->abouttograb) { - /* Let the "about to grab" thread know this isn't valid anymore, and let it - kill it later */ - p->abouttograb = 0; - } else if (p->dead) { - ast_mutex_unlock(&p->lock); - agent_pvt_destroy(p); - return 0; - } else { - /* Store last disconnect time */ - p->lastdisc = ast_tvadd(ast_tvnow(), ast_samp2tv(p->wrapuptime, 1000)); - } - - /* Release ownership of the agent to other threads (presumably running the login app). */ - p->app_sleep_cond = 1; - p->app_lock_flag = 0; - ast_cond_signal(&p->app_complete_cond); - - ast_mutex_unlock(&p->lock); - return 0; -} - -static int agent_cont_sleep(void *data) -{ - struct agent_pvt *p; - int res; - - p = (struct agent_pvt *) data; - - ast_mutex_lock(&p->lock); - res = p->app_sleep_cond; - if (res && p->lastdisc.tv_sec) { - if (ast_tvdiff_ms(ast_tvnow(), p->lastdisc) > 0) { - res = 0; - } - } - ast_mutex_unlock(&p->lock); - - if (!res) { - ast_debug(5, "agent_cont_sleep() returning %d\n", res); - } - - return res; -} - -static int agent_ack_sleep(struct agent_pvt *p) -{ - int digit; - int to = 1000; - struct ast_frame *f; - struct timeval start = ast_tvnow(); - int ms; - - /* Wait a second and look for something */ - while ((ms = ast_remaining_ms(start, to))) { - ms = ast_waitfor(p->chan, ms); - if (ms < 0) { - return -1; - } - if (ms == 0) { - return 0; - } - f = ast_read(p->chan); - if (!f) { - return -1; - } - if (f->frametype == AST_FRAME_DTMF) { - digit = f->subclass.integer; - } else { - digit = 0; - } - ast_frfree(f); - ast_mutex_lock(&p->lock); - if (!p->app_sleep_cond) { - ast_mutex_unlock(&p->lock); - return 0; - } - if (digit == p->acceptdtmf) { - ast_mutex_unlock(&p->lock); - return 1; - } - if (p->lastdisc.tv_sec) { - if (ast_tvdiff_ms(ast_tvnow(), p->lastdisc) > 0) { - ast_mutex_unlock(&p->lock); - return 0; - } - } - ast_mutex_unlock(&p->lock); - } - return 0; -} - -static struct ast_channel *agent_bridgedchannel(struct ast_channel *chan, struct ast_channel *bridge) -{ - struct agent_pvt *p = ast_channel_tech_pvt(bridge); - struct ast_channel *ret = NULL; - - if (p) { - if (chan == p->chan) - ret = ast_channel_internal_bridged_channel(bridge); - else if (chan == ast_channel_internal_bridged_channel(bridge)) - ret = p->chan; - } - - ast_debug(1, "Asked for bridged channel on '%s'/'%s', returning '%s'\n", ast_channel_name(chan), ast_channel_name(bridge), ret ? ast_channel_name(ret) : ""); - return ret; -} - -/*! \brief Create new agent channel */ -static struct ast_channel *agent_new(struct agent_pvt *p, int state, const char *linkedid, struct ast_callid *callid) -{ - struct ast_channel *tmp; -#if 0 - if (!p->chan) { - ast_log(LOG_WARNING, "No channel? :(\n"); - return NULL; - } -#endif - if (p->pending) - tmp = ast_channel_alloc(0, state, 0, 0, "", p->chan ? ast_channel_exten(p->chan):"", p->chan ? ast_channel_context(p->chan):"", linkedid, 0, "Agent/P%s-%d", p->agent, (int) ast_random() & 0xffff); - else - tmp = ast_channel_alloc(0, state, 0, 0, "", p->chan ? ast_channel_exten(p->chan):"", p->chan ? ast_channel_context(p->chan):"", linkedid, 0, "Agent/%s", p->agent); - if (!tmp) { - ast_log(LOG_WARNING, "Unable to allocate agent channel structure\n"); - return NULL; - } - - if (callid) { - ast_channel_callid_set(tmp, callid); - } - - ast_channel_tech_set(tmp, &agent_tech); - if (p->chan) { - ast_format_cap_copy(ast_channel_nativeformats(tmp), ast_channel_nativeformats(p->chan)); - ast_format_copy(ast_channel_writeformat(tmp), ast_channel_writeformat(p->chan)); - ast_format_copy(ast_channel_rawwriteformat(tmp), ast_channel_writeformat(p->chan)); - ast_format_copy(ast_channel_readformat(tmp), ast_channel_readformat(p->chan)); - ast_format_copy(ast_channel_rawreadformat(tmp), ast_channel_readformat(p->chan)); - ast_channel_language_set(tmp, ast_channel_language(p->chan)); - ast_channel_context_set(tmp, ast_channel_context(p->chan)); - ast_channel_exten_set(tmp, ast_channel_exten(p->chan)); - /* XXX Is this really all we copy form the originating channel?? */ - } else { - ast_format_set(ast_channel_writeformat(tmp), AST_FORMAT_SLINEAR, 0); - ast_format_set(ast_channel_rawwriteformat(tmp), AST_FORMAT_SLINEAR, 0); - ast_format_set(ast_channel_readformat(tmp), AST_FORMAT_SLINEAR, 0); - ast_format_set(ast_channel_rawreadformat(tmp), AST_FORMAT_SLINEAR, 0); - ast_format_cap_add(ast_channel_nativeformats(tmp), ast_channel_writeformat(tmp)); - } - /* Safe, agentlock already held */ - ast_channel_tech_pvt_set(tmp, p); - p->owner = tmp; - ast_channel_priority_set(tmp, 1); - return tmp; -} - - -/*! - * Read configuration data. The file named agents.conf. - * - * \returns Always 0, or so it seems. - */ -static int read_agent_config(int reload) -{ - struct ast_config *cfg; - struct ast_config *ucfg; - struct ast_variable *v; - struct agent_pvt *p; - const char *catname; - const char *hasagent; - int genhasagent; - struct ast_flags config_flags = { reload ? CONFIG_FLAG_FILEUNCHANGED : 0 }; - - group = 0; - autologoff = 0; - wrapuptime = 0; - ackcall = 0; - endcall = 1; - cfg = ast_config_load(config, config_flags); - if (!cfg) { - ast_log(LOG_NOTICE, "No agent configuration found -- agent support disabled\n"); - return 0; - } else if (cfg == CONFIG_STATUS_FILEUNCHANGED) { - return -1; - } else if (cfg == CONFIG_STATUS_FILEINVALID) { - ast_log(LOG_ERROR, "%s contains a parsing error. Aborting\n", config); - return 0; - } - if ((ucfg = ast_config_load("users.conf", config_flags))) { - if (ucfg == CONFIG_STATUS_FILEUNCHANGED) { - ucfg = NULL; - } else if (ucfg == CONFIG_STATUS_FILEINVALID) { - ast_log(LOG_ERROR, "users.conf contains a parsing error. Aborting\n"); - return 0; - } - } - - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - p->dead = 1; - } - strcpy(moh, "default"); - /* set the default recording values */ - recordagentcalls = 0; - strcpy(recordformat, "wav"); - strcpy(recordformatext, "wav"); - urlprefix[0] = '\0'; - savecallsin[0] = '\0'; - - /* Read in the [agents] section */ - v = ast_variable_browse(cfg, "agents"); - while(v) { - /* Create the interface list */ - if (!strcasecmp(v->name, "agent")) { - add_agent(v->value, 0); - } else if (!strcasecmp(v->name, "group")) { - group = ast_get_group(v->value); - } else if (!strcasecmp(v->name, "autologoff")) { - autologoff = atoi(v->value); - if (autologoff < 0) - autologoff = 0; - } else if (!strcasecmp(v->name, "ackcall")) { - if (ast_true(v->value) || !strcasecmp(v->value, "always")) { - ackcall = 1; - } - } else if (!strcasecmp(v->name, "endcall")) { - endcall = ast_true(v->value); - } else if (!strcasecmp(v->name, "acceptdtmf")) { - acceptdtmf = *(v->value); - ast_log(LOG_NOTICE, "Set acceptdtmf to %c\n", acceptdtmf); - } else if (!strcasecmp(v->name, "enddtmf")) { - enddtmf = *(v->value); - } else if (!strcasecmp(v->name, "wrapuptime")) { - wrapuptime = atoi(v->value); - if (wrapuptime < 0) - wrapuptime = 0; - } else if (!strcasecmp(v->name, "maxlogintries") && !ast_strlen_zero(v->value)) { - maxlogintries = atoi(v->value); - if (maxlogintries < 0) - maxlogintries = 0; - } else if (!strcasecmp(v->name, "goodbye") && !ast_strlen_zero(v->value)) { - strcpy(agentgoodbye,v->value); - } else if (!strcasecmp(v->name, "musiconhold")) { - ast_copy_string(moh, v->value, sizeof(moh)); - } else if (!strcasecmp(v->name, "autologoffunavail")) { - if (ast_true(v->value)) - autologoffunavail = 1; - else - autologoffunavail = 0; - } else if (!strcasecmp(v->name, "recordagentcalls")) { - recordagentcalls = ast_true(v->value); - } else if (!strcasecmp(v->name, "recordformat")) { - ast_copy_string(recordformat, v->value, sizeof(recordformat)); - if (!strcasecmp(v->value, "wav49")) - strcpy(recordformatext, "WAV"); - else - ast_copy_string(recordformatext, v->value, sizeof(recordformatext)); - } else if (!strcasecmp(v->name, "urlprefix")) { - ast_copy_string(urlprefix, v->value, sizeof(urlprefix)); - if (urlprefix[strlen(urlprefix) - 1] != '/') - strncat(urlprefix, "/", sizeof(urlprefix) - strlen(urlprefix) - 1); - } else if (!strcasecmp(v->name, "savecallsin")) { - if (v->value[0] == '/') - ast_copy_string(savecallsin, v->value, sizeof(savecallsin)); - else - snprintf(savecallsin, sizeof(savecallsin) - 2, "/%s", v->value); - if (savecallsin[strlen(savecallsin) - 1] != '/') - strncat(savecallsin, "/", sizeof(savecallsin) - strlen(savecallsin) - 1); - } else if (!strcasecmp(v->name, "custom_beep")) { - ast_copy_string(beep, v->value, sizeof(beep)); - } - v = v->next; - } - if (ucfg) { - genhasagent = ast_true(ast_variable_retrieve(ucfg, "general", "hasagent")); - catname = ast_category_browse(ucfg, NULL); - while(catname) { - if (strcasecmp(catname, "general")) { - hasagent = ast_variable_retrieve(ucfg, catname, "hasagent"); - if (ast_true(hasagent) || (!hasagent && genhasagent)) { - char tmp[256]; - const char *fullname = ast_variable_retrieve(ucfg, catname, "fullname"); - const char *secret = ast_variable_retrieve(ucfg, catname, "secret"); - if (!fullname) - fullname = ""; - if (!secret) - secret = ""; - snprintf(tmp, sizeof(tmp), "%s,%s,%s", catname, secret,fullname); - add_agent(tmp, 0); - } - } - catname = ast_category_browse(ucfg, catname); - } - ast_config_destroy(ucfg); - } - AST_LIST_TRAVERSE_SAFE_BEGIN(&agents, p, list) { - if (p->dead) { - AST_LIST_REMOVE_CURRENT(list); - /* Destroy if appropriate */ - if (!p->owner) { - if (!p->chan) { - agent_pvt_destroy(p); - } else { - /* Cause them to hang up */ - ast_softhangup(p->chan, AST_SOFTHANGUP_EXPLICIT); - } - } - } - } - AST_LIST_TRAVERSE_SAFE_END; - AST_LIST_UNLOCK(&agents); - ast_config_destroy(cfg); - return 1; -} - -static int check_availability(struct agent_pvt *newlyavailable, int needlock) -{ - struct ast_channel *chan=NULL, *parent=NULL; - struct agent_pvt *p; - int res; - - ast_debug(1, "Checking availability of '%s'\n", newlyavailable->agent); - if (needlock) - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - if (p == newlyavailable) { - continue; - } - ast_mutex_lock(&p->lock); - if (!p->abouttograb && p->pending && ((p->group && (newlyavailable->group & p->group)) || !strcmp(p->agent, newlyavailable->agent))) { - ast_debug(1, "Call '%s' looks like a winner for agent '%s'\n", ast_channel_name(p->owner), newlyavailable->agent); - /* We found a pending call, time to merge */ - chan = agent_new(newlyavailable, AST_STATE_DOWN, p->owner ? ast_channel_linkedid(p->owner) : NULL, NULL); - parent = p->owner; - p->abouttograb = 1; - ast_mutex_unlock(&p->lock); - break; - } - ast_mutex_unlock(&p->lock); - } - if (needlock) - AST_LIST_UNLOCK(&agents); - if (parent && chan) { - if (newlyavailable->ackcall) { - /* Don't do beep here */ - res = 0; - } else { - ast_debug(3, "Playing beep, lang '%s'\n", ast_channel_language(newlyavailable->chan)); - res = ast_streamfile(newlyavailable->chan, beep, ast_channel_language(newlyavailable->chan)); - ast_debug(3, "Played beep, result '%d'\n", res); - if (!res) { - res = ast_waitstream(newlyavailable->chan, ""); - ast_debug(1, "Waited for stream, result '%d'\n", res); - } - } - if (!res) { - /* Note -- parent may have disappeared */ - if (p->abouttograb) { - newlyavailable->acknowledged = 1; - /* Safe -- agent lock already held */ - ast_setstate(parent, AST_STATE_UP); - ast_setstate(chan, AST_STATE_UP); - ast_channel_context_set(parent, ast_channel_context(chan)); - ast_channel_masquerade(parent, chan); - ast_hangup(chan); - p->abouttograb = 0; - } else { - ast_debug(1, "Sneaky, parent disappeared in the mean time...\n"); - agent_cleanup(newlyavailable); - } - } else { - ast_debug(1, "Ugh... Agent hung up at exactly the wrong time\n"); - agent_cleanup(newlyavailable); - } - } - return 0; -} - -static int check_beep(struct agent_pvt *newlyavailable, int needlock) -{ - struct agent_pvt *p; - int res=0; - - ast_debug(1, "Checking beep availability of '%s'\n", newlyavailable->agent); - if (needlock) - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - if (p == newlyavailable) { - continue; - } - ast_mutex_lock(&p->lock); - if (!p->abouttograb && p->pending && ((p->group && (newlyavailable->group & p->group)) || !strcmp(p->agent, newlyavailable->agent))) { - ast_debug(1, "Call '%s' looks like a would-be winner for agent '%s'\n", ast_channel_name(p->owner), newlyavailable->agent); - ast_mutex_unlock(&p->lock); - break; - } - ast_mutex_unlock(&p->lock); - } - if (needlock) - AST_LIST_UNLOCK(&agents); - if (p) { - ast_mutex_unlock(&newlyavailable->lock); - ast_debug(3, "Playing beep, lang '%s'\n", ast_channel_language(newlyavailable->chan)); - res = ast_streamfile(newlyavailable->chan, beep, ast_channel_language(newlyavailable->chan)); - ast_debug(1, "Played beep, result '%d'\n", res); - if (!res) { - res = ast_waitstream(newlyavailable->chan, ""); - ast_debug(1, "Waited for stream, result '%d'\n", res); - } - ast_mutex_lock(&newlyavailable->lock); - } - return res; -} - -/*! \brief Part of the Asterisk PBX interface */ -static struct ast_channel *agent_request(const char *type, struct ast_format_cap *cap, const struct ast_channel* requestor, const char *data, int *cause) -{ - struct agent_pvt *p; - struct ast_channel *chan = NULL; - const char *s; - ast_group_t groupmatch; - int groupoff; - int waitforagent=0; - int hasagent = 0; - struct timeval now; - struct ast_callid *callid = ast_read_threadstorage_callid(); - - s = data; - if ((s[0] == '@') && (sscanf(s + 1, "%30d", &groupoff) == 1)) { - groupmatch = (1 << groupoff); - } else if ((s[0] == ':') && (sscanf(s + 1, "%30d", &groupoff) == 1)) { - groupmatch = (1 << groupoff); - waitforagent = 1; - } else - groupmatch = 0; - - /* Check actual logged in agents first */ - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - ast_mutex_lock(&p->lock); - if (!p->pending && ((groupmatch && (p->group & groupmatch)) || !strcmp(data, p->agent))) { - if (p->chan) { - hasagent++; - } - now = ast_tvnow(); - if (p->loginstart - && (!p->lastdisc.tv_sec || ast_tvdiff_ms(now, p->lastdisc) > 0)) { - p->lastdisc = ast_tv(0, 0); - /* Agent must be registered, but not have any active call, and not be in a waiting state */ - if (!p->owner && p->chan) { - /* Fixed agent */ - chan = agent_new(p, AST_STATE_DOWN, requestor ? ast_channel_linkedid(requestor) : NULL, callid); - } - if (chan) { - ast_mutex_unlock(&p->lock); - break; - } - } - } - ast_mutex_unlock(&p->lock); - } - - if (!chan && waitforagent) { - /* No agent available -- but we're requesting to wait for one. - Allocate a place holder */ - if (hasagent) { - ast_debug(1, "Creating place holder for '%s'\n", s); - p = add_agent(data, 1); - if (p) { - p->group = groupmatch; - chan = agent_new(p, AST_STATE_DOWN, requestor ? ast_channel_linkedid(requestor) : NULL, callid); - if (!chan) { - AST_LIST_REMOVE(&agents, p, list); - agent_pvt_destroy(p); - } - } - } else { - ast_debug(1, "Not creating place holder for '%s' since nobody logged in\n", s); - } - } - *cause = hasagent ? AST_CAUSE_BUSY : AST_CAUSE_UNREGISTERED; - AST_LIST_UNLOCK(&agents); - - if (callid) { - callid = ast_callid_unref(callid); - } - - if (chan) { - ast_mutex_lock(&p->lock); - if (p->pending) { - ast_mutex_unlock(&p->lock); - return chan; - } - - if (!p->chan) { - ast_debug(1, "Agent disconnected before we could connect the call\n"); - ast_mutex_unlock(&p->lock); - ast_hangup(chan); - *cause = AST_CAUSE_UNREGISTERED; - return NULL; - } - - /* we need to take control of the channel from the login app - * thread */ - p->app_sleep_cond = 0; - p->app_lock_flag = 1; - ast_queue_frame(p->chan, &ast_null_frame); - ast_cond_wait(&p->login_wait_cond, &p->lock); - - if (!p->chan) { - ast_debug(1, "Agent disconnected while we were connecting the call\n"); - ast_mutex_unlock(&p->lock); - ast_hangup(chan); - *cause = AST_CAUSE_UNREGISTERED; - return NULL; - } - - ast_indicate(p->chan, AST_CONTROL_UNHOLD); - ast_mutex_unlock(&p->lock); - } - - return chan; -} - -static force_inline int powerof(unsigned int d) -{ - int x = ffs(d); - - if (x) - return x - 1; - - return 0; -} - -/*! - * Lists agents and their status to the Manager API. - * It is registered on load_module() and it gets called by the manager backend. - * This function locks both the pvt and the channel that owns it for a while, but - * does not keep these locks. - * \param s - * \param m - * \returns - * \sa action_agent_logoff(), load_module(). - */ -static int action_agents(struct mansession *s, const struct message *m) -{ - const char *id = astman_get_header(m,"ActionID"); - char idText[256] = ""; - struct agent_pvt *p; - char *username = NULL; - char *loginChan = NULL; - char *talkingto = NULL; - char *talkingtoChan = NULL; - char *status = NULL; - struct ast_channel *bridge; - - if (!ast_strlen_zero(id)) - snprintf(idText, sizeof(idText) ,"ActionID: %s\r\n", id); - astman_send_ack(s, m, "Agents will follow"); - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - struct ast_channel *owner; - ast_mutex_lock(&p->lock); - owner = agent_lock_owner(p); - - /* Status Values: - AGENT_LOGGEDOFF - Agent isn't logged in - AGENT_IDLE - Agent is logged in, and waiting for call - AGENT_ONCALL - Agent is logged in, and on a call - AGENT_UNKNOWN - Don't know anything about agent. Shouldn't ever get this. */ - - username = S_OR(p->name, "None"); - - /* Set a default status. It 'should' get changed. */ - status = "AGENT_UNKNOWN"; - - if (p->chan) { - loginChan = ast_strdupa(ast_channel_name(p->chan)); - if (owner && ast_channel_internal_bridged_channel(owner)) { - talkingto = S_COR(ast_channel_caller(p->chan)->id.number.valid, - ast_channel_caller(p->chan)->id.number.str, "n/a"); - if ((bridge = ast_bridged_channel(owner))) { - talkingtoChan = ast_strdupa(ast_channel_name(bridge)); - } else { - talkingtoChan = "n/a"; - } - status = "AGENT_ONCALL"; - } else { - talkingto = "n/a"; - talkingtoChan = "n/a"; - status = "AGENT_IDLE"; - } - } else { - loginChan = "n/a"; - talkingto = "n/a"; - talkingtoChan = "n/a"; - status = "AGENT_LOGGEDOFF"; - } - - if (owner) { - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - - astman_append(s, "Event: Agents\r\n" - "Agent: %s\r\n" - "Name: %s\r\n" - "Status: %s\r\n" - "LoggedInChan: %s\r\n" - "LoggedInTime: %d\r\n" - "TalkingTo: %s\r\n" - "TalkingToChan: %s\r\n" - "%s" - "\r\n", - p->agent, username, status, loginChan, (int)p->loginstart, talkingto, talkingtoChan, idText); - ast_mutex_unlock(&p->lock); - } - AST_LIST_UNLOCK(&agents); - astman_append(s, "Event: AgentsComplete\r\n" - "%s" - "\r\n",idText); - return 0; -} - -static int agent_logoff(const char *agent, int soft) -{ - struct agent_pvt *p; - int ret = -1; /* Return -1 if no agent if found */ - - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - if (!strcasecmp(p->agent, agent)) { - ret = 0; - if (p->owner || p->chan) { - if (!soft) { - struct ast_channel *owner; - ast_mutex_lock(&p->lock); - owner = agent_lock_owner(p); - - if (owner) { - ast_softhangup(owner, AST_SOFTHANGUP_EXPLICIT); - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - - while (p->chan && ast_channel_trylock(p->chan)) { - DEADLOCK_AVOIDANCE(&p->lock); - } - if (p->chan) { - ast_softhangup(p->chan, AST_SOFTHANGUP_EXPLICIT); - ast_channel_unlock(p->chan); - } - - ast_mutex_unlock(&p->lock); - } else - p->deferlogoff = 1; - } - break; - } - } - AST_LIST_UNLOCK(&agents); - - return ret; -} - -static char *agent_logoff_cmd(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - int ret; - const char *agent; - - switch (cmd) { - case CLI_INIT: - e->command = "agent logoff"; - e->usage = - "Usage: agent logoff [soft]\n" - " Sets an agent as no longer logged in.\n" - " If 'soft' is specified, do not hangup existing calls.\n"; - return NULL; - case CLI_GENERATE: - return complete_agent_logoff_cmd(a->line, a->word, a->pos, a->n); - } - - if (a->argc < 3 || a->argc > 4) - return CLI_SHOWUSAGE; - if (a->argc == 4 && strcasecmp(a->argv[3], "soft")) - return CLI_SHOWUSAGE; - - agent = a->argv[2] + 6; - ret = agent_logoff(agent, a->argc == 4); - if (ret == 0) - ast_cli(a->fd, "Logging out %s\n", agent); - - return CLI_SUCCESS; -} - -/*! - * Sets an agent as no longer logged in in the Manager API. - * It is registered on load_module() and it gets called by the manager backend. - * \param s - * \param m - * \returns - * \sa action_agents(), load_module(). - */ -static int action_agent_logoff(struct mansession *s, const struct message *m) -{ - const char *agent = astman_get_header(m, "Agent"); - const char *soft_s = astman_get_header(m, "Soft"); /* "true" is don't hangup */ - int soft; - int ret; /* return value of agent_logoff */ - - if (ast_strlen_zero(agent)) { - astman_send_error(s, m, "No agent specified"); - return 0; - } - - soft = ast_true(soft_s) ? 1 : 0; - ret = agent_logoff(agent, soft); - if (ret == 0) - astman_send_ack(s, m, "Agent logged out"); - else - astman_send_error(s, m, "No such agent"); - - return 0; -} - -static char *complete_agent_logoff_cmd(const char *line, const char *word, int pos, int state) -{ - char *ret = NULL; - - if (pos == 2) { - struct agent_pvt *p; - char name[AST_MAX_AGENT]; - int which = 0, len = strlen(word); - - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - snprintf(name, sizeof(name), "Agent/%s", p->agent); - if (!strncasecmp(word, name, len) && p->loginstart && ++which > state) { - ret = ast_strdup(name); - break; - } - } - AST_LIST_UNLOCK(&agents); - } else if (pos == 3 && state == 0) - return ast_strdup("soft"); - - return ret; -} - -/*! - * Show agents in cli. - */ -static char *agents_show(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct agent_pvt *p; - char username[AST_MAX_BUF]; - char location[AST_MAX_BUF] = ""; - char talkingto[AST_MAX_BUF] = ""; - char music[AST_MAX_BUF]; - int count_agents = 0; /*!< Number of agents configured */ - int online_agents = 0; /*!< Number of online agents */ - int offline_agents = 0; /*!< Number of offline agents */ - - switch (cmd) { - case CLI_INIT: - e->command = "agent show"; - e->usage = - "Usage: agent show\n" - " Provides summary information on agents.\n"; - return NULL; - case CLI_GENERATE: - return NULL; - } - - if (a->argc != 2) - return CLI_SHOWUSAGE; - - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - struct ast_channel *owner; - ast_mutex_lock(&p->lock); - owner = agent_lock_owner(p); - if (p->pending) { - if (p->group) - ast_cli(a->fd, "-- Pending call to group %d\n", powerof(p->group)); - else - ast_cli(a->fd, "-- Pending call to agent %s\n", p->agent); - } else { - if (!ast_strlen_zero(p->name)) - snprintf(username, sizeof(username), "(%s) ", p->name); - else - username[0] = '\0'; - if (p->chan) { - snprintf(location, sizeof(location), "logged in on %s", ast_channel_name(p->chan)); - if (owner && ast_bridged_channel(owner)) { - snprintf(talkingto, sizeof(talkingto), " talking to %s", ast_channel_name(ast_bridged_channel(p->owner))); - } else { - strcpy(talkingto, " is idle"); - } - online_agents++; - } else { - strcpy(location, "not logged in"); - talkingto[0] = '\0'; - offline_agents++; - } - if (!ast_strlen_zero(p->moh)) - snprintf(music, sizeof(music), " (musiconhold is '%s')", p->moh); - ast_cli(a->fd, "%-12.12s %s%s%s%s\n", p->agent, - username, location, talkingto, music); - count_agents++; - } - - if (owner) { - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - ast_mutex_unlock(&p->lock); - } - AST_LIST_UNLOCK(&agents); - if ( !count_agents ) - ast_cli(a->fd, "No Agents are configured in %s\n",config); - else - ast_cli(a->fd, "%d agents configured [%d online , %d offline]\n",count_agents, online_agents, offline_agents); - ast_cli(a->fd, "\n"); - - return CLI_SUCCESS; -} - - -static char *agents_show_online(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a) -{ - struct agent_pvt *p; - char username[AST_MAX_BUF]; - char location[AST_MAX_BUF] = ""; - char talkingto[AST_MAX_BUF] = ""; - char music[AST_MAX_BUF]; - int count_agents = 0; /* Number of agents configured */ - int online_agents = 0; /* Number of online agents */ - int agent_status = 0; /* 0 means offline, 1 means online */ - - switch (cmd) { - case CLI_INIT: - e->command = "agent show online"; - e->usage = - "Usage: agent show online\n" - " Provides a list of all online agents.\n"; - return NULL; - case CLI_GENERATE: - return NULL; - } - - if (a->argc != 3) - return CLI_SHOWUSAGE; - - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - struct ast_channel *owner; - - agent_status = 0; /* reset it to offline */ - ast_mutex_lock(&p->lock); - owner = agent_lock_owner(p); - - if (!ast_strlen_zero(p->name)) - snprintf(username, sizeof(username), "(%s) ", p->name); - else - username[0] = '\0'; - if (p->chan) { - snprintf(location, sizeof(location), "logged in on %s", ast_channel_name(p->chan)); - if (p->owner && ast_bridged_channel(p->owner)) { - snprintf(talkingto, sizeof(talkingto), " talking to %s", ast_channel_name(ast_bridged_channel(p->owner))); - } else { - strcpy(talkingto, " is idle"); - } - agent_status = 1; - online_agents++; - } - - if (owner) { - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - - if (!ast_strlen_zero(p->moh)) - snprintf(music, sizeof(music), " (musiconhold is '%s')", p->moh); - if (agent_status) - ast_cli(a->fd, "%-12.12s %s%s%s%s\n", p->agent, username, location, talkingto, music); - count_agents++; - ast_mutex_unlock(&p->lock); - } - AST_LIST_UNLOCK(&agents); - if (!count_agents) - ast_cli(a->fd, "No Agents are configured in %s\n", config); - else - ast_cli(a->fd, "%d agents online\n", online_agents); - ast_cli(a->fd, "\n"); - return CLI_SUCCESS; -} - -static const char agent_logoff_usage[] = -"Usage: agent logoff [soft]\n" -" Sets an agent as no longer logged in.\n" -" If 'soft' is specified, do not hangup existing calls.\n"; - -static struct ast_cli_entry cli_agents[] = { - AST_CLI_DEFINE(agents_show, "Show status of agents"), - AST_CLI_DEFINE(agents_show_online, "Show all online agents"), - AST_CLI_DEFINE(agent_logoff_cmd, "Sets an agent offline"), -}; - -/*! - * Called by the AgentLogin application (from the dial plan). - * - * \brief Log in agent application. - * - * \param chan - * \param data - * \returns - * \sa agentmonitoroutgoing_exec(), load_module(). - */ -static int login_exec(struct ast_channel *chan, const char *data) -{ - int res=0; - int tries = 0; - int max_login_tries = maxlogintries; - struct agent_pvt *p; - char user[AST_MAX_AGENT]; - char pass[AST_MAX_AGENT]; - char xpass[AST_MAX_AGENT]; - char *errmsg; - char *parse; - AST_DECLARE_APP_ARGS(args, - AST_APP_ARG(agent_id); - AST_APP_ARG(options); - AST_APP_ARG(extension); - ); - const char *tmpoptions = NULL; - int play_announcement = 1; - char agent_goodbye[AST_MAX_FILENAME_LEN]; - - user[0] = '\0'; - xpass[0] = '\0'; - - parse = ast_strdupa(data); - - AST_STANDARD_APP_ARGS(args, parse); - - ast_copy_string(agent_goodbye, agentgoodbye, sizeof(agent_goodbye)); - - ast_channel_lock(chan); - /* Set Channel Specific Login Overrides */ - if (!ast_strlen_zero(pbx_builtin_getvar_helper(chan, "AGENTLMAXLOGINTRIES"))) { - max_login_tries = atoi(pbx_builtin_getvar_helper(chan, "AGENTMAXLOGINTRIES")); - if (max_login_tries < 0) - max_login_tries = 0; - tmpoptions=pbx_builtin_getvar_helper(chan, "AGENTMAXLOGINTRIES"); - ast_verb(3, "Saw variable AGENTMAXLOGINTRIES=%s, setting max_login_tries to: %d on Channel '%s'.\n",tmpoptions,max_login_tries,ast_channel_name(chan)); - } - if (!ast_strlen_zero(pbx_builtin_getvar_helper(chan, "AGENTGOODBYE"))) { - strcpy(agent_goodbye, pbx_builtin_getvar_helper(chan, "AGENTGOODBYE")); - tmpoptions=pbx_builtin_getvar_helper(chan, "AGENTGOODBYE"); - ast_verb(3, "Saw variable AGENTGOODBYE=%s, setting agent_goodbye to: %s on Channel '%s'.\n",tmpoptions,agent_goodbye,ast_channel_name(chan)); - } - ast_channel_unlock(chan); - /* End Channel Specific Login Overrides */ - - if (!ast_strlen_zero(args.options)) { - if (strchr(args.options, 's')) { - play_announcement = 0; - } - } - - if (ast_channel_state(chan) != AST_STATE_UP) - res = ast_answer(chan); - if (!res) { - if (!ast_strlen_zero(args.agent_id)) - ast_copy_string(user, args.agent_id, AST_MAX_AGENT); - else - res = ast_app_getdata(chan, "agent-user", user, sizeof(user) - 1, 0); - } - while (!res && (max_login_tries==0 || tries < max_login_tries)) { - tries++; - /* Check for password */ - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - if (!strcmp(p->agent, user) && !p->pending) - ast_copy_string(xpass, p->password, sizeof(xpass)); - } - AST_LIST_UNLOCK(&agents); - if (!res) { - if (!ast_strlen_zero(xpass)) - res = ast_app_getdata(chan, "agent-pass", pass, sizeof(pass) - 1, 0); - else - pass[0] = '\0'; - } - errmsg = "agent-incorrect"; - -#if 0 - ast_log(LOG_NOTICE, "user: %s, pass: %s\n", user, pass); -#endif - - /* Check again for accuracy */ - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - int unlock_channel = 1; - - ast_channel_lock(chan); - ast_mutex_lock(&p->lock); - if (!strcmp(p->agent, user) && - !strcmp(p->password, pass) && !p->pending) { - - /* Set Channel Specific Agent Overrides */ - if (!ast_strlen_zero(pbx_builtin_getvar_helper(chan, "AGENTACKCALL"))) { - if (ast_true(pbx_builtin_getvar_helper(chan, "AGENTACKCALL"))) { - p->ackcall = 1; - } else { - p->ackcall = 0; - } - tmpoptions=pbx_builtin_getvar_helper(chan, "AGENTACKCALL"); - ast_verb(3, "Saw variable AGENTACKCALL=%s, setting ackcall to: %d for Agent '%s'.\n", tmpoptions, p->ackcall, p->agent); - ast_set_flag(p, AGENT_FLAG_ACKCALL); - } else { - p->ackcall = ackcall; - } - if (!ast_strlen_zero(pbx_builtin_getvar_helper(chan, "AGENTAUTOLOGOFF"))) { - p->autologoff = atoi(pbx_builtin_getvar_helper(chan, "AGENTAUTOLOGOFF")); - if (p->autologoff < 0) - p->autologoff = 0; - tmpoptions=pbx_builtin_getvar_helper(chan, "AGENTAUTOLOGOFF"); - ast_verb(3, "Saw variable AGENTAUTOLOGOFF=%s, setting autologff to: %d for Agent '%s'.\n", tmpoptions, p->autologoff, p->agent); - ast_set_flag(p, AGENT_FLAG_AUTOLOGOFF); - } else { - p->autologoff = autologoff; - } - if (!ast_strlen_zero(pbx_builtin_getvar_helper(chan, "AGENTWRAPUPTIME"))) { - p->wrapuptime = atoi(pbx_builtin_getvar_helper(chan, "AGENTWRAPUPTIME")); - if (p->wrapuptime < 0) - p->wrapuptime = 0; - tmpoptions=pbx_builtin_getvar_helper(chan, "AGENTWRAPUPTIME"); - ast_verb(3, "Saw variable AGENTWRAPUPTIME=%s, setting wrapuptime to: %d for Agent '%s'.\n", tmpoptions, p->wrapuptime, p->agent); - ast_set_flag(p, AGENT_FLAG_WRAPUPTIME); - } else { - p->wrapuptime = wrapuptime; - } - tmpoptions = pbx_builtin_getvar_helper(chan, "AGENTACCEPTDTMF"); - if (!ast_strlen_zero(tmpoptions)) { - p->acceptdtmf = *tmpoptions; - ast_verb(3, "Saw variable AGENTACCEPTDTMF=%s, setting acceptdtmf to: %c for Agent '%s'.\n", tmpoptions, p->acceptdtmf, p->agent); - ast_set_flag(p, AGENT_FLAG_ACCEPTDTMF); - } - tmpoptions = pbx_builtin_getvar_helper(chan, "AGENTENDDTMF"); - if (!ast_strlen_zero(tmpoptions)) { - p->enddtmf = *tmpoptions; - ast_verb(3, "Saw variable AGENTENDDTMF=%s, setting enddtmf to: %c for Agent '%s'.\n", tmpoptions, p->enddtmf, p->agent); - ast_set_flag(p, AGENT_FLAG_ENDDTMF); - } - ast_channel_unlock(chan); - unlock_channel = 0; - /* End Channel Specific Agent Overrides */ - - if (!p->chan) { - /* Ensure nobody else can be this agent until we're done. */ - p->chan = chan; - - p->acknowledged = 0; - - if (!res) { - struct ast_format tmpfmt; - res = ast_set_read_format_from_cap(chan, ast_channel_nativeformats(chan)); - if (res) { - ast_log(LOG_WARNING, "Unable to set read format to %s\n", ast_getformatname(&tmpfmt)); - } - } - if (!res) { - struct ast_format tmpfmt; - res = ast_set_write_format_from_cap(chan, ast_channel_nativeformats(chan)); - if (res) { - ast_log(LOG_WARNING, "Unable to set write format to %s\n", ast_getformatname(&tmpfmt)); - } - } - if (!res && play_announcement == 1) { - ast_mutex_unlock(&p->lock); - AST_LIST_UNLOCK(&agents); - res = ast_streamfile(chan, "agent-loginok", ast_channel_language(chan)); - if (!res) { - ast_waitstream(chan, ""); - } - AST_LIST_LOCK(&agents); - ast_mutex_lock(&p->lock); - } - - if (!res) { - long logintime; - char agent[AST_MAX_AGENT]; - - snprintf(agent, sizeof(agent), "Agent/%s", p->agent); - - /* Login this channel and wait for it to go away */ - ast_indicate_data(chan, AST_CONTROL_HOLD, - S_OR(p->moh, NULL), - !ast_strlen_zero(p->moh) ? strlen(p->moh) + 1 : 0); - - /* Must be done after starting HOLD. */ - p->lastdisc = ast_tvnow(); - time(&p->loginstart); - - /*** DOCUMENTATION - - Raised when an Agent has logged in. - - - The name of the agent. - - - - AgentLogin - Agentlogoff - - - ***/ - manager_event(EVENT_FLAG_AGENT, "Agentlogin", - "Agent: %s\r\n" - "Channel: %s\r\n" - "Uniqueid: %s\r\n", - p->agent, ast_channel_name(chan), ast_channel_uniqueid(chan)); - ast_queue_log("NONE", ast_channel_uniqueid(chan), agent, "AGENTLOGIN", "%s", ast_channel_name(chan)); - ast_verb(2, "Agent '%s' logged in (format %s/%s)\n", p->agent, - ast_getformatname(ast_channel_readformat(chan)), ast_getformatname(ast_channel_writeformat(chan))); - - ast_mutex_unlock(&p->lock); - AST_LIST_UNLOCK(&agents); - - while (res >= 0) { - ast_mutex_lock(&p->lock); - if (p->deferlogoff) { - p->deferlogoff = 0; - ast_softhangup(chan, AST_SOFTHANGUP_EXPLICIT); - ast_mutex_unlock(&p->lock); - break; - } - ast_mutex_unlock(&p->lock); - - AST_LIST_LOCK(&agents); - ast_mutex_lock(&p->lock); - if (p->lastdisc.tv_sec) { - if (ast_tvdiff_ms(ast_tvnow(), p->lastdisc) > 0) { - ast_debug(1, "Wrapup time for %s expired!\n", agent); - p->lastdisc = ast_tv(0, 0); - ast_devstate_changed(AST_DEVICE_UNKNOWN, AST_DEVSTATE_CACHABLE, "%s", agent); - if (p->ackcall) { - check_beep(p, 0); - } else { - check_availability(p, 0); - } - } - } - ast_mutex_unlock(&p->lock); - AST_LIST_UNLOCK(&agents); - - /* Synchronize channel ownership between call to agent and itself. */ - ast_mutex_lock(&p->lock); - if (p->app_lock_flag) { - ast_cond_signal(&p->login_wait_cond); - ast_cond_wait(&p->app_complete_cond, &p->lock); - if (ast_check_hangup(chan)) { - /* Agent hungup */ - ast_mutex_unlock(&p->lock); - break; - } - } - ast_mutex_unlock(&p->lock); - - if (p->ackcall) { - res = agent_ack_sleep(p); - if (res == 1) { - AST_LIST_LOCK(&agents); - ast_mutex_lock(&p->lock); - check_availability(p, 0); - ast_mutex_unlock(&p->lock); - AST_LIST_UNLOCK(&agents); - } - } else { - res = ast_safe_sleep_conditional( chan, 1000, agent_cont_sleep, p ); - } - } - ast_mutex_lock(&p->lock); - - /* Logoff this channel */ - p->chan = NULL; - logintime = time(NULL) - p->loginstart; - p->loginstart = 0; - - /* Synchronize channel ownership between call to agent and itself. */ - if (p->app_lock_flag) { - ast_cond_signal(&p->login_wait_cond); - ast_cond_wait(&p->app_complete_cond, &p->lock); - } - - if (p->owner) { - ast_log(LOG_WARNING, "Huh? We broke out when there was still an owner?\n"); - } - - p->acknowledged = 0; - ast_mutex_unlock(&p->lock); - - ast_devstate_changed(AST_DEVICE_UNKNOWN, AST_DEVSTATE_CACHABLE, "%s", agent); - /*** DOCUMENTATION - - Raised when an Agent has logged off. - - - - - Agentlogin - - - ***/ - manager_event(EVENT_FLAG_AGENT, "Agentlogoff", - "Agent: %s\r\n" - "Logintime: %ld\r\n" - "Uniqueid: %s\r\n", - p->agent, logintime, ast_channel_uniqueid(chan)); - ast_queue_log("NONE", ast_channel_uniqueid(chan), agent, "AGENTLOGOFF", "%s|%ld", ast_channel_name(chan), logintime); - ast_verb(2, "Agent '%s' logged out\n", p->agent); - - /* If there is no owner, go ahead and kill it now */ - if (p->dead && !p->owner) { - agent_pvt_destroy(p); - } - AST_LIST_LOCK(&agents); - } else { - /* Agent hung up before could be logged in. */ - p->chan = NULL; - - ast_mutex_unlock(&p->lock); - } - res = -1; - } else { - ast_mutex_unlock(&p->lock); - errmsg = "agent-alreadyon"; - } - break; - } - ast_mutex_unlock(&p->lock); - if (unlock_channel) { - ast_channel_unlock(chan); - } - } - AST_LIST_UNLOCK(&agents); - - if (!res && (max_login_tries==0 || tries < max_login_tries)) - res = ast_app_getdata(chan, errmsg, user, sizeof(user) - 1, 0); - } - - if (!res) - res = ast_safe_sleep(chan, 500); - - return -1; -} - -/*! - * \brief Called by the AgentMonitorOutgoing application (from the dial plan). - * - * \param chan - * \param data - * \returns - * \sa login_exec(), load_module(). - */ -static int agentmonitoroutgoing_exec(struct ast_channel *chan, const char *data) -{ - int exitifnoagentid = 0; - int nowarnings = 0; - int res = 0; - char agent[AST_MAX_AGENT]; - - if (data) { - if (strchr(data, 'd')) { - exitifnoagentid = 1; - } - if (strchr(data, 'n')) { - nowarnings = 1; - } - } - if (ast_channel_caller(chan)->id.number.valid - && !ast_strlen_zero(ast_channel_caller(chan)->id.number.str)) { - const char *tmp; - char agentvar[AST_MAX_BUF]; - snprintf(agentvar, sizeof(agentvar), "%s_%s", GETAGENTBYCALLERID, - ast_channel_caller(chan)->id.number.str); - if ((tmp = pbx_builtin_getvar_helper(NULL, agentvar))) { - struct agent_pvt *p; - ast_copy_string(agent, tmp, sizeof(agent)); - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - if (!strcasecmp(p->agent, tmp)) { - __agent_start_monitoring(chan, p, 1); - break; - } - } - AST_LIST_UNLOCK(&agents); - - } else { - res = -1; - if (!nowarnings) - ast_log(LOG_WARNING, "Couldn't find the global variable %s, so I can't figure out which agent (if it's an agent) is placing outgoing call.\n", agentvar); - } - } else { - res = -1; - if (!nowarnings) - ast_log(LOG_WARNING, "There is no callerid on that call, so I can't figure out which agent (if it's an agent) is placing outgoing call.\n"); - } - if (res) { - if (exitifnoagentid) - return res; - } - return 0; -} - -/*! \brief Part of PBX channel interface */ -static int agent_devicestate(const char *data) -{ - struct agent_pvt *p; - const char *device = data; - int res = AST_DEVICE_INVALID; - - if (device[0] == '@' || device[0] == ':') { - /* Device state of groups not supported. */ - return AST_DEVICE_INVALID; - } - - /* Want device state of a specific agent. */ - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - ast_mutex_lock(&p->lock); - if (!p->pending && !strcmp(device, p->agent)) { - if (p->owner) { - res = AST_DEVICE_BUSY; - } else if (p->chan) { - if (p->lastdisc.tv_sec || p->deferlogoff) { - /* Agent is in wrapup time so unavailable for another call. */ - res = AST_DEVICE_INUSE; - } else { - res = AST_DEVICE_NOT_INUSE; - } - } else { - res = AST_DEVICE_UNAVAILABLE; - } - ast_mutex_unlock(&p->lock); - break; - } - ast_mutex_unlock(&p->lock); - } - AST_LIST_UNLOCK(&agents); - return res; -} - -/*! - * \note This function expects the agent list to be locked - */ -static struct agent_pvt *find_agent(char *agentid) -{ - struct agent_pvt *cur; - - AST_LIST_TRAVERSE(&agents, cur, list) { - if (!strcmp(cur->agent, agentid)) - break; - } - - return cur; -} - -static int function_agent(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) -{ - char *parse; - AST_DECLARE_APP_ARGS(args, - AST_APP_ARG(agentid); - AST_APP_ARG(item); - ); - char *tmp; - struct agent_pvt *agent; - - buf[0] = '\0'; - - if (ast_strlen_zero(data)) { - ast_log(LOG_WARNING, "The AGENT function requires an argument - agentid!\n"); - return -1; - } - - parse = ast_strdupa(data); - - AST_NONSTANDARD_APP_ARGS(args, parse, ':'); - if (!args.item) - args.item = "status"; - - AST_LIST_LOCK(&agents); - - if (!(agent = find_agent(args.agentid))) { - AST_LIST_UNLOCK(&agents); - ast_log(LOG_WARNING, "Agent '%s' not found!\n", args.agentid); - return -1; - } - - if (!strcasecmp(args.item, "status")) { - char *status = "LOGGEDOUT"; - if (agent->chan) { - status = "LOGGEDIN"; - } - ast_copy_string(buf, status, len); - } else if (!strcasecmp(args.item, "password")) - ast_copy_string(buf, agent->password, len); - else if (!strcasecmp(args.item, "name")) - ast_copy_string(buf, agent->name, len); - else if (!strcasecmp(args.item, "mohclass")) - ast_copy_string(buf, agent->moh, len); - else if (!strcasecmp(args.item, "channel")) { - if (agent->chan) { - ast_channel_lock(agent->chan); - ast_copy_string(buf, ast_channel_name(agent->chan), len); - ast_channel_unlock(agent->chan); - tmp = strrchr(buf, '-'); - if (tmp) - *tmp = '\0'; - } - } else if (!strcasecmp(args.item, "fullchannel")) { - if (agent->chan) { - ast_channel_lock(agent->chan); - ast_copy_string(buf, ast_channel_name(agent->chan), len); - ast_channel_unlock(agent->chan); - } - } else if (!strcasecmp(args.item, "exten")) { - buf[0] = '\0'; - } - - AST_LIST_UNLOCK(&agents); - - return 0; -} - -static struct ast_custom_function agent_function = { - .name = "AGENT", - .read = function_agent, -}; - -/*! - * \internal - * \brief Callback used to generate the agents tree. - * \param[in] search The search pattern tree. - * \retval NULL on error. - * \retval non-NULL The generated tree. - */ -static int agents_data_provider_get(const struct ast_data_search *search, - struct ast_data *data_root) -{ - struct agent_pvt *p; - struct ast_data *data_agent, *data_channel, *data_talkingto; - - AST_LIST_LOCK(&agents); - AST_LIST_TRAVERSE(&agents, p, list) { - struct ast_channel *owner; - - data_agent = ast_data_add_node(data_root, "agent"); - if (!data_agent) { - continue; - } - - ast_mutex_lock(&p->lock); - owner = agent_lock_owner(p); - - if (!(p->pending)) { - ast_data_add_str(data_agent, "id", p->agent); - ast_data_add_structure(agent_pvt, data_agent, p); - - ast_data_add_bool(data_agent, "logged", p->chan ? 1 : 0); - if (p->chan) { - data_channel = ast_data_add_node(data_agent, "loggedon"); - if (!data_channel) { - ast_mutex_unlock(&p->lock); - ast_data_remove_node(data_root, data_agent); - if (owner) { - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - continue; - } - ast_channel_data_add_structure(data_channel, p->chan, 0); - if (owner && ast_bridged_channel(owner)) { - data_talkingto = ast_data_add_node(data_agent, "talkingto"); - if (!data_talkingto) { - ast_mutex_unlock(&p->lock); - ast_data_remove_node(data_root, data_agent); - if (owner) { - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - continue; - } - ast_channel_data_add_structure(data_talkingto, ast_bridged_channel(owner), 0); - } - } else { - ast_data_add_node(data_agent, "talkingto"); - ast_data_add_node(data_agent, "loggedon"); - } - ast_data_add_str(data_agent, "musiconhold", p->moh); - } - - if (owner) { - ast_channel_unlock(owner); - owner = ast_channel_unref(owner); - } - - ast_mutex_unlock(&p->lock); - - /* if this agent doesn't match remove the added agent. */ - if (!ast_data_search_match(search, data_agent)) { - ast_data_remove_node(data_root, data_agent); - } - } - AST_LIST_UNLOCK(&agents); - - return 0; -} - -static const struct ast_data_handler agents_data_provider = { - .version = AST_DATA_HANDLER_VERSION, - .get = agents_data_provider_get -}; - -static const struct ast_data_entry agents_data_providers[] = { - AST_DATA_ENTRY("asterisk/channel/agent/list", &agents_data_provider), -}; - -/*! - * \brief Initialize the Agents module. - * This function is being called by Asterisk when loading the module. - * Among other things it registers applications, cli commands and reads the cofiguration file. - * - * \returns int Always 0. - */ -static int load_module(void) -{ - if (!(agent_tech.capabilities = ast_format_cap_alloc())) { - ast_log(LOG_ERROR, "ast_format_cap_alloc_nolock fail.\n"); - return AST_MODULE_LOAD_FAILURE; - } - ast_format_cap_add_all(agent_tech.capabilities); - /* Make sure we can register our agent channel type */ - if (ast_channel_register(&agent_tech)) { - agent_tech.capabilities = ast_format_cap_destroy(agent_tech.capabilities); - ast_log(LOG_ERROR, "Unable to register channel class 'Agent'\n"); - return AST_MODULE_LOAD_FAILURE; - } - /* Read in the config */ - if (!read_agent_config(0)) { - agent_tech.capabilities = ast_format_cap_destroy(agent_tech.capabilities); - return AST_MODULE_LOAD_DECLINE; - } - /* Dialplan applications */ - ast_register_application_xml(app, login_exec); - ast_register_application_xml(app3, agentmonitoroutgoing_exec); - - /* data tree */ - ast_data_register_multiple(agents_data_providers, ARRAY_LEN(agents_data_providers)); - - /* Manager commands */ - ast_manager_register_xml("Agents", EVENT_FLAG_AGENT, action_agents); - ast_manager_register_xml("AgentLogoff", EVENT_FLAG_AGENT, action_agent_logoff); - - /* CLI Commands */ - ast_cli_register_multiple(cli_agents, ARRAY_LEN(cli_agents)); - - /* Dialplan Functions */ - ast_custom_function_register(&agent_function); - - return AST_MODULE_LOAD_SUCCESS; -} - -static int reload(void) -{ - return read_agent_config(1); -} - -static int unload_module(void) -{ - struct agent_pvt *p; - /* First, take us out of the channel loop */ - ast_channel_unregister(&agent_tech); - /* Unregister dialplan functions */ - ast_custom_function_unregister(&agent_function); - /* Unregister CLI commands */ - ast_cli_unregister_multiple(cli_agents, ARRAY_LEN(cli_agents)); - /* Unregister dialplan applications */ - ast_unregister_application(app); - ast_unregister_application(app3); - /* Unregister manager command */ - ast_manager_unregister("Agents"); - ast_manager_unregister("AgentLogoff"); - /* Unregister the data tree */ - ast_data_unregister(NULL); - /* Unregister channel */ - AST_LIST_LOCK(&agents); - /* Hangup all interfaces if they have an owner */ - while ((p = AST_LIST_REMOVE_HEAD(&agents, list))) { - if (p->owner) - ast_softhangup(p->owner, AST_SOFTHANGUP_APPUNLOAD); - ast_free(p); - } - AST_LIST_UNLOCK(&agents); - - agent_tech.capabilities = ast_format_cap_destroy(agent_tech.capabilities); - return 0; -} - -AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_LOAD_ORDER, "Agent Proxy Channel", - .load = load_module, - .unload = unload_module, - .reload = reload, - .load_pri = AST_MODPRI_CHANNEL_DRIVER, - .nonoptreq = "res_monitor", - ); diff --git a/configs/agents.conf.sample b/configs/agents.conf.sample index 29e6e07ead..0cf0c4c90b 100644 --- a/configs/agents.conf.sample +++ b/configs/agents.conf.sample @@ -1,102 +1,70 @@ ; -; Agent configuration +; Agent pool configuration ; [general] +; The general section of this config is not currently used, but reserved +; for future use. -[agents] -; -; Define maxlogintries to allow agent to try max logins before -; failed. -; default to 3 -; -;maxlogintries=5 -; -; -; Define autologoff times if appropriate. This is how long -; the phone has to ring with no answer before the agent is -; automatically logged off (in seconds) -; -;autologoff=15 -; -; Define autologoffunavail to have agents automatically logged -; out when the extension that they are at returns a CHANUNAVAIL -; status when a call is attempted to be sent there. +;[agent-id] +; Define ackcall to require the agent to give a DTMF acknowledgement +; when the agent receives a call. +; The channel variable AGENTACKCALL overrides on agent login. ; Default is "no". -; -;autologoffunavail=yes -; -; Define ackcall to require a DTMF acknowledgement when -; a logged-in agent receives a call. Default is "no". -; Use the acceptdtmf option to configure what DTMF key -; press should be used to acknowledge the call. The -; default is '#'. -; ;ackcall=no -;acceptdtmf=# -; -; Define endcall to allow an agent to hangup a call with a -; DTMF keypress. Default is "yes". Use the enddtmf option to -; configure which DTMF key will end a call. The default is -; '*'. -; -;endcall=yes -;enddtmf=* ; -; Define wrapuptime. This is the minimum amount of time when -; after disconnecting before the caller can receive a new call -; note this is in milliseconds. +; Set what DTMF key sequence the agent should use to acknowledge a call. +; The channel variable AGENTACCEPTDTMF overrides on agent login. +; This option is ignored unless ackcall is enabled. +; Default is "#". +;acceptdtmf=## +; +; 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. +; The channel variable AGENTAUTOLOGOFF overrides on agent login. +; This option is ignored unless ackcall is enabled. +; Default is 0. +;autologoff=15 ; +; Set the minimum amount of time after disconnecting a call before +; the agent can receive a new call in milliseconds. +; The channel variable AGENTWRAPUPTIME overrides on agent login. +; Default is 0. ;wrapuptime=5000 ; -; Define the default musiconhold for agents -; musiconhold => music_class -; -;musiconhold => default -; -; Define the default good bye sound file for agents -; default to vm-goodbye +; Set the musiconhold class for the agent. +; Default is "default". +;musiconhold=default ; -;goodbye => goodbye_file -; -; Define updatecdr. This is whether or not to change the source -; channel in the CDR record for this call to agent/agent_id so -; that we know which agent generates the call -; -;updatecdr=no -; -; Group memberships for agents (may change in mid-file) -; -;group=3 -;group=1,2 -;group= -; -; -------------------------------------------------- -; This section is devoted to recording agent's calls -; The keywords are global to the chan_agent channel driver -; -; Enable recording calls addressed to agents. It's turned off by default. +; Enable recording calls the agent takes automatically by invoking the +; DTMF automixmon feature when the agent connects to a caller. +; See features.conf.sample for information about the automixmon feature. +; Default is "no". ;recordagentcalls=yes ; -; The format to be used to record the calls: wav, gsm, wav49. -; By default its "wav". -;recordformat=gsm -; -; The text to be added to the name of the recording. Allows forming a url link. -;urlprefix=http://localhost/calls/ -; -; The optional directory to save the conversations in. The default is -; /var/spool/asterisk/monitor -;savecallsin=/var/calls -; -; An optional custom beep sound file to play to always-connected agents. +; The sound file played to alert the agent when a call is present. +; Default is "beep". ;custom_beep=beep ; +; A friendly name for the agent used in log messages. +; Default is "". +;fullname=Mark Spencer +; ; -------------------------------------------------- ; -; This section contains the agent definitions, in the form: +; This section contains example agent definitions: +; +; Define a template called my-agents: +;[my-agents](!) +;autologoff=15 +;ackcall=yes +;acceptdtmf=## ; -; agent => agentid,agentpassword,name +; Define agent 1001 using the my-agents template: +;[1001](my-agents) +;fullname=Mark Spencer ; -;agent => 1001,4321,Mark Spencer -;agent => 1002,4321,Will Meadows +; Define agent 1002 using the my-agents template: +;[1002](my-agents) +;fullname=Will Meadows diff --git a/configs/queues.conf.sample b/configs/queues.conf.sample index 9f3ba97def..994ad31daf 100644 --- a/configs/queues.conf.sample +++ b/configs/queues.conf.sample @@ -543,18 +543,7 @@ monitor-type = MixMonitor ;member => DAHDI/1 ;member => DAHDI/2,10 ;member => DAHDI/3,10,Bob Johnson -;member => Agent/1001 -;member => Agent/1002 +;member => Local/1001@agents,0,May Flowers,Agent:1001 +;member => Local/1002@agents,0,John Doe,Agent:1002 ;member => Local/1000@default,0,John Smith,SIP/1000 ;member => Local/2000@default,0,Lorem Ipsum,SIP/2000,no - -; -; Note that using agent groups is probably not what you want. Strategies do -; not propagate down to the Agent system so if you want round robin, least -; recent, etc, you should list all the agents in this file individually and not -; use agent groups. -; -;member => Agent/@1 ; Any agent in group 1 -;member => Agent/:1,1 ; Any agent in group 1, wait for first - ; available, but consider with penalty - diff --git a/include/asterisk/bridging.h b/include/asterisk/bridging.h index 08d0023e5b..d770937a62 100644 --- a/include/asterisk/bridging.h +++ b/include/asterisk/bridging.h @@ -1651,7 +1651,7 @@ void ast_after_bridge_goto_read(struct ast_channel *chan, char *buffer, size_t b /*! Reason the the after bridge callback will not be called. */ enum ast_after_bridge_cb_reason { - /*! The datastore is being destroyed. Likely due to hangup. */ + /*! The datastore is being destroyed. Likely due to hangup. (Enum value must be zero.) */ AST_AFTER_BRIDGE_CB_REASON_DESTROY, /*! Something else replaced the callback with another. */ AST_AFTER_BRIDGE_CB_REASON_REPLACED, @@ -1670,6 +1670,9 @@ enum ast_after_bridge_cb_reason { * \param reason Reason callback is failing. * \param data Extra data what setup the callback wanted to pass. * + * \note Called when the channel leaves the bridging system or + * is destroyed. + * * \return Nothing */ typedef void (*ast_after_bridge_cb_failed)(enum ast_after_bridge_cb_reason reason, void *data); @@ -1709,6 +1712,9 @@ void ast_after_bridge_callback_discard(struct ast_channel *chan, enum ast_after_ * * \note chan is locked by this function. * + * \note failed is called when the channel leaves the bridging + * system or is destroyed. + * * \retval 0 on success. * \retval -1 on error. */ diff --git a/include/asterisk/config_options.h b/include/asterisk/config_options.h index 6b444466f5..d557b5685f 100644 --- a/include/asterisk/config_options.h +++ b/include/asterisk/config_options.h @@ -461,7 +461,7 @@ enum aco_process_status { /*! \brief Process a config info via the options registered with an aco_info * * \param info The config_options_info to be used for handling the config - * \param reload Whether or not this is a reload + * \param reload Non-zero if this is for a reload. * * \retval ACO_PROCESS_OK Success * \retval ACO_PROCESS_ERROR Failure diff --git a/include/asterisk/stasis_channels.h b/include/asterisk/stasis_channels.h index af2d15c3d7..78e3c6e418 100644 --- a/include/asterisk/stasis_channels.h +++ b/include/asterisk/stasis_channels.h @@ -435,6 +435,22 @@ struct stasis_message_type *ast_channel_monitor_start_type(void); */ struct stasis_message_type *ast_channel_monitor_stop_type(void); +/*! + * \since 12.0.0 + * \brief Message type for agent login on a channel + * + * \retval A stasis message type + */ +struct stasis_message_type *ast_channel_agent_login_type(void); + +/*! + * \since 12.0.0 + * \brief Message type for agent logoff on a channel + * + * \retval A stasis message type + */ +struct stasis_message_type *ast_channel_agent_logoff_type(void); + /*! * \since 12 * \brief Message type for starting music on hold on a channel diff --git a/main/bridging.c b/main/bridging.c index 07c04a1604..6febf61b0b 100644 --- a/main/bridging.c +++ b/main/bridging.c @@ -3242,15 +3242,70 @@ static struct ast_bridge_channel *bridge_channel_alloc(struct ast_bridge *bridge return bridge_channel; } -struct after_bridge_cb_ds { +struct after_bridge_cb_node { + /*! Next list node. */ + AST_LIST_ENTRY(after_bridge_cb_node) list; /*! Desired callback function. */ ast_after_bridge_cb callback; /*! After bridge callback will not be called and destroy any resources data may contain. */ ast_after_bridge_cb_failed failed; /*! Extra data to pass to the callback. */ void *data; + /*! Reason the after bridge callback failed. */ + enum ast_after_bridge_cb_reason reason; +}; + +struct after_bridge_cb_ds { + /*! After bridge callbacks container. */ + AST_LIST_HEAD(, after_bridge_cb_node) callbacks; }; +/*! + * \internal + * \brief Indicate after bridge callback failed. + * \since 12.0.0 + * + * \param node After bridge callback node. + * + * \return Nothing + */ +static void after_bridge_cb_failed(struct after_bridge_cb_node *node) +{ + if (node->failed) { + node->failed(node->reason, node->data); + node->failed = NULL; + } +} + +/*! + * \internal + * \brief Run discarding any after bridge callbacks. + * \since 12.0.0 + * + * \param after_bridge After bridge callback container process. + * \param reason Why are we doing this. + * + * \return Nothing + */ +static void after_bridge_cb_run_discard(struct after_bridge_cb_ds *after_bridge, enum ast_after_bridge_cb_reason reason) +{ + struct after_bridge_cb_node *node; + + for (;;) { + AST_LIST_LOCK(&after_bridge->callbacks); + node = AST_LIST_REMOVE_HEAD(&after_bridge->callbacks, list); + AST_LIST_UNLOCK(&after_bridge->callbacks); + if (!node) { + break; + } + if (!node->reason) { + node->reason = reason; + } + after_bridge_cb_failed(node); + ast_free(node); + } +} + /*! * \internal * \brief Destroy the after bridge callback datastore. @@ -3264,10 +3319,9 @@ static void after_bridge_cb_destroy(void *data) { struct after_bridge_cb_ds *after_bridge = data; - if (after_bridge->failed) { - after_bridge->failed(AST_AFTER_BRIDGE_CB_REASON_DESTROY, after_bridge->data); - after_bridge->failed = NULL; - } + after_bridge_cb_run_discard(after_bridge, AST_AFTER_BRIDGE_CB_REASON_DESTROY); + + AST_LIST_HEAD_DESTROY(&after_bridge->callbacks); ast_free(after_bridge); } @@ -3284,7 +3338,6 @@ static void after_bridge_cb_destroy(void *data) */ static void after_bridge_cb_fixup(void *data, struct ast_channel *old_chan, struct ast_channel *new_chan) { - /* There can be only one. Discard any already on the new channel. */ ast_after_bridge_callback_discard(new_chan, AST_AFTER_BRIDGE_CB_REASON_MASQUERADE); } @@ -3296,47 +3349,67 @@ static const struct ast_datastore_info after_bridge_cb_info = { /*! * \internal - * \brief Remove channel after the bridge callback and return it. + * \brief Setup/create an after bridge callback datastore container. * \since 12.0.0 * - * \param chan Channel to remove after bridge callback. + * \param chan Channel to setup/create the after bridge callback container on. * - * \retval datastore on success. - * \retval NULL on error or not found. + * \retval after_bridge datastore container on success. + * \retval NULL on error. */ -static struct ast_datastore *after_bridge_cb_remove(struct ast_channel *chan) +static struct after_bridge_cb_ds *after_bridge_cb_setup(struct ast_channel *chan) { struct ast_datastore *datastore; + struct after_bridge_cb_ds *after_bridge; + SCOPED_CHANNELLOCK(lock, chan); - ast_channel_lock(chan); datastore = ast_channel_datastore_find(chan, &after_bridge_cb_info, NULL); - if (datastore && ast_channel_datastore_remove(chan, datastore)) { - datastore = NULL; + if (datastore) { + return datastore->data; } - ast_channel_unlock(chan); - return datastore; + /* Create a new datastore. */ + datastore = ast_datastore_alloc(&after_bridge_cb_info, NULL); + if (!datastore) { + return NULL; + } + after_bridge = ast_calloc(1, sizeof(*after_bridge)); + if (!after_bridge) { + ast_datastore_free(datastore); + return NULL; + } + AST_LIST_HEAD_INIT(&after_bridge->callbacks); + datastore->data = after_bridge; + ast_channel_datastore_add(chan, datastore); + + return datastore->data; } -void ast_after_bridge_callback_discard(struct ast_channel *chan, enum ast_after_bridge_cb_reason reason) +/*! + * \internal + * \brief Find an after bridge callback datastore container. + * \since 12.0.0 + * + * \param chan Channel to find the after bridge callback container on. + * + * \retval after_bridge datastore container on success. + * \retval NULL on error. + */ +static struct after_bridge_cb_ds *after_bridge_cb_find(struct ast_channel *chan) { struct ast_datastore *datastore; + SCOPED_CHANNELLOCK(lock, chan); - datastore = after_bridge_cb_remove(chan); - if (datastore) { - struct after_bridge_cb_ds *after_bridge = datastore->data; - - if (after_bridge && after_bridge->failed) { - after_bridge->failed(reason, after_bridge->data); - after_bridge->failed = NULL; - } - ast_datastore_free(datastore); + datastore = ast_channel_datastore_find(chan, &after_bridge_cb_info, NULL); + if (!datastore) { + return NULL; } + return datastore->data; } /*! * \internal - * \brief Run any after bridge callback if possible. + * \brief Run any after bridge callback. * \since 12.0.0 * * \param chan Channel to run after bridge callback. @@ -3345,33 +3418,75 @@ void ast_after_bridge_callback_discard(struct ast_channel *chan, enum ast_after_ */ static void after_bridge_callback_run(struct ast_channel *chan) { - struct ast_datastore *datastore; struct after_bridge_cb_ds *after_bridge; + struct after_bridge_cb_node *node; - if (ast_check_hangup(chan)) { + after_bridge = after_bridge_cb_find(chan); + if (!after_bridge) { return; } - /* Get after bridge goto datastore. */ - datastore = after_bridge_cb_remove(chan); - if (!datastore) { + for (;;) { + AST_LIST_LOCK(&after_bridge->callbacks); + node = AST_LIST_REMOVE_HEAD(&after_bridge->callbacks, list); + AST_LIST_UNLOCK(&after_bridge->callbacks); + if (!node) { + break; + } + if (node->reason) { + after_bridge_cb_failed(node); + } else { + node->failed = NULL; + node->callback(chan, node->data); + } + ast_free(node); + } +} + +/*! + * \internal + * \brief Run discarding any after bridge callbacks. + * \since 12.0.0 + * + * \param chan Channel to run after bridge callback. + * + * \return Nothing + */ +static void after_bridge_callback_run_discard(struct ast_channel *chan, enum ast_after_bridge_cb_reason reason) +{ + struct after_bridge_cb_ds *after_bridge; + + after_bridge = after_bridge_cb_find(chan); + if (!after_bridge) { return; } - after_bridge = datastore->data; - if (after_bridge) { - after_bridge->failed = NULL; - after_bridge->callback(chan, after_bridge->data); + after_bridge_cb_run_discard(after_bridge, reason); +} + +void ast_after_bridge_callback_discard(struct ast_channel *chan, enum ast_after_bridge_cb_reason reason) +{ + struct after_bridge_cb_ds *after_bridge; + struct after_bridge_cb_node *node; + + after_bridge = after_bridge_cb_find(chan); + if (!after_bridge) { + return; } - /* Discard after bridge callback datastore. */ - ast_datastore_free(datastore); + AST_LIST_LOCK(&after_bridge->callbacks); + node = AST_LIST_LAST(&after_bridge->callbacks); + if (node && !node->reason) { + node->reason = reason; + } + AST_LIST_UNLOCK(&after_bridge->callbacks); } int ast_after_bridge_callback_set(struct ast_channel *chan, ast_after_bridge_cb callback, ast_after_bridge_cb_failed failed, void *data) { - struct ast_datastore *datastore; struct after_bridge_cb_ds *after_bridge; + struct after_bridge_cb_node *new_node; + struct after_bridge_cb_node *last_node; /* Sanity checks. */ ast_assert(chan != NULL); @@ -3379,29 +3494,28 @@ int ast_after_bridge_callback_set(struct ast_channel *chan, ast_after_bridge_cb return -1; } - /* Create a new datastore. */ - datastore = ast_datastore_alloc(&after_bridge_cb_info, NULL); - if (!datastore) { - return -1; - } - after_bridge = ast_calloc(1, sizeof(*after_bridge)); + after_bridge = after_bridge_cb_setup(chan); if (!after_bridge) { - ast_datastore_free(datastore); return -1; } - /* Initialize it. */ - after_bridge->callback = callback; - after_bridge->failed = failed; - after_bridge->data = data; - datastore->data = after_bridge; - - /* Put it on the channel replacing any existing one. */ - ast_channel_lock(chan); - ast_after_bridge_callback_discard(chan, AST_AFTER_BRIDGE_CB_REASON_REPLACED); - ast_channel_datastore_add(chan, datastore); - ast_channel_unlock(chan); + /* Create a new callback node. */ + new_node = ast_calloc(1, sizeof(*new_node)); + if (!new_node) { + return -1; + } + new_node->callback = callback; + new_node->failed = failed; + new_node->data = data; + /* Put it in the container disabling any previously active one. */ + AST_LIST_LOCK(&after_bridge->callbacks); + last_node = AST_LIST_LAST(&after_bridge->callbacks); + if (last_node && !last_node->reason) { + last_node->reason = AST_AFTER_BRIDGE_CB_REASON_REPLACED; + } + AST_LIST_INSERT_TAIL(&after_bridge->callbacks, new_node, list); + AST_LIST_UNLOCK(&after_bridge->callbacks); return 0; } @@ -3866,7 +3980,7 @@ static void *bridge_channel_depart_thread(void *data) ast_bridge_features_destroy(bridge_channel->features); bridge_channel->features = NULL; - ast_after_bridge_callback_discard(bridge_channel->chan, AST_AFTER_BRIDGE_CB_REASON_DEPART); + after_bridge_callback_run_discard(bridge_channel->chan, AST_AFTER_BRIDGE_CB_REASON_DEPART); ast_after_bridge_goto_discard(bridge_channel->chan); return NULL; diff --git a/main/stasis_channels.c b/main/stasis_channels.c index eb71531c2d..be65213567 100644 --- a/main/stasis_channels.c +++ b/main/stasis_channels.c @@ -55,6 +55,35 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") + + + Raised when an Agent has logged in. + + + + Agent ID of the agent. + + + + AgentLogin + AgentLogoff + + + + + + Raised when an Agent has logged off. + + + + The number of seconds the agent was logged in. + + + + AgentLogin + + + ***/ #define NUM_MULTI_CHANNEL_BLOB_BUCKETS 7 @@ -627,6 +656,44 @@ static struct ast_manager_event_blob *varset_to_ami(struct stasis_message *msg) ast_str_buffer(channel_event_string), variable, value); } +static struct ast_manager_event_blob *agent_login_to_ami(struct stasis_message *msg) +{ + RAII_VAR(struct ast_str *, channel_string, NULL, ast_free); + RAII_VAR(struct ast_str *, party_string, ast_str_create(256), ast_free); + struct ast_channel_blob *obj = stasis_message_data(msg); + const char *agent = ast_json_string_get(ast_json_object_get(obj->blob, "agent")); + + channel_string = ast_manager_build_channel_state_string(obj->snapshot); + if (!channel_string) { + return NULL; + } + + return ast_manager_event_blob_create(EVENT_FLAG_AGENT, "AgentLogin", + "%s" + "Agent: %s\r\n", + ast_str_buffer(channel_string), agent); +} + +static struct ast_manager_event_blob *agent_logoff_to_ami(struct stasis_message *msg) +{ + RAII_VAR(struct ast_str *, channel_string, NULL, ast_free); + RAII_VAR(struct ast_str *, party_string, ast_str_create(256), ast_free); + struct ast_channel_blob *obj = stasis_message_data(msg); + const char *agent = ast_json_string_get(ast_json_object_get(obj->blob, "agent")); + long logintime = ast_json_integer_get(ast_json_object_get(obj->blob, "logintime")); + + channel_string = ast_manager_build_channel_state_string(obj->snapshot); + if (!channel_string) { + return NULL; + } + + return ast_manager_event_blob_create(EVENT_FLAG_AGENT, "AgentLogoff", + "%s" + "Agent: %s\r\n" + "Logintime: %ld\r\n", + ast_str_buffer(channel_string), agent, logintime); +} + void ast_publish_channel_state(struct ast_channel *chan) { RAII_VAR(struct ast_channel_snapshot *, snapshot, NULL, ao2_cleanup); @@ -827,6 +894,12 @@ STASIS_MESSAGE_TYPE_DEFN(ast_channel_moh_start_type); STASIS_MESSAGE_TYPE_DEFN(ast_channel_moh_stop_type); STASIS_MESSAGE_TYPE_DEFN(ast_channel_monitor_start_type); STASIS_MESSAGE_TYPE_DEFN(ast_channel_monitor_stop_type); +STASIS_MESSAGE_TYPE_DEFN(ast_channel_agent_login_type, + .to_ami = agent_login_to_ami, + ); +STASIS_MESSAGE_TYPE_DEFN(ast_channel_agent_logoff_type, + .to_ami = agent_logoff_to_ami, + ); /*! @} */ @@ -853,6 +926,8 @@ static void stasis_channels_cleanup(void) STASIS_MESSAGE_TYPE_CLEANUP(ast_channel_moh_stop_type); STASIS_MESSAGE_TYPE_CLEANUP(ast_channel_monitor_start_type); STASIS_MESSAGE_TYPE_CLEANUP(ast_channel_monitor_stop_type); + STASIS_MESSAGE_TYPE_CLEANUP(ast_channel_agent_login_type); + STASIS_MESSAGE_TYPE_CLEANUP(ast_channel_agent_logoff_type); } void ast_stasis_channels_init(void) @@ -876,6 +951,8 @@ void ast_stasis_channels_init(void) STASIS_MESSAGE_TYPE_INIT(ast_channel_moh_stop_type); STASIS_MESSAGE_TYPE_INIT(ast_channel_monitor_start_type); STASIS_MESSAGE_TYPE_INIT(ast_channel_monitor_stop_type); + STASIS_MESSAGE_TYPE_INIT(ast_channel_agent_login_type); + STASIS_MESSAGE_TYPE_INIT(ast_channel_agent_logoff_type); channel_topic_all = stasis_topic_create("ast_channel_topic_all"); channel_topic_all_cached = stasis_caching_topic_create(channel_topic_all, channel_snapshot_get_id);