diff --git a/applications/acdc/Makefile b/applications/acdc/Makefile index 11d3162d16d..12c853ca897 100644 --- a/applications/acdc/Makefile +++ b/applications/acdc/Makefile @@ -1,7 +1,8 @@ -CWD = $(shell pwd -P) -ROOT = $(realpath $(CWD)/../..) +ROOT = ../.. PROJECT = acdc all: compile include $(ROOT)/make/kz.mk +TMPVAR := $(ERLC_OPTS) +ERLC_OPTS = $(filter-out -Werror, $(TMPVAR)) diff --git a/applications/acdc/doc/acdc_agent_maintenance.md b/applications/acdc/doc/acdc_agent_maintenance.md index 6885a280017..81f0f9fa1ce 100644 --- a/applications/acdc/doc/acdc_agent_maintenance.md +++ b/applications/acdc/doc/acdc_agent_maintenance.md @@ -2,8 +2,8 @@ | Function | Arguments | Description | | -------- | --------- | ----------- | -| `acct_restart/1` | `(AcctId)` | | -| `acct_status/1` | `(AcctId)` | | -| `agent_restart/2` | `(AcctId,AgentId)` | | -| `agent_status/2` | `(AcctId,AgentId)` | | +| `acct_restart/1` | `(AccountId)` | | +| `acct_status/1` | `(AccountId)` | | +| `agent_restart/2` | `(AccountId,AgentId)` | | +| `agent_status/2` | `(AccountId,AgentId)` | | | `status/0` | | | diff --git a/applications/acdc/doc/architecture.md b/applications/acdc/doc/architecture.md index 40a2d9ca108..90d157d0d64 100644 --- a/applications/acdc/doc/architecture.md +++ b/applications/acdc/doc/architecture.md @@ -4,7 +4,7 @@ Agents represent the endpoints that a call in a queue will try to connect with. #### Agents -Agents are comprised of two processes, a `gen_listener` (named process) and a `gen_statem`. A supervisor ensures that the `gen_listener` and `gen_statem` are kept alive together. A supervisor above that manages the list of agents. +Agents are comprised of two processes, a `gen_listener` (named process) and a `gen_fsm`. A supervisor ensures that the `gen_listener` and `gen_fsm` are kept alive together. A supervisor above that manages the list of agents. > Agent process tree @@ -52,9 +52,11 @@ The interplay between AMQP messages, the agent process, and the agent FSM proces <- member_connect_resp -- ``` -An agent will respond to all connect requests while in the *ready* state. The acdc_agent will pass along the `member_connect_req` to the FSM. If the FSM is in the *ready* state, the FSM will pass the request JSON to the agent process to send the `member_connect_resp` message. If the FSM is in any other state, the `member_connect_req` payload will be ignored. +An agent will respond to all connect requests while in the *ready* state. The acdc_agent will pass along the `member_connect_req` to the FSM. If the FSM is in the *ready* state, the FSM will pass the request JSON to the agent process to send the `member_connect_resp` message. If the FSM is in any other state, the `member_connect_req` payload will be ignored. -> Member Connect Win +Depending on the Queue strategy (ring_all, round robin), 1 or more agents that responded may receive the `member_connect_win` message + +> Member Connect Win and same Agent answers the member call ```asciiart AMQP Erlang @@ -79,7 +81,24 @@ An agent will respond to all connect requests while in the *ready* state. The ac {ready} ``` -When an agent receives a `member_connect_win` while in the *ready* state, the FSM will instruct the agent process to bridge to the agent's endpoint(s) and enter the *ringing* state. The agent process binds to the call events and attempts the bridge. Once the bridge is established, the FSM will instruct the agent process to send a `member_connect_accepted` and move to the *answered* state. On receiving a hangup event, the FSM will instruct the agent process to unbind from call events and will start the wrapup timer and enter the *wrapup* state. Once the timer fires, the FSM will return to the *ready* state. +> Member Connect Win and different Agent answers the member call + +```asciiart + AMQP Erlang +[Queue] [Agent] [AgentFSM] + {ready} + ---- member_connect_win ----> + -- member_connect_win --> ok + <- bridge_member -------- + {ringing} + {bind} + -- call_event ----------> + -- member_connect_satisfied --> + -- member_connect_satisfied --> + {ready} +``` + +When an agent receives a `member_connect_win` while in the *ready* state, the FSM will instruct the agent process to bridge to the agent's endpoint(s) and enter the *ringing* state. The agent process binds to the call events and attempts the bridge. Once the bridge is established, the FSM will instruct the agent process to send a `member_connect_accepted` and move to the *answered* state. On receiving a hangup event, the FSM will instruct the agent process to unbind from call events and will start the wrapup timer and enter the *wrapup* state. Once the timer fires, the FSM will return to the *ready* state. If another answers the member call then the agent process will receive a `member_connect_satisfied` and move back tp the *ready* state. > Member Connect Win (fail to bridge) @@ -226,10 +245,10 @@ When `acdc_init` starts up (last child of acdc_sup), it iterates through each ac acdc_init: foreach acct in accts foreach queue in acct - acdc_queues_sup:new(AcctId, QueueId) + acdc_queues_sup:new(AccountId, QueueId) ``` -`acdc_queues_sup` will start an `acdc_queue_sup` process to represent the AcctId/QueueId combo. The child list of the `acdc_queue_sup` has two entries: `acdc_queue_manager` and `acdc_queue_workers_sup`. +`acdc_queues_sup` will start an `acdc_queue_sup` process to represent the AccountId/QueueId combo. The child list of the `acdc_queue_sup` has two entries: `acdc_queue_manager` and `acdc_queue_workers_sup`. `acdc_queue_manager` handles receiving updates from config docs about the queue, member calls to the queue, etc. `acdc_queue_workers_sup` handles managing actual member_call processing units. Its really a pool of `member_call workers`. This allows us to utilize AMQP's ack/nack features to handle worker crashes and not lose the member call, but still process multiple calls at once (meaning, if we have a queue with 10 agents, and 5 members call in, 5 phones had better start ringing). diff --git a/applications/acdc/doc/features.md b/applications/acdc/doc/features.md new file mode 100644 index 00000000000..6e2dd319567 --- /dev/null +++ b/applications/acdc/doc/features.md @@ -0,0 +1,96 @@ +### Features + +#### Annoucements + +audio file played to caller to annouce position in the queue and estimated wait time. + +add this object to the queue document +``` + "announcements": { + "wait_time_announcements_enabled": true, + "position_announcements_enabled": true, + "interval": 30 + }, +``` + +#### Queue Strategies + +`round_robin` +`ring_all` +`most_idle` +`sbrr` + +#### Agent Priorities + +1 to 128 : Higher priority agents will be called 1st + +#### Call Priorities + +Higher priority calls can jump the queue +1) callflow can set "priority" in data to an integer value, higher the number the higher the priority +2) and/or the incoming call can have a custom variable <<"Call-Priority">> again an integer + + +#### Member Callback + +Member can have the option of being called back when they reach the head of the queue rather than waiting in line + + +#### Agent Pause + +An agent can be on a break and pause for set time either via the API or via a feature code + + +#### Early wrapup + +An Agent can make himself available before the wrapup period via the API + + +#### Agent Availability check callflow + +Example callflow: + +``` + "flow": { + "module": "acdc_agent_availability", + "data": {"id": "9a218da18b8104c888f0d47d946ffac0"}, + "children": { + "available": { + "data": { + "id": "9a218da18b8104c888f0d47d946ffac0" + }, + "module": "acdc_member", + "children": {} + }, + "unavailable": { + "data": { + "id": "/system_media/en-us%2Fqueue-no_agents_available" + }, + "module": "play", + "children": {} + } + } + } +``` + +#### Average wait time check callflow + +Example callflow: + +``` + "module": "acdc_wait_time", + "data": {"id": "9a218da18b8104c888f0d47d946ffac0"}, + "children": { + "_": { + "data": {"id": "9a218da18b8104c888f0d47d946ffac0"}, + "module": "acdc_member", + "children": {} + }, + "60": { + "data": {"id": "/system_media/en-us%2Fqueue-no_agents_available"}, + "module": "play", + "children": {} + } + } +``` + diff --git a/applications/acdc/doc/issues.md b/applications/acdc/doc/issues.md new file mode 100644 index 00000000000..59cfc814ab7 --- /dev/null +++ b/applications/acdc/doc/issues.md @@ -0,0 +1,23 @@ +### Known Issues + +#### Round Robin strategy + +In a multi node/zone cluster round robin doesn't work as an enduser would expect. + +The problem is that each node in the cluster has an independent Queue Manager process which picks the next suitable agent. Queue Managers only manage the node and so calls handled by other nodes in the cluster are not taken into account when selecting the next agent. + +Consider this scenario: + +Lets say we have a round robin queue Q1 with 4 agents (A1, A2, A3, A4) all with the same priority and there are 2 nodes in a cluster N1 and N2. + +when we start up Q1 will have a Queue Manager process on N1 and a Queue Manager process on N2. Each manager creates its own agent queue AQ1 and AQ2. Lets assume the Agents are initally added to the AQs in the same order [A1,A2,A3,A4] + +Now a call comes into Q1 and is handled by N1. The Queue Manager on N1 looks at the head of AQ1 and selects agent A1 for taking the call, A1 is then put at the back so AQ1 on N1 becomes [A2,A3,A4,A1] and agent A1's device starts ringing. + +A1 finishes the call. + +A 2nd call comes in and this time is handled by N2. AQ2 on N2 is still [A1,A2,A3,A4] and so A1 is selected again, hardly round robin. + +If, however, the 2nd call was again handled by N1 then everything would be as expected as A2 is at the head of AQ1. + +So the problem is how to tell N2 to move A1 to the back so that it matches N1. A possible solution would be to broadcast a federated agent_win message on the AMQP bus that all Queue Managers listen to and they update thier AQ. Another possible solution would be to make the AQ a shared resource by implementing it as a AMQP worker queue, but this would be a much bigger task. diff --git a/applications/acdc/doc/maintenance.md b/applications/acdc/doc/maintenance.md index bad7a656bcf..b8c54dd9a22 100644 --- a/applications/acdc/doc/maintenance.md +++ b/applications/acdc/doc/maintenance.md @@ -2,20 +2,20 @@ | Function | Arguments | Description | | -------- | --------- | ----------- | -| `agent_detail/2` | `(AcctId,AgentId)` | | -| `agent_login/2` | `(AcctId,AgentId)` | | -| `agent_logout/2` | `(AcctId,AgentId)` | | -| `agent_pause/2` | `(AcctId,AgentId)` | | -| `agent_pause/3` | `(AcctId,AgentId,Timeout)` | | +| `agent_detail/2` | `(AccountId,AgentId)` | | +| `agent_login/2` | `(AccountId,AgentId)` | | +| `agent_logout/2` | `(AccountId,AgentId)` | | +| `agent_pause/2` | `(AccountId,AgentId)` | | +| `agent_pause/3` | `(AccountId,AgentId,Timeout)` | | | `agent_presence_id/2` | `(AccountId,AgentId)` | | -| `agent_queue_login/3` | `(AcctId,AgentId,QueueId)` | | -| `agent_queue_logout/3` | `(AcctId,AgentId,QueueId)` | | -| `agent_resume/2` | `(AcctId,AgentId)` | | -| `agent_summary/2` | `(AcctId,AgentId)` | | +| `agent_queue_login/3` | `(AccountId,AgentId,QueueId)` | | +| `agent_queue_logout/3` | `(AccountId,AgentId,QueueId)` | | +| `agent_resume/2` | `(AccountId,AgentId)` | | +| `agent_summary/2` | `(AccountId,AgentId)` | | | `agents_detail/0` | | | -| `agents_detail/1` | `(AcctId)` | | +| `agents_detail/1` | `(AccountId)` | | | `agents_summary/0` | | | -| `agents_summary/1` | `(AcctId)` | | +| `agents_summary/1` | `(AccountId)` | | | `current_agents/1` | `(AccountId)` | | | `current_calls/1` | `(AccountId)` | | | `current_calls/2` | `(AccountId,Props) | (AccountId,QueueId)` | | @@ -26,14 +26,14 @@ | `logout_agents/1` | `(AccountId)` | | | `migrate/0` | | | | `migrate_to_acdc_db/0` | | | -| `queue_detail/2` | `(AcctId,QueueId)` | | -| `queue_restart/2` | `(AcctId,QueueId)` | | -| `queue_summary/2` | `(AcctId,QueueId)` | | +| `queue_detail/2` | `(AccountId,QueueId)` | | +| `queue_restart/2` | `(AccountId,QueueId)` | | +| `queue_summary/2` | `(AccountId,QueueId)` | | | `queues_detail/0` | | | -| `queues_detail/1` | `(AcctId)` | | -| `queues_restart/1` | `(AcctId)` | | +| `queues_detail/1` | `(AccountId)` | | +| `queues_restart/1` | `(AccountId)` | | | `queues_summary/0` | | | -| `queues_summary/1` | `(AcctId)` | | +| `queues_summary/1` | `(AccountId)` | | | `refresh/0` | | | | `refresh_account/1` | `(Account)` | | | `register_views/0` | | | diff --git a/applications/acdc/doc/ref/acdc_agent_maintenance.md b/applications/acdc/doc/ref/acdc_agent_maintenance.md index 6885a280017..81f0f9fa1ce 100644 --- a/applications/acdc/doc/ref/acdc_agent_maintenance.md +++ b/applications/acdc/doc/ref/acdc_agent_maintenance.md @@ -2,8 +2,8 @@ | Function | Arguments | Description | | -------- | --------- | ----------- | -| `acct_restart/1` | `(AcctId)` | | -| `acct_status/1` | `(AcctId)` | | -| `agent_restart/2` | `(AcctId,AgentId)` | | -| `agent_status/2` | `(AcctId,AgentId)` | | +| `acct_restart/1` | `(AccountId)` | | +| `acct_status/1` | `(AccountId)` | | +| `agent_restart/2` | `(AccountId,AgentId)` | | +| `agent_status/2` | `(AccountId,AgentId)` | | | `status/0` | | | diff --git a/applications/acdc/doc/ref/maintenance.md b/applications/acdc/doc/ref/maintenance.md index bad7a656bcf..709dacfc9f8 100644 --- a/applications/acdc/doc/ref/maintenance.md +++ b/applications/acdc/doc/ref/maintenance.md @@ -2,20 +2,20 @@ | Function | Arguments | Description | | -------- | --------- | ----------- | -| `agent_detail/2` | `(AcctId,AgentId)` | | -| `agent_login/2` | `(AcctId,AgentId)` | | -| `agent_logout/2` | `(AcctId,AgentId)` | | -| `agent_pause/2` | `(AcctId,AgentId)` | | -| `agent_pause/3` | `(AcctId,AgentId,Timeout)` | | +| `agent_detail/2` | `(AccountId,AgentId)` | | +| `agent_login/2` | `(AccountId,AgentId)` | | +| `agent_logout/2` | `(AccountId,AgentId)` | | +| `agent_pause/2` | `(AccountId,AgentId)` | | +| `agent_pause/3` | `(AccountId,AgentId,Timeout)` | | | `agent_presence_id/2` | `(AccountId,AgentId)` | | -| `agent_queue_login/3` | `(AcctId,AgentId,QueueId)` | | -| `agent_queue_logout/3` | `(AcctId,AgentId,QueueId)` | | -| `agent_resume/2` | `(AcctId,AgentId)` | | -| `agent_summary/2` | `(AcctId,AgentId)` | | +| `agent_queue_login/3` | `(AccountId,AgentId,QueueId)` | | +| `agent_queue_logout/3` | `(AccountId,AgentId,QueueId)` | | +| `agent_resume/2` | `(AccountId,AgentId)` | | +| `agent_summary/2` | `(AccountId,AgentId)` | | | `agents_detail/0` | | | -| `agents_detail/1` | `(AcctId)` | | +| `agents_detail/1` | `(AccountId)` | | | `agents_summary/0` | | | -| `agents_summary/1` | `(AcctId)` | | +| `agents_summary/1` | `(AccountId)` | | | `current_agents/1` | `(AccountId)` | | | `current_calls/1` | `(AccountId)` | | | `current_calls/2` | `(AccountId,Props) | (AccountId,QueueId)` | | @@ -26,14 +26,15 @@ | `logout_agents/1` | `(AccountId)` | | | `migrate/0` | | | | `migrate_to_acdc_db/0` | | | -| `queue_detail/2` | `(AcctId,QueueId)` | | -| `queue_restart/2` | `(AcctId,QueueId)` | | -| `queue_summary/2` | `(AcctId,QueueId)` | | +| `migrate_to_acdc_db/1` | `(AccountId)` | | +| `queue_detail/2` | `(AccountId,QueueId)` | | +| `queue_restart/2` | `(AccountId,QueueId)` | | +| `queue_summary/2` | `(AccountId,QueueId)` | | | `queues_detail/0` | | | -| `queues_detail/1` | `(AcctId)` | | -| `queues_restart/1` | `(AcctId)` | | +| `queues_detail/1` | `(AccountId)` | | +| `queues_restart/1` | `(AccountId)` | | | `queues_summary/0` | | | -| `queues_summary/1` | `(AcctId)` | | +| `queues_summary/1` | `(AccountId)` | | | `refresh/0` | | | -| `refresh_account/1` | `(Account)` | | +| `refresh_account/1` | `(Acct)` | | | `register_views/0` | | | diff --git a/applications/acdc/media/callback-call_back_at.mp3 b/applications/acdc/media/callback-call_back_at.mp3 new file mode 100755 index 00000000000..61ca07522b8 Binary files /dev/null and b/applications/acdc/media/callback-call_back_at.mp3 differ diff --git a/applications/acdc/media/callback-callback_registered.mp3 b/applications/acdc/media/callback-callback_registered.mp3 new file mode 100755 index 00000000000..b99e3aab169 Binary files /dev/null and b/applications/acdc/media/callback-callback_registered.mp3 differ diff --git a/applications/acdc/media/callback-enter_callback_number.mp3 b/applications/acdc/media/callback-enter_callback_number.mp3 new file mode 100755 index 00000000000..b2f488bbcf8 Binary files /dev/null and b/applications/acdc/media/callback-enter_callback_number.mp3 differ diff --git a/applications/acdc/media/callback-number_correct.mp3 b/applications/acdc/media/callback-number_correct.mp3 new file mode 100755 index 00000000000..154d8bb539c Binary files /dev/null and b/applications/acdc/media/callback-number_correct.mp3 differ diff --git a/applications/acdc/media/callback-prompt.mp3 b/applications/acdc/media/callback-prompt.mp3 new file mode 100755 index 00000000000..8ce6b835c47 Binary files /dev/null and b/applications/acdc/media/callback-prompt.mp3 differ diff --git a/applications/acdc/media/queue-now_calling_back.mp3 b/applications/acdc/media/queue-now_calling_back.mp3 new file mode 100644 index 00000000000..403a6221f8d Binary files /dev/null and b/applications/acdc/media/queue-now_calling_back.mp3 differ diff --git a/applications/acdc/priv/couchdb/views/agents.json b/applications/acdc/priv/couchdb/views/agents.json index 09e2f86f3fe..ab95340e2da 100644 --- a/applications/acdc/priv/couchdb/views/agents.json +++ b/applications/acdc/priv/couchdb/views/agents.json @@ -4,6 +4,9 @@ "view_map": [ { "classification": "account" + }, + { + "database": "acdc" } ] }, @@ -16,7 +19,9 @@ " emit(doc._id, {", " 'first_name': doc.first_name,", " 'last_name': doc.last_name,", - " 'queues': doc.queues", + " 'queues': doc.queues,", + " 'agent_priority': doc.acdc_agent_priority || 0,", + " 'skills': doc.acdc_skills || []", " });", "}" ] diff --git a/applications/acdc/priv/couchdb/views/call_stats.json b/applications/acdc/priv/couchdb/views/call_stats.json index 094dcd25d8a..29cd9d26d7a 100644 --- a/applications/acdc/priv/couchdb/views/call_stats.json +++ b/applications/acdc/priv/couchdb/views/call_stats.json @@ -17,6 +17,51 @@ "}" ] }, + "call_summary": { + "map": [ + "function(doc) {", + " if (doc.pvt_type != 'call_summary_stat') return;", + " if (doc.status === \"abandoned\") {", + " emit([doc.queue_id, doc.timestamp], {", + " 'entered_position': doc.entered_position,", + " 'status': doc.status,", + " 'wait_time': doc.wait_time,", + " 'talk_time': 0,", + " 'calls': 1,", + " 'abandoned': 1", + " });", + " } else {", + " emit([doc.queue_id, doc.timestamp], {", + " 'entered_position': doc.entered_position,", + " 'status': doc.status,", + " 'wait_time': doc.wait_time,", + " 'talk_time': doc.talk_time,", + " 'calls': 1,", + " 'abandoned': 0", + " });", + " }", + "}" + ], + "reduce": [ + "function(key, values, rereduce) {", + " var result = {", + " calls: 0,", + " abandoned: 0,", + " wait_time: 0,", + " talk_time: 0,", + " entered_position: 0", + " };", + " for (var i = 0; i < values.length; i++) {", + " result.calls = result.calls + values[i].calls;", + " result.abandoned = result.abandoned + values[i].abandoned;", + " result.wait_time = result.wait_time + values[i].wait_time;", + " result.talk_time = result.talk_time + values[i].talk_time;", + " result.entered_position = Math.max(result.entered_position, values[i].entered_position);", + " }", + " return (result);", + "}" + ] + }, "crossbar_listing": { "map": [ "function(doc) {", @@ -24,14 +69,20 @@ " emit(doc.entered_timestamp, {", " id: doc._id,", " entered_timestamp: doc.entered_timestamp,", + " abandoned_timestamp: doc.abandoned_timestamp,", " handled_timestamp: doc.handled_timestamp,", + " processed_timestamp: doc.processed_timestamp,", " caller_id_number: doc.caller_id_number,", " caller_id_name: doc.caller_id_name,", " entered_position: doc.entered_position,", + " exited_position: doc.exited_position,", " status: doc.status,", " agent_id: doc.agent_id,", " wait_time: doc.wait_time,", " talk_time: doc.talk_time,", + " misses: doc.misses,", + " required_skills: doc.required_skills,", + " call_id: doc.call_id,", " queue_id: doc.queue_id", " });", "}" diff --git a/applications/acdc/priv/couchdb/views/queues.json b/applications/acdc/priv/couchdb/views/queues.json index 97e5f545fc3..3d0b681b295 100644 --- a/applications/acdc/priv/couchdb/views/queues.json +++ b/applications/acdc/priv/couchdb/views/queues.json @@ -4,6 +4,9 @@ "view_map": [ { "classification": "account" + }, + { + "database": "acdc" } ] }, @@ -14,7 +17,11 @@ "function(doc) {", " if (doc.pvt_type !== 'user' || doc.pvt_deleted || typeof doc.queues !== 'object') return;", " for (i in doc.queues) {", - " emit([doc.queues[i], doc._id], doc._id);", + " emit(doc.queues[i], {", + " 'id': doc._id,", + " 'agent_priority': doc.acdc_agent_priority || 0,", + " 'skills': doc.acdc_skills || []", + " });", " }", "}" ], @@ -26,8 +33,7 @@ " if (doc.pvt_type != 'queue' || doc.pvt_deleted) return;", " emit(doc._id, {", " 'id': doc._id,", - " 'name': doc.name,", - " 'strategy': doc.strategy", + " 'name': doc.name", " });", "}" ] diff --git a/applications/acdc/src/acdc.app.src b/applications/acdc/src/acdc.app.src index 0ee34463ea2..971e56cfb46 100644 --- a/applications/acdc/src/acdc.app.src +++ b/applications/acdc/src/acdc.app.src @@ -1,9 +1,11 @@ {application,acdc, - [{applications,[callflow,crossbar,crypto,gproc,kazoo,kazoo_amqp, - kazoo_apps,kazoo_caches,kazoo_call,kazoo_data, + [{applications,[blackhole,callflow,crossbar,crypto,gproc,hackney, + kazoo,kazoo_amqp,kazoo_apps,kazoo_caches, + kazoo_call,kazoo_config,kazoo_data, kazoo_documents,kazoo_edr,kazoo_endpoint, - kazoo_events,kazoo_media,kazoo_modb,kazoo_stdlib, - kazoo_web,kernel,lager,stdlib,webseq]}, + kazoo_events,kazoo_media,kazoo_modb, + kazoo_numbers,kazoo_stdlib,kazoo_web,kernel, + lager,pqueue,stdlib,webseq]}, {description,"ACDc - Automatic Call Distribution commander"}, {env,[{is_kazoo_app,true}]}, {mod,{acdc_app,[]}}, diff --git a/applications/acdc/src/acdc.hrl b/applications/acdc/src/acdc.hrl index 6f205189fdc..cf42b50c4d9 100644 --- a/applications/acdc/src/acdc.hrl +++ b/applications/acdc/src/acdc.hrl @@ -5,38 +5,37 @@ -include_lib("kazoo_amqp/include/kz_api_literals.hrl"). -include("acdc_config.hrl"). --define(APP_NAME, <<"acdc">>). +-define(APP, acdc). +-define(APP_NAME, (atom_to_binary(?APP, utf8))). -define(APP_VERSION, <<"4.0.0">>). -define(CONFIG_CAT, ?APP_NAME). -define(CACHE_NAME, 'acdc_cache'). --define(ABANDON_TIMEOUT, 'member_timeout'). --define(ABANDON_EXIT, 'member_exit'). --define(ABANDON_HANGUP, 'member_hangup'). --define(ABANDON_EMPTY, 'member_exit_empty'). +-define(ABANDON_TIMEOUT, <<"member_timeout">>). +-define(ABANDON_EXIT, <<"member_exit">>). +-define(ABANDON_HANGUP, <<"member_hangup">>). +-define(ABANDON_EMPTY, <<"member_exit_empty">>). +-define(ABANDON_INTERNAL_ERROR, <<"INTERNAL ERROR">>). -define(PRESENCE_GREEN, <<"terminated">>). -define(PRESENCE_RED_FLASH, <<"early">>). -define(PRESENCE_RED_SOLID, <<"confirmed">>). --define(ENDPOINT_UPDATE_REG(AcctId, EPId), {'p', 'l', {'endpoint_update', AcctId, EPId}}). +-define(ENDPOINT_UPDATE_REG(AccountId, EPId), {'p', 'l', {'endpoint_update', AccountId, EPId}}). -define(ENDPOINT_CREATED(EP), {'endpoint_created', EP}). -define(ENDPOINT_EDITED(EP), {'endpoint_edited', EP}). -define(ENDPOINT_DELETED(EP), {'endpoint_deleted', EP}). --define(OWNER_UPDATE_REG(AcctId, OwnerId), {'p', 'l', {'owner_update', AcctId, OwnerId}}). +-define(OWNER_UPDATE_REG(AccountId, OwnerId), {'p', 'l', {'owner_update', AccountId, OwnerId}}). --define(NEW_CHANNEL_REG(AcctId, User), {'p', 'l', {'new_channel', AcctId, User}}). --define(NEW_CHANNEL_FROM(CallId), {'call_from', CallId}). --define(NEW_CHANNEL_TO(CallId, MemberCallId), {'call_to', CallId, MemberCallId}). +-define(NEW_CHANNEL_REG(AccountId, User), {'p', 'l', {'new_channel', AccountId, User}}). +-define(NEW_CHANNEL_TO(CallId, Number, Name), {{'call_to', Number, Name}, CallId}). +-define(NEW_CHANNEL_FROM(CallId, Number, Name, MemberCallId), {{'call_from', Number, Name}, CallId, MemberCallId}). --define(DESTROYED_CHANNEL_REG(AcctId, User), {'p', 'l', {'destroyed_channel', AcctId, User}}). +-define(DESTROYED_CHANNEL_REG(AccountId, User), {'p', 'l', {'destroyed_channel', AccountId, User}}). -define(DESTROYED_CHANNEL(CallId, HangupCause), {'call_down', CallId, HangupCause}). --type abandon_reason() :: ?ABANDON_TIMEOUT | ?ABANDON_EXIT | - ?ABANDON_HANGUP. - -type deliveries() :: [gen_listener:basic_deliver()]. -type announcements_pids() :: #{kz_term:ne_binary() => pid()}. @@ -45,6 +44,8 @@ 'ringing_callback' | 'awaiting_callback' | 'answered' | 'wrapup' | 'paused' | 'outbound'. +-type agent_priority() :: -128..128. + %% Check for cleanup every 5 minutes -define(CLEANUP_PERIOD, kapps_config:get_integer(?CONFIG_CAT, <<"cleanup_period_ms">>, 360000)). diff --git a/applications/acdc/src/acdc_agent_fsm.erl b/applications/acdc/src/acdc_agent_fsm.erl index 7e02e0ca50b..3f8b505790f 100644 --- a/applications/acdc/src/acdc_agent_fsm.erl +++ b/applications/acdc/src/acdc_agent_fsm.erl @@ -5,7 +5,6 @@ %%% %%% @author James Aimonetti %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -13,21 +12,22 @@ %%% @end %%%----------------------------------------------------------------------------- -module(acdc_agent_fsm). - -behaviour(gen_statem). %% API -export([start_link/3, start_link/4, start_link/5 ,call_event/4 ,member_connect_req/2 - ,member_connect_win/2 - ,member_connect_satisfied/2 + ,member_connect_win/3 + ,member_connect_satisfied/3 ,agent_timeout/2 + ,shared_failure/2 + ,shared_call_id/2 ,originate_ready/2 ,originate_resp/2, originate_started/2, originate_uuid/2 ,originate_failed/2 ,sync_req/2, sync_resp/2 - ,pause/2 + ,pause/3 ,resume/1 ,end_wrapup/1 @@ -57,10 +57,13 @@ ,sync/3 ,ready/3 ,ringing/3 + ,ringing_callback/3 + ,awaiting_callback/3 ,answered/3 ,wrapup/3 ,paused/3 ,outbound/3 + ,inbound/3 ]). -ifdef(TEST). @@ -75,7 +78,7 @@ -define(SYNC_RESPONSE_TIMEOUT, 5000). -define(SYNC_RESPONSE_MESSAGE, 'sync_response_timeout'). -%% We weren't able to join our brethren, how long to wait to check again +%% We weren't able to join our brethern, how long to wait to check again -define(RESYNC_RESPONSE_TIMEOUT, 15000). -define(RESYNC_RESPONSE_MESSAGE, 'resync_response_timeout'). @@ -97,42 +100,48 @@ -record(state, {account_id :: kz_term:ne_binary() ,account_db :: kz_term:ne_binary() ,agent_id :: kz_term:ne_binary() - ,agent_listener :: kz_types:server_ref() - ,agent_listener_id :: kz_term:api_ne_binary() + ,agent_listener :: kz_term:server_ref() + ,agent_listener_id :: api_kz_term:ne_binary() ,agent_name :: kz_term:api_binary() ,wrapup_timeout = 0 :: integer() % optionally set on win ,wrapup_ref :: kz_term:api_reference() ,sync_ref :: kz_term:api_reference() - ,pause_ref :: kz_term:api_reference() + ,pause_ref :: kz_term:api_reference() | 'infinity' + ,pause_alias :: kz_term:api_binary() - ,member_call :: kapps_call:call() | 'undefined' + ,member_call :: kapps_call:call() ,member_call_id :: kz_term:api_binary() + ,member_callback_candidates = [] :: kz_term:proplist() + ,member_original_call :: kapps_call:call() + ,member_original_call_id :: kz_term:api_binary() ,member_call_queue_id :: kz_term:api_binary() - ,member_call_start :: kz_time:start_time() | 'undefined' + ,member_call_start :: kz_term:kz_now() | undefined ,caller_exit_key = <<"#">> :: kz_term:ne_binary() ,queue_notifications :: kz_term:api_object() ,agent_call_id :: kz_term:api_binary() + ,agent_callback_call = 'undefined' :: kapps_call:call() | 'undefined' ,next_status :: kz_term:api_binary() - ,statem_call_id :: kz_term:api_binary() % used when no call-ids are available + ,fsm_call_id :: kz_term:api_binary() % used when no call-ids are available ,endpoints = [] :: kz_json:objects() ,outbound_call_ids = [] :: kz_term:ne_binaries() - ,max_connect_failures :: timeout() + ,inbound_call_ids = [] :: kz_term:ne_binaries() + ,max_connect_failures :: kz_term:timeout() | 'infinity' ,connect_failures = 0 :: non_neg_integer() ,agent_state_updates = [] :: list() + ,monitoring = 'false' :: boolean() }). -type state() :: #state{}. %%%============================================================================= %%% API %%%============================================================================= - %%------------------------------------------------------------------------------ %% @doc When a queue receives a call and needs an agent, it will send a -%% `member_connect_req'. The agent will respond (if possible) with a -%% `member_connect_resp' payload or ignore the request +%% member_connect_req. The agent will respond (if possible) with a +%% member_connect_resp payload or ignore the request %% @end %%------------------------------------------------------------------------------ -spec member_connect_req(pid(), kz_json:object()) -> 'ok'. @@ -140,27 +149,38 @@ member_connect_req(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'member_connect_req', JObj}). %%------------------------------------------------------------------------------ -%% @doc When a queue receives a call and needs an agent, it will send a -%% `member_connect_req'. The agent will respond (if possible) with a -%% `member_connect_resp' payload or ignore the request +%% @doc When an agent has been selected to handle the queue call, each process +%% for the agent will receive a `member_connect_win' event. The event will +%% include a flag of whether the winner is on the current node - if true, the +%% agent process will handle call control. Otherwise, the agent process will +%% just follow along through state transitions. %% @end %%------------------------------------------------------------------------------ --spec member_connect_win(pid(), kz_json:object()) -> 'ok'. -member_connect_win(ServerRef, JObj) -> - gen_statem:cast(ServerRef, {'member_connect_win', JObj}). +-type member_connect_win_node() :: 'same_node' | 'different_node'. +-spec member_connect_win(pid(), kz_json:object(), member_connect_win_node()) -> 'ok'. +member_connect_win(ServerRef, JObj, Node) -> + gen_statem:cast(ServerRef, {'member_connect_win', JObj, Node}). --spec member_connect_satisfied(pid(), kz_json:object()) -> 'ok'. -member_connect_satisfied(ServerRef, JObj) -> - gen_statem:cast(ServerRef, {'member_connect_satisfied', JObj}). +-spec member_connect_satisfied(pid(), kz_json:object(), 'same_node'|'different_node') -> 'ok'. +member_connect_satisfied(ServerRef, JObj, Node) -> + gen_statem:cast(ServerRef, {'member_connect_satisfied', JObj, Node}). -spec agent_timeout(pid(), kz_json:object()) -> 'ok'. agent_timeout(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'agent_timeout', JObj}). +-spec shared_failure(pid(), kz_json:object()) -> 'ok'. +shared_failure(ServerRef, JObj) -> + gen_statem:cast(ServerRef, {'shared_failure', JObj}). + +-spec shared_call_id(pid(), kz_json:object()) -> 'ok'. +shared_call_id(ServerRef, JObj) -> + gen_statem:cast(ServerRef, {'shared_call_id', JObj}). + %%------------------------------------------------------------------------------ %% @doc When an agent is involved in a call, it will receive call events. -%% Pass the call event to the `statem' to see if action is needed (usually -%% for bridge and hangup events). +%% Pass the call event to the ServerRef to see if action is needed (usually +%% for bridge and hangup events). %% @end %%------------------------------------------------------------------------------ -spec call_event(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. @@ -171,15 +191,24 @@ call_event(ServerRef, <<"call_event">>, <<"CHANNEL_UNBRIDGE">>, JObj) -> call_event(ServerRef, <<"call_event">>, <<"usurp_control">>, JObj) -> gen_statem:cast(ServerRef, {'usurp_control', call_id(JObj)}); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_DESTROY">>, JObj) -> - ServerRef ! ?DESTROYED_CHANNEL(call_id(JObj), acdc_util:hangup_cause(JObj)); + gen_statem:cast(ServerRef, ?DESTROYED_CHANNEL(call_id(JObj), acdc_util:hangup_cause(JObj))); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_DISCONNECTED">>, JObj) -> - ServerRef ! ?DESTROYED_CHANNEL(call_id(JObj), <<"MEDIA_SERVER_UNREACHABLE">>); + gen_statem:cast(ServerRef, ?DESTROYED_CHANNEL(call_id(JObj), <<"MEDIA_SERVER_UNREACHABLE">>)); call_event(ServerRef, <<"call_event">>, <<"LEG_CREATED">>, JObj) -> - gen_statem:cast(ServerRef, {'leg_created', call_id(JObj)}); + %% Due to change in kapi_call to send events based on Origination-Call-ID, + %% we do not want to bind to any of the loopback legs + case kz_json:get_ne_binary_value(<<"Channel-Loopback-Leg">>, JObj) of + 'undefined' -> + gen_statem:cast(ServerRef, {'leg_created' + ,call_id(JObj) + ,kz_call_event:other_leg_call_id(JObj) + }); + _ -> 'ok' + end; call_event(ServerRef, <<"call_event">>, <<"LEG_DESTROYED">>, JObj) -> gen_statem:cast(ServerRef, {'leg_destroyed', call_id(JObj)}); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_ANSWER">>, JObj) -> - gen_statem:cast(ServerRef, {'channel_answered', call_id(JObj)}); + gen_statem:cast(ServerRef, {'channel_answered', JObj}); call_event(ServerRef, <<"call_event">>, <<"DTMF">>, EvtJObj) -> gen_statem:cast(ServerRef, {'dtmf_pressed', kz_json:get_value(<<"DTMF-Digit">>, EvtJObj)}); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_EXECUTE_COMPLETE">>, JObj) -> @@ -194,9 +223,12 @@ call_event(ServerRef, <<"error">>, <<"dialplan">>, JObj) -> call_event(ServerRef, <<"call_event">>, <<"CHANNEL_REPLACED">>, JObj) -> gen_statem:cast(ServerRef, {'channel_replaced', JObj}); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_TRANSFEREE">>, JObj) -> - gen_statem:cast(ServerRef, {'channel_unbridged', call_id(JObj)}); + Transferor = kz_call_event:other_leg_call_id(JObj), + Transferee = kz_call_event:call_id(JObj), + gen_statem:cast(ServerRef, {'channel_transferee', Transferor, Transferee}); call_event(_, _C, _E, _) -> - lager:info("unhandled combo: ~s/~s", [_C, _E]). + lager:debug("unhandled combo: ~s/~s", [_C, _E]), + 'ok'. %%------------------------------------------------------------------------------ %% @doc @@ -212,36 +244,44 @@ maybe_send_execute_complete(ServerRef, <<"bridge">>, JObj) -> gen_statem:cast(ServerRef, {'channel_unbridged', call_id(JObj)}); maybe_send_execute_complete(ServerRef, <<"call_pickup">>, JObj) -> gen_statem:cast(ServerRef, {'channel_bridged', call_id(JObj)}); +maybe_send_execute_complete(ServerRef, <<"play">>, JObj) -> + case kz_json:get_value(<<"Application-Response">>, JObj) of + <<"FILE PLAYED">> -> + gen_statem:cast(ServerRef, {'playback_stop', call_id(JObj)}); + AR -> + lager:debug("application Response: ~p", [AR]), + gen_statem:cast(ServerRef, {'playback_stop', call_id(JObj)}) + end; maybe_send_execute_complete(_, _, _) -> 'ok'. %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ --spec originate_ready(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec originate_ready(kz_term:server_ref(), kz_json:object()) -> 'ok'. originate_ready(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'originate_ready', JObj}). --spec originate_resp(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec originate_resp(kz_term:server_ref(), kz_json:object()) -> 'ok'. originate_resp(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'originate_resp', kz_json:get_value(<<"Call-ID">>, JObj)}). --spec originate_started(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec originate_started(kz_term:server_ref(), kz_json:object()) -> 'ok'. originate_started(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'originate_started', kz_json:get_value(<<"Call-ID">>, JObj)}). --spec originate_uuid(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec originate_uuid(kz_term:server_ref(), kz_json:object()) -> 'ok'. originate_uuid(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'originate_uuid' - ,kz_json:get_value(<<"Outbound-Call-ID">>, JObj) - ,kz_json:get_value(<<"Outbound-Call-Control-Queue">>, JObj) - }). + ,kz_json:get_value(<<"Outbound-Call-ID">>, JObj) + ,kz_json:get_value(<<"Outbound-Call-Control-Queue">>, JObj) + }). %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ --spec originate_failed(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec originate_failed(kz_term:server_ref(), kz_json:object()) -> 'ok'. originate_failed(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'originate_failed', JObj}). @@ -249,7 +289,7 @@ originate_failed(ServerRef, JObj) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec sync_req(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec sync_req(kz_term:server_ref(), kz_json:object()) -> 'ok'. sync_req(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'sync_req', JObj}). @@ -257,7 +297,7 @@ sync_req(ServerRef, JObj) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec sync_resp(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec sync_resp(kz_term:server_ref(), kz_json:object()) -> 'ok'. sync_resp(ServerRef, JObj) -> gen_statem:cast(ServerRef, {'sync_resp', JObj}). @@ -265,15 +305,15 @@ sync_resp(ServerRef, JObj) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec pause(kz_types:server_ref(), timeout()) -> 'ok'. -pause(ServerRef, Timeout) -> - gen_statem:cast(ServerRef, {'pause', Timeout}). +-spec pause(kz_term:server_ref(), kz_term:timeout(), kz_term:api_binary()) -> 'ok'. +pause(ServerRef, Timeout, Alias) -> + gen_statem:cast(ServerRef, {'pause', Timeout, Alias}). %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ --spec resume(kz_types:server_ref()) -> 'ok'. +-spec resume(kz_term:server_ref()) -> 'ok'. resume(ServerRef) -> gen_statem:cast(ServerRef, {'resume'}). @@ -281,7 +321,7 @@ resume(ServerRef) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec end_wrapup(kz_types:server_ref()) -> 'ok'. +-spec end_wrapup(kz_term:server_ref()) -> 'ok'. end_wrapup(ServerRef) -> gen_statem:cast(ServerRef, {'end_wrapup'}). @@ -290,7 +330,7 @@ end_wrapup(ServerRef) -> %% availability update depending on agent state %% @end %%------------------------------------------------------------------------------ --spec add_acdc_queue(kz_types:server_ref(), kz_term:ne_binary()) -> 'ok'. +-spec add_acdc_queue(kz_term:server_ref(), kz_term:ne_binary()) -> 'ok'. add_acdc_queue(ServerRef, QueueId) -> gen_statem:cast(ServerRef, {'add_acdc_queue', QueueId}). @@ -299,7 +339,7 @@ add_acdc_queue(ServerRef, QueueId) -> %% unavailability update %% @end %%------------------------------------------------------------------------------ --spec rm_acdc_queue(kz_types:server_ref(), kz_term:ne_binary()) -> 'ok'. +-spec rm_acdc_queue(kz_term:server_ref(), kz_term:ne_binary()) -> 'ok'. rm_acdc_queue(ServerRef, QueueId) -> gen_statem:cast(ServerRef, {'rm_acdc_queue', QueueId}). @@ -307,7 +347,7 @@ rm_acdc_queue(ServerRef, QueueId) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec update_presence(kz_types:server_ref(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +-spec update_presence(kz_term:server_ref(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. update_presence(ServerRef, PresenceId, PresenceState) -> gen_statem:cast(ServerRef, {'update_presence', PresenceId, PresenceState}). @@ -315,7 +355,7 @@ update_presence(ServerRef, PresenceId, PresenceState) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec agent_logout(kz_types:server_ref()) -> 'ok'. +-spec agent_logout(kz_term:server_ref()) -> 'ok'. agent_logout(ServerRef) -> gen_statem:cast(ServerRef, {'agent_logout'}). @@ -333,7 +373,7 @@ current_call(ServerRef) -> gen_statem:call(ServerRef, 'current_call'). status(ServerRef) -> gen_statem:call(ServerRef, 'status'). %%------------------------------------------------------------------------------ -%% @doc Creates a gen_statem process which calls Module:init/1 to +%% @doc Creates a gen_ServerRef process which calls Module:init/1 to %% initialize. To ensure a synchronized start-up procedure, this %% function does not return until Module:init/1 has returned. %% @end @@ -372,43 +412,52 @@ deleted_endpoint(ServerRef, EP) -> lager:debug("sending EP to ~p: ~p", [ServerRef, EP]). %%%============================================================================= -%%% gen_statem callbacks +%%% gen_ServerRef callbacks %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a gen_statem is started using -%% gen_statem:start_link/[3,4], this function is called by the new +%% @private +%% @doc Whenever a gen_ServerRef is started using gen_ServerRef:start/[3,4] or +%% gen_ServerRef:start_link/[3,4], this function is called by the new %% process to initialize. %% %% @end %%------------------------------------------------------------------------------ -spec init(list()) -> {'ok', atom(), state()}. init([AccountId, AgentId, Supervisor, Props, IsThief]) -> - StateMCallId = <<"statem_", AccountId/binary, "_", AgentId/binary>>, - kz_log:put_callid(StateMCallId), - lager:debug("started acdc agent statem"), + ServerRefCallId = <<"fsm_", AccountId/binary, "_", AgentId/binary>>, + kz_log:put_callid(ServerRefCallId), + lager:debug("started acdc agent ServerRef"), _P = kz_process:spawn(fun wait_for_listener/4, [Supervisor, self(), Props, IsThief]), lager:debug("waiting for listener in ~p", [_P]), + AccountDb = kzs_util:format_account_db(AccountId), + {'ok', UserDoc} = kz_datamgr:open_cache_doc(AccountDb, AgentId), + {'ok' ,'wait' ,#state{account_id = AccountId - ,account_db = kzs_util:format_account_db(AccountId) + ,account_db = AccountDb ,agent_id = AgentId - ,statem_call_id = StateMCallId + ,agent_name = kz_json:get_value(<<"username">>, UserDoc) + ,fsm_call_id = ServerRefCallId ,max_connect_failures = max_failures(AccountId) } }. --spec max_failures(kz_term:ne_binary() | kz_json:object()) -> non_neg_integer(). +-spec max_failures(kz_term:ne_binary() | kz_json:object()) -> non_neg_integer() | 'infinity'. max_failures(Account) when is_binary(Account) -> case kzd_accounts:fetch(Account) of {'ok', AccountJObj} -> max_failures(AccountJObj); {'error', _} -> ?MAX_FAILURES end; +max_failures(0) -> + 'infinity'; +max_failures(V) when is_integer(V), V > 0 -> + V; max_failures(JObj) -> - kz_json:get_integer_value(?MAX_CONNECT_FAILURES, JObj, ?MAX_FAILURES). + max_failures(kz_json:get_integer_value(?MAX_CONNECT_FAILURES, JObj, ?MAX_FAILURES)). -spec wait_for_listener(pid(), pid(), kz_term:proplist(), boolean()) -> 'ok'. wait_for_listener(Supervisor, ServerRef, Props, IsThief) -> @@ -447,12 +496,9 @@ callback_mode() -> %% @end %%------------------------------------------------------------------------------ -spec wait(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). -wait('cast', {'listener', AgentListener, NextState, SyncRef}, #state{account_id=AccountId - ,agent_id=AgentId - }=State) -> +wait('cast', {'listener', AgentListener, NextState, SyncRef}, State) -> lager:debug("setting agent proc to ~p", [AgentListener]), acdc_agent_listener:fsm_started(AgentListener, self()), - acdc_agent_stats:agent_ready(AccountId, AgentId), {'next_state', NextState, State#state{agent_listener=AgentListener ,sync_ref=SyncRef ,agent_listener_id=acdc_util:proc_id() @@ -470,22 +516,23 @@ wait('info', Evt, State) -> handle_info(Evt, 'wait', State). %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ -spec sync(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). sync('cast', 'send_sync_event', #state{agent_listener=AgentListener - ,agent_listener_id=_AProcId - }=State) -> + ,agent_listener_id=_AProcId + }=State) -> lager:debug("sending sync_req event to other agent processes: ~s", [_AProcId]), acdc_agent_listener:send_sync_req(AgentListener), {'next_state', 'sync', State}; sync('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener - ,agent_listener_id=AProcId - }=State) -> + ,agent_listener_id=AProcId + }=State) -> case kz_json:get_value(<<"Process-ID">>, JObj) of AProcId -> - lager:debug("recv sync req from ourselves"), + lager:debug("recv sync req from ourself"), {'next_state', 'sync', State}; _OtherProcId -> lager:debug("recv sync_req from ~s (we are ~s)", [_OtherProcId, AProcId]), @@ -493,8 +540,8 @@ sync('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener {'next_state', 'sync', State} end; sync('cast', {'sync_resp', JObj}, #state{sync_ref=Ref - ,agent_listener=AgentListener - }=State) -> + ,agent_listener=AgentListener + }=State) -> case catch kz_term:to_atom(kz_json:get_value(<<"Status">>, JObj)) of 'sync' -> lager:debug("other agent is in sync too"), @@ -523,28 +570,31 @@ sync({'call', From}, 'status', State) -> {'next_state', 'sync', State, {'reply', From, [{'state', <<"sync">>}]}}; sync({'call', From}, 'current_call', State) -> {'next_state', 'sync', State, {'reply', From, 'undefined'}}; -sync('info', ?NEW_CHANNEL_FROM(CallId), State) -> - lager:debug("sync call_from outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; -sync('info', ?NEW_CHANNEL_TO(CallId, _), State) -> - lager:debug("sync call_to outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; sync('info', {'timeout', Ref, ?SYNC_RESPONSE_MESSAGE}, #state{sync_ref=Ref - ,agent_listener=AgentListener - }=State) when is_reference(Ref) -> + ,agent_listener=AgentListener + }=State) when is_reference(Ref) -> lager:debug("done waiting for sync responses"), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - apply_state_updates(State#state{sync_ref=Ref}); sync('info', {'timeout', Ref, ?RESYNC_RESPONSE_MESSAGE}, #state{sync_ref=Ref}=State) when is_reference(Ref) -> lager:debug("resync timer expired, lets check with the others again"), SyncRef = start_sync_timer(), gen_statem:cast(self(), 'send_sync_event'), {'next_state', 'sync', State#state{sync_ref=SyncRef}}; +sync('info', ?NEW_CHANNEL_FROM(CallId, Number, Name, _), State) -> + lager:debug("sync call_from inbound: ~s", [CallId]), + {'next_state', 'inbound', start_inbound_call_handling(CallId, Number, Name, State), 'hibernate'}; +sync('info', ?NEW_CHANNEL_TO(CallId, Number, Name), State) -> + lager:debug("sync call_to outbound: ~s", [CallId]), + {'next_state', 'outbound', start_outbound_call_handling(CallId, Number, Name, State), 'hibernate'}; sync('info', Evt, State) -> + lager:debug("unhandled event while syncing: ~p", [Evt]), handle_info(Evt, 'sync', State). + + %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ @@ -553,15 +603,12 @@ ready('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener}=State) -> lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Server-ID">>, JObj)]), acdc_agent_listener:send_sync_resp(AgentListener, 'ready', JObj), {'next_state', 'ready', State}; -ready('cast', {'sync_resp', _}, State) -> - {'next_state', 'ready', State}; -ready('cast', {'member_connect_win', JObj}, #state{agent_listener=AgentListener - ,endpoints=OrigEPs - ,agent_listener_id=MyId - ,account_id=AccountId - ,agent_id=AgentId - ,connect_failures=CF - }=State) -> +ready('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=AgentListener + ,endpoints=OrigEPs + ,account_id=AccountId + ,agent_id=AgentId + ,connect_failures=CF + }=State) -> Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, JObj)), CallId = kapps_call:call_id(Call), @@ -574,61 +621,101 @@ ready('cast', {'member_connect_win', JObj}, #state{agent_listener=AgentListener CDRUrl = cdr_url(JObj), RecordingUrl = recording_url(JObj), - case lists:member(MyId, kz_json:get_list_value(<<"Agent-Process-IDs">>, JObj, [])) of - true -> - lager:debug("trying to ring agent ~s to connect to caller in queue ~s", [AgentId, QueueId]), - - case get_endpoints(OrigEPs, Call, AgentId, QueueId) of - {'error', 'no_endpoints'} -> - lager:info("agent ~s has no endpoints assigned; logging agent out", [AgentId]), - acdc_agent_stats:agent_logged_out(AccountId, AgentId), - agent_logout(self()), - acdc_agent_listener:member_connect_retry(AgentListener, JObj), - {'next_state', 'paused', State}; - {'error', _E} -> - lager:debug("can't take the call, skip me: ~p", [_E]), - acdc_agent_listener:member_connect_retry(AgentListener, JObj), - {'next_state', 'ready', State#state{connect_failures=CF+1}}; - {'ok', UpdatedEPs} -> - acdc_agent_listener:bridge_to_member(AgentListener, Call, JObj, UpdatedEPs, CDRUrl, RecordingUrl), - - CIDName = kapps_call:caller_id_name(Call), - CIDNum = kapps_call:caller_id_number(Call), - - acdc_agent_stats:agent_connecting(AccountId, AgentId, CallId, CIDName, CIDNum, QueueId), - lager:info("trying to ring agent endpoints(~p)", [length(UpdatedEPs)]), - lager:debug("notifications for the queue: ~p", [kz_json:get_value(<<"Notifications">>, JObj)]), - {'next_state', 'ringing', State#state{wrapup_timeout=WrapupTimer - ,member_call=Call - ,member_call_id=CallId - ,member_call_start=kz_time:start_time() - ,member_call_queue_id=QueueId - ,caller_exit_key=CallerExitKey - ,endpoints=UpdatedEPs - ,queue_notifications=kz_json:get_value(<<"Notifications">>, JObj) - }} - end; - _ -> - lager:debug("monitoring agent ~s to connect to caller in queue ~s", [AgentId, QueueId]), + lager:debug("trying to ring agent ~s to connect to caller in queue ~s", [AgentId, QueueId]), + + case get_endpoints(OrigEPs, Call, AgentId, QueueId) of + {'error', 'no_endpoints'} -> + lager:info("agent ~s has no endpoints assigned; logging agent out", [AgentId]), + acdc_agent_stats:agent_logged_out(AccountId, AgentId), + agent_logout(self()), + acdc_agent_listener:member_connect_retry(AgentListener, JObj), + {'next_state', 'paused', State}; + {'error', _E} -> + lager:debug("can't take the call, skip me: ~p", [_E]), + acdc_agent_listener:member_connect_retry(AgentListener, JObj), + {'next_state', 'ready', State#state{connect_failures=CF+1}}; + {'ok', UpdatedEPs} -> + acdc_util:bind_to_call_events(Call, AgentListener), + + %% Need to check if a callback is required to the caller + NextState = case kz_json:get_value(<<"Callback-Details">>, JObj) of + 'undefined' -> + acdc_agent_listener:bridge_to_member(AgentListener, Call, JObj, UpdatedEPs, CDRUrl, RecordingUrl), + 'ringing'; + Details -> + acdc_agent_listener:originate_callback_to_agent(AgentListener, Call, JObj, UpdatedEPs, CDRUrl, RecordingUrl, Details), + 'ringing_callback' + end, + + {CIDNumber, CIDName} = acdc_util:caller_id(Call), + + acdc_agent_stats:agent_connecting(AccountId, AgentId, CallId, CIDName, CIDNumber), + lager:info("trying to ring agent endpoints(~p)", [length(UpdatedEPs)]), + lager:debug("notifications for the queue: ~p", [kz_json:get_value(<<"Notifications">>, JObj)]), + {'next_state', NextState, State#state{wrapup_timeout=WrapupTimer + ,member_call=Call + ,member_call_id=CallId + ,member_call_start=kz_time:now() + ,member_call_queue_id=QueueId + ,caller_exit_key=CallerExitKey + ,endpoints=UpdatedEPs + ,queue_notifications=kz_json:get_value(<<"Notifications">>, JObj) + }} + end; +ready('cast', {'member_connect_win', JObj, 'different_node'}, #state{agent_listener=AgentListener + ,endpoints=OrigEPs + ,agent_id=AgentId + ,connect_failures=CF + }=State) -> + Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, JObj)), + CallId = kapps_call:call_id(Call), + + kz_log:put_callid(CallId), + + WrapupTimer = kz_json:get_integer_value(<<"Wrapup-Timeout">>, JObj, 0), + CallerExitKey = kz_json:get_value(<<"Caller-Exit-Key">>, JObj, <<"#">>), + QueueId = kz_json:get_value(<<"Queue-ID">>, JObj), - acdc_agent_listener:monitor_call(AgentListener, Call, CDRUrl, RecordingUrl), + RecordingUrl = recording_url(JObj), - {'next_state', 'ringing', State#state{wrapup_timeout=WrapupTimer + %% Only start monitoring if the agent can actually take the call + case get_endpoints(OrigEPs, Call, AgentId, QueueId) of + {'error', 'no_endpoints'} -> + lager:info("agent ~s has no endpoints assigned; logging agent out", [AgentId]), + {'next_state', 'paused', State}; + {'error', _E} -> + lager:debug("can't take the call, skip me: ~p", [_E]), + {'next_state', 'ready', State#state{connect_failures=CF+1}}; + {'ok', UpdatedEPs} -> + acdc_util:bind_to_call_events(Call, AgentListener), + + acdc_agent_listener:monitor_call(AgentListener, Call, JObj, RecordingUrl), + %% Need to check if a callback is required to the caller + NextState = case kz_json:get_value(<<"Callback-Details">>, JObj) of + 'undefined' -> 'ringing'; + _Details -> 'ringing_callback' + end, + lager:debug("monitoring agent ~s to connect to caller in queue ~s", [AgentId, QueueId]), + {'next_state', NextState, State#state{wrapup_timeout=WrapupTimer + ,member_call=Call ,member_call_id=CallId - ,member_call_start=kz_time:start_time() + ,member_call_start=kz_time:now() ,member_call_queue_id=QueueId ,caller_exit_key=CallerExitKey - ,agent_call_id='undefined' + ,endpoints=UpdatedEPs + ,queue_notifications=kz_json:get_value(<<"Notifications">>, JObj) + ,monitoring = 'true' }} end; -ready('cast', {'member_connect_satisfied', _}, State) -> - lager:info("unexpected connect_satisfied"), +ready('cast', {'member_connect_satisfied', _, _Node}, State) -> + lager:debug("unexpected connect_satisfied in state 'ready'"), {'next_state', 'ready', State}; + ready('cast', {'member_connect_req', _}, #state{max_connect_failures=Max - ,connect_failures=Fails - ,account_id=AccountId - ,agent_id=AgentId - }=State) when is_integer(Max), Fails >= Max -> + ,connect_failures=Fails + ,account_id=AccountId + ,agent_id=AgentId + }=State) when is_integer(Max), Fails >= Max -> lager:info("agent has failed to connect ~b times, logging out", [Fails]), acdc_agent_stats:agent_logged_out(AccountId, AgentId), agent_logout(self()), @@ -637,11 +724,10 @@ ready('cast', {'member_connect_req', JObj}, #state{agent_listener=AgentListener} acdc_agent_listener:member_connect_resp(AgentListener, JObj), {'next_state', 'ready', State}; ready('cast', {'originate_uuid', ACallId, ACtrlQ}, #state{agent_listener=AgentListener}=State) -> - lager:debug("ignoring an outbound call that is the result of a failed originate"), acdc_agent_listener:originate_uuid(AgentListener, ACallId, ACtrlQ), - acdc_agent_listener:channel_hungup(AgentListener, ACallId), {'next_state', 'ready', State}; -ready('cast', {'channel_answered', CallId}, #state{outbound_call_ids=OutboundCallIds}=State) -> +ready('cast', {'channel_answered', JObj}, #state{outbound_call_ids=OutboundCallIds}=State) -> + CallId = call_id(JObj), case lists:member(CallId, OutboundCallIds) of 'true' -> lager:debug("agent picked up outbound call ~s", [CallId]), @@ -653,30 +739,26 @@ ready('cast', {'channel_answered', CallId}, #state{outbound_call_ids=OutboundCal ready('cast', {'channel_unbridged', CallId}, #state{agent_listener=_AgentListener}=State) -> lager:debug("channel unbridged: ~s", [CallId]), {'next_state', 'ready', State}; -ready('cast', {'leg_destroyed', CallId}, #state{agent_listener=_AgentListener}=State) -> +ready('cast',{'leg_destroyed', CallId}, #state{agent_listener=_AgentListener}=State) -> lager:debug("channel unbridged: ~s", [CallId]), {'next_state', 'ready', State}; ready('cast', {'dtmf_pressed', _}, State) -> {'next_state', 'ready', State}; ready('cast', {'originate_failed', _E}, State) -> {'next_state', 'ready', State}; -ready('cast', Evt, State) -> - handle_event(Evt, 'ready', State); -ready({'call', From}, 'status', State) -> - {'next_state', 'ready', State, {'reply', From, [{'state', <<"ready">>}]}}; -ready({'call', From}, 'current_call', State) -> - {'next_state', 'ready', State, {'reply', From, 'undefined'}}; -ready('info', ?NEW_CHANNEL_FROM(CallId), State) -> - lager:debug("ready call_from outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; -ready('info', ?NEW_CHANNEL_TO(CallId, 'undefined'), State) -> - lager:debug("ready call_to outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; -ready('info', ?NEW_CHANNEL_TO(_CallId, _MemberCallId), State) -> +ready('cast', {'playback_stop', _CallId}, State) -> {'next_state', 'ready', State}; -ready('info', ?DESTROYED_CHANNEL(CallId, _Cause), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds - }=State) -> +ready('cast', ?NEW_CHANNEL_FROM(CallId, Number, Name, 'undefined'), State) -> + lager:debug("ready call_from inbound: ~s", [CallId]), + {'next_state', 'inbound', start_inbound_call_handling(CallId, Number, Name, State), 'hibernate'}; +ready('cast', ?NEW_CHANNEL_FROM(CallId,_,_, MemberCallId), State) -> + cancel_if_failed_originate(CallId, MemberCallId, 'ready', State); +ready('cast', ?NEW_CHANNEL_TO(CallId, Number, Name), State) -> + lager:debug("ready call_to outbound: ~s", [CallId]), + {'next_state', 'outbound', start_outbound_call_handling(CallId, Number, Name, State), 'hibernate'}; +ready('cast', ?DESTROYED_CHANNEL(CallId, _Cause), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> case lists:member(CallId, OutboundCallIds) of 'true' -> lager:debug("agent outbound channel ~s down", [CallId]), @@ -687,36 +769,55 @@ ready('info', ?DESTROYED_CHANNEL(CallId, _Cause), #state{agent_listener=AgentLis acdc_agent_listener:channel_hungup(AgentListener, CallId), {'next_state', 'ready', State} end; +ready('cast', Evt, State) -> + handle_event(Evt, 'ready', State); +ready({'call', From}, 'status', State) -> + {'next_state', 'ready', State, {'reply', From, [{'state', <<"ready">>}]}}; +ready({'call', From}, 'current_call', State) -> + {'next_state', 'ready', State, {'reply', From, 'undefined'}}; ready('info', Evt, State) -> handle_info(Evt, 'ready', State). + %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ -spec ringing(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). ringing('cast', {'member_connect_req', _}, State) -> {'next_state', 'ringing', State}; -ringing('cast', {'member_connect_win', JObj}, #state{agent_listener=AgentListener}=State) -> +ringing('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=AgentListener}=State) -> lager:debug("agent won, but can't process this right now (already ringing)"), acdc_agent_listener:member_connect_retry(AgentListener, JObj), {'next_state', 'ringing', State}; -ringing('cast', {'member_connect_satisfied', JObj}, #state{agent_listener=AgentListener - ,member_call_id=MemberCallId - ,account_id=AccountId - ,member_call_queue_id=QueueId - ,agent_id=AgentId - ,connect_failures=Fails - ,max_connect_failures=MaxFails - }=State) -> - lager:info("received connect_satisfied: check if I should hangup: ~p", [JObj]), +ringing('cast', {'member_connect_win', _, 'different_node'}, State) -> + lager:debug("received member_connect_win for different node (ringing)"), + {'next_state', 'ringing', State}; +ringing('cast', {'originate_ready', JObj}, #state{agent_listener=AgentListener}=State) -> + CallId = kz_json:get_value(<<"Call-ID">>, JObj), + + lager:debug("ringing agent's phone with call-id ~s", [CallId]), + acdc_agent_listener:originate_execute(AgentListener, JObj), + {'next_state', 'ringing', State}; +ringing('cast', {'member_connect_satisfied', JObj, Node}, #state{agent_listener=AgentListener + ,member_call_id=MemberCallId + ,account_id=AccountId + ,member_call_queue_id=QueueId + ,agent_id=AgentId + ,connect_failures=Fails + ,max_connect_failures=MaxFails + }=State) -> CallId = kz_json:get_ne_binary_value([<<"Call">>, <<"Call-ID">>], JObj, []), - case CallId =:= MemberCallId of + AcceptedAgentId = kz_json:get_binary_value(<<"Accept-Agent-ID">>, JObj), + lager:info("received connect_satisfied: check if I should hangup:~p MemberCall:~p CallId:~p AgentId:~p AcceptedAgentId:~p", [Node, MemberCallId, CallId, AgentId, AcceptedAgentId]), + case CallId =:= MemberCallId + andalso AgentId /= AcceptedAgentId of true -> - lager:info("hanging up: someother agent replies"), + lager:info("agent ~p hanging up: agent ~p is handling the call", [AgentId, AcceptedAgentId]), acdc_agent_listener:channel_hungup(AgentListener, MemberCallId), - acdc_stats:call_missed(AccountId, QueueId, AgentId, MemberCallId, <<"LOSE_RACE">>), + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, MemberCallId, <<"LOSE_RACE">>), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), State1 = clear_call(State, 'failed'), @@ -727,177 +828,219 @@ ringing('cast', {'member_connect_satisfied', JObj}, #state{agent_listener=AgentL end; _ -> {'next_state', 'ringing', State} end; -ringing('cast', {'originate_ready', JObj}, #state{agent_listener=AgentListener}=State) -> - CallId = kz_json:get_value(<<"Call-ID">>, JObj), - - lager:debug("ringing agent's phone with call-id ~s", [CallId]), - acdc_agent_listener:originate_execute(AgentListener, JObj), - {'next_state', 'ringing', State}; ringing('cast', {'originate_uuid', ACallId, ACtrlQ}, #state{agent_listener=AgentListener}=State) -> lager:debug("recv originate_uuid for agent call ~s(~s)", [ACallId, ACtrlQ]), acdc_agent_listener:originate_uuid(AgentListener, ACallId, ACtrlQ), {'next_state', 'ringing', State}; ringing('cast', {'originate_started', ACallId}, #state{agent_listener=AgentListener - ,member_call_id=MemberCallId - ,member_call=MemberCall - ,account_id=AccountId - ,agent_id=AgentId - ,queue_notifications=Ns - ,member_call_queue_id=QueueId - }=State) -> + ,member_call_id=MemberCallId + ,member_call=MemberCall + ,account_id=AccountId + ,agent_id=AgentId + ,queue_notifications=Ns + }=State) -> lager:debug("originate resp on ~s, connecting to caller", [ACallId]), acdc_agent_listener:member_connect_accepted(AgentListener, ACallId), maybe_notify(Ns, ?NOTIFY_PICKUP, State), - CIDName = kapps_call:caller_id_name(MemberCall), - CIDNum = kapps_call:caller_id_number(MemberCall), + {CIDNumber, CIDName} = acdc_util:caller_id(MemberCall), - acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNum, QueueId), + acdc_util:bind_to_call_events(ACallId, AgentListener), + + acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNumber), {'next_state', 'answered', State#state{agent_call_id=ACallId ,connect_failures=0 }}; ringing('cast', {'originate_failed', E}, #state{agent_listener=AgentListener - ,account_id=AccountId - ,agent_id=AgentId - ,member_call_queue_id=QueueId - ,member_call_id=CallId - ,connect_failures=Fails - ,max_connect_failures=MaxFails - }=State) -> - acdc_agent_listener:member_connect_retry(AgentListener, CallId), - + ,account_id=AccountId + ,agent_id=AgentId + ,member_call_queue_id=QueueId + ,member_call_id=CallId + }=State) -> ErrReason = missed_reason(kz_json:get_value(<<"Error-Message">>, E)), + lager:debug("originate failed (~s), broadcasting", [ErrReason]), + kapi_acdc_agent:publish_shared_originate_failure([{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), - lager:debug("ringing agent failed: ~s", [ErrReason]), + acdc_agent_listener:member_connect_retry(AgentListener, CallId), - acdc_stats:call_missed(AccountId, QueueId, AgentId, CallId, ErrReason), + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, CallId, ErrReason), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - State1 = clear_call(State, 'failed'), - StateName1 = return_to_state(Fails+1, MaxFails), - case StateName1 of - 'paused' -> {'next_state', 'paused', State1}; - 'ready' -> apply_state_updates(State1) - end; + {'next_state', 'ringing', State}; ringing('cast', {'agent_timeout', _JObj}, #state{agent_listener=AgentListener - ,account_id=AccountId - ,agent_id=AgentId - ,member_call_queue_id=QueueId - ,member_call_id=CallId - ,connect_failures=Fails - ,max_connect_failures=MaxFails - }=State) -> + ,account_id=AccountId + ,agent_id=AgentId + ,member_call_queue_id=QueueId + ,member_call_id=CallId + }=State) -> + ErrReason = <<"timeout">>, + lager:debug("agent timeout, publishing originate failed"), + kapi_acdc_agent:publish_shared_originate_failure([{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + acdc_agent_listener:agent_timeout(AgentListener), - lager:debug("recv timeout from queue process"), - acdc_stats:call_missed(AccountId, QueueId, AgentId, CallId, <<"timeout">>), + + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, CallId, ErrReason), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - State1 = clear_call(State, 'failed'), - StateName1 = return_to_state(Fails+1, MaxFails), - case StateName1 of - 'paused' -> {'next_state', 'paused', State1}; - 'ready' -> apply_state_updates(State1) - end; + + {'next_state', 'ringing', State}; +ringing('cast', {'playback_stop', _CallId}, State) -> + {'next_state', 'ringing', State}; ringing('cast', {'channel_bridged', MemberCallId}, #state{member_call_id=MemberCallId - ,member_call=MemberCall - ,agent_listener=AgentListener - ,account_id=AccountId - ,agent_id=AgentId - ,queue_notifications=Ns - ,member_call_queue_id=QueueId - }=State) -> + ,member_call=MemberCall + ,agent_listener=AgentListener + ,account_id=AccountId + ,agent_id=AgentId + ,queue_notifications=Ns + }=State) -> lager:debug("agent phone has been connected to caller"), acdc_agent_listener:member_connect_accepted(AgentListener), maybe_notify(Ns, ?NOTIFY_PICKUP, State), - CIDName = kapps_call:caller_id_name(MemberCall), - CIDNum = kapps_call:caller_id_number(MemberCall), + {CIDNumber, CIDName} = acdc_util:caller_id(MemberCall), - acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNum, QueueId), + acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNumber), {'next_state', 'answered', State#state{connect_failures=0}}; ringing('cast', {'channel_bridged', _CallId}, State) -> {'next_state', 'ringing', State}; -ringing('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=DTMF - ,agent_listener=AgentListener - ,agent_call_id=AgentCallId - }=State) when is_binary(DTMF) -> - lager:debug("caller exit key pressed: ~s", [DTMF]), - acdc_agent_listener:channel_hungup(AgentListener, AgentCallId), - - acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - - apply_state_updates(clear_call(State, 'ready')); -ringing('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=_ExitKey}=State) -> - lager:debug("caller pressed ~s, exit key is ~s", [DTMF, _ExitKey]), - {'next_state', 'ringing', State}; -ringing('cast', {'channel_answered', MemberCallId}, #state{member_call_id=MemberCallId}=State) -> - lager:debug("caller's channel answered"), - {'next_state', 'ringing', State}; -ringing('cast', {'channel_answered', OtherCallId}, #state{account_id=AccountId - ,agent_id=AgentId - ,member_call=MemberCall - ,member_call_id=MemberCallId - ,agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds - ,member_call_queue_id=QueueId - }=State) -> - case lists:member(OtherCallId, OutboundCallIds) of - 'true' -> - lager:debug("agent picked up outbound call ~s instead of the queue call ~s", [OtherCallId, MemberCallId]), - acdc_agent_listener:hangup_call(AgentListener), - {'next_state', 'outbound', start_outbound_call_handling(OtherCallId, clear_call(State, 'ready')), 'hibernate'}; - 'false' -> - lager:debug("recv answer for ~s, probably the agent's call", [OtherCallId]), - - CIDName = kapps_call:caller_id_name(MemberCall), - CIDNum = kapps_call:caller_id_number(MemberCall), - - acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNum, QueueId), - - acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_RED_SOLID), - - {'next_state', 'answered', State#state{agent_call_id=OtherCallId - ,connect_failures=0 - }} +ringing('cast', {'channel_answered', JObj}, #state{member_call_id=MemberCallId + ,agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + case call_id(JObj) of + MemberCallId -> + lager:debug("caller's channel answered"), + {'next_state', 'ringing', State}; + OtherCallId -> + case lists:member(OtherCallId, OutboundCallIds) of + 'true' -> + lager:debug("agent picked up outbound call ~s instead of the queue call ~s", [OtherCallId, MemberCallId]), + acdc_agent_listener:hangup_call(AgentListener), + {'next_state', 'outbound', start_outbound_call_handling(OtherCallId, clear_call(State, 'ready')), 'hibernate'}; + 'false' -> + lager:debug("recv answer for ~s, probably the agent's call", [OtherCallId]), + {'next_state', 'ringing', State#state{agent_call_id=OtherCallId}} + end end; ringing('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener}=State) -> lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Process-ID">>, JObj)]), acdc_agent_listener:send_sync_resp(AgentListener, 'ringing', JObj), {'next_state', 'ringing', State}; -ringing('cast', {'sync_resp', _}, State) -> - {'next_state', 'ringing', State}; ringing('cast', {'originate_resp', ACallId}, #state{agent_listener=AgentListener - ,member_call_id=MemberCallId - ,member_call=MemberCall - ,account_id=AccountId - ,agent_id=AgentId - ,queue_notifications=Ns - ,member_call_queue_id=QueueId - }=State) -> - lager:debug("originate resp on ~s, connecting to caller", [ACallId]), - acdc_agent_listener:member_connect_accepted(AgentListener, ACallId), + ,member_call_id=MemberCallId + ,member_call=MemberCall + ,account_id=AccountId + ,agent_id=AgentId + ,queue_notifications=Ns + }=State) -> + lager:debug("originate resp on ~s, broadcasting", [ACallId]), + kapi_acdc_agent:publish_shared_call_id([{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Agent-Call-ID">>, ACallId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), maybe_notify(Ns, ?NOTIFY_PICKUP, State), - CIDName = kapps_call:caller_id_name(MemberCall), - CIDNum = kapps_call:caller_id_number(MemberCall), + {CIDNumber, CIDName} = acdc_util:caller_id(MemberCall), + + acdc_agent_listener:member_connect_accepted(AgentListener, ACallId), + acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNumber), + + {'next_state', 'ringing', State}; +ringing('cast', {'shared_failure', _JObj}, #state{connect_failures=Fails + ,max_connect_failures=MaxFails + }=State) -> + lager:debug("shared originate failure"), + + NewServerRefState = clear_call(State, 'failed'), + NextState = return_to_state(Fails+1, MaxFails), + case NextState of + 'paused' -> {'next_state', 'paused', NewServerRefState}; + 'ready' -> apply_state_updates(NewServerRefState) + end; +ringing('cast', {'shared_call_id', JObj}, #state{agent_listener=AgentListener}=State) -> + ACallId = kz_json:get_value(<<"Agent-Call-ID">>, JObj), + + lager:debug("shared call id ~s acquired, connecting to caller", [ACallId]), - acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNum, QueueId), + acdc_util:bind_to_call_events(ACallId, AgentListener), + acdc_agent_listener:monitor_connect_accepted(AgentListener, ACallId), {'next_state', 'answered', State#state{agent_call_id=ACallId ,connect_failures=0 }}; -ringing('cast', {'leg_created', _CallId}, State) -> +ringing('info', ?NEW_CHANNEL_FROM(CallId,_,_, MemberCallId), #state{member_call_id=MemberCallId}=State) -> + lager:debug("new channel ~s for agent", [CallId]), + {'next_state', 'ringing', State}; +ringing('info', ?NEW_CHANNEL_FROM(CallId, Number, Name,_), #state{agent_listener=AgentListener}=State) -> + lager:debug("ringing call_from inbound: ~s", [CallId]), + acdc_agent_listener:hangup_call(AgentListener), + {'next_state', 'inbound', start_inbound_call_handling(CallId, Number, Name, clear_call(State, 'ready')), 'hibernate'}; +ringing('info', ?NEW_CHANNEL_TO(CallId,_,_), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + lager:debug("ringing call_to outbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'ringing', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; +ringing('cast', {'leg_created', _, _}, State) -> {'next_state', 'ringing', State}; ringing('cast', {'leg_destroyed', _CallId}, State) -> {'next_state', 'ringing', State}; ringing('cast', {'usurp_control', _CallId}, State) -> {'next_state', 'ringing', State}; +ringing('cast', ?DESTROYED_CHANNEL(AgentCallId, _Cause), #state{agent_listener=AgentListener + ,agent_call_id=AgentCallId + ,connect_failures=Fails + ,max_connect_failures=MaxFails + }=State) -> + lager:debug("agent's channel (~s) down", [AgentCallId]), + + acdc_agent_listener:hangup_call(AgentListener), + + acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), + + NewServerRefState = clear_call(State, 'failed'), + NextState = return_to_state(Fails+1, MaxFails), + case NextState of + 'paused' -> {'next_state', 'paused', NewServerRefState}; + 'ready' -> apply_state_updates(NewServerRefState) + end; +ringing('cast', ?DESTROYED_CHANNEL(MemberCallId, _Cause), #state{agent_listener=AgentListener + ,account_id=AccountId + ,member_call_id=MemberCallId + ,member_call_queue_id=QueueId + }=State) -> + lager:debug("caller's channel (~s) has gone down, stop agent's call: ~s", [MemberCallId, _Cause]), + acdc_agent_listener:channel_hungup(AgentListener, MemberCallId), + + _ = acdc_stats:call_abandoned(AccountId, QueueId, MemberCallId, ?ABANDON_HANGUP), + + acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), + apply_state_updates(clear_call(State, 'ready')); +ringing('cast', ?DESTROYED_CHANNEL(CallId, _Cause), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + case lists:member(CallId, OutboundCallIds) of + 'true' -> + lager:debug("agent outbound channel ~s down", [CallId]), + acdc_util:unbind_from_call_events(CallId, AgentListener), + {'next_state', 'ringing', State#state{outbound_call_ids=lists:delete(CallId, OutboundCallIds)}}; + 'false' -> + lager:debug("unexpected channel ~s down", [CallId]), + {'next_state', 'ringing', State} + end; ringing('cast', Evt, State) -> handle_event(Evt, 'ringing', State); ringing({'call', From}, 'status', #state{member_call_id=MemberCallId @@ -914,443 +1057,771 @@ ringing({'call', From}, 'current_call', #state{member_call=Call {'next_state', 'ringing', State ,{'reply', From, current_call(Call, 'ringing', QueueId, 'undefined')} }; -ringing('info', ?NEW_CHANNEL_FROM(CallId), #state{agent_listener=AgentListener}=State) -> - lager:debug("ringing call_from outbound: ~s", [CallId]), - acdc_agent_listener:hangup_call(AgentListener), - {'next_state', 'outbound', start_outbound_call_handling(CallId, clear_call(State, 'ready')), 'hibernate'}; -ringing('info', ?NEW_CHANNEL_TO(CallId, 'undefined'), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds +ringing('info', Evt, State) -> + handle_info(Evt, 'ringing', State). + +%%------------------------------------------------------------------------------ +%% @private +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec ringing_callback(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). +ringing_callback('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener}=State) -> + lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Server-ID">>, JObj)]), + acdc_agent_listener:send_sync_resp(AgentListener, 'ringing_callback', JObj), + {'next_state', 'ringing_callback', State}; +ringing_callback('cast', {'originate_uuid', ACallId, ACtrlQ}, #state{agent_listener=AgentListener}=State) -> + lager:debug("recv originate_uuid for agent call ~s(~s)", [ACallId, ACtrlQ]), + acdc_agent_listener:originate_uuid(AgentListener, ACallId, ACtrlQ), + {'next_state', 'ringing_callback', State}; +ringing_callback('cast', {'originate_resp', ACallId}, #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_call_id=MemberCallId + ,member_call=MemberCall + ,queue_notifications=Ns + ,agent_call_id=ACallId + ,agent_callback_call=ACall + }=State) -> + lager:debug("originate resp on ~s, broadcasting", [ACallId]), + kapi_acdc_agent:publish_shared_call_id([{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Agent-Call-ID">>, ACallId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + + maybe_notify(Ns, ?NOTIFY_PICKUP, State), + + {CIDNumber, CIDName} = acdc_util:caller_id(MemberCall), + + %% TODO: remove if unnecessary + %% acdc_util:b_bind_to_call_events(ACallId, AgentListener), + acdc_agent_listener:member_callback_accepted(AgentListener, ACall), + acdc_agent_stats:agent_connected(AccountId, AgentId, MemberCallId, CIDName, CIDNumber), + {'next_state', 'ringing_callback', State#state{connect_failures=0}}; +ringing_callback('cast', {'originate_failed', JObj}, #state{agent_listener=AgentListener + ,account_id=AccountId + ,agent_id=AgentId + ,member_call_queue_id=QueueId + ,member_call_id=CallId + }=State) -> + ErrReason = missed_reason(kz_json:get_value(<<"Error-Message">>, JObj)), + lager:debug("originate failed (~s), broadcasting", [ErrReason]), + kapi_acdc_agent:publish_shared_originate_failure([{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + + acdc_agent_listener:member_connect_retry(AgentListener, CallId), + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, CallId, ErrReason), + acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), + + {'next_state', 'ringing_callback', State}; +ringing_callback('cast', {'shared_failure', _JObj}, #state{connect_failures=Fails + ,max_connect_failures=MaxFails + }=State) -> + lager:debug("shared originate failure"), + + NewServerRefState = clear_call(State, 'failed'), + NextState = return_to_state(Fails+1, MaxFails), + case NextState of + 'paused' -> {'next_state', 'paused', NewServerRefState}; + 'ready' -> apply_state_updates(NewServerRefState) + end; +%% For the monitoring processes, fake the agent_callback_call so playback_stop isn't ignored +ringing_callback('cast', {'shared_call_id', JObj}, #state{agent_callback_call='undefined'}=State) -> + ACallId = kz_json:get_value(<<"Agent-Call-ID">>, JObj), + ACall = kapps_call:set_call_id(ACallId, kapps_call:new()), + ringing_callback('cast', {'shared_call_id', JObj}, State#state{agent_callback_call=ACall}); +ringing_callback('cast', {'shared_call_id', JObj}, #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds} + =State) -> + ACallId = kz_json:get_value(<<"Agent-Call-ID">>, JObj), + + lager:debug("shared call id ~s acquired, connecting to caller", [ACallId]), + + acdc_util:b_bind_to_call_events(ACallId, AgentListener), + acdc_agent_listener:monitor_connect_accepted(AgentListener, ACallId), + + {'next_state', 'ringing_callback', State#state{agent_call_id=ACallId + ,connect_failures=0 + ,outbound_call_ids=[ACallId | lists:delete(ACallId, OutboundCallIds)] + }}; +ringing_callback('cast', {'member_connect_satisfied', JObj, Node}, #state{agent_listener=AgentListener + ,member_call_id=MemberCallId + ,account_id=AccountId + ,member_call_queue_id=QueueId + ,agent_id=AgentId + ,connect_failures=Fails + ,max_connect_failures=MaxFails + }=State) -> + CallId = kz_json:get_ne_binary_value([<<"Call">>, <<"Call-ID">>], JObj, []), + AcceptedAgentId = kz_json:get_binary_value(<<"Accept-Agent-ID">>, JObj), + lager:info("received connect_satisfied: check if I should hangup:~p MemberCall:~p CallId:~p AgentId:~p AcceptedAgentId:~p", [Node, MemberCallId, CallId, AgentId, AcceptedAgentId]), + case CallId =:= MemberCallId + andalso AgentId /= AcceptedAgentId of + true -> + lager:info("agent ~p hanging up: agent ~p is handling the call", [AgentId, AcceptedAgentId]), + acdc_agent_listener:channel_hungup(AgentListener, MemberCallId), + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, MemberCallId, <<"LOSE_RACE">>), + acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), + + State1 = clear_call(State, 'failed'), + StateName1 = return_to_state(Fails+1, MaxFails), + case StateName1 of + 'paused' -> {'next_state', 'paused', State1}; + 'ready' -> apply_state_updates(State1) + end; + _ -> {'next_state', 'ringing_callback', State} + end; +ringing_callback('cast', {'channel_answered', JObj}, State) -> + CallId = call_id(JObj), + lager:debug("agent answered phone on ~s", [CallId]), + ACall = kapps_call:set_account_id(kz_json:get_value([<<"Custom-Channel-Vars">>, <<"Account-ID">>], JObj), kapps_call:from_json(JObj)), + {'next_state', 'ringing_callback', State#state{agent_call_id=CallId + ,agent_callback_call=ACall + }}; +ringing_callback('cast', {'playback_stop', _}, #state{agent_callback_call='undefined'}=State) -> + {'next_state', 'ringing_callback', State}; +ringing_callback('cast', {'playback_stop', ACallId}, #state{member_call=Call + ,agent_call_id=ACallId + ,member_call_id=MemberCallId + ,monitoring='true' + }=State) -> + {'next_state', 'awaiting_callback', State#state{member_original_call=Call + ,member_original_call_id=MemberCallId + }}; +ringing_callback('cast', {'playback_stop', ACallId}, #state{agent_listener=AgentListener + ,member_call=Call + ,member_call_id=MemberCallId + ,agent_callback_call=AgentCallbackCall + ,agent_call_id=ACallId + ,outbound_call_ids=OutboundCallIds + }=State) -> + NewMemberCallId = acdc_agent_listener:originate_callback_return(AgentListener, AgentCallbackCall), + kz_log:put_callid(NewMemberCallId), + acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_RED_SOLID), + + %% Preserve old call information for sake of stats + {'next_state', 'awaiting_callback', State#state{member_call_id=NewMemberCallId + ,member_original_call=Call + ,member_original_call_id=MemberCallId + ,outbound_call_ids=[NewMemberCallId | lists:delete(NewMemberCallId, OutboundCallIds)] + }}; +ringing_callback('cast', {'usurp_control', _}, State) -> + {'next_state', 'ringing_callback', State}; +ringing_callback('cast', ?DESTROYED_CHANNEL(ACallId, _Cause), #state{agent_call_id=ACallId + ,connect_failures=Fails + ,max_connect_failures=MaxFails + ,monitoring='true' }=State) -> - lager:debug("ringing call_to outbound: ~s", [CallId]), - acdc_util:bind_to_call_events(CallId, AgentListener), - {'next_state', 'ringing', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; -ringing('info', ?NEW_CHANNEL_TO(CallId, MemberCallId), #state{member_call_id=MemberCallId}=State) -> - lager:debug("new channel ~s for agent", [CallId]), - {'next_state', 'ringing', State}; -ringing('info', ?NEW_CHANNEL_TO(CallId, _MemberCallId), #state{agent_listener=AgentListener}=State) -> - lager:debug("found a uuid ~s that was from a previous queue call", [CallId]), - acdc_agent_listener:channel_hungup(AgentListener, CallId), - {'next_state', 'ringing', State}; -ringing('info', ?DESTROYED_CHANNEL(AgentCallId, Cause), #state{agent_listener=AgentListener - ,agent_call_id=AgentCallId - ,account_id=AccountId - ,agent_id=AgentId - ,member_call_queue_id=QueueId - ,member_call_id=MemberCallId - ,connect_failures=Fails - ,max_connect_failures=MaxFails - }=State) -> - lager:debug("ringing agent failed: timeout on ~s ~s", [AgentCallId, Cause]), - - acdc_agent_listener:member_connect_retry(AgentListener, MemberCallId), - acdc_agent_listener:channel_hungup(AgentListener, MemberCallId), + NewServerRefState = clear_call(State, 'failed'), + NextState = return_to_state(Fails+1, MaxFails), + case NextState of + 'paused' -> {'next_state', 'paused', NewServerRefState}; + 'ready' -> apply_state_updates(NewServerRefState) + end; +ringing_callback('cast', ?DESTROYED_CHANNEL(ACallId, Cause), #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_call_id=CallId + ,member_call_queue_id=QueueId + ,agent_call_id=ACallId + ,max_connect_failures=MaxFails + ,connect_failures=Fails + }=State) -> + lager:info("agent hungup ~s while they were supposed to wait for a callback", [ACallId]), + + acdc_agent_listener:member_connect_retry(AgentListener, CallId), - acdc_stats:call_missed(AccountId, QueueId, AgentId, MemberCallId, Cause), + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, CallId, Cause), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - State1 = clear_call(State, 'failed'), - StateName1 = return_to_state(Fails+1, MaxFails), - case StateName1 of - 'paused' -> {'next_state', 'paused', State1}; - 'ready' -> apply_state_updates(State1) + NewServerRefState = clear_call(State, 'failed'), + NextState = return_to_state(Fails+1, MaxFails), + case NextState of + 'paused' -> {'next_state', 'paused', NewServerRefState}; + 'ready' -> apply_state_updates(NewServerRefState) end; -ringing('info', ?DESTROYED_CHANNEL(MemberCallId, _Cause), #state{agent_listener=AgentListener - ,member_call_id=MemberCallId - }=State) -> - lager:debug("caller's channel (~s) has gone down, stop agent's call: ~s", [MemberCallId, _Cause]), - acdc_agent_listener:channel_hungup(AgentListener, MemberCallId), +ringing_callback('cast', ?NEW_CHANNEL_FROM(CallId,Name,Number, MemberCallId), #state{member_call_id=MemberCallId + } + = State) -> + lager:debug("new inbound channel ~s to agent from ~s(~s)", [CallId, Number, Name]), + {'next_state', 'ringing_callback', State}; +ringing_callback('cast', ?NEW_CHANNEL_TO(CallId,Name,Number), State) -> + lager:debug("new outbound channel ~s from agent ~s(~s)", [CallId, Number, Name]), + {'next_state', 'ringing_callback', State}; +ringing_callback('cast', Evt, State) -> + handle_event(Evt, 'ringing_callback', State); +ringing_callback({'call', From}, 'status', State) -> + {'next_state', 'ringing_callback', State + ,{'reply', From, [{'state', <<"ringing_callback">>} + ]}}; +ringing_callback({'call', From}, 'current_call', #state{member_call=Call + ,member_call_queue_id=QueueId + }=State) -> + {'next_state', 'ringing_callback', State + ,{'reply', From, current_call(Call, 'ringing_callback', QueueId, 'undefined')} + }; +ringing_callback('info', Evt, State) -> + handle_info(Evt, 'ringing_callback', State). +%%------------------------------------------------------------------------------ +%% @private +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec awaiting_callback(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). +awaiting_callback('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener}=State) -> + lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Server-ID">>, JObj)]), + acdc_agent_listener:send_sync_resp(AgentListener, 'awaiting_callback', JObj), + {'next_state', 'awaiting_callback', State}; +awaiting_callback('cast', {'originate_uuid', MemberCallbackCallId, CtrlQ}, #state{member_callback_candidates=Candidates}=State) -> + lager:debug("recv originate_uuid for member callback call ~s(~s)", [MemberCallbackCallId, CtrlQ]), + {'next_state', 'awaiting_callback', State#state{member_callback_candidates=props:set_value(MemberCallbackCallId, CtrlQ, Candidates)}}; +awaiting_callback('cast', {'originate_resp', _}, State) -> + {'next_state', 'awaiting_callback', State}; +awaiting_callback('cast', {'originate_failed', JObj}, #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_call=MemberCall + ,member_original_call_id=OriginalMemberCallId + ,member_call_queue_id=QueueId + ,agent_call_id=ACallId + }=State) -> + ErrReason = missed_reason(kz_json:get_value(<<"Error-Message">>, JObj)), + lager:debug("originate failed (~s), broadcasting", [ErrReason]), + kapi_acdc_agent:publish_shared_originate_failure([{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + acdc_agent_listener:member_connect_accepted(AgentListener, ACallId, MemberCall), + _ = acdc_stats:call_handled(AccountId, QueueId, OriginalMemberCallId, AgentId), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - apply_state_updates(clear_call(State, 'ready')); -ringing('info', ?DESTROYED_CHANNEL(CallId, _Cause), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds - }=State) -> - case lists:member(CallId, OutboundCallIds) of - 'true' -> - lager:debug("agent outbound channel ~s down", [CallId]), - acdc_util:unbind_from_call_events(CallId, AgentListener), - {'next_state', 'ringing', State#state{outbound_call_ids=lists:delete(CallId, OutboundCallIds)}}; - 'false' -> - lager:debug("unexpected channel ~s down", [CallId]), - {'next_state', 'ringing', State} + {'next_state', 'wrapup', State#state{wrapup_ref=hangup_call(State, 'member')}}; +awaiting_callback('cast', {'shared_failure', _}, #state{agent_listener=AgentListener + ,agent_call_id=ACallId + }=State) -> + lager:debug("shared originate failure"), + acdc_agent_listener:channel_hungup(AgentListener, ACallId), + + {'next_state', 'wrapup', State#state{wrapup_ref=hangup_call(State, 'member')}}; +awaiting_callback('cast', {'shared_call_id', JObj}, #state{agent_listener=AgentListener + ,member_call=MemberCall + ,member_callback_candidates=Candidates + ,monitoring='true' + }=State) -> + NewMemberCallId = kz_json:get_value(<<"Member-Call-ID">>, JObj), + acdc_util:bind_to_call_events(NewMemberCallId, AgentListener), + NewMemberCall = kapps_call:exec([fun(Call) -> kapps_call:set_account_id(kapps_call:account_id(MemberCall), Call) end + ,fun(Call) -> kapps_call:set_call_id(NewMemberCallId, Call) end + ], kapps_call:new()), + + {'next_state', 'answered', State#state{member_call=NewMemberCall + ,member_call_id=NewMemberCallId + ,member_callback_candidates=props:set_value(NewMemberCallId, NewMemberCall, Candidates) + ,connect_failures=0 + }}; +awaiting_callback('cast', {'shared_call_id', _}, State) -> + {'next_state', 'answered', State}; +awaiting_callback('cast', {'channel_answered', JObj}=Evt, #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_callback_candidates=Candidates + ,member_original_call=OriginalMemberCall + ,member_original_call_id=OriginalMemberCallId + ,member_call_queue_id=QueueId + ,agent_call_id=ACallId + }=State) -> + CallId = call_id(JObj), + case props:get_value(CallId, Candidates) of + 'undefined' -> awaiting_callback_unhandled_event(Evt, State); + CtrlQ -> + lager:info("member answered phone on ~s", [CallId]), + + %% Update control queue so call recordings work + %% Also preserve some metadata included in call recordings + MemberCall = kapps_call:exec([fun(Call) -> kapps_call:set_account_id(AccountId, Call) end + ,fun(Call) -> kapps_call:set_control_queue(CtrlQ, Call) end + ,fun(Call) -> kapps_call:set_custom_channel_var(<<"Queue-ID">>, QueueId, Call) end + ], kapps_call:from_json(JObj)), + + kapi_acdc_agent:publish_shared_call_id([{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Member-Call-ID">>, CallId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + + %% Notify the queue_fsm that the call is now fully accepted + acdc_agent_listener:member_connect_accepted(AgentListener, ACallId, MemberCall), + + {CIDNumber, CIDName} = acdc_util:caller_id(OriginalMemberCall), + + acdc_agent_stats:agent_connected(AccountId, AgentId, OriginalMemberCallId, CIDName, CIDNumber), + _ = acdc_stats:call_handled(AccountId, QueueId, OriginalMemberCallId, AgentId), + + {'next_state', 'awaiting_callback', State#state{member_call=MemberCall + ,member_call_id=CallId + ,connect_failures=0 + }} end; -ringing('info', Evt, State) -> - handle_info(Evt, 'ringing', State). +awaiting_callback('cast', {'leg_created', CallId, OtherLegCallId}=Evt, #state{agent_listener=AgentListener + ,member_callback_candidates=Candidates + }=State) -> + case props:get_value(CallId, Candidates) of + 'undefined' -> awaiting_callback_unhandled_event(Evt, State); + CtrlQ -> + %% Unbind from originate UUID, bind to bridge of loopback + lager:debug("rebinding from ~s to ~s due to loopback", [CallId, OtherLegCallId]), + acdc_agent_listener:rebind_events(AgentListener, CallId, OtherLegCallId), + + Candidates1 = props:set_value(OtherLegCallId, CtrlQ, []), + {'next_state', 'awaiting_callback', State#state{member_callback_candidates=Candidates1}} + end; +awaiting_callback('cast', {'leg_destroyed', _}, State) -> + {'next_state', 'awaiting_callback', State}; +awaiting_callback('cast', {'playback_stop', _JObj}, State) -> + {'next_state', 'awaiting_callback', State}; +awaiting_callback('cast', {'usurp_control', _}, State) -> + {'next_state', 'awaiting_callback', State}; +awaiting_callback('cast', ?DESTROYED_CHANNEL(OriginalMemberCallId, _Cause), #state{member_original_call_id=OriginalMemberCallId + ,monitoring='true' + }=State) -> + {'next_state', 'awaiting_callback', State}; +awaiting_callback('cast', ?DESTROYED_CHANNEL(ACallId, _Cause), #state{agent_listener=AgentListener + ,agent_call_id=ACallId + ,monitoring='true' + }=State) -> + lager:debug("agent hungup ~s while waiting for a callback to connect", [ACallId]), + acdc_agent_listener:channel_hungup(AgentListener, ACallId), + {'next_state', 'wrapup', State#state{wrapup_ref=hangup_call(State, 'member')}}; +awaiting_callback('cast', ?DESTROYED_CHANNEL(ACallId, _Cause), #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_call=MemberCall + ,member_original_call_id=OriginalMemberCallId + ,member_call_queue_id=QueueId + ,agent_call_id=ACallId + }=State) -> + lager:info("agent hungup ~s while waiting for a callback to connect", [ACallId]), -%%------------------------------------------------------------------------------ + acdc_agent_listener:member_connect_accepted(AgentListener, ACallId, MemberCall), + _ = acdc_stats:call_handled(AccountId, QueueId, OriginalMemberCallId, AgentId), + acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), + {'next_state', 'wrapup', State#state{wrapup_ref=hangup_call(State, 'member')}}; +awaiting_callback('cast', ?DESTROYED_CHANNEL(CallId, Cause), State) -> + maybe_member_no_answer(CallId, Cause, State); +awaiting_callback('cast', ?NEW_CHANNEL_TO(CallId,_,_), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + lager:debug("answered call_to outbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'awaiting_callback', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; +awaiting_callback('cast', Evt, State) -> + handle_event(Evt, 'awaiting_callback', State); +awaiting_callback({'call', From}, 'status', #state{member_call_id=MemberCallId + ,agent_call_id=ACallId + }=State) -> + {'next_state', 'awaiting_callback', State + ,{'reply', From, [{'state', <<"awaiting_callback">>} + ,{'member_call_id', MemberCallId} + ,{'agent_call_id', ACallId} + ]}}; +awaiting_callback({'call', From}, 'current_call', #state{member_call=Call + ,member_call_queue_id=QueueId + }=State) -> + {'next_state', 'awaiting_callback', State + ,{'reply', From, current_call(Call, 'awaiting_callback', QueueId, 'undefined')}}; +awaiting_callback('info', Evt, State) -> + handle_info(Evt, 'awaiting_callback', State). + +-spec awaiting_callback_unhandled_event(any(), state()) -> + {'next_state', 'awaiting_callback', state()}. +awaiting_callback_unhandled_event(Evt, State) -> + lager:debug("unhandled event while awaiting callback: ~p", [Evt]), + {'next_state', 'awaiting_callback', State}. + +-spec maybe_member_no_answer(kz_term:ne_binary(), kz_term:ne_binary(), state()) -> + {'next_state', atom(), state()}. +maybe_member_no_answer(CallId, Cause, #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_call=MemberCall + ,member_callback_candidates=Candidates + ,member_original_call_id=OriginalMemberCallId + ,member_call_queue_id=QueueId + ,agent_call_id=ACallId + }=State) -> + _ = case props:get_value(CallId, Candidates) of + 'undefined' -> 'ok'; + _ -> + ErrReason = missed_reason(Cause), + lager:debug("member did not answer callback ~s (~s)", [CallId, ErrReason]), + + acdc_agent_listener:member_connect_accepted(AgentListener, ACallId, MemberCall), + + acdc_stats:call_handled(AccountId, QueueId, OriginalMemberCallId, AgentId) + end, + {'next_state', 'awaiting_callback', State}. + +%%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ -spec answered(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). answered('cast', {'member_connect_req', _}, State) -> {'next_state', 'answered', State}; -answered('cast', {'member_connect_win', JObj}, #state{agent_listener=AgentListener}=State) -> +answered('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=AgentListener}=State) -> lager:debug("agent won, but can't process this right now (on the phone with someone)"), acdc_agent_listener:member_connect_retry(AgentListener, JObj), - {'next_state', 'answered', State}; -answered('cast', {'member_connect_satisfied', _}, State) -> - lager:info("unexpected connect_satisfied"), +answered('cast', {'member_connect_win', _, 'different_node'}, State) -> + lager:debug("received member_connect_win for different node (answered)"), + {'next_state', 'answered', State}; +answered('cast', {'member_connect_satisfied', _, Node}, State) -> + lager:debug("received member_connect_satisfied for ~p (answered)", [Node]), {'next_state', 'answered', State}; answered('cast', {'dialplan_error', _App}, #state{agent_listener=AgentListener - ,account_id=AccountId - ,agent_id=AgentId - ,member_call_queue_id=QueueId - ,member_call_id=CallId - ,agent_call_id=ACallId - }=State) -> + ,account_id=AccountId + ,agent_id=AgentId + ,member_call_queue_id=QueueId + ,member_call_id=CallId + ,agent_call_id=ACallId + }=State) -> lager:debug("connecting agent to caller failed(~p), clearing call", [_App]), acdc_agent_listener:channel_hungup(AgentListener, ACallId), acdc_agent_listener:member_connect_retry(AgentListener, CallId), - acdc_stats:call_missed(AccountId, QueueId, AgentId, CallId, <<"dialplan_error">>), + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, original_call_id(State), <<"dialplan_error">>), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), apply_state_updates(clear_call(State, 'ready')); -answered('cast', {'channel_bridged', CallId}, #state{member_call_id=CallId - ,agent_listener=AgentListener - ,queue_notifications=Ns - }=State) -> - lager:debug("agent has connected to member"), - acdc_agent_listener:member_connect_accepted(AgentListener), - maybe_notify(Ns, ?NOTIFY_PICKUP, State), - {'next_state', 'answered', State}; -answered('cast', {'channel_bridged', CallId}, #state{agent_call_id=CallId - ,agent_listener=AgentListener - ,queue_notifications=Ns - }=State) -> - lager:debug("agent has connected (~s) to caller", [CallId]), - acdc_agent_listener:member_connect_accepted(AgentListener, CallId), - maybe_notify(Ns, ?NOTIFY_PICKUP, State), +answered('cast', {'playback_stop', _JObj}, State) -> {'next_state', 'answered', State}; -answered('cast', {'channel_replaced', JObj}, #state{agent_listener=AgentListener}=State) -> - CallId = kz_call_event:call_id(JObj), - ReplacedBy = kz_call_event:replaced_by(JObj), - acdc_agent_listener:rebind_events(AgentListener, CallId, ReplacedBy), - kz_log:put_callid(ReplacedBy), - lager:info("channel ~s replaced by ~s", [CallId, ReplacedBy]), - {'next_state', 'answered', State#state{member_call_id = ReplacedBy}}; answered('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener - ,member_call_id=CallId - }=State) -> + ,member_call_id=CallId + }=State) -> lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Process-ID">>, JObj)]), acdc_agent_listener:send_sync_resp(AgentListener, 'answered', JObj, [{<<"Call-ID">>, CallId}]), {'next_state', 'answered', State}; -answered('cast', {'sync_resp', _}, State) -> - {'next_state', 'answered', State}; answered('cast', {'channel_unbridged', CallId}, #state{member_call_id=CallId}=State) -> lager:info("caller channel ~s unbridged", [CallId]), {'next_state', 'answered', State}; answered('cast', {'channel_unbridged', CallId}, #state{agent_call_id=CallId}=State) -> lager:info("agent channel unbridged"), {'next_state', 'answered', State}; -answered('cast', {'channel_answered', MemberCallId}, #state{member_call_id=MemberCallId}=State) -> - lager:debug("member's channel has answered"), - {'next_state', 'answered', State}; -answered('cast', {'channel_answered', AgentCallId}, #state{agent_call_id=AgentCallId}=State) -> - lager:debug("agent's channel ~s has answered", [AgentCallId]), - {'next_state', 'answered', State}; -answered('cast', {'channel_answered', OtherCallId}=Evt, #state{outbound_call_ids=OutboundCallIds}=State) -> - case lists:member(OtherCallId, OutboundCallIds) of - 'true' -> - lager:debug("agent answered outbound call ~s", [OtherCallId]), +answered('cast', {'channel_answered', JObj}=Evt, #state{agent_call_id=AgentCallId + ,member_call_id=MemberCallId + ,outbound_call_ids=OutboundCallIds + }=State) -> + case call_id(JObj) of + AgentCallId -> + lager:debug("agent's channel ~s has answered", [AgentCallId]), {'next_state', 'answered', State}; - 'false' -> - lager:debug("unexpected event while answered: ~p", [Evt]), - {'next_state', 'answered', State} + MemberCallId -> + lager:debug("member's channel has answered"), + {'next_state', 'answered', State}; + OtherCallId -> + case lists:member(OtherCallId, OutboundCallIds) of + 'true' -> + lager:debug("agent answered outbound call ~s", [OtherCallId]), + {'next_state', 'answered', State}; + 'false' -> + lager:debug("unexpected event while answered: ~p", [Evt]), + {'next_state', 'answered', State} + end end; +answered('cast', {'channel_bridged', _}, State) -> + {'next_state', 'answered', State}; +answered('cast', {'channel_unbridged', _}, State) -> + {'next_state', 'answered', State}; +answered('cast', {'channel_transferee', Transferor, Transferee}, #state{account_id=AccountId + ,agent_id=AgentId + ,member_call_id=Transferor + ,member_call_queue_id=QueueId + ,queue_notifications=Ns + ,agent_call_id=Transferee + }=State) -> + lager:info("caller transferred the agent"), + _ = acdc_stats:call_processed(AccountId, QueueId, AgentId, Transferor, 'member'), + maybe_notify(Ns, ?NOTIFY_HANGUP, State), + {'next_state', 'outbound', start_outbound_call_handling(Transferee, clear_call(State, 'ready'))}; +answered('cast', {'channel_transferee', _, _}, State) -> + {'next_state', 'answered', State}; +answered('cast', {'channel_replaced', _}, State) -> + {'next_state', 'answered', State}; answered('cast', {'originate_started', _CallId}, State) -> {'next_state', 'answered', State}; -answered('cast', {'leg_created', _CallId}, State) -> +answered('cast', {'leg_created', _, _}, State) -> {'next_state', 'answered', State}; answered('cast', {'usurp_control', _CallId}, State) -> {'next_state', 'answered', State}; -answered('cast', Evt, State) -> - handle_event(Evt, 'answered', State); -answered({'call', From}, 'status', #state{member_call_id=MemberCallId - ,agent_call_id=ACallId - }=State) -> - {'next_state', 'answered', State - ,{'reply', From, [{'state', <<"answered">>} - ,{'member_call_id', MemberCallId} - ,{'agent_call_id', ACallId} - ]}}; -answered({'call', From}, 'current_call', #state{member_call=Call - ,member_call_start=Start - ,member_call_queue_id=QueueId - }=State) -> - {'next_state', 'answered', State - ,{'reply', From, current_call(Call, 'answered', QueueId, Start)} - }; -answered('info', ?NEW_CHANNEL_FROM(CallId), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds +answered('cast', ?DESTROYED_CHANNEL(CallId, Cause), #state{member_call_id=CallId + ,outbound_call_ids=[] }=State) -> - lager:debug("answered call_from outbound: ~s", [CallId]), - acdc_util:bind_to_call_events(CallId, AgentListener), - {'next_state', 'answered', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; -answered('info', ?NEW_CHANNEL_TO(CallId, 'undefined'), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds - }=State) -> - lager:debug("answered call_to outbound: ~s", [CallId]), - acdc_util:bind_to_call_events(CallId, AgentListener), - {'next_state', 'answered', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; -answered('info', ?NEW_CHANNEL_TO(CallId, MemberCallId), #state{member_call_id=MemberCallId}=State) -> - lager:debug("new channel ~s for agent", [CallId]), - {'next_state', 'answered', State}; -answered('info', ?DESTROYED_CHANNEL(CallId, Cause), #state{member_call_id=CallId - ,outbound_call_ids=[] - }=State) -> lager:debug("caller's channel hung up: ~s", [Cause]), {'next_state', 'wrapup', State#state{wrapup_ref=hangup_call(State, 'member')}}; -answered('info', ?DESTROYED_CHANNEL(CallId, _Cause), #state{account_id=AccountId - ,agent_id=AgentId - ,agent_listener=AgentListener - ,member_call_id=CallId - ,member_call_queue_id=QueueId - ,queue_notifications=Ns - ,outbound_call_ids=[OutboundCallId|_] - }=State) -> +answered('cast', ?DESTROYED_CHANNEL(CallId, _Cause), #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_call_id=CallId + ,member_call_queue_id=QueueId + ,queue_notifications=Ns + ,outbound_call_ids=[OutboundCallId|_] + }=State) -> lager:debug("caller's channel hung up, but there are still some outbounds"), - acdc_stats:call_processed(AccountId, QueueId, AgentId, CallId, 'member'), + _ = acdc_stats:call_processed(AccountId, QueueId, AgentId, original_call_id(State), 'member'), acdc_agent_listener:channel_hungup(AgentListener, CallId), maybe_notify(Ns, ?NOTIFY_HANGUP, State), - {'next_state', 'outbound', start_outbound_call_handling(OutboundCallId, clear_call(State, 'ready')), 'hibernate'}; -answered('info', ?DESTROYED_CHANNEL(CallId, Cause), #state{agent_call_id=CallId - ,outbound_call_ids=[] - }=State) -> +%% {'next_state', 'outbound', start_outbound_call_handling(OutboundCallId, clear_call(State, 'ready')), 'hibernate'}; + {'next_state', 'outbound', start_outbound_call_handling(OutboundCallId, State), 'hibernate'}; + +answered('cast', ?DESTROYED_CHANNEL(CallId, Cause), #state{agent_call_id=CallId + ,outbound_call_ids=[] + }=State) -> lager:debug("agent's channel has hung up: ~s", [Cause]), {'next_state', 'wrapup', State#state{wrapup_ref=hangup_call(State, 'agent')}}; -answered('info', ?DESTROYED_CHANNEL(CallId, _Cause), #state{account_id=AccountId - ,agent_id=AgentId - ,agent_listener=AgentListener - ,member_call_id=MemberCallId - ,member_call_queue_id=QueueId - ,queue_notifications=Ns - ,agent_call_id=CallId - ,outbound_call_ids=[OutboundCallId|_] - }=State) -> +answered('cast', ?DESTROYED_CHANNEL(CallId, _Cause), #state{account_id=AccountId + ,agent_id=AgentId + ,agent_listener=AgentListener + ,member_call_id=MemberCallId + ,member_call_queue_id=QueueId + ,queue_notifications=Ns + ,agent_call_id=CallId + ,outbound_call_ids=[OutboundCallId|_] + }=State) -> lager:debug("agent's channel hung up, but there are still some outbounds"), - acdc_stats:call_processed(AccountId, QueueId, AgentId, CallId, 'agent'), + _ = acdc_stats:call_processed(AccountId, QueueId, AgentId, original_call_id(State), 'agent'), acdc_agent_listener:channel_hungup(AgentListener, MemberCallId), maybe_notify(Ns, ?NOTIFY_HANGUP, State), - {'next_state', 'outbound', start_outbound_call_handling(OutboundCallId, clear_call(State, 'ready')), 'hibernate'}; -answered('info', ?DESTROYED_CHANNEL(CallId, _Cause), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds - }=State) -> +%% {'next_state', 'outbound', start_outbound_call_handling(OutboundCallId, clear_call(State, 'ready')), 'hibernate'}; + {'next_state', 'outbound', start_outbound_call_handling(OutboundCallId, State), 'hibernate'}; +%% {'next_state', 'answered', State, 'hibernate'}; + +answered('cast', ?DESTROYED_CHANNEL(CallId, _Cause), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> case lists:member(CallId, OutboundCallIds) of 'true' -> lager:debug("agent outbound channel ~s down", [CallId]), acdc_util:unbind_from_call_events(CallId, AgentListener), {'next_state', 'answered', State#state{outbound_call_ids=lists:delete(CallId, OutboundCallIds)}}; + % {'next_state', 'answered', State}; 'false' -> lager:debug("unexpected channel ~s down", [CallId]), {'next_state', 'answered', State} end; +answered('cast', ?NEW_CHANNEL_FROM(CallId,_,_, MemberCallId), #state{member_call_id=MemberCallId}=State) -> + lager:debug("new channel ~s for agent", [CallId]), + {'next_state', 'answered', State}; +answered('cast', ?NEW_CHANNEL_FROM(CallId,_,_,_), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + lager:debug("answered call_from inbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'answered', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; +answered('cast', ?NEW_CHANNEL_TO(CallId,_,_), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + lager:debug("answered call_to outbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'answered', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; +answered('cast', Evt, State) -> + handle_event(Evt, 'answered', State); + +answered({'call', From}, 'status', #state{member_call_id=MemberCallId + ,agent_call_id=ACallId + }=State) -> + {'next_state', 'answered', State + ,{'reply', From, [{'state', <<"answered">>} + ,{'member_call_id', MemberCallId} + ,{'agent_call_id', ACallId} + ]}}; +answered({'call', From}, 'current_call', #state{member_call=Call + ,member_call_start=Start + ,member_call_queue_id=QueueId + }=State) -> + {'next_state', 'answered', State + ,{'reply', From, current_call(Call, 'answered', QueueId, Start)}}; answered('info', Evt, State) -> handle_info(Evt, 'answered', State). + %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ -spec wrapup(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). -wrapup('cast', {'pause', Timeout}, #state{account_id=AccountId - ,agent_id=AgentId - ,agent_listener=AgentListener - }=State) -> - lager:debug("recv status update: pausing for up to ~b s", [Timeout]), - Ref = start_pause_timer(Timeout), - acdc_agent_stats:agent_paused(AccountId, AgentId, Timeout), - acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_RED_FLASH), - - {'next_state', 'paused', State#state{pause_ref=Ref}}; wrapup('cast', {'member_connect_req', _}, State) -> {'next_state', 'wrapup', State#state{wrapup_timeout=0}}; -wrapup('cast', {'member_connect_win', JObj}, #state{agent_listener=AgentListener}=State) -> +wrapup('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=AgentListener}=State) -> lager:debug("agent won, but can't process this right now (in wrapup)"), acdc_agent_listener:member_connect_retry(AgentListener, JObj), {'next_state', 'wrapup', State#state{wrapup_timeout=0}}; -wrapup('cast', {'member_connect_satisfied', _}, State) -> - lager:info("unexpected connect_satisfied"), +wrapup('cast', {'member_connect_win', _, 'different_node'}, State) -> + lager:debug("received member_connect_win for different node (wrapup)"), + {'next_state', 'wrapup', State#state{wrapup_timeout=0}}; +wrapup('cast', {'member_connect_satisfied', _, Node}, State) -> + lager:info("unexpected connect_satisfied for ~p", [Node]), {'next_state', 'wrapup', State}; + + wrapup('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener - ,wrapup_ref=Ref - }=State) -> + ,wrapup_ref=Ref + }=State) -> lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Process-ID">>, JObj)]), acdc_agent_listener:send_sync_resp(AgentListener, 'wrapup', JObj, [{<<"Time-Left">>, time_left(Ref)}]), {'next_state', 'wrapup', State}; -wrapup('cast', {'sync_resp', _}, State) -> +wrapup('cast',{'channel_bridged', _}, State) -> + {'next_state', 'wrapup', State}; +wrapup('cast',{'channel_unbridged', _}, State) -> {'next_state', 'wrapup', State}; -wrapup('cast', {'channel_hungup', _, _}, State) -> +wrapup('cast',{'channel_transferee', _, _}, State) -> {'next_state', 'wrapup', State}; -wrapup('cast', {'leg_destroyed', CallId}, #state{agent_listener=AgentListener}=State) -> +wrapup('cast',{'leg_destroyed', CallId}, #state{agent_listener=AgentListener}=State) -> lager:debug("leg ~s destroyed", [CallId]), acdc_agent_listener:channel_hungup(AgentListener, CallId), {'next_state', 'wrapup', State}; -wrapup('cast', {'originate_resp', _}, State) -> +wrapup('cast',{'playback_stop', _}, State) -> + {'next_state', 'wrapup', State}; +wrapup('cast',{'originate_resp', _}, State) -> + {'next_state', 'wrapup', State}; +wrapup('cast', ?DESTROYED_CHANNEL(_, _), State) -> {'next_state', 'wrapup', State}; wrapup('cast', Evt, State) -> handle_event(Evt, 'wrapup', State); -wrapup({'call', From}, 'status', #state{wrapup_ref=Ref}=State) -> +wrapup({call, From}, 'status', #state{wrapup_ref=Ref}=State) -> {'next_state', 'wrapup', State ,{'reply', From, [{'state', <<"wrapup">>} - ,{'wrapup_left', time_left(Ref)} - ]}}; -wrapup({'call', From}, 'current_call', #state{member_call=Call - ,member_call_start=Start - ,member_call_queue_id=QueueId - }=State) -> + ,{'wrapup_left', time_left(Ref)} + ]}}; +wrapup({call, From}, 'current_call', #state{member_call=Call + ,member_call_start=Start + ,member_call_queue_id=QueueId + }=State) -> {'next_state', 'wrapup', State - ,{'reply', From, current_call(Call, 'wrapup', QueueId, Start)} - }; -wrapup('info', ?NEW_CHANNEL_FROM(CallId), State) -> - lager:debug("wrapup call_from outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; -wrapup('info', ?NEW_CHANNEL_TO(CallId, _), State) -> - lager:debug("wrapup call_to outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; + ,{'reply', From, current_call(Call, 'wrapup', QueueId, Start)}}; wrapup('info', {'timeout', Ref, ?WRAPUP_FINISHED}, #state{wrapup_ref=Ref - ,agent_listener=AgentListener - }=State) -> + ,agent_listener=AgentListener + }=State) -> lager:debug("wrapup timer expired, ready for action!"), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - apply_state_updates(clear_call(State, 'ready')); +wrapup('info', ?NEW_CHANNEL_FROM(CallId, Number, Name,_), State) -> + lager:debug("wrapup call_from inbound: ~s", [CallId]), + {'next_state', 'inbound', start_inbound_call_handling(CallId, Number, Name, State), 'hibernate'}; +wrapup('info', ?NEW_CHANNEL_TO(CallId, Number, Name), State) -> + lager:debug("wrapup call_to outbound: ~s", [CallId]), + {'next_state', 'outbound', start_outbound_call_handling(CallId, Number, Name, State), 'hibernate'}; wrapup('info', Evt, State) -> - handle_info(Evt, 'wrapup', State). + handle_info(Evt, 'wrapup', State#state{wrapup_timeout=0}). %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ -spec paused(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). paused('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener - ,pause_ref=Ref - }=State) -> + ,pause_ref=Ref + }=State) -> lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Process-ID">>, JObj)]), acdc_agent_listener:send_sync_resp(AgentListener, 'paused', JObj, [{<<"Time-Left">>, time_left(Ref)}]), {'next_state', 'paused', State}; -paused('cast', {'sync_resp', _}, State) -> - {'next_state', 'paused', State}; paused('cast', {'member_connect_req', _}, State) -> {'next_state', 'paused', State}; -paused('cast', {'member_connect_win', JObj}, #state{agent_listener=AgentListener}=State) -> +paused('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=AgentListener}=State) -> lager:debug("agent won, but can't process this right now"), acdc_agent_listener:member_connect_retry(AgentListener, JObj), - {'next_state', 'paused', State}; -paused('cast', {'member_connect_satisfied', _}, State) -> - lager:info("unexpected connect_satisfied"), +paused('cast', {'member_connect_win', _, 'different_node'}, State) -> + lager:debug("received member_connect_win for different node (paused)"), + {'next_state', 'paused', State}; +paused('cast', {'member_connect_satisfied', _, Node}, State) -> + lager:info("unexpected connect_satisfied for ~p", [Node]), {'next_state', 'paused', State}; paused('cast', {'originate_uuid', ACallId, ACtrlQ}, #state{agent_listener=AgentListener}=State) -> - lager:debug("ignoring an outbound call that is the result of a failed originate"), acdc_agent_listener:originate_uuid(AgentListener, ACallId, ACtrlQ), - acdc_agent_listener:channel_hungup(AgentListener, ACallId), {'next_state', 'paused', State}; paused('cast', Evt, State) -> handle_event(Evt, 'paused', State); -paused({'call', From}, 'status', #state{pause_ref=Ref}=State) -> +paused({call, From}, 'status', #state{pause_ref=Ref}=State) -> {'next_state', 'paused', State ,{'reply', From, [{'state', <<"paused">>} - ,{'pause_left', time_left(Ref)} - ]}}; -paused({'call', From}, 'current_call', State) -> - {'next_state', 'paused', State, {'reply', From, 'undefined'}}; -paused('info', ?NEW_CHANNEL_FROM(CallId), State) -> - lager:debug("paused call_from outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; -paused('info', ?NEW_CHANNEL_TO(CallId, 'undefined'), State) -> - lager:debug("paused call_to outbound: ~s", [CallId]), - {'next_state', 'outbound', start_outbound_call_handling(CallId, State), 'hibernate'}; -paused('info', ?NEW_CHANNEL_TO(_CallId, _MemberCallId), State) -> - {'next_state', 'paused', State}; + ,{'pause_left', time_left(Ref)} + ]}}; +paused({call, From}, 'current_call', State) -> + {'next_state', 'paused', State + ,{'reply', From, 'undefined'}}; paused('info', {'timeout', Ref, ?PAUSE_MESSAGE}, #state{pause_ref=Ref - ,agent_listener=AgentListener - }=State) when is_reference(Ref) -> + ,agent_listener=AgentListener + }=State) when is_reference(Ref) -> lager:debug("pause timer expired, putting agent back into action"), - acdc_agent_listener:update_agent_status(AgentListener, <<"resume">>), - acdc_agent_listener:send_status_resume(AgentListener), - acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - apply_state_updates(clear_call(State#state{sync_ref='undefined'}, 'ready')); +paused('info', ?NEW_CHANNEL_FROM(CallId,_,_, MemberCallId), #state{member_call_id = MemberCallId} = State) -> + cancel_if_failed_originate(CallId, MemberCallId, 'paused', State); +paused('info', ?NEW_CHANNEL_FROM(CallId, Number, Name,_), State) -> + lager:debug("paused call_from inbound: ~s", [CallId]), + {'next_state', 'inbound', start_inbound_call_handling(CallId, Number, Name, State), 'hibernate'}; +paused('info', ?NEW_CHANNEL_TO(CallId,Number, Name), State) -> + lager:debug("paused call_to outbound: ~s", [CallId]), + {'next_state', 'outbound', start_outbound_call_handling(CallId, Number, Name, State), 'hibernate'}; paused('info', Evt, State) -> handle_info(Evt, 'paused', State). %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ -spec outbound(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). -outbound('cast', {'member_connect_win', JObj}, #state{agent_listener=AgentListener}=State) -> +outbound('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener}=State) -> + lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Server-ID">>, JObj)]), + acdc_agent_listener:send_sync_resp(AgentListener, 'outbound', JObj), + {'next_state', 'outbound', State}; +outbound('cast',{'playback_stop', _JObj}, State) -> + {'next_state', 'outbound', State}; +outbound('cast',{'member_connect_win', JObj, 'same_node'}, #state{agent_listener=AgentListener}=State) -> lager:debug("agent won, but can't process this right now (on outbound call)"), acdc_agent_listener:member_connect_retry(AgentListener, JObj), {'next_state', 'outbound', State}; -outbound('cast', {'member_connect_satisfied', _}, State) -> - lager:info("unexpected connect_satisfied"), +outbound('cast',{'member_connect_win', _, 'different_node'}, State) -> + lager:debug("received member_connect_win for different node (outbound)"), + {'next_state', 'outbound', State}; +outbound('cast',{'member_connect_satisfied', _, Node}, State) -> + lager:info("unexpected connect_satisfied for ~p", [Node]), {'next_state', 'wrapup', State}; -outbound('cast', {'originate_uuid', ACallId, ACtrlQ}, #state{agent_listener=AgentListener}=State) -> - lager:debug("ignoring an outbound call that is the result of a failed originate"), +outbound('cast',{'originate_uuid', ACallId, ACtrlQ}, #state{agent_listener=AgentListener}=State) -> acdc_agent_listener:originate_uuid(AgentListener, ACallId, ACtrlQ), - acdc_agent_listener:channel_hungup(AgentListener, ACallId), - {'next_state', 'outbound', State}; -outbound('cast', {'originate_failed', _E}, State) -> - {'next_state', 'outbound', State}; -outbound('cast', {'member_connect_req', _}, State) -> {'next_state', 'outbound', State}; -outbound('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener}=State) -> - lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Process-ID">>, JObj)]), - acdc_agent_listener:send_sync_resp(AgentListener, 'outbound', JObj), +outbound('cast',{'originate_failed', _E}, State) -> {'next_state', 'outbound', State}; -outbound('cast', {'sync_resp', _}, State) -> +outbound('cast',{'member_connect_req', _}, State) -> {'next_state', 'outbound', State}; -outbound('cast', {'leg_created', _}, State) -> +outbound('cast',{'leg_created', _, _}, State) -> {'next_state', 'outbound', State}; outbound('cast', {'channel_answered', _}, State) -> {'next_state', 'outbound', State}; -outbound('cast', {'channel_bridged', _}, State) -> +outbound('cast',{'channel_bridged', _}, State) -> {'next_state', 'outbound', State}; -outbound('cast', {'channel_unbridged', _}, State) -> +outbound('cast',{'channel_unbridged', _}, State) -> {'next_state', 'outbound', State}; -outbound('cast', {'leg_destroyed', _CallId}, State) -> +outbound('cast',{'channel_replaced', _}, State) -> {'next_state', 'outbound', State}; -outbound('cast', {'usurp_control', _CallId}, State) -> +outbound('cast',{'leg_destroyed', _CallId}, State) -> {'next_state', 'outbound', State}; -outbound('cast', Evt, State) -> - handle_event(Evt, 'outbound', State); -outbound({'call', From}, 'status', #state{wrapup_ref=Ref - ,outbound_call_ids=OutboundCallIds - }=State) -> - {'next_state', 'outbound', State - ,{'reply', From, [{'state', <<"outbound">>} - ,{'wrapup_left', time_left(Ref)} - ,{'outbound_call_id', hd(OutboundCallIds)} - ]}}; -outbound({'call', From}, 'current_call', State) -> - {'next_state', 'outbound', State, {'reply', From, 'undefined'}}; -outbound('info', ?NEW_CHANNEL_FROM(CallId), #state{agent_listener=AgentListener +outbound('cast',{'usurp_control', _CallId}, State) -> + {'next_state', 'outbound', State}; +outbound('cast', ?DESTROYED_CHANNEL(CallId, Cause), #state{agent_listener=AgentListener ,outbound_call_ids=OutboundCallIds }=State) -> - lager:debug("outbound call_from outbound: ~s", [CallId]), - acdc_util:bind_to_call_events(CallId, AgentListener), - {'next_state', 'outbound', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; -outbound('info', ?NEW_CHANNEL_TO(CallId, _), #state{outbound_call_ids=[CallId]}=State) -> - {'next_state', 'outbound', State}; -outbound('info', ?NEW_CHANNEL_TO(CallId, 'undefined'), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds - }=State) -> - lager:debug("outbound call_to outbound: ~s", [CallId]), - acdc_util:bind_to_call_events(CallId, AgentListener), - {'next_state', 'outbound', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; -outbound('info', ?NEW_CHANNEL_TO(_CallId, _MemberCallId), State) -> - {'next_state', 'outbound', State}; -outbound('info', ?DESTROYED_CHANNEL(CallId, Cause), #state{agent_listener=AgentListener - ,outbound_call_ids=OutboundCallIds - }=State) -> acdc_agent_listener:channel_hungup(AgentListener, CallId), case lists:member(CallId, OutboundCallIds) of 'true' -> @@ -1360,6 +1831,35 @@ outbound('info', ?DESTROYED_CHANNEL(CallId, Cause), #state{agent_listener=AgentL lager:debug("unexpected channel ~s down", [CallId]), {'next_state', 'outbound', State} end; +outbound('cast', ?NEW_CHANNEL_FROM(CallId,_,_, MemberCallId), #state{member_call_id = MemberCallId} = State) -> + cancel_if_failed_originate(CallId, MemberCallId, 'outbound', State); +outbound('cast', ?NEW_CHANNEL_FROM(CallId,_,_,_), #state{outbound_call_ids=[CallId]}=State) -> + {'next_state', 'outbound', State}; +outbound('cast', ?NEW_CHANNEL_FROM(CallId,_,_,_), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + lager:debug("outbound call_from outbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'outbound', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; +outbound('cast', ?NEW_CHANNEL_TO(CallId,_,_), #state{agent_listener=AgentListener + ,outbound_call_ids=OutboundCallIds + }=State) -> + lager:debug("outbound call_to outbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'outbound', State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}}; +outbound('cast', Evt, State) -> + handle_event(Evt, 'outbound', State); +outbound({call, From}, 'status', #state{wrapup_ref=Ref + ,outbound_call_ids=OutboundCallIds + }=State) -> + {'next_state', 'outbound', State + ,{'reply', From, [{'state', <<"outbound">>} + ,{'wrapup_left', time_left(Ref)} + ,{'outbound_call_id', hd(OutboundCallIds)} + ]}}; +outbound({call, From}, 'current_call', State) -> + {'next_state', 'outbound', State + ,{'reply', From, 'undefined'}}; outbound('info', {'timeout', Ref, ?PAUSE_MESSAGE}, #state{pause_ref=Ref}=State) -> lager:debug("pause timer expired while outbound"), {'next_state', 'outbound', State#state{pause_ref='undefined'}}; @@ -1369,10 +1869,108 @@ outbound('info', {'timeout', WRef, ?WRAPUP_FINISHED}, #state{wrapup_ref=WRef}=St outbound('info', Evt, State) -> handle_info(Evt, 'outbound', State). + %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ +-spec inbound(gen_statem:event_type(), any(), state()) -> kz_types:handle_fsm_ret(state()). +inbound('cast', {'sync_req', JObj}, #state{agent_listener=AgentListener}=State) -> + lager:debug("recv sync_req from ~s", [kz_json:get_value(<<"Server-ID">>, JObj)]), + acdc_agent_listener:send_sync_resp(AgentListener, 'inbound', JObj), + {'next_state', 'inbound', State}; +inbound('cast', {'playback_stop', _JObj}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=AgentListener}=State) -> + lager:debug("agent won, but can't process this right now (on inbound call)"), + acdc_agent_listener:member_connect_retry(AgentListener, JObj), + {'next_state', 'inbound', State}; +inbound('cast', {'member_connect_win', _, 'different_node'}, State) -> + lager:debug("received member_connect_win for different node (inbound)"), + {'next_state', 'inbound', State}; +inbound('cast', {'member_connect_satisfied', _, Node}, State) -> + lager:info("unexpected connect_satisfied for ~p", [Node]), + {'next_state', 'wrapup', State}; +inbound('cast', {'originate_uuid', ACallId, ACtrlQ}, #state{agent_listener=AgentListener}=State) -> + acdc_agent_listener:originate_uuid(AgentListener, ACallId, ACtrlQ), + {'next_state', 'inbound', State}; +inbound('cast', {'originate_failed', _E}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'member_connect_req', _}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'leg_created', _, _}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'channel_answered', _}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'channel_bridged', _}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'channel_unbridged', _}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'channel_replaced', _}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'leg_destroyed', _CallId}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', {'usurp_control', _CallId}, State) -> + {'next_state', 'inbound', State}; +inbound('cast', ?DESTROYED_CHANNEL(CallId, Cause), #state{agent_listener=AgentListener + ,inbound_call_ids=InboundCallIds + }=State) -> + acdc_agent_listener:channel_hungup(AgentListener, CallId), + case lists:member(CallId, InboundCallIds) of + 'true' -> + lager:debug("agent inbound channel ~s down: ~s", [CallId, Cause]), + inbound_hungup(State#state{inbound_call_ids=lists:delete(CallId, InboundCallIds)}); + 'false' -> + lager:debug("unexpected channel ~s down", [CallId]), + {'next_state', 'inbound', State} + end; +inbound('cast', ?NEW_CHANNEL_FROM(CallId,_,_, MemberCallId), #state{member_call_id = MemberCallId} = State) -> + cancel_if_failed_originate(CallId, MemberCallId, 'inbound', State); +inbound('cast', ?NEW_CHANNEL_FROM(CallId,_,_,_), #state{inbound_call_ids=[CallId]}=State) -> + {'next_state', 'inbound', State}; +inbound('cast', ?NEW_CHANNEL_FROM(CallId,_,_,_), #state{agent_listener=AgentListener + ,inbound_call_ids=InboundCallIds + }=State) -> + lager:debug("inbound call_from inbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'inbound', State#state{inbound_call_ids=[CallId | lists:delete(CallId, InboundCallIds)]}}; +inbound('cast', ?NEW_CHANNEL_TO(CallId,_,_), #state{agent_listener=AgentListener + ,inbound_call_ids=InboundCallIds + }=State) -> + lager:debug("inbound call_to inbound: ~s", [CallId]), + acdc_util:bind_to_call_events(CallId, AgentListener), + {'next_state', 'inbound', State#state{inbound_call_ids=[CallId | lists:delete(CallId, InboundCallIds)]}}; +inbound('cast', Evt, State) -> + handle_event(Evt, 'inbound', State); +inbound({call, From}, 'status', #state{wrapup_ref=Ref + ,inbound_call_ids=InboundCallIds + }=State) -> + {'next_state', 'inbound', State + ,{'reply', From, [{'state', <<"inbound">>} + ,{'wrapup_left', time_left(Ref)} + ,{'inbound_call_id', hd(InboundCallIds)} + ]}}; +inbound({call, From}, 'current_call', State) -> + {'next_state', 'inbound', State + ,{'reply', From, 'undefined'}}; +inbound('info', {'timeout', Ref, ?PAUSE_MESSAGE}, #state{pause_ref=Ref}=State) -> + lager:debug("pause timer expired while inbound"), + {'next_state', 'inbound', State#state{pause_ref='undefined'}}; +inbound('info', {'timeout', WRef, ?WRAPUP_FINISHED}, #state{wrapup_ref=WRef}=State) -> + lager:debug("wrapup timer ended while on inbound call"), + {'next_state', 'inbound', State#state{wrapup_ref='undefined'}, 'hibernate'}; +inbound('info', Evt, State) -> + handle_info(Evt, 'inbound', State). + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Whenever a gen_ServerRef receives an event sent using +%% gen_statem:cast/2, this function is called to handle +%% the event. +%% +%% @end +%%------------------------------------------------------------------------------ -spec handle_event(any(), atom(), state()) -> kz_types:handle_fsm_ret(state()). handle_event({'agent_logout'}=Event, StateName, #state{agent_state_updates=Queue}=State) -> case valid_state_for_logout(StateName) of @@ -1390,13 +1988,31 @@ handle_event({'resume'}=Event, StateName, #state{agent_state_updates=Queue}=Stat lager:debug("recv resume during ~p, delaying", [StateName]), NewQueue = [Event | Queue], {'next_state', StateName, State#state{agent_state_updates=NewQueue}}; -handle_event({'pause', Timeout}=Event, 'ready', #state{agent_state_updates=Queue}=State) -> +handle_event({'pause', Timeout, Alias}, 'ringing_callback', State) -> + handle_event({'pause', Timeout, Alias}, 'ringing', State); +handle_event({'pause', Timeout, Alias}, 'ringing', #state{agent_listener=AgentListener + ,account_id=AccountId + ,agent_id=AgentId + ,member_call_queue_id=QueueId + }=State) -> + %% Give up the current ringing call + acdc_agent_listener:hangup_call(AgentListener), + lager:debug("stopping ringing agent in order to move to pause"), + _ = acdc_stats:call_missed(AccountId, QueueId, AgentId, original_call_id(State), <<"agent pausing">>), + NewServerRefState = clear_call(State, 'failed'), + %% After clearing we are basically 'ready' state, pause from that state + handle_event({'pause', Timeout, Alias}, 'ready', NewServerRefState); +handle_event({'pause', 0, _}=Event, 'ready', #state{agent_state_updates=Queue}=State) -> + lager:debug("recv status update:, pausing for up to infinity s"), + NewQueue = [Event | Queue], + apply_state_updates(State#state{agent_state_updates=NewQueue}); +handle_event({'pause', Timeout, _}=Event, 'ready', #state{agent_state_updates=Queue}=State) -> lager:debug("recv status update: pausing for up to ~b s", [Timeout]), NewQueue = [Event | Queue], apply_state_updates(State#state{agent_state_updates=NewQueue}); -handle_event({'pause', Timeout}, 'paused', State) -> - handle_event({'pause', Timeout}, 'ready', State); -handle_event({'pause', _}=Event, StateName, #state{agent_state_updates=Queue}=State) -> +handle_event({'pause', Timeout, Alias}, 'paused', State) -> + handle_event({'pause', Timeout, Alias}, 'ready', State); +handle_event({'pause', _, _}=Event, StateName, #state{agent_state_updates=Queue}=State) -> lager:debug("recv pause during ~p, delaying", [StateName]), NewQueue = [Event | Queue], {'next_state', StateName, State#state{agent_state_updates=NewQueue}}; @@ -1418,7 +2034,9 @@ handle_event({'update_presence', _, _}=Event, StateName, #state{agent_state_upda NewQueue = [Event | Queue], {'next_state', StateName, State#state{agent_state_updates=NewQueue}}; handle_event({'refresh', AgentJObj}, StateName, #state{agent_listener=AgentListener}=State) -> - acdc_agent_listener:refresh_config(AgentListener, kz_json:get_value(<<"queues">>, AgentJObj), StateName), + acdc_agent_listener:refresh_config(AgentListener + ,AgentJObj + ,StateName), {'next_state', StateName, State}; handle_event('load_endpoints', StateName, #state{agent_listener='undefined'}=State) -> lager:debug("agent proc not ready, not loading endpoints yet"), @@ -1444,16 +2062,21 @@ handle_event('load_endpoints', StateName, #state{agent_id=AgentId {'ok', EPs} -> {'next_state', StateName, State#state{endpoints=EPs}}; {'error', E} -> {'stop', E, State} end; -handle_event(Event, StateName, State) -> - lager:debug("unhandled message in state ~s: ~p", [StateName, Event]), +handle_event(_Event, StateName, State) -> + lager:debug("unhandled event in state ~s: ~p", [StateName, _Event]), {'next_state', StateName, State}. %%------------------------------------------------------------------------------ -%% @doc +%% @private +%% @doc This function is called by a gen_ServerRef when it receives any +%% message other than a synchronous or asynchronous event +%% (or a system message). +%% %% @end %%------------------------------------------------------------------------------ -spec handle_info(any(), atom(), state()) -> kz_types:handle_fsm_ret(state()). -handle_info({'timeout', _Ref, ?SYNC_RESPONSE_MESSAGE}, StateName, State) -> +handle_info({'timeout', _Ref, ?SYNC_RESPONSE_MESSAGE}=Msg, StateName, State) -> + gen_statem:cast(self(), Msg), {'next_state', StateName, State}; handle_info({'endpoint_edited', EP}, StateName, #state{endpoints=EPs ,account_id=AccountId @@ -1493,20 +2116,24 @@ handle_info({'endpoint_created', EP}, StateName, #state{endpoints=EPs {'next_state', StateName, State#state{endpoints=maybe_add_endpoint(EPId, EP, EPs, AccountId)}, 'hibernate'} end end; -handle_info(?NEW_CHANNEL_FROM(_CallId), StateName, State) -> +handle_info(?NEW_CHANNEL_FROM(_CallId,_,_,_)=Evt, StateName, State) -> + gen_statem:cast(self(), Evt), {'next_state', StateName, State}; -handle_info(?NEW_CHANNEL_TO(_CallId, _), StateName, State) -> +handle_info(?NEW_CHANNEL_TO(_CallId,_,_)=Evt, StateName, State) -> + gen_statem:cast(self(), Evt), {'next_state', StateName, State}; -handle_info(?DESTROYED_CHANNEL(_, _), StateName, State) -> +handle_info(?DESTROYED_CHANNEL(_, _)=Evt, StateName, State) -> + gen_statem:cast(self(), Evt), {'next_state', StateName, State}; handle_info(_Info, StateName, State) -> lager:debug("unhandled message in state ~s: ~p", [StateName, _Info]), {'next_state', StateName, State}. %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_statem' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_statem' terminates with +%% @private +%% @doc This function is called by a gen_ServerRef when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_ServerRef terminates with %% Reason. The return value is ignored. %% %% @end @@ -1518,21 +2145,15 @@ terminate(Reason, _StateName, #state{account_id=AccountId }) -> lager:debug("acdc agent statem terminating while in ~s: ~p", [_StateName, Reason]), - maybe_stop_agent(Reason, AccountId, AgentId), + Reason =:= 'normal' + andalso kz_process:spawn(fun acdc_agents_sup:stop_agent/2, [AccountId, AgentId]), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_RED_SOLID). -maybe_stop_agent('normal', AccountId, AgentId) -> - stop_agent(AccountId, AgentId); -maybe_stop_agent(_Reason, _AccountId, _AgentId) -> - 'ok'. - -stop_agent(AccountId, AgentId) -> - kz_process:spawn(fun acdc_agents_sup:stop_agent/2, [AccountId, AgentId]), - 'ok'. - %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), atom(), state(), any()) -> {'ok', atom(), state()}. @@ -1543,6 +2164,22 @@ code_change(_OldVsn, StateName, State, _Extra) -> %%% Internal functions %%%============================================================================= +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec cancel_if_failed_originate(kz_term:ne_binary(), kz_term:ne_binary(), atom(), state()) -> + {'next_state', atom(), state()}. +cancel_if_failed_originate(CallId, MemberCallId, StateName, #state{agent_listener=AgentListener + ,member_call_id=MemberCallId1 + }=State) when MemberCallId =/= MemberCallId1 -> + lager:debug("cancelling ~s (failed originate from queue call ~s" + ,[CallId, MemberCallId]), + acdc_agent_listener:channel_hungup(AgentListener, CallId), + {'next_state', StateName, State}; +cancel_if_failed_originate(_, _, StateName, State) -> + {'next_state', StateName, State}. + %%------------------------------------------------------------------------------ %% @doc %% @end @@ -1567,7 +2204,7 @@ start_resync_timer() -> start_pause_timer('undefined') -> start_pause_timer(1); start_pause_timer(0) -> 'undefined'; start_pause_timer(Timeout) -> - erlang:start_timer(Timeout * 1000, self(), ?PAUSE_MESSAGE). + erlang:start_timer(Timeout * ?MILLISECONDS_IN_SECOND, self(), ?PAUSE_MESSAGE). -spec call_id(kz_json:object()) -> kz_term:api_binary(). call_id(JObj) -> @@ -1577,11 +2214,12 @@ call_id(JObj) -> end. %% returns time left in seconds --spec time_left(reference() | 'false' | kz_term:api_integer()) -> kz_term:api_integer(). +-spec time_left(kz_term:api_reference() | 'false' | timeout()) -> timeout() | 'undefined'. time_left(Ref) when is_reference(Ref) -> time_left(erlang:read_timer(Ref)); time_left('false') -> 'undefined'; time_left('undefined') -> 'undefined'; +time_left('infinity') -> 'infinity'; time_left(Ms) when is_integer(Ms) -> Ms div 1000. -spec clear_call(state(), atom()) -> state(). @@ -1597,13 +2235,13 @@ clear_call(#state{connect_failures=Fails clear_call(#state{connect_failures=Fails ,max_connect_failures=_MaxFails }=State, 'failed') -> - lager:debug("agent has failed to connect ~b times(~b)", [Fails+1, _MaxFails]), + lager:debug("agent has failed to connect ~b times(~p)", [Fails+1, _MaxFails]), clear_call(State#state{connect_failures=Fails+1}, 'ready'); -clear_call(#state{statem_call_id=StateMCallId +clear_call(#state{fsm_call_id=FSMemberCallId ,wrapup_ref=WRef ,pause_ref=PRef }=State, NextState)-> - kz_log:put_callid(StateMCallId), + kz_log:put_callid(FSMemberCallId), ReadyForAction = NextState =/= 'wrapup' andalso NextState =/= 'paused', @@ -1617,13 +2255,18 @@ clear_call(#state{statem_call_id=StateMCallId ,pause_ref = case ReadyForAction of 'true' -> 'undefined'; 'false' -> PRef end ,member_call = 'undefined' ,member_call_id = 'undefined' + ,member_callback_candidates = [] + ,member_original_call = 'undefined' + ,member_original_call_id = 'undefined' ,member_call_start = 'undefined' ,member_call_queue_id = 'undefined' ,agent_call_id = 'undefined' + ,agent_callback_call = 'undefined' ,caller_exit_key = <<"#">> + ,monitoring = 'false' }. --spec current_call(kapps_call:call() | 'undefined', atom(), kz_term:ne_binary(), 'undefined' | kz_time:start_time()) -> +-spec current_call(kapps_call:call() | 'undefined', atom(), kz_term:ne_binary(), 'undefined' | kz_term:kz_now()) -> kz_term:api_object(). current_call('undefined', _, _, _) -> 'undefined'; current_call(Call, AgentState, QueueId, Start) -> @@ -1637,7 +2280,7 @@ current_call(Call, AgentState, QueueId, Start) -> ,{<<"queue_id">>, QueueId} ]). --spec elapsed('undefined' | kz_time:start_time()) -> kz_term:api_integer(). +-spec elapsed('undefined' | kz_term:kz_now()) -> kz_term:api_integer(). elapsed('undefined') -> 'undefined'; elapsed(Start) -> kz_time:elapsed_s(Start). @@ -1663,37 +2306,54 @@ hangup_call(#state{agent_listener=AgentListener ,agent_id=AgentId ,queue_notifications=Ns }=State, Initiator) -> - acdc_stats:call_processed(AccountId, QueueId, AgentId, CallId, Initiator), + _ = acdc_stats:call_processed(AccountId, QueueId, AgentId, original_call_id(State), Initiator), acdc_agent_listener:channel_hungup(AgentListener, CallId), maybe_notify(Ns, ?NOTIFY_HANGUP, State), wrapup_timer(State). --spec maybe_stop_timer(kz_term:api_reference()) -> 'ok'. +-spec maybe_stop_timer(kz_term:api_reference() | 'infinity') -> 'ok'. maybe_stop_timer('undefined') -> 'ok'; +maybe_stop_timer('infinity') -> 'ok'; maybe_stop_timer(ConnRef) when is_reference(ConnRef) -> _ = erlang:cancel_timer(ConnRef), 'ok'. --spec maybe_stop_timer(kz_term:api_reference(), boolean()) -> 'ok'. +-spec maybe_stop_timer(kz_term:api_reference() | 'infinity', boolean()) -> 'ok'. maybe_stop_timer(TimerRef, 'true') -> maybe_stop_timer(TimerRef); maybe_stop_timer(_, 'false') -> 'ok'. -spec start_outbound_call_handling(kz_term:ne_binary() | kapps_call:call(), state()) -> state(). -start_outbound_call_handling(CallId, #state{agent_listener=AgentListener - ,account_id=AccountId - ,agent_id=AgentId - ,outbound_call_ids=OutboundCallIds - }=State) when is_binary(CallId) -> +start_outbound_call_handling(CallId, State) when is_binary(CallId) -> + start_outbound_call_handling(CallId, <<"unknown">>, <<"unknown">>, State); +start_outbound_call_handling(Call, State) -> + start_outbound_call_handling(kapps_call:call_id(Call), State). + +start_outbound_call_handling(CallId, Number, Name, #state{agent_listener=AgentListener + ,account_id=AccountId + ,agent_id=AgentId + ,outbound_call_ids=OutboundCallIds + }=State) when is_binary(CallId) -> kz_log:put_callid(CallId), lager:debug("agent making outbound call, not receiving ACDc calls"), - acdc_agent_listener:outbound_call(AgentListener, CallId), + acdc_agent_listener:outbound_call(AgentListener, CallId, Number, Name), acdc_agent_stats:agent_outbound(AccountId, AgentId, CallId), - State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}; -start_outbound_call_handling(Call, State) -> - start_outbound_call_handling(kapps_call:call_id(Call), State). + State#state{outbound_call_ids=[CallId | lists:delete(CallId, OutboundCallIds)]}. + --spec outbound_hungup(state()) -> kz_types:handle_fsm_ret(state()). +-spec start_inbound_call_handling(kz_term:ne_binary() | kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary(), state()) -> state(). +start_inbound_call_handling(CallId, Number, Name, #state{agent_listener=AgentListener + ,account_id=AccountId + ,agent_id=AgentId + ,inbound_call_ids=InboundCallIds + }=State) when is_binary(CallId) -> + kz_log:put_callid(CallId), + lager:debug("agent receiving inbound call, not receiving ACDc calls"), + acdc_agent_listener:inbound_call(AgentListener, CallId, Number, Name), + acdc_agent_stats:agent_inbound(AccountId, AgentId, CallId), + State#state{inbound_call_ids=[CallId | lists:delete(CallId, InboundCallIds)]}. + +-spec outbound_hungup(state()) -> kz_term:handle_fsm_ret(state()). outbound_hungup(#state{agent_listener=AgentListener ,wrapup_ref=WRef ,pause_ref=PRef @@ -1704,6 +2364,7 @@ outbound_hungup(#state{agent_listener=AgentListener _W -> case time_left(PRef) of N when is_integer(N), N > 0 -> apply_state_updates(clear_call(State, 'paused')); + 'infinity' -> apply_state_updates(clear_call(State, 'paused')); _P -> lager:debug("wrapup left: ~p pause left: ~p", [_W, _P]), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), @@ -1714,6 +2375,28 @@ outbound_hungup(State) -> lager:debug("agent still has some outbound calls active"), {'next_state', 'outbound', State}. +-spec inbound_hungup(state()) -> kz_term:handle_fsm_ret(state()). +inbound_hungup(#state{agent_listener=AgentListener + ,wrapup_ref=WRef + ,pause_ref=PRef + ,inbound_call_ids=[] + }=State) -> + case time_left(WRef) of + N when is_integer(N), N > 0 -> apply_state_updates(clear_call(State, 'wrapup')); + _W -> + case time_left(PRef) of + N when is_integer(N), N > 0 -> apply_state_updates(clear_call(State, 'paused')); + 'infinity' -> apply_state_updates(clear_call(State, 'paused')); + _P -> + lager:debug("wrapup left: ~p pause left: ~p", [_W, _P]), + acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), + apply_state_updates(clear_call(State, 'ready')) + end + end; +inbound_hungup(State) -> + lager:debug("agent still has some inbound calls active"), + {'next_state', 'inbound', State}. + -spec missed_reason(kz_term:ne_binary()) -> kz_term:ne_binary(). missed_reason(<<"-ERR ", Reason/binary>>) -> missed_reason(binary:replace(Reason, <<"\n">>, <<>>, ['global'])); @@ -1725,11 +2408,11 @@ missed_reason(Reason) -> Reason. -spec find_username(kz_json:object()) -> kz_term:api_binary(). find_username(EP) -> - find_sip_username(EP, kzd_devices:sip_username(EP)). - --spec find_sip_username(kz_json:object(), kz_term:api_binary()) -> kz_term:api_binary(). -find_sip_username(EP, 'undefined') -> kz_json:get_value(<<"To-User">>, EP); -find_sip_username(_EP, Username) -> Username. + AccountId = kz_json:get_value(<<"Account-ID">>, EP), + AgentId = kz_json:get_value(<<"Endpoint-ID">>, EP), + AccountDb = kzs_util:format_account_db(AccountId), + [Device] = acdc_util:agent_devices(AccountDb, AgentId), + kz_json:get_ne_value([<<"sip">>, <<"username">>], Device). -spec find_endpoint_id(kz_json:object()) -> kz_term:api_binary(). find_endpoint_id(EP) -> @@ -1739,7 +2422,7 @@ find_endpoint_id(EP) -> find_endpoint_id(EP, 'undefined') -> kz_json:get_value(<<"Endpoint-ID">>, EP); find_endpoint_id(_EP, EPId) -> EPId. --spec monitor_endpoint(kz_json:api_object(), kz_term:ne_binary()) -> any(). +-spec monitor_endpoint(kz_term:api_object(), kz_term:ne_binary()) -> _. monitor_endpoint('undefined', _) -> 'ok'; monitor_endpoint(EP, AccountId) -> Username = find_username(EP), @@ -1785,7 +2468,7 @@ convert_to_endpoint(EPDoc) -> Call = kapps_call:exec(Setters, kapps_call:new()), case kz_endpoint:build(kz_doc:id(EPDoc), kz_json:new(), Call) of - {'ok', [EP|_]} -> EP; + {'ok', EP} -> EP; {'error', _} -> 'undefined' end. @@ -1795,7 +2478,11 @@ convert_to_endpoint(EPDoc) -> get_endpoints(OrigEPs, Call, AgentId, QueueId) -> case catch acdc_util:get_endpoints(Call, AgentId) of [] -> - {'error', 'no_endpoints'}; + %% Survive couch connection issue by using last list of valid endpoints + case OrigEPs of + [] -> {'error', 'no_endpoints'}; + _ -> {'ok', [kz_json:set_value([<<"Custom-Channel-Vars">>, <<"Queue-ID">>], QueueId, EP) || EP <- OrigEPs]} + end; [_|_]=EPs -> AccountId = kapps_call:account_id(Call), @@ -1809,7 +2496,9 @@ get_endpoints(OrigEPs, Call, AgentId, QueueId) -> {'error', E} end. --spec return_to_state(non_neg_integer(), pos_integer()) -> 'paused' | 'ready'. +-spec return_to_state(non_neg_integer(), pos_integer() | 'infinity') -> 'paused' | 'ready'. +return_to_state(_, 'infinity') -> + 'ready'; return_to_state(Fails, MaxFails) -> lager:debug("fails ~b max ~b going to pause", [Fails, MaxFails]), case is_integer(MaxFails) @@ -1868,40 +2557,42 @@ get_method(Ns) -> standardize_method(<<"post">>) -> 'post'; standardize_method(_) -> 'get'. --spec notify(kz_term:ne_binary(), 'get' | 'post', kz_term:ne_binary(), state()) -> 'ok'. notify(Url, Method, Key, #state{account_id=AccountId ,agent_id=AgentId + ,agent_name=AgentName ,member_call=MemberCall ,agent_call_id=AgentCallId ,member_call_queue_id=QueueId }) -> kz_log:put_callid(kapps_call:call_id(MemberCall)), - Data = kz_json:from_list( - [{<<"account_id">>, AccountId} - ,{<<"agent_id">>, AgentId} - ,{<<"agent_call_id">>, AgentCallId} - ,{<<"queue_id">>, QueueId} - ,{<<"member_call_id">>, kapps_call:call_id(MemberCall)} - ,{<<"caller_id_name">>, kapps_call:caller_id_name(MemberCall)} - ,{<<"caller_id_number">>, kapps_call:caller_id_number(MemberCall)} - ,{<<"call_state">>, Key} - ,{<<"now">>, kz_time:now_s()} - ]), - notify(Url, Method, Data). - --spec notify(kz_term:ne_binary(), 'get' | 'post', kz_json:object()) -> 'ok'. -notify(Url, 'post', Data) -> + {CIDNumber, CIDName} = acdc_util:caller_id(MemberCall), + Params = props:filter_undefined( + [{<<"account_id">>, AccountId} + ,{<<"agent_id">>, AgentId} + ,{<<"agent_call_id">>, AgentCallId} + ,{<<"queue_id">>, QueueId} + ,{<<"member_call_id">>, kapps_call:call_id(MemberCall)} + ,{<<"caller_id_name">>, CIDName} + ,{<<"caller_id_number">>, CIDNumber} + ,{<<"call_state">>, Key} + ,{<<"now">>, kz_time:current_tstamp()} + ,{<<"agent_username">>, AgentName} + ]), + notify_method(Url, Method, kz_json:from_list(Params)). + +-spec notify_method(kz_term:ne_binary(), 'get' | 'post', kz_json:object()) -> 'ok'. +notify_method(Url, 'post', Data) -> notify(Url, [{"Content-Type", "application/json"}] ,'post', kz_json:encode(Data), [] ); -notify(Url, 'get', Data) -> +notify_method(Url, 'get', Data) -> notify(uri(Url, kz_http_util:json_to_querystring(Data)) ,[], 'get', <<>>, [] ). -spec notify(kz_term:ne_binary(), kz_term:proplist(), 'get' | 'post', binary(), kz_term:proplist()) -> 'ok'. notify(Uri, Headers, Method, Body, Opts) -> - Options = [{'connect_timeout', 200} + Options = [{'connect_timeout', 1000} ,{'timeout', 1000} | Opts ], @@ -1927,16 +2618,17 @@ recording_url(JObj) -> Url -> Url end. --spec uri(kz_term:ne_binary(), iodata()) -> kz_term:ne_binary(). +-spec uri(kz_term:ne_binary(), iolist()) -> kz_term:ne_binary(). uri(URI, QueryString) -> + QueryBinary = kz_term:to_binary(QueryString), case kz_http_util:urlsplit(URI) of {Scheme, Host, Path, <<>>, Fragment} -> - kz_http_util:urlunsplit({Scheme, Host, Path, iolist_to_binary(QueryString), Fragment}); + kz_http_util:urlunsplit({Scheme, Host, Path, QueryBinary, Fragment}); {Scheme, Host, Path, QS, Fragment} -> - kz_http_util:urlunsplit({Scheme, Host, Path, <>, Fragment}) + kz_http_util:urlunsplit({Scheme, Host, Path, <>, Fragment}) end. --spec apply_state_updates(state()) -> kz_types:handle_fsm_ret(state()). +-spec apply_state_updates(state()) -> kz_term:handle_fsm_ret(state()). apply_state_updates(#state{agent_state_updates=Q ,wrapup_ref=WRef ,pause_ref=PRef @@ -1946,18 +2638,20 @@ apply_state_updates(#state{agent_state_updates=Q _W -> case time_left(PRef) of N when is_integer(N), N > 0 -> 'paused'; + 'infinity' -> 'paused'; _P -> 'ready' end end, lager:debug("default state for applying state updates ~s", [FoldDefaultState]), apply_state_updates_fold({'next_state', FoldDefaultState, State#state{agent_state_updates=[]}}, lists:reverse(Q)). --spec apply_state_updates_fold({'next_state', atom(), state()}, list()) -> kz_types:handle_fsm_ret(state()). +-spec apply_state_updates_fold({'next_state', atom(), state()}, list()) -> kz_term:handle_fsm_ret(state()). apply_state_updates_fold({_, StateName, #state{account_id=AccountId ,agent_id=AgentId ,agent_listener=AgentListener ,wrapup_ref=WRef ,pause_ref=PRef + ,pause_alias=Alias }}=Acc, []) -> lager:debug("resulting agent state ~s", [StateName]), case StateName of @@ -1967,11 +2661,15 @@ apply_state_updates_fold({_, StateName, #state{account_id=AccountId 'wrapup' -> acdc_agent_stats:agent_wrapup(AccountId, AgentId, time_left(WRef)); 'paused' -> acdc_agent_listener:send_agent_busy(AgentListener), - acdc_agent_stats:agent_paused(AccountId, AgentId, time_left(PRef)) + acdc_agent_stats:agent_paused(AccountId, AgentId, time_left(PRef), Alias) end, Acc; -apply_state_updates_fold({_, _, State}, [{'pause', Timeout}|Updates]) -> - apply_state_updates_fold(handle_pause(Timeout, State), Updates); +apply_state_updates_fold({_, _, State}, [{'pause', 'infinity', Alias}|Updates]) -> + apply_state_updates_fold(handle_pause('infinity', Alias, State), Updates); +apply_state_updates_fold({_, _, State}, [{'pause', 0, Alias}|Updates]) -> + apply_state_updates_fold(handle_pause('infinity', Alias, State), Updates); +apply_state_updates_fold({_, _, State}, [{'pause', Timeout, Alias}|Updates]) -> + apply_state_updates_fold(handle_pause(Timeout, Alias, State), Updates); apply_state_updates_fold({_, _, State}, [{'resume'}|Updates]) -> apply_state_updates_fold(handle_resume(State), Updates); apply_state_updates_fold({_, 'wrapup', State}, [{'end_wrapup'}|Updates]) -> @@ -1980,7 +2678,7 @@ apply_state_updates_fold({_, StateName, State}, [{'end_wrapup'}|Updates]) -> apply_state_updates_fold(handle_end_wrapup(StateName, State), Updates); apply_state_updates_fold({_, _, State}, [{'agent_logout'}|_]) -> lager:debug("agent logging out"), - %% Do not continue fold, stop statem + %% Do not continue fold, stop ServerRef handle_agent_logout(State); apply_state_updates_fold({_, _, State}=Acc, [{'update_presence', PresenceId, PresenceState}|Updates]) -> handle_presence_update(PresenceId, PresenceState, State), @@ -1992,7 +2690,7 @@ valid_state_for_logout('wrapup') -> 'true'; valid_state_for_logout('paused') -> 'true'; valid_state_for_logout(_) -> 'false'. --spec handle_agent_logout(state()) -> kz_types:handle_fsm_ret(state()). +-spec handle_agent_logout(state()) -> kz_term:handle_fsm_ret(state()). handle_agent_logout(#state{account_id = AccountId ,agent_id = AgentId }=State) -> @@ -2008,7 +2706,7 @@ handle_presence_update(PresenceId, PresenceState, #state{agent_id = AgentId acdc_agent_listener:maybe_update_presence_id(Listener, PresenceId), acdc_agent_listener:presence_update(Listener, PresenceState). --spec handle_resume(state()) -> kz_types:handle_fsm_ret(state()). +-spec handle_resume(state()) -> kz_term:handle_fsm_ret(state()). handle_resume(#state{agent_listener=AgentListener ,pause_ref=Ref }=State) -> @@ -2017,19 +2715,35 @@ handle_resume(#state{agent_listener=AgentListener acdc_agent_listener:update_agent_status(AgentListener, <<"resume">>), - acdc_agent_listener:send_status_resume(AgentListener), acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), {'next_state', 'ready', State#state{pause_ref='undefined'}}. --spec handle_pause(integer(), state()) -> kz_types:handle_fsm_ret(state()). -handle_pause(Timeout, #state{agent_listener=AgentListener}=State) -> +-spec handle_pause(timeout(), kz_term:ne_binary(), state()) -> kz_types:handle_fsm_ret(state()). +handle_pause(Timeout, Alias, #state{agent_listener=AgentListener}=State) -> acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_RED_FLASH), - Ref = start_pause_timer(Timeout), - State1 = State#state{pause_ref=Ref}, + State1 = case Timeout of + 'infinity' -> + State#state{pause_ref='infinity' + ,pause_alias=Alias + }; + _ -> + Ref = start_pause_timer(Timeout), + State#state{pause_ref=Ref + ,pause_alias=Alias + } + end, {'next_state', 'paused', State1}. --spec handle_end_wrapup(atom(), state()) -> kz_types:handle_fsm_ret(state()). +-spec handle_end_wrapup(atom(), state()) -> kz_term:handle_fsm_ret(state()). handle_end_wrapup(NextState, #state{wrapup_ref=Ref}=State) -> lager:debug("end_wrapup received, cancelling wrapup timers"), maybe_stop_timer(Ref), {'next_state', NextState, State#state{wrapup_ref='undefined'}}. + +-spec original_call_id(state()) -> kz_term:ne_binary(). +original_call_id(#state{member_call_id=MemberCallId + ,member_original_call_id='undefined' + }) -> + MemberCallId; +original_call_id(#state{member_original_call_id=OriginalCallId}) -> + OriginalCallId. diff --git a/applications/acdc/src/acdc_agent_handler.erl b/applications/acdc/src/acdc_agent_handler.erl index 55373dd0ce0..7c941f0235d 100644 --- a/applications/acdc/src/acdc_agent_handler.erl +++ b/applications/acdc/src/acdc_agent_handler.erl @@ -4,6 +4,8 @@ %%% @author James Aimonetti %%% @author Daniel Finke %%% +%%% @author James Aimonetti +%%% @author Daniel Finke %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -29,7 +31,7 @@ -include("acdc.hrl"). -include_lib("kazoo_amqp/include/kapi_conf.hrl"). --define(DEFAULT_PAUSE, kapps_config:get_integer(?CONFIG_CAT, <<"default_agent_pause_timeout">>, 600)). +-define(DEFAULT_PAUSE, kapps_config:get(?CONFIG_CAT, <<"default_agent_pause_timeout">>, 600)). -spec handle_status_update(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_status_update(JObj, _Props) -> @@ -48,14 +50,19 @@ handle_status_update(JObj, _Props) -> maybe_stop_agent(AccountId, AgentId, JObj); <<"pause">> -> 'true' = kapi_acdc_agent:pause_v(JObj), - Timeout = kz_json:get_integer_value(<<"Time-Limit">>, JObj, ?DEFAULT_PAUSE), - maybe_pause_agent(AccountId, AgentId, Timeout, JObj); + Timeout = kz_json:get_value(<<"Time-Limit">>, JObj, ?DEFAULT_PAUSE), + Alias = kz_json:get_value(<<"Alias">>, JObj), + maybe_pause_agent(AccountId, AgentId, Timeout, Alias, JObj); <<"resume">> -> 'true' = kapi_acdc_agent:resume_v(JObj), maybe_resume_agent(AccountId, AgentId, JObj); <<"end_wrapup">> -> 'true' = kapi_acdc_agent:end_wrapup_v(JObj), maybe_end_wrapup_agent(AccountId, AgentId, JObj); + <<"restart">> -> + 'true' = kapi_acdc_agent:restart_v(JObj), + _ = acdc_agents_sup:restart_agent(AccountId, AgentId), + 'ok'; Event -> maybe_agent_queue_change(AccountId, AgentId, Event ,kz_json:get_value(<<"Queue-ID">>, JObj) ,JObj @@ -101,7 +108,11 @@ maybe_start_agent(AccountId, AgentId, JObj) -> end; {'exists', Sup} -> FSM = acdc_agent_sup:fsm(Sup), - acdc_agent_fsm:update_presence(FSM, presence_id(JObj), presence_state(JObj, 'undefined')), + acdc_agent_stats:agent_logged_in(AccountId, AgentId), + case presence_state(JObj, 'undefined') of + 'undefined' -> 'ok'; + PresenceState -> acdc_agent_fsm:update_presence(FSM, presence_id(JObj), PresenceState) + end, Sup; {'error', _E} -> acdc_agent_stats:agent_logged_out(AccountId, AgentId), @@ -166,15 +177,21 @@ maybe_stop_agent(AccountId, AgentId, JObj) -> end. -maybe_pause_agent(AccountId, AgentId, Timeout, JObj) -> +maybe_pause_agent(AccountId, AgentId, <<"infinity">>, Alias, JObj) -> + maybe_pause_agent(AccountId, AgentId, 'infinity', Alias, JObj); +maybe_pause_agent(AccountId, AgentId, Timeout, Alias, JObj) when is_integer(Timeout) + orelse Timeout =:= 'infinity' -> case acdc_agents_sup:find_agent_supervisor(AccountId, AgentId) of 'undefined' -> lager:debug("agent ~s (~s) not found, nothing to do", [AgentId, AccountId]); Sup when is_pid(Sup) -> - lager:debug("agent ~s(~s) is pausing for ~p", [AccountId, AgentId, Timeout]), + lager:debug("agent ~s(~s) is pausing (~p) for ~p", [AccountId, AgentId, Alias, Timeout]), FSM = acdc_agent_sup:fsm(Sup), acdc_agent_fsm:update_presence(FSM, presence_id(JObj), presence_state(JObj, 'undefined')), - acdc_agent_fsm:pause(FSM, Timeout) - end. + acdc_agent_fsm:pause(FSM, Timeout, Alias) + end; +maybe_pause_agent(AccountId, AgentId, Timeout, _, _) -> + lager:error("not pausing agent ~s(~s) invalid Timeout: ~p", [AccountId, AgentId, Timeout]), + ok. maybe_resume_agent(AccountId, AgentId, JObj) -> case acdc_agents_sup:find_agent_supervisor(AccountId, AgentId) of @@ -224,7 +241,7 @@ handle_call_event(JObj, Props) -> end end. --spec handle_call_event(kz_term:ne_binary(), kz_term:ne_binary(), kz_types:server_ref(), kz_json:object(), kz_term:proplist()) -> any(). +-spec handle_call_event(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:server_ref(), kz_json:object(), kz_term:proplist()) -> any(). handle_call_event(Category, <<"CHANNEL_DESTROY">> = Name, FSM, JObj, Props) -> Urls = props:get_value('cdr_urls', Props), CallId = kz_json:get_value(<<"Call-ID">>, JObj), @@ -248,20 +265,22 @@ handle_new_channel(JObj, AccountId) -> -spec handle_new_channel_acct(kz_json:object(), kz_term:api_binary()) -> 'ok'. handle_new_channel_acct(_, 'undefined') -> 'ok'; handle_new_channel_acct(JObj, AccountId) -> - FromUser = hd(binary:split(kz_json:get_value(<<"From">>, JObj), <<"@">>)), - ToUser = hd(binary:split(kz_json:get_value(<<"To">>, JObj), <<"@">>)), - ReqUser = hd(binary:split(kz_json:get_value(<<"Request">>, JObj), <<"@">>)), - + FromUser = + case kz_json:is_defined(<<"From-Uri">>, JObj) of + false -> hd(binary:split(kz_json:get_value(<<"From">>, JObj), <<"@">>)); + true -> hd(binary:split(kz_json:get_value(<<"From-Uri">>, JObj), <<"@">>)) + end, + ToUser = get_to_user(JObj), CallId = kz_json:get_value(<<"Call-ID">>, JObj), MemberCallId = kz_json:get_value([<<"Custom-Channel-Vars">>, <<"Member-Call-ID">>], JObj), - - lager:debug("new channel in acct ~s: from ~s to ~s(~s)", [AccountId, FromUser, ToUser, ReqUser]), - + lager:debug("new channel in acct ~s: from ~s to ~s", [AccountId, FromUser, ToUser]), case kz_call_event:call_direction(JObj) of - <<"inbound">> -> gproc:send(?NEW_CHANNEL_REG(AccountId, FromUser), ?NEW_CHANNEL_FROM(CallId)); + <<"inbound">> -> + gproc:send(?NEW_CHANNEL_REG(AccountId, FromUser), ?NEW_CHANNEL_TO(CallId, ToUser, <<"unknown">>)); <<"outbound">> -> - gproc:send(?NEW_CHANNEL_REG(AccountId, ToUser), ?NEW_CHANNEL_TO(CallId, MemberCallId)), - gproc:send(?NEW_CHANNEL_REG(AccountId, ReqUser), ?NEW_CHANNEL_TO(CallId, MemberCallId)); + CR_IDNumber = kz_json:get_value(<<"Caller-ID-Number">>, JObj), + CR_IDName = kz_json:get_value(<<"Caller-ID-Name">>, JObj), + gproc:send(?NEW_CHANNEL_REG(AccountId, ToUser), ?NEW_CHANNEL_FROM(CallId, CR_IDNumber, CR_IDName, MemberCallId)); _ -> lager:debug("invalid call direction for call ~s", [CallId]) end. @@ -276,8 +295,13 @@ handle_new_channel_acct(JObj, AccountId) -> %%------------------------------------------------------------------------------ -spec handle_destroyed_channel(kz_json:object(), kz_term:api_binary()) -> 'ok'. handle_destroyed_channel(JObj, AccountId) -> - FromUser = hd(binary:split(kz_json:get_value(<<"From">>, JObj), <<"@">>)), - ToUser = hd(binary:split(kz_json:get_value(<<"To">>, JObj), <<"@">>)), + FromUser = + case kz_json:is_defined(<<"From-Uri">>, JObj) of + false -> hd(binary:split(kz_json:get_value(<<"From">>, JObj), <<"@">>)); + true -> hd(binary:split(kz_json:get_value(<<"From-Uri">>, JObj), <<"@">>)) + end, + + ToUser = get_to_user(JObj), CallId = kz_json:get_value(<<"Call-ID">>, JObj), HangupCause = acdc_util:hangup_cause(JObj), @@ -295,6 +319,20 @@ handle_destroyed_channel(JObj, AccountId) -> _ -> 'ok' end. +-spec get_to_user(kz_json:object()) -> kz_term:api_binary(). +get_to_user(JObj) -> + case kz_json:is_defined(<<"To-Uri">>, JObj) of + true -> hd(binary:split(kz_json:get_value(<<"To-Uri">>, JObj), <<"@">>)); + false -> get_username_or_destination_number(JObj) + end. + +-spec get_username_or_destination_number(kz_json:object()) -> kz_term:api_binary(). +get_username_or_destination_number(JObj) -> + case kz_json:is_defined([<<"Custom-Channel-Vars">>,<<"Username">>], JObj) of + true -> kz_json:get_ne_value([<<"Custom-Channel-Vars">>,<<"Username">>], JObj); + false -> kz_json:get_value(<<"Caller-Destination-Number">>, JObj) + end. + -spec handle_originate_resp(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_originate_resp(JObj, Props) -> case kz_json:get_value(<<"Event-Name">>, JObj) of @@ -309,6 +347,7 @@ handle_originate_resp(JObj, Props) -> acdc_agent_fsm:originate_uuid(props:get_value('fsm_pid', Props), JObj) end. + -spec handle_member_message(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_member_message(JObj, Props) -> handle_member_message(JObj, Props, kz_json:get_value(<<"Event-Name">>, JObj)). @@ -319,10 +358,22 @@ handle_member_message(JObj, Props, <<"connect_req">>) -> acdc_agent_fsm:member_connect_req(props:get_value('fsm_pid', Props), JObj); handle_member_message(JObj, Props, <<"connect_win">>) -> 'true' = kapi_acdc_queue:member_connect_win_v(JObj), - acdc_agent_fsm:member_connect_win(props:get_value('fsm_pid', Props), JObj); + MyId = acdc_util:proc_id(props:get_value('fsm_pid', Props)), + lager:debug("myid ~p", [MyId]), + lager:debug("procids ~p", [kz_json:get_value(<<"Agent-Process-IDs">>, JObj)]), + case lists:member(MyId, kz_json:get_value(<<"Agent-Process-IDs">>, JObj)) of + true -> acdc_agent_fsm:member_connect_win(props:get_value('fsm_pid', Props), JObj, 'same_node'); + false -> acdc_agent_fsm:member_connect_win(props:get_value('fsm_pid', Props), JObj, 'different_node') + end; handle_member_message(JObj, Props, <<"connect_satisfied">>) -> 'true' = kapi_acdc_queue:member_connect_satisfied_v(JObj), - acdc_agent_fsm:member_connect_satisfied(props:get_value('fsm_pid', Props), JObj); + MyId = acdc_util:proc_id(props:get_value('fsm_pid', Props)), + lager:debug("myid ~p", [MyId]), + lager:debug("procids ~p", [kz_json:get_value(<<"Agent-Process-IDs">>, JObj)]), + case lists:member(MyId, kz_json:get_value(<<"Agent-Process-IDs">>, JObj)) of + true -> acdc_agent_fsm:member_connect_satisfied(props:get_value('fsm_pid', Props), JObj, 'same_node'); + false -> acdc_agent_fsm:member_connect_satisfied(props:get_value('fsm_pid', Props), JObj, 'different_node') + end; handle_member_message(_, _, EvtName) -> lager:debug("not handling member event ~s", [EvtName]). @@ -334,6 +385,12 @@ handle_agent_message(JObj, Props) -> handle_agent_message(JObj, Props, <<"connect_timeout">>) -> 'true' = kapi_acdc_queue:agent_timeout_v(JObj), acdc_agent_fsm:agent_timeout(props:get_value('fsm_pid', Props), JObj); +handle_agent_message(JObj, Props, <<"shared_failure">>) -> + 'true' = kapi_acdc_agent:shared_originate_failure_v(JObj), + acdc_agent_fsm:shared_failure(props:get_value('fsm_pid', Props), JObj); +handle_agent_message(JObj, Props, <<"shared_call_id">>) -> + 'true' = kapi_acdc_agent:shared_call_id_v(JObj), + acdc_agent_fsm:shared_call_id(props:get_value('fsm_pid', Props), JObj); handle_agent_message(_, _, _EvtName) -> lager:debug("not handling agent event ~s", [_EvtName]). @@ -371,11 +428,6 @@ handle_change(JObj, <<"undefined">>) -> end. handle_device_change(AccountDb, AccountId, DeviceId, Rev, Type) -> - %% Since this event is broadcast to listeners simultaneously, the kz_cache_listener - %% may have not flushed the caches needed by this handler yet. Do so manually - kz_datamgr:flush_cache_doc(AccountDb, DeviceId), - kz_endpoint:flush_local(AccountDb, DeviceId), - handle_device_change(AccountDb, AccountId, DeviceId, Rev, Type, 0). handle_device_change(_AccountDb, _AccountId, DeviceId, Rev, _Type, Cnt) when Cnt > 3 -> @@ -461,12 +513,11 @@ update_probe(JObj, P) when is_pid(P) -> send_probe(JObj, State) -> To = <<(kz_json:get_value(<<"Username">>, JObj))/binary ,"@" - ,(kz_json:get_value(<<"Realm">>, JObj))/binary - >>, + ,(kz_json:get_value(<<"Realm">>, JObj))/binary>>, PresenceUpdate = [{<<"State">>, State} ,{<<"Presence-ID">>, To} - ,{<<"Call-ID">>, kz_term:to_hex_binary(crypto:hash('md5', To))} + ,{<<"Call-ID">>, kz_term:to_hex_binary(crypto:hash(md5, To))} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], kapi_presence:publish_update(PresenceUpdate). diff --git a/applications/acdc/src/acdc_agent_listener.erl b/applications/acdc/src/acdc_agent_listener.erl index 65f18e306fc..2b741937ca5 100644 --- a/applications/acdc/src/acdc_agent_listener.erl +++ b/applications/acdc/src/acdc_agent_listener.erl @@ -3,7 +3,6 @@ %%% @doc %%% @author James Aimonetti %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -17,9 +16,13 @@ -export([start_link/3, start_link/4, start_link/5 ,member_connect_resp/2 ,member_connect_retry/2 - ,member_connect_accepted/1, member_connect_accepted/2 + ,member_connect_accepted/1, member_connect_accepted/2, member_connect_accepted/3 + ,monitor_connect_accepted/2 + ,member_callback_accepted/2 ,agent_timeout/1 ,bridge_to_member/6 + ,originate_callback_to_agent/7 + ,originate_callback_return/2 ,hangup_call/1 ,monitor_call/4 ,channel_hungup/2 @@ -27,7 +30,8 @@ ,unbind_from_events/2 ,originate_execute/2 ,originate_uuid/3 - ,outbound_call/2 + ,outbound_call/4 + ,inbound_call/4 ,send_agent_available/1 ,send_agent_busy/1 ,send_sync_req/1 @@ -43,7 +47,6 @@ ,logout_agent/1 ,agent_info/2 ,maybe_update_presence_id/2 - ,maybe_update_presence_state/2 ,presence_update/2 ,update_agent_status/2 ]). @@ -71,20 +74,23 @@ -define(SERVER, ?MODULE). --record(state, {call :: kapps_call:call() | 'undefined' - ,acdc_queue_id :: kz_term:api_ne_binary() % the ACDc Queue ID - ,msg_queue_id :: kz_term:api_ne_binary() % the AMQP Queue ID of the ACDc Queue process +-record(state, {call :: kapps_call:call() + ,original_call :: kapps_call:call() + ,acdc_queue_id :: api_kz_term:ne_binary() % the ACDc Queue ID + ,msg_queue_id :: api_kz_term:ne_binary() % the AMQP Queue ID of the ACDc Queue process ,agent_id :: kz_term:api_ne_binary() + ,agent_priority :: agent_priority() + ,skills :: kz_term:ne_binaries() % skills this agent has ,acct_db :: kz_term:api_ne_binary() - ,acct_id :: kz_term:api_ne_binary() + ,acct_id :: kz_term:api_binary() ,fsm_pid :: kz_term:api_pid() ,agent_queues = [] :: kz_term:ne_binaries() - ,last_connect :: kz_time:start_time() | 'undefined' % last connection - ,last_attempt :: kz_time:start_time() | 'undefined' % last attempt to connect + ,last_connect :: kz_term:kz_now() | undefined % last connection + ,last_attempt :: kz_term:kz_now() | undefined % last attempt to connect ,my_id :: kz_term:ne_binary() ,my_q :: kz_term:api_binary() % AMQP queue name ,timer_ref :: kz_term:api_reference() - ,sync_resp :: kz_term:api_object() % furthest along resp + ,sync_resp :: kz_json:object() % furthest along resp ,supervisor :: pid() ,record_calls = 'false' :: boolean() ,recording_url :: kz_term:api_binary() %% where to send recordings after the call @@ -102,7 +108,7 @@ %%% Defines for different functionality %%%============================================================================= -%% On init, an agent process sends a sync_req and waits SYNC_TIMER_TIMEOUT ms +%% On init, an aget process sends a sync_req and waits SYNC_TIMER_TIMEOUT ms %% The agent process checks its list of received -define(SYNC_TIMER_MESSAGE, 'sync_timeout'). -define(SYNC_TIMER_TIMEOUT, 5000). @@ -120,17 +126,17 @@ %% When an agent is paused (on break, logged out, etc) -define(PAUSED_TIMER_MESSAGE, 'paused_timeout'). --define(BINDINGS(AcctId, AgentId), [{'self', []} - ,{'acdc_agent', [{'account_id', AcctId} - ,{'agent_id', AgentId} - ,{'restrict_to', ['sync', 'stats_req']} - ]} - ,{'conf', [{'action', <<"*">>} - ,{'db', kzs_util:format_account_db(AcctId)} - ,{'id', AgentId} - ,'federate' - ]} - ]). +-define(BINDINGS(AccountId, AgentId), [{'self', []} + ,{'acdc_agent', [{'account_id', AccountId} + ,{'agent_id', AgentId} + ,{'restrict_to', ['member_connect_win', 'member_connect_satisfied', 'sync', 'fsm_shared']} + ]} + ,{'conf', [{'action', <<"*">>} + ,{'db', kzs_util:format_account_db(AccountId)} + ,{'id', AgentId} + ,'federate' + ]} + ]). -define(RESPONDERS, [{{'acdc_agent_handler', 'handle_sync_req'} ,[{<<"agent">>, <<"sync_req">>}] @@ -166,44 +172,44 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the server. +%% @doc Starts the server %% @end %%------------------------------------------------------------------------------ -spec start_link(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object(), kz_term:ne_binaries()) -> kz_types:startlink_ret(). -start_link(Supervisor, AcctId, AgentId, AgentJObj, Queues) -> - lager:debug("start bindings for ~s(~s) in ready", [AcctId, AgentId]), +start_link(Supervisor, AccountId, AgentId, AgentJObj, Queues) -> + lager:debug("start bindings for ~s(~s) in ready", [AccountId, AgentId]), gen_listener:start_link(?SERVER - ,[{'bindings', ?BINDINGS(AcctId, AgentId)} + ,[{'bindings', ?BINDINGS(AccountId, AgentId)} ,{'responders', ?RESPONDERS} ] ,[Supervisor, AgentJObj, Queues] ). --spec start_link(pid(), kapps_call:call(), kz_term:ne_binary()) -> kz_types:startlink_ret(). +-spec start_link(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> kz_types:startlink_ret(). +start_link(Supervisor, AccountId, AgentId, AgentJObj) -> + Queues = kz_json:get_value(<<"queues">>, AgentJObj, []), + start_link(Supervisor, AccountId, AgentId, AgentJObj, Queues). + +-spec start_link(pid(), kapps_call:call(), kz_term:ne_binary()) -> kz_term:startlink_ret(). start_link(Supervisor, ThiefCall, QueueId) -> AgentId = kapps_call:owner_id(ThiefCall), - AcctId = kapps_call:account_id(ThiefCall), + AccountId = kapps_call:account_id(ThiefCall), - lager:debug("starting thief agent ~s(~s)", [AgentId, AcctId]), + lager:debug("starting thief agent ~s(~s)", [AgentId, AccountId]), gen_listener:start_link(?SERVER - ,[{'bindings', ?BINDINGS(AcctId, AgentId)} + ,[{'bindings', ?BINDINGS(AccountId, AgentId)} ,{'responders', ?RESPONDERS} ] ,[Supervisor, ThiefCall, [QueueId]] ). --spec start_link(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> kz_types:startlink_ret(). -start_link(Supervisor, AcctId, AgentId, AgentJObj) -> - Queues = kz_json:get_value(<<"queues">>, AgentJObj, []), - start_link(Supervisor, AcctId, AgentId, AgentJObj, Queues). - -spec member_connect_resp(pid(), kz_json:object()) -> 'ok'. member_connect_resp(Srv, ReqJObj) -> gen_listener:cast(Srv, {'member_connect_resp', ReqJObj}). --spec member_connect_retry(pid(), kz_term:ne_binary() | kz_json:object()) -> 'ok'. -member_connect_retry(Srv, WinOrCallId) -> - gen_listener:cast(Srv, {'member_connect_retry', WinOrCallId}). +-spec member_connect_retry(pid(), kz_json:object()) -> 'ok'. +member_connect_retry(Srv, WinJObj) -> + gen_listener:cast(Srv, {'member_connect_retry', WinJObj}). -spec agent_timeout(pid()) -> 'ok'. agent_timeout(Srv) -> gen_listener:cast(Srv, 'agent_timeout'). @@ -216,19 +222,42 @@ member_connect_accepted(Srv) -> member_connect_accepted(Srv, ACallId) -> gen_listener:cast(Srv, {'member_connect_accepted', ACallId}). +-spec member_connect_accepted(pid(), kz_term:ne_binary(), kapps_call:call()) -> 'ok'. +member_connect_accepted(Srv, ACallId, MemberCall) -> + gen_listener:cast(Srv, {'member_connect_accepted', ACallId, MemberCall}). + +-spec monitor_connect_accepted(pid(), kz_term:ne_binary()) -> 'ok'. +monitor_connect_accepted(Srv, ACallId) -> + gen_listener:cast(Srv, {'monitor_connect_accepted', ACallId}). + +-spec member_callback_accepted(pid(), kapps_call:call()) -> 'ok'. +member_callback_accepted(Srv, ACall) -> + gen_listener:cast(Srv, {'member_callback_accepted', ACall}). + -spec hangup_call(pid()) -> 'ok'. hangup_call(Srv) -> gen_listener:cast(Srv, {'hangup_call'}). +-spec monitor_call(pid(), kapps_call:call(), kz_json:object(), kz_term:api_binary()) -> + 'ok'. +monitor_call(Srv, Call, WinJObj, RecordingUrl) -> + gen_listener:cast(Srv, {'monitor_call', Call, WinJObj, RecordingUrl}). + -spec bridge_to_member(pid(), kapps_call:call(), kz_json:object() ,kz_json:objects(), kz_term:api_binary(), kz_term:api_binary() ) -> 'ok'. bridge_to_member(Srv, Call, WinJObj, EPs, CDRUrl, RecordingUrl) -> gen_listener:cast(Srv, {'bridge_to_member', Call, WinJObj, EPs, CDRUrl, RecordingUrl}). --spec monitor_call(pid(), kapps_call:call(), kz_term:api_binary(), kz_term:api_binary()) -> 'ok'. -monitor_call(Srv, Call, CDRUrl, RecordingUrl) -> - gen_listener:cast(Srv, {'monitor_call', Call, CDRUrl, RecordingUrl}). +-spec originate_callback_to_agent(pid(), kapps_call:call(), kz_json:object() + ,kz_json:objects(), kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary() + ) -> 'ok'. +originate_callback_to_agent(Srv, Call, WinJObj, EPs, CDRUrl, RecordingUrl, Number) -> + gen_listener:cast(Srv, {'originate_callback_to_agent', Call, WinJObj, EPs, CDRUrl, RecordingUrl, Number}). + +-spec originate_callback_return(pid(), kapps_call:call()) -> kz_term:ne_binary(). +originate_callback_return(Srv, Call) -> + gen_listener:call(Srv, {'originate_callback_return', Call}). -spec channel_hungup(pid(), kz_term:ne_binary()) -> 'ok'. channel_hungup(Srv, CallId) -> @@ -250,9 +279,13 @@ originate_execute(Srv, JObj) -> originate_uuid(Srv, UUID, CtlQ) -> gen_listener:cast(Srv, {'originate_uuid', UUID, CtlQ}). --spec outbound_call(pid(), kz_term:ne_binary()) -> 'ok'. -outbound_call(Srv, CallId) -> - gen_listener:cast(Srv, {'outbound_call', CallId}). +-spec outbound_call(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +outbound_call(Srv, CallId, Number, Name) -> + gen_listener:cast(Srv, {'outbound_call', CallId, Number, Name}). + +-spec inbound_call(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +inbound_call(Srv, CallId, Number, Name) -> + gen_listener:cast(Srv, {'inbound_call', CallId, Number, Name}). -spec send_agent_available(pid()) -> 'ok'. send_agent_available(Srv) -> @@ -275,10 +308,9 @@ send_sync_resp(Srv, Status, ReqJObj, Options) -> -spec config(pid()) -> config(). config(Srv) -> gen_listener:call(Srv, 'config'). --spec refresh_config(pid(), kz_term:api_ne_binaries(), fsm_state_name()) -> 'ok'. -refresh_config(_, 'undefined', _) -> 'ok'; -refresh_config(Srv, Qs, StateName) -> - gen_listener:cast(Srv, {'refresh_config', Qs, StateName}). +-spec refresh_config(pid(), kz_json:object(), fsm_state_name()) -> 'ok'. +refresh_config(Srv, JObj, StateName) -> + gen_listener:cast(Srv, {'refresh_config', JObj, StateName}). -spec agent_info(pid(), kz_json:path()) -> kz_json:api_json_term(). agent_info(Srv, Field) -> gen_listener:call(Srv, {'agent_info', Field}). @@ -313,17 +345,12 @@ remove_cdr_urls(Srv, CallId) -> gen_listener:cast(Srv, {'remove_cdr_urls', CallI -spec logout_agent(pid()) -> 'ok'. logout_agent(Srv) -> gen_listener:cast(Srv, 'logout_agent'). --spec maybe_update_presence_id(pid(), kz_term:api_ne_binary()) -> 'ok'. +-spec maybe_update_presence_id(pid(), api_kz_term:ne_binary()) -> 'ok'. maybe_update_presence_id(_Srv, 'undefined') -> 'ok'; maybe_update_presence_id(Srv, Id) -> gen_listener:cast(Srv, {'presence_id', Id}). --spec maybe_update_presence_state(pid(), kz_term:api_ne_binary()) -> 'ok'. -maybe_update_presence_state(_Srv, 'undefined') -> 'ok'; -maybe_update_presence_state(Srv, State) -> - presence_update(Srv, State). - --spec presence_update(pid(), kz_term:api_ne_binary()) -> 'ok'. +-spec presence_update(pid(), api_kz_term:ne_binary()) -> 'ok'. presence_update(_, 'undefined') -> 'ok'; presence_update(Srv, PresenceState) -> gen_listener:cast(Srv, {'presence_update', PresenceState}). @@ -349,7 +376,9 @@ id(Srv) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Initializes the server. +%% @private +%% @doc Initializes the server +%% %% @end %%------------------------------------------------------------------------------ -spec init([atom() | agent() | kz_term:ne_binaries()]) -> {'ok', state()}. @@ -359,6 +388,8 @@ init([Supervisor, Agent, Queues]) -> lager:debug("starting acdc agent listener"), {'ok', #state{agent_id=AgentId + ,agent_priority=acdc_agent_util:agent_priority(Agent) + ,skills=kz_json:get_list_value(<<"acdc_skills">>, Agent, []) ,acct_id=account_id(Agent) ,acct_db=account_db(Agent) ,my_id=acdc_util:proc_id() @@ -371,10 +402,17 @@ init([Supervisor, Agent, Queues]) -> }}. %%------------------------------------------------------------------------------ -%% @doc Handling call messages. +%% @private +%% @doc Handling call messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_term:handle_call_ret_state(state()). +handle_call({'originate_callback_return', Call}, _, #state{my_q=MyQ}=State) -> + MemberCallId = do_originate_callback_return(MyQ, Call), + {'reply', MemberCallId, State}; +handle_call('last_connect', _, #state{last_connect=LastConnect}=State) -> + {'reply', LastConnect, State, 'hibernate'}; handle_call('presence_id', _, #state{agent_presence_id=PresenceId}=State) -> {'reply', PresenceId, State, 'hibernate'}; handle_call('queues', _, #state{agent_queues=Queues}=State) -> @@ -383,27 +421,48 @@ handle_call('my_id', _, #state{agent_id=AgentId}=State) -> {'reply', AgentId, State, 'hibernate'}; handle_call({'agent_info', Field}, _, #state{agent=Agent}=State) -> {'reply', kz_json:get_value(Field, Agent), State}; -handle_call('config', _From, #state{acct_id=AcctId +handle_call('config', _From, #state{acct_id=AccountId ,agent_id=AgentId ,my_q=Q }=State) -> - {'reply', {AcctId, AgentId, Q}, State}; + {'reply', {AccountId, AgentId, Q}, State}; handle_call(_Request, _From, #state{}=State) -> lager:debug("unhandled call from ~p: ~p", [_From, _Request]), {'reply', {'error', 'unhandled_call'}, State}. %%------------------------------------------------------------------------------ -%% @doc Handling cast messages. +%% @private +%% @doc Handling cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). -handle_cast({'refresh_config', Qs, StateName}, #state{agent_queues=Queues}=State) -> - {Add, Rm} = acdc_agent_util:changed(Queues, Qs), +-spec handle_cast(any(), state()) -> kz_term:handle_cast_ret_state(state()). +handle_cast({'refresh_config', JObj, StateName}, #state{agent_priority=Priority0 + ,skills=Skills0 + ,agent_queues=Queues + }=State) -> + Qs = kz_json:get_list_value(<<"queues">>, JObj, []), + Priority = acdc_agent_util:agent_priority(JObj), + Skills = kz_json:get_list_value(<<"acdc_skills">>, JObj, []), + + {Add, Rm} = case is_prio_or_skills_updated(Priority0, Skills0, Priority, Skills) of + 'true' -> + {_, Rm1} = acdc_agent_util:changed(Queues, Qs), + %% If true, all agent's resulting queues must be updated + Add1 = lists:subtract(Queues, Rm1), + lists:foreach(fun(Queue) -> + lager:debug("prio/skills update for queue ~s", [Queue]) + end, Queues), + {Add1, Rm1}; + 'false' -> acdc_agent_util:changed(Queues, Qs) + end, Self = self(), _ = [gen_listener:cast(Self, {'add_acdc_queue', A, StateName}) || A <- Add], _ = [gen_listener:cast(Self, {'rm_acdc_queue', R}) || R <- Rm], - {'noreply', State}; + {'noreply', State#state{agent_priority=Priority + ,skills=Skills + }}; handle_cast({'fsm_started', FSMPid}, State) -> lager:debug("fsm started: ~p", [FSMPid]), @@ -415,46 +474,41 @@ handle_cast({'fsm_started', FSMPid}, State) -> handle_cast({'gen_listener', {'created_queue', Q}}, State) -> {'noreply', State#state{my_q=Q}, 'hibernate'}; -handle_cast({'add_acdc_queue', Q, StateName}, #state{agent_queues=Qs - ,acct_id=AcctId - ,agent_id=AgentId - }=State) when is_binary(Q) -> +handle_cast({'add_acdc_queue', Q, StateName}, #state{agent_queues=Qs}=State) when is_binary(Q) -> case lists:member(Q, Qs) of 'true' -> lager:debug("queue ~s already added", [Q]), + send_availability_update(Q, StateName, State), {'noreply', State}; 'false' -> - add_queue_binding(AcctId, AgentId, Q, StateName), + add_queue_binding(Q, StateName, State), {'noreply', State#state{agent_queues=[Q|Qs]}} end; handle_cast({'rm_acdc_queue', Q}, #state{agent_queues=[Q] - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId ,fsm_pid=FSM }=State) -> lager:debug("agent logged out of last known queue ~s, logging out", [Q]), - rm_queue_binding(AcctId, AgentId, Q), + rm_queue_binding(AccountId, AgentId, Q), acdc_agent_fsm:agent_logout(FSM), {'noreply', State#state{agent_queues=[]}}; handle_cast({'rm_acdc_queue', Q}, #state{agent_queues=Qs - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId }=State) -> case lists:member(Q, Qs) of 'true' -> - rm_queue_binding(AcctId, AgentId, Q), + rm_queue_binding(AccountId, AgentId, Q), {'noreply', State#state{agent_queues=lists:delete(Q, Qs)}, 'hibernate'}; 'false' -> lager:debug("not logged into queue ~s", [Q]), {'noreply', State} end; -handle_cast('bind_to_member_reqs', #state{agent_queues=Qs - ,acct_id=AcctId - ,agent_id=AgentId - }=State) -> - _ = [add_queue_binding(AcctId, AgentId, Q, 'ready') || Q <- Qs], +handle_cast('bind_to_member_reqs', #state{agent_queues=Qs}=State) -> + _ = [add_queue_binding(Q, 'ready', State) || Q <- Qs], {'noreply', State}; handle_cast({'rebind_events', OldCallId, NewCallId}, State) -> @@ -477,16 +531,18 @@ handle_cast({'channel_hungup', CallId}, #state{call=Call lager:debug("member channel hungup, done with this call"), acdc_util:unbind_from_call_events(Call), - _ = filter_agent_calls(ACallIds, CallId), + ACallIds1 = filter_agent_calls(ACallIds, CallId), kz_log:put_callid(AgentId), case IsThief of 'false' -> {'noreply', State#state{call='undefined' + ,original_call='undefined' ,msg_queue_id='undefined' ,acdc_queue_id='undefined' - ,agent_call_ids=[] + ,agent_call_ids=ACallIds1 ,recording_url='undefined' + ,last_connect=os:timestamp() } ,'hibernate'}; 'true' -> @@ -499,15 +555,15 @@ handle_cast({'channel_hungup', CallId}, #state{call=Call lager:debug("agent channel ~s hungup/needs hanging up", [CallId]), acdc_util:unbind_from_call_events(CallId), {'noreply', State#state{agent_call_ids=lists:delete(CallId, ACallIds)}, 'hibernate'}; - {ACallId, ACtrlQ} -> - lager:debug("agent channel ~s hungup, stop call on ctlq ~s", [ACallId, ACtrlQ]), - acdc_util:unbind_from_call_events(ACallId), - stop_agent_leg(ACallId, ACtrlQ), - {'noreply', State#state{agent_call_ids=props:delete(ACallId, ACallIds)}}; 'undefined' -> lager:debug("unknown call id ~s for channel_hungup, ignoring", [CallId]), lager:debug("listening for call id(~s) and agents (~p)", [CCallId, ACallIds]), - {'noreply', State} + {'noreply', State}; + CtrlQ -> + lager:debug("agent channel ~s hungup, stop call on ctlq ~s", [CallId, CtrlQ]), + acdc_util:unbind_from_call_events(CallId), + stop_agent_leg(CallId, CtrlQ), + {'noreply', State#state{agent_call_ids=props:delete(CallId, ACallIds)}} end end; @@ -516,12 +572,12 @@ handle_cast('agent_timeout', #state{agent_call_ids=ACallIds }=State) -> lager:debug("agent timeout recv, stopping agent call"), - _ = filter_agent_calls(ACallIds, AgentId), + ACallIds1 = filter_agent_calls(ACallIds, AgentId), kz_log:put_callid(AgentId), {'noreply', State#state{msg_queue_id='undefined' ,acdc_queue_id='undefined' - ,agent_call_ids=[] + ,agent_call_ids=ACallIds1 ,call='undefined' } ,'hibernate'}; @@ -536,14 +592,15 @@ handle_cast({'member_connect_retry', CallId}, #state{my_id=MyId lager:debug("need to retry member connect, agent isn't able to take it"), send_member_connect_retry(Server, CallId, MyId, AgentId), - lists:foreach(fun acdc_util:unbind_from_call_events/1, ACallIds), + ACallIds1 = filter_agent_calls(ACallIds, AgentId), acdc_util:unbind_from_call_events(CallId), kz_log:put_callid(AgentId), - {'noreply', State#state{msg_queue_id='undefined' + {'noreply', State#state{original_call='undefined' + ,msg_queue_id='undefined' ,acdc_queue_id='undefined' - ,agent_call_ids=[] + ,agent_call_ids=ACallIds1 ,call='undefined' } ,'hibernate' @@ -561,9 +618,10 @@ handle_cast({'member_connect_retry', WinJObj}, #state{my_id=MyId handle_cast({'bridge_to_member', Call, WinJObj, EPs, CDRUrl, RecordingUrl}, #state{is_thief='false' ,agent_queues=Qs - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId ,my_q=MyQ + ,agent_call_ids=ACallIds ,cdr_urls=Urls ,agent=Agent }=State) -> @@ -577,18 +635,23 @@ handle_cast({'bridge_to_member', Call, WinJObj, EPs, CDRUrl, RecordingUrl}, #sta ,kz_json:is_true(<<"Record-Caller">>, WinJObj, 'false') ), - acdc_util:bind_to_call_events(Call), + AgentCallIds = lists:append(maybe_connect_to_agent(MyQ, EPs, Call, RingTimeout, AgentId, CDRUrl) + ,ACallIds), - AgentCallIds = maybe_connect_to_agent(MyQ, EPs, Call, RingTimeout, AgentId, CDRUrl), + gen_listener:add_binding(self(), 'acdc_agent', [{'callid', call_id(Call)} + ,{'restrict_to', ['stats_req']} + ]), lager:debug("originate sent, waiting on successful bridge now"), - update_my_queues_of_change(AcctId, AgentId, Qs), + update_my_queues_of_change(AccountId, AgentId, Call, Qs), {'noreply', State#state{call=Call + ,acdc_queue_id=kz_json:get_value(<<"Queue-ID">>, WinJObj) ,record_calls=ShouldRecord ,msg_queue_id=kz_json:get_value(<<"Server-ID">>, WinJObj) ,agent_call_ids=AgentCallIds - ,cdr_urls=dict:store(kapps_call:call_id(Call), CDRUrl, - dict:store(AgentCallIds, CDRUrl, Urls) + ,cdr_urls=dict:store(kapps_call:call_id(Call) + ,CDRUrl + ,dict:store(AgentCallIds, CDRUrl, Urls) ) ,recording_url=RecordingUrl } @@ -597,6 +660,7 @@ handle_cast({'bridge_to_member', Call, WinJObj, EPs, CDRUrl, RecordingUrl}, #sta handle_cast({'bridge_to_member', Call, WinJObj, _, CDRUrl, RecordingUrl}, #state{is_thief='true' ,agent=Agent ,agent_id=AgentId + ,agent_call_ids=ACallIds ,cdr_urls=Urls }=State) -> _ = kapps_call:put_callid(Call), @@ -612,19 +676,69 @@ handle_cast({'bridge_to_member', Call, WinJObj, _, CDRUrl, RecordingUrl}, #state kapps_call_command:pickup(kapps_call:call_id(Agent), <<"now">>, Call), {'noreply', State#state{call=Call + ,acdc_queue_id=kz_json:get_value(<<"Queue-ID">>, WinJObj) ,msg_queue_id=kz_json:get_value(<<"Server-ID">>, WinJObj) - ,agent_call_ids=[AgentCallId] - ,cdr_urls=dict:store(kapps_call:call_id(Call), CDRUrl, - dict:store(AgentCallId, CDRUrl, Urls) + ,agent_call_ids=[AgentCallId | ACallIds] + ,cdr_urls=dict:store(kapps_call:call_id(Call) + ,CDRUrl + ,dict:store(AgentCallId, CDRUrl, Urls) ) ,record_calls=ShouldRecord ,recording_url=RecordingUrl } ,'hibernate'}; +handle_cast({'monitor_call', Call, WinJObj, RecordingUrl}, State) -> + _ = kapps_call:put_callid(Call), + + lager:debug("monitoring member call ~s", [kapps_call:call_id(Call)]), + + {'noreply', State#state{call=Call + ,acdc_queue_id=kz_json:get_value(<<"Queue-ID">>, WinJObj) + ,msg_queue_id=kz_json:get_value(<<"Server-ID">>, WinJObj) + ,recording_url=RecordingUrl + } + ,'hibernate'}; + +handle_cast({'originate_callback_to_agent', Call, WinJObj, EPs, CDRUrl, RecordingUrl, Number}, #state{agent_queues=Qs + ,acct_id=AccountId + ,agent_id=AgentId + ,my_q=MyQ + ,agent_call_ids=ACallIds + ,cdr_urls=Urls + ,agent=Agent + }=State) -> + _ = kapps_call:put_callid(Call), + lager:debug("calling agent to begin callback"), + + RingTimeout = kz_json:get_value(<<"Ring-Timeout">>, WinJObj), + lager:debug("ring agent for ~ps", [RingTimeout]), + + ShouldRecord = should_record_endpoints(EPs, record_calls(Agent) + ,kz_json:is_true(<<"Record-Caller">>, WinJObj, 'false') + ), + + AgentCallIds = lists:append(maybe_originate_callback(MyQ, EPs, Call, RingTimeout, AgentId, CDRUrl, Number) + ,ACallIds), + + lager:debug("originate sent, waiting on bridge of agent and callback call"), + update_my_queues_of_change(AccountId, AgentId, Call, Qs), + {'noreply', State#state{call=Call + ,record_calls=ShouldRecord + ,acdc_queue_id=kz_json:get_value(<<"Queue-ID">>, WinJObj) + ,msg_queue_id=kz_json:get_value(<<"Server-ID">>, WinJObj) + ,agent_call_ids=AgentCallIds + ,cdr_urls=dict:store(kapps_call:call_id(Call) + ,CDRUrl + ,dict:store(AgentCallIds, CDRUrl, Urls) + ) + ,recording_url=RecordingUrl + } + ,'hibernate'}; + handle_cast({'member_connect_accepted'}, #state{msg_queue_id=AmqpQueue ,call=Call - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId ,agent_queues=Qs ,my_id=MyId @@ -634,13 +748,13 @@ handle_cast({'member_connect_accepted'}, #state{msg_queue_id=AmqpQueue lager:debug("member bridged to agent! waiting on agent call id though"), maybe_start_recording(Call, ShouldRecord, RecordingUrl), - send_member_connect_accepted(AmqpQueue, call_id(Call), AcctId, AgentId, MyId), - [send_agent_busy(AcctId, AgentId, QueueId) || QueueId <- Qs], + send_member_connect_accepted(AmqpQueue, call_id(Call), AccountId, AgentId, MyId), + _ = [send_agent_busy(AccountId, AgentId, QueueId, Call) || QueueId <- Qs], {'noreply', State}; handle_cast({'member_connect_accepted', ACallId}, #state{msg_queue_id=AmqpQueue ,call=Call - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId ,agent_queues=Qs ,my_id=MyId @@ -655,8 +769,59 @@ handle_cast({'member_connect_accepted', ACallId}, #state{msg_queue_id=AmqpQueue lager:debug("new agent call ids: ~p", [ACallIds1]), - send_member_connect_accepted(AmqpQueue, call_id(Call), AcctId, AgentId, MyId), - [send_agent_busy(AcctId, AgentId, QueueId) || QueueId <- Qs], + send_member_connect_accepted(AmqpQueue, call_id(Call), AccountId, AgentId, MyId), + _ = [send_agent_busy(AccountId, AgentId, QueueId, Call) || QueueId <- Qs], + {'noreply', State#state{agent_call_ids=ACallIds1}, 'hibernate'}; + +handle_cast({'member_connect_accepted', ACallId, NewCall}, #state{msg_queue_id=AmqpQueue + ,call=Call + ,acct_id=AccountId + ,agent_id=AgentId + ,agent_queues=Qs + ,my_id=MyId + ,record_calls=ShouldRecord + ,recording_url=RecordingUrl + ,agent_call_ids=ACallIds + }=State) -> + lager:debug("member's new call bridged to agent!"), + maybe_start_recording(NewCall, ShouldRecord, RecordingUrl), + + ACallIds1 = filter_agent_calls(ACallIds, ACallId), + + lager:debug("new agent call ids: ~p", [ACallIds1]), + + send_member_connect_accepted(AmqpQueue, call_id(Call), call_id(NewCall), AccountId, AgentId, MyId), + _ = [send_agent_busy(AccountId, AgentId, QueueId, Call) || QueueId <- Qs], + {'noreply', State#state{call=NewCall + ,original_call=Call + ,agent_call_ids=ACallIds1 + }, 'hibernate'}; + +handle_cast({'monitor_connect_accepted', ACallId}, #state{agent_call_ids=ACallIds}=State) -> + lager:debug("monitoring ~s", [ACallId]), + {'noreply', State#state{agent_call_ids=[ACallId | ACallIds]}, 'hibernate'}; + +handle_cast({'member_callback_accepted', ACall}, #state{msg_queue_id=AmqpQueue + ,call=Call + ,acct_id=AccountId + ,agent_id=AgentId + ,my_id=MyId + ,agent_call_ids=ACallIds + }=State) -> + lager:debug("agent answered callback, mark call as accepted"), + + ACallId = kapps_call:call_id(ACall), + %% ACallIds1 = filter_agent_calls(ACallIds, ACallId), + ACallIds1 = [ACallId], + + lager:debug("new agent call ids: ~p", [ACallIds1]), + + send_member_callback_accepted(AmqpQueue, call_id(Call), AccountId, AgentId, MyId), + + CtrlQ = props:get_value(ACallId, ACallIds), + ACall1 = kapps_call:set_control_queue(CtrlQ, ACall), + kapps_call_command:audio_macro([{'prompt', <<"queue-now_calling_back">>}], ACall1), + {'noreply', State#state{agent_call_ids=ACallIds1}, 'hibernate'}; handle_cast({'member_connect_resp', ReqJObj}, #state{agent_id=AgentId @@ -674,9 +839,7 @@ handle_cast({'member_connect_resp', ReqJObj}, #state{agent_id=AgentId lager:debug("responding to member_connect_req"), send_member_connect_resp(ReqJObj, MyQ, AgentId, MyId, LastConn), - {'noreply', State#state{acdc_queue_id = ACDcQueue - ,msg_queue_id = kz_json:get_value(<<"Server-ID">>, ReqJObj) - } + {'noreply', State#state{msg_queue_id = kz_json:get_value(<<"Server-ID">>, ReqJObj)} ,'hibernate'} end; @@ -688,7 +851,7 @@ handle_cast({'hangup_call'}, #state{my_id=MyId }=State) -> %% Hangup this agent's calls lager:debug("agent FSM requested a hangup of the agent call, sending retry"), - _ = filter_agent_calls(ACallIds, AgentId), + ACallIds1 = filter_agent_calls(ACallIds, AgentId), %% Pass the call on to another agent CallId = kapps_call:call_id(Call), @@ -699,24 +862,11 @@ handle_cast({'hangup_call'}, #state{my_id=MyId {'noreply', State#state{call='undefined' ,msg_queue_id='undefined' ,acdc_queue_id='undefined' - ,agent_call_ids=[] + ,agent_call_ids=ACallIds1 ,recording_url='undefined' } ,'hibernate'}; -handle_cast({'monitor_call', Call, _CDRUrl, RecordingUrl}, State) -> - _ = kapps_call:put_callid(Call), - - acdc_util:bind_to_call_events(Call), - - lager:debug("monitoring member call ~s", [kapps_call:call_id(Call)]), - - {'noreply', State#state{call=Call - ,agent_call_ids=[] - ,recording_url=RecordingUrl - } - ,'hibernate'}; - handle_cast({'originate_execute', JObj}, #state{my_q=Q}=State) -> lager:debug("execute the originate for agent: ~p", [JObj]), send_originate_execute(JObj, Q), @@ -724,36 +874,51 @@ handle_cast({'originate_execute', JObj}, #state{my_q=Q}=State) -> handle_cast({'originate_uuid', UUID, CtlQ}, #state{agent_call_ids=ACallIds}=State) -> lager:debug("updating ~s with ~s in ~p", [UUID, CtlQ, ACallIds]), - {'noreply', State#state{agent_call_ids=[{UUID, CtlQ} | props:delete(UUID, ACallIds)]}}; + {'noreply', State#state{agent_call_ids=props:set_value(UUID, CtlQ, ACallIds)}}; -handle_cast({'outbound_call', CallId}, #state{agent_id=AgentId - ,acct_id=AcctId - ,agent_queues=Qs - }=State) -> +handle_cast({'outbound_call', CallId, Number, Name}, #state{agent_id=AgentId + ,acct_id=AccountId + ,agent_queues=Qs + }=State) -> + _ = kz_log:put_callid(CallId), + acdc_util:bind_to_call_events(CallId), + Call = kapps_call:set_call_id(CallId, kapps_call:new()), + _ = [send_agent_busy(AccountId, AgentId, QueueId, Call, 'outbound', Number, Name) || QueueId <- Qs], + + lager:debug("bound to agent's outbound call ~s", [CallId]), + {'noreply', State#state{call=Call}, 'hibernate'}; + +handle_cast({'inbound_call', CallId, Number, Name}, #state{agent_id=AgentId + ,acct_id=AccountId + ,agent_queues=Qs + }=State) -> _ = kz_log:put_callid(CallId), acdc_util:bind_to_call_events(CallId), - [send_agent_busy(AcctId, AgentId, QueueId) || QueueId <- Qs], + Call = kapps_call:set_call_id(CallId, kapps_call:new()), + _ = [send_agent_busy(AccountId, AgentId, QueueId, Call, 'inbound', Number, Name) || QueueId <- Qs], lager:debug("bound to agent's outbound call ~s", [CallId]), - {'noreply', State#state{call=kapps_call:set_call_id(CallId, kapps_call:new())}, 'hibernate'}; + {'noreply', State#state{call=Call}, 'hibernate'}; handle_cast('send_agent_available', #state{agent_id=AgentId - ,acct_id=AcctId + ,agent_priority=Priority + ,skills=Skills + ,acct_id=AccountId ,agent_queues=Qs }=State) -> - [send_agent_available(AcctId, AgentId, QueueId) || QueueId <- Qs], + [send_agent_available(AccountId, AgentId, QueueId, Priority, Skills) || QueueId <- Qs], {'noreply', State}; handle_cast('send_agent_busy', #state{agent_id=AgentId - ,acct_id=AcctId + ,acct_id=AccountId ,agent_queues=Qs }=State) -> - [send_agent_busy(AcctId, AgentId, QueueId) || QueueId <- Qs], + [send_agent_busy(AccountId, AgentId, QueueId) || QueueId <- Qs], {'noreply', State}; handle_cast({'send_sync_req'}, #state{my_id=MyId ,my_q=MyQ - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId }=State) -> _ = case MyQ of @@ -762,22 +927,22 @@ handle_cast({'send_sync_req'}, #state{my_id=MyId timer:apply_after(100 , 'gen_listener', 'cast', [self(), {'send_sync_req'}]); _ -> lager:debug("queue retrieved: ~p , sending sync request", [MyQ]), - send_sync_request(AcctId, AgentId, MyId, MyQ) + send_sync_request(AccountId, AgentId, MyId, MyQ) end, {'noreply', State}; handle_cast({'send_sync_resp', Status, ReqJObj, Options}, #state{my_id=MyId - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId ,my_q=MyQ }=State) -> - send_sync_response(ReqJObj, AcctId, AgentId, MyId, MyQ, Status, Options), + send_sync_response(ReqJObj, AccountId, AgentId, MyId, MyQ, Status, Options), {'noreply', State}; -handle_cast({'send_status_update', Status}, #state{acct_id=AcctId +handle_cast({'send_status_update', Status}, #state{acct_id=AccountId ,agent_id=AgentId }=State) -> - send_status_update(AcctId, AgentId, Status), + send_status_update(AccountId, AgentId, Status), {'noreply', State}; handle_cast('call_status_req', #state{call=Call, my_q=Q}=State) -> @@ -788,7 +953,7 @@ handle_cast('call_status_req', #state{call=Call, my_q=Q}=State) -> | kz_api:default_headers(Q, ?APP_NAME, ?APP_VERSION) ], - _ = kapi_call:publish_channel_status_req(Command), + kapi_call:publish_channel_status_req(CallId, Command), {'noreply', State}; handle_cast({'call_status_req', CallId}, #state{my_q=Q}=State) when is_binary(CallId) -> @@ -796,7 +961,7 @@ handle_cast({'call_status_req', CallId}, #state{my_q=Q}=State) when is_binary(Ca ,{<<"Server-ID">>, Q} | kz_api:default_headers(Q, ?APP_NAME, ?APP_VERSION) ], - _ = kapi_call:publish_channel_status_req(Command), + kapi_call:publish_channel_status_req(CallId, Command), {'noreply', State}; handle_cast({'call_status_req', Call}, State) -> handle_cast({'call_status_req', kapps_call:call_id(Call)}, State); @@ -804,11 +969,11 @@ handle_cast({'call_status_req', Call}, State) -> handle_cast({'remove_cdr_urls', CallId}, #state{cdr_urls=Urls}=State) -> {'noreply', State#state{cdr_urls=dict:erase(CallId, Urls)}, 'hibernate'}; -handle_cast('logout_agent', #state{acct_id=AcctId +handle_cast('logout_agent', #state{acct_id=AccountId ,agent_id=AgentId }=State) -> Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), @@ -823,24 +988,24 @@ handle_cast({'presence_id', PresenceId}, #state{agent_presence_id=_Id}=State) -> lager:debug("updating presence id from ~s to ~s", [_Id, PresenceId]), {'noreply', State#state{agent_presence_id=PresenceId}}; -handle_cast({'presence_update', PresenceState}, #state{acct_id=AcctId +handle_cast({'presence_update', PresenceState}, #state{acct_id=AccountId ,agent_presence_id='undefined' ,agent_id=AgentId }=State) -> lager:debug("no custom presence id, using ~s for ~s", [AgentId, PresenceState]), - acdc_util:presence_update(AcctId, AgentId, PresenceState), + acdc_util:presence_update(AccountId, AgentId, PresenceState), {'noreply', State}; -handle_cast({'presence_update', PresenceState}, #state{acct_id=AcctId +handle_cast({'presence_update', PresenceState}, #state{acct_id=AccountId ,agent_presence_id=PresenceId }=State) -> lager:debug("custom presence id, using ~s for ~s", [PresenceId, PresenceState]), - acdc_util:presence_update(AcctId, PresenceId, PresenceState), + acdc_util:presence_update(AccountId, PresenceId, PresenceState), {'noreply', State}; handle_cast({'update_status', Status}, #state{agent_id=AgentId - ,acct_id=AcctId + ,acct_id=AccountId }=State) -> - catch acdc_agent_util:update_status(AcctId, AgentId, Status), + catch acdc_agent_util:update_status(AccountId, AgentId, Status), {'noreply', State}; handle_cast({'gen_listener',{'is_consuming',_IsConsuming}}, State) -> @@ -851,63 +1016,63 @@ handle_cast(_Msg, State) -> {'noreply', State, 'hibernate'}. %%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. +%% @private +%% @doc Handling all non call/cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +-spec handle_info(any(), state()) -> kz_term:handle_info_ret_state(state()). handle_info(_Info, State) -> lager:debug("unhandled message: ~p", [_Info]), {'noreply', State}. %%------------------------------------------------------------------------------ +%% @private %% @doc Handling all messages from the message bus +%% %% @end %%------------------------------------------------------------------------------ -spec handle_event(kz_json:object(), state()) -> gen_listener:handle_event_return(). handle_event(_JObj, #state{fsm_pid='undefined'}) -> 'ignore'; handle_event(_JObj, #state{fsm_pid=FSM ,agent_id=AgentId - ,acct_id=AcctId + ,acct_id=AccountId ,cdr_urls=Urls + ,agent_call_ids=AgentCallIds }) -> {'reply', [{'fsm_pid', FSM} ,{'agent_id', AgentId} - ,{'acct_id', AcctId} + ,{'acct_id', AccountId} ,{'cdr_urls', Urls} + ,{'agent_call_ids', AgentCallIds} ]}. %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_server' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_server' terminates +%% @private +%% @doc This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @end %%------------------------------------------------------------------------------ -spec terminate(any(), state()) -> 'ok'. terminate(Reason, #state{agent_queues=Queues - ,acct_id=AcctId + ,acct_id=AccountId ,agent_id=AgentId } ) when Reason == 'normal'; Reason == 'shutdown' -> - _ = [rm_queue_binding(AcctId, AgentId, QueueId) || QueueId <- Queues], - maybe_stop_agent(Reason, AcctId, AgentId), + _ = [rm_queue_binding(AccountId, AgentId, QueueId) || QueueId <- Queues], + Reason =:= 'normal' %% Prevent race condition of supervisor delete_child/restart_child + andalso kz_process:spawn(fun acdc_agents_sup:stop_agent/2, [AccountId, AgentId]), lager:debug("agent process going down: ~p", [Reason]); terminate(_Reason, _State) -> lager:debug("agent process going down: ~p", [_Reason]). -%% Prevent race condition of supervisor delete_child/restart_child -maybe_stop_agent('normal', AccountId, AgentId) -> - stop_agent(AccountId, AgentId); -maybe_stop_agent(_Reason, _AccountId, _AgentId) -> - 'ok'. - -stop_agent(AccountId, AgentId) -> - kz_process:spawn(fun acdc_agents_sup:stop_agent/2, [AccountId, AgentId]), - 'ok'. - %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), state(), any()) -> {'ok', state()}. @@ -927,10 +1092,10 @@ is_valid_queue(Q, Qs) -> lists:member(Q, Qs). -spec send_member_connect_resp(kz_json:object(), kz_term:ne_binary() ,kz_term:ne_binary(), kz_term:ne_binary() - , kz_time:start_time() | 'undefined' + , kz_term:kz_now() | 'undefined' ) -> 'ok'. send_member_connect_resp(JObj, MyQ, AgentId, MyId, LastConn) -> - Queue = kz_api:server_id(JObj), + Queue = kz_json:get_value(<<"Server-ID">>, JObj), IdleTime = idle_time(LastConn), Resp = props:filter_undefined( [{<<"Agent-ID">>, AgentId} @@ -944,7 +1109,7 @@ send_member_connect_resp(JObj, MyQ, AgentId, MyId, LastConn) -> -spec send_member_connect_retry(kz_json:object(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. send_member_connect_retry(JObj, MyId, AgentId) -> - send_member_connect_retry(kz_api:server_id(JObj) + send_member_connect_retry(kz_json:get_value(<<"Server-ID">>, JObj) ,call_id(JObj) ,MyId ,AgentId @@ -960,57 +1125,79 @@ send_member_connect_retry(Queue, CallId, MyId, AgentId) -> ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - kapi_acdc_queue:publish_member_connect_retry(Queue, Resp). + %% Delay the retry by 0.5 secs to allow agents to settle after RING timeout + timer:apply_after(2000, kapi_acdc_queue, publish_member_connect_retry, [Queue, Resp]). -spec send_member_connect_accepted(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -send_member_connect_accepted(Queue, CallId, AcctId, AgentId, MyId) -> +send_member_connect_accepted(Queue, CallId, AccountId, AgentId, MyId) -> Resp = props:filter_undefined([{<<"Call-ID">>, CallId} - ,{<<"Account-ID">>, AcctId} + ,{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Process-ID">>, MyId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + kapi_acdc_queue:publish_member_connect_accepted(Queue, Resp). + +-spec send_member_connect_accepted(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +send_member_connect_accepted(Queue, CallId, NewCallId, AccountId, AgentId, MyId) -> + Resp = props:filter_undefined([{<<"Call-ID">>, NewCallId} + ,{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Process-ID">>, MyId} + ,{<<"Old-Call-ID">>, CallId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), kapi_acdc_queue:publish_member_connect_accepted(Queue, Resp). +-spec send_member_callback_accepted(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +send_member_callback_accepted(Queue, CallId, AccountId, AgentId, MyId) -> + Resp = props:filter_undefined([{<<"Call-ID">>, CallId} + ,{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Process-ID">>, MyId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + kapi_acdc_queue:publish_member_callback_accepted(Queue, Resp). + -spec send_originate_execute(kz_json:object(), kz_term:ne_binary()) -> 'ok'. send_originate_execute(JObj, Q) -> - Prop = [{<<"Call-ID">>, kz_api:call_id(JObj)} - ,{<<"Msg-ID">>, kz_api:msg_id(JObj)} + Prop = [{<<"Call-ID">>, kz_json:get_value(<<"Call-ID">>, JObj)} + ,{<<"Msg-ID">>, kz_json:get_value(<<"Msg-ID">>, JObj)} | kz_api:default_headers(Q, ?APP_NAME, ?APP_VERSION) ], - kapi_dialplan:publish_originate_execute(kz_api:server_id(JObj), Prop). + kapi_dialplan:publish_originate_execute(kz_json:get_value(<<"Server-ID">>, JObj), Prop). -spec send_sync_request(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -send_sync_request(AcctId, AgentId, MyId, MyQ) -> - Prop = [{<<"Account-ID">>, AcctId} +send_sync_request(AccountId, AgentId, MyId, MyQ) -> + Prop = [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Process-ID">>, MyId} | kz_api:default_headers(MyQ, ?APP_NAME, ?APP_VERSION) ], kapi_acdc_agent:publish_sync_req(Prop). -send_sync_response(ReqJObj, AcctId, AgentId, MyId, MyQ, Status, Options) -> - Prop = [{<<"Account-ID">>, AcctId} +send_sync_response(ReqJObj, AccountId, AgentId, MyId, MyQ, Status, Options) -> + Prop = [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Process-ID">>, MyId} ,{<<"Status">>, kz_term:to_binary(Status)} - ,{<<"Msg-ID">>, kz_api:msg_id(ReqJObj)} + ,{<<"Msg-ID">>, kz_json:get_value(<<"Msg-ID">>, ReqJObj)} | Options ++ kz_api:default_headers(MyQ, ?APP_NAME, ?APP_VERSION) ], - Q = kz_api:server_id(ReqJObj), + Q = kz_json:get_value(<<"Server-ID">>, ReqJObj), lager:debug("sending sync resp to ~s", [Q]), kapi_acdc_agent:publish_sync_resp(Q, Prop). -send_status_update(AcctId, AgentId, 'resume') -> +send_status_update(AccountId, AgentId, 'resume') -> Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), kapi_acdc_agent:publish_resume(Update). --spec idle_time('undefined' | kz_time:start_time()) -> kz_term:api_integer(). +-spec idle_time('undefined' | kz_term:kz_now()) -> kz_term:api_integer(). idle_time('undefined') -> 'undefined'; idle_time(T) -> kz_time:elapsed_s(T). @@ -1031,15 +1218,15 @@ call_id(Call) -> end. -spec maybe_connect_to_agent(kz_term:ne_binary(), kz_json:objects(), kapps_call:call(), kz_term:api_integer(), kz_term:ne_binary(), kz_term:api_binary()) -> - kz_term:ne_binaries(). + kz_term:proplist(). maybe_connect_to_agent(MyQ, EPs, Call, Timeout, AgentId, _CdrUrl) -> MCallId = kapps_call:call_id(Call), kz_log:put_callid(MCallId), ReqId = kz_binary:rand_hex(6), - AcctId = kapps_call:account_id(Call), + AccountId = kapps_call:account_id(Call), - CCVs = props:filter_undefined([{<<"Account-ID">>, AcctId} + CCVs = props:filter_undefined([{<<"Account-ID">>, AccountId} ,{<<"Authorizing-ID">>, kapps_call:authorizing_id(Call)} ,{<<"Request-ID">>, ReqId} ,{<<"Retain-CID">>, <<"true">>} @@ -1060,6 +1247,8 @@ maybe_connect_to_agent(MyQ, EPs, Call, Timeout, AgentId, _CdrUrl) -> ]} end, {[], []}, EPs), + {CIDNumber, CIDName} = acdc_util:caller_id(Call), + Prop = props:filter_undefined( [{<<"Msg-ID">>, kz_binary:rand_hex(6)} ,{<<"Custom-Channel-Vars">>, kz_json:from_list(CCVs)} @@ -1070,22 +1259,87 @@ maybe_connect_to_agent(MyQ, EPs, Call, Timeout, AgentId, _CdrUrl) -> ,<<"Authorizing-ID">> ,<<"Authorizing-Type">> ]} - ,{<<"Account-ID">>, AcctId} + ,{<<"Account-ID">>, AccountId} ,{<<"Resource-Type">>, <<"originate">>} ,{<<"Application-Name">>, <<"bridge">>} - ,{<<"Caller-ID-Name">>, kapps_call:caller_id_name(Call)} - ,{<<"Caller-ID-Number">>, kapps_call:caller_id_number(Call)} - ,{<<"Outbound-Caller-ID-Name">>, kapps_call:caller_id_name(Call)} - ,{<<"Outbound-Caller-ID-Number">>, kapps_call:caller_id_number(Call)} + ,{<<"Caller-ID-Name">>, CIDName} + ,{<<"Caller-ID-Number">>, CIDNumber} + ,{<<"Outbound-Caller-ID-Name">>, CIDName} + ,{<<"Outbound-Caller-ID-Number">>, CIDNumber} ,{<<"Existing-Call-ID">>, kapps_call:call_id(Call)} ,{<<"Dial-Endpoint-Method">>, <<"simultaneous">>} + ,{<<"Ignore-Early-Media">>, <<"true">>} | kz_api:default_headers(MyQ, ?APP_NAME, ?APP_VERSION) ]), lager:debug("sending originate request with agent call-ids ~p", [ACallIds]), kapi_resource:publish_originate_req(Prop), - ACallIds. + lists:map(fun(ACallId) -> {ACallId, 'undefined'} end, ACallIds). + +-spec maybe_originate_callback(kz_term:ne_binary(), kz_json:objects(), kapps_call:call(), kz_term:api_integer(), kz_term:ne_binary(), kz_term:api_binary() + ,kz_json:object()) -> + kz_term:proplist(). +maybe_originate_callback(MyQ, EPs, Call, Timeout, AgentId, _CdrUrl, Details) -> + MCallId = kapps_call:call_id(Call), + put('callid', MCallId), + + ReqId = kz_binary:rand_hex(6), + AccountId = kapps_call:account_id(Call), + + CCVs = props:filter_undefined([{<<"Account-ID">>, AccountId} + ,{<<"Authorizing-ID">>, kapps_call:authorizing_id(Call)} + ,{<<"Authorizing-Type">>, <<"user">>} + ,{<<"Request-ID">>, ReqId} + ,{<<"Retain-CID">>, <<"true">>} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Member-Call-ID">>, MCallId} + ,{<<"Callback-Number">>, kz_json:get_value(<<"Callback-Number">>, Details)} + ]), + + {ACallIds, Endpoints} = lists:foldl(fun(EP, {Cs, Es}) -> + ACallId = outbound_call_id(Call, AgentId), + acdc_util:bind_to_call_events(ACallId), + + {[ACallId | Cs] + ,[kz_json:set_values([{<<"Endpoint-Timeout">>, Timeout} + ,{<<"Outbound-Call-ID">>, ACallId} + ], EP) + | Es + ]} + end, {[], []}, EPs), + + {CIDNumber, CIDName} = acdc_util:caller_id(Call), + + Prop = props:filter_undefined([{<<"Application-Name">>, <<"park">>} + ,{<<"Resource-Type">>, <<"originate">>} + ,{<<"Account-ID">>, AccountId} + ,{<<"Endpoints">>, Endpoints} + ,{<<"Msg-ID">>, kz_binary:rand_hex(6)} + ,{<<"Timeout">>, Timeout} + ,{<<"Ignore-Display-Updates">>, <<"true">>} + ,{<<"Ignore-Early-Media">>, <<"true">>} + ,{<<"Caller-ID-Name">>, CIDName} + ,{<<"Caller-ID-Number">>, CIDNumber} + ,{<<"Outbound-Caller-ID-Name">>, CIDName} + ,{<<"Outbound-Caller-ID-Number">>, CIDNumber} + ,{<<"Dial-Endpoint-Method">>, <<"simultaneous">>} + ,{<<"Continue-On-Fail">>, 'false'} + ,{<<"Custom-Channel-Vars">>, kz_json:from_list(CCVs)} + ,{<<"Export-Custom-Channel-Vars">>, [<<"Account-ID">> + ,<<"Retain-CID">> + ,<<"Authorizing-ID">> + ,<<"Authorizing-Type">> + ,<<"Callback-Number">> + ]} + ,{<<"Originate-Immediate">>, <<"true">>} + | kz_api:default_headers(MyQ, ?APP_NAME, ?APP_VERSION) + ]), + + lager:debug("sending originate request with agent call-ids ~p", [ACallIds]), + + kapi_resource:publish_originate_req(Prop), + lists:map(fun(ACallId) -> {ACallId, 'undefined'} end, ACallIds). -spec outbound_call_id(kapps_call:call() | kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). outbound_call_id(CallId, AgentId) when is_binary(CallId) -> @@ -1095,59 +1349,123 @@ outbound_call_id(CallId, AgentId) when is_binary(CallId) -> outbound_call_id(Call, AgentId) -> outbound_call_id(kapps_call:call_id(Call), AgentId). --spec add_queue_binding(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), fsm_state_name()) -> - 'ok'. -add_queue_binding(AcctId, AgentId, QueueId, StateName) -> +%%------------------------------------------------------------------------------ +%% @doc Complete a callback to the callback number (in CCV) +%% Returns a target call id that has been hooked for events +%% @end +%%------------------------------------------------------------------------------ +-spec do_originate_callback_return(kz_term:ne_binary(), kapps_call:call()) -> kz_term:ne_binary(). +do_originate_callback_return(MyQ, Call) -> + MsgId = kz_binary:rand_hex(4), + + Extension = kapps_call:custom_channel_var(<<"Callback-Number">>, Call), + TransferorLeg = kapps_call:call_id(Call), + FromUser = kapps_call:to_user(Call), + + CCVs = props:filter_undefined( + [{<<"Account-ID">>, kapps_call:account_id(Call)} + ,{<<"Authorizing-ID">>, kapps_call:authorizing_id(Call)} + ,{<<"Authorizing-Type">>, kapps_call:authorizing_type(Call)} + ,{<<"Channel-Authorized">>, 'true'} + ,{<<"From-URI">>, <>} + ,{<<"Retain-CID">>, 'true'} + ]), + + TargetCallId = create_call_id(), + acdc_util:bind_to_call_events(TargetCallId), + + Endpoint = kz_json:from_list( + props:filter_undefined( + [{<<"Invite-Format">>, <<"loopback">>} + ,{<<"Route">>, Extension} + ,{<<"To-DID">>, Extension} + ,{<<"To-Realm">>, kapps_call:account_realm(Call)} + ,{<<"Custom-Channel-Vars">>, kz_json:from_list(CCVs)} + ,{<<"Outbound-Call-ID">>, TargetCallId} + ,{<<"Existing-Call-ID">>, TransferorLeg} + ])), + + Request = props:filter_undefined( + [{<<"Endpoints">>, [Endpoint]} + ,{<<"Outbound-Call-ID">>, TargetCallId} + ,{<<"Dial-Endpoint-Method">>, <<"single">>} + ,{<<"Msg-ID">>, MsgId} + ,{<<"Continue-On-Fail">>, 'true'} + ,{<<"Custom-Channel-Vars">>, kz_json:from_list(CCVs)} + ,{<<"Export-Custom-Channel-Vars">>, [<<"Account-ID">>, <<"Retain-CID">> + ,<<"Authorizing-Type">>, <<"Authorizing-ID">> + ,<<"Channel-Authorized">> + ]} + ,{<<"Application-Name">>, <<"bridge">>} + ,{<<"Timeout">>, 60} + + ,{<<"Outbound-Caller-ID-Name">>, kapps_call:callee_id_number(Call)} + ,{<<"Outbound-Caller-ID-Number">>, kapps_call:callee_id_number(Call)} + ,{<<"Caller-ID-Name">>, kapps_call:callee_id_number(Call)} + ,{<<"Caller-ID-Number">>, kapps_call:callee_id_number(Call)} + + ,{<<"Existing-Call-ID">>, TransferorLeg} + ,{<<"Resource-Type">>, <<"originate">>} + ,{<<"Originate-Immediate">>, 'true'} + | kz_api:default_headers(MyQ, ?APP_NAME, ?APP_VERSION) + ]), + + kapi_resource:publish_originate_req(Request), + TargetCallId. + +-spec create_call_id() -> kz_term:ne_binary(). +create_call_id() -> + <<"callback-", (kz_binary:rand_hex(4))/binary>>. + +-spec add_queue_binding(kz_term:ne_binary(), fsm_state_name(), state()) -> 'ok'. +add_queue_binding(QueueId, StateName, #state{acct_id=AccountId}=State) -> lager:debug("adding queue binding for ~s", [QueueId]), - Body = kz_json:from_list([{<<"agent_id">>, AgentId} - ,{<<"queue_id">>, QueueId} - ,{<<"event">>, <<"logged_into_queue">>} - ]), - kz_edr:event(?APP_NAME, ?APP_VERSION, 'ok', 'info', Body, AcctId), gen_listener:add_binding(self() ,'acdc_queue' ,[{'restrict_to', ['member_connect_req']} ,{'queue_id', QueueId} - ,{'account_id', AcctId} + ,{'account_id', AccountId} ]), - send_availability_update(AcctId, AgentId, QueueId, StateName). + send_availability_update(QueueId, StateName, State). -spec rm_queue_binding(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -rm_queue_binding(AcctId, AgentId, QueueId) -> +rm_queue_binding(AccountId, AgentId, QueueId) -> lager:debug("removing queue binding for ~s", [QueueId]), - Body = kz_json:from_list([{<<"agent_id">>, AgentId} - ,{<<"queue_id">>, QueueId} - ,{<<"event">>, <<"logged_out_of_queue">>} - ]), - kz_edr:event(?APP_NAME, ?APP_VERSION, 'ok', 'info', Body, AcctId), gen_listener:rm_binding(self() ,'acdc_queue' ,[{'restrict_to', ['member_connect_req']} ,{'queue_id', QueueId} - ,{'account_id', AcctId} + ,{'account_id', AccountId} ]), - send_agent_unavailable(AcctId, AgentId, QueueId). - --spec send_availability_update(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), fsm_state_name()) -> - 'ok'. -send_availability_update(AcctId, AgentId, QueueId, 'ready') -> - send_agent_available(AcctId, AgentId, QueueId); -send_availability_update(AcctId, AgentId, QueueId, _) -> - send_agent_busy(AcctId, AgentId, QueueId). - --spec send_agent_available(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -send_agent_available(AcctId, AgentId, QueueId) -> - Prop = [{<<"Account-ID">>, AcctId} + send_agent_unavailable(AccountId, AgentId, QueueId). + +-spec send_availability_update(kz_term:ne_binary(), fsm_state_name(), state()) -> 'ok'. +send_availability_update(QueueId, 'ready', #state{agent_id=AgentId + ,agent_priority=Priority + ,skills=Skills + ,acct_id=AccountId + }) -> + send_agent_available(AccountId, AgentId, QueueId, Priority, Skills); +send_availability_update(QueueId, _, #state{agent_id=AgentId + ,acct_id=AccountId + }) -> + send_agent_busy(AccountId, AgentId, QueueId). + +-spec send_agent_available(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), agent_priority(), kz_term:ne_binaries()) -> 'ok'. +send_agent_available(AccountId, AgentId, QueueId, Priority, Skills) -> + Prop = [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Queue-ID">>, QueueId} + ,{<<"Priority">>, Priority} + ,{<<"Skills">>, Skills} ,{<<"Change">>, <<"available">>} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], kapi_acdc_queue:publish_agent_change(Prop). -spec send_agent_busy(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -send_agent_busy(AcctId, AgentId, QueueId) -> - Prop = [{<<"Account-ID">>, AcctId} +send_agent_busy(AccountId, AgentId, QueueId) -> + Prop = [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Queue-ID">>, QueueId} ,{<<"Change">>, <<"busy">>} @@ -1155,9 +1473,42 @@ send_agent_busy(AcctId, AgentId, QueueId) -> ], kapi_acdc_queue:publish_agent_change(Prop). +send_agent_busy(AccountId, AgentId, QueueId, Call) -> + Direction = kapps_call:direction(Call), + {CIDNumber, CIDName} = acdc_util:caller_id(Call), + send_agent_busy(AccountId, AgentId, QueueId, Call, Direction, CIDNumber, CIDName). + +send_agent_busy(AccountId, AgentId, QueueId, _Call, <<"outbound">>, Number, Name) -> + send_agent_busy(AccountId, AgentId, QueueId, _Call, 'outbound', Number, Name); +send_agent_busy(AccountId, AgentId, QueueId, _Call, 'outbound', Number, Name) -> + Prop = [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Change">>, <<"busy">>} + ,{<<"Call-Direction">>, <<"outbound">>} + ,{<<"Callee-ID-Number">>, Number} + ,{<<"Callee-ID-Name">>, Name} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_queue:publish_agent_change(Prop); +send_agent_busy(AccountId, AgentId, QueueId, _Call, <<"inbound">>, Number, Name) -> + send_agent_busy(AccountId, AgentId, QueueId, _Call, 'inbound', Number, Name); +send_agent_busy(AccountId, AgentId, QueueId, _Call, 'inbound', Number, Name) -> + Prop = [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Change">>, <<"busy">>} + ,{<<"Call-Direction">>, <<"inbound">>} + ,{<<"Caller-ID-Number">>, Number} + ,{<<"Caller-ID-Name">>, Name} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_queue:publish_agent_change(Prop). + + -spec send_agent_unavailable(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -send_agent_unavailable(AcctId, AgentId, QueueId) -> - Prop = [{<<"Account-ID">>, AcctId} +send_agent_unavailable(AccountId, AgentId, QueueId) -> + Prop = [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Queue-ID">>, QueueId} ,{<<"Change">>, <<"unavailable">>} @@ -1165,9 +1516,12 @@ send_agent_unavailable(AcctId, AgentId, QueueId) -> ], kapi_acdc_queue:publish_agent_change(Prop). -update_my_queues_of_change(AcctId, AgentId, Qs) -> - Props = [{<<"Account-ID">>, AcctId} +update_my_queues_of_change(AccountId, AgentId, Call, Qs) -> + {CIDNumber, CIDName} = acdc_util:caller_id(Call), + Props = [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} + ,{<<"Caller-ID-Number">>, CIDNumber} + ,{<<"Caller-ID-Name">>, CIDName} ,{<<"Change">>, <<"ringing">>} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], @@ -1176,6 +1530,12 @@ update_my_queues_of_change(AcctId, AgentId, Qs) -> ], 'ok'. +-spec is_prio_or_skills_updated(agent_priority(), kz_term:ne_binaries(), agent_priority(), kz_term:ne_binaries()) -> + boolean(). +is_prio_or_skills_updated(Priority0, Skills0, Priority, Skills) -> + Priority0 =/= Priority + orelse Skills0 =/= Skills. + -spec should_record_endpoints(kz_json:objects(), boolean(), kz_term:api_boolean()) -> boolean(). should_record_endpoints(_EPs, 'true', _) -> 'true'; should_record_endpoints(_EPs, 'false', 'true') -> 'true'; @@ -1205,28 +1565,28 @@ recording_format() -> -spec agent_id(agent()) -> kz_term:api_binary(). agent_id(Agent) -> - case kz_json:is_json_object(Agent) of - 'true' -> kz_doc:id(Agent); - 'false' -> kapps_call:owner_id(Agent) + case is_thief(Agent) of + 'true' -> kapps_call:owner_id(Agent); + 'false' -> kz_doc:id(Agent) end. -spec account_id(agent()) -> kz_term:api_binary(). account_id(Agent) -> - case kz_json:is_json_object(Agent) of - 'true' -> find_account_id(Agent); - 'false' -> kapps_call:account_id(Agent) + case is_thief(Agent) of + 'true' -> kapps_call:account_id(Agent); + 'false' -> find_account_id(Agent) end. -spec account_db(agent()) -> kz_term:api_binary(). account_db(Agent) -> - case kz_json:is_json_object(Agent) of - 'true' -> kz_doc:account_db(Agent); - 'false' -> kapps_call:account_db(Agent) + case is_thief(Agent) of + 'true' -> kapps_call:account_db(Agent); + 'false' -> kz_doc:account_db(Agent) end. -spec record_calls(agent()) -> boolean(). record_calls(Agent) -> - kz_json:is_json_object(Agent) + not is_thief(Agent) andalso kz_json:is_true(<<"record_calls">>, Agent, 'false'). -spec is_thief(agent()) -> boolean(). @@ -1245,19 +1605,27 @@ stop_agent_leg(ACallId, ACtrlQ) -> lager:debug("sending hangup to ~s: ~s", [ACallId, ACtrlQ]), kapi_dialplan:publish_command(ACtrlQ, Command). +-spec find_account_id(kz_json:object()) -> kz_term:api_ne_binary(). find_account_id(JObj) -> case kz_doc:account_id(JObj) of 'undefined' -> kzs_util:format_account_id(kz_doc:account_db(JObj)); - AcctId -> AcctId + AccountId -> AccountId end. -spec filter_agent_calls(kz_term:proplist(), kz_term:ne_binary()) -> kz_term:proplist(). filter_agent_calls(ACallIds, ACallId) -> - lists:filter(fun({ACancelId, ACtrlQ}) when ACancelId =/= ACallId -> + %% These calls should be cancelled, but need to wait for CtrlQ + lists:filter(fun({ACancelId, 'undefined'}) when ACancelId =/= ACallId -> + lager:debug("~s will have to be cancelled when ctrl queue arrives" + ,[ACancelId]), + 'true'; + %% Cancel all calls =/= ACallId that have CtrlQs + ({ACancelId, ACtrlQ}) when ACancelId =/= ACallId -> lager:debug("cancelling and stopping leg ~s", [ACancelId]), acdc_util:unbind_from_call_events(ACancelId), stop_agent_leg(ACancelId, ACtrlQ), 'false'; + %% Keep ACallId ({_, _}) -> 'true'; (ACancelId) when ACancelId =/= ACallId -> lager:debug("cancelling leg ~s", [ACancelId]), diff --git a/applications/acdc/src/acdc_agent_maintenance.erl b/applications/acdc/src/acdc_agent_maintenance.erl index 9573fd7385c..8af5a3b2e4b 100644 --- a/applications/acdc/src/acdc_agent_maintenance.erl +++ b/applications/acdc/src/acdc_agent_maintenance.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2013-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -26,35 +25,35 @@ status() -> acdc_agents_sup:status(). -spec acct_status(kz_term:text()) -> 'ok'. -acct_status(AcctId) when not is_binary(AcctId) -> - acct_status(kz_term:to_binary(AcctId)); -acct_status(AcctId) -> - case acdc_agents_sup:find_acct_supervisors(AcctId) of - [] -> lager:info("no agents with account id ~s available", [AcctId]); +acct_status(AccountId) when not is_binary(AccountId) -> + acct_status(kz_term:to_binary(AccountId)); +acct_status(AccountId) -> + case acdc_agents_sup:find_acct_supervisors(AccountId) of + [] -> lager:info("no agents with account id ~s available", [AccountId]); As -> - lager:info("agent Statuses in ~s", [AcctId]), + lager:info("agent Statuses in ~s", [AccountId]), lists:foreach(fun acdc_agent_sup:status/1, As) end. -spec agent_status(kz_term:text(), kz_term:text()) -> 'ok'. -agent_status(AcctId, AgentId) when not is_binary(AcctId); - not is_binary(AgentId) -> - agent_status(kz_term:to_binary(AcctId), kz_term:to_binary(AgentId)); -agent_status(AcctId, AgentId) -> - case acdc_agents_sup:find_agent_supervisor(AcctId, AgentId) of - 'undefined' -> lager:info("no agent ~s in account ~s available", [AgentId, AcctId]); +agent_status(AccountId, AgentId) when not is_binary(AccountId); + not is_binary(AgentId) -> + agent_status(kz_term:to_binary(AccountId), kz_term:to_binary(AgentId)); +agent_status(AccountId, AgentId) -> + case acdc_agents_sup:find_agent_supervisor(AccountId, AgentId) of + 'undefined' -> lager:info("no agent ~s in account ~s available", [AgentId, AccountId]); S -> acdc_agent_sup:status(S) end. -spec acct_restart(kz_term:text()) -> [kz_types:sup_startchild_ret()]. -acct_restart(AcctId) when not is_binary(AcctId) -> - acct_restart(kz_term:to_binary(AcctId)); -acct_restart(AcctId) -> - acdc_agents_sup:restart_acct(AcctId). +acct_restart(AccountId) when not is_binary(AccountId) -> + acct_restart(kz_term:to_binary(AccountId)); +acct_restart(AccountId) -> + acdc_agents_sup:restart_acct(AccountId). -spec agent_restart(kz_term:text(), kz_term:text()) -> kz_types:sup_startchild_ret(). -agent_restart(AcctId, AgentId) when not is_binary(AcctId); - not is_binary(AgentId) -> - agent_restart(kz_term:to_binary(AcctId), kz_term:to_binary(AgentId)); -agent_restart(AcctId, AgentId) -> - acdc_agents_sup:restart_agent(AcctId, AgentId). +agent_restart(AccountId, AgentId) when not is_binary(AccountId); + not is_binary(AgentId) -> + agent_restart(kz_term:to_binary(AccountId), kz_term:to_binary(AgentId)); +agent_restart(AccountId, AgentId) -> + acdc_agents_sup:restart_agent(AccountId, AgentId). \ No newline at end of file diff --git a/applications/acdc/src/acdc_agent_manager.erl b/applications/acdc/src/acdc_agent_manager.erl index ba6deb09146..efc275a168c 100644 --- a/applications/acdc/src/acdc_agent_manager.erl +++ b/applications/acdc/src/acdc_agent_manager.erl @@ -7,7 +7,7 @@ %%% and more!!! %%% %%% @author James Aimonetti -%%% +%%% @author Daniel Finke %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -18,7 +18,9 @@ -behaviour(gen_listener). %% API --export([start_link/0]). +-export([start_link/0 + ,start_agent/3 + ]). %% gen_server callbacks -export([init/1 @@ -38,7 +40,7 @@ -define(SERVER, ?MODULE). --define(BINDINGS, [{'acdc_agent', [{'restrict_to', ['status', 'stats_req']}]} +-define(BINDINGS, [{'acdc_agent', [{'restrict_to', ['status']}]} ,{'presence', [{'restrict_to', ['probe']}]} ,{'conf', [{'type', <<"user">>} ,'federate' @@ -55,6 +57,7 @@ ,{<<"agent">>, <<"end_wrapup">>} ,{<<"agent">>, <<"login_queue">>} ,{<<"agent">>, <<"logout_queue">>} + ,{<<"agent">>, <<"restart">>} ] } ,{{'acdc_agent_handler', 'handle_stats_req'} @@ -73,10 +76,10 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the server. +%% @doc Starts the server %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> gen_listener:start_link({'local', ?SERVER}, ?MODULE ,[{'bindings', ?BINDINGS} @@ -84,13 +87,22 @@ start_link() -> ] ,[] ). +%%------------------------------------------------------------------------------ +%% @doc Start a new agent supervisor +%% +%% @end +%%------------------------------------------------------------------------------ +-spec start_agent(kz_term:ne_binary(), kz_term:ne_binary(), list()) -> kz_term:sup_startchild_ret(). +start_agent(AccountId, AgentId, Args) -> + gen_listener:call(?SERVER, {'start_agent', AccountId, AgentId, Args}). %%%============================================================================= %%% gen_server callbacks %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Initializes the server. +%% @private +%% @doc Initializes the server %% @end %%------------------------------------------------------------------------------ -spec init([]) -> {'ok', state()}. @@ -99,19 +111,31 @@ init([]) -> {'ok', #state{}}. %%------------------------------------------------------------------------------ -%% @doc Handling call messages. +%% @private +%% @doc Handling call messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_term:handle_call_ret_state(state()). +handle_call({'start_agent', AccountId, AgentId, Args}, _, State) -> + case acdc_agents_sup:find_agent_supervisor(AccountId, AgentId) of + 'undefined' -> + {'reply', supervisor:start_child('acdc_agents_sup', Args), State}; + Sup -> + lager:error("agent ~s(~s) already started here: ~p", [AgentId, AccountId, Sup]), + {'reply', {'already_started', Sup}, State} + end; handle_call(_Request, _From, State) -> Reply = 'ok', {'reply', Reply, State}. %%------------------------------------------------------------------------------ -%% @doc Handling cast messages. +%% @private +%% @doc Handling cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). +-spec handle_cast(any(), state()) -> kz_term:handle_cast_ret_state(state()). handle_cast({'gen_listener',{'is_consuming',_IsConsuming}}, State) -> {'noreply', State}; handle_cast({'gen_listener',{'created_queue',_QueueName}}, State) -> @@ -121,10 +145,12 @@ handle_cast(_Msg, State) -> {'noreply', State}. %%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. +%% @private +%% @doc Handling all non call/cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +-spec handle_info(any(), state()) -> kz_term:handle_info_ret_state(state()). handle_info(?HOOK_EVT(AccountId, <<"CHANNEL_CREATE">>, JObj), State) -> lager:debug("channel_create event"), _ = kz_process:spawn(fun acdc_agent_handler:handle_new_channel/2, [JObj, AccountId]), @@ -144,9 +170,10 @@ handle_event(_JObj, _State) -> {'reply', []}. %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_server' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_server' terminates +%% @private +%% @doc This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @end @@ -156,7 +183,9 @@ terminate(_Reason, _State) -> lager:debug("agent manager terminating: ~p", [_Reason]). %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), state(), any()) -> {'ok', state()}. diff --git a/applications/acdc/src/acdc_agent_stats.erl b/applications/acdc/src/acdc_agent_stats.erl index 9cef14e06d5..be40d167a41 100644 --- a/applications/acdc/src/acdc_agent_stats.erl +++ b/applications/acdc/src/acdc_agent_stats.erl @@ -2,8 +2,9 @@ %%% @copyright (C) 2014-2020, 2600Hz %%% @doc Collector of stats for agents %%% @author James Aimonetti -%%% @author Daniel Finke %%% +%%% @author James Aimonetti +%%% @author Daniel Finke %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -16,54 +17,67 @@ ,agent_logged_in/2 ,agent_logged_out/2 ,agent_pending_logged_out/2 - ,agent_connecting/3, agent_connecting/6 - ,agent_connected/3, agent_connected/6 + ,agent_connecting/5, agent_connecting/6 + ,agent_connected/5, agent_connected/6 ,agent_wrapup/3 - ,agent_paused/3 + ,agent_paused/4 ,agent_outbound/3 + ,agent_inbound/3 ,handle_status_stat/2 ,handle_status_query/2 + ,handle_agent_cur_status_req/2 - ,status_stat_key/3 ,status_stat_id/3 ,status_table_id/0 ,status_key_pos/0 ,status_table_opts/0 + ,agent_cur_status_table_id/0 + ,agent_cur_status_key_pos/0 + ,agent_cur_status_table_opts/0 + ,archive_status_data/2 ]). -include("acdc.hrl"). -include("acdc_stats.hrl"). -%%------------------------------------------------------------------------------ -%% @doc Status stat table configuration -%% @end -%%------------------------------------------------------------------------------ -spec status_table_id() -> atom(). status_table_id() -> 'acdc_stats_status'. -spec status_key_pos() -> pos_integer(). -status_key_pos() -> #status_stat.key. +status_key_pos() -> #status_stat.id. -spec status_table_opts() -> kz_term:proplist(). status_table_opts() -> - ['ordered_set', 'protected', 'named_table' + ['protected', 'named_table' ,{'keypos', status_key_pos()} ]. +-spec agent_cur_status_table_id() -> atom(). +agent_cur_status_table_id() -> 'acdc_stats_agent_cur_status'. + +-spec agent_cur_status_key_pos() -> pos_integer(). +agent_cur_status_key_pos() -> #status_stat.agent_id. + +-spec agent_cur_status_table_opts() -> kz_term:proplist(). +agent_cur_status_table_opts() -> + ['protected', 'named_table' + ,{'keypos', agent_cur_status_key_pos()} + ]. + -spec agent_ready(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. agent_ready(AccountId, AgentId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"ready">>} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_ready/1 ). @@ -73,11 +87,11 @@ agent_logged_in(AccountId, AgentId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"logged_in">>} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_logged_in/1 ). @@ -87,11 +101,11 @@ agent_logged_out(AccountId, AgentId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"logged_out">>} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_logged_out/1 ). @@ -102,26 +116,27 @@ agent_pending_logged_out(AccountId, AgentId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"pending_logged_out">>} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_pending_logged_out/1 ). --spec agent_connecting(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> +-spec agent_connecting(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_connecting(AccountId, AgentId, CallId) -> - agent_connecting(AccountId, AgentId, CallId, 'undefined', 'undefined', 'undefined'). +agent_connecting(AccountId, AgentId, CallId, CIDName, CIDNumber) -> + agent_connecting(AccountId, AgentId, CallId, CIDName, CIDNumber, 'undefined'). + -spec agent_connecting(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary()) -> 'ok'. agent_connecting(AccountId, AgentId, CallId, CallerIDName, CallerIDNumber, QueueId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"connecting">>} ,{<<"Call-ID">>, CallId} ,{<<"Caller-ID-Name">>, CallerIDName} @@ -129,22 +144,23 @@ agent_connecting(AccountId, AgentId, CallId, CallerIDName, CallerIDNumber, Queue ,{<<"Queue-ID">>, QueueId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_connecting/1 ). --spec agent_connected(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> +-spec agent_connected(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_connected(AccountId, AgentId, CallId) -> - agent_connected(AccountId, AgentId, CallId, 'undefined', 'undefined', 'undefined'). +agent_connected(AccountId, AgentId, CallId, CIDName, CIDNumber) -> + agent_connected(AccountId, AgentId, CallId, CIDName, CIDNumber, 'undefined'). + -spec agent_connected(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_binary(), kz_term:api_binary(), kz_term:api_binary()) -> 'ok'. agent_connected(AccountId, AgentId, CallId, CallerIDName, CallerIDNumber, QueueId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"connected">>} ,{<<"Call-ID">>, CallId} ,{<<"Caller-ID-Name">>, CallerIDName} @@ -152,7 +168,7 @@ agent_connected(AccountId, AgentId, CallId, CallerIDName, CallerIDNumber, QueueI ,{<<"Queue-ID">>, QueueId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_connected/1 ). @@ -162,29 +178,30 @@ agent_wrapup(AccountId, AgentId, WaitTime) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"wrapup">>} ,{<<"Wait-Time">>, WaitTime} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_wrapup/1 ). --spec agent_paused(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_integer()) -> 'ok'. -agent_paused(AccountId, AgentId, 'undefined') -> +-spec agent_paused(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_integer(), kz_term:api_binary()) -> 'ok'. +agent_paused(AccountId, AgentId, 'undefined', _) -> lager:debug("undefined pause time for ~s(~s)", [AgentId, AccountId]); -agent_paused(AccountId, AgentId, PauseTime) -> +agent_paused(AccountId, AgentId, PauseTime, Alias) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"paused">>} ,{<<"Pause-Time">>, PauseTime} + ,{<<"Pause-Alias">>, Alias} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_paused/1 ). @@ -194,16 +211,31 @@ agent_outbound(AccountId, AgentId, CallId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Timestamp">>, kz_time:now_s()} + ,{<<"Timestamp">>, kz_time:current_tstamp()} ,{<<"Status">>, <<"outbound">>} ,{<<"Call-ID">>, CallId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - log_status_change(AccountId, Prop), + edr_log_status_change(AccountId, Prop), kz_amqp_worker:cast(Prop ,fun kapi_acdc_stats:publish_status_outbound/1 ). +-spec agent_inbound(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +agent_inbound(AccountId, AgentId, CallId) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"inbound">>} + ,{<<"Call-ID">>, CallId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + edr_log_status_change(AccountId, Prop), + kz_amqp_worker:cast(Prop + ,fun kapi_acdc_stats:publish_status_inbound/1 + ). + -spec handle_status_stat(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_status_stat(JObj, Props) -> 'true' = case (EventName = kz_json:get_value(<<"Event-Name">>, JObj)) of @@ -221,37 +253,27 @@ handle_status_stat(JObj, Props) -> 'false' end, - AccountId = kz_json:get_ne_binary_value(<<"Account-ID">>, JObj), - AgentId = kz_json:get_ne_binary_value(<<"Agent-ID">>, JObj), + AgentId = kz_json:get_value(<<"Agent-ID">>, JObj), Timestamp = kz_json:get_integer_value(<<"Timestamp">>, JObj), gen_listener:cast(props:get_value('server', Props) ,{'create_status' - ,#status_stat{key=status_stat_key(AccountId, AgentId, Timestamp) - ,id=status_stat_id(AgentId, Timestamp, EventName) - ,status=EventName - ,callid=kz_json:get_value(<<"Call-ID">>, JObj) - ,wait_time=acdc_stats_util:wait_time(EventName, JObj) - ,pause_time=acdc_stats_util:pause_time(EventName, JObj) - ,caller_id_name=acdc_stats_util:caller_id_name(EventName, JObj) - ,caller_id_number=acdc_stats_util:caller_id_number(EventName, JObj) - ,queue_id=acdc_stats_util:queue_id(EventName, JObj) - } + ,#status_stat{ + id=status_stat_id(AgentId, Timestamp, EventName) + ,agent_id=AgentId + ,account_id=kz_json:get_value(<<"Account-ID">>, JObj) + ,status=EventName + ,timestamp=Timestamp + ,callid=kz_json:get_value(<<"Call-ID">>, JObj) + ,wait_time=acdc_stats_util:wait_time(EventName, JObj) + ,pause_time=acdc_stats_util:pause_time(EventName, JObj) + ,pause_alias=kz_json:get_value(<<"Pause-Alias">>, JObj) + ,caller_id_name=acdc_stats_util:caller_id_name(EventName, JObj) + ,caller_id_number=acdc_stats_util:caller_id_number(EventName, JObj) + } } ). -%%------------------------------------------------------------------------------ -%% @doc Status stat table key is in an order which can optimize the ordered_set -%% lookup if partially bound -%% @end -%%------------------------------------------------------------------------------ --spec status_stat_key(kz_term:ne_binary(), kz_term:ne_binary(), pos_integer()) -> status_stat_key(). -status_stat_key(AccountId, AgentId, Timestamp) -> - #status_stat_key{account_id=AccountId - ,agent_id=AgentId - ,timestamp=Timestamp - }. - -spec status_stat_id(kz_term:ne_binary(), pos_integer(), any()) -> kz_term:ne_binary(). status_stat_id(AgentId, Timestamp, _EventName) -> <>. @@ -265,31 +287,41 @@ handle_status_query(JObj, _Prop) -> case status_build_match_spec(JObj) of {'ok', Match} -> query_statuses(RespQ, MsgId, Match, Limit); - {'error', Errors} -> publish_query_errors(RespQ, MsgId, Errors) + {'error', Errors} -> publish_status_query_errors(RespQ, MsgId, Errors) end. --spec publish_query_errors(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. -publish_query_errors(RespQ, MsgId, Errors) -> +-spec handle_agent_cur_status_req(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_agent_cur_status_req(JObj, _Prop) -> + 'true' = kapi_acdc_stats:agent_cur_status_req_v(JObj), + RespQ = kz_json:get_value(<<"Server-ID">>, JObj), + MsgId = kz_json:get_value(<<"Msg-ID">>, JObj), + + case cur_status_build_match_spec(JObj) of + {'ok', Match} -> query_cur_statuses(RespQ, MsgId, Match); + {'error', Errors} -> publish_agent_cur_status_query_errors(RespQ, MsgId, Errors) + end. + +publish_status_query_errors(RespQ, MsgId, Errors) -> + publish_query_errors(RespQ, MsgId, Errors, fun kapi_acdc_stats:publish_status_err/2). + +publish_agent_cur_status_query_errors(RespQ, MsgId, Errors) -> + publish_query_errors(RespQ, MsgId, Errors, fun kapi_acdc_stats:publish_agent_cur_status_err/2). + +-spec publish_query_errors(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object(), function()) -> 'ok'. +publish_query_errors(RespQ, MsgId, Errors, PubFun) -> API = [{<<"Error-Reason">>, Errors} ,{<<"Msg-ID">>, MsgId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], lager:debug("responding with errors to req ~s: ~p", [MsgId, Errors]), - kapi_acdc_stats:publish_status_err(RespQ, API). + PubFun(RespQ, API). -%%------------------------------------------------------------------------------ -%% @doc Build a match spec for querying status stats -%% @end -%%------------------------------------------------------------------------------ --spec status_build_match_spec(kz_json:object()) -> - {'ok', ets:match_spec()} | - {'error', kz_json:object()}. status_build_match_spec(JObj) -> case kz_json:get_value(<<"Account-ID">>, JObj) of 'undefined' -> {'error', kz_json:from_list([{<<"Account-ID">>, <<"missing but required">>}])}; AccountId -> - AcctMatch = {#status_stat{key=#status_stat_key{account_id='$1'}, _='_'} + AcctMatch = {#status_stat{account_id='$1', _='_'} ,[{'=:=', '$1', {'const', AccountId}}] }, status_build_match_spec(JObj, AcctMatch) @@ -306,15 +338,15 @@ status_build_match_spec(JObj, AcctMatch) -> status_match_builder_fold(_, _, {'error', _Err}=E) -> E; status_match_builder_fold(<<"Agent-ID">>, AgentId, {StatusStat, Contstraints}) -> - Key = StatusStat#status_stat.key, - {StatusStat#status_stat{key=Key#status_stat_key{agent_id='$2'}} + {StatusStat#status_stat{agent_id='$2'} ,[{'=:=', '$2', {'const', AgentId}} | Contstraints] }; status_match_builder_fold(<<"Start-Range">>, Start, {StatusStat, Contstraints}) -> - Now = kz_time:now_s(), + Now = kz_time:current_tstamp(), Past = Now - ?CLEANUP_WINDOW, + Start1 = acdc_stats_util:apply_query_window_wiggle_room(Start, Past), - try kz_term:to_integer(Start) of + try kz_term:to_integer(Start1) of N when N < Past -> {'error', kz_json:from_list([{<<"Start-Range">>, <<"supplied value is too far in the past">>} ,{<<"Window-Size">>, ?CLEANUP_WINDOW} @@ -327,8 +359,7 @@ status_match_builder_fold(<<"Start-Range">>, Start, {StatusStat, Contstraints}) ,{<<"Current-Timestamp">>, Now} ])}; N -> - Key = StatusStat#status_stat.key, - {StatusStat#status_stat{key=Key#status_stat_key{timestamp='$3'}} + {StatusStat#status_stat{timestamp='$3'} ,[{'>=', '$3', N} | Contstraints] } catch @@ -336,10 +367,11 @@ status_match_builder_fold(<<"Start-Range">>, Start, {StatusStat, Contstraints}) {'error', kz_json:from_list([{<<"Start-Range">>, <<"supplied value is not an integer">>}])} end; status_match_builder_fold(<<"End-Range">>, End, {StatusStat, Contstraints}) -> - Now = kz_time:now_s(), + Now = kz_time:current_tstamp(), Past = Now - ?CLEANUP_WINDOW, + End1 = acdc_stats_util:apply_query_window_wiggle_room(End, Past), - try kz_term:to_integer(End) of + try kz_term:to_integer(End1) of N when N < Past -> {'error', kz_json:from_list([{<<"End-Range">>, <<"supplied value is too far in the past">>} ,{<<"Window-Size">>, ?CLEANUP_WINDOW} @@ -350,8 +382,7 @@ status_match_builder_fold(<<"End-Range">>, End, {StatusStat, Contstraints}) -> ,{<<"Current-Timestamp">>, Now} ])}; N -> - Key = StatusStat#status_stat.key, - {StatusStat#status_stat{key=Key#status_stat_key{timestamp='$3'}} + {StatusStat#status_stat{timestamp='$3'} ,[{'=<', '$3', N} | Contstraints] } catch @@ -364,96 +395,112 @@ status_match_builder_fold(<<"Status">>, Status, {StatusStat, Contstraints}) -> }; status_match_builder_fold(_, _, Acc) -> Acc. -%%------------------------------------------------------------------------------ -%% @doc Execute a status query -%% @end -%%------------------------------------------------------------------------------ +cur_status_build_match_spec(JObj) -> + case kz_json:get_value(<<"Account-ID">>, JObj) of + 'undefined' -> + {'error', kz_json:from_list([{<<"Account-ID">>, <<"missing but required">>}])}; + AccountId -> + AcctMatch = {#status_stat{account_id='$1', _='_'} + ,[{'=:=', '$1', {'const', AccountId}}] + }, + cur_status_build_match_spec(JObj, AcctMatch) + end. + +-spec cur_status_build_match_spec(kz_json:object(), {status_stat(), list()}) -> + {'ok', ets:match_spec()} | + {'error', kz_json:object()}. +cur_status_build_match_spec(JObj, AcctMatch) -> + case kz_json:foldl(fun cur_status_match_builder_fold/3, AcctMatch, JObj) of + {'error', _Errs}=Errors -> Errors; + {StatusStat, Constraints} -> {'ok', [{StatusStat, Constraints, ['$_']}]} + end. + +cur_status_match_builder_fold(_, _, {'error', _Err}=E) -> E; +cur_status_match_builder_fold(<<"Agent-ID">>, AgentId, {StatusStat, Contstraints}) -> + {StatusStat#status_stat{agent_id='$2'} + ,[{'=:=', '$2', {'const', AgentId}} | Contstraints] + }; +cur_status_match_builder_fold(_, _, Acc) -> Acc. + -spec query_statuses(kz_term:ne_binary(), kz_term:ne_binary(), ets:match_spec(), pos_integer() | 'no_limit') -> 'ok'. query_statuses(RespQ, MsgId, Match, Limit) -> - Stats = ets:select_reverse(status_table_id(), Match), + case ets:select(status_table_id(), Match) of + [] -> + lager:debug("no Agents found, sorry ~s", [RespQ]), + Resp = [{<<"Error-Reason">>, <<"No agents found">>} + ,{<<"Agents">>, <<"none">>} + ,{<<"Msg-ID">>, MsgId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_stats:publish_status_err(RespQ, Resp); + Stats -> + QueryResults = lists:foldl(fun query_status_fold/2, kz_json:new(), Stats), + TrimmedResults = kz_json:map(fun(A, B) -> + {A, trim_query_statuses(B, Limit)} + end, QueryResults), + + Resp = [{<<"Agents">>, TrimmedResults} + ,{<<"Msg-ID">>, MsgId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_stats:publish_status_resp(RespQ, Resp) + end. - case Stats of - [] -> lager:debug("no stats found (requester: ~s)", [RespQ]); - _ -> 'ok' - end, +-spec query_cur_statuses(kz_term:ne_binary(), kz_term:ne_binary(), ets:match_spec()) -> 'ok'. +query_cur_statuses(RespQ, MsgId, Match) -> + case ets:select(agent_cur_status_table_id(), Match) of + [] -> + lager:debug("no stats found, sorry ~s", [RespQ]), + Resp = [{<<"Error-Reason">>, <<"No agents found">>} + ,{<<"Agents">>, <<"none">>} + ,{<<"Msg-ID">>, MsgId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_stats:publish_agent_cur_status_err(RespQ, Resp); + Stats -> + QueryResults = lists:foldl(fun query_status_fold/2, kz_json:new(), Stats), + Results = kz_json:map(fun(AgentId, QueryResult) -> + StatusJObj = hd(kz_json:values(QueryResult)), + {AgentId, StatusJObj} + end, QueryResults), + Resp = [{<<"Agents">>, Results} + ,{<<"Msg-ID">>, MsgId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_stats:publish_agent_cur_status_resp(RespQ, Resp) + end. - Resp = [{<<"Agents">>, query_statuses_group_by_agent(Stats, Limit)} - ,{<<"Msg-ID">>, MsgId} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], - kapi_acdc_stats:publish_status_resp(RespQ, Resp). - -%%------------------------------------------------------------------------------ -%% @doc Group status stats by agent and return the whole map as a JObj. Each -%% agent grouping will contain at max "Limit" number of status stats -%% @end -%%------------------------------------------------------------------------------ -query_statuses_group_by_agent(Stats, Limit) -> - query_statuses_fold(Stats, Limit, #{}). - -query_statuses_fold([], _, StatsByAgent) -> kz_json:from_map(StatsByAgent); -query_statuses_fold([#status_stat{key=#status_stat_key{agent_id=AgentId - ,timestamp=Timestamp - } - }=Stat - | Stats - ], Limit, StatsByAgent) -> - AgentStats = maps:get(AgentId, StatsByAgent, #{}), - StatsByAgent1 = case Limit =:= 'no_limit' - orelse maps:size(AgentStats) < Limit - of - 'true' -> - TimestampBin = kz_term:to_binary(Timestamp), - Stat1 = status_stat_to_map(Stat), - AgentStats1 = AgentStats#{TimestampBin => Stat1}, - StatsByAgent#{AgentId => AgentStats1}; - 'false' -> StatsByAgent - end, - query_statuses_fold(Stats, Limit, StatsByAgent1). - -%%------------------------------------------------------------------------------ -%% @doc Convert a status stat record to a map that can be efficiently parsed -%% into a JObj -%% @end -%%------------------------------------------------------------------------------ --spec status_stat_to_map(status_stat()) -> map(). -status_stat_to_map(#status_stat{key=#status_stat_key{agent_id=AgentId - ,timestamp=Timestamp - } - ,id=Id - ,status=Status - ,wait_time=WT - ,pause_time=PT - ,callid=CallId - ,caller_id_name=CIDName - ,caller_id_number=CIDNum - ,queue_id=QueueId - }) -> - #{'agent_id' => AgentId - ,'timestamp' => Timestamp - ,'id' => Id - ,'status' => Status - ,'wait_time' => WT - ,'pause_time' => PT - ,'call_id' => CallId - ,'caller_id_name' => CIDName - ,'caller_id_number' => CIDNum - ,'queue_id' => QueueId - }. +-spec trim_query_statuses(kz_json:object(), pos_integer() | 'no_limit') -> kz_json:object(). +trim_query_statuses(Statuses, Limit) -> + StatusProps = kz_json:to_proplist(Statuses), + SortedProps = lists:sort(fun({A, _}, {B, _}) -> + kz_term:to_integer(A) >= kz_term:to_integer(B) + end, StatusProps), + LimitedProps = case Limit of + 'no_limit' -> SortedProps; + _ -> lists:sublist(SortedProps, Limit) + end, + kz_json:from_list(LimitedProps). + +-spec query_status_fold(status_stat(), kz_json:object()) -> kz_json:object(). +query_status_fold(#status_stat{agent_id=AgentId + ,timestamp=T + }=Stat, Acc) -> + Doc = kz_doc:public_fields(status_stat_to_doc(Stat)), + kz_json:set_value([AgentId, kz_term:to_binary(T)], Doc, Acc). -spec status_stat_to_doc(status_stat()) -> kz_json:object(). -status_stat_to_doc(#status_stat{key=#status_stat_key{account_id=AccountId - ,agent_id=AgentId - ,timestamp=Timestamp - } - ,id=Id +status_stat_to_doc(#status_stat{id=Id + ,agent_id=AgentId + ,account_id=AccountId ,status=Status + ,timestamp=Timestamp ,wait_time=WT ,pause_time=PT + ,pause_alias=Alias ,callid=CallId ,caller_id_name=CIDName ,caller_id_number=CIDNum - ,queue_id=QueueId }) -> Prop = [{<<"_id">>, Id} ,{<<"call_id">>, CallId} @@ -462,9 +509,9 @@ status_stat_to_doc(#status_stat{key=#status_stat_key{account_id=AccountId ,{<<"status">>, Status} ,{<<"wait_time">>, WT} ,{<<"pause_time">>, PT} + ,{<<"pause_alias">>, Alias} ,{<<"caller_id_name">>, CIDName} ,{<<"caller_id_number">>, CIDNum} - ,{<<"queue_id">>, QueueId} ], kz_doc:update_pvt_parameters(kz_json:from_list(Prop) ,acdc_stats_util:db_name(AccountId) @@ -485,8 +532,8 @@ archive_status_data(Srv, 'true') -> archive_status_data(Srv, 'false') -> kz_log:put_callid(<<"acdc_stats.status_archiver">>), - Past = kz_time:now_s() - ?ARCHIVE_WINDOW, - Match = [{#status_stat{key=#status_stat_key{timestamp='$1'} + Past = kz_time:current_tstamp() - ?ARCHIVE_WINDOW, + Match = [{#status_stat{timestamp='$1' ,is_archived='$2' ,_='_' } @@ -513,10 +560,11 @@ maybe_archive_status_data(Srv, Match) -> end. -spec archive_status_fold(status_stat(), dict:dict()) -> dict:dict(). -archive_status_fold(#status_stat{key=#status_stat_key{account_id=AccountId}}=Stat, Acc) -> +archive_status_fold(#status_stat{account_id=AccountId}=Stat, Acc) -> Doc = status_stat_to_doc(Stat), dict:update(AccountId, fun(L) -> [Doc | L] end, [Doc], Acc). -log_status_change(AccountId, Prop) -> +-spec edr_log_status_change(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. +edr_log_status_change(AccountId, Prop) -> Body = kz_json:normalize(kz_json:from_list([{<<"Event">>, <<"agent_status_change">>} | Prop])), kz_edr:event(?APP_NAME, ?APP_VERSION, 'ok', 'info', Body, AccountId). diff --git a/applications/acdc/src/acdc_agent_sup.erl b/applications/acdc/src/acdc_agent_sup.erl index 7ff4ac6f5e3..86a21002e56 100644 --- a/applications/acdc/src/acdc_agent_sup.erl +++ b/applications/acdc/src/acdc_agent_sup.erl @@ -3,7 +3,6 @@ %%% @doc %%% @author James Aimonetti %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/applications/acdc/src/acdc_agent_util.erl b/applications/acdc/src/acdc_agent_util.erl index 66ec431e755..c7a5d7ada87 100644 --- a/applications/acdc/src/acdc_agent_util.erl +++ b/applications/acdc/src/acdc_agent_util.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2013-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -24,7 +23,7 @@ ,changed/2, find_most_recent_fold/3 - ,status_should_auto_start/1 + ,agent_priority/1 ]). -include("acdc.hrl"). @@ -44,7 +43,8 @@ update_status(?NE_BINARY = AccountId, AgentId, Status, Options) -> kz_amqp_worker:cast(API, fun kapi_acdc_stats:publish_status_update/1). -spec most_recent_status(kz_term:ne_binary(), kz_term:ne_binary()) -> - {'ok', kz_term:ne_binary()}. + {'ok', kz_term:ne_binary()} | + {'error', any()}. most_recent_status(AccountId, AgentId) -> case most_recent_ets_status(AccountId, AgentId) of {'ok', _}=OK -> OK; @@ -60,25 +60,26 @@ most_recent_status(AccountId, AgentId) -> {'ok', kz_term:ne_binary()} | {'error', any()}. most_recent_ets_status(AccountId, AgentId) -> - case most_recent_ets_statuses(AccountId, AgentId) of - {'error', _}=E -> E; - {'ok', Statuses} -> - most_recent_ets_agent_status(kz_json:get_json_value(AgentId, Statuses)) + API = [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + case kz_amqp_worker:call(API + ,fun kapi_acdc_stats:publish_status_req/1 + ,fun kapi_acdc_stats:status_resp_v/1 + ) + of + {'error', _E}=E -> E; + {'ok', Resp} -> + Stats = kz_json:get_value([<<"Agents">>, AgentId], Resp), + {_, StatusJObj} = kz_json:foldl(fun find_most_recent_fold/3, {0, kz_json:new()}, Stats), + {'ok', kz_json:get_value(<<"status">>, StatusJObj)} end. --spec most_recent_ets_agent_status(kz_term:api_object()) -> - {'ok', kz_term:ne_binary()} | - {'error', 'not_found'}. -most_recent_ets_agent_status('undefined') -> {'error', 'not_found'}; -most_recent_ets_agent_status(Stats) -> - {_, StatusJObj} = kz_json:foldl(fun find_most_recent_fold/3, {0, kz_json:new()}, Stats), - {'ok', kz_json:get_value(<<"status">>, StatusJObj)}. - -spec most_recent_db_status(kz_term:ne_binary(), kz_term:ne_binary()) -> {'ok', kz_term:ne_binary()}. most_recent_db_status(AccountId, AgentId) -> Opts = [{'startkey', [AgentId, kz_time:now_s()]} - ,{'endkey', [AgentId, 0]} ,{'limit', 1} ,'descending' ], @@ -101,7 +102,6 @@ most_recent_db_status(AccountId, AgentId) -> {'ok', kz_term:ne_binary()}. prev_month_recent_db_status(AccountId, AgentId) -> Opts = [{'startkey', [AgentId, kz_time:now_s()]} - ,{'endkey', [AgentId, 0]} ,{'limit', 1} ,'descending' ], @@ -138,38 +138,79 @@ most_recent_statuses(AccountId, Options) when is_list(Options) -> -spec most_recent_statuses(kz_term:ne_binary(), kz_term:api_binary(), kz_term:proplist()) -> statuses_return(). most_recent_statuses(AccountId, AgentId, Options) -> - ETSStatuses = case most_recent_ets_statuses(AccountId, AgentId, Options) of - {'ok', Statuses} -> Statuses; - {'error', _} -> kz_json:new() - end, - DBStatuses = case fetch_db_statuses(AccountId, AgentId) of - {'ok', Statuses2} -> Statuses2; - {'error', _} -> kz_json:new() - end, - {'ok', kz_json:merge(DBStatuses, ETSStatuses)}. - -fetch_db_statuses(AccountId, AgentId) -> - case kz_cache:fetch_local(?CACHE_NAME, db_fetch_key(AccountId)) of - {'ok', Statuses} -> {'ok', filter_agent_statuses(Statuses, AgentId)}; - {'error', 'not_found'} -> maybe_db_lookup(AccountId, AgentId) + ETS = kz_process:spawn_monitor(fun async_most_recent_ets_statuses/4, [AccountId, AgentId, Options, self()]), + DB = maybe_start_db_lookup('async_most_recent_db_statuses' + ,fun async_most_recent_db_statuses/4 + ,AccountId, AgentId, Options, self() + ), + {'ok', receive_statuses([ETS, DB])}. + +-spec maybe_start_db_lookup(atom(), fun(), kz_term:ne_binary(), kz_term:api_binary(), list(), pid()) -> + kz_term:pid_ref() | 'undefined'. +maybe_start_db_lookup(F, Fun, AccountId, AgentId, Options, Self) -> + case kz_cache:fetch_local(?CACHE_NAME, db_fetch_key(F, AccountId, AgentId)) of + {'ok', _} -> 'undefined'; + {'error', 'not_found'} -> + kz_process:spawn_monitor(Fun, [AccountId, AgentId, Options, Self]) end. --spec maybe_db_lookup(kz_term:ne_binary(), kz_term:ne_binary()) -> - statuses_return() | {'error', any()}. -maybe_db_lookup(AccountId, AgentId) -> - case most_recent_db_statuses(AccountId) of - {'ok', Statuses} -> - kz_cache:store_local(?CACHE_NAME, db_fetch_key(AccountId), Statuses), - {'ok', filter_agent_statuses(Statuses, AgentId)}; - {'error', _}=E -> E +db_fetch_key(F, AccountId, AgentId) -> {F, AccountId, AgentId}. + +-type receive_info() :: [{pid(), reference()} | 'undefined']. + +-spec receive_statuses(receive_info()) -> + kz_json:object(). +receive_statuses(Reqs) -> receive_statuses(Reqs, kz_json:new()). + +-spec receive_statuses(receive_info(), kz_json:object()) -> + kz_json:object(). +receive_statuses([], AccJObj) -> AccJObj; +receive_statuses(['undefined' | Reqs], AccJObj) -> + receive_statuses(Reqs, AccJObj); +receive_statuses([{Pid, Ref} | Reqs], AccJObj) -> + receive + {'statuses', Statuses, Pid} -> + clear_monitor(Ref), + receive_statuses(Reqs, kz_json:merge(Statuses, AccJObj)); + {'DOWN', Ref, 'process', Pid, _R} -> + lager:debug("req in ~p died: ~p", [Pid, _R]), + clear_monitor(Ref), + receive_statuses(Reqs, AccJObj) + after 3000 -> + lager:debug("timed out waiting for ~p to respond", [Pid]), + receive_statuses(Reqs, AccJObj) end. --spec db_fetch_key(AccountId) -> {'async_most_recent_db_statuses', AccountId}. -db_fetch_key(AccountId) -> {'async_most_recent_db_statuses', AccountId}. +-spec clear_monitor(reference()) -> 'ok'. +clear_monitor(Ref) -> + erlang:demonitor(Ref, ['flush']), + receive + {'DOWN', Ref, 'process', _, _} -> clear_monitor(Ref) + after 0 -> 'ok' + end. -filter_agent_statuses(Statuses, 'undefined') -> Statuses; -filter_agent_statuses(Statuses, KeepAgentId) -> - kz_json:filter(fun({AgentId, _}) -> AgentId =:= KeepAgentId end, Statuses). +-spec async_most_recent_ets_statuses(kz_term:ne_binary(), kz_term:api_binary(), kz_term:proplist(), pid()) -> 'ok'. +async_most_recent_ets_statuses(AccountId, AgentId, Options, Pid) -> + case most_recent_ets_statuses(AccountId, AgentId, Options) of + {'ok', Statuses} -> + Pid ! {'statuses', Statuses, self()}, + 'ok'; + {'error', _E} -> + Pid ! {'statuses', kz_json:new(), self()}, + 'ok' + end. + +-spec async_most_recent_db_statuses(kz_term:ne_binary(), kz_term:api_binary(), kz_term:proplist(), pid()) -> 'ok'. +async_most_recent_db_statuses(AccountId, AgentId, Options, Pid) -> + case most_recent_db_statuses(AccountId, AgentId, Options) of + {'ok', Statuses} -> + Pid ! {'statuses', Statuses, self()}, + kz_cache:store_local(?CACHE_NAME, db_fetch_key('async_most_recent_db_statuses', AccountId, AgentId), 'true'), + 'ok'; + {'error', _E} -> + Pid ! {'statuses', kz_json:new(), self()}, + 'ok' + end. -spec most_recent_ets_statuses(kz_term:ne_binary()) -> statuses_return() | @@ -194,21 +235,14 @@ most_recent_ets_statuses(AccountId, AgentId, Options) -> ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ++ Options ]), - case kz_amqp_worker:call_collect(API - ,fun kapi_acdc_stats:publish_status_req/1 - ,'acdc' - ,3 * ?MILLISECONDS_IN_SECOND - ) + case kz_amqp_worker:call(API + ,fun kapi_acdc_stats:publish_status_req/1 + ,fun kapi_acdc_stats:status_resp_v/1 + ) of {'error', _}=E -> E; - {Result, Resps} when Result =:= 'ok' - orelse Result =:= 'timeout' -> - OKResps = lists:filter(fun kapi_acdc_stats:status_resp_v/1, Resps), - Statuses = lists:foldl(fun(Resp, AccJObj) -> - AgentsStatuses = kz_json:get_json_value(<<"Agents">>, Resp), - kz_json:merge(AgentsStatuses, AccJObj) - end, kz_json:new(), OKResps), - {'ok', Statuses} + {'ok', Resp} -> + {'ok', kz_json:get_value([<<"Agents">>], Resp, kz_json:new())} end. -spec most_recent_db_statuses(kz_term:ne_binary()) -> @@ -217,7 +251,7 @@ most_recent_ets_statuses(AccountId, AgentId, Options) -> most_recent_db_statuses(AccountId) -> most_recent_db_statuses(AccountId, 'undefined', []). --spec most_recent_db_statuses(kz_term:ne_binary(), kz_term:api_binary() | kz_term:proplist()) -> +-spec most_recent_db_statuses(kz_term:ne_binary(), kz_term:api_binary()) -> statuses_return() | {'error', any()}. most_recent_db_statuses(AccountId, ?NE_BINARY = AgentId) -> @@ -381,7 +415,7 @@ changed([F|From], To, Add, Rm) -> 'false' -> changed(From, To, Add, [F|Rm]) end. --spec status_should_auto_start(kz_term:ne_binary()) -> boolean(). -status_should_auto_start(<<"logged_out">>) -> 'false'; -status_should_auto_start(<<"unknown">>) -> 'false'; -status_should_auto_start(_) -> 'true'. +-spec agent_priority(wh_json:object()) -> agent_priority(). +agent_priority(AgentJObj) -> + -1 * kz_json:get_integer_value(<<"acdc_agent_priority">>, AgentJObj, 0). + diff --git a/applications/acdc/src/acdc_agents_sup.erl b/applications/acdc/src/acdc_agents_sup.erl index 579dab57eca..6305a88b02a 100644 --- a/applications/acdc/src/acdc_agents_sup.erl +++ b/applications/acdc/src/acdc_agents_sup.erl @@ -3,7 +3,6 @@ %%% @doc %%% @author James Aimonetti %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -61,18 +60,18 @@ status() -> -spec new(kz_json:object()) -> kz_types:sup_startchild_ret(). new(JObj) -> - AcctId = kz_doc:account_id(JObj), + AccountId = kz_doc:account_id(JObj), AgentId = kz_doc:id(JObj), - start_agent(AcctId, AgentId, JObj). + start_agent(AccountId, AgentId, JObj). -spec new(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:sup_startchild_ret(). -new(AcctId, AgentId) -> - {'ok', JObj} = kz_datamgr:open_doc(kzs_util:format_account_db(AcctId), AgentId), - start_agent(AcctId, AgentId, JObj). +new(AccountId, AgentId) -> + {'ok', JObj} = kz_datamgr:open_doc(kzs_util:format_account_db(AccountId), AgentId), + start_agent(AccountId, AgentId, JObj). -spec new(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object(), kz_term:ne_binaries()) -> kz_types:sup_startchild_ret(). -new(AcctId, AgentId, AgentJObj, Queues) -> - start_agent(AcctId, AgentId, AgentJObj, [Queues]). +new(AccountId, AgentId, AgentJObj, Queues) -> + start_agent(AccountId, AgentId, AgentJObj, [Queues]). -spec new_thief(kapps_call:call(), kz_term:ne_binary()) -> kz_types:sup_startchild_ret(). new_thief(Call, QueueId) -> @@ -82,14 +81,14 @@ new_thief(Call, QueueId) -> supervisor:start_child(?MODULE, ?CHILD(Id, [Call, QueueId])). -spec stop_agent(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:sup_deletechild_ret(). -stop_agent(AcctId, AgentId) -> - Id = ?CHILD_ID(AcctId, AgentId), +stop_agent(AccountId, AgentId) -> + Id = ?CHILD_ID(AccountId, AgentId), case supervisor:terminate_child(?SERVER, Id) of 'ok' -> - lager:info("stopping agent ~s(~s)", [AgentId, AcctId]), + lager:info("stopping agent ~s(~s)", [AgentId, AccountId]), supervisor:delete_child(?SERVER, Id); E -> - lager:info("no supervisor for agent ~s(~s) to stop", [AgentId, AcctId]), + lager:info("no supervisor for agent ~s(~s) to stop", [AgentId, AccountId]), E end. @@ -97,32 +96,32 @@ stop_agent(AcctId, AgentId) -> workers() -> [Pid || {_, Pid, 'supervisor', [_]} <- supervisor:which_children(?SERVER)]. -spec restart_acct(kz_term:ne_binary()) -> [kz_types:sup_startchild_ret()]. -restart_acct(AcctId) -> - [restart_agent(AcctId, AgentId) - || {_, {AcctId1, AgentId, _}} <- agents_running() - ,AcctId =:= AcctId1 +restart_acct(AccountId) -> + [restart_agent(AccountId, AgentId) + || {_, {AccountId1, AgentId, _}} <- agents_running() + ,AccountId =:= AccountId1 ]. -spec restart_agent(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:sup_startchild_ret(). -restart_agent(AcctId, AgentId) -> - Id = ?CHILD_ID(AcctId, AgentId), +restart_agent(AccountId, AgentId) -> + Id = ?CHILD_ID(AccountId, AgentId), case supervisor:terminate_child(?SERVER, Id) of 'ok' -> - lager:info("restarting agent ~s(~s)", [AgentId, AcctId]), + lager:info("restarting agent ~s(~s)", [AgentId, AccountId]), supervisor:restart_child(?SERVER, Id); E -> - lager:info("no supervisor for agent ~s(~s) to restart", [AgentId, AcctId]), + lager:info("no supervisor for agent ~s(~s) to restart", [AgentId, AccountId]), E end. -spec find_acct_supervisors(kz_term:ne_binary()) -> kz_term:pids(). -find_acct_supervisors(AcctId) -> [S || S <- workers(), is_agent_in_acct(S, AcctId)]. +find_acct_supervisors(AccountId) -> [S || S <- workers(), is_agent_in_acct(S, AccountId)]. -spec is_agent_in_acct(pid(), kz_term:ne_binary()) -> boolean(). -is_agent_in_acct(Super, AcctId) -> +is_agent_in_acct(Super, AccountId) -> case catch acdc_agent_listener:config(acdc_agent_sup:listener(Super)) of {'EXIT', _} -> 'false'; - {AcctId, _, _} -> 'true'; + {AccountId, _, _} -> 'true'; _ -> 'false' end. @@ -131,21 +130,21 @@ agents_running() -> [{W, catch acdc_agent_listener:config(acdc_agent_sup:listener(W))} || W <- workers()]. -spec find_agent_supervisor(kz_term:api_binary(), kz_term:api_binary()) -> kz_term:api_pid(). -find_agent_supervisor(AcctId, AgentId) -> find_agent_supervisor(AcctId, AgentId, workers()). +find_agent_supervisor(AccountId, AgentId) -> find_agent_supervisor(AccountId, AgentId, workers()). -spec find_agent_supervisor(kz_term:api_binary(), kz_term:api_binary(), kz_term:pids()) -> kz_term:api_pid(). -find_agent_supervisor(AcctId, AgentId, _) when AcctId =:= 'undefined'; +find_agent_supervisor(AccountId, AgentId, _) when AccountId =:= 'undefined'; AgentId =:= 'undefined' -> - lager:debug("failed to get good data: ~s ~s", [AcctId, AgentId]), + lager:debug("failed to get good data: ~s ~s", [AccountId, AgentId]), 'undefined'; -find_agent_supervisor(AcctId, AgentId, []) -> - lager:debug("supervisor for agent ~s(~s) not found", [AgentId, AcctId]), +find_agent_supervisor(AccountId, AgentId, []) -> + lager:debug("supervisor for agent ~s(~s) not found", [AgentId, AccountId]), 'undefined'; -find_agent_supervisor(AcctId, AgentId, [Super|Rest]) -> +find_agent_supervisor(AccountId, AgentId, [Super|Rest]) -> case catch acdc_agent_listener:config(acdc_agent_sup:listener(Super)) of - {'EXIT', _E} -> find_agent_supervisor(AcctId, AgentId, Rest); - {AcctId, AgentId, _} -> Super; - _E -> find_agent_supervisor(AcctId, AgentId, Rest) + {'EXIT', _E} -> find_agent_supervisor(AccountId, AgentId, Rest); + {AccountId, AgentId, _} -> Super; + _E -> find_agent_supervisor(AccountId, AgentId, Rest) end. %%%============================================================================= @@ -179,18 +178,18 @@ init([]) -> %% @end %%------------------------------------------------------------------------------ -spec start_agent(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> kz_types:sup_startchild_ret(). -start_agent(AcctId, AgentId, AgentJObj) -> - start_agent(AcctId, AgentId, AgentJObj, []). +start_agent(AccountId, AgentId, AgentJObj) -> + start_agent(AccountId, AgentId, AgentJObj, []). -spec start_agent(kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object(), [any()]) -> kz_types:sup_startchild_ret(). -start_agent(AcctId, AgentId, AgentJObj, ExtraArgs) -> - Id = ?CHILD_ID(AcctId, AgentId), - case supervisor:start_child(?SERVER, ?CHILD(Id, [AcctId, AgentId, AgentJObj] ++ ExtraArgs)) of +start_agent(AccountId, AgentId, AgentJObj, ExtraArgs) -> + Id = ?CHILD_ID(AccountId, AgentId), + case supervisor:start_child(?SERVER, ?CHILD(Id, [AccountId, AgentId, AgentJObj] ++ ExtraArgs)) of {'error', 'already_present'}=E -> - lager:debug("agent ~s(~s) already present", [AgentId, AcctId]), + lager:debug("agent ~s(~s) already present", [AgentId, AccountId]), E; {'error', {'already_started', Pid}}=E -> - lager:debug("agent ~s(~s) already started here: ~p", [AgentId, AcctId, Pid]), + lager:debug("agent ~s(~s) already started here: ~p", [AgentId, AccountId, Pid]), E; StartChildRet -> StartChildRet end. diff --git a/applications/acdc/src/acdc_announcements.erl b/applications/acdc/src/acdc_announcements.erl index 6b96a557ded..830654ce350 100644 --- a/applications/acdc/src/acdc_announcements.erl +++ b/applications/acdc/src/acdc_announcements.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2017, Voxter Communications %%% @doc %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -29,14 +28,16 @@ %%------------------------------------------------------------------------------ %% @doc Starts the announcements process +%% %% @end %%------------------------------------------------------------------------------ --spec start_link(pid(), kapps_call:call(), kz_term:proplist()) -> kz_types:startlink_ret(). +-spec start_link(pid(), kapps_call:call(), kz_term:proplist()) -> kz_term:startlink_ret(). start_link(Manager, Call, Props) -> {'ok', kz_process:spawn_link(fun ?MODULE:init/3, [Manager, Call, Props])}. %%------------------------------------------------------------------------------ %% @doc Initializes the announcements process +%% %% @end %%------------------------------------------------------------------------------ -spec init(pid(), kapps_call:call(), kz_term:proplist()) -> 'no_return'. @@ -52,6 +53,7 @@ init(Manager, Call, Props) -> %%------------------------------------------------------------------------------ %% @doc Load config from props into map +%% %% @end %%------------------------------------------------------------------------------ -spec get_config(kz_term:proplist()) -> map(). @@ -64,6 +66,7 @@ get_config(Props) -> %%------------------------------------------------------------------------------ %% @doc Get media file configuration from props +%% %% @end %%------------------------------------------------------------------------------ -spec announcements_media(kz_term:proplist()) -> kz_term:proplist(). @@ -77,6 +80,7 @@ announcements_media(Props) -> %%------------------------------------------------------------------------------ %% @doc Initialize state for the announcements process +%% %% @end %%------------------------------------------------------------------------------ -spec init_state(pid(), kapps_call:call(), map()) -> map(). @@ -89,6 +93,7 @@ init_state(Manager, Call, Config) -> %%------------------------------------------------------------------------------ %% @doc Loop entry point +%% %% @end %%------------------------------------------------------------------------------ -spec loop(map()) -> 'no_return'. @@ -97,6 +102,7 @@ loop(State) -> %%------------------------------------------------------------------------------ %% @doc Conditionally add position announcements prompts to playlist +%% %% @end %%------------------------------------------------------------------------------ -spec maybe_announce_position(map()) -> 'no_return'. @@ -107,7 +113,7 @@ maybe_announce_position(#{manager := Manager ,config := Config }=State) -> Language = kapps_call:language(Call), - Position = gen_listener:call(Manager, {'queue_position', kapps_call:call_id(Call)}), + Position = gen_listener:call(Manager, {'queue_member_position', kapps_call:call_id(Call)}), Prompts = [{'prompt', announcements_media_file(<<"you_are_at_position">>, Config), Language, <<"A">>} ,{'say', kz_term:to_binary(Position), <<"number">>} @@ -116,6 +122,7 @@ maybe_announce_position(#{manager := Manager %%------------------------------------------------------------------------------ %% @doc Conditionally add wait time announcements prompts to playlist +%% %% @end %%------------------------------------------------------------------------------ -spec maybe_announce_wait_time(kapps_call_command:audio_macro_prompts(), map()) -> 'no_return'. @@ -144,21 +151,23 @@ maybe_announce_wait_time(PromptAcc, #{call := Call %%------------------------------------------------------------------------------ %% @doc Play the prompts on the playlist, sleep till the next execution, -%% repeat. +%% repeat +%% %% @end %%------------------------------------------------------------------------------ -spec play_announcements(kapps_call_command:audio_macro_prompts(), map()) -> 'no_return'. play_announcements(Prompts, #{call := Call ,config := Config }=State) -> - _ = kapps_call_command:audio_macro(Prompts, Call), + kapps_call_command:audio_macro(Prompts, Call), AnnouncementsInterval = announcements_interval(Config), timer:sleep(AnnouncementsInterval * ?MILLISECONDS_IN_SECOND), loop(State). %%------------------------------------------------------------------------------ -%% @doc Get the average wait time from stats via AMQP. +%% @doc Get the average wait time from stats via AMQP +%% %% @end %%------------------------------------------------------------------------------ -spec get_average_wait_time(kapps_call:call()) -> kz_term:api_non_neg_integer(). @@ -182,7 +191,8 @@ get_average_wait_time(Call) -> end. %%------------------------------------------------------------------------------ -%% @doc Structure for time prompt entries. +%% @doc Structure for time prompt entries +%% %% @end %%------------------------------------------------------------------------------ -spec time_prompt(pos_integer(), binary()) -> {'prompt', kz_term:ne_binary(), binary(), kz_term:ne_binary()}. @@ -190,7 +200,8 @@ time_prompt(Time, Language) -> {'prompt', time_prompt2(Time), Language, <<"A">>}. %%------------------------------------------------------------------------------ -%% @doc Returns the appropriate prompt name for the given average wait time. +%% @doc Returns the appropriate prompt name for the given average wait time +%% %% @end %%------------------------------------------------------------------------------ -spec time_prompt2(pos_integer()) -> kz_term:ne_binary(). @@ -212,7 +223,8 @@ time_prompt2(_) -> <<"queue-at_least_1_hour">>. %%------------------------------------------------------------------------------ -%% @doc Return the time interval between announcements. +%% @doc Return the time interval between announcements +%% %% @end %%------------------------------------------------------------------------------ -spec announcements_interval(map()) -> non_neg_integer(). @@ -220,9 +232,10 @@ announcements_interval(#{announcements_interval := Interval}) -> Interval. %%------------------------------------------------------------------------------ -%% @doc Return the media file of a given name from the config. +%% @doc Return the media file of a given name from the config +%% %% @end %%------------------------------------------------------------------------------ --spec announcements_media_file(kz_term:ne_binary(), map()) -> kz_term:api_ne_binary(). +-spec announcements_media_file(kz_term:ne_binary(), map()) -> api_kz_term:ne_binary(). announcements_media_file(Name, #{announcements_media := Media}) -> props:get_binary_value(Name, Media). diff --git a/applications/acdc/src/acdc_announcements_sup.erl b/applications/acdc/src/acdc_announcements_sup.erl index d42ac5d35f3..3362407896f 100644 --- a/applications/acdc/src/acdc_announcements_sup.erl +++ b/applications/acdc/src/acdc_announcements_sup.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2017, Voxter Communications %%% @doc %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -32,10 +31,11 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the supervisor. +%% @doc Starts the supervisor +%% %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:sup_startchild_ret(). +-spec start_link() -> kz_term:sup_startchild_ret(). start_link() -> supervisor:start_link({local, ?SERVER}, ?MODULE, []). @@ -54,9 +54,10 @@ maybe_start_announcements(Manager, Call, Props) -> %%------------------------------------------------------------------------------ %% @doc Stop an announcements child process +%% %% @end %%------------------------------------------------------------------------------ --spec stop_announcements(pid()) -> 'ok' | {'error', atom()}. +-spec stop_announcements(pid()) -> kz_types:sup_terminatechild_ret(). stop_announcements(Pid) -> supervisor:terminate_child(?SERVER, Pid). @@ -65,7 +66,8 @@ stop_announcements(Pid) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a supervisor is started using `supervisor:start_link/[2,3]', +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], %% this function is called by the new process to find out about %% restart strategy, maximum restart intensity, and child %% specifications. diff --git a/applications/acdc/src/acdc_app.erl b/applications/acdc/src/acdc_app.erl index dda246676d1..362901bd7d5 100644 --- a/applications/acdc/src/acdc_app.erl +++ b/applications/acdc/src/acdc_app.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/applications/acdc/src/acdc_eavesdrop.erl b/applications/acdc/src/acdc_eavesdrop.erl index acc16f6653f..7ba61c21492 100644 --- a/applications/acdc/src/acdc_eavesdrop.erl +++ b/applications/acdc/src/acdc_eavesdrop.erl @@ -1,9 +1,7 @@ %%%----------------------------------------------------------------------------- %%% @copyright (C) 2012-2020, 2600Hz -%%% @doc ACDc Eavesdrop. -%%% +%%% @doc Created : 29 Nov 2012 by James Aimonetti %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -17,13 +15,14 @@ -export([start/3]). -spec start(kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -start(MCall, AcctId, AgentCallId) -> +start(MCall, AccountId, AgentCallId) -> + {CIDNumber, CIDName} = acdc_util:caller_id(MCall), Prop = [{<<"Eavesdrop-Mode">>, <<"listen">>} - ,{<<"Account-ID">>, AcctId} + ,{<<"Account-ID">>, AccountId} ,{<<"Endpoint-ID">>, <<"5381e0c5caa8d34eec06e0f75d0b4189">>} ,{<<"Eavesdrop-Call-ID">>, AgentCallId} - ,{<<"Outbound-Caller-ID-Name">>, kapps_call:caller_id_name(MCall)} - ,{<<"Outbound-Caller-ID-Number">>, kapps_call:caller_id_number(MCall)} + ,{<<"Outbound-Caller-ID-Name">>, CIDName} + ,{<<"Outbound-Caller-ID-Number">>, CIDNumber} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], eavesdrop_req(Prop). diff --git a/applications/acdc/src/acdc_handlers.erl b/applications/acdc/src/acdc_handlers.erl index 0e97ff5c98f..f121be5369f 100644 --- a/applications/acdc/src/acdc_handlers.erl +++ b/applications/acdc/src/acdc_handlers.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -24,16 +23,16 @@ handle_route_req(JObj, Props) -> Call = kapps_call:set_controller_queue(props:get_value('queue', Props) ,kapps_call:from_route_req(JObj) ), - AcctId = kapps_call:account_id(Call), + AccountId = kapps_call:account_id(Call), Id = kapps_call:request_user(Call), - maybe_route_respond(JObj, Call, AcctId, Id). + maybe_route_respond(JObj, Call, AccountId, Id). maybe_route_respond(_JObj, _Call, 'undefined', _Id) -> 'ok'; -maybe_route_respond(ReqJObj, Call, AcctId, Id) -> +maybe_route_respond(ReqJObj, Call, AccountId, Id) -> case kz_datamgr:open_cache_doc(kapps_call:account_db(Call), Id) of {'error', _} -> 'ok'; {'ok', Doc} -> - maybe_route_respond(ReqJObj, Call, AcctId, Id, kz_doc:type(Doc)) + maybe_route_respond(ReqJObj, Call, AccountId, Id, kz_doc:type(Doc)) end. maybe_route_respond(ReqJObj, Call, AccountId, QueueId, <<"queue">> = T) -> @@ -43,7 +42,7 @@ maybe_route_respond(ReqJObj, Call, AccountId, AgentId, <<"user">> = T) -> maybe_route_respond(_ReqJObj, _Call, _AccountId, _Id, _) -> 'ok'. send_route_response(ReqJObj, Call, AccountId, Id, Type) -> - lager:debug("sending route response to park the call for ~s(~s)", [Id, AccountId]), + lager:debug("sendig route response to park the call for ~s(~s)", [Id, AccountId]), CCVs = [{<<"ACDc-ID">>, Id} ,{<<"ACDc-Type">>, Type} ], @@ -99,26 +98,26 @@ update_acdc_actor(Call, QueueId, <<"queue">>) -> lager:debug("started thief at ~p", [_P]), OK; update_acdc_actor(Call, AgentId, <<"user">>) -> - AcctId = kapps_call:account_id(Call), + AccountId = kapps_call:account_id(Call), - case acdc_agent_util:most_recent_status(AcctId, AgentId) of - {'ok', <<"logged_out">>} -> - update_acdc_agent(Call, AcctId, AgentId, <<"login">>, fun kapi_acdc_agent:publish_login/1); + case acdc_agent_util:most_recent_status(AccountId, AgentId) of + {'ok', <<"logout">>} -> + update_acdc_agent(Call, AccountId, AgentId, <<"login">>, fun kapi_acdc_agent:publish_login/1); {'ok', <<"pause">>} -> - update_acdc_agent(Call, AcctId, AgentId, <<"resume">>, fun kapi_acdc_agent:publish_resume/1); + update_acdc_agent(Call, AccountId, AgentId, <<"resume">>, fun kapi_acdc_agent:publish_resume/1); {'ok', _S} -> - update_acdc_agent(Call, AcctId, AgentId, <<"logout">>, fun kapi_acdc_agent:publish_logout/1) + update_acdc_agent(Call, AccountId, AgentId, <<"logout">>, fun kapi_acdc_agent:publish_logout/1) end. -spec update_acdc_agent(kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), fun()) -> kz_term:ne_binary(). -update_acdc_agent(Call, AcctId, AgentId, Status, PubFun) -> +update_acdc_agent(Call, AccountId, AgentId, Status, PubFun) -> lager:debug("agent ~s going to new status ~s", [AgentId, Status]), try update_agent_device(Call, AgentId, Status) of {'ok', _D} -> lager:debug("updated device with new owner"), 'ok' = save_status(Call, AgentId, Status), - send_new_status(AcctId, AgentId, PubFun), + send_new_status(AccountId, AgentId, PubFun), play_status_prompt(Call, Status); {'error', 'not_owner'} -> play_failed_update(Call) catch @@ -171,9 +170,9 @@ move_agent_device(Call, AgentId, Device) -> {'ok', _} = kz_datamgr:save_doc(kapps_call:account_db(Call), kz_json:set_value(<<"owner_id">>, AgentId, Device)). -spec send_new_status(kz_term:ne_binary(), kz_term:ne_binary(), kz_amqp_worker:publish_fun()) -> 'ok'. -send_new_status(AcctId, AgentId, PubFun) -> +send_new_status(AccountId, AgentId, PubFun) -> Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), diff --git a/applications/acdc/src/acdc_init.erl b/applications/acdc/src/acdc_init.erl index ff7d5490a83..b589e5d1839 100644 --- a/applications/acdc/src/acdc_init.erl +++ b/applications/acdc/src/acdc_init.erl @@ -5,7 +5,6 @@ %%% %%% @author James Aimonetti %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -24,21 +23,22 @@ -include("acdc.hrl"). +-define(CB_AGENTS_LIST, <<"queues/agents_listing">>). + -spec start_link() -> 'ignore'. start_link() -> _ = declare_exchanges(), _ = kz_process:spawn(fun init_acdc/0, []), 'ignore'. --spec init_acdc() -> 'ok'. +-spec init_acdc() -> any(). init_acdc() -> kz_log:put_callid(?MODULE), case kz_datamgr:get_all_results(?KZ_ACDC_DB, <<"acdc/accounts_listing">>) of {'ok', []} -> lager:debug("no accounts configured for acdc"); {'ok', Accounts} -> - _ = [init_acct(kz_json:get_value(<<"key">>, Account)) || Account <- Accounts], - 'ok'; + [init_acct(kz_json:get_value(<<"key">>, Account)) || Account <- Accounts]; {'error', 'not_found'} -> lager:debug("acdc db not found, initializing"), _ = init_db(), @@ -50,8 +50,7 @@ init_acdc() -> -spec init_db() -> any(). init_db() -> _ = kz_datamgr:db_create(?KZ_ACDC_DB), - _ = kapps_maintenance:refresh(?KZ_ACDC_DB), - 'ok'. + _ = kz_datamgr:revise_doc_from_file(?KZ_ACDC_DB, 'crossbar', <<"views/acdc.json">>). -spec init_acct(kz_term:ne_binary()) -> 'ok'. init_acct(Account) -> @@ -65,7 +64,7 @@ init_acct(Account) -> _ = init_acct_queues(AccountDb, AccountId), init_acct_agents(AccountDb, AccountId). --spec init_acct_queues(kz_term:ne_binary()) -> 'ok'. +-spec init_acct_queues(kz_term:ne_binary()) -> any(). init_acct_queues(Account) -> AccountDb = kzs_util:format_account_db(Account), AccountId = kzs_util:format_account_id(Account), @@ -73,7 +72,7 @@ init_acct_queues(Account) -> lager:debug("init acdc account queues: ~s", [AccountId]), init_acct_queues(AccountDb, AccountId). --spec init_acct_agents(kz_term:ne_binary()) -> 'ok'. +-spec init_acct_agents(kz_term:ne_binary()) -> any(). init_acct_agents(Account) -> AccountDb = kzs_util:format_account_db(Account), AccountId = kzs_util:format_account_id(Account), @@ -81,20 +80,20 @@ init_acct_agents(Account) -> lager:debug("init acdc account agents: ~s", [AccountId]), init_acct_agents(AccountDb, AccountId). --spec init_acct_queues(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +-spec init_acct_queues(kz_term:ne_binary(), kz_term:ne_binary()) -> any(). init_acct_queues(AccountDb, AccountId) -> init_queues(AccountId ,kz_datamgr:get_results(AccountDb, <<"queues/crossbar_listing">>, []) ). --spec init_acct_agents(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +-spec init_acct_agents(kz_term:ne_binary(), kz_term:ne_binary()) -> any(). init_acct_agents(AccountDb, AccountId) -> init_agents(AccountId - ,kz_datamgr:get_results(AccountDb, <<"queues/agents_listing">> - ,[{'reduce', 'false'}]) + ,kz_datamgr:get_results(AccountDb, ?CB_AGENTS_LIST + ,[{'reduce', 'false'}]) ). --spec init_queues(kz_term:ne_binary(), kazoo_data:get_results_return()) -> 'ok'. +-spec init_queues(kz_term:ne_binary(), kazoo_data:get_results_return()) -> any(). init_queues(_, {'ok', []}) -> 'ok'; init_queues(AccountId, {'error', 'gateway_timeout'}) -> lager:debug("gateway timed out loading queues in account ~s, trying again in a moment", [AccountId]), @@ -102,8 +101,7 @@ init_queues(AccountId, {'error', 'gateway_timeout'}) -> wait_a_bit(), 'ok'; init_queues(AccountId, {'error', 'not_found'}) -> - lager:error("the queues view for ~s appears to be missing; you should probably fix that", [AccountId]), - 'ok'; + lager:error("the queues view for ~s appears to be missing; you should probably fix that", [AccountId]); init_queues(AccountId, {'error', _E}) -> lager:debug("error fetching queues: ~p", [_E]), try_queues_again(AccountId), @@ -111,10 +109,9 @@ init_queues(AccountId, {'error', _E}) -> 'ok'; init_queues(AccountId, {'ok', Qs}) -> acdc_stats:init_db(AccountId), - _ = [acdc_queues_sup:new(AccountId, kz_doc:id(Q)) || Q <- Qs], - 'ok'. + [acdc_queues_sup:new(AccountId, kz_doc:id(Q)) || Q <- Qs]. --spec init_agents(kz_term:ne_binary(), kazoo_data:get_results_return()) -> 'ok'. +-spec init_agents(kz_term:ne_binary(), kazoo_data:get_results_return()) -> any(). init_agents(_, {'ok', []}) -> 'ok'; init_agents(AccountId, {'error', 'gateway_timeout'}) -> lager:debug("gateway timed out loading agents in account ~s, trying again in a moment", [AccountId]), @@ -129,8 +126,7 @@ init_agents(AccountId, {'error', _E}) -> wait_a_bit(), 'ok'; init_agents(AccountId, {'ok', As}) -> - _ = [spawn_previously_logged_in_agent(AccountId, kz_doc:id(A)) || A <- As], - 'ok'. + [spawn_previously_logged_in_agent(AccountId, kz_doc:id(A)) || A <- As]. wait_a_bit() -> timer:sleep(1000 + rand:uniform(500)). @@ -151,10 +147,9 @@ try_again(AccountId, F) -> spawn_previously_logged_in_agent(AccountId, AgentId) -> kz_process:spawn( fun() -> - {'ok', Status} = acdc_agent_util:most_recent_status(AccountId, AgentId), - case acdc_agent_util:status_should_auto_start(Status) of - 'false' -> lager:debug("agent ~s in ~s is ~s, not starting", [AgentId, AccountId, Status]); - 'true' -> acdc_agents_sup:new(AccountId, AgentId) + case acdc_agent_util:most_recent_status(AccountId, AgentId) of + {'ok', <<"logged_out">>} -> lager:debug("agent ~s in ~s is logged out, not starting", [AgentId, AccountId]); + {'ok', _Status} -> acdc_agents_sup:new(AccountId, AgentId) end end). diff --git a/applications/acdc/src/acdc_listener.erl b/applications/acdc/src/acdc_listener.erl index fd7aead6609..d7500cc5e11 100644 --- a/applications/acdc/src/acdc_listener.erl +++ b/applications/acdc/src/acdc_listener.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -35,6 +34,7 @@ ,{'self', []} ,{'conf', [{'doc_type', <<"queue">>} ,{'action', <<"created">>} + ,'federate' ]} ]). -define(RESPONDERS, [ @@ -52,10 +52,10 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the server. +%% @doc Starts the server %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> gen_listener:start_link(?SERVER ,[{'bindings', ?BINDINGS} @@ -67,25 +67,30 @@ start_link() -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Initializes the server. +%% @private +%% @doc Initializes the server %% @end %%------------------------------------------------------------------------------ -spec init([]) -> {'ok', #state{}}. init([]) -> {'ok', #state{}}. %%------------------------------------------------------------------------------ -%% @doc Handling call messages. +%% @private +%% @doc Handling call messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_term:handle_call_ret_state(state()). handle_call(_Request, _From, State) -> {'reply', {'error', 'not_implemented'}, State}. %%------------------------------------------------------------------------------ -%% @doc Handling cast messages. +%% @private +%% @doc Handling cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). +-spec handle_cast(any(), state()) -> kz_term:handle_cast_ret_state(state()). handle_cast({'gen_listener',{'is_consuming',_IsConsuming}}, State) -> {'noreply', State}; handle_cast({'gen_listener',{'created_queue',_QueueName}}, State) -> @@ -95,16 +100,20 @@ handle_cast(_Msg, State) -> {'noreply', State}. %%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. +%% @private +%% @doc Handling all non call/cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +-spec handle_info(any(), state()) -> kz_term:handle_info_ret_state(state()). handle_info(_Info, State) -> lager:debug("unhandled message: ~p", [_Info]), {'noreply', State}. %%------------------------------------------------------------------------------ -%% @doc Allows listener to pass options to handlers. +%% @private +%% @doc Allows listener to pass options to handlers +%% %% @end %%------------------------------------------------------------------------------ -spec handle_event(kz_json:object(), kz_term:proplist()) -> gen_listener:handle_event_return(). @@ -112,9 +121,10 @@ handle_event(_JObj, _State) -> {'reply', []}. %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_server' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_server' terminates +%% @private +%% @doc This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @end @@ -124,7 +134,9 @@ terminate(_Reason, _State) -> lager:debug("acdc listener terminating: ~p", [_Reason]). %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), state(), any()) -> {'ok', state()}. diff --git a/applications/acdc/src/acdc_maintenance.erl b/applications/acdc/src/acdc_maintenance.erl index 027c1c405b6..d758d048d05 100644 --- a/applications/acdc/src/acdc_maintenance.erl +++ b/applications/acdc/src/acdc_maintenance.erl @@ -3,6 +3,7 @@ %%% @doc Helpers for cli commands %%% @author James Aimonetti %%% +%%% @author James Aimonetti %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -17,11 +18,8 @@ ,current_agents/1 ,logout_agents/1, logout_agent/2 ,agent_presence_id/2 - ,migrate_to_acdc_db/0, migrate/0 - + ,migrate_to_acdc_db/0, migrate_to_acdc_db/1, migrate/0 ,refresh/0, refresh_account/1 - ,register_views/0 - ,flush_call_stat/1 ,queues_summary/0, queues_summary/1, queue_summary/2 ,queues_detail/0, queues_detail/1, queue_detail/2 @@ -37,6 +35,8 @@ ,agent_queue_logout/3 ]). +-export([register_views/0]). + -include("acdc.hrl"). -spec logout_agents(kz_term:ne_binary()) -> 'ok'. @@ -198,29 +198,26 @@ refresh() -> end. -spec refresh_account(kz_term:ne_binary()) -> 'ok'. -refresh_account(Account) -> - MODB = acdc_stats_util:db_name(Account), - refresh_account(MODB, kazoo_modb:maybe_create(MODB)), - lager:debug("refreshed: ~s", [MODB]). - -refresh_account(MODB, 'true') -> - lager:debug("created ~s", [MODB]), - _ = kapps_maintenance:refresh(MODB), - 'ok'; -refresh_account(MODB, 'false') -> - case kz_datamgr:db_exists(MODB) of +refresh_account(Acct) -> + AccountDb = kzs_util:format_account_db(Acct), + kz_datamgr:revise_views_from_folder(AccountDb, 'acdc'), + io:format("revised acdc views for ~s~n", [AccountDb]), + MoDB = acdc_stats_util:db_name(Acct), + refresh_account(MoDB, kazoo_modb:maybe_create(MoDB)), + lager:debug("refreshed: ~s", [MoDB]). + +refresh_account(MoDB, 'true') -> + lager:debug("created ~s", [MoDB]), + kz_datamgr:revise_views_from_folder(MoDB, 'acdc'); +refresh_account(MoDB, 'false') -> + case kz_datamgr:db_exists(MoDB) of 'true' -> - lager:debug("exists ~s", [MODB]), - _ = kapps_maintenance:refresh(MODB), - 'ok'; + lager:debug("exists ~s", [MoDB]), + kz_datamgr:revise_views_from_folder(MoDB, 'acdc'); 'false' -> - lager:debug("modb ~s was not created", [MODB]) + lager:debug("modb ~s was not created", [MoDB]) end. --spec register_views() -> 'ok'. -register_views() -> - kz_datamgr:register_views_from_folder('acdc'). - -spec migrate() -> 'ok'. migrate() -> migrate_to_acdc_db(). @@ -283,8 +280,8 @@ maybe_migrate(AccountId) -> ,[{'account_id', AccountId} ,{'type', <<"acdc_activation">>} ]), - _ = kz_datamgr:ensure_saved(?KZ_ACDC_DB, Doc), - io:format("saved account ~s to db~n", [AccountId]); + kz_datamgr:ensure_saved(?KZ_ACDC_DB, Doc), + io:format("saved account ~s to acdc db~n", [AccountId]); {'error', _E} -> io:format("failed to query queue listing for account ~s: ~p~n", [AccountId, _E]) end. @@ -304,11 +301,11 @@ flush_call_stat(CallId) -> case acdc_stats:find_call(CallId) of 'undefined' -> io:format("nothing found for call ~s~n", [CallId]); Call -> - acdc_stats:call_abandoned(kz_json:get_value(<<"Account-ID">>, Call) - ,kz_json:get_value(<<"Queue-ID">>, Call) - ,CallId - ,'INTERNAL_ERROR' - ), + _ = acdc_stats:call_abandoned(kz_json:get_value(<<"Account-ID">>, Call) + ,kz_json:get_value(<<"Queue-ID">>, Call) + ,CallId + ,?ABANDON_INTERNAL_ERROR + ), io:format("setting call to 'abandoned'~n", []) end. @@ -318,26 +315,26 @@ queues_summary() -> show_queues_summary(acdc_queues_sup:queues_running()). -spec queues_summary(kz_term:ne_binary()) -> 'ok'. -queues_summary(AcctId) -> +queues_summary(AccountId) -> kz_log:put_callid(?MODULE), show_queues_summary( - [Q || {_, {QAcctId, _}} = Q <- acdc_queues_sup:queues_running(), - QAcctId =:= AcctId + [Q || {_, {QAccountId, _}} = Q <- acdc_queues_sup:queues_running(), + QAccountId =:= AccountId ]). -spec queue_summary(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -queue_summary(AcctId, QueueId) -> +queue_summary(AccountId, QueueId) -> kz_log:put_callid(?MODULE), show_queues_summary( - [Q || {_, {QAcctId, QQueueId}} = Q <- acdc_queues_sup:queues_running(), - QAcctId =:= AcctId, + [Q || {_, {QAccountId, QQueueId}} = Q <- acdc_queues_sup:queues_running(), + QAccountId =:= AccountId, QQueueId =:= QueueId ]). -spec show_queues_summary([{pid(), {kz_term:ne_binary(), kz_term:ne_binary()}}]) -> 'ok'. show_queues_summary([]) -> 'ok'; -show_queues_summary([{P, {AcctId, QueueId}}|Qs]) -> - ?PRINT(" Supervisor: ~p Acct: ~s Queue: ~s~n", [P, AcctId, QueueId]), +show_queues_summary([{P, {AccountId, QueueId}}|Qs]) -> + ?PRINT(" Supervisor: ~p Acct: ~s Queue: ~s~n", [P, AccountId, QueueId]), show_queues_summary(Qs). -spec queues_detail() -> 'ok'. @@ -345,57 +342,57 @@ queues_detail() -> acdc_queues_sup:status(). -spec queues_detail(kz_term:ne_binary()) -> 'ok'. -queues_detail(AcctId) -> +queues_detail(AccountId) -> kz_log:put_callid(?MODULE), - Supervisors = acdc_queues_sup:find_acct_supervisors(AcctId), + Supervisors = acdc_queues_sup:find_acct_supervisors(AccountId), lists:foreach(fun acdc_queue_sup:status/1, Supervisors). -spec queue_detail(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -queue_detail(AcctId, QueueId) -> - case acdc_queues_sup:find_queue_supervisor(AcctId, QueueId) of - 'undefined' -> lager:info("no queue ~s in account ~s", [QueueId, AcctId]); +queue_detail(AccountId, QueueId) -> + case acdc_queues_sup:find_queue_supervisor(AccountId, QueueId) of + 'undefined' -> lager:info("no queue ~s in account ~s", [QueueId, AccountId]); Pid -> acdc_queue_sup:status(Pid) end. -spec queues_restart(kz_term:ne_binary()) -> 'ok'. -queues_restart(AcctId) -> +queues_restart(AccountId) -> kz_log:put_callid(?MODULE), - case acdc_queues_sup:find_acct_supervisors(AcctId) of - [] -> lager:info("there are no running queues in ~s", [AcctId]); + case acdc_queues_sup:find_acct_supervisors(AccountId) of + [] -> lager:info("there are no running queues in ~s", [AccountId]); Pids -> - F = fun (Pid) -> maybe_stop_then_start_queue(AcctId, Pid) end, + F = fun (Pid) -> maybe_stop_then_start_queue(AccountId, Pid) end, lists:foreach(F, Pids) end. -spec queue_restart(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -queue_restart(AcctId, QueueId) -> +queue_restart(AccountId, QueueId) -> kz_log:put_callid(?MODULE), - case acdc_queues_sup:find_queue_supervisor(AcctId, QueueId) of + case acdc_queues_sup:find_queue_supervisor(AccountId, QueueId) of 'undefined' -> - lager:info("queue ~s in account ~s not running", [QueueId, AcctId]); + lager:info("queue ~s in account ~s not running", [QueueId, AccountId]); Pid -> - maybe_stop_then_start_queue(AcctId, QueueId, Pid) + maybe_stop_then_start_queue(AccountId, QueueId, Pid) end. -spec maybe_stop_then_start_queue(kz_term:ne_binary(), pid()) -> 'ok'. -maybe_stop_then_start_queue(AcctId, Pid) -> - {AcctId, QueueId} = acdc_queue_manager:config(acdc_queue_sup:manager(Pid)), - maybe_stop_then_start_queue(AcctId, QueueId, Pid). +maybe_stop_then_start_queue(AccountId, Pid) -> + {AccountId, QueueId} = acdc_queue_manager:config(acdc_queue_sup:manager(Pid)), + maybe_stop_then_start_queue(AccountId, QueueId, Pid). -spec maybe_stop_then_start_queue(kz_term:ne_binary(), kz_term:ne_binary(), pid()) -> 'ok'. -maybe_stop_then_start_queue(AcctId, QueueId, Pid) -> +maybe_stop_then_start_queue(AccountId, QueueId, Pid) -> case supervisor:terminate_child('acdc_queues_sup', Pid) of 'ok' -> lager:info("stopped queue supervisor ~p", [Pid]), - maybe_start_queue(AcctId, QueueId); + maybe_start_queue(AccountId, QueueId); {'error', 'not_found'} -> lager:info("queue supervisor ~p not found", [Pid]); {'error', _E} -> lager:info("failed to terminate queue supervisor ~p: ~p", [_E]) end. -maybe_start_queue(AcctId, QueueId) -> - case acdc_queues_sup:new(AcctId, QueueId) of +maybe_start_queue(AccountId, QueueId) -> + case acdc_queues_sup:new(AccountId, QueueId) of {'ok', 'undefined'} -> lager:info("tried to start queue but it asked to be ignored"); {'ok', Pid} -> @@ -414,26 +411,26 @@ agents_summary() -> show_agents_summary(acdc_agents_sup:agents_running()). -spec agents_summary(kz_term:ne_binary()) -> 'ok'. -agents_summary(AcctId) -> +agents_summary(AccountId) -> kz_log:put_callid(?MODULE), show_agents_summary( - [A || {_, {AAcctId, _, _}} = A <- acdc_agents_sup:agents_running(), - AAcctId =:= AcctId + [A || {_, {AAccountId, _, _}} = A <- acdc_agents_sup:agents_running(), + AAccountId =:= AccountId ]). -spec agent_summary(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_summary(AcctId, AgentId) -> +agent_summary(AccountId, AgentId) -> kz_log:put_callid(?MODULE), show_agents_summary( - [Q || {_, {AAcctId, AAgentId, _}} = Q <- acdc_agents_sup:agents_running(), - AAcctId =:= AcctId, + [Q || {_, {AAccountId, AAgentId, _}} = Q <- acdc_agents_sup:agents_running(), + AAccountId =:= AccountId, AAgentId =:= AgentId ]). -spec show_agents_summary([{pid(), acdc_agent_listener:config()}]) -> 'ok'. show_agents_summary([]) -> 'ok'; -show_agents_summary([{P, {AcctId, QueueId, _AMQPQueue}}|Qs]) -> - lager:info(" Supervisor: ~p Acct: ~s Agent: ~s", [P, AcctId, QueueId]), +show_agents_summary([{P, {AccountId, QueueId, _AMQPQueue}}|Qs]) -> + lager:info(" Supervisor: ~p Acct: ~s Agent: ~s", [P, AccountId, QueueId]), show_queues_summary(Qs). -spec agents_detail() -> 'ok'. @@ -442,89 +439,93 @@ agents_detail() -> acdc_agents_sup:status(). -spec agents_detail(kz_term:ne_binary()) -> 'ok'. -agents_detail(AcctId) -> +agents_detail(AccountId) -> kz_log:put_callid(?MODULE), - Supervisors = acdc_agents_sup:find_acct_supervisors(AcctId), + Supervisors = acdc_agents_sup:find_acct_supervisors(AccountId), lists:foreach(fun acdc_agent_sup:status/1, Supervisors). -spec agent_detail(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_detail(AcctId, AgentId) -> +agent_detail(AccountId, AgentId) -> kz_log:put_callid(?MODULE), - case acdc_agents_sup:find_agent_supervisor(AcctId, AgentId) of - 'undefined' -> lager:info("no agent ~s in account ~s", [AgentId, AcctId]); + case acdc_agents_sup:find_agent_supervisor(AccountId, AgentId) of + 'undefined' -> lager:info("no agent ~s in account ~s", [AgentId, AccountId]); Pid -> acdc_agent_sup:status(Pid) end. -spec agent_login(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_login(AcctId, AgentId) -> +agent_login(AccountId, AgentId) -> kz_log:put_callid(?MODULE), Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - _ = kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_login/1), + kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_login/1), lager:info("published login update for agent"). -spec agent_logout(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_logout(AcctId, AgentId) -> +agent_logout(AccountId, AgentId) -> kz_log:put_callid(?MODULE), Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - _ = kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_logout/1), + kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_logout/1), lager:info("published logout update for agent"). -spec agent_pause(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_pause(AcctId, AgentId) -> +agent_pause(AccountId, AgentId) -> Timeout = kapps_config:get_integer(?CONFIG_CAT, <<"default_agent_pause_timeout">>, 600), - agent_pause(AcctId, AgentId, Timeout). + agent_pause(AccountId, AgentId, Timeout). -spec agent_pause(kz_term:ne_binary(), kz_term:ne_binary(), pos_integer()) -> 'ok'. -agent_pause(AcctId, AgentId, Timeout) -> +agent_pause(AccountId, AgentId, Timeout) -> kz_log:put_callid(?MODULE), Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Timeout">>, kz_term:to_integer(Timeout)} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - _ = kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_pause/1), + kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_pause/1), lager:info("published pause for agent"). -spec agent_resume(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_resume(AcctId, AgentId) -> +agent_resume(AccountId, AgentId) -> kz_log:put_callid(?MODULE), Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - _ = kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_resume/1), + kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_resume/1), lager:info("published resume for agent"). -spec agent_queue_login(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_queue_login(AcctId, AgentId, QueueId) -> +agent_queue_login(AccountId, AgentId, QueueId) -> kz_log:put_callid(?MODULE), Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Queue-ID">>, QueueId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - _ = kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_login_queue/1), + kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_login_queue/1), lager:info("published login update for agent"). -spec agent_queue_logout(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_queue_logout(AcctId, AgentId, QueueId) -> +agent_queue_logout(AccountId, AgentId, QueueId) -> kz_log:put_callid(?MODULE), Update = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Agent-ID">>, AgentId} ,{<<"Queue-ID">>, QueueId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - _ = kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_logout_queue/1), + kz_amqp_worker:cast(Update, fun kapi_acdc_agent:publish_logout_queue/1), lager:info("published logout update for agent"). + +-spec register_views() -> 'ok'. +register_views() -> + kz_datamgr:register_views_from_folder(?APP). diff --git a/applications/acdc/src/acdc_queue_fsm.erl b/applications/acdc/src/acdc_queue_fsm.erl index bab5358a2b7..c062cceac26 100644 --- a/applications/acdc/src/acdc_queue_fsm.erl +++ b/applications/acdc/src/acdc_queue_fsm.erl @@ -3,6 +3,7 @@ %%% @doc Controls how a queue process progresses a member_call %%% @author James Aimonetti %%% +%%% @author James Aimonetti %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -10,7 +11,6 @@ %%% @end %%%----------------------------------------------------------------------------- -module(acdc_queue_fsm). - -behaviour(gen_statem). %% API @@ -18,17 +18,20 @@ %% Event injectors -export([member_call/3 + ,member_call_cancel/2 ,member_connect_resp/2 ,member_accepted/2 + ,member_callback_accepted/2 ,member_connect_retry/2 ,call_event/4 ,refresh/2 ,current_call/1 ,status/1 - ,finish_member_call/1 %% Accessors ,cdr_url/1 + + ,register_callback/2 ]). %% State handlers @@ -60,10 +63,10 @@ -define(AGENT_RING_TIMEOUT, 5). -define(AGENT_RING_TIMEOUT_MESSAGE, 'agent_timer_expired'). --record(state, {listener_proc :: kz_term:api_pid() +-record(state, {listener_proc :: pid() | undefined ,manager_proc :: pid() - ,connect_resps = [] :: kz_json:objects() ,connect_wins = [] :: kz_json:objects() + ,connect_resps = [] :: kz_json:objects() ,collect_ref :: kz_term:api_reference() ,account_id :: kz_term:ne_binary() ,account_db :: kz_term:ne_binary() @@ -73,9 +76,9 @@ ,connection_timer_ref :: kz_term:api_reference() % how long can a caller wait in the queue ,agent_ring_timer_ref :: kz_term:api_reference() % how long to ring an agent before moving to the next - ,member_call :: kapps_call:call() | 'undefined' - ,member_call_start :: kz_time:start_time() | 'undefined' - ,member_call_winners :: [kz_term:api_object()] %% who won the call + ,member_call :: kapps_call:call() + ,member_call_start :: kz_term:api_non_neg_integer() + ,member_call_winners :: [kz_term:api_object()] | 'undefined' %% list of who won the call %% Config options ,name :: kz_term:ne_binary() @@ -94,10 +97,12 @@ ,cdr_url :: kz_term:api_binary() % optional URL to request for extra CDR data ,notifications :: kz_term:api_object() + + ,callback_details :: {kz_term:ne_binary(), kz_term:ne_binary()} | 'undefined' }). -type state() :: #state{}. --define(WSD_ID, {'file', <<(get('callid'))/binary, "_queue_statem">>}). +-define(WSD_ID, {'file', <<(get('callid'))/binary, "_queue_fsm">>}). %%%============================================================================= %%% API @@ -114,81 +119,97 @@ start_link(WorkerSup, MgrPid, AccountId, QueueId) -> gen_statem:start_link(?SERVER, [WorkerSup, MgrPid, AccountId, QueueId], []). -spec refresh(pid(), kz_json:object()) -> 'ok'. -refresh(ServerRef, QueueJObj) -> - gen_statem:cast(ServerRef, {'refresh', QueueJObj}). +refresh(FSM, QueueJObj) -> + gen_statem:cast(FSM, {'refresh', QueueJObj}). + %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec member_call(pid(), kz_json:object(), gen_listener:basic_deliver()) -> 'ok'. -member_call(ServerRef, CallJObj, Delivery) -> - gen_statem:cast(ServerRef, {'member_call', CallJObj, Delivery}). +member_call(FSM, CallJObj, Delivery) -> + gen_statem:cast(FSM, {'member_call', CallJObj, Delivery}). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec member_call_cancel(pid(), kz_json:object()) -> 'ok'. +member_call_cancel(FSM, JObj) -> + gen_statem:cast(FSM, {'member_call_cancel', JObj}). %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec member_connect_resp(pid(), kz_json:object()) -> 'ok'. -member_connect_resp(ServerRef, Resp) -> - gen_statem:cast(ServerRef, {'agent_resp', Resp}). +member_connect_resp(FSM, Resp) -> + gen_statem:cast(FSM, {'agent_resp', Resp}). %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec member_accepted(pid(), kz_json:object()) -> 'ok'. -member_accepted(ServerRef, AcceptJObj) -> - gen_statem:cast(ServerRef, {'accepted', AcceptJObj}). +member_accepted(FSM, AcceptJObj) -> + gen_statem:cast(FSM, {'accepted', AcceptJObj}). + +-spec member_callback_accepted(pid(), kz_json:object()) -> 'ok'. +member_callback_accepted(FSM, AcceptJObj) -> + gen_statem:cast(FSM, {'callback_accepted', AcceptJObj}). %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec member_connect_retry(pid(), kz_json:object()) -> 'ok'. -member_connect_retry(ServerRef, RetryJObj) -> - gen_statem:cast(ServerRef, {'retry', RetryJObj}). +member_connect_retry(FSM, RetryJObj) -> + gen_statem:cast(FSM, {'retry', RetryJObj}). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec register_callback(pid(), kz_json:object()) -> 'ok'. +register_callback(FSM, JObj) -> + gen_statem:cast(FSM, {'register_callback', JObj}). + %%------------------------------------------------------------------------------ %% @doc When a queue is processing a call, it will receive call events. -%% Pass the call event to the statem to see if action is needed (usually +%% Pass the call event to the FSM to see if action is needed (usually %% for hangup events). %% @end %%------------------------------------------------------------------------------ -spec call_event(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. -call_event(ServerRef, <<"call_event">>, <<"CHANNEL_DESTROY">>, EvtJObj) -> - gen_statem:cast(ServerRef, {'member_hungup', EvtJObj}); -call_event(ServerRef, <<"call_event">>, <<"DTMF">>, EvtJObj) -> - gen_statem:cast(ServerRef, {'dtmf_pressed', kz_json:get_value(<<"DTMF-Digit">>, EvtJObj)}); -call_event(ServerRef, <<"call_event">>, <<"CHANNEL_BRIDGE">>, EvtJObj) -> - gen_statem:cast(ServerRef, {'channel_bridged', EvtJObj}); +call_event(FSM, <<"call_event">>, <<"CHANNEL_DESTROY">>, EvtJObj) -> + gen_statem:cast(FSM, {'member_hungup', EvtJObj}); +call_event(FSM, <<"call_event">>, <<"DTMF">>, EvtJObj) -> + gen_statem:cast(FSM, {'dtmf_pressed', kz_json:get_value(<<"DTMF-Digit">>, EvtJObj)}); +call_event(FSM, <<"call_event">>, <<"CHANNEL_BRIDGE">>, EvtJObj) -> + gen_statem:cast(FSM, {'channel_bridged', EvtJObj}); call_event(_, _E, _N, _J) -> 'ok'. -%% lager:debug("unhandled event: ~s: ~s (~s)" -%% ,[_E, _N, kz_json:get_value(<<"Application-Name">>, _J)] -%% ). - --spec finish_member_call(pid()) -> 'ok'. -finish_member_call(ServerRef) -> - gen_statem:cast(ServerRef, {'member_finished'}). -spec current_call(pid()) -> kz_term:api_object(). -current_call(ServerRef) -> - gen_statem:call(ServerRef, 'current_call'). +current_call(FSM) -> + gen_statem:call(FSM, 'current_call'). -spec status(pid()) -> kz_term:proplist(). -status(ServerRef) -> - gen_statem:call(ServerRef, 'status'). +status(FSM) -> + gen_statem:call(FSM, 'status'). -spec cdr_url(pid()) -> kz_term:api_binary(). -cdr_url(ServerRef) -> - gen_statem:call(ServerRef, 'cdr_url'). +cdr_url(FSM) -> + gen_statem:call(FSM, 'cdr_url'). %%%============================================================================= %%% gen_statem callbacks %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a gen_statem is started using +%% @private +%% @doc Whenever a gen_statem is started using gen_statem:start/[3,4] or %% gen_statem:start_link/[3,4], this function is called by the new %% process to initialize. %% @@ -208,10 +229,9 @@ init([WorkerSup, MgrPid, AccountId, QueueId]) -> {'ok' ,'ready' ,#state{manager_proc = MgrPid - ,account_id = AccountId - ,account_db = AccountDb + ,account_id = kz_doc:account_id(QueueJObj) + ,account_db = kz_doc:account_db(QueueJObj) ,queue_id = QueueId - ,name = kz_json:get_value(<<"name">>, QueueJObj) ,connection_timeout = connection_timeout(kz_json:get_integer_value(<<"connection_timeout">>, QueueJObj)) ,agent_ring_timeout = agent_ring_timeout(kz_json:get_integer_value(<<"agent_ring_timeout">>, QueueJObj)) @@ -225,8 +245,6 @@ init([WorkerSup, MgrPid, AccountId, QueueId]) -> ,recording_url = kz_json:get_ne_value(<<"call_recording_url">>, QueueJObj) ,cdr_url = kz_json:get_ne_value(<<"cdr_url">>, QueueJObj) ,member_call = 'undefined' - ,member_call_winners = [] - ,notifications = kz_json:get_value(<<"notifications">>, QueueJObj) } }. @@ -248,9 +266,10 @@ ready('cast', {'get_listener_proc', WorkerSup}, State) -> ListenerSrv = acdc_queue_worker_sup:listener(WorkerSup), lager:debug("got listener proc: ~p", [ListenerSrv]), {'next_state', 'ready', State#state{listener_proc=ListenerSrv}}; + ready('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=ListenerSrv - ,manager_proc=MgrSrv - }=State) -> + ,manager_proc=MgrSrv + }=State) -> Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, CallJObj)), CallId = kapps_call:call_id(Call), kz_log:put_callid(CallId), @@ -263,38 +282,54 @@ ready('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=Listener acdc_queue_listener:ignore_member_call(ListenerSrv, Call, Delivery), {'next_state', 'ready', State} end; + ready('cast', {'agent_resp', _Resp}, State) -> lager:debug("someone jumped the gun, or was slow on the draw"), {'next_state', 'ready', State}; + ready('cast', {'accepted', _AcceptJObj}, State) -> - lager:debug("weird to receive an acceptance"), + lager:debug("weird to receive accepted in state 'ready'"), + {'next_state', 'ready', State}; + +ready('cast', {'callback_accepted', _AcceptJObj}, State) -> + lager:debug("weird to receive callback_acceptance in state 'ready'"), {'next_state', 'ready', State}; + ready('cast', {'retry', _RetryJObj}, State) -> lager:debug("weird to receive a retry when we're just hanging here"), {'next_state', 'ready', State}; + ready('cast', {'member_hungup', _CallEvt}, State) -> lager:debug("member hungup from previous call: ~p", [_CallEvt]), {'next_state', 'ready', State}; -ready('cast', {'member_finished'}, State) -> - lager:debug("member finished while in 'ready', ignore"), - {'next_state', 'ready', State}; + ready('cast', {'dtmf_pressed', _DTMF}, State) -> lager:debug("DTMF(~s) for old call", [_DTMF]), {'next_state', 'ready', State}; + ready('cast', Event, State) -> handle_event(Event, ready, State); + ready({'call', From}, 'status', #state{cdr_url=Url - ,recording_url=RecordingUrl - }=State) -> + ,recording_url=RecordingUrl + }=State) -> {'next_state', 'ready', State ,{'reply', From, [{'state', <<"ready">>} - ,{<<"cdr_url">>, Url} - ,{<<"recording_url">>, RecordingUrl} - ]}}; + ,{<<"cdr_url">>, Url} + ,{<<"recording_url">>, RecordingUrl} + ]}}; + ready({'call', From}, 'current_call', State) -> - {'next_state', 'ready', State, {'reply', From, 'undefined'}}; + {'next_state', 'ready', State + ,{'reply', From, 'undefined'}}; + ready({'call', From}, Event, State) -> - handle_sync_event(Event, From, ready, State). + handle_sync_event(Event, From, ready, State); + +ready('info', {'timeout', _, ?COLLECT_RESP_MESSAGE}, State) -> + {'next_state', 'ready', State}; +ready('info', Evt, State) -> + handle_info(Evt, 'ready', State). %%------------------------------------------------------------------------------ %% @doc @@ -308,9 +343,29 @@ connect_req('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=Li acdc_queue_listener:cancel_member_call(ListenerSrv, CallJObj, Delivery), {'next_state', 'connect_req', State}; -connect_req('cast', {'agent_resp', Resp}, #state{connect_resps=CRs - ,manager_proc=MgrSrv +connect_req('cast', {'member_call_cancel', JObj}, #state{listener_proc=ListenerSrv + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=Call + ,caller_exit_key=DTMF }=State) -> + CallId = kapps_call:call_id(Call), + case kz_json:get_value(<<"Reason">>, JObj) =:= <<"dtmf_exit">> + andalso kz_json:get_value(<<"Call-ID">>, JObj) =:= CallId of + 'true' -> + lager:debug("member pressed the exit key (~s)", [DTMF]), + + webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - DTMF">>), + + acdc_queue_listener:exit_member_call(ListenerSrv), + _ = acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_EXIT), + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; + 'false' -> {'next_state', 'connect_req', State} + end; + +connect_req('cast', {'agent_resp', Resp}, #state{connect_resps=CRs + ,manager_proc=MgrSrv + }=State) -> Agents = acdc_queue_manager:current_agents(MgrSrv), Resps = [Resp | CRs], {NextState, State1} = @@ -329,15 +384,16 @@ connect_req('cast', {'accepted', AcceptJObj}=Accept, #state{member_call=Call}=St lager:debug("received (and ignoring) acceptance payload"), {'next_state', 'connect_req', State} end; + connect_req('cast', {'retry', _RetryJObj}, State) -> lager:debug("recv retry response before win sent"), {'next_state', 'connect_req', State}; connect_req('cast', {'member_hungup', JObj}, #state{listener_proc=ListenerSrv - ,member_call=Call - ,account_id=AccountId - ,queue_id=QueueId - }=State) -> + ,member_call=Call + ,account_id=AccountId + ,queue_id=QueueId + }=State) -> CallId = kapps_call:call_id(Call), case kz_json:get_value(<<"Call-ID">>, JObj) =:= CallId of 'true' -> @@ -346,7 +402,7 @@ connect_req('cast', {'member_hungup', JObj}, #state{listener_proc=ListenerSrv webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - abandon">>), acdc_queue_listener:cancel_member_call(ListenerSrv, JObj), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_HANGUP), + _ = acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_HANGUP), {'next_state', 'ready', clear_member_call(State), 'hibernate'}; 'false' -> lager:debug("hangup recv for ~s while processing ~s, ignoring", [kz_json:get_value(<<"Call-ID">>, JObj) @@ -355,94 +411,81 @@ connect_req('cast', {'member_hungup', JObj}, #state{listener_proc=ListenerSrv {'next_state', 'connect_req', State} end; -connect_req('cast', {'member_finished'}, #state{member_call=Call}=State) -> - case catch kapps_call:call_id(Call) of - CallId when is_binary(CallId) -> - lager:debug("member finished while in connect_req: ~s", [CallId]), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finished - forced">>); - _E-> - lager:debug("member finished, but callid became ~p", [_E]) - end, - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; - -connect_req('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=DTMF - ,listener_proc=ListenerSrv - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) when is_binary(DTMF) -> - lager:debug("member pressed the exit key (~s)", [DTMF]), - CallId = kapps_call:call_id(Call), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - DTMF">>), - - acdc_queue_listener:exit_member_call(ListenerSrv), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_EXIT), - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; +connect_req('cast', {'register_callback', JObj}, #state{connection_timer_ref=ConnRef}=State) -> + lager:debug("register_callback recv'd for ~s during connect_req", [kz_json:get_value(<<"Call-ID">>, JObj)]), + %% disable queue timeout for callback + maybe_stop_timer(ConnRef), + {'next_state', 'connect_req', State#state{connection_timer_ref='undefined'}}; connect_req('cast', Event, State) -> handle_event(Event, connect_req, State); -connect_req({'call', From}, 'status', #state{member_call=Call - ,member_call_start=Start - ,connection_timer_ref=ConnRef - ,cdr_url=Url - ,recording_url=RecordingUrl - }=State) -> +connect_req({call, From}, 'status', #state{member_call=Call + ,member_call_start=Start + ,connection_timer_ref=ConnRef + ,cdr_url=Url + ,recording_url=RecordingUrl + }=State) -> {'next_state', 'connect_req', State ,{'reply', From, [{<<"state">>, <<"connect_req">>} - ,{<<"call_id">>, kapps_call:call_id(Call)} - ,{<<"caller_id_name">>, kapps_call:caller_id_name(Call)} - ,{<<"caller_id_number">>, kapps_call:caller_id_name(Call)} - ,{<<"to">>, kapps_call:to_user(Call)} - ,{<<"from">>, kapps_call:from_user(Call)} - ,{<<"wait_left">>, elapsed(ConnRef)} - ,{<<"wait_time">>, elapsed(Start)} - ,{<<"cdr_url">>, Url} - ,{<<"recording_url">>, RecordingUrl} - ]}}; -connect_req({'call', From}, 'current_call', #state{member_call=Call - ,member_call_start=Start - ,connection_timer_ref=ConnRef - }=State) -> + ,{<<"call_id">>, kapps_call:call_id(Call)} + ,{<<"caller_id_name">>, kapps_call:caller_id_name(Call)} + ,{<<"caller_id_number">>, kapps_call:caller_id_name(Call)} + ,{<<"to">>, kapps_call:to_user(Call)} + ,{<<"from">>, kapps_call:from_user(Call)} + ,{<<"wait_left">>, elapsed(ConnRef)} + ,{<<"wait_time">>, elapsed(Start)} + ,{<<"cdr_url">>, Url} + ,{<<"recording_url">>, RecordingUrl} + ]}}; + +connect_req({call, From}, 'current_call', #state{member_call=Call + ,member_call_start=Start + ,connection_timer_ref=ConnRef + }=State) -> {'next_state', 'connect_req', State - ,{'reply', From, current_call(Call, ConnRef, Start)} - }; + ,{'reply', From, current_call(Call, ConnRef, Start)}}; + connect_req({'call', From}, Event, State) -> handle_sync_event(Event, From, connect_req, State); connect_req('info', {'timeout', Ref, ?COLLECT_RESP_MESSAGE}, #state{collect_ref=Ref - ,connect_resps=[] - ,manager_proc=MgrSrv - ,member_call=Call - ,listener_proc=ListenerSrv - ,account_id=AccountId - ,queue_id=QueueId - }=State) -> + ,connect_resps=[] + ,manager_proc=MgrSrv + ,member_call=Call + ,listener_proc=ListenerSrv + ,account_id=AccountId + ,queue_id=QueueId + }=State) -> maybe_stop_timer(Ref), case acdc_queue_manager:should_ignore_member_call(MgrSrv, Call, AccountId, QueueId) of 'true' -> lager:debug("queue mgr said to ignore this call: ~s, not retrying agents", [kapps_call:call_id(Call)]), acdc_queue_listener:finish_member_call(ListenerSrv), - {'next_state', 'ready', State}; + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; 'false' -> maybe_connect_re_req(MgrSrv, ListenerSrv, State) end; + connect_req('info', {'timeout', Ref, ?COLLECT_RESP_MESSAGE}, #state{collect_ref=Ref}=State) -> {NextState, State1} = handle_agent_responses(State), {'next_state', NextState, State1}; + connect_req('info', {'timeout', ConnRef, ?CONNECTION_TIMEOUT_MESSAGE}, #state{listener_proc=ListenerSrv - ,connection_timer_ref=ConnRef - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) -> + ,connection_timer_ref=ConnRef + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=Call + }=State) -> lager:debug("connection timeout occurred, bounce the caller out of the queue"), CallId = kapps_call:call_id(Call), webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - timeout">>), acdc_queue_listener:timeout_member_call(ListenerSrv), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_TIMEOUT), - {'next_state', 'ready', clear_member_call(State), 'hibernate'}. + _ = acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_TIMEOUT), + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; +connect_req('info', Evt, State) -> + handle_info(Evt, 'connect_req', State). %%------------------------------------------------------------------------------ %% @doc @@ -454,182 +497,250 @@ connecting('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=Lis acdc_queue_listener:cancel_member_call(ListenerSrv, CallJObj, Delivery), {'next_state', 'connecting', State}; +connecting('cast', {'member_call_cancel', JObj}, #state{listener_proc=ListenerSrv + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=Call + ,member_call_winners=Winners + ,caller_exit_key=DTMF + }=State) -> + CallId = kapps_call:call_id(Call), + case kz_json:get_value(<<"Reason">>, JObj) =:= <<"dtmf_exit">> + andalso kz_json:get_value(<<"Call-ID">>, JObj) =:= CallId of + 'true' -> + lager:debug("member pressed the exit key (~s)", [DTMF]), + + webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - DTMF">>), + + lists:foreach(fun(Winner) -> + lager:debug("sending timeout agent to ~s(~s)", [kz_json:get_value(<<"Agent-ID">>, Winner) ,kz_json:get_value(<<"Process-ID">>, Winner) ]), + acdc_queue_listener:timeout_agent(ListenerSrv, Winner) + end, + Winners), + acdc_queue_listener:exit_member_call(ListenerSrv), + _ = acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_EXIT), + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; + 'false' -> {'next_state', 'connecting', State} + end; + connecting('cast', {'agent_resp', _Resp}, State) -> lager:debug("agent resp must have just missed cutoff"), {'next_state', 'connecting', State}; connecting('cast', {'accepted', AcceptJObj}, #state{listener_proc=ListenerSrv - ,member_call=Call - ,account_id=AccountId - ,queue_id=QueueId - ,connect_wins=Wins - }=State) -> + ,connect_wins=Wins + ,member_call=Call + ,account_id=AccountId + ,queue_id=QueueId + }=State) -> + AcceptAgentID = kz_json:get_value(<<"Agent-ID">>, AcceptJObj), case accept_is_for_call(AcceptJObj, Call) of 'true' -> lager:debug("recv acceptance from agent"), CallId = kapps_call:call_id(Call), webseq:evt(?WSD_ID, self(), CallId, <<"member call - agent acceptance">>), - lists:foreach(fun(Win) -> acdc_queue_listener:member_connect_satisfied(ListenerSrv, Win, []) end, Wins), + lists:foreach(fun(Win) -> acdc_queue_listener:member_connect_satisfied(ListenerSrv, kz_json:set_value(<<"Accept-Agent-ID">>, AcceptAgentID, Win), []) end, Wins), + %% lists:foreach(fun(Win) -> acdc_queue_listener:member_connect_satisfied(ListenerSrv, Win, []) end, Wins), + acdc_queue_listener:finish_member_call(ListenerSrv, AcceptJObj), - acdc_stats:call_handled(AccountId, QueueId, CallId - ,kz_json:get_value(<<"Agent-ID">>, AcceptJObj) - ), + _ = case kz_json:get_value(<<"Old-Call-ID">>, AcceptJObj) of + 'undefined' -> + acdc_stats:call_handled(AccountId, QueueId, CallId + ,kz_json:get_value(<<"Agent-ID">>, AcceptJObj) + ); + %% If the old call id is set, we've already done the call handled stat update + _ -> 'ok' + end, {'next_state', 'ready', clear_member_call(State), 'hibernate'}; 'false' -> lager:debug("ignoring accepted message"), {'next_state', 'connecting', State} end; +connecting('cast', {'callback_accepted', AcceptJObj}, #state{listener_proc=ListenerSrv + ,connect_wins=Wins + ,agent_ring_timer_ref=AgentRef + ,member_call=Call + }=State) -> + AcceptAgentID = kz_json:get_value(<<"Agent-ID">>, AcceptJObj), + case accept_is_for_call(AcceptJObj, Call) of + 'true' -> + lager:debug("recv acceptance from agent, agent is calling back member"), + CallId = kapps_call:call_id(Call), + webseq:evt(?WSD_ID, self(), CallId, <<"member call - agent callback acceptance">>), + + lists:foreach(fun(Win) -> acdc_queue_listener:member_connect_satisfied(ListenerSrv, kz_json:set_value(<<"Accept-Agent-ID">>, AcceptAgentID, Win), []) end, Wins), + + %% Do not send timeout to the agent once they've picked up the + %% initiating call of the callback + maybe_stop_timer(AgentRef), + {'next_state', 'connecting', State#state{agent_ring_timer_ref='undefined'}}; + 'false' -> + lager:debug("ignoring callback_accepted message"), + {'next_state', 'connecting', State} + end; + connecting('cast', {'retry', RetryJObj}, #state{agent_ring_timer_ref=AgentRef - ,collect_ref=CollectRef - ,member_call_winners=[Winner|[]] - }=State) -> + ,collect_ref=CollectRef + ,member_call_winners=Winners + }=State) -> RetryProcId = kz_json:get_value(<<"Process-ID">>, RetryJObj), RetryAgentId = kz_json:get_value(<<"Agent-ID">>, RetryJObj), - case {kz_json:get_value(<<"Agent-ID">>, Winner), kz_json:get_value(<<"Process-ID">>, Winner)} of - {RetryAgentId, RetryProcId} -> - lager:debug("recv retry from our winning agent ~s(~s)", [RetryAgentId, RetryProcId]), + NewWinners = + lists:filter(fun(Winner) -> + RetryAgentId =/= kz_json:get_value(<<"Agent-ID">>, Winner) + andalso RetryProcId =/= kz_json:get_value(<<"Process-ID">>, Winner) + end, + Winners), - lager:debug("but wait, we have others who wanted to try"), + case NewWinners of + [] -> + lager:debug("recv retry from all of our winning agents"), erlang:send(self(), {'timeout', 'undefined', ?COLLECT_RESP_MESSAGE}), - maybe_stop_timer(CollectRef), maybe_stop_timer(AgentRef), - webseq:evt(?WSD_ID, webseq:process_pid(RetryJObj), self(), <<"member call - retry">>), - {'next_state', 'connect_req', State#state{agent_ring_timer_ref='undefined' - ,member_call_winners=[] + ,member_call_winners='undefined' ,collect_ref='undefined' }}; - {RetryAgentId, _OtherProcId} -> - lager:debug("recv retry from monitoring proc ~s(~s)", [RetryAgentId, RetryProcId]), - {'next_state', 'connecting', State}; - {_OtherAgentId, _OtherProcId} -> - lager:debug("recv retry from unknown agent ~s(~s)", [RetryAgentId, RetryProcId]), - {'next_state', 'connecting', State} + _ -> + lager:debug("recv retry from agent ~s(~s), removing from Winnners", [RetryAgentId, RetryProcId]), + {'next_state', 'connecting', State#state{member_call_winners=NewWinners}} end; -connecting('cast', {'retry', RetryJObj}, #state{member_call_winners=Winners}=State) -> - RetryAgentId = kz_json:get_value(<<"Agent-ID">>, RetryJObj), - RetryProcId = kz_json:get_value(<<"Process-ID">>, RetryJObj), - lager:info("~p told to retry but we have other winners", [RetryAgentId]), - {_Loser, Rest} = lists:partition(fun(Winner) -> {kz_json:get_value(<<"Agent-ID">>, Winner), kz_json:get_value(<<"Process-ID">>, Winner)} == {RetryAgentId, RetryProcId} end, Winners), - {'next_state', 'connecting', State#state{member_call_winners=Rest}}; connecting('cast', {'member_hungup', CallEvt}, #state{listener_proc=ListenerSrv - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) -> + ,connection_timer_ref='undefined' + ,agent_ring_timer_ref='undefined' + }=State) -> + lager:debug("caller did not answer a callback"), + acdc_queue_listener:finish_member_call(ListenerSrv), + + webseq:evt(?WSD_ID, self(), kz_json:get_value(<<"Call-ID">>, CallEvt), <<"member call - hungup">>), + + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; + +connecting('cast', {'member_hungup', CallEvt}, #state{listener_proc=ListenerSrv + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=Call + }=State) -> lager:debug("caller hungup while we waited for the agent to connect"), acdc_queue_listener:cancel_member_call(ListenerSrv, CallEvt), CallId = kapps_call:call_id(Call), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_HANGUP), + _ = acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_HANGUP), webseq:evt(?WSD_ID, self(), CallId, <<"member call - hungup">>), {'next_state', 'ready', clear_member_call(State), 'hibernate'}; -connecting('cast', {'member_finished'}, #state{member_call=Call}=State) -> - case catch kapps_call:call_id(Call) of - CallId when is_binary(CallId) -> - lager:debug("member finished while in connecting: ~s", [CallId]), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finished - forced">>); - _E-> - lager:debug("member finished, but callid became ~p", [_E]) - end, - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; -connecting('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=DTMF - ,listener_proc=ListenerSrv - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) when is_binary(DTMF) -> - lager:debug("member pressed the exit key (~s)", [DTMF]), - acdc_queue_listener:exit_member_call(ListenerSrv), - CallId = kapps_call:call_id(Call), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - DTMF">>), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_EXIT), - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; - -connecting('cast', {'dtmf_pressed', _DTMF}, State) -> - lager:debug("caller pressed ~s, ignoring", [_DTMF]), - {'next_state', 'connecting', State}; +connecting('cast', {'register_callback', JObj}, #state{listener_proc=ListenerSrv + ,connection_timer_ref=ConnRef + ,agent_ring_timer_ref=AgentRef + ,member_call_winners=Winners + }=State) -> + lager:debug("register_callback recv'd for ~s while connecting", [kz_json:get_value(<<"Call-ID">>, JObj)]), + %% disable queue timeout for callback + maybe_stop_timer(ConnRef), + %% cancel agent ringing and do re_req + erlang:send(self(), {'timeout', 'undefined', ?COLLECT_RESP_MESSAGE}), + maybe_stop_timer(AgentRef), + lists:foreach(fun(Winner) -> + lager:debug("sending timeout agent to ~s(~s)", [kz_json:get_value(<<"Agent-ID">>, Winner) ,kz_json:get_value(<<"Process-ID">>, Winner) ]), + acdc_queue_listener:timeout_agent(ListenerSrv, Winner) + end, + Winners), + + {'next_state', 'connect_req', State#state{connection_timer_ref='undefined' + ,agent_ring_timer_ref='undefined' + ,member_call_winners='undefined' + }}; connecting('cast', Event, State) -> handle_event(Event, connecting, State); -connecting({'call', From}, 'status', #state{member_call=Call - ,member_call_start=Start - ,connection_timer_ref=ConnRef - ,agent_ring_timer_ref=AgentRef - ,cdr_url=Url - ,recording_url=RecordingUrl - }=State) -> +connecting({call, From}, 'status', #state{member_call=Call + ,member_call_start=Start + ,connection_timer_ref=ConnRef + ,agent_ring_timer_ref=AgentRef + ,cdr_url=Url + ,recording_url=RecordingUrl + }=State) -> {'next_state', 'connecting', State ,{'reply', From, [{<<"state">>, <<"connecting">>} - ,{<<"call_id">>, kapps_call:call_id(Call)} - ,{<<"caller_id_name">>, kapps_call:caller_id_name(Call)} - ,{<<"caller_id_number">>, kapps_call:caller_id_name(Call)} - ,{<<"to">>, kapps_call:to_user(Call)} - ,{<<"from">>, kapps_call:from_user(Call)} - ,{<<"wait_left">>, elapsed(ConnRef)} - ,{<<"wait_time">>, elapsed(Start)} - ,{<<"agent_wait_left">>, elapsed(AgentRef)} - ,{<<"cdr_url">>, Url} - ,{<<"recording_url">>, RecordingUrl} - ]}}; -connecting({'call', From}, 'current_call', #state{member_call=Call - ,member_call_start=Start - ,connection_timer_ref=ConnRef - }=State) -> + ,{<<"call_id">>, kapps_call:call_id(Call)} + ,{<<"caller_id_name">>, kapps_call:caller_id_name(Call)} + ,{<<"caller_id_number">>, kapps_call:caller_id_name(Call)} + ,{<<"to">>, kapps_call:to_user(Call)} + ,{<<"from">>, kapps_call:from_user(Call)} + ,{<<"wait_left">>, elapsed(ConnRef)} + ,{<<"wait_time">>, elapsed(Start)} + ,{<<"agent_wait_left">>, elapsed(AgentRef)} + ,{<<"cdr_url">>, Url} + ,{<<"recording_url">>, RecordingUrl} + ]}}; + +connecting({call, From}, 'current_call', #state{member_call=Call + ,member_call_start=Start + ,connection_timer_ref=ConnRef + }=State) -> {'next_state', 'connecting', State - ,{'reply', From, current_call(Call, ConnRef, Start)} - }; + ,{'reply', From, current_call(Call, ConnRef, Start)}}; + connecting({'call', From}, Event, State) -> - handle_sync_event(Event, From, 'connecting', State); + handle_sync_event(Event, From, connecting, State); connecting('info', {'timeout', AgentRef, ?AGENT_RING_TIMEOUT_MESSAGE}, #state{agent_ring_timer_ref=AgentRef - ,member_call_winners=[Winner|[]] - ,listener_proc=ListenerSrv - }=State) -> + ,member_call_winners=Winners + ,listener_proc=ListenerSrv + }=State) -> lager:debug("timed out waiting for agent to pick up"), lager:debug("let's try another agent"), erlang:send(self(), {'timeout', 'undefined', ?COLLECT_RESP_MESSAGE}), - acdc_queue_listener:timeout_agent(ListenerSrv, Winner), - + lists:foreach(fun(Winner) -> + lager:debug("sending timeout agent to ~s(~s)", [kz_json:get_value(<<"Agent-ID">>, Winner) ,kz_json:get_value(<<"Process-ID">>, Winner) ]), + acdc_queue_listener:timeout_agent(ListenerSrv, Winner) + end, + Winners), {'next_state', 'connect_req', State#state{agent_ring_timer_ref='undefined' - ,member_call_winners=[] + ,member_call_winners='undefined' }}; -connecting('info', {'timeout', _AgentRef, ?AGENT_RING_TIMEOUT_MESSAGE}, #state{member_call_winners=[_Winner|Winners]}=State)-> - % TODO: we should detect who told timeout and remove him - % I remove the first (but this is not the right way) - {'next_state', 'connect_req', State#state{member_call_winners=Winners}}; + connecting('info', {'timeout', _OtherAgentRef, ?AGENT_RING_TIMEOUT_MESSAGE}, #state{agent_ring_timer_ref=_AgentRef}=State) -> lager:debug("unknown agent ref: ~p known: ~p", [_OtherAgentRef, _AgentRef]), {'next_state', 'connect_req', State}; + connecting('info', {'timeout', ConnRef, ?CONNECTION_TIMEOUT_MESSAGE}, #state{listener_proc=ListenerSrv - ,connection_timer_ref=ConnRef - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - ,member_call_winners=Winners - }=State) -> + ,connection_timer_ref=ConnRef + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=Call + ,member_call_winners=Winners + }=State) -> lager:debug("connection timeout occurred, bounce the caller out of the queue"), - lists:foreach(fun(Winner) -> maybe_timeout_winner(ListenerSrv, Winner) end, Winners), - CallId = kapps_call:call_id(Call), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_TIMEOUT), + lists:foreach(fun(Winner) -> + lager:debug("maybe sending timeout agent to ~s(~s)", [kz_json:get_value(<<"Agent-ID">>, Winner) ,kz_json:get_value(<<"Process-ID">>, Winner) ]), + maybe_timeout_winner(ListenerSrv, Winner) + end, + Winners), + CallId = kapps_call:call_id(Call), + _ = acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_TIMEOUT), webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - timeout">>), - - {'next_state', 'ready', clear_member_call(State), 'hibernate'}. + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; +connecting('info', Evt, State) -> + handle_info(Evt, 'connecting', State). %%------------------------------------------------------------------------------ -%% @doc +%% @private +%% @doc Whenever a gen_statem receives an event sent using +%% gen_statem:send_all_state_event/2, this function is called to handle +%% the event. +%% %% @end %%------------------------------------------------------------------------------ -spec handle_event(any(), atom(), state()) -> kz_types:handle_fsm_ret(state()). @@ -641,7 +752,11 @@ handle_event(_Event, StateName, State) -> {'next_state', StateName, State}. %%------------------------------------------------------------------------------ -%% @doc +%% @private +%% @doc Whenever a gen_statem receives an event sent using +%% gen_statem:sync_send_all_state_event/[2,3], this function is called +%% to handle the event. +%% %% @end %%------------------------------------------------------------------------------ -spec handle_sync_event(any(), From :: pid(), StateName :: atom(), state()) -> @@ -658,20 +773,31 @@ handle_sync_event(_Event, From, StateName, State) -> ,{'reply', From, Reply} }. +-spec handle_info(any(), atom(), state()) -> kz_types:handle_fsm_ret(state()). +handle_info({'member_call', CallJObj, Delivery}, 'ready', State) -> + ?MODULE:member_call(self(), CallJObj, Delivery), + {'next_state', 'ready', State}; +handle_info(_Info, StateName, State) -> + lager:debug("unhandled message in state ~s: ~p", [StateName, _Info]), + {'next_state', StateName, State}. + %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_statem' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_statem' terminates with +%% @private +%% @doc This function is called by a gen_statem when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_statem terminates with %% Reason. The return value is ignored. %% %% @end %%------------------------------------------------------------------------------ -spec terminate(any(), atom(), state()) -> 'ok'. terminate(_Reason, _StateName, _State) -> - lager:debug("acdc queue statem terminating: ~p", [_Reason]). + lager:debug("acdc queue fsm terminating: ~p", [_Reason]). %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), atom(), state(), any()) -> {'ok', atom(), state()}. @@ -733,30 +859,31 @@ clear_member_call(#state{connection_timer_ref=ConnRef ,connection_timer_ref='undefined' ,agent_ring_timer_ref='undefined' ,member_call_start='undefined' - ,member_call_winners=[] + ,member_call_winners='undefined' + ,callback_details='undefined' }. update_properties(QueueJObj, State) -> - State#state{name = kz_json:get_value(<<"name">>, QueueJObj) - ,connection_timeout = connection_timeout(kz_json:get_integer_value(<<"connection_timeout">>, QueueJObj)) - ,agent_ring_timeout = agent_ring_timeout(kz_json:get_integer_value(<<"agent_ring_timeout">>, QueueJObj)) - ,max_queue_size = kz_json:get_integer_value(<<"max_queue_size">>, QueueJObj) - ,ring_simultaneously = kz_json:get_value(<<"ring_simultaneously">>, QueueJObj) - ,enter_when_empty = kz_json:is_true(<<"enter_when_empty">>, QueueJObj, 'true') - ,agent_wrapup_time = kz_json:get_integer_value(<<"agent_wrapup_time">>, QueueJObj) - ,announce = kz_json:get_value(<<"announce">>, QueueJObj) - ,caller_exit_key = kz_json:get_value(<<"caller_exit_key">>, QueueJObj, <<"#">>) - ,record_caller = kz_json:is_true(<<"record_caller">>, QueueJObj, 'false') - ,recording_url = kz_json:get_ne_value(<<"call_recording_url">>, QueueJObj) - ,cdr_url = kz_json:get_ne_value(<<"cdr_url">>, QueueJObj) - ,notifications = kz_json:get_value(<<"notifications">>, QueueJObj) - - %% Changing queue strategy currently isn't feasible; definitely a TODO - %%,strategy = get_strategy(kz_json:get_value(<<"strategy">>, QueueJObj)) - }. - --spec current_call('undefined' | kapps_call:call(), kz_term:api_reference() | timeout(), kz_time:start_time()) -> - kz_term:api_object(). + State#state{ + name = kz_json:get_value(<<"name">>, QueueJObj) + ,connection_timeout = connection_timeout(kz_json:get_integer_value(<<"connection_timeout">>, QueueJObj)) + ,agent_ring_timeout = agent_ring_timeout(kz_json:get_integer_value(<<"agent_ring_timeout">>, QueueJObj)) + ,max_queue_size = kz_json:get_integer_value(<<"max_queue_size">>, QueueJObj) + ,ring_simultaneously = kz_json:get_value(<<"ring_simultaneously">>, QueueJObj) + ,enter_when_empty = kz_json:is_true(<<"enter_when_empty">>, QueueJObj, 'true') + ,agent_wrapup_time = kz_json:get_integer_value(<<"agent_wrapup_time">>, QueueJObj) + ,announce = kz_json:get_value(<<"announce">>, QueueJObj) + ,caller_exit_key = kz_json:get_value(<<"caller_exit_key">>, QueueJObj, <<"#">>) + ,record_caller = kz_json:is_true(<<"record_caller">>, QueueJObj, 'false') + ,recording_url = kz_json:get_ne_value(<<"call_recording_url">>, QueueJObj) + ,cdr_url = kz_json:get_ne_value(<<"cdr_url">>, QueueJObj) + ,notifications = kz_json:get_value(<<"notifications">>, QueueJObj) + + %% Changing queue strategy currently isn't feasible; definitely a TODO + %%,strategy = get_strategy(kz_json:get_value(<<"strategy">>, QueueJObj)) + }. + +-spec current_call('undefined' | kapps_call:call(), kz_term:api_reference() | kz_term:timeout(), kz_term:timeout()) -> kz_term:api_object(). current_call('undefined', _, _) -> 'undefined'; current_call(Call, QueueTimeLeft, Start) -> kz_json:from_list([{<<"call_id">>, kapps_call:call_id(Call)} @@ -768,7 +895,7 @@ current_call(Call, QueueTimeLeft, Start) -> ,{<<"wait_time">>, elapsed(Start)} ]). --spec elapsed(kz_term:api_reference() | kz_time:start_time()) -> kz_term:api_integer(). +-spec elapsed(kz_term:api_reference() | kz_term:timeout() | integer()) -> kz_term:api_integer(). elapsed('undefined') -> 'undefined'; elapsed(Ref) when is_reference(Ref) -> case erlang:read_timer(Ref) of @@ -778,6 +905,7 @@ elapsed(Ref) when is_reference(Ref) -> elapsed(Time) -> kz_time:elapsed_s(Time). %%------------------------------------------------------------------------------ +%% @private %% @doc If some agents are busy, the manager will tell us to delay our %% connect reqs %% @@ -805,25 +933,27 @@ maybe_delay_connect_req(Call, CallJObj, Delivery, #state{listener_proc=ListenerS {'next_state', 'connect_req', State#state{collect_ref=start_collect_timer() ,member_call=Call - ,member_call_start=kz_time:start_time() + ,member_call_start=kz_time:current_tstamp() ,connection_timer_ref=start_connection_timer(ConnTimeout) }}; 'false' -> lager:debug("connect_req delayed (not up next)"), - _ = timer:apply_after(1000, 'gen_statem', 'cast', [self(), {'member_call', CallJObj, Delivery}]), + erlang:send_after(1000, self(), {'member_call', CallJObj, Delivery}), {'next_state', 'ready', State} end. %%------------------------------------------------------------------------------ +%% @private %% @doc Abort a queue call between connect_reqs if agents have left the %% building %% %% @end %%------------------------------------------------------------------------------ --spec maybe_connect_re_req(pid(), pid(), state()) -> kz_types:handle_fsm_ret(state()). +-spec maybe_connect_re_req(pid(), pid(), state()) -> kz_term:handle_fsm_ret(state()). maybe_connect_re_req(MgrSrv, ListenerSrv, #state{account_id=AccountId ,queue_id=QueueId ,member_call=Call + ,callback_details='undefined' }=State) -> case acdc_queue_manager:are_agents_available(MgrSrv) of 'true' -> @@ -832,9 +962,12 @@ maybe_connect_re_req(MgrSrv, ListenerSrv, #state{account_id=AccountId lager:debug("all agents have left the queue, failing call"), webseq:note(?WSD_ID, self(), 'right', <<"all agents have left the queue, failing call">>), acdc_queue_listener:exit_member_call_empty(ListenerSrv), - acdc_stats:call_abandoned(AccountId, QueueId, kapps_call:call_id(Call), ?ABANDON_EMPTY), + _ = acdc_stats:call_abandoned(AccountId, QueueId, kapps_call:call_id(Call), ?ABANDON_EMPTY), {'next_state', 'ready', clear_member_call(State), 'hibernate'} - end. + end; +maybe_connect_re_req(MgrSrv, ListenerSrv, State) -> + %% Don't cancel calls when they are a callback - save them for a long time + maybe_delay_connect_re_req(MgrSrv, ListenerSrv, State). -spec maybe_delay_connect_re_req(pid(), pid(), state()) -> {'next_state', 'connect_req', state()}. @@ -854,7 +987,8 @@ maybe_delay_connect_re_req(MgrSrv, ListenerSrv, #state{member_call=Call}=State) -spec accept_is_for_call(kz_json:object(), kapps_call:call()) -> boolean(). accept_is_for_call(AcceptJObj, Call) -> - kz_json:get_value(<<"Call-ID">>, AcceptJObj) =:= kapps_call:call_id(Call). + (kz_json:get_value(<<"Call-ID">>, AcceptJObj) =:= kapps_call:call_id(Call)) or + (kz_json:get_value(<<"Old-Call-ID">>, AcceptJObj) =:= kapps_call:call_id(Call)). -spec update_agent(kz_json:object(), kz_json:objects()) -> kz_json:object(). update_agent(Agent, Winners) -> @@ -874,16 +1008,18 @@ handle_agent_responses(#state{collect_ref=Ref 'true' -> lager:debug("queue mgr said to ignore this call: ~s, not connecting to agents", [kapps_call:call_id(Call)]), acdc_queue_listener:finish_member_call(ListenerSrv), - {'ready', State}; + {'ready', clear_member_call(State)}; 'false' -> lager:debug("done waiting for agents to respond, picking a winner"), - maybe_pick_winner(State) + CallbackDetails = acdc_queue_manager:callback_details(MgrSrv, kapps_call:call_id(Call)), + maybe_pick_winner(State#state{callback_details=CallbackDetails}) end. -spec maybe_pick_winner(state()) -> {atom(), state()}. maybe_pick_winner(#state{connect_resps=CRs ,listener_proc=ListenerSrv ,manager_proc=Mgr + ,member_call=Call ,agent_ring_timeout=RingTimeout ,agent_wrapup_time=AgentWrapup ,caller_exit_key=CallerExitKey @@ -892,7 +1028,7 @@ maybe_pick_winner(#state{connect_resps=CRs ,recording_url=RecordUrl ,notifications=Notifications }=State) -> - case acdc_queue_manager:pick_winner(Mgr, CRs) of + case acdc_queue_manager:pick_winner(Mgr, Call, CRs) of {Winners, Rest} -> QueueOpts = [{<<"Ring-Timeout">>, RingTimeout} ,{<<"Wrapup-Timeout">>, AgentWrapup} @@ -901,6 +1037,7 @@ maybe_pick_winner(#state{connect_resps=CRs ,{<<"Record-Caller">>, ShouldRecord} ,{<<"Recording-URL">>, RecordUrl} ,{<<"Notifications">>, Notifications} + ,{<<"Callback-Details">>, callback_details(State)} ], ConnectWins = lists:foldl(fun(Winner, Wins) -> @@ -919,10 +1056,9 @@ maybe_pick_winner(#state{connect_resps=CRs ,member_call_winners=Winners }}; 'undefined' -> - lager:debug("no more responses to choose from"), - - acdc_queue_listener:cancel_member_call(ListenerSrv), - {'ready', clear_member_call(State)} + lager:info("no response from the winner"), + {_, NextState, State1} = maybe_connect_re_req(Mgr, ListenerSrv, State#state{connect_resps=[]}), + {NextState, State1} end. -spec have_agents_responded(kz_json:objects(), kz_term:ne_binaries()) -> boolean(). @@ -932,3 +1068,10 @@ have_agents_responded(Resps, Agents) -> -spec filter_agents(kz_json:object(), kz_term:ne_binaries()) -> kz_term:ne_binaries(). filter_agents(Resp, AgentsAcc) -> lists:delete(kz_json:get_value(<<"Agent-ID">>, Resp), AgentsAcc). + +-spec callback_details(state()) -> kz_term:api_object(). +callback_details(#state{callback_details='undefined'}) -> 'undefined'; +callback_details(#state{callback_details={Number, CIDPrepend}}) -> + kz_json:from_list([{<<"Callback-Number">>, Number} + ,{<<"Prepend-CID">>, CIDPrepend} + ]). diff --git a/applications/acdc/src/acdc_queue_handler.erl b/applications/acdc/src/acdc_queue_handler.erl index ccb33b3ddd5..08a59f72d8b 100644 --- a/applications/acdc/src/acdc_queue_handler.erl +++ b/applications/acdc/src/acdc_queue_handler.erl @@ -1,8 +1,7 @@ %%%----------------------------------------------------------------------------- -%%% @copyright (C) 2012-2020, 2600Hz +%%% @copyright (C) 2012-2020, 2600Hz INC %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -13,9 +12,12 @@ -export([handle_call_event/2 ,handle_member_call/3 + ,handle_member_call_cancel/2 ,handle_member_resp/2 ,handle_member_accepted/2 ,handle_member_retry/2 + ,handle_member_callback_reg/2 + ,handle_member_callback_accepted/2 ,handle_config_change/2 ,handle_presence_probe/2 ]). @@ -57,6 +59,11 @@ handle_member_call(JObj, Props, Delivery) -> acdc_queue_fsm:member_call(props:get_value('fsm_pid', Props), JObj, Delivery), gen_listener:cast(props:get_value('server', Props), {'delivery', Delivery}). +-spec handle_member_call_cancel(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_member_call_cancel(JObj, Props) -> + 'true' = kapi_acdc_queue:member_call_cancel_v(JObj), + acdc_queue_fsm:member_call_cancel(props:get_value('fsm_pid', Props), JObj). + -spec handle_member_resp(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_member_resp(JObj, Props) -> 'true' = kapi_acdc_queue:member_connect_resp_v(JObj), @@ -72,6 +79,18 @@ handle_member_retry(JObj, Props) -> 'true' = kapi_acdc_queue:member_connect_retry_v(JObj), acdc_queue_fsm:member_connect_retry(props:get_value('fsm_pid', Props), JObj). +-spec handle_member_callback_reg(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_member_callback_reg(JObj, Props) -> + Srv = props:get_value('server', Props), + CallId = kz_json:get_value(<<"Call-ID">>, JObj), + acdc_util:unbind_from_call_events(CallId, Srv), + acdc_queue_fsm:register_callback(props:get_value('fsm_pid', Props), JObj). + +-spec handle_member_callback_accepted(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_member_callback_accepted(JObj, Props) -> + 'true' = kapi_acdc_queue:member_callback_accepted_v(JObj), + acdc_queue_fsm:member_callback_accepted(props:get_value('fsm_pid', Props), JObj). + -spec handle_config_change(kz_json:object(), kz_term:proplist()) -> any(). handle_config_change(JObj, _Props) -> 'true' = kapi_conf:doc_update_v(JObj), @@ -110,48 +129,48 @@ handle_queue_change(_, AccountId, QueueId, ?DOC_DELETED) -> acdc_queue_sup:stop(P) end. -%%------------------------------------------------------------------------------ -%% @doc Handle presence probes for queues by filtering out those that cannot be -%% queue probes (do not look like 32 character hex binaries) and those that do -%% not correspond to an existing account ID/queue ID pair -%% -%% @end -%%------------------------------------------------------------------------------ -spec handle_presence_probe(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_presence_probe(JObj, _Props) -> 'true' = kapi_presence:probe_v(JObj), - Username = kz_json:get_ne_binary_value(<<"Username">>, JObj), - case potentially_queue_presence_probe(Username) of - 'true' -> - Realm = kz_json:get_ne_binary_value(<<"Realm">>, JObj), - maybe_respond_to_presence_probe( - acdc_presence_realm_lookup:lookup(Realm) - ,Username - ); - 'false' -> 'ok' + Realm = kz_json:get_value(<<"Realm">>, JObj), + case kapps_util:get_account_by_realm(Realm) of + {'ok', AcctDb} -> + AccountId = kzs_util:format_account_id(AcctDb), + maybe_respond_to_presence_probe(JObj, AccountId); + _ -> 'ok' end. --spec potentially_queue_presence_probe(kz_term:api_binary()) -> boolean(). -potentially_queue_presence_probe(<<_:32/binary>>) -> 'true'; -potentially_queue_presence_probe(_) -> 'false'. - --spec maybe_respond_to_presence_probe(kz_term:ne_binary() | 'not_found', kz_term:ne_binary()) -> 'ok'. -maybe_respond_to_presence_probe('not_found', _) -> 'ok'; -maybe_respond_to_presence_probe(AcctId, QueueId) -> - case acdc_queues_sup:find_queue_supervisor(AcctId, QueueId) of +maybe_respond_to_presence_probe(JObj, AccountId) -> + case kz_json:get_value(<<"Username">>, JObj) of 'undefined' -> 'ok'; - QueueSup -> - Manager = acdc_queue_sup:manager(QueueSup), - update_probe(Manager, AcctId, QueueId) + QueueId -> + update_probe(JObj + ,acdc_queues_sup:find_queue_supervisor(AccountId, QueueId) + ,AccountId, QueueId + ) end. --spec update_probe(kz_term:api_pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -update_probe(Manager, AcctId, QueueId) -> - case acdc_queue_manager:queue_size(Manager) of +update_probe(_JObj, 'undefined', _, _) -> 'ok'; +update_probe(JObj, _Sup, AccountId, QueueId) -> + case kapi_acdc_queue:queue_size(AccountId, QueueId) of 0 -> lager:debug("no calls in queue, ignore!"), - acdc_util:presence_update(AcctId, QueueId, ?PRESENCE_GREEN); - N -> + send_probe(JObj, ?PRESENCE_GREEN); + N when is_integer(N), N > 0 -> lager:debug("~b calls in queue, redify!", [N]), - acdc_util:presence_update(AcctId, QueueId, ?PRESENCE_RED_FLASH) + send_probe(JObj, ?PRESENCE_RED_FLASH); + _E -> + lager:debug("unhandled size return: ~p", [_E]) end. + +send_probe(JObj, State) -> + To = <<(kz_json:get_value(<<"Username">>, JObj))/binary + ,"@" + ,(kz_json:get_value(<<"Realm">>, JObj))/binary>>, + PresenceUpdate = + [{<<"State">>, State} + ,{<<"Presence-ID">>, To} + ,{<<"Call-ID">>, kz_term:to_hex_binary(crypto:hash(md5, To))} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_presence:publish_update(PresenceUpdate). diff --git a/applications/acdc/src/acdc_queue_listener.erl b/applications/acdc/src/acdc_queue_listener.erl index bf03815d30d..c077a6e183b 100644 --- a/applications/acdc/src/acdc_queue_listener.erl +++ b/applications/acdc/src/acdc_queue_listener.erl @@ -1,18 +1,17 @@ %%%----------------------------------------------------------------------------- -%%% @copyright (C) 2012-2020, 2600Hz +%%% @copyright (C) 2012-2020, 2600Hz INC %%% @doc The queue process manages two queues %%% 1. a private one that Agents will send member_connect_* messages %%% and such %%% 2. a shared queue that member_call messages will be published to, %%% each consumer will be round-robined. The consumers aren't going -%%% to auto-ack the payloads, deferring that until the connection is +%%% to auto-ack the payloads, defering that until the connection is %%% accepted by the agent. %%% %%% %%% @author James Aimonetti -%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -24,7 +23,7 @@ %% API -export([start_link/4 - ,accept_member_calls/1 + ,member_call/3 ,member_connect_req/4 ,member_connect_re_req/1 ,member_connect_win/3 @@ -60,19 +59,21 @@ -record(state, {queue_id :: kz_term:ne_binary() ,account_id :: kz_term:ne_binary() + %% PIDs of the gang + ,worker_sup :: pid() | undefined ,mgr_pid :: pid() ,fsm_pid :: kz_term:api_pid() ,shared_pid :: kz_term:api_pid() %% AMQP-related ,my_id :: kz_term:ne_binary() - ,my_q :: kz_term:api_ne_binary() - ,member_call_queue :: kz_term:api_ne_binary() + ,my_q :: api_kz_term:ne_binary() + ,member_call_queue :: api_kz_term:ne_binary() %% While processing a call - ,call :: kapps_call:call() | 'undefined' - ,agent_id :: kz_term:api_ne_binary() - ,delivery :: gen_listener:basic_deliver() | 'undefined' + ,call :: kapps_call:call() + ,agent_id :: api_kz_term:ne_binary() + ,delivery :: gen_listener:basic_deliver() }). -type state() :: #state{}. @@ -83,6 +84,9 @@ ,{{'acdc_queue_handler', 'handle_call_event'} ,[{<<"error">>, <<"*">>}] } + ,{{'acdc_queue_handler', 'handle_member_call_cancel'} + ,[{<<"member">>, <<"call_cancel">>}] + } ,{{'acdc_queue_handler', 'handle_member_resp'} ,[{<<"member">>, <<"connect_resp">>}] } @@ -95,6 +99,12 @@ ,{{'acdc_queue_handler', 'handle_sync_req'} ,[{<<"queue">>, <<"sync_req">>}] } + ,{{'acdc_queue_handler', 'handle_member_callback_reg'} + ,[{<<"member">>, <<"callback_reg">>}] + } + ,{{'acdc_queue_handler', 'handle_member_callback_accepted'} + ,[{<<"member">>, <<"callback_accepted">>}] + } ]). %%%============================================================================= @@ -102,10 +112,10 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the server. +%% @doc Starts the server %% @end %%------------------------------------------------------------------------------ --spec start_link(pid(), pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:startlink_ret(). +-spec start_link(pid(), pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:startlink_ret(). start_link(WorkerSup, MgrPid, AccountId, QueueId) -> gen_listener:start_link(?SERVER ,[{'bindings', [{'acdc_queue', [{'restrict_to', ['sync_req']} @@ -119,14 +129,15 @@ start_link(WorkerSup, MgrPid, AccountId, QueueId) -> ,[WorkerSup, MgrPid, AccountId, QueueId] ). --spec accept_member_calls(pid()) -> 'ok'. -accept_member_calls(Srv) -> - gen_listener:cast(Srv, {'accept_member_calls'}). +-spec member_call(pid(), kz_json:object(), any()) -> 'ok'. +member_call(Srv, MemberCallJObj, Delivery) -> + gen_listener:cast(Srv, {'member_call', MemberCallJObj, Delivery}). -spec member_connect_req(pid(), kz_json:object(), any(), kz_term:api_binary()) -> 'ok'. member_connect_req(Srv, MemberCallJObj, Delivery, Url) -> gen_listener:cast(Srv, {'member_connect_req', MemberCallJObj, Delivery, Url}). + -spec member_connect_re_req(pid()) -> 'ok'. member_connect_re_req(Srv) -> gen_listener:cast(Srv, {'member_connect_re_req'}). @@ -205,7 +216,8 @@ delivery(Srv) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Initializes the listener. +%% @private +%% @doc Initializes the listener %% @end %%------------------------------------------------------------------------------ -spec init(list()) -> {'ok', state()}. @@ -219,11 +231,14 @@ init([WorkerSup, MgrPid, AccountId, QueueId]) -> ,mgr_pid = MgrPid }}. + %%------------------------------------------------------------------------------ -%% @doc Handling call messages. +%% @private +%% @doc Handling call messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_term:handle_call_ret_state(state()). handle_call('delivery', _From, #state{delivery=D}=State) -> {'reply', D, State}; handle_call('config', _From, #state{account_id=AccountId @@ -235,7 +250,9 @@ handle_call(_Request, _From, State) -> {'reply', {'error', 'unhandled_call'}, State}. %%------------------------------------------------------------------------------ -%% @doc Handling cast messages. +%% @private +%% @doc Handling cast messages +%% %% @end %%------------------------------------------------------------------------------ -spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). @@ -247,28 +264,70 @@ handle_cast({'get_friends', WorkerSup}, State) -> {'noreply', State#state{fsm_pid=FSMPid ,shared_pid=SharedPid }}; -handle_cast({'gen_listener', {'created_queue', Q}}, State) -> + +handle_cast({'gen_listener', {'created_queue', Q}}, #state{my_q='undefined'}=State) -> {'noreply', State#state{my_q=Q}, 'hibernate'}; +handle_cast({'member_call', MemberCallJObj, Delivery}, #state{queue_id=QueueId + ,account_id=AccountId + }=State) -> + Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, MemberCallJObj)), + CallId = kapps_call:call_id(Call), + + kz_log:put_callid(CallId), + + acdc_util:bind_to_call_events(Call), + lager:debug("bound to call events for ~s", [CallId]), + + %% Be ready in case a cancel comes in while queue_listener is handling call + gen_listener:add_binding(self(), 'acdc_queue', [{'restrict_to', ['member_call_result']} + ,{'account_id', AccountId} + ,{'queue_id', QueueId} + ,{'callid', CallId} + ]), + + {'noreply', State#state{call=Call + ,delivery=Delivery + ,member_call_queue=kz_json:get_value(<<"Server-ID">>, MemberCallJObj) + }}; + handle_cast({'member_connect_req', MemberCallJObj, Delivery, _Url} ,#state{my_q=MyQ ,my_id=MyId ,account_id=AccountId + ,mgr_pid=MgrPid ,queue_id=QueueId }=State) -> Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, MemberCallJObj)), - - kz_log:put_callid(kapps_call:call_id(Call)), - - acdc_util:bind_to_call_events(Call), - lager:debug("bound to call events for ~s", [kapps_call:call_id(Call)]), - send_member_connect_req(kapps_call:call_id(Call), AccountId, QueueId, MyQ, MyId), + CallId = kapps_call:call_id(Call), + + kz_log:put_callid(CallId), + + %% If a callback is registered before queue_fsm gets the call, + %% do not bind to events (as we don't care about hangups) + case acdc_queue_manager:callback_details(MgrPid, CallId) of + 'undefined' -> + acdc_util:bind_to_call_events(Call), + lager:debug("bound to call events for ~s", [CallId]); + _ -> + 'ok' + end, + send_member_connect_req(CallId, AccountId, QueueId, MyQ, MyId), + + %% Be ready in case a callback or cancel comes in while queue_listener is handling call + gen_listener:add_binding(self(), 'acdc_queue', [{'restrict_to', ['member_callback_reg', 'member_call_result']} + ,{'account_id', AccountId} + ,{'queue_id', QueueId} + ,{'callid', CallId} + ]), {'noreply', State#state{call=Call ,delivery=Delivery ,member_call_queue=kz_json:get_value(<<"Server-ID">>, MemberCallJObj) } ,'hibernate'}; + + handle_cast({'member_connect_re_req'}, #state{my_q=MyQ ,my_id=MyId ,account_id=AccountId @@ -283,9 +342,12 @@ handle_cast({'member_connect_win', RespJObj, QueueOpts}, #state{my_q=MyQ ,queue_id=QueueId }=State) -> lager:debug("agent process won the call, sending the win"), + Call1 = apply_callback_details(Call, QueueOpts), + send_member_connect_win(RespJObj, Call1, QueueId, MyQ, MyId, QueueOpts), - send_member_connect_win(RespJObj, Call, QueueId, MyQ, MyId, QueueOpts), - {'noreply', State#state{agent_id=kz_json:get_value(<<"Agent-ID">>, RespJObj)}, 'hibernate'}; + {'noreply', State#state{call=Call1 + ,agent_id=kz_json:get_value(<<"Agent-ID">>, RespJObj) + }, 'hibernate'}; handle_cast({'member_connect_satisfied', RespJObj, QueueOpts}, #state{my_q=MyQ ,my_id=MyId ,call=Call @@ -294,6 +356,7 @@ handle_cast({'member_connect_satisfied', RespJObj, QueueOpts}, #state{my_q=MyQ lager:debug("agent process satisfied the connect, sending the satisfied"), send_member_connect_satisfied(RespJObj, Call, QueueId, MyQ, MyId, QueueOpts), {'noreply', State, 'hibernate'}; + handle_cast({'timeout_agent', RespJObj}, #state{queue_id=QueueId ,call=Call }=State) -> @@ -445,16 +508,20 @@ handle_cast(_Msg, State) -> {'noreply', State}. %%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. +%% @private +%% @doc Handling all non call/cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +-spec handle_info(any(), state()) -> kz_term:handle_info_ret_state(state()). handle_info(_Info, State) -> lager:debug("unhandled message: ~p", [_Info]), {'noreply', State}. %%------------------------------------------------------------------------------ +%% @private %% @doc Handling all messages from the message bus +%% %% @end %%------------------------------------------------------------------------------ -spec handle_event(kz_json:object(), state()) -> gen_listener:handle_event_return(). @@ -462,9 +529,10 @@ handle_event(_JObj, #state{fsm_pid=FSM}) -> {'reply', [{'fsm_pid', FSM}]}. %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_listener' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_listener' terminates +%% @private +%% @doc This function is called by a gen_listener when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_listener terminates %% with Reason. The return value is ignored. %% %% @end @@ -474,7 +542,9 @@ terminate(_Reason, _State) -> lager:debug("ACDc queue terminating: ~p", [_Reason]). %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), state(), any()) -> {'ok', state()}. @@ -489,8 +559,9 @@ code_change(_OldVsn, State, _Extra) -> %% @doc %% @end %%------------------------------------------------------------------------------ --spec maybe_timeout_agent(kz_term:api_object(), kz_term:ne_binary(), kapps_call:call(), kz_json:object()) -> 'ok'. +-spec maybe_timeout_agent(kz_term:api_object(), kz_term:ne_binary(), kapps_call:call(), kz_term:api_object()) -> 'ok'. maybe_timeout_agent('undefined', _QueueId, _Call, _JObj) -> 'ok'; +maybe_timeout_agent(_AgentId, _QueueId, _Call, 'undefined') -> 'ok'; maybe_timeout_agent(_AgentId, QueueId, Call, JObj) -> lager:debug("timing out winning agent because they should not be able to pick up after the queue timeout"), send_agent_timeout(JObj, Call, QueueId). @@ -510,34 +581,37 @@ send_member_connect_req(CallId, AccountId, QueueId, MyQ, MyId) -> -spec send_member_connect_win(kz_json:object(), kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. send_member_connect_win(RespJObj, Call, QueueId, MyQ, MyId, QueueOpts) -> CallJSON = kapps_call:to_json(Call), - Q = kz_json:get_value(<<"Server-ID">>, RespJObj), Win = props:filter_undefined( [{<<"Call">>, CallJSON} ,{<<"Process-ID">>, MyId} ,{<<"Agent-Process-IDs">>, kz_json:get_value(<<"Agent-Process-IDs">>, RespJObj)} ,{<<"Queue-ID">>, QueueId} + ,{<<"Agent-ID">>, kz_json:get_value(<<"Agent-ID">>, RespJObj)} | QueueOpts ++ kz_api:default_headers(MyQ, ?APP_NAME, ?APP_VERSION) ]), - publish(Q, Win, fun kapi_acdc_queue:publish_member_connect_win/2). + publish(Win, fun kapi_acdc_queue:publish_member_connect_win/1). -spec send_member_connect_satisfied(kz_json:object(), kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. send_member_connect_satisfied(RespJObj, Call, QueueId, MyQ, MyId, QueueOpts) -> CallJSON = kapps_call:to_json(Call), - Q = kz_json:get_value(<<"Server-ID">>, RespJObj), + %% Q = kz_json:get_value(<<"Server-ID">>, RespJObj), Satisfied = props:filter_undefined( [{<<"Call">>, CallJSON} ,{<<"Process-ID">>, MyId} ,{<<"Agent-Process-IDs">>, kz_json:get_list_value(<<"Agent-Process-IDs">>, RespJObj)} ,{<<"Queue-ID">>, QueueId} + ,{<<"Agent-ID">>, kz_json:get_value(<<"Agent-ID">>, RespJObj)} + ,{<<"Accept-Agent-ID">>, kz_json:get_value(<<"Accept-Agent-ID">>, RespJObj)} | QueueOpts ++ kz_api:default_headers(MyQ, ?APP_NAME, ?APP_VERSION) ]), - publish(Q, Satisfied, fun kapi_acdc_queue:publish_member_connect_satisfied/2). + %% publish(Q, Satisfied, fun kapi_acdc_queue:publish_member_connect_satisfied/2). + publish(Satisfied, fun kapi_acdc_queue:publish_member_connect_satisfied/1). -spec send_agent_timeout(kz_json:object(), kapps_call:call(), kz_term:ne_binary()) -> 'ok'. send_agent_timeout(RespJObj, Call, QueueId) -> Prop = [{<<"Queue-ID">>, QueueId} ,{<<"Call-ID">>, kapps_call:call_id(Call)} - ,{<<"Agent-Process-IDs">>, kz_json:get_value(<<"Agent-Process-IDs">>, RespJObj)} + ,{<<"Agent-Process-ID">>, kz_json:get_value(<<"Agent-Process-ID">>, RespJObj)} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], publish(kz_json:get_value(<<"Server-ID">>, RespJObj), Prop @@ -601,6 +675,16 @@ publish_sync_resp(Strategy, StrategyState, ReqJObj, Id) -> ]), publish(kz_json:get_value(<<"Server-ID">>, ReqJObj), Resp, fun kapi_acdc_queue:publish_sync_resp/2). +-spec apply_callback_details(kapps_call:call(), kz_term:proplist()) -> + kapps_call:call(). +apply_callback_details(Call, Props) -> + case props:get_value(<<"Callback-Details">>, Props) of + 'undefined' -> Call; + CallbackDetails -> + CIDPrepend = kz_json:get_value(<<"Prepend-CID">>, CallbackDetails), + kapps_call:kvs_store('prepend_cid_name', CIDPrepend, Call) + end. + -spec maybe_nack(kapps_call:call(), gen_listener:basic_deliver(), pid()) -> boolean(). maybe_nack(Call, Delivery, SharedPid) -> case is_call_alive(Call) of @@ -630,11 +714,23 @@ is_call_alive(Call) -> end. -spec clear_call_state(state()) -> state(). -clear_call_state(#state{account_id=AccountId +clear_call_state(#state{call=Call + ,account_id=AccountId ,queue_id=QueueId }=State) -> _ = acdc_util:queue_presence_update(AccountId, QueueId), + case Call of + 'undefined' -> 'ok'; + _ -> + CallId = kapps_call:call_id(Call), + gen_listener:rm_binding(self(), 'acdc_queue', [{'restrict_to', ['member_callback_reg', 'member_call_result']} + ,{'account_id', AccountId} + ,{'queue_id', QueueId} + ,{'callid', CallId} + ]) + end, + kz_log:put_callid(QueueId), State#state{call='undefined' ,member_call_queue='undefined' diff --git a/applications/acdc/src/acdc_queue_manager.erl b/applications/acdc/src/acdc_queue_manager.erl index 25e0c494008..b0061bb1110 100644 --- a/applications/acdc/src/acdc_queue_manager.erl +++ b/applications/acdc/src/acdc_queue_manager.erl @@ -6,9 +6,8 @@ %%% collecting stats from queues %%% and more!!! %%% -%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -18,27 +17,33 @@ -module(acdc_queue_manager). -behaviour(gen_listener). +-define(CB_AGENTS_LIST, <<"queues/agents_listing">>). + %% API -export([start_link/2, start_link/3 ,handle_member_call/2 ,handle_member_call_success/2 ,handle_member_call_cancel/2 ,handle_agent_change/2 + ,handle_agents_available_req/2 ,handle_queue_member_add/2 ,handle_queue_member_remove/2 + ,handle_member_callback_reg/2 ,are_agents_available/1 ,handle_config_change/2 - ,queue_size/1 ,should_ignore_member_call/3, should_ignore_member_call/4 ,up_next/2 ,config/1 ,status/1 + ,calls/1 ,current_agents/1 ,refresh/2 + ,callback_details/2 ]). + %% FSM helpers --export([pick_winner/2]). +-export([pick_winner/3]). %% gen_server callbacks -export([init/1 @@ -51,8 +56,9 @@ ]). -ifdef(TEST). --export([ss_size/2 - ,update_strategy_with_agent/5 +-export([reseed_sbrrss_maps/3 + ,ss_size/3 + ,update_strategy_with_agent/6 ]). -endif. @@ -66,14 +72,19 @@ ,{'id', Q} ,'federate' ]} - ,{'acdc_queue', [{'restrict_to', ['stats_req', 'agent_change' - ,'member_addremove', 'member_call_result' + ,{'acdc_queue', [{'restrict_to', ['stats_req', 'agent_change', 'agents_availability' + ,'member_addremove', 'member_call_result', 'member_callback_reg' ]} ,{'account_id', A} ,{'queue_id', Q} ]} ,{'presence', [{'restrict_to', ['probe']}]} + ,{'acdc_stats', [{'restrict_to', ['status_stat']} + ,{'account_id', A} + ]} ]). +-define(AGENT_BINDINGS(AccountId, AgentId), [ + ]). -define(RESPONDERS, [{{'acdc_queue_handler', 'handle_config_change'} ,[{<<"configuration">>, <<"*">>}] @@ -96,12 +107,18 @@ ,{{?MODULE, 'handle_agent_change'} ,[{<<"queue">>, <<"agent_change">>}] } + ,{{?MODULE, 'handle_agents_available_req'} + ,[{<<"queue">>, <<"agents_available_req">>}] + } ,{{?MODULE, 'handle_queue_member_add'} ,[{<<"queue">>, <<"member_add">>}] } ,{{?MODULE, 'handle_queue_member_remove'} ,[{<<"queue">>, <<"member_remove">>}] } + ,{{?MODULE, 'handle_member_callback_reg'} + ,[{<<"member">>, <<"callback_reg">>}] + } ]). -define(SECONDARY_BINDINGS(AccountId, QueueId) @@ -121,10 +138,10 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the server. +%% @doc Starts the server %% @end %%------------------------------------------------------------------------------ --spec start_link(pid(), kz_json:object()) -> kz_types:startlink_ret(). +-spec start_link(pid(), kz_json:object()) -> kz_term:startlink_ret(). start_link(Super, QueueJObj) -> AccountId = kz_doc:account_id(QueueJObj), QueueId = kz_doc:id(QueueJObj), @@ -136,7 +153,7 @@ start_link(Super, QueueJObj) -> ,[Super, QueueJObj] ). --spec start_link(pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:startlink_ret(). +-spec start_link(pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:startlink_ret(). start_link(Super, AccountId, QueueId) -> gen_listener:start_link(?SERVER ,[{'bindings', ?BINDINGS(AccountId, QueueId)} @@ -162,10 +179,10 @@ handle_member_call(JObj, Props) -> ,{'reject_member_call', Call, JObj} ); 'true' -> - start_queue_call(JObj, Props, Call) + start_queue_call(JObj, Props, Call, kz_json:is_true(<<"Enter-As-Callback">>, JObj)) end. --spec are_agents_available(kz_types:server_ref()) -> boolean(). +-spec are_agents_available(kz_term:server_ref()) -> boolean(). are_agents_available(Srv) -> are_agents_available(Srv, gen_listener:call(Srv, 'enter_when_empty')). @@ -173,37 +190,51 @@ are_agents_available(Srv, EnterWhenEmpty) -> agents_available(Srv) > 0 orelse EnterWhenEmpty. -start_queue_call(JObj, Props, Call) -> +start_queue_call(JObj, Props, Call, 'false') -> _ = kapps_call:put_callid(Call), QueueId = kz_json:get_value(<<"Queue-ID">>, JObj), + Call1 = kapps_call:set_custom_channel_var(<<"Queue-ID">>, QueueId, Call), + lager:info("member call for queue ~s recv", [QueueId]), lager:debug("answering call"), - kapps_call_command:answer_now(Call), + kapps_call_command:answer_now(Call1), case kz_media_util:media_path(props:get_value('moh', Props) - ,kapps_call:account_id(Call) + ,kapps_call:account_id(Call1) ) of 'undefined' -> lager:debug("using default moh"), - kapps_call_command:hold(Call); + kapps_call_command:hold(Call1); MOH -> lager:debug("using MOH ~s (~p)", [MOH, Props]), - kapps_call_command:hold(MOH, Call) + kapps_call_command:hold(MOH, Call1) end, - JObj2 = kz_json:set_value([<<"Call">>, <<"Custom-Channel-Vars">>, <<"Queue-ID">>], QueueId, JObj), - _ = kapps_call_command:set('undefined' ,kz_json:from_list([{<<"Eavesdrop-Group-ID">>, QueueId} ,{<<"Queue-ID">>, QueueId} ]) - ,Call + ,Call1 ), + JObj1 = kz_json:set_value(<<"Call">>, kapps_call:to_json(Call1), JObj), + + %% Add member to queue for tracking position + gen_listener:cast(props:get_value('server', Props), {'add_queue_member', JObj1}); +start_queue_call(JObj, Props, Call, 'true') -> + _ = kapps_call:put_callid(Call), + QueueId = kz_json:get_value(<<"Queue-ID">>, JObj), + + Call1 = kapps_call:set_custom_channel_var(<<"Queue-ID">>, QueueId, Call), + + lager:info("member callback for queue ~s recv", [QueueId]), + + JObj1 = kz_json:set_value(<<"Call">>, kapps_call:to_json(Call1), JObj), + %% Add member to queue for tracking position - gen_listener:cast(props:get_value('server', Props), {'add_queue_member', JObj2}). + gen_listener:cast(props:get_value('server', Props), {'add_queue_member', JObj1}). -spec handle_member_call_success(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_member_call_success(JObj, Prop) -> @@ -211,9 +242,8 @@ handle_member_call_success(JObj, Prop) -> -spec handle_member_call_cancel(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_member_call_cancel(JObj, Props) -> - kz_log:put_callid(JObj), - lager:debug("cancel call ~p", [JObj]), 'true' = kapi_acdc_queue:member_call_cancel_v(JObj), + _ = kz_log:put_callid(JObj), K = make_ignore_key(kz_json:get_value(<<"Account-ID">>, JObj) ,kz_json:get_value(<<"Queue-ID">>, JObj) ,kz_json:get_value(<<"Call-ID">>, JObj) @@ -235,6 +265,10 @@ handle_agent_change(JObj, Prop) -> gen_listener:cast(Server, {'agent_unavailable', JObj}) end. +-spec handle_agents_available_req(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_agents_available_req(JObj, Prop) -> + gen_listener:cast(props:get_value('server', Prop), {'agents_available_req', JObj}). + -spec handle_queue_member_add(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_queue_member_add(JObj, Prop) -> gen_listener:cast(props:get_value('server', Prop), {'handle_queue_member_add', JObj}). @@ -243,15 +277,15 @@ handle_queue_member_add(JObj, Prop) -> handle_queue_member_remove(JObj, Prop) -> gen_listener:cast(props:get_value('server', Prop), {'handle_queue_member_remove', kz_json:get_value(<<"Call-ID">>, JObj)}). --spec handle_config_change(kz_types:server_ref(), kz_json:object()) -> 'ok'. +-spec handle_member_callback_reg(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_member_callback_reg(JObj, Prop) -> + gen_listener:cast(props:get_value('server', Prop), {'handle_member_callback_reg', JObj}). + +-spec handle_config_change(kz_term:server_ref(), kz_json:object()) -> 'ok'. handle_config_change(Srv, JObj) -> gen_listener:cast(Srv, {'update_queue_config', JObj}). --spec queue_size(kz_types:server_ref()) -> kz_term:non_neg_integer(). -queue_size(Srv) -> - gen_listener:call(Srv, 'queue_size'). - --spec should_ignore_member_call(kz_types:server_ref(), kapps_call:call(), kz_json:object()) -> boolean(). +-spec should_ignore_member_call(kz_term:server_ref(), kapps_call:call(), kz_json:object()) -> boolean(). should_ignore_member_call(Srv, Call, CallJObj) -> should_ignore_member_call(Srv ,Call @@ -259,7 +293,7 @@ should_ignore_member_call(Srv, Call, CallJObj) -> ,kz_json:get_value(<<"Queue-ID">>, CallJObj) ). --spec should_ignore_member_call(kz_types:server_ref(), kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary()) -> boolean(). +-spec should_ignore_member_call(kz_term:server_ref(), kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary()) -> boolean(). should_ignore_member_call(Srv, Call, AccountId, QueueId) -> K = make_ignore_key(AccountId, QueueId, kapps_call:call_id(Call)), gen_listener:call(Srv, {'should_ignore_member_call', K}). @@ -271,31 +305,39 @@ up_next(Srv, CallId) -> -spec config(pid()) -> {kz_term:ne_binary(), kz_term:ne_binary()}. config(Srv) -> gen_listener:call(Srv, 'config'). --spec current_agents(kz_types:server_ref()) -> kz_term:ne_binaries(). +-spec current_agents(kz_term:server_ref()) -> kz_term:ne_binaries(). current_agents(Srv) -> gen_listener:call(Srv, 'current_agents'). -spec status(pid()) -> kz_term:ne_binaries(). status(Srv) -> gen_listener:call(Srv, 'status'). +-spec calls(pid()) -> kz_term:ne_binaries(). +calls(Srv) -> gen_listener:call(Srv, 'calls'). + -spec refresh(pid(), kz_json:object()) -> 'ok'. refresh(Mgr, QueueJObj) -> gen_listener:cast(Mgr, {'refresh', QueueJObj}). strategy(Srv) -> gen_listener:call(Srv, 'strategy'). -next_winner(Srv) -> gen_listener:call(Srv, 'next_winner'). +next_winner(Srv, Call) -> gen_listener:call(Srv, {'next_winner', Call}). agents_available(Srv) -> gen_listener:call(Srv, 'agents_available'). --spec pick_winner(pid(), kz_json:objects()) -> +-spec pick_winner(pid(), kapps_call:call(), kz_json:objects()) -> 'undefined' | {kz_json:objects(), kz_json:objects()}. -pick_winner(Srv, Resps) -> pick_winner(Srv, Resps, strategy(Srv), next_winner(Srv)). +pick_winner(Srv, Call, Resps) -> pick_winner_(Resps, strategy(Srv), next_winner(Srv, Call)). + +-spec callback_details(pid(), kz_term:ne_binary()) -> kz_term:api_binary(). +callback_details(Srv, CallId) -> + gen_listener:call(Srv, {'callback_details', CallId}). %%%============================================================================= %%% gen_server callbacks %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Initializes the server. +%% @private +%% @doc Initializes the server %% @end %%------------------------------------------------------------------------------ -spec init([pid() | kz_json:object() | kz_term:ne_binary()]) -> {'ok', mgr_state()}. @@ -310,14 +352,16 @@ init([Super, QueueJObj]) -> init([Super, AccountId, QueueId]) -> kz_log:put_callid(<<"mgr_", QueueId/binary>>), - AcctDb = kzs_util:format_account_db(AccountId), - {'ok', QueueJObj} = kz_datamgr:open_cache_doc(AcctDb, QueueId), + AccountDb = kzs_util:format_account_db(AccountId), + {'ok', QueueJObj} = kz_datamgr:open_cache_doc(AccountDb, QueueId), init(Super, AccountId, QueueId, QueueJObj). init(Super, AccountId, QueueId, QueueJObj) -> + process_flag('trap_exit', 'true'), + AccountDb = kzs_util:format_account_db(AccountId), - _ = kz_datamgr:add_to_doc_cache(AccountDb, QueueId, QueueJObj), + kz_datamgr:add_to_doc_cache(AccountDb, QueueId, QueueJObj), _ = start_secondary_queue(AccountId, QueueId), @@ -336,12 +380,12 @@ init(Super, AccountId, QueueId, QueueJObj) -> })}. %%------------------------------------------------------------------------------ -%% @doc Handling call messages. +%% @private +%% @doc Handling call messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), mgr_state()) -> kz_types:handle_call_ret_state(mgr_state()). -handle_call('queue_size', _, #state{current_member_calls=Calls}=State) -> - {'reply', length(Calls), State}; +-spec handle_call(any(), kz_term:pid_ref(), mgr_state()) -> kz_term:handle_call_ret_state(mgr_state()). handle_call({'should_ignore_member_call', {AccountId, QueueId, CallId}=K}, _, #state{ignored_member_calls=Dict ,account_id=AccountId ,queue_id=QueueId @@ -353,11 +397,16 @@ handle_call({'should_ignore_member_call', {AccountId, QueueId, CallId}=K}, _, #s {'reply', 'true', State#state{ignored_member_calls=dict:erase(K, Dict)}} end; -handle_call({'up_next', CallId}, _, #state{strategy_state=SS - ,current_member_calls=CurrentCalls +handle_call({'up_next', CallId}, _, #state{strategy='sbrr' + ,strategy_state=#strategy_state{agents=#{call_id_map := CallIdMap}} + }=State) -> + {'reply', maps:is_key(CallId, CallIdMap), State}; +handle_call({'up_next', CallId}, _, #state{strategy=Strategy + ,strategy_state=SS + ,current_member_calls=Calls }=State) -> - FreeAgents = ss_size(SS, 'free'), - Position = call_position(CallId, lists:reverse(CurrentCalls)), + FreeAgents = ss_size(Strategy, SS, 'free'), + Position = queue_member_position(CallId, Calls), {'reply', FreeAgents >= Position, State}; handle_call('config', _, #state{account_id=AccountId @@ -366,57 +415,88 @@ handle_call('config', _, #state{account_id=AccountId {'reply', {AccountId, QueueId}, State}; handle_call('status', _, #state{strategy_state=#strategy_state{details=Details}}=State) -> - Known = [A || {A, {N, _}} <- dict:to_list(Details), N > 0], - {'reply', Known, State}; + Available = [A || {A, {N, _}} <- dict:to_list(Details), N > 0], + Busy = [A || {A, {_, 'busy'}} <- dict:to_list(Details)], + {'reply', {Available, Busy}, State}; + +handle_call('calls', _, #state{current_member_calls=Calls}=State) -> + Call_ids = [kapps_call:call_id(Call) || {_, Call} <- Calls], + {'reply', Call_ids, State}; handle_call('strategy', _, #state{strategy=Strategy}=State) -> {'reply', Strategy, State, 'hibernate'}; -handle_call('agents_available', _, #state{strategy_state=SS}=State) -> - {'reply', ss_size(SS, 'logged_in'), State}; +handle_call('agents_available', _, #state{strategy=Strategy + ,strategy_state=SS + }=State) -> + {'reply', ss_size(Strategy, SS, 'logged_in'), State}; handle_call('enter_when_empty', _, #state{enter_when_empty=EnterWhenEmpty}=State) -> {'reply', EnterWhenEmpty, State}; -handle_call('next_winner', _, #state{strategy='mi'}=State) -> +handle_call({'next_winner', _}, _, #state{strategy='mi'}=State) -> {'reply', 'undefined', State}; -handle_call('next_winner', _, #state{strategy='rr' - ,strategy_state=#strategy_state{agents=Agents}=SS - }=State) -> - case queue:out(Agents) of - {{'value', Winner}, Agents1} -> - {'reply', Winner, State#state{strategy_state=SS#strategy_state{agents=queue:in(Winner, Agents1)}}, 'hibernate'}; +handle_call({'next_winner', _}, _, #state{strategy='rr' + ,strategy_state=#strategy_state{agents=Agents}=SS + }=State) -> + case pqueue4:pout(Agents) of + {{'value', Winner, Priority}, Agents1} -> + {'reply', Winner, State#state{strategy_state=SS#strategy_state{agents=pqueue4:in(Winner, Priority, Agents1)}}, 'hibernate'}; {'empty', _} -> {'reply', 'undefined', State} end; -handle_call('next_winner', _, #state{strategy='all'}=State) -> - {'reply', 'undefined', State}; +handle_call({'next_winner', Call}, _, #state{strategy='sbrr' + ,strategy_state=#strategy_state{agents=#{call_id_map := CallIdMap}} + }=State) -> + CallId = kapps_call:call_id(Call), + {'reply', maps:get(CallId, CallIdMap, 'undefined'), State}; +handle_call({'next_winner', _}, _, #state{strategy='all' + ,strategy_state=#strategy_state{agents=Agents} + }=State) -> + case pqueue4:to_plist(Agents) of + [{_Priority, P_Agents}|_T] -> + {'reply', P_Agents, State, 'hibernate'}; + _ -> + {'reply', 'undefined', State} + end; -handle_call('current_agents', _, #state{strategy='rr' - ,strategy_state=#strategy_state{agents=Q} - }=State) -> - {'reply', queue:to_list(Q), State}; +handle_call('current_agents', _, #state{strategy=S + ,strategy_state=#strategy_state{agents=Q + ,ringing_agents=RingingAgents + ,busy_agents=BusyAgents + } + }=State) when S =:= 'rr' + orelse S =:= 'all' -> + {'reply', pqueue4:to_list(Q) ++ RingingAgents ++ BusyAgents, State}; handle_call('current_agents', _, #state{strategy='mi' ,strategy_state=#strategy_state{agents=L} }=State) -> {'reply', L, State}; -handle_call('current_agents', _, #state{strategy='all' - ,strategy_state=#strategy_state{agents=Q} +handle_call('current_agents', _, #state{strategy='sbrr' + ,strategy_state=#strategy_state{agents=#{rr_queue := RRQueue} + ,ringing_agents=RingingAgents + ,busy_agents=BusyAgents + } }=State) -> - {'reply', queue:to_list(Q), State}; + {'reply', pqueue4:to_list(RRQueue) ++ RingingAgents ++ BusyAgents, State}; -handle_call({'queue_position', CallId}, _, #state{current_member_calls=CurrentCalls}=State) -> - Position = call_position(CallId, lists:reverse(CurrentCalls)), +handle_call({'queue_member_position', CallId}, _, #state{current_member_calls=Calls}=State) -> + Position = queue_member_position(CallId, Calls), {'reply', Position, State}; +handle_call({'callback_details', CallId}, _, #state{registered_callbacks=Callbacks}=State) -> + {'reply', props:get_value(CallId, Callbacks), State}; + handle_call(_Request, _From, State) -> {'reply', 'ok', State}. %%------------------------------------------------------------------------------ -%% @doc Handling cast messages. +%% @private +%% @doc Handling cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_cast(any(), mgr_state()) -> kz_types:handle_cast_ret_state(mgr_state()). +-spec handle_cast(any(), mgr_state()) -> kz_term:handle_cast_ret_state(mgr_state()). handle_cast({'update_strategy', StrategyState}, State) -> {'noreply', State#state{strategy_state=StrategyState}, 'hibernate'}; @@ -431,17 +511,30 @@ handle_cast({'member_call_cancel', K, JObj}, #state{ignored_member_calls=Dict}=S CallId = kz_json:get_value(<<"Call-ID">>, JObj), Reason = kz_json:get_value(<<"Reason">>, JObj), - 'ok' = acdc_stats:call_abandoned(AccountId, QueueId, CallId, Reason), - {'noreply', State#state{ignored_member_calls=dict:store(K, 'true', Dict)}}; + _ = acdc_stats:call_abandoned(AccountId, QueueId, CallId, Reason), + case Reason of + %% Don't add to ignored_member_calls because an FSM has already dealt with this call + <<"No agents left in queue">> -> + {'noreply', State}; + _ -> + {'noreply', State#state{ignored_member_calls=dict:store(K, 'true', Dict)}} + end; + +handle_cast({'monitor_call', Call}, State) -> + CallId = kapps_call:call_id(Call), + gen_listener:add_binding(self(), 'call', [{'callid', CallId} + ,{'restrict_to', [<<"CHANNEL_DESTROY">>]} + ]), + lager:debug("bound for call events for ~s", [CallId]), + {'noreply', State}; handle_cast({'start_workers'}, #state{account_id=AccountId ,queue_id=QueueId ,supervisor=QueueSup }=State) -> WorkersSup = acdc_queue_sup:workers_sup(QueueSup), case kz_datamgr:get_results(kzs_util:format_account_db(AccountId) - ,<<"queues/agents_listing">> - ,[{'startkey', [QueueId]} - ,{'endkey', [QueueId, kz_json:new()]} + ,?CB_AGENTS_LIST + ,[{'key', QueueId} ,{'group', 'true'} ,{'group_level', 1} ]) @@ -470,49 +563,72 @@ handle_cast({'start_worker', N}, #state{account_id=AccountId acdc_queue_workers_sup:new_workers(WorkersSup, AccountId, QueueId, N), {'noreply', State}; -handle_cast({'agent_available', AgentId}, #state{strategy=Strategy - ,strategy_state=StrategyState - ,supervisor=QueueSup - }=State) when is_binary(AgentId) -> - lager:info("adding agent ~s to strategy ~s", [AgentId, Strategy]), - StrategyState1 = update_strategy_with_agent(Strategy, StrategyState, AgentId, 'add', 'undefined'), - maybe_start_queue_workers(QueueSup, ss_size(StrategyState1, 'logged_in')), +handle_cast({'agent_available', AgentId, Priority, Skills}, #state{supervisor=QueueSup + ,strategy=Strategy + }=State) when is_binary(AgentId) -> + StrategyState1 = update_strategy_with_agent(State, AgentId, Priority, Skills, 'add', 'undefined'), + maybe_start_queue_workers(QueueSup, ss_size(Strategy, StrategyState1, 'logged_in')), {'noreply', State#state{strategy_state=StrategyState1} ,'hibernate'}; handle_cast({'agent_available', JObj}, State) -> - handle_cast({'agent_available', kz_json:get_value(<<"Agent-ID">>, JObj)}, State); + handle_cast({'agent_available' + ,kz_json:get_ne_binary_value(<<"Agent-ID">>, JObj) + ,kz_json:get_integer_value(<<"Priority">>, JObj, 0) + ,kz_json:get_list_value(<<"Skills">>, JObj, []) + }, State); -handle_cast({'agent_ringing', AgentId}, #state{strategy=Strategy - ,strategy_state=StrategyState - }=State) when is_binary(AgentId) -> +handle_cast({'agent_ringing', AgentId, Priority}, #state{strategy=Strategy}=State) when is_binary(AgentId) -> lager:info("agent ~s ringing, maybe updating strategy ~s", [AgentId, Strategy]), - - StrategyState1 = maybe_update_strategy(Strategy, StrategyState, AgentId), - {'noreply', State#state{strategy_state=StrategyState1}, 'hibernate'}; + StrategyState1 = update_strategy_with_agent(State, AgentId, Priority, [], 'remove', 'ringing'), + {'noreply', State#state{strategy_state=StrategyState1} + ,'hibernate'}; handle_cast({'agent_ringing', JObj}, State) -> - handle_cast({'agent_ringing', kz_json:get_value(<<"Agent-ID">>, JObj)}, State); + handle_cast({'agent_ringing' + ,kz_json:get_ne_binary_value(<<"Agent-ID">>, JObj) + ,kz_json:get_integer_value(<<"Priority">>, JObj, 0) + }, State); -handle_cast({'agent_busy', AgentId}, #state{strategy=Strategy - ,strategy_state=StrategyState - }=State) when is_binary(AgentId) -> +handle_cast({'agent_busy', AgentId, Priority}, #state{strategy=Strategy}=State) when is_binary(AgentId) -> lager:info("agent ~s busy, maybe updating strategy ~s", [AgentId, Strategy]), - StrategyState1 = update_strategy_with_agent(Strategy, StrategyState, AgentId, 'remove', 'busy'), + StrategyState1 = update_strategy_with_agent(State, AgentId, Priority, [], 'remove', 'busy'), {'noreply', State#state{strategy_state=StrategyState1} ,'hibernate'}; handle_cast({'agent_busy', JObj}, State) -> - handle_cast({'agent_busy', kz_json:get_value(<<"Agent-ID">>, JObj)}, State); + handle_cast({'agent_busy' + ,kz_json:get_ne_binary_value(<<"Agent-ID">>, JObj) + ,kz_json:get_integer_value(<<"Priority">>, JObj, 0) + }, State); -handle_cast({'agent_unavailable', AgentId}, #state{strategy=Strategy - ,strategy_state=StrategyState - }=State) when is_binary(AgentId) -> +handle_cast({'agent_unavailable', AgentId, Priority}, #state{strategy=Strategy}=State) when is_binary(AgentId) -> lager:info("agent ~s unavailable, maybe updating strategy ~s", [AgentId, Strategy]), - StrategyState1 = update_strategy_with_agent(Strategy, StrategyState, AgentId, 'remove', 'undefined'), + StrategyState1 = update_strategy_with_agent(State, AgentId, Priority, [], 'remove', 'undefined'), {'noreply', State#state{strategy_state=StrategyState1} ,'hibernate'}; handle_cast({'agent_unavailable', JObj}, State) -> - handle_cast({'agent_unavailable', kz_json:get_value(<<"Agent-ID">>, JObj)}, State); + handle_cast({'agent_unavailable' + ,kz_json:get_ne_binary_value(<<"Agent-ID">>, JObj) + ,kz_json:get_integer_value(<<"Priority">>, JObj, 0) + }, State); + +handle_cast({'agents_available_req', JObj}, #state{account_id=AccountId + ,queue_id=QueueId + ,strategy='sbrr' + ,strategy_state=#strategy_state{agents=#{skill_map := SkillMap}} + }=State) -> + Skills = lists:sort(kz_json:get_list_value(<<"Skills">>, JObj, [])), + AgentCount = sets:size(maps:get(Skills, SkillMap, sets:new())), + publish_agents_available_resp(AccountId, QueueId, AgentCount, JObj), + {'noreply', State}; +handle_cast({'agents_available_req', JObj}, #state{account_id=AccountId + ,queue_id=QueueId + ,strategy=Strategy + ,strategy_state=StrategyState + }=State) -> + AgentCount = ss_size(Strategy, StrategyState, 'logged_in'), + publish_agents_available_resp(AccountId, QueueId, AgentCount, JObj), + {'noreply', State}; handle_cast({'reject_member_call', Call, JObj}, #state{account_id=AccountId ,queue_id=QueueId @@ -528,10 +644,9 @@ handle_cast({'reject_member_call', Call, JObj}, #state{account_id=AccountId {'noreply', State}; handle_cast({'sync_with_agent', A}, #state{account_id=AccountId}=State) -> - {'ok', Status} = acdc_agent_util:most_recent_status(AccountId, A), - case acdc_agent_util:status_should_auto_start(Status) of - 'true' -> 'ok'; - 'false' -> gen_listener:cast(self(), {'agent_unavailable', A}) + case acdc_agent_util:most_recent_status(AccountId, A) of + {'ok', <<"logged_out">>} -> gen_listener:cast(self(), {'agent_unavailable', A, 0}); + _ -> 'ok' end, {'noreply', State}; @@ -547,62 +662,137 @@ handle_cast({'gen_listener',{'is_consuming',_IsConsuming}}, State) -> handle_cast({'add_queue_member', JObj}, #state{account_id=AccountId ,queue_id=QueueId - ,current_member_calls=CurrentCalls - ,announcements_config=AnnouncementsConfig - ,announcements_pids=AnnouncementsPids + ,supervisor=QueueSup + ,strategy=Strategy + ,current_member_calls=Calls }=State) -> - Position = length(CurrentCalls)+1, - Call = kapps_call:set_custom_channel_var(<<"Queue-Position">> - ,Position - ,kapps_call:from_json(kz_json:get_value(<<"Call">>, JObj))), - - 'ok' = acdc_stats:call_waiting(AccountId, QueueId - ,kapps_call:call_id(Call) - ,kapps_call:caller_id_name(Call) - ,kapps_call:caller_id_number(Call) - ,kz_json:get_integer_value(<<"Member-Priority">>, JObj) - ), - - publish_queue_member_add(AccountId, QueueId, Call), + Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, JObj)), + CallId = kapps_call:call_id(Call), + Priority = kz_json:get_integer_value(<<"Member-Priority">>, JObj), + Position = queue_member_insert_position(CallId, Priority, Calls), + %% Skills-based engine is optimized if required skills are sorted + Skills = lists:sort(kapps_call:kvs_fetch(?ACDC_REQUIRED_SKILLS_KEY, [], Call)), + + Call1 = kapps_call:exec([{fun kapps_call:set_custom_channel_var/3, <<"Queue-Position">>, Position} + ,{fun kapps_call:kvs_store/3, ?ACDC_REQUIRED_SKILLS_KEY, Skills} + ], Call), + JObj1 = kz_json:set_value(<<"Call">>, kapps_call:to_json(Call), JObj), + + {CIDNumber, CIDName} = acdc_util:caller_id(Call1), + %% Only going to submit skills to stats if the sbrr strategy is enabled + StatSkills = case Strategy of + 'sbrr' -> Skills; + _ -> + lager:warning("skills ~p required, but queue ~s is not set to skills_based_round_robin", [Skills, QueueId]), + 'undefined' + end, + _ = acdc_stats:call_waiting(AccountId, QueueId, Position + ,kapps_call:call_id(Call1) + ,CIDName + ,CIDNumber + ,Priority + ,StatSkills + ), + + publish_queue_member_add(AccountId, QueueId, Call1, Priority + ,kz_json:is_true(<<"Enter-As-Callback">>, JObj1) + ,kz_json:get_binary_value(<<"Callback-Number">>, JObj1) + ), %% Add call to shared queue - kapi_acdc_queue:publish_shared_member_call(AccountId, QueueId, JObj), + kapi_acdc_queue:publish_shared_member_call(AccountId, QueueId, JObj1), lager:debug("put call into shared messaging queue"), + gen_listener:cast(self(), {'monitor_call', Call1}), + acdc_util:presence_update(AccountId, QueueId, ?PRESENCE_RED_FLASH), - %% Schedule position/wait time announcements - AnnouncementsPids1 = case acdc_announcements_sup:maybe_start_announcements(self(), Call, AnnouncementsConfig) of - 'false' -> AnnouncementsPids; - {'ok', Pid} -> - CallId = kapps_call:call_id(Call), - AnnouncementsPids#{CallId => Pid} - end, + %% SBRR needs extra workers, priority feature needs extra workers + maybe_start_queue_workers(QueueSup, length(Calls) + 1), - {'noreply', State#state{current_member_calls=[Call | CurrentCalls] - ,announcements_pids=AnnouncementsPids1 - }}; + State1 = lists:foldl(fun({Updater, Args}, StateAcc) -> apply(Updater, Args ++ [StateAcc]) end + ,State + ,[{fun add_queue_member/4, [Call1, Priority, Position]} + ,{fun maybe_schedule_position_announcements/3, [JObj1, Call1]} + ,{fun maybe_add_queue_member_as_callback/3, [JObj1, Call1]} + ]), + {'noreply', State1}; -handle_cast({'handle_queue_member_add', JObj}, #state{current_member_calls=CurrentCalls}=State) -> +handle_cast({'handle_queue_member_add', JObj}, #state{supervisor=QueueSup + ,current_member_calls=Calls + }=State) -> Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, JObj)), CallId = kapps_call:call_id(Call), - lager:debug("received notification of new queue member ~s", [CallId]), + Priority = kz_json:get_integer_value(<<"Member-Priority">>, JObj), + Position = kapps_call:custom_channel_var(<<"Queue-Position">>, Call), + lager:debug("received notification of new queue member ~s with priority ~p", [CallId, Priority]), + + %% SBRR needs extra workers, priority feature needs extra workers + maybe_start_queue_workers(QueueSup, length(Calls) + 1), + + State1 = lists:foldl(fun({Updater, Args}, StateAcc) -> apply(Updater, Args ++ [StateAcc]) end + ,State + ,[{fun add_queue_member/4, [Call, Priority, Position]} + ,{fun maybe_add_queue_member_as_callback/3, [JObj, Call]} + ,{fun maybe_reseed_sbrrss_maps/1, []} + ]), + {'noreply', State1}; - {'noreply', State#state{current_member_calls = [Call | lists:keydelete(CallId, 2, CurrentCalls)]}}; +handle_cast({'handle_queue_member_remove', CallId}, #state{current_member_calls=Calls + ,announcements_pids=Pids + }=State) -> + Call = queue_member(CallId, Calls), + Position = queue_member_position(CallId, Calls), + lager:debug("removing call id ~s", [CallId]), -handle_cast({'handle_queue_member_remove', CallId}, State) -> - State1 = remove_queue_member(CallId, State), - {'noreply', State1}; + publish_call_exited_position(CallId, Position, State), + + State1 = State#state{announcements_pids=cancel_position_announcements(Call, Pids)}, + State2 = lists:foldl(fun({Updater, Args}, StateAcc) -> apply(Updater, Args ++ [StateAcc]) end + ,State1 + ,[{fun remove_queue_member/2, [CallId]} + ,{fun maybe_remove_callback_reg/2, [CallId]} + ,{fun maybe_reseed_sbrrss_maps/1, []} + ] + ), + {'noreply', State2}; + +handle_cast({'handle_member_callback_reg', JObj}, #state{account_id=AccountId + ,queue_id=QueueId + ,current_member_calls=Calls + ,announcements_pids=Pids + ,registered_callbacks=RegCallbacks}=State) -> + CallId = kz_json:get_value(<<"Call-ID">>, JObj), + case queue_member(CallId, Calls) of + 'undefined' -> + lager:debug("not accepting callback reg for ~s (call not in my list of calls)", [CallId]), + {'noreply', State}; + Call -> + lager:debug("call ~s marked as callback", [CallId]), + Number = kz_json:get_value(<<"Number">>, JObj), + Call1 = callback_flag(AccountId, QueueId, Call), + CIDPrepend = kapps_call:kvs_fetch('prepend_cid_name', Call1), + Priority = queue_member_priority(CallId, Calls), + Position = queue_member_position(CallId, Calls), + + State1 = State#state{announcements_pids=cancel_position_announcements(Call, Pids) + ,registered_callbacks=[{CallId, {Number, CIDPrepend}} | RegCallbacks] + }, + State2 = add_queue_member(Call, Priority, Position, State1), + {'noreply', State2} + end; handle_cast(_Msg, State) -> lager:debug("unhandled cast: ~p", [_Msg]), {'noreply', State}. %%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. +%% @private +%% @doc Handling all non call/cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_info(any(), mgr_state()) -> kz_types:handle_info_ret_state(mgr_state()). +-spec handle_info(any(), mgr_state()) -> kz_term:handle_info_ret_state(mgr_state()). handle_info(_Info, State) -> lager:debug("unhandled message: ~p", [_Info]), {'noreply', State}. @@ -616,19 +806,24 @@ handle_event(_JObj, #state{enter_when_empty=EnterWhenEmpty ]}. %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_server' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_server' terminates +%% @private +%% @doc This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @end %%------------------------------------------------------------------------------ -spec terminate(any(), mgr_state()) -> 'ok'. -terminate(_Reason, _State) -> - lager:debug("queue manager terminating: ~p", [_Reason]). +terminate(_Reason, #state{queue_id = QueueId}) -> + lager:debug("queue manager terminating: ~p", [_Reason]), + gen_listener:rm_queue(self(), ?SECONDARY_QUEUE_NAME(QueueId)), + ok. %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), mgr_state(), any()) -> {'ok', mgr_state()}. @@ -645,7 +840,7 @@ code_change(_OldVsn, State, _Extra) -> %%------------------------------------------------------------------------------ start_secondary_queue(AccountId, QueueId) -> AccountDb = kzs_util:format_account_db(AccountId), - Priority = lookup_priority_levels(AccountDb, QueueId), + Priority = acdc_util:max_priority(AccountDb, QueueId), kz_process:spawn(fun gen_listener:add_queue/4 ,[self() ,?SECONDARY_QUEUE_NAME(QueueId) @@ -655,23 +850,104 @@ start_secondary_queue(AccountId, QueueId) -> ,?SECONDARY_BINDINGS(AccountId, QueueId) ]). --spec lookup_priority_levels(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:api_integer(). -lookup_priority_levels(AccountDB, QueueId) -> - case kz_datamgr:open_cache_doc(AccountDB, QueueId) of - {'ok', JObj} -> kz_json:get_value(<<"max_priority">>, JObj); - _ -> 'undefined' - end. - make_ignore_key(AccountId, QueueId, CallId) -> {AccountId, QueueId, CallId}. --spec publish_queue_member_add(kz_term:ne_binary(), kz_term:ne_binary(), kapps_call:call()) -> 'ok'. -publish_queue_member_add(AccountId, QueueId, Call) -> - Prop = [{<<"Account-ID">>, AccountId} +-spec queue_member(kz_term:ne_binary(), list()) -> kapps_call:call() | 'undefined'. +queue_member(CallId, Calls) -> + case queue_member_lookup(CallId, Calls) of + 'undefined' -> 'undefined'; + {Call, _, _} -> Call + end. + +-spec queue_member_priority(kz_term:ne_binary(), list()) -> kz_term:api_non_neg_integer(). +queue_member_priority(CallId, Calls) -> + case queue_member_lookup(CallId, Calls) of + 'undefined' -> 'undefined'; + {_, Priority, _} -> Priority + end. + +-spec queue_member_position(kz_term:ne_binary(), list()) -> kz_term:api_pos_integer(). +queue_member_position(CallId, Calls) -> + case queue_member_lookup(CallId, Calls) of + 'undefined' -> 'undefined'; + {_, _, Position} -> Position + end. + +-spec queue_member_lookup(kz_term:ne_binary(), list()) -> + {kapps_call:call(), non_neg_integer(), pos_integer()} | 'undefined'. +queue_member_lookup(CallId, Calls) -> + queue_member_lookup(CallId, Calls, 1). + +-spec queue_member_lookup(kz_term:ne_binary(), list(), pos_integer()) -> + {kapps_call:call(), non_neg_integer(), pos_integer()} | 'undefined'. +queue_member_lookup(_, [], _) -> 'undefined'; +queue_member_lookup(CallId, [{Priority, Call}|Calls], Position) -> + case kapps_call:call_id(Call) of + CallId -> {Call, Priority, Position}; + _ -> queue_member_lookup(CallId, Calls, Position + 1) + end. + +-spec queue_member_insert_position(kz_term:ne_binary(), kz_term:api_integer(), list()) -> pos_integer(). +queue_member_insert_position(CallId, 'undefined', Calls) -> + queue_member_insert_position(CallId, 0, Calls); +queue_member_insert_position(CallId, Priority, Calls) -> + queue_member_insert_position(CallId, Priority, Calls, 1). + +-spec queue_member_insert_position(kz_term:ne_binary(), kz_term:api_integer(), list(), pos_integer()) -> pos_integer(). +queue_member_insert_position(_, _, [], Position) -> + Position; +queue_member_insert_position(CallId, Priority, [{OtherPriority, OtherCall}|Calls], Position) -> + OtherCallId = kapps_call:call_id(OtherCall), + case CallId =:= OtherCallId + orelse Priority > OtherPriority + of + 'true' -> Position; + 'false' -> queue_member_insert_position(CallId, Priority, Calls, Position + 1) + end. + +-spec add_queue_member(kapps_call:call(), kz_term:api_non_neg_integer(), pos_integer(), mgr_state()) -> mgr_state(). +add_queue_member(Call, 'undefined', Position, State) -> + add_queue_member(Call, 0, Position, State); +add_queue_member(Call, Priority, Position, State) -> + #state{current_member_calls=Calls}=State1 = remove_queue_member(Call, State), + %% Handles the case where calls were removed since the Position was assigned + SplitIndex = min(Position - 1, length(Calls)), + {Before, After} = lists:split(SplitIndex, Calls), + Calls1 = Before ++ [{Priority, Call}|After], + State1#state{current_member_calls=Calls1}. + +-spec remove_queue_member(kz_term:ne_binary() | kapps_call:call(), mgr_state()) -> mgr_state(). +remove_queue_member(CallId, #state{current_member_calls=Calls}=State) when is_binary(CallId) -> + Calls1 = lists:filter(fun({_, Call1}) -> + kapps_call:call_id(Call1) =/= CallId + end, Calls), + State#state{current_member_calls=Calls1}; +remove_queue_member(Call, State) -> + remove_queue_member(kapps_call:call_id(Call), State). + +-spec publish_agents_available_resp(kz_term:ne_binary(), kz_term:ne_binary(), non_neg_integer(), kz_json:object()) -> 'ok'. +publish_agents_available_resp(AccountId, QueueId, AgentCount, JObj) -> + Resp = [{<<"Account-ID">>, AccountId} ,{<<"Queue-ID">>, QueueId} - ,{<<"Call">>, kapps_call:to_json(Call)} + ,{<<"Agent-Count">>, AgentCount} + ,{<<"Msg-ID">>, kz_json:get_value(<<"Msg-ID">>, JObj)} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], + Q = kz_json:get_value(<<"Server-ID">>, JObj), + kapi_acdc_queue:publish_agents_available_resp(Q, Resp). + +-spec publish_queue_member_add(kz_term:ne_binary(), kz_term:ne_binary(), kapps_call:call(), kz_term:api_non_neg_integer(), boolean(), kz_term:api_binary()) -> 'ok'. +publish_queue_member_add(AccountId, QueueId, Call, Priority, EnterAsCallback, CallbackNumber) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Call">>, kapps_call:to_json(Call)} + ,{<<"Member-Priority">>, Priority} + ,{<<"Enter-As-Callback">>, EnterAsCallback} + ,{<<"Callback-Number">>, CallbackNumber} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), kapi_acdc_queue:publish_queue_member_add(Prop). -spec publish_queue_member_remove(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. @@ -684,101 +960,180 @@ publish_queue_member_remove(AccountId, QueueId, CallId) -> kapi_acdc_queue:publish_queue_member_remove(Prop). %% Really sophisticated selection algorithm --spec pick_winner(pid(), kz_json:objects(), queue_strategy(), kz_term:api_binary()) -> - 'undefined' | - {kz_json:objects(), kz_json:objects()}. -pick_winner(_, [], _, _) -> - lager:debug("no agent responses are left to choose from"), +-spec pick_winner_(kz_json:objects(), queue_strategy(), kz_term:api_binary()) -> + 'undefined' | {kz_json:objects(), kz_json:objects()}. +pick_winner_(_, S, 'undefined') -> + lager:error("no next_agent (~p) available; try again", [S]), 'undefined'; -pick_winner(Mgr, CRs, 'rr', AgentId) -> +pick_winner_(CRs, 'rr', AgentId) -> case split_agents(AgentId, CRs) of {[], _O} -> - lager:debug("oops, agent ~s appears to have not responded; try again", [AgentId]), - pick_winner(Mgr, remove_unknown_agents(Mgr, CRs), 'rr', next_winner(Mgr)); - {Winners, OtherAgents} -> + lager:debug("oops, (rr) agent ~s appears to have not responded; try again", [AgentId]), + 'undefined'; + {[Winner|_], OtherAgents} -> lager:debug("found winning responders for agent: ~s", [AgentId]), - {Winners, OtherAgents} + {[Winner], OtherAgents} end; -pick_winner(_Mgr, CRs, 'mi', _) -> +pick_winner_(CRs, 'mi', _) -> [MostIdle | Rest] = lists:usort(fun sort_agent/2, CRs), - AgentId = kz_json:get_value(<<"Agent-ID">>, MostIdle), - {Same, Other} = split_agents(AgentId, Rest), + %% AgentId = kz_json:get_value(<<"Agent-ID">>, MostIdle), + %% {Same, Other} = split_agents(AgentId, Rest), + %% {[MostIdle|Same], Other}; + {[MostIdle], Rest}; +pick_winner_(CRs, 'sbrr', AgentId) -> + pick_winner_(CRs, 'rr', AgentId); +pick_winner_(CRs, 'all', Agents) -> + %% case lists:flatten([lists:last(lists:filter(fun(R) -> AgentId =:= kz_json:get_value(<<"Agent-ID">>, R) end, CRs)) || AgentId <- Agents]) of + case lists:flatten([filter_winners(CRs, AgentId) || AgentId <- Agents]) of + [] -> + lager:debug("oops, (all) agent(s) ~p appears to have not responded; try again", [Agents]), + 'undefined'; + Winners -> + lager:debug("found winning responders for agent(s): ~p", [Agents]), + {Winners, []} + end. - {[MostIdle|Same], Other}; -pick_winner(_Mgr, CRs, 'all', _AgentId) -> - {CRs, []}. +filter_winners(CRs, AgentId) -> + case lists:filter(fun(R) -> AgentId =:= kz_json:get_value(<<"Agent-ID">>, R) end, CRs) of + [] -> []; + Winners -> lists:last(Winners) + end. --spec update_strategy_with_agent(queue_strategy(), strategy_state(), kz_term:ne_binary(), 'add' | 'remove', 'busy' | 'undefined') -> +-spec update_strategy_with_agent(mgr_state(), kz_term:ne_binary(), agent_priority(), kz_term:ne_binaries(), 'add' | 'remove', 'ringing' | 'busy' | 'undefined') -> strategy_state(). -update_strategy_with_agent('rr', #strategy_state{agents=AgentQueue}=SS, AgentId, 'add', Busy) -> - case queue:member(AgentId, AgentQueue) of - 'true' -> set_busy(AgentId, Busy, SS); - 'false' -> set_busy(AgentId, Busy, add_agent('rr', AgentId, SS)) - end; -update_strategy_with_agent('rr', SS, AgentId, 'remove', 'busy') -> - set_busy(AgentId, 'busy', SS); -update_strategy_with_agent('rr', #strategy_state{agents=AgentQueue}=SS, AgentId, 'remove', Busy) -> - case queue:member(AgentId, AgentQueue) of - 'false' -> set_busy(AgentId, Busy, SS); - 'true' -> set_busy(AgentId, Busy, remove_agent('rr', AgentId, SS)) - end; -update_strategy_with_agent('mi', #strategy_state{agents=AgentL}=SS, AgentId, 'add', Busy) -> - case lists:member(AgentId, AgentL) of - 'true' -> set_busy(AgentId, Busy, SS); - 'false' -> set_busy(AgentId, Busy, add_agent('mi', AgentId, SS)) - end; -update_strategy_with_agent('mi', SS, AgentId, 'remove', 'busy') -> - set_busy(AgentId, 'busy', SS); -update_strategy_with_agent('mi', #strategy_state{agents=AgentL}=SS, AgentId, 'remove', Busy) -> - case lists:member(AgentId, AgentL) of - 'false' -> set_busy(AgentId, Busy, SS); - 'true' -> set_busy(AgentId, Busy, remove_agent('mi', AgentId, SS)) - end; -update_strategy_with_agent('all', #strategy_state{agents=AgentQueue}=SS, AgentId, 'add', Busy) -> - case queue:member(AgentId, AgentQueue) of - 'true' -> set_busy(AgentId, Busy, SS); - 'false' -> set_busy(AgentId, Busy, add_agent('all', AgentId, SS)) - end; -update_strategy_with_agent('all', SS, AgentId, 'remove', 'busy') -> - set_busy(AgentId, 'busy', SS); -update_strategy_with_agent('all', #strategy_state{agents=AgentQueue}=SS, AgentId, 'remove', Busy) -> - case queue:member(AgentId, AgentQueue) of - 'false' -> set_busy(AgentId, Busy, SS); - 'true' -> set_busy(AgentId, Busy, remove_agent('all', AgentId, SS)) +update_strategy_with_agent(#state{strategy=S + ,strategy_state=SS + }, AgentId, Priority, _, Action, Flag) when S =:= 'rr' + orelse S =:= 'all' -> + update_rr_strategy_with_agent(SS, AgentId, Priority, Action, Flag); +update_strategy_with_agent(#state{strategy='mi' + ,strategy_state=SS + }, AgentId, _, _, Action, Flag) -> + update_mi_strategy_with_agent(SS, AgentId, Action, Flag); +update_strategy_with_agent(#state{strategy='sbrr' + ,strategy_state=SS + ,current_member_calls=Calls + }, AgentId, Priority, Skills, Action, Flag) -> + update_sbrrss_with_agent(AgentId, Priority, Skills, Action, Flag, SS, Calls). + +-spec update_rr_strategy_with_agent(strategy_state(), kz_term:ne_binary(), agent_priority(), 'add' | 'remove', 'ringing' | 'busy' | 'undefined') -> + strategy_state(). +update_rr_strategy_with_agent(#strategy_state{agents=AgentQueue + ,details=Details + }=SS + ,AgentId, Priority, 'add', Flag + ) -> + SS1 = case pqueue4:remove_unique(fun(AgentId1) when AgentId =:= AgentId1 -> + 'true'; + (_) -> + 'false' + end, AgentQueue) of + {'true', AgentQueue1} -> + lager:info("re-adding agent ~s (prio ~b) to strategy rr", [AgentId, -1 * Priority]), + SS#strategy_state{agents=pqueue4:in(AgentId, Priority, AgentQueue1)}; + {'false', AgentQueue1} -> + lager:info("adding agent ~s (prio ~b) to strategy rr", [AgentId, -1 * Priority]), + SS#strategy_state{agents=pqueue4:in(AgentId, Priority, AgentQueue1) + ,details=incr_agent(AgentId, Details) + } + end, + set_flag(AgentId, Flag, SS1); +update_rr_strategy_with_agent(#strategy_state{agents=AgentQueue}=SS, AgentId, _Priority, 'remove', Flag) -> + SS1 = case lists:member(AgentId, pqueue4:to_list(AgentQueue)) of + 'false' -> SS; + 'true' -> + lager:info("removing agent ~s from strategy rr", [AgentId]), + remove_agent('rr', AgentId, SS) + end, + set_flag(AgentId, Flag, SS1). + +-spec update_mi_strategy_with_agent(strategy_state(), kz_term:ne_binary(), 'add' | 'remove', 'ringing' | 'busy' | 'undefined') -> + strategy_state(). +update_mi_strategy_with_agent(#strategy_state{agents=AgentL + ,details=Details + }=SS + ,AgentId, 'add', Flag + ) -> + SS1 = case lists:member(AgentId, AgentL) of + 'true' -> SS; + 'false' -> + lager:info("adding agent ~s to strategy mi", [AgentId]), + SS#strategy_state{agents=[AgentId | AgentL] + ,details=incr_agent(AgentId, Details) + } + end, + set_flag(AgentId, Flag, SS1); +update_mi_strategy_with_agent(#strategy_state{agents=AgentL}=SS, AgentId, 'remove', Flag) -> + SS1 = case lists:member(AgentId, AgentL) of + 'false' -> SS; + 'true' -> + lager:info("removing agent ~s from strategy mi", [AgentId]), + remove_agent('mi', AgentId, SS) + end, + set_flag(AgentId, Flag, SS1). + +-spec update_sbrrss_with_agent(kz_json:object(), strategy_state()) -> strategy_state(). +update_sbrrss_with_agent(JObj, SS) -> + AgentId = kz_doc:id(JObj), + Priority = -1 * kz_json:get_integer_value([<<"value">>, <<"agent_priority">>], JObj, 0), + Skills = kz_json:get_list_value([<<"value">>, <<"skills">>], JObj, []), + update_sbrrss_with_agent(AgentId, Priority, Skills, 'add', 'undefined', SS, []). + +-spec update_sbrrss_with_agent(kz_term:ne_binary(), agent_priority(), kz_term:ne_binaries(), 'add' | 'remove', 'ringing' | 'busy' | 'undefined', strategy_state(), list()) -> + strategy_state(). +update_sbrrss_with_agent(AgentId, Priority, Skills, 'add', Flag, #strategy_state{agents=#{rr_queue := RRQueue + ,skill_map := SkillMap + }=SBRRSS + ,details=Details + }=SS, Calls) -> + SS1 = case pqueue4:remove_unique(fun(AgentId1) when AgentId =:= AgentId1 -> + 'true'; + (_) -> + 'false' + end, RRQueue) of + {'true', RRQueue1} -> + lager:info("re-adding agent ~s (prio ~b) to strategy sbrr with skills ~p", [AgentId, -1 * Priority, Skills]), + SS#strategy_state{agents=SBRRSS#{rr_queue := pqueue4:in(AgentId, Priority, RRQueue1) + ,skill_map := update_skill_map_with_agent(AgentId, Skills, SkillMap) + }}; + {'false', RRQueue1} -> + lager:info("adding agent ~s (prio ~b) to strategy sbrr with skills ~p", [AgentId, -1 * Priority, Skills]), + SS#strategy_state{agents=SBRRSS#{rr_queue := pqueue4:in(AgentId, Priority, RRQueue1) + ,skill_map := update_skill_map_with_agent(AgentId, Skills, SkillMap) + } + ,details=incr_agent(AgentId, Details) + } + end, + SS2 = set_flag(AgentId, Flag, SS1), + %% Reseed the map assigning calls to agents since agents changed. + SBRRSS1 = reseed_sbrrss_maps(SS2#strategy_state.agents, ss_size('sbrr', SS2, 'free'), Calls), + SS2#strategy_state{agents=SBRRSS1}; +update_sbrrss_with_agent(AgentId, _Priority, _Skills, 'remove', Flag, SS, Calls) -> + %% In sbrr, the set_flag needs to happen first, as the ss_size controls the + %% MaxAssignments variable in remove_agent + #strategy_state{agents=#{rr_queue := RRQueue}}=SS1 = set_flag(AgentId, Flag, SS), + case lists:member(AgentId, pqueue4:to_list(RRQueue)) of + 'false' -> SS1; + 'true' -> + lager:info("removing agent ~s from strategy sbrr", [AgentId]), + remove_agent('sbrr', AgentId, SS1, Calls) end. --spec add_agent(queue_strategy(), kz_term:ne_binary(), strategy_state()) -> strategy_state(). -add_agent('rr', AgentId, #strategy_state{agents=AgentQueue - ,details=Details - }=SS) -> - SS#strategy_state{agents=queue:in(AgentId, AgentQueue) - ,details=incr_agent(AgentId, Details) - }; -add_agent('mi', AgentId, #strategy_state{agents=AgentL - ,details=Details - }=SS) -> - SS#strategy_state{agents=[AgentId | AgentL] - ,details=incr_agent(AgentId, Details) - }; -add_agent('all', AgentId, #strategy_state{agents=AgentQueue - ,details=Details - }=SS) -> - SS#strategy_state{agents=queue:in(AgentId, AgentQueue) - ,details=incr_agent(AgentId, Details) - }. - -spec remove_agent(queue_strategy(), kz_term:ne_binary(), strategy_state()) -> strategy_state(). -remove_agent('rr', AgentId, #strategy_state{agents=AgentQueue - ,details=Details - }=SS) -> +remove_agent(S, AgentId, #strategy_state{agents=AgentQueue + ,details=Details + }=SS) when S =:= 'rr' + orelse S =:= 'all' -> case dict:find(AgentId, Details) of {'ok', {Count, _}} when Count > 1 -> SS#strategy_state{details=decr_agent(AgentId, Details)}; _ -> - SS#strategy_state{agents=queue:filter(fun(AgentId1) when AgentId =:= AgentId1 -> 'false'; - (_) -> 'true' end - ,AgentQueue - ) + {_, AgentQueue1} = pqueue4:remove_unique(fun(AgentId1) when AgentId1 =:= AgentId -> 'true'; + (_) -> 'false' + end + ,AgentQueue + ), + SS#strategy_state{agents=AgentQueue1 ,details=decr_agent(AgentId, Details) } end; @@ -792,73 +1147,238 @@ remove_agent('mi', AgentId, #strategy_state{agents=AgentL SS#strategy_state{agents=[A || A <- AgentL, A =/= AgentId] ,details=decr_agent(AgentId, Details) } - end; -remove_agent('all', AgentId, #strategy_state{agents=AgentQueue - ,details=Details - }=SS) -> + end. + + +-spec remove_agent('sbrr', kz_term:ne_binary(), strategy_state(), list()) -> strategy_state(). +remove_agent('sbrr', AgentId, #strategy_state{agents=#{rr_queue := RRQueue + ,skill_map := SkillMap + }=SBRRSS + ,details=Details + }=SS, Calls) -> case dict:find(AgentId, Details) of {'ok', {Count, _}} when Count > 1 -> SS#strategy_state{details=decr_agent(AgentId, Details)}; _ -> - SS#strategy_state{agents=queue:filter(fun(AgentId1) when AgentId =:= AgentId1 -> 'false'; - (_) -> 'true' end - ,AgentQueue - ) - ,details=decr_agent(AgentId, Details) - } + {_, RRQueue1} = pqueue4:remove_unique(fun(AgentId1) when AgentId1 =:= AgentId -> 'true'; + (_) -> 'false' + end + ,RRQueue + ), + SS1 = SS#strategy_state{agents=SBRRSS#{rr_queue := RRQueue1 + ,skill_map := remove_agent_from_skill_map(AgentId, SkillMap) + } + ,details=decr_agent(AgentId, Details) + }, + %% Reseed the map assigning calls to agents since agents changed. + SBRRSS1 = reseed_sbrrss_maps(SS1#strategy_state.agents, ss_size('sbrr', SS1, 'free'), Calls), + SS1#strategy_state{agents=SBRRSS1} end. --spec incr_agent(kz_term:ne_binary(), dict:dict(kz_term:ne_binary(), ss_details())) -> - dict:dict(kz_term:ne_binary(), ss_details()). +-spec incr_agent(kz_term:ne_binary(), dict:dict()) -> dict:dict(). incr_agent(AgentId, Details) -> - dict:update(AgentId, fun({Count, Busy}) -> {Count + 1, Busy} end, {1, 'undefined'}, Details). + dict:update(AgentId, fun({Count, Flag}) -> {Count + 1, Flag} end, {1, 'undefined'}, Details). --spec decr_agent(kz_term:ne_binary(), dict:dict(kz_term:ne_binary(), ss_details())) -> - dict:dict(kz_term:ne_binary(), ss_details()). +-spec decr_agent(kz_term:ne_binary(), dict:dict()) -> dict:dict(). decr_agent(AgentId, Details) -> - dict:update(AgentId, fun({Count, Busy}) when Count > 1 -> {Count - 1, Busy}; - ({_, Busy}) -> {0, Busy} end + dict:update(AgentId, fun({Count, Flag}) when Count > 1 -> {Count - 1, Flag}; + ({_, Flag}) -> {0, Flag} end ,{0, 'undefined'}, Details). --spec set_busy(kz_term:ne_binary(), 'busy' | 'undefined', strategy_state()) -> strategy_state(). -set_busy(AgentId, Busy, #strategy_state{details=Details}=SS) -> - SS#strategy_state{details=dict:update(AgentId, fun({Count, _}) -> {Count, Busy} end, {0, Busy}, Details)}. - -maybe_update_strategy('mi', StrategyState, _AgentId) -> StrategyState; -maybe_update_strategy('rr', #strategy_state{agents=AgentQueue}=SS, AgentId) -> - case queue:out(AgentQueue) of - {{'value', AgentId}, AgentQueue1} -> - lager:debug("agent ~s was front of queue, moving", [AgentId]), - SS#strategy_state{agents=queue:in(AgentId, AgentQueue1)}; - _ -> SS +-spec set_flag(kz_term:ne_binary(), 'ringing' | 'busy' | 'undefined', strategy_state()) -> strategy_state(). +set_flag(AgentId, Flag, #strategy_state{details=Details + ,ringing_agents=RingingAgents + ,busy_agents=BusyAgents + }=SS) -> + RingingAgents1 = case Flag of + 'ringing' -> [AgentId | lists:delete(AgentId, RingingAgents)]; + _ -> lists:delete(AgentId, RingingAgents) + end, + BusyAgents1 = case Flag of + 'busy' -> [AgentId | lists:delete(AgentId, BusyAgents)]; + _ -> lists:delete(AgentId, BusyAgents) + end, + SS#strategy_state{details=dict:update(AgentId, fun({Count, _}) -> {Count, Flag} end, {0, Flag}, Details) + ,ringing_agents=RingingAgents1 + ,busy_agents=BusyAgents1 + }. + +-spec update_skill_map_with_agent(kz_term:ne_binary(), kz_term:ne_binaries(), sbrr_skill_map()) -> sbrr_skill_map(). +update_skill_map_with_agent(AgentId, Skills, SkillMap) -> + Combos = skill_combinations(Skills), + lists:foldl(fun(Combo, MapAcc) -> + AgentIds = maps:get(Combo, MapAcc, sets:new()), + MapAcc#{Combo => sets:add_element(AgentId, AgentIds)} + end + ,SkillMap + ,Combos + ). + +-spec remove_agent_from_skill_map(kz_term:ne_binary(), sbrr_skill_map()) -> sbrr_skill_map(). +remove_agent_from_skill_map(AgentId, SkillMap) -> + maps:fold(fun(Combo, AgentIds, MapAcc) -> + AgentIds1 = sets:del_element(AgentId, AgentIds), + case sets:size(AgentIds1) of + 0 -> MapAcc; + _ -> MapAcc#{Combo => AgentIds1} + end + end + ,#{} + ,SkillMap + ). + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Compute all combinations of the list defined by Skills. +%% @end +%%------------------------------------------------------------------------------ +-spec skill_combinations(kz_term:ne_binaries()) -> [kz_term:ne_binaries(), ...]. +skill_combinations(Skills) -> + %% reverse sort skills, so they end up in order after fold + skill_combinations(lists:reverse(lists:sort(Skills)), [[]]). + +-spec skill_combinations(kz_term:ne_binaries(), [kz_term:ne_binaries(), ...]) -> [kz_term:ne_binaries(), ...]. +skill_combinations([], Combos) -> Combos; +skill_combinations([Skill|Skills], Combos) -> + CombosWithSkillPrefix = lists:map(fun(Combo) -> + [Skill | Combo] + end + ,Combos), + Combos1 = Combos ++ CombosWithSkillPrefix, + skill_combinations(Skills, Combos1). + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Only perform a reseed of SBRR map if using that mode. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_reseed_sbrrss_maps(mgr_state()) -> mgr_state(). +maybe_reseed_sbrrss_maps(#state{strategy='sbrr' + ,strategy_state=#strategy_state{agents=SBRRSS}=SS + ,current_member_calls=Calls + }=State) -> + SBRRSS1 = reseed_sbrrss_maps(SBRRSS, ss_size('sbrr', SS, 'free'), Calls), + State#state{strategy_state=SS#strategy_state{agents=SBRRSS1}}; +maybe_reseed_sbrrss_maps(State) -> State. + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Reseed the map assigning calls to agents when using skills-based +%% round robin strategy. The algorithm will try to preserve agents who +%% are needed for lower priority calls with restrictive skill +%% requirements if other agents can pick up less restrictive calls. +%% MaxAssignments causes short-circuit end to assignment if all +%% available agents have been assigned. +%% @end +%%------------------------------------------------------------------------------ +-spec reseed_sbrrss_maps(sbrr_strategy_state(), non_neg_integer(), list()) -> sbrr_strategy_state(). +reseed_sbrrss_maps(SBRRSS, MaxAssignments, Calls) -> + do_reseed_sbrrss_maps(clear_sbrrss_maps(SBRRSS), sets:new(), MaxAssignments, Calls). + +-spec do_reseed_sbrrss_maps(sbrr_strategy_state(), sets:set(), non_neg_integer(), list()) -> sbrr_strategy_state(). +do_reseed_sbrrss_maps(SBRRSS, _, 0, _) -> + %% No more agents to assign, quit early + SBRRSS; +do_reseed_sbrrss_maps(SBRRSS, _, _, []) -> + %% No more calls to assign, quit early + SBRRSS; +do_reseed_sbrrss_maps(#{skill_map := SkillMap}=SBRRSS, AssignedAgentIds, MaxAssignments, [{_, Call}|OtherCalls]) -> + %% Assumption: the call's required skills are sorted, as done in add_queue_member + Skills = kapps_call:kvs_fetch(?ACDC_REQUIRED_SKILLS_KEY, [], Call), + case maps:is_key(Skills, SkillMap) of + 'true' -> + Candidates = sets:subtract(maps:get(Skills, SkillMap), AssignedAgentIds), + case sbrrss_maybe_assign_agent(SBRRSS, Candidates, Call, OtherCalls) of + {SBRRSS1, 'undefined'} -> + do_reseed_sbrrss_maps(SBRRSS1, AssignedAgentIds, MaxAssignments, OtherCalls); + {SBRRSS1, AgentId} -> + do_reseed_sbrrss_maps(SBRRSS1, sets:add_element(AgentId, AssignedAgentIds), MaxAssignments - 1, OtherCalls) + end; + 'false' -> + do_reseed_sbrrss_maps(SBRRSS, AssignedAgentIds, MaxAssignments, OtherCalls) + end. + +-spec sbrrss_maybe_assign_agent(sbrr_strategy_state(), sets:set(), kapps_call:call(), list()) -> + {sbrr_strategy_state(), api_kz_term:ne_binary()}. +sbrrss_maybe_assign_agent(#{agent_id_map := AgentIdMap + ,call_id_map := CallIdMap + }=SBRRSS, Candidates, Call, OtherCalls) -> + case sbrrss_assign_agent(SBRRSS, Candidates, Call, OtherCalls) of + 'undefined' -> {SBRRSS, 'undefined'}; + AgentId -> + CallId = kapps_call:call_id(Call), + {SBRRSS#{agent_id_map := AgentIdMap#{AgentId => CallId} + ,call_id_map := CallIdMap#{CallId => AgentId} + }, AgentId} + end. + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Performs the actual assignment of calls to agents while trying to +%% preserve agents who would be better suited to later calls than +%% "Call". +%% @end +%%------------------------------------------------------------------------------ +-spec sbrrss_assign_agent(sbrr_strategy_state(), sets:set(), kapps_call:call(), list()) -> api_kz_term:ne_binary(). +sbrrss_assign_agent(#{skill_map := SkillMap}=SBRRSS, Candidates, Call, [{_, OtherCall}|OtherCalls]) -> + case sets:size(Candidates) of + 0 -> + %% There is no agent remaining with skills for this call + 'undefined'; + 1 -> + %% Unique ideal candidate for this call was found. Assign them + lists:nth(1, sets:to_list(Candidates)); + _ -> + OtherCallSkills = kapps_call:kvs_fetch(?ACDC_REQUIRED_SKILLS_KEY, [], OtherCall), + Candidates1 = sets:subtract(Candidates, maps:get(OtherCallSkills, SkillMap, sets:new())), + ignore_empty_candidates(SBRRSS, Candidates, Candidates1, Call, OtherCalls) end; -maybe_update_strategy('all', #strategy_state{agents=AgentQueue}=SS, AgentId) -> - case queue:out(AgentQueue) of - {{'value', AgentId}, AgentQueue1} -> - lager:debug("agent ~s was front of queue, moving", [AgentId]), - SS#strategy_state{agents=queue:in(AgentId, AgentQueue1)}; - _ -> SS +sbrrss_assign_agent(#{rr_queue := RRQueue}, Candidates, _, []) -> + case sets:size(Candidates) of + 0 -> + %% There is no agent remaining with skills for this call + 'undefined'; + _ -> + %% Multiple candidate options, find the first candidate that is in RRQueue + RRQueueCandidates = pqueue4:filter(fun(AgentId) -> sets:is_element(AgentId, Candidates) end, RRQueue), + %% Assumption: RRQueue should never be empty as every agent should be in it + %% We won't update RRQueue, but instead add this winner to AssignedAgentIds + {{'value', AgentId}, _} = pqueue4:out(RRQueueCandidates), + AgentId end. +ignore_empty_candidates(SBRRSS, Candidates, Candidates1, Call, OtherCalls) -> + Candidates2 = case sets:size(Candidates1) of + %% When empty, we forget this subtraction, because this call should + %% take precedence over OtherCall (they both have similar requirements) + 0 -> Candidates; + %% Multiple options remain, continue + _ -> Candidates1 + end, + sbrrss_assign_agent(SBRRSS, Candidates2, Call, OtherCalls). + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Remove all assignments of calls to agents (and vise-versa) in +%% skills-based round robin strategy state. +%% @end +%%------------------------------------------------------------------------------ +-spec clear_sbrrss_maps(sbrr_strategy_state()) -> sbrr_strategy_state(). +clear_sbrrss_maps(SBRRSS) -> + SBRRSS#{agent_id_map := #{} + ,call_id_map := #{} + }. + %% If A's idle time is greater, it should come before B -spec sort_agent(kz_json:object(), kz_json:object()) -> boolean(). sort_agent(A, B) -> - kz_json:get_integer_value(<<"Idle-Time">>, A, 0) > - kz_json:get_integer_value(<<"Idle-Time">>, B, 0). - -%% Handle when an agent process has responded to the connect_req -%% but then the agent logs out of their phone (removing the agent -%% from the list in the queue manager). -%% Otherwise CRs will never be empty --spec remove_unknown_agents(pid(), kz_json:objects()) -> kz_json:objects(). -remove_unknown_agents(Mgr, CRs) -> - case gen_listener:call(Mgr, 'current_agents') of - [] -> []; - Agents -> - [CR || CR <- CRs, - lists:member(kz_json:get_value(<<"Agent-ID">>, CR), Agents) - ] - end. + sort_agent2(kz_json:get_integer_value(<<"Idle-Time">>, A) + ,kz_json:get_integer_value(<<"Idle-Time">>, B)). + +-spec sort_agent2(kz_term:api_integer(), kz_term:api_integer()) -> boolean(). +sort_agent2('undefined', _) -> 'true'; +sort_agent2(_, 'undefined') -> 'false'; +sort_agent2(A, B) -> A > B. -spec split_agents(kz_term:ne_binary(), kz_json:objects()) -> {kz_json:objects(), kz_json:objects()}. @@ -870,41 +1390,62 @@ split_agents(AgentId, Rest) -> -spec get_strategy(kz_term:api_binary()) -> queue_strategy(). get_strategy(<<"round_robin">>) -> 'rr'; get_strategy(<<"most_idle">>) -> 'mi'; +get_strategy(<<"skills_based_round_robin">>) -> 'sbrr'; get_strategy(<<"ring_all">>) -> 'all'; get_strategy(_) -> 'rr'. --spec create_strategy_state(queue_strategy(), kz_term:ne_binary(), kz_term:ne_binary()) -> strategy_state(). -create_strategy_state(Strategy, AcctDb, QueueId) -> - create_strategy_state(Strategy, #strategy_state{}, AcctDb, QueueId). - --spec create_strategy_state(queue_strategy(), strategy_state(), kz_term:ne_binary(), kz_term:ne_binary()) -> strategy_state(). -create_strategy_state('rr', #strategy_state{agents='undefined'}=SS, AcctDb, QueueId) -> - create_strategy_state('rr', SS#strategy_state{agents=queue:new()}, AcctDb, QueueId); -create_strategy_state('rr', #strategy_state{agents=AgentQ}=SS, AcctDb, QueueId) -> - case acdc_util:agents_in_queue(AcctDb, QueueId) of +-spec create_strategy_state(queue_strategy() + ,kz_term:ne_binary(), kz_term:ne_binary() + ) -> strategy_state(). +create_strategy_state(Strategy, AccountDb, QueueId) -> + create_strategy_state(Strategy, #strategy_state{}, AccountDb, QueueId). + +-spec create_strategy_state(queue_strategy() + ,strategy_state() + ,kz_term:ne_binary(), kz_term:ne_binary() + ) -> strategy_state(). +create_strategy_state(S, #strategy_state{agents='undefined'}=SS, AccountDb, QueueId) when S =:= 'rr' + orelse S =:= 'all' -> + create_strategy_state(S, SS#strategy_state{agents=pqueue4:new()}, AccountDb, QueueId); +create_strategy_state(S, #strategy_state{agents=AgentQ}=SS, AccountDb, QueueId) when S =:= 'rr' + orelse S =:= 'all' -> + case acdc_util:agents_in_queue(AccountDb, QueueId) of [] -> lager:debug("no agents around"), SS; + {'error', _E} -> lager:debug("error creating strategy rr: ~p", [_E]), SS; JObjs -> - Q = queue:from_list([Id || JObj <- JObjs, - not queue:member((Id = kz_doc:id(JObj)), AgentQ) - ]), + AgentMap = lists:map(fun(JObj) -> + {kz_doc:id(JObj) + ,-1 * kz_json:get_integer_value([<<"value">>, <<"agent_priority">>], JObj, 0) + } + end, JObjs), + Q1 = lists:foldl(fun({AgentId, Priority}, Q) -> + lager:info("adding agent ~s (prio ~b) to strategy rr", [AgentId, -1 * Priority]), + pqueue4:in(AgentId, Priority, Q) + end + ,AgentQ + ,lists:usort(AgentMap) + ), Details = lists:foldl(fun(JObj, Acc) -> dict:store(kz_doc:id(JObj), {1, 'undefined'}, Acc) end, dict:new(), JObjs), - SS#strategy_state{agents=queue:join(AgentQ, Q) + SS#strategy_state{agents=Q1 ,details=Details } end; -create_strategy_state('mi', #strategy_state{agents='undefined'}=SS, AcctDb, QueueId) -> - create_strategy_state('mi', SS#strategy_state{agents=[]}, AcctDb, QueueId); -create_strategy_state('mi', #strategy_state{agents=AgentL}=SS, AcctDb, QueueId) -> - case acdc_util:agents_in_queue(AcctDb, QueueId) of +create_strategy_state('mi', #strategy_state{agents='undefined'}=SS, AccountDb, QueueId) -> + create_strategy_state('mi', SS#strategy_state{agents=[]}, AccountDb, QueueId); +create_strategy_state('mi', #strategy_state{agents=AgentL}=SS, AccountDb, QueueId) -> + case acdc_util:agents_in_queue(AccountDb, QueueId) of [] -> lager:debug("no agents around"), SS; + {'error', _E} -> lager:debug("error creating strategy mi: ~p", [_E]), SS; JObjs -> AgentL1 = lists:foldl(fun(JObj, Acc) -> - Id = kz_doc:id(JObj), - case lists:member(Id, Acc) of + AgentId = kz_doc:id(JObj), + case lists:member(AgentId, Acc) of 'true' -> Acc; - 'false' -> [Id | Acc] + 'false' -> + lager:info("adding agent ~s to strategy mi", [AgentId]), + [AgentId | Acc] end end, AgentL, JObjs), Details = lists:foldl(fun(JObj, Acc) -> @@ -914,87 +1455,99 @@ create_strategy_state('mi', #strategy_state{agents=AgentL}=SS, AcctDb, QueueId) ,details=Details } end; -create_strategy_state('all', #strategy_state{agents='undefined'}=SS, AcctDb, QueueId) -> - create_strategy_state('all', SS#strategy_state{agents=queue:new()}, AcctDb, QueueId); -create_strategy_state('all', #strategy_state{agents=AgentQ}=SS, AcctDb, QueueId) -> - case acdc_util:agents_in_queue(AcctDb, QueueId) of +create_strategy_state('sbrr', #strategy_state{agents='undefined'}=SS, AccountDb, QueueId) -> + SBRRStrategyState = #{agent_id_map => #{} + ,call_id_map => #{} + ,rr_queue => pqueue4:new() + ,skill_map => #{} + }, + create_strategy_state('sbrr', SS#strategy_state{agents=SBRRStrategyState}, AccountDb, QueueId); +create_strategy_state('sbrr', SS, AccountDb, QueueId) -> + case acdc_util:agents_in_queue(AccountDb, QueueId) of [] -> lager:debug("no agents around"), SS; - JObjs -> - Q = queue:from_list([Id || JObj <- JObjs, - not queue:member((Id = kz_doc:id(JObj)), AgentQ) - ]), - Details = lists:foldl(fun(JObj, Acc) -> - dict:store(kz_doc:id(JObj), {1, 'undefined'}, Acc) - end, dict:new(), JObjs), - SS#strategy_state{agents=queue:join(AgentQ, Q) - ,details=Details - } + {'error', _E} -> lager:debug("error creating strategy mi: ~p", [_E]), SS; + JObjs -> lists:foldl(fun update_sbrrss_with_agent/2, SS, JObjs) end. -update_strategy_state(Srv, 'rr', #strategy_state{agents=AgentQueue}) -> - L = queue:to_list(AgentQueue), +update_strategy_state(Srv, S, #strategy_state{agents=AgentQueue}) when S =:= 'rr' + orelse S =:= 'all' -> + L = pqueue4:to_list(AgentQueue), update_strategy_state(Srv, L); update_strategy_state(Srv, 'mi', #strategy_state{agents=AgentL}) -> update_strategy_state(Srv, AgentL); -update_strategy_state(Srv, 'all', #strategy_state{agents=AgentQueue}) -> - L = queue:to_list(AgentQueue), +update_strategy_state(Srv, 'sbrr', #strategy_state{agents=#{rr_queue := RRQueue}}) -> + L = pqueue4:to_list(RRQueue), update_strategy_state(Srv, L). update_strategy_state(Srv, L) -> [gen_listener:cast(Srv, {'sync_with_agent', A}) || A <- L]. --spec call_position(kz_term:ne_binary(), [kapps_call:call()]) -> kz_term:api_integer(). -call_position(CallId, Calls) -> - call_position(CallId, Calls, 1). - --spec call_position(kz_term:ne_binary(), [kapps_call:call()], pos_integer()) -> pos_integer(). -call_position(_, [], _) -> - 'undefined'; -call_position(CallId, [Call|Calls], Position) -> - case kapps_call:call_id(Call) of - CallId -> Position; - _ -> call_position(CallId, Calls, Position + 1) - end. - --spec ss_size(strategy_state(), 'free' | 'logged_in') -> integer(). -ss_size(#strategy_state{agents=Agents}, 'logged_in') -> - case queue:is_queue(Agents) of - 'true' -> queue:len(Agents); - 'false' -> length(Agents) - end; -ss_size(#strategy_state{agents=Agents - ,details=Details - }, 'free') when is_list(Agents) -> +-spec ss_size(queue_strategy(), strategy_state(), 'free' | 'logged_in') -> integer(). +ss_size(Strategy, #strategy_state{agents=Agents + ,ringing_agents=RingingAgents + ,busy_agents=BusyAgents + }, 'logged_in') -> + case Strategy of + 'rr' -> pqueue4:len(Agents); + 'mi' -> length(Agents); + 'sbrr' -> pqueue4:len(maps:get('rr_queue', Agents, [])); + 'all' -> pqueue4:len(Agents) + end + length(RingingAgents) + length(BusyAgents); +ss_size('rr', #strategy_state{agents=Agents}=SS, 'free') -> + ss_size('mi', SS#strategy_state{agents=pqueue4:to_list(Agents)}, 'free'); +ss_size('all', #strategy_state{agents=Agents}=SS, 'free') -> + ss_size('mi', SS#strategy_state{agents=pqueue4:to_list(Agents)}, 'free'); +ss_size('mi', #strategy_state{agents=Agents + ,details=Details + ,ringing_agents=RingingAgents + }, 'free') -> lists:foldl(fun(AgentId, Count) -> case dict:find(AgentId, Details) of {'ok', {ProcCount, 'undefined'}} when ProcCount > 0 -> Count + 1; _ -> Count end - end, 0, Agents); -ss_size(#strategy_state{agents=Agents}=SS, 'free') -> - ss_size(SS#strategy_state{agents=queue:to_list(Agents)}, 'free'). + end, 0, Agents) + length(RingingAgents); +ss_size('sbrr', #strategy_state{agents=#{rr_queue := RRQueue}}=SS, 'free') -> + ss_size('mi', SS#strategy_state{agents=pqueue4:to_list(RRQueue)}, 'free'). -maybe_start_queue_workers(QueueSup, AgentCount) -> +maybe_start_queue_workers(QueueSup, Count) -> WSup = acdc_queue_sup:workers_sup(QueueSup), case acdc_queue_workers_sup:worker_count(WSup) of - N when N >= AgentCount -> 'ok'; - N when N < AgentCount -> gen_listener:cast(self(), {'start_worker', AgentCount-N}) + N when N >= Count -> 'ok'; + N when N < Count -> gen_listener:cast(self(), {'start_worker', Count - N}) end. -spec update_properties(kz_json:object(), mgr_state()) -> mgr_state(). update_properties(QueueJObj, State) -> - State#state{enter_when_empty=kz_json:is_true(<<"enter_when_empty">>, QueueJObj, 'true') - ,moh=kz_json:get_ne_value(<<"moh">>, QueueJObj) - ,announcements_config=announcements_config(QueueJObj) - }. + State#state{ + enter_when_empty=kz_json:is_true(<<"enter_when_empty">>, QueueJObj, 'true') + ,moh=kz_json:get_ne_value(<<"moh">>, QueueJObj) + ,announcements_config=announcements_config(QueueJObj) + }. -spec announcements_config(kz_json:object()) -> kz_term:proplist(). announcements_config(Config) -> - kz_json:recursive_to_proplist( - kz_json:get_json_value(<<"announcements">>, Config, kz_json:new())). + AC = + kz_json:recursive_to_proplist( + kz_json:get_json_value(<<"announcements">>, Config, kz_json:new())), + props:set_value(<<"moh">>, kz_json:get_value(<<"moh">>, Config), AC). + +-spec maybe_schedule_position_announcements(kz_json:object(), kapps_call:call(), mgr_state()) -> mgr_state(). +maybe_schedule_position_announcements(JObj, Call, #state{announcements_config=AnnouncementsConfig + ,announcements_pids=AnnouncementsPids + }=State) -> + EnterAsCallback = kz_json:is_true(<<"Enter-As-Callback">>, JObj), + case not EnterAsCallback + andalso acdc_announcements_sup:maybe_start_announcements(self(), Call, AnnouncementsConfig) + of + 'false' -> State; + {'ok', Pid} -> + CallId = kapps_call:call_id(Call), + State#state{announcements_pids=AnnouncementsPids#{CallId => Pid}} + end. --spec cancel_position_announcements(kapps_call:call() | 'false', map()) -> +-spec cancel_position_announcements(kapps_call:call() | 'undefined', map()) -> map(). -cancel_position_announcements('false', Pids) -> Pids; +cancel_position_announcements('undefined', Pids) -> Pids; cancel_position_announcements(Call, Pids) -> CallId = kapps_call:call_id(Call), case catch maps:get(CallId, Pids) of @@ -1017,14 +1570,67 @@ cancel_position_announcements(Call, Pids) -> Pids1 end. --spec remove_queue_member(kz_term:api_binary(), mgr_state()) -> mgr_state(). -remove_queue_member(CallId, #state{current_member_calls=CurrentCalls - ,announcements_pids=AnnouncementsPids - }=State) -> - lager:debug("removing call id ~s", [CallId]), - - AnnouncementsPids1 = cancel_position_announcements(lists:keyfind(CallId, 2, CurrentCalls), AnnouncementsPids), +-spec publish_call_exited_position(kz_term:ne_binary(), pos_integer(), mgr_state()) -> 'ok'. +publish_call_exited_position(CallId, Position, #state{account_id=AccountId + ,queue_id=QueueId + }) -> + Prop = [{<<"Account-ID">>, AccountId} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Call-ID">>, CallId} + ,{<<"Exited-Position">>, Position} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_stats:publish_call_exited_position(Prop). + +-spec maybe_add_queue_member_as_callback(kz_json:object(), kapps_call:call(), mgr_state()) -> mgr_state(). +maybe_add_queue_member_as_callback(JObj, Call, #state{account_id=AccountId + ,queue_id=QueueId + ,current_member_calls=Calls + ,registered_callbacks=RegCallbacks + }=State) -> + EnterAsCallback = kz_json:is_true(<<"Enter-As-Callback">>, JObj), + case EnterAsCallback of + 'false' -> State; + 'true' -> + CallId = kapps_call:call_id(Call), + lager:debug("call ~s marked as callback", [CallId]), + Number = kz_json:get_ne_binary_value(<<"Callback-Number">>, JObj), + Priority = kz_json:get_integer_value(<<"Member-Priority">>, JObj), + Position = queue_member_position(CallId, Calls), + Call1 = callback_flag(AccountId, QueueId, Call), + CIDPrepend = kapps_call:kvs_fetch('prepend_cid_name', Call1), + + State1 = State#state{registered_callbacks=props:set_value(CallId, {Number, CIDPrepend}, RegCallbacks)}, + add_queue_member(Call, Priority, Position, State1) + end. - State#state{current_member_calls=lists:keydelete(CallId, 2, CurrentCalls) - ,announcements_pids=AnnouncementsPids1 - }. +%%------------------------------------------------------------------------------ +%% @private +%% @doc Prepend CB: onto CID of callback calls and flag call ID as callback +%% in acdc_stats +%% +%% @end +%%------------------------------------------------------------------------------ +-spec callback_flag(kz_term:ne_binary(), kz_term:ne_binary(), kapps_call:call()) -> + kapps_call:call(). +callback_flag(AccountId, QueueId, Call) -> + Call1 = prepend_cid_name(<<"CB:">>, Call), + {_, CIDName} = acdc_util:caller_id(Call1), + _ = acdc_stats:call_marked_callback(AccountId + ,QueueId + ,kapps_call:call_id(Call) + ,CIDName + ), + Call1. + +-spec prepend_cid_name(kz_term:ne_binary(), kapps_call:call()) -> kapps_call:call(). +prepend_cid_name(Prefix, Call) -> + Prefix1 = case kapps_call:kvs_fetch('prepend_cid_name', Call) of + 'undefined' -> Prefix; + Prepend -> <> + end, + kapps_call:kvs_store('prepend_cid_name', Prefix1, Call). + +-spec maybe_remove_callback_reg(kz_term:ne_binary(), mgr_state()) -> mgr_state(). +maybe_remove_callback_reg(CallId, #state{registered_callbacks=RegCallbacks}=State) -> + State#state{registered_callbacks=lists:keydelete(CallId, 1, RegCallbacks)}. diff --git a/applications/acdc/src/acdc_queue_manager.hrl b/applications/acdc/src/acdc_queue_manager.hrl index 5c3b16ea68d..4a992963b53 100644 --- a/applications/acdc/src/acdc_queue_manager.hrl +++ b/applications/acdc/src/acdc_queue_manager.hrl @@ -2,30 +2,42 @@ %% rr :: Round Robin %% mi :: Most Idle --type queue_strategy() :: 'rr' | 'mi' | 'all'. +-type queue_strategy() :: 'rr' | 'mi' | 'sbrr' | 'all'. --type queue_strategy_state() :: queue:queue() | kz_term:ne_binaries(). --type ss_details() :: {non_neg_integer(), 'busy' | 'undefined'}. --record(strategy_state, {agents :: queue_strategy_state() | 'undefined' +-type sbrr_skill_map() :: #{kz_term:ne_binaries() := sets:set()}. % skill keys must be alphabetically sorted +-type sbrr_id_map() :: #{kz_term:ne_binary() := kz_term:ne_binary()}. +-type sbrr_strategy_state() :: #{agent_id_map := sbrr_id_map() % maps agent IDs to assigned call IDs + ,call_id_map := sbrr_id_map() % maps assigned call IDs to agent IDs + ,rr_queue := pqueue4:queue() + ,skill_map := sbrr_skill_map() + }. + +-type queue_strategy_state() :: pqueue4:queue() | kz_term:ne_binaries() | sbrr_strategy_state(). +-type ss_details() :: {non_neg_integer(), 'ringing' | 'busy' | 'undefined'}. +-record(strategy_state, {agents :: queue_strategy_state() %% details include # of agent processes and availability - ,details = dict:new() :: dict:dict(kz_term:ne_binary(), ss_details()) + ,details = dict:new() :: dict:dict() + ,ringing_agents = [] :: kz_term:ne_binaries() + ,busy_agents = [] :: kz_term:ne_binaries() }). -type strategy_state() :: #strategy_state{}. -record(state, {ignored_member_calls = dict:new() :: dict:dict() - ,account_id :: kz_term:api_ne_binary() - ,queue_id :: kz_term:api_ne_binary() + ,account_id :: api_kz_term:ne_binary() + ,queue_id :: api_kz_term:ne_binary() ,supervisor :: kz_term:api_pid() ,strategy = 'rr' :: queue_strategy() % round-robin | most-idle ,strategy_state = #strategy_state{} :: strategy_state() % based on the strategy - ,known_agents = dict:new() :: dict:dict() % how many agent processes are available {AgentId, Count} ,enter_when_empty = 'true' :: boolean() % allow caller into queue if no agents are logged in - ,moh :: kz_term:api_ne_binary() + ,moh :: api_kz_term:ne_binary() ,current_member_calls = [] :: list() % ordered list of current members waiting ,announcements_config = [] :: kz_term:proplist() - ,announcements_pids = #{} :: announcements_pids() + ,announcements_pids = #{} :: announcements_kz_term:pids() + ,registered_callbacks = [] :: list() }). -type mgr_state() :: #state{}. +-define(ACDC_REQUIRED_SKILLS_KEY, 'acdc_required_skills'). + -define(ACDC_QUEUE_MANAGER_HRL, 'true'). -endif. diff --git a/applications/acdc/src/acdc_queue_shared.erl b/applications/acdc/src/acdc_queue_shared.erl index 66355990cea..b8bf38d22bf 100644 --- a/applications/acdc/src/acdc_queue_shared.erl +++ b/applications/acdc/src/acdc_queue_shared.erl @@ -2,8 +2,7 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC -%%% +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -34,7 +33,7 @@ -define(SERVER, ?MODULE). --record(state, {fsm_pid :: kz_term:api_pid() +-record(state, {fsm_pid :: pid() | undefined ,deliveries = [] :: deliveries() }). -type state() :: #state{}. @@ -54,7 +53,7 @@ } ]). --define(SHARED_QUEUE_BINDINGS(AcctId, QueueId), [{'self', []}]). +-define(SHARED_QUEUE_BINDINGS(AccountId, QueueId), [{'self', []}]). -define(RESPONDERS, [{{'acdc_queue_handler', 'handle_member_call'} ,[{<<"member">>, <<"call">>}] @@ -66,13 +65,12 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the server. +%% @doc Starts the server %% @end %%------------------------------------------------------------------------------ --spec start_link(pid(), pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:startlink_ret(). +-spec start_link(kz_term:server_ref(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_integer()) -> kz_term:startlink_ret(). start_link(WorkerSup, _, AccountId, QueueId) -> - {'ok', QueueJObj} = kz_datamgr:open_cache_doc(kzs_util:format_account_db(AccountId), QueueId), - Priority = kz_json:get_integer_value(<<"max_priority">>, QueueJObj), + Priority = acdc_util:max_priority(kzs_util:format_account_db(AccountId), QueueId), gen_listener:start_link(?SERVER ,[{'bindings', ?SHARED_QUEUE_BINDINGS(AccountId, QueueId)} ,{'responders', ?RESPONDERS} @@ -82,17 +80,18 @@ start_link(WorkerSup, _, AccountId, QueueId) -> ,[WorkerSup] ). --spec ack(kz_types:server_ref(), gen_listener:basic_deliver()) -> 'ok'. + +-spec ack(kz_term:server_ref(), gen_listener:basic_deliver()) -> 'ok'. ack(Srv, Delivery) -> gen_listener:ack(Srv, Delivery), gen_listener:cast(Srv, {'ack', Delivery}). --spec nack(kz_types:server_ref(), gen_listener:basic_deliver()) -> 'ok'. +-spec nack(kz_term:server_ref(), gen_listener:basic_deliver()) -> 'ok'. nack(Srv, Delivery) -> gen_listener:nack(Srv, Delivery), gen_listener:cast(Srv, {'noack', Delivery}). --spec deliveries(kz_types:server_ref()) -> deliveries(). +-spec deliveries(kz_term:server_ref()) -> deliveries(). deliveries(Srv) -> gen_listener:call(Srv, 'deliveries'). @@ -101,7 +100,9 @@ deliveries(Srv) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Initializes the server. +%% @private +%% @doc Initializes the server +%% %% @end %%------------------------------------------------------------------------------ -spec init([pid()]) -> {'ok', state()}. @@ -112,21 +113,26 @@ init([WorkerSup]) -> gen_listener:cast(self(), {'get_fsm_proc', WorkerSup}), {'ok', #state{}}. + %%------------------------------------------------------------------------------ -%% @doc Handling call messages. +%% @private +%% @doc Handling call messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_term:handle_call_ret_state(state()). handle_call('deliveries', _From, #state{deliveries=Ds}=State) -> {'reply', Ds, State}; handle_call(_Request, _From, State) -> {'noreply', State}. %%------------------------------------------------------------------------------ -%% @doc Handling cast messages. +%% @private +%% @doc Handling cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). +-spec handle_cast(any(), state()) -> kz_term:handle_cast_ret_state(state()). handle_cast({'get_fsm_proc', WorkerSup}, State) -> FSMPid = acdc_queue_worker_sup:fsm(WorkerSup), lager:debug("sending messages to FSM ~p", [FSMPid]), @@ -146,10 +152,12 @@ handle_cast(_Msg, State) -> {'noreply', State}. %%------------------------------------------------------------------------------ -%% @doc Handling all non call/cast messages. +%% @private +%% @doc Handling all non call/cast messages +%% %% @end %%------------------------------------------------------------------------------ --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +-spec handle_info(any(), state()) -> kz_term:handle_info_ret_state(state()). handle_info({'basic.cancel',_,'true'}, State) -> lager:debug("recv basic.cancel...no!!!"), {'noreply', State}; @@ -158,7 +166,9 @@ handle_info(_Info, State) -> {'noreply', State}. %%------------------------------------------------------------------------------ +%% @private %% @doc Handling all messages from the message bus +%% %% @end %%------------------------------------------------------------------------------ -spec handle_event(kz_json:object(), state()) -> gen_listener:handle_event_return(). @@ -166,9 +176,10 @@ handle_event(_JObj, #state{fsm_pid=FSM}) -> {'reply', [{'fsm_pid', FSM}]}. %%------------------------------------------------------------------------------ -%% @doc This function is called by a `gen_server' when it is about to -%% terminate. It should be the opposite of `Module:init/1' and do any -%% necessary cleaning up. When it returns, the `gen_server' terminates +%% @private +%% @doc This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any +%% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @end @@ -179,7 +190,9 @@ terminate(_Reason, #state{deliveries=Ds}) -> lager:debug("acdc_queue_shared terminating: ~p", [_Reason]). %%------------------------------------------------------------------------------ -%% @doc Convert process state when code is changed. +%% @private +%% @doc Convert process state when code is changed +%% %% @end %%------------------------------------------------------------------------------ -spec code_change(any(), state(), any()) -> {'ok', state()}. diff --git a/applications/acdc/src/acdc_queue_sup.erl b/applications/acdc/src/acdc_queue_sup.erl index 68de506733c..0b4391f43ed 100644 --- a/applications/acdc/src/acdc_queue_sup.erl +++ b/applications/acdc/src/acdc_queue_sup.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -36,12 +35,12 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the supervisor. +%% @doc Starts the supervisor %% @end %%------------------------------------------------------------------------------ --spec start_link(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:startlink_ret(). -start_link(AcctId, QueueId) -> - supervisor:start_link(?SERVER, [AcctId, QueueId]). +-spec start_link(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:startlink_ret(). +start_link(AccountId, QueueId) -> + supervisor:start_link(?SERVER, [AccountId, QueueId]). -spec stop(pid()) -> 'ok' | {'error', 'not_found'}. stop(Super) -> @@ -60,17 +59,29 @@ status(Supervisor) -> Manager = manager(Supervisor), WorkersSup = workers_sup(Supervisor), - {AcctId, QueueId} = acdc_queue_manager:config(Manager), + {AccountId, QueueId} = acdc_queue_manager:config(Manager), - ?PRINT("Queue ~s (Account ~s)", [QueueId, AcctId]), + ?PRINT("Queue ~s (Account ~s)", [QueueId, AccountId]), ?PRINT(" Supervisor: ~p", [Supervisor]), ?PRINT(" Manager: ~p", [Manager]), - ?PRINT(" Known Agents:"), - _ = case acdc_queue_manager:status(Manager) of + {Available, Busy} = acdc_queue_manager:status(Manager), + ?PRINT(" Available Agents: (Total : ~p) ", [length(Available)]), + _ = case Available of [] -> ?PRINT(" NONE"); As -> [?PRINT(" ~s", [A]) || A <- As] end, + ?PRINT(" Busy Agents: (Total : ~p) ", [length(Busy)]), + _ = case Busy of + [] -> ?PRINT(" NONE"); + Bs -> [?PRINT(" ~s", [B]) || B <- Bs] + end, + Queued_calls = acdc_queue_manager:calls(Manager), + ?PRINT(" Queued Calls: (Total : ~p) ", [length(Queued_calls)]), + _ = case Queued_calls of + [] -> ?PRINT(" NONE"); + Cs -> [?PRINT(" ~s", [C]) || C <- Cs] + end, _ = acdc_queue_workers_sup:status(WorkersSup), 'ok'. @@ -80,7 +91,8 @@ status(Supervisor) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a supervisor is started using `supervisor:start_link/[2,3]', +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], %% this function is called by the new process to find out about %% restart strategy, maximum restart frequency and child %% specifications. diff --git a/applications/acdc/src/acdc_queue_thief.erl b/applications/acdc/src/acdc_queue_thief.erl index 1034d597ff1..5c425792ac8 100644 --- a/applications/acdc/src/acdc_queue_thief.erl +++ b/applications/acdc/src/acdc_queue_thief.erl @@ -3,6 +3,7 @@ %%% @doc Caller calls in and gets the first available call from the queue %%% @author James Aimonetti %%% +%%% @author James Aimonetti %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/applications/acdc/src/acdc_queue_worker_sup.erl b/applications/acdc/src/acdc_queue_worker_sup.erl index 626b7b664c8..d9d4d2cde89 100644 --- a/applications/acdc/src/acdc_queue_worker_sup.erl +++ b/applications/acdc/src/acdc_queue_worker_sup.erl @@ -2,8 +2,7 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC -%%% +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -39,12 +38,12 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the supervisor. +%% @doc Starts the supervisor %% @end %%------------------------------------------------------------------------------ --spec start_link(pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:startlink_ret(). -start_link(MgrPid, AcctId, QueueId) -> - supervisor:start_link(?SERVER, [MgrPid, AcctId, QueueId]). +-spec start_link(pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:startlink_ret(). +start_link(MgrPid, AccountId, QueueId) -> + supervisor:start_link(?SERVER, [MgrPid, AccountId, QueueId]). -spec stop(pid()) -> 'ok' | {'error', 'not_found'}. stop(WorkerSup) -> supervisor:terminate_child('acdc_queues_sup', WorkerSup). @@ -103,7 +102,8 @@ print_status([{K, V}|T]) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a supervisor is started using `supervisor:start_link/[2,3]', +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], %% this function is called by the new process to find out about %% restart strategy, maximum restart frequency and child %% specifications. diff --git a/applications/acdc/src/acdc_queue_workers_sup.erl b/applications/acdc/src/acdc_queue_workers_sup.erl index 1c7257993ea..9cacecab8e0 100644 --- a/applications/acdc/src/acdc_queue_workers_sup.erl +++ b/applications/acdc/src/acdc_queue_workers_sup.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -34,22 +33,22 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the supervisor. +%% @doc Starts the supervisor %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> supervisor:start_link(?SERVER, []). -spec new_worker(pid(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -new_worker(WorkersSup, AcctId, QueueId) -> - new_workers(WorkersSup, AcctId, QueueId, 1). +new_worker(WorkersSup, AccountId, QueueId) -> + new_workers(WorkersSup, AccountId, QueueId, 1). -spec new_workers(pid(), kz_term:ne_binary(), kz_term:ne_binary(), integer()) -> 'ok'. new_workers(_, _,_,N) when N =< 0 -> 'ok'; -new_workers(WorkersSup, AcctId, QueueId, N) when is_integer(N) -> - _ = supervisor:start_child(WorkersSup, [self(), AcctId, QueueId]), - new_workers(WorkersSup, AcctId, QueueId, N-1). +new_workers(WorkersSup, AccountId, QueueId, N) when is_integer(N) -> + _ = supervisor:start_child(WorkersSup, [self(), AccountId, QueueId]), + new_workers(WorkersSup, AccountId, QueueId, N-1). -spec workers(pid()) -> kz_term:pids(). workers(Super) -> @@ -68,7 +67,8 @@ status(Super) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a supervisor is started using `supervisor:start_link/[2,3]', +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], %% this function is called by the new process to find out about %% restart strategy, maximum restart frequency and child %% specifications. diff --git a/applications/acdc/src/acdc_queues_sup.erl b/applications/acdc/src/acdc_queues_sup.erl index 70b2d3fc4bc..e6f7ae20e86 100644 --- a/applications/acdc/src/acdc_queues_sup.erl +++ b/applications/acdc/src/acdc_queues_sup.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -37,18 +36,18 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the supervisor. +%% @doc Starts the supervisor %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> supervisor:start_link({'local', ?SERVER}, ?MODULE, []). --spec new(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_types:startlink_ret(). -new(AcctId, QueueId) -> - case find_queue_supervisor(AcctId, QueueId) of +-spec new(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:startlink_ret(). +new(AccountId, QueueId) -> + case find_queue_supervisor(AccountId, QueueId) of P when is_pid(P) -> {'ok', P}; - 'undefined' -> supervisor:start_child(?SERVER, [AcctId, QueueId]) + 'undefined' -> supervisor:start_child(?SERVER, [AccountId, QueueId]) end. -spec workers() -> kz_term:pids(). @@ -56,35 +55,36 @@ workers() -> [Pid || {_, Pid, 'supervisor', _} <- supervisor:which_children(?SERVER), is_pid(Pid)]. -spec find_acct_supervisors(kz_term:ne_binary()) -> kz_term:pids(). -find_acct_supervisors(AcctId) -> - [Super || Super <- workers(), is_queue_in_acct(Super, AcctId)]. +find_acct_supervisors(AccountId) -> + [Super || Super <- workers(), is_queue_in_acct(Super, AccountId)]. -spec is_queue_in_acct(pid(), kz_term:ne_binary()) -> boolean(). -is_queue_in_acct(Super, AcctId) -> +is_queue_in_acct(Super, AccountId) -> case catch acdc_queue_manager:config(acdc_queue_sup:manager(Super)) of {'EXIT', _} -> 'false'; - {AcctId, _} -> 'true'; + {AccountId, _} -> 'true'; _ -> 'false' end. -spec find_queue_supervisor(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:api_pid(). -find_queue_supervisor(AcctId, QueueId) -> - find_queue_supervisor(AcctId, QueueId, workers()). +find_queue_supervisor(AccountId, QueueId) -> + find_queue_supervisor(AccountId, QueueId, workers()). -spec find_queue_supervisor(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:pids()) -> kz_term:api_pid(). -find_queue_supervisor(_AcctId, _QueueId, []) -> 'undefined'; -find_queue_supervisor(AcctId, QueueId, [Super|Rest]) -> +find_queue_supervisor(_AccountId, _QueueId, []) -> 'undefined'; +find_queue_supervisor(AccountId, QueueId, [Super|Rest]) -> case catch acdc_queue_manager:config(acdc_queue_sup:manager(Super)) of - {'EXIT', _} -> find_queue_supervisor(AcctId, QueueId, Rest); - {AcctId, QueueId} -> Super; - _ -> find_queue_supervisor(AcctId, QueueId, Rest) + {'EXIT', _} -> find_queue_supervisor(AccountId, QueueId, Rest); + {AccountId, QueueId} -> Super; + _ -> find_queue_supervisor(AccountId, QueueId, Rest) end. -spec status() -> 'ok'. status() -> ?PRINT("ACDc Queues Status"), Ws = workers(), - _ = kz_process:spawn(fun() -> lists:foreach(fun acdc_queue_sup:status/1, Ws) end), + % _ = kz_process:spawn(fun() -> lists:foreach(fun acdc_queue_sup:status/1, Ws) end), + lists:foreach(fun acdc_queue_sup:status/1, Ws), 'ok'. -spec queues_running() -> [{pid(), any()}]. @@ -96,7 +96,8 @@ queues_running() -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a supervisor is started using `supervisor:start_link/[2,3]', +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], %% this function is called by the new process to find out about %% restart strategy, maximum restart frequency and child %% specifications. diff --git a/applications/acdc/src/acdc_recordings_sup.erl b/applications/acdc/src/acdc_recordings_sup.erl index e414750c15f..d4f9c297949 100644 --- a/applications/acdc/src/acdc_recordings_sup.erl +++ b/applications/acdc/src/acdc_recordings_sup.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2010-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -32,14 +31,14 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the supervisor. +%% @doc Starts the supervisor %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> supervisor:start_link({'local', ?SERVER}, ?MODULE, []). --spec new(kapps_call:call(), kz_json:object()) -> kz_types:startlink_ret(). +-spec new(kapps_call:call(), kz_json:object()) -> kz_term:startlink_ret(). new(Call, Data) -> supervisor:start_child(?SERVER, [Call, Data]). @@ -48,7 +47,8 @@ new(Call, Data) -> %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a supervisor is started using `supervisor:start_link/[2,3]', +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], %% this function is called by the new process to find out about %% restart strategy, maximum restart frequency and child %% specifications. diff --git a/applications/acdc/src/acdc_stats.erl b/applications/acdc/src/acdc_stats.erl index 2c5f57003aa..af790886a94 100644 --- a/applications/acdc/src/acdc_stats.erl +++ b/applications/acdc/src/acdc_stats.erl @@ -5,6 +5,9 @@ %%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC %%% @author Daniel Finke %%% +%%% @author James Aimonetti +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC +%%% @author Daniel Finke %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -16,13 +19,27 @@ %% Public API -export([call_waiting/6 + ,call_waiting/8 ,call_abandoned/4 + ,call_marked_callback/4 ,call_handled/4 ,call_missed/5 ,call_processed/5 ,find_call/1 ,call_stat_to_json/1 + ,agent_ready/2 + ,agent_logged_in/2 + ,agent_logged_out/2 + ,agent_connecting/3, agent_connecting/5 + ,agent_connected/3, agent_connected/5 + ,agent_wrapup/3 + ,agent_paused/4 + ,agent_outbound/3 + + ,agent_statuses/0 + ,manual_cleanup_calls/1 + ,manual_cleanup_statuses/1 ]). %% ETS config @@ -30,13 +47,23 @@ ,call_key_pos/0 ,call_table_opts/0 + ,call_summary_table_id/0 + ,call_summary_key_pos/0 + ,call_summary_table_opts/0 + + ,agent_call_table_id/0 + ,agent_call_key_pos/0 + ,agent_call_table_opts/0 + ,init_db/1 ,archive_call_data/2 ]). %% AMQP Callbacks -export([handle_call_stat/2 + ,handle_call_summary_req/2 ,handle_call_query/2 + ,handle_agent_calls_req/2 ,handle_average_wait_time_req/2 ]). @@ -57,13 +84,14 @@ -define(SERVER, ?MODULE). %% Public API + -spec call_waiting(kz_term:api_binary() ,kz_term:api_binary() ,kz_term:api_binary() ,kz_term:api_binary() ,kz_term:api_binary() ,kz_term:api_binary() - ) -> 'ok'. + ) -> 'ok' | {'error', any()}. call_waiting(AccountId, QueueId, CallId, CallerIdName, CallerIdNumber, CallerPriority) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} @@ -71,40 +99,84 @@ call_waiting(AccountId, QueueId, CallId, CallerIdName, CallerIdNumber, CallerPri ,{<<"Call-ID">>, CallId} ,{<<"Caller-ID-Name">>, CallerIdName} ,{<<"Caller-ID-Number">>, CallerIdNumber} - ,{<<"Entered-Timestamp">>, kz_time:now_s()} + ,{<<"Entered-Timestamp">>, kz_time:current_tstamp()} + ,{<<"Caller-Priority">>, CallerPriority} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"call_waiting">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_waiting/1). + +-spec call_waiting(kz_term:api_binary() + ,kz_term:api_binary() + ,integer() + ,kz_term:api_binary() + ,kz_term:api_binary() + ,kz_term:api_binary() + ,kz_term:api_binary() + ,kz_term:api_binaries() + ) -> 'ok' | {'error', any()}. +call_waiting(AccountId, QueueId, Position, CallId, CallerIdName, CallerIdNumber, CallerPriority, RequiredSkills) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Call-ID">>, CallId} + ,{<<"Caller-ID-Name">>, CallerIdName} + ,{<<"Caller-ID-Number">>, CallerIdNumber} + ,{<<"Entered-Timestamp">>, kz_time:current_tstamp()} + ,{<<"Entered-Position">>, Position} ,{<<"Caller-Priority">>, CallerPriority} + ,{<<"Required-Skills">>, RequiredSkills} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - call_state_change(AccountId, 'waiting', Prop), - 'ok' = kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_waiting/1). + call_state_change(AccountId, <<"call_waiting">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_waiting/1). --spec call_abandoned(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), atom()) -> 'ok'. +-spec call_abandoned(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. call_abandoned(AccountId, QueueId, CallId, Reason) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Queue-ID">>, QueueId} ,{<<"Call-ID">>, CallId} ,{<<"Abandon-Reason">>, Reason} - ,{<<"Abandon-Timestamp">>, kz_time:now_s()} + ,{<<"Abandon-Timestamp">>, kz_time:current_tstamp()} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"call_abandoned">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_abandoned/1). + +-spec call_marked_callback(kz_term:ne_binary() + ,kz_term:ne_binary() + ,kz_term:ne_binary() + ,kz_term:ne_binary() + ) -> 'ok' | {'error', any()}. +call_marked_callback(AccountId, QueueId, CallId, CallerIdName) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Call-ID">>, CallId} + ,{<<"Caller-ID-Name">>, CallerIdName} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - call_state_change(AccountId, 'abandoned', Prop), - 'ok' = kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_abandoned/1). + call_state_change(AccountId, <<"call_marked_callback">>, Prop), + kz_amqp_worker:cast( + Prop + ,fun kapi_acdc_stats:publish_call_marked_callback/1 + ). --spec call_handled(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +-spec call_handled(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. call_handled(AccountId, QueueId, CallId, AgentId) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Queue-ID">>, QueueId} ,{<<"Call-ID">>, CallId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Handled-Timestamp">>, kz_time:now_s()} + ,{<<"Handled-Timestamp">>, kz_time:current_tstamp()} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - call_state_change(AccountId, 'handled', Prop), - 'ok' = kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_handled/1). + call_state_change(AccountId, <<"call_handled">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_handled/1). --spec call_missed(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +-spec call_missed(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. call_missed(AccountId, QueueId, AgentId, CallId, ErrReason) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} @@ -112,25 +184,189 @@ call_missed(AccountId, QueueId, AgentId, CallId, ErrReason) -> ,{<<"Call-ID">>, CallId} ,{<<"Agent-ID">>, AgentId} ,{<<"Miss-Reason">>, ErrReason} - ,{<<"Miss-Timestamp">>, kz_time:now_s()} + ,{<<"Miss-Timestamp">>, kz_time:current_tstamp()} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - call_state_change(AccountId, 'missed', Prop), - 'ok' = kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_missed/1). + call_state_change(AccountId, <<"call_missed">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_missed/1). --spec call_processed(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), atom()) -> 'ok'. +-spec call_processed(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. call_processed(AccountId, QueueId, AgentId, CallId, Initiator) -> Prop = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Queue-ID">>, QueueId} ,{<<"Call-ID">>, CallId} ,{<<"Agent-ID">>, AgentId} - ,{<<"Processed-Timestamp">>, kz_time:now_s()} + ,{<<"Processed-Timestamp">>, kz_time:current_tstamp()} ,{<<"Hung-Up-By">>, Initiator} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - call_state_change(AccountId, 'processed', Prop), - 'ok' = kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_processed/1). + call_state_change(AccountId, <<"call_processed">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_call_processed/1). + +-spec agent_ready(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_ready(AccountId, AgentId) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"ready">>} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_ready">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_ready/1). + +-spec agent_logged_in(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_logged_in(AccountId, AgentId) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"logged_in">>} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_logged_in">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_logged_in/1). + +-spec agent_logged_out(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_logged_out(AccountId, AgentId) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"logged_out">>} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_logged_out">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_logged_out/1). + +-spec agent_connecting(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_connecting(AccountId, AgentId, CallId) -> + agent_connecting(AccountId, AgentId, CallId, 'undefined', 'undefined'). + +-spec agent_connecting(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), api_kz_term:ne_binary(), api_kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_connecting(AccountId, AgentId, CallId, CallerIDName, CallerIDNumber) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"connecting">>} + ,{<<"Call-ID">>, CallId} + ,{<<"Caller-ID-Name">>, CallerIDName} + ,{<<"Caller-ID-Number">>, CallerIDNumber} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_connecting">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_connecting/1). + +-spec agent_connected(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_connected(AccountId, AgentId, CallId) -> + agent_connected(AccountId, AgentId, CallId, 'undefined', 'undefined'). + +-spec agent_connected(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), api_kz_term:ne_binary(), api_kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_connected(AccountId, AgentId, CallId, CallerIDName, CallerIDNumber) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"connected">>} + ,{<<"Call-ID">>, CallId} + ,{<<"Caller-ID-Name">>, CallerIDName} + ,{<<"Caller-ID-Number">>, CallerIDNumber} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_connected">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_connected/1). + +-spec agent_wrapup(kz_term:ne_binary(), kz_term:ne_binary(), pos_integer()) -> 'ok' | {'error', any()}. +agent_wrapup(AccountId, AgentId, WaitTime) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"wrapup">>} + ,{<<"Wait-Time">>, WaitTime} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_wrapup">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_wrapup/1). + +-spec agent_paused(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_pos_integer(), kz_term:api_binary()) -> 'ok' | {'error', any()}. +agent_paused(AccountId, AgentId, 'undefined', _) -> + lager:debug("undefined pause time for ~s(~s)", [AgentId, AccountId]); +agent_paused(AccountId, AgentId, PauseTime, Alias) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"paused">>} + ,{<<"Pause-Time">>, PauseTime} + ,{<<"Pause-Alias">>, Alias} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_paused">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_paused/1). + +-spec agent_outbound(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok' | {'error', any()}. +agent_outbound(AccountId, AgentId, CallId) -> + Prop = props:filter_undefined( + [{<<"Account-ID">>, AccountId} + ,{<<"Agent-ID">>, AgentId} + ,{<<"Timestamp">>, kz_time:current_tstamp()} + ,{<<"Status">>, <<"outbound">>} + ,{<<"Call-ID">>, CallId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + call_state_change(AccountId, <<"agent_outbound">>, Prop), + kz_amqp_worker:cast(Prop, fun kapi_acdc_stats:publish_status_outbound/1). + +-spec agent_statuses() -> kz_term:ne_binaries(). +agent_statuses() -> + ?STATUS_STATUSES. + +-spec manual_cleanup_calls(pos_integer()) -> 'ok'. +manual_cleanup_calls(Window) -> + {'ok', Srv} = acdc_stats_sup:stats_srv(), + + Past = kz_time:current_tstamp() - Window, + PastConstraint = {'=<', '$1', Past}, + + TypeConstraints = [{'=/=', '$2', {'const', <<"waiting">>}} + ,{'=/=', '$2', {'const', <<"handled">>}} + ], + + CallMatch = [{#call_stat{entered_timestamp='$1', status='$2', _='_'} + ,[PastConstraint | TypeConstraints] + ,['$_'] + }], + gen_listener:cast(Srv, {'remove_call', CallMatch}), + + case ets:select(?MODULE:call_table_id() + ,[{#call_stat{entered_timestamp='$1', status= <<"waiting">>, _='_'} + ,[PastConstraint] + ,['$_'] + } + ,{#call_stat{entered_timestamp='$1', status= <<"handled">>, _='_'} + ,[PastConstraint] + ,['$_'] + } + ]) + of + [] -> 'ok'; + Unfinished -> cleanup_unfinished(Unfinished) + end. + +-spec manual_cleanup_statuses(pos_integer()) -> 'ok'. +manual_cleanup_statuses(Window) -> + {'ok', Srv} = acdc_stats_sup:stats_srv(), + + Past = kz_time:current_tstamp() - Window, + + StatusMatch = [{#status_stat{timestamp='$1', _='_'} + ,[{'=<', '$1', Past}] + ,['$_'] + }], + gen_listener:cast(Srv, {'remove_status', StatusMatch}). %% ETS config @@ -146,6 +382,30 @@ call_table_opts() -> ,{'keypos', call_key_pos()} ]. +-spec call_summary_table_id() -> atom(). +call_summary_table_id() -> 'acdc_stats_call_summary'. + +-spec call_summary_key_pos() -> pos_integer(). +call_summary_key_pos() -> #call_summary_stat.id. + +-spec call_summary_table_opts() -> kz_term:proplist(). +call_summary_table_opts() -> + ['protected', 'named_table' + ,{'keypos', call_summary_key_pos()} + ]. + +-spec agent_call_table_id() -> atom(). +agent_call_table_id() -> 'acdc_stats_agent_call'. + +-spec agent_call_key_pos() -> pos_integer(). +agent_call_key_pos() -> #agent_call_stat.id. + +-spec agent_call_table_opts() -> kz_term:proplist(). +agent_call_table_opts() -> + ['bag', 'protected', 'named_table' + ,{'keypos', agent_call_key_pos()} + ]. + -define(BINDINGS, [{'self', []} ,{?MODULE, []} ]). @@ -153,8 +413,11 @@ call_table_opts() -> ,[{<<"acdc_call_stat">>, <<"waiting">>} ,{<<"acdc_call_stat">>, <<"missed">>} ,{<<"acdc_call_stat">>, <<"abandoned">>} + ,{<<"acdc_call_stat">>, <<"marked_callback">>} ,{<<"acdc_call_stat">>, <<"handled">>} ,{<<"acdc_call_stat">>, <<"processed">>} + ,{<<"acdc_call_stat">>, <<"exited-position">>} + ,{<<"acdc_call_stat">>, <<"id-change">>} ,{<<"acdc_call_stat">>, <<"flush">>} ] } @@ -173,23 +436,33 @@ call_table_opts() -> ,{{?MODULE, 'handle_call_query'} ,[{<<"acdc_stat">>, <<"current_calls_req">>}] } + ,{{?MODULE, 'handle_call_summary_req'} + ,[{<<"acdc_stat">>, <<"call_summary_req">>}] + } + ,{{?MODULE, 'handle_agent_calls_req'} + ,[{<<"acdc_stat">>, <<"agent_calls_req">>}] + } ,{{?MODULE, 'handle_average_wait_time_req'} ,[{<<"acdc_stat">>, <<"average_wait_time_req">>}] } ,{{'acdc_agent_stats', 'handle_status_query'} ,[{<<"acdc_stat">>, <<"status_req">>}] } + ,{{'acdc_agent_stats', 'handle_agent_cur_status_req'} + ,[{<<"acdc_stat">>, <<"agent_cur_status_req">>}] + } ]). -define(QUEUE_NAME, <<>>). --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> gen_listener:start_link(?SERVER ,[{'bindings', ?BINDINGS} ,{'responders', ?RESPONDERS} ,{'queue_name', ?QUEUE_NAME} - ], - []). + ] + ,[] + ). -spec handle_call_stat(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_call_stat(JObj, Props) -> @@ -197,8 +470,10 @@ handle_call_stat(JObj, Props) -> <<"waiting">> -> handle_waiting_stat(JObj, Props); <<"missed">> -> handle_missed_stat(JObj, Props); <<"abandoned">> -> handle_abandoned_stat(JObj, Props); + <<"marked_callback">> -> handle_marked_callback_stat(JObj, Props); <<"handled">> -> handle_handled_stat(JObj, Props); <<"processed">> -> handle_processed_stat(JObj, Props); + <<"exited-position">> -> handle_exited_stat(JObj, Props); <<"flush">> -> flush_call_stat(JObj, Props); _Name -> lager:debug("recv unknown call stat type ~s: ~p", [_Name, JObj]) @@ -209,11 +484,62 @@ handle_call_query(JObj, _Prop) -> 'true' = kapi_acdc_stats:current_calls_req_v(JObj), RespQ = kz_json:get_value(<<"Server-ID">>, JObj), MsgId = kz_json:get_value(<<"Msg-ID">>, JObj), - Limit = acdc_stats_util:get_query_limit(JObj), case call_build_match_spec(JObj) of - {'ok', Match} -> query_calls(RespQ, MsgId, Match, Limit); - {'error', Errors} -> publish_query_errors(RespQ, MsgId, Errors) + {'ok', Match} -> + Limit = acdc_stats_util:get_query_limit(JObj), + Result = query_calls(Match, Limit), + Resp = Result ++ + kz_api:default_headers(?APP_NAME, ?APP_VERSION) ++ + [{<<"Query-Time">>, kz_time:current_tstamp()} + ,{<<"Msg-ID">>, MsgId} + ], + kapi_acdc_stats:publish_current_calls_resp(RespQ, Resp); + {'error', Errors} -> acdc_stats_util:publish_call_query_errors(RespQ, MsgId, Errors) + end. + +-spec handle_call_summary_req(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_call_summary_req(JObj, _Prop) -> + 'true' = kapi_acdc_stats:call_summary_req_v(JObj), + Now = kz_time:current_tstamp(), + Past = Now - ?CLEANUP_WINDOW, + StartRange = kz_json:get_value(<<"Start-Range">>, JObj), + %% If there is no StartRange then get data from ETS + %% If StartRange >= Past then get data from ETS + %% Otherwise get data from MODB + case StartRange of + undefined -> call_summary_req(JObj); + X when X >= Past -> call_summary_req(JObj); + _ -> acdc_stats_util:call_summary_req(JObj) + end. + +%% Get data from ETS DB +-spec call_summary_req(kz_json:object()) -> 'ok'. +call_summary_req(JObj) -> + RespQ = kz_json:get_value(<<"Server-ID">>, JObj), + MsgId = kz_json:get_value(<<"Msg-ID">>, JObj), + Limit = acdc_stats_util:get_query_limit(JObj), + + Summary = case call_summary_build_match_spec(JObj) of + {'ok', Match} -> query_call_summary(Match, Limit); + {'error', _Errors}=E -> E + end, + % Active = case call_build_match_spec(kz_json:set_value(<<"Status">>, [<<"waiting">>, <<"handled">>], JObj)) of + % {'ok', Match1} -> query_calls(Match1, Limit); + % {'error', _Errors1}=E1 -> E1 + % end, + acdc_stats_util:publish_summary_data(RespQ, MsgId, Summary, []). + +-spec handle_agent_calls_req(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_agent_calls_req(JObj, _Prop) -> + 'true' = kapi_acdc_stats:agent_calls_req_v(JObj), + RespQ = kz_json:get_value(<<"Server-ID">>, JObj), + MsgId = kz_json:get_value(<<"Msg-ID">>, JObj), + Limit = acdc_stats_util:get_query_limit(JObj), + + case agent_call_build_match_spec(JObj) of + {'ok', Match} -> query_agent_calls(RespQ, MsgId, Match, Limit); + {'error', Errors} -> publish_agent_call_query_errors(RespQ, MsgId, Errors) end. %%------------------------------------------------------------------------------ @@ -237,19 +563,9 @@ find_call(CallId) -> }], case ets:select(call_table_id(), MS) of [] -> 'undefined'; - [Stat] -> call_stat_to_json(Stat); - Stats -> call_stat_to_json(get_recent_stat_for_call(Stats)) + [Stat] -> call_stat_to_json(Stat) end. --spec get_recent_stat_for_call(call_stats()) -> call_stat(). -get_recent_stat_for_call(Stats) -> - Sorted = lists:sort(fun sort_by_entered_timestamp/2, Stats), - lists:nth(1, Sorted). - --spec sort_by_entered_timestamp(call_stat(), call_stat()) -> boolean(). -sort_by_entered_timestamp(#call_stat{entered_timestamp=ATimestamp}, #call_stat{entered_timestamp=BTimestamp}) -> - ATimestamp > BTimestamp. - -record(state, {archive_ref :: reference() ,cleanup_ref :: reference() }). @@ -273,33 +589,88 @@ start_archive_timer() -> start_cleanup_timer() -> erlang:send_after(?CLEANUP_PERIOD, self(), ?CLEANUP_MSG). --spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). +-spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_term:handle_call_ret_state(state()). handle_call(_Req, _From, State) -> {'reply', 'ok', State}. --spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). -handle_cast({'create_call', #call_stat{id=_Id}=Stat}, State) -> - lager:debug("creating new call stat ~s", [_Id]), +-spec handle_cast(any(), state()) -> kz_term:handle_cast_ret_state(state()). +handle_cast({'create_call', JObj}, State) -> + Id = call_stat_id(JObj), + lager:debug("creating new call stat ~s", [Id]), + Stat = #call_stat{id = Id + ,call_id = kz_json:get_value(<<"Call-ID">>, JObj) + ,account_id = kz_json:get_value(<<"Account-ID">>, JObj) + ,queue_id = kz_json:get_value(<<"Queue-ID">>, JObj) + ,entered_timestamp = kz_json:get_value(<<"Entered-Timestamp">>, JObj, kz_time:current_tstamp()) + ,abandoned_timestamp = kz_json:get_value(<<"Abandon-Timestamp">>, JObj) + ,entered_position = kz_json:get_value(<<"Entered-Position">>, JObj) + ,abandoned_reason = kz_json:get_value(<<"Abandon-Reason">>, JObj) + ,misses = [] + ,status = kz_json:get_value(<<"Event-Name">>, JObj) + ,caller_id_name = kz_json:get_value(<<"Caller-ID-Name">>, JObj) + ,caller_id_number = kz_json:get_value(<<"Caller-ID-Number">>, JObj) + ,caller_priority = kz_json:get_integer_value(<<"Caller-Priority">>, JObj) + ,required_skills = kz_json:get_list_value(<<"Required-Skills">>, JObj, []) + }, ets:insert_new(call_table_id(), Stat), {'noreply', State}; -handle_cast({'create_status', #status_stat{id=_Id, status=_Status}=Stat}, State) -> +handle_cast({'create_status', #status_stat{id=_Id + ,agent_id=AgentId + ,status=_Status + }=Stat}, State) -> lager:debug("creating new status stat ~s: ~s", [_Id, _Status]), case ets:insert_new(acdc_agent_stats:status_table_id(), Stat) of - 'true' -> {'noreply', State}; + 'true' -> 'ok'; 'false' -> lager:debug("stat ~s already exists, updating", [_Id]), - ets:insert(acdc_agent_stats:status_table_id(), Stat), - {'noreply', State} - end; + ets:insert(acdc_agent_stats:status_table_id(), Stat) + end, + %% Only set the agent's current status if the timestamp of this + %% stat is newer than the current one + case ets:lookup(acdc_agent_stats:agent_cur_status_table_id(), AgentId) of + [] -> ets:insert(acdc_agent_stats:agent_cur_status_table_id(), Stat); + [OldStat] -> maybe_insert_agent_cur_status(OldStat, Stat) + end, + {'noreply', State}; handle_cast({'update_call', Id, Updates}, State) -> lager:debug("updating call stat ~s: ~p", [Id, Updates]), ets:update_element(call_table_id(), Id, Updates), + Stat = find_call_stat(Id), + maybe_add_summary_stat(Stat), + maybe_add_agent_call_stat(Stat), + maybe_send_summary_stat(Stat), + {'noreply', State}; +handle_cast({'update_call_summ', Id, Updates}, State) -> + lager:debug("updating call summary stat ~s: ~p", [Id, Updates]), + ets:update_element(call_summary_table_id(), Id, Updates), + {'noreply', State}; +handle_cast({'add_miss', JObj}, State) -> + Id = call_stat_id(JObj), + lager:debug("adding miss to stat ~s", [Id]), + #call_stat{misses=Misses}=Stat = find_call_stat(Id), + Updates = [{#call_stat.misses, [create_miss(JObj) | Misses]}], + ets:update_element(call_table_id(), Id, Updates), + + add_agent_call_stat_miss(Stat + ,kz_json:get_value(<<"Agent-ID">>, JObj) + ,kz_json:get_value(<<"Miss-Timestamp">>, JObj)), + {'noreply', State}; handle_cast({'flush_call', Id}, State) -> lager:debug("flushing call stat ~s", [Id]), + ets:delete(call_table_id(), Id), + ets:delete(call_summary_table_id(), Id), + ets:delete(agent_call_table_id(), Id), + {'noreply', State}; -handle_cast({'remove_call', [{M, P, _}]}, State) -> +handle_cast({'remove_call', [{M, P, _}]=MatchSpec}, State) -> + Stats = ets:select(call_table_id(), MatchSpec), + lists:foreach(fun(#call_stat{id=Id}) -> + ets:delete(call_summary_table_id(), Id), + ets:delete(agent_call_table_id(), Id) + end, Stats), + Match = [{M, P, ['true']}], N = ets:select_delete(call_table_id(), Match), N > 1 @@ -325,7 +696,7 @@ handle_cast(_Req, State) -> lager:debug("unhandled cast: ~p", [_Req]), {'noreply', State}. --spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +-spec handle_info(any(), state()) -> kz_term:handle_info_ret_state(state()). handle_info({'ETS-TRANSFER', _TblId, _From, _Data}, State) -> lager:debug("ETS control for ~p transferred to me for writing", [_TblId]), {'noreply', State}; @@ -336,7 +707,7 @@ handle_info(?CLEANUP_MSG, State) -> _ = cleanup_data(self()), {'noreply', State#state{cleanup_ref=start_cleanup_timer()}}; handle_info(_Msg, State) -> - lager:debug("unhandled message: ~p", [_Msg]), + lager:debug("unhandling message: ~p", [_Msg]), {'noreply', State}. -spec handle_event(kz_json:object(), state()) -> gen_listener:handle_event_return(). @@ -352,13 +723,11 @@ terminate(_Reason, _) -> code_change(_OldVsn, State, _Extra) -> {'ok', State}. -publish_query_errors(RespQ, MsgId, Errors) -> - API = [{<<"Error-Reason">>, Errors} - ,{<<"Msg-ID">>, MsgId} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], - lager:debug("responding with errors to req ~s: ~p", [MsgId, Errors]), - kapi_acdc_stats:publish_current_calls_err(RespQ, API). + +publish_agent_call_query_errors(RespQ, MsgId, Errors) -> + acdc_stats_util:publish_query_errors(RespQ, MsgId, Errors, fun kapi_acdc_stats:publish_agent_calls_err/2). + + call_build_match_spec(JObj) -> case kz_json:get_value(<<"Account-ID">>, JObj) of @@ -389,6 +758,22 @@ call_match_builder_fold(<<"Agent-ID">>, AgentId, {CallStat, Contstraints}) -> {CallStat#call_stat{agent_id='$3'} ,[{'=:=', '$3', {'const', AgentId}} | Contstraints] }; +call_match_builder_fold(<<"Status">>, Statuses, {CallStat, Constraints}) when is_list(Statuses) -> + CallStat1 = CallStat#call_stat{status='$4'}, + Constraints1 = lists:foldl(fun(_Status, {'error', _Err}=E) -> + E; + (Status, OrdConstraints) -> + case is_valid_call_status(Status) of + {'true', Normalized} -> + erlang:append_element(OrdConstraints, {'=:=', '$4', {'const', Normalized}}); + 'false' -> + {'error', kz_json:from_list([{<<"Status">>, <<"unknown status supplied">>}])} + end + end, {'orelse'}, Statuses), + case Constraints1 of + {'error', _Err}=E -> E; + _ -> {CallStat1, [Constraints1 | Constraints]} + end; call_match_builder_fold(<<"Status">>, Status, {CallStat, Contstraints}) -> case is_valid_call_status(Status) of {'true', Normalized} -> @@ -399,10 +784,11 @@ call_match_builder_fold(<<"Status">>, Status, {CallStat, Contstraints}) -> {'error', kz_json:from_list([{<<"Status">>, <<"unknown status supplied">>}])} end; call_match_builder_fold(<<"Start-Range">>, Start, {CallStat, Contstraints}) -> - Now = kz_time:now_s(), + Now = kz_time:current_tstamp(), Past = Now - ?CLEANUP_WINDOW, + Start1 = acdc_stats_util:apply_query_window_wiggle_room(Start, Past), - try kz_term:to_integer(Start) of + try kz_term:to_integer(Start1) of N when N < Past -> {'error', kz_json:from_list([{<<"Start-Range">>, <<"supplied value is too far in the past">>} ,{<<"Window-Size">>, ?CLEANUP_WINDOW} @@ -422,10 +808,11 @@ call_match_builder_fold(<<"Start-Range">>, Start, {CallStat, Contstraints}) -> {'error', kz_json:from_list([{<<"Start-Range">>, <<"supplied value is not an integer">>}])} end; call_match_builder_fold(<<"End-Range">>, End, {CallStat, Contstraints}) -> - Now = kz_time:now_s(), + Now = kz_time:current_tstamp(), Past = Now - ?CLEANUP_WINDOW, + End1 = acdc_stats_util:apply_query_window_wiggle_room(End, Past), - try kz_term:to_integer(End) of + try kz_term:to_integer(End1) of N when N < Past -> {'error', kz_json:from_list([{<<"End-Range">>, <<"supplied value is too far in the past">>} ,{<<"Window-Size">>, ?CLEANUP_WINDOW} @@ -445,10 +832,78 @@ call_match_builder_fold(<<"End-Range">>, End, {CallStat, Contstraints}) -> end; call_match_builder_fold(_, _, Acc) -> Acc. +call_summary_build_match_spec(JObj) -> + case kz_json:get_value(<<"Account-ID">>, JObj) of + 'undefined' -> + {'error', kz_json:from_list([{<<"Account-ID">>, <<"missing but required">>}])}; + AccountId -> + AccountMatch = {#call_summary_stat{account_id='$1', _='_'} + ,[{'=:=', '$1', {'const', AccountId}}] + }, + call_summary_build_match_spec(JObj, AccountMatch) + end. + +-spec call_summary_build_match_spec(kz_json:object(), {call_summary_stat(), list()}) -> + {'ok', ets:match_spec()} | + {'error', kz_json:object()}. +call_summary_build_match_spec(JObj, AccountMatch) -> + case kz_json:foldl(fun call_summary_match_builder_fold/3, AccountMatch, JObj) of + {'error', _Errs}=Errors -> Errors; + {Stat, Constraints} -> {'ok', [{Stat, Constraints, ['$_']}]} + end. + +call_summary_match_builder_fold(_, _, {'error', _Err}=E) -> E; +call_summary_match_builder_fold(<<"Queue-ID">>, QueueId, {CallStat, Contstraints}) -> + {CallStat#call_summary_stat{queue_id='$2'} + ,[{'=:=', '$2', {'const', QueueId}} | Contstraints] + }; +call_summary_match_builder_fold(<<"Status">>, Status, {CallStat, Contstraints}) -> + case is_valid_call_status(Status) of + {'true', Normalized} -> + {CallStat#call_summary_stat{status='$3'} + ,[{'=:=', '$3', {'const', Normalized}} | Contstraints] + }; + 'false' -> + {'error', kz_json:from_list([{<<"Status">>, <<"unknown status supplied">>}])} + end; +call_summary_match_builder_fold(<<"Start-Range">>, Start, {CallStat, Contstraints}) -> + {CallStat#call_summary_stat{timestamp='$4'} + ,[{'>=', '$4', {'const', Start}} | Contstraints] + }; +call_summary_match_builder_fold(<<"End-Range">>, End, {CallStat, Contstraints}) -> + {CallStat#call_summary_stat{timestamp='$4'} + ,[{'=<', '$4', {'const', End}} | Contstraints] + }; +call_summary_match_builder_fold(_, _, Acc) -> Acc. + +agent_call_build_match_spec(JObj) -> + case kz_json:get_value(<<"Account-ID">>, JObj) of + 'undefined' -> + {'error', kz_json:from_list([{<<"Account-ID">>, <<"missing but required">>}])}; + AccountId -> + AccountMatch = {#agent_call_stat{account_id='$1', _='_'} + ,[{'=:=', '$1', {'const', AccountId}}] + }, + agent_call_build_match_spec(JObj, AccountMatch) + end. + +-spec agent_call_build_match_spec(kz_json:object(), {agent_call_stat(), list()}) -> + {'ok', ets:match_spec()} | + {'error', kz_json:object()}. +agent_call_build_match_spec(JObj, AccountMatch) -> + case kz_json:foldl(fun agent_call_match_builder_fold/3, AccountMatch, JObj) of + {'error', _Errs}=Errors -> Errors; + {Stat, Constraints} -> {'ok', [{Stat, Constraints, ['$_']}]} + end. + +agent_call_match_builder_fold(_, _, {'error', _Err}=E) -> E; +agent_call_match_builder_fold(_, _, Acc) -> Acc. + -spec average_wait_time_build_match_spec(kz_json:object()) -> ets:match_spec(). average_wait_time_build_match_spec(JObj) -> AccountId = kz_json:get_ne_binary_value(<<"Account-ID">>, JObj), QueueId = kz_json:get_ne_binary_value(<<"Queue-ID">>, JObj), + Skills = lists:sort(kz_json:get_list_value(<<"Skills">>, JObj, [])), Match = [{#call_stat{account_id=AccountId ,queue_id=QueueId @@ -456,6 +911,7 @@ average_wait_time_build_match_spec(JObj) -> ,abandoned_timestamp='$2' ,handled_timestamp='$3' ,status='$4' + ,required_skills=Skills ,_='_' } ,[{'orelse' @@ -484,37 +940,110 @@ is_valid_call_status(S) -> 'false' -> 'false' end. --spec query_calls(kz_term:ne_binary(), kz_term:ne_binary(), ets:match_spec(), pos_integer() | 'no_limit') -> 'ok'. -query_calls(RespQ, MsgId, Match, _Limit) -> +-spec query_calls(ets:match_spec(), pos_integer() | 'no_limit') -> kz_term:proplist(). +query_calls(Match, _Limit) -> case ets:select(call_table_id(), Match) of [] -> - lager:debug("no stats found, sorry ~s", [RespQ]), - Resp = [{<<"Query-Time">>, kz_time:now_s()} - ,{<<"Msg-ID">>, MsgId} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], - kapi_acdc_stats:publish_current_calls_resp(RespQ, Resp); + lager:debug("no stats found, sorry"), + []; Stats -> Dict = dict:from_list([{<<"waiting">>, []} ,{<<"handled">>, []} ,{<<"abandoned">>, []} ,{<<"processed">>, []} + ,{<<"entered_position">>, []} + ,{<<"exited_position">>, []} ]), QueryResult = lists:foldl(fun query_call_fold/2, Dict, Stats), - Resp = [{<<"Waiting">>, dict:fetch(<<"waiting">>, QueryResult)} - ,{<<"Handled">>, dict:fetch(<<"handled">>, QueryResult)} - ,{<<"Abandoned">>, dict:fetch(<<"abandoned">>, QueryResult)} - ,{<<"Processed">>, dict:fetch(<<"processed">>, QueryResult)} - ,{<<"Query-Time">>, kz_time:now_s()} + [{<<"Waiting">>, dict:fetch(<<"waiting">>, QueryResult)} + ,{<<"Handled">>, dict:fetch(<<"handled">>, QueryResult)} + ,{<<"Abandoned">>, dict:fetch(<<"abandoned">>, QueryResult)} + ,{<<"Processed">>, dict:fetch(<<"processed">>, QueryResult)} + ,{<<"Entered-Position">>, dict:fetch(<<"entered_position">>, QueryResult)} + ,{<<"Exited-Position">>, dict:fetch(<<"exited_position">>, QueryResult)} + ] + end. + +-spec query_call_summary(ets:match_spec(), pos_integer() | 'no_limit') -> kz_term:proplist(). +query_call_summary(Match, _Limit) -> + case ets:select(call_summary_table_id(), Match) of + [] -> + lager:debug("no stats found, sorry"), + []; + Stats -> + QueryResult = lists:foldl(fun query_call_summary_fold/2, [], Stats), + JsonResult = lists:foldl(fun({QueueId, {TotalCalls, AbandonedCalls, TotalWaitTime, TotalTalkTime, MaxEnteredPosition}}, JObj) -> + QueueJObj = kz_json:set_values([{<<"total_calls">>, TotalCalls} + ,{<<"abandoned_calls">>, AbandonedCalls} + ,{<<"average_wait_time">>, TotalWaitTime div TotalCalls} + ,{<<"average_talk_time">>, TotalTalkTime div (TotalCalls - AbandonedCalls)} + ,{<<"max_entered_position">>, MaxEnteredPosition} + ] + ,kz_json:new()), + kz_json:set_value(QueueId, QueueJObj, JObj) + end + ,kz_json:new() + ,QueryResult), + [{<<"Data">>, JsonResult}] + end. + +-spec query_call_summary_fold(call_summary_stat(), kz_term:proplist()) -> kz_term:proplist(). +query_call_summary_fold(#call_summary_stat{queue_id=QueueId + ,status=Status + ,wait_time=WaitTime + ,talk_time=TalkTime + ,entered_position=EnteredPos + }, Props) -> + {TotalCalls, AbandonedCalls, TotalWaitTime, TotalTalkTime, MaxEnteredPosition} = props:get_value(QueueId, Props, {0, 0, 0, 0, 0}), + {AbandonedCalls1, TotalWaitTime1, TotalTalkTime1} = case Status of + <<"processed">> -> {AbandonedCalls, TotalWaitTime + WaitTime, TotalTalkTime + TalkTime}; + <<"abandoned">> -> {AbandonedCalls + 1, TotalWaitTime, TotalTalkTime} + end, + props:set_value(QueueId, {TotalCalls+1, AbandonedCalls1, TotalWaitTime1, TotalTalkTime1, max(MaxEnteredPosition,EnteredPos)}, Props). + +-spec query_agent_calls(kz_term:ne_binary(), kz_term:ne_binary(), ets:match_spec(), pos_integer() | 'no_limit') -> 'ok'. +query_agent_calls(RespQ, MsgId, Match, _Limit) -> + case ets:select(agent_call_table_id(), Match) of + [] -> + lager:debug("no stats found, sorry ~s", [RespQ]), + Resp = [{<<"Query-Time">>, kz_time:current_tstamp()} ,{<<"Msg-ID">>, MsgId} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ], + kapi_acdc_stats:publish_agent_calls_resp(RespQ, Resp); + Stats -> + QueryResult = lists:foldl(fun query_agent_calls_fold/2, kz_json:new(), Stats), + Resp = kz_json:to_proplist(kz_json:set_value(<<"Data">>, QueryResult, kz_json:new())) ++ + kz_api:default_headers(?APP_NAME, ?APP_VERSION) ++ + [{<<"Query-Time">>, kz_time:current_tstamp()} + ,{<<"Msg-ID">>, MsgId} + ], + kapi_acdc_stats:publish_agent_calls_resp(RespQ, Resp) + end. - kapi_acdc_stats:publish_current_calls_resp(RespQ, Resp) +-spec query_agent_calls_fold(agent_call_stat(), kz_json:object()) -> kz_json:object(). +query_agent_calls_fold(#agent_call_stat{agent_id=AgentId}=Stat, JObj) -> + AgentJObj = kz_json:get_value(AgentId, JObj, []), + kz_json:set_value(AgentId, increment_agent_calls(Stat, AgentJObj), JObj). + +-spec increment_agent_calls(agent_call_stat(), kz_json:object()) -> kz_json:object(). +increment_agent_calls(#agent_call_stat{queue_id=QueueId + ,status=Status + }, AgentJObj) -> + case Status of + <<"handled">> -> increment_agent_calls(QueueId, AgentJObj, <<"answered_calls">>); + <<"missed">> -> increment_agent_calls(QueueId, AgentJObj, <<"missed_calls">>); + _ -> AgentJObj end. +-spec increment_agent_calls(kz_term:ne_binary(), kz_json:object(), kz_term:ne_binary()) -> kz_json:object(). +increment_agent_calls(QueueId, AgentJObj, Key) -> + Count = kz_json:get_integer_value([QueueId, Key], AgentJObj, 0) + 1, + kz_json:set_value([QueueId, Key], Count, AgentJObj). + %%------------------------------------------------------------------------------ +%% @private %% @doc Calculate and reply with the average wait time on a queue %% %% @end @@ -531,6 +1060,7 @@ query_average_wait_time(Match, JObj) -> kapi_acdc_stats:publish_average_wait_time_resp(RespQ, Resp). %%------------------------------------------------------------------------------ +%% @private %% @doc Calculate the average wait time given a list of finished call stat %% timestamps %% @@ -567,7 +1097,7 @@ force_archive_data() -> 'ok'. cleanup_data(Srv) -> - Past = kz_time:now_s() - ?CLEANUP_WINDOW, + Past = kz_time:current_tstamp() - ?CLEANUP_WINDOW, PastConstraint = {'=<', '$1', Past}, TypeConstraints = [{'=/=', '$2', {'const', <<"waiting">>}} @@ -580,7 +1110,7 @@ cleanup_data(Srv) -> }], gen_listener:cast(Srv, {'remove_call', CallMatch}), - StatusMatch = [{#status_stat{key=#status_stat_key{timestamp='$1'}, _='_'} + StatusMatch = [{#status_stat{timestamp='$1', _='_'} ,[{'=<', '$1', Past}] ,['$_'] }], @@ -608,21 +1138,31 @@ cleanup_unfinished(Unfinished) -> archive_call_data(Srv, 'true') -> kz_log:put_callid(<<"acdc_stats.force_call_archiver">>), - Match = [{#call_stat{status='$1' - ,is_archived='$2' - ,_='_' - } - ,[{'=/=', '$1', {'const', <<"waiting">>}} - ,{'=/=', '$1', {'const', <<"handled">>}} - ,{'=:=', '$2', 'false'} - ] - ,['$_'] - }], - maybe_archive_call_data(Srv, Match); + CallStatMatch = [{#call_stat{status='$1' + ,is_archived='$2' + ,_='_' + } + ,[{'=/=', '$1', {'const', <<"waiting">>}} + ,{'=/=', '$1', {'const', <<"handled">>}} + ,{'=:=', '$2', 'false'} + ] + ,['$_'] + }], + maybe_archive_call_data(Srv, CallStatMatch), + + CallSumStatMatch = [{#call_summary_stat{is_archived='$1' + ,_='_' + } + ,[ + {'=:=', '$1', 'false'} + ] + ,['$_'] + }], + maybe_archive_call_summary_data(Srv, CallSumStatMatch); archive_call_data(Srv, 'false') -> kz_log:put_callid(<<"acdc_stats.call_archiver">>), - Past = kz_time:now_s() - ?ARCHIVE_WINDOW, + Past = kz_time:current_tstamp() - ?ARCHIVE_WINDOW, Match = [{#call_stat{entered_timestamp='$1' ,status='$2' ,is_archived='$3' @@ -635,7 +1175,18 @@ archive_call_data(Srv, 'false') -> ] ,['$_'] }], - maybe_archive_call_data(Srv, Match). + maybe_archive_call_data(Srv, Match), + + CallSumStatMatch = [{#call_summary_stat{timestamp='$1' + ,is_archived='$2' + ,_='_' + } + ,[{'=<', '$1', Past} + ,{'=:=', '$2', 'false'} + ] + ,['$_'] + }], + maybe_archive_call_summary_data(Srv, CallSumStatMatch). maybe_archive_call_data(Srv, Match) -> case ets:select(call_table_id(), Match) of @@ -652,6 +1203,21 @@ maybe_archive_call_data(Srv, Match) -> 'ok' end. +maybe_archive_call_summary_data(Srv, Match) -> + case ets:select(call_summary_table_id(), Match) of + [] -> 'ok'; + Stats -> + kz_datamgr:suppress_change_notice(), + ToSave = lists:foldl(fun archive_call_summary_fold/2, dict:new(), Stats), + _ = [kz_datamgr:save_docs(acdc_stats_util:db_name(Account), Docs) + || {Account, Docs} <- dict:to_list(ToSave) + ], + _ = [gen_listener:cast(Srv, {'update_call_summ', Id, [{#call_summary_stat.is_archived, 'true'}]}) + || #call_summary_stat{id=Id} <- Stats + ], + 'ok' + end. + -spec query_call_fold(call_stat(), dict:dict()) -> dict:dict(). query_call_fold(#call_stat{status=Status}=Stat, Acc) -> Doc = call_stat_to_doc(Stat), @@ -662,6 +1228,11 @@ archive_call_fold(#call_stat{account_id=AccountId}=Stat, Acc) -> Doc = call_stat_to_doc(Stat), dict:update(AccountId, fun(L) -> [Doc | L] end, [Doc], Acc). +-spec archive_call_summary_fold(call_summary_stat(), dict:dict()) -> dict:dict(). +archive_call_summary_fold(#call_summary_stat{account_id=AccountId}=Stat, Acc) -> + Doc = call_summary_stat_to_doc(Stat), + dict:update(AccountId, fun(L) -> [Doc | L] end, [Doc], Acc). + -spec call_stat_to_doc(call_stat()) -> kz_json:object(). call_stat_to_doc(#call_stat{id=Id ,call_id=CallId @@ -673,12 +1244,16 @@ call_stat_to_doc(#call_stat{id=Id ,handled_timestamp=HandledT ,processed_timestamp=ProcessedT ,hung_up_by=HungUpBy + ,entered_position=EnteredPos + ,exited_position=ExitedPos ,abandoned_reason=AbandonedR + ,is_callback=IsCallback ,misses=Misses ,status=Status ,caller_id_name=CallerIdName ,caller_id_number=CallerIdNumber ,caller_priority=CallerPriority + ,required_skills=RequiredSkills }) -> kz_doc:update_pvt_parameters(kz_json:from_list( [{<<"_id">>, Id} @@ -690,12 +1265,16 @@ call_stat_to_doc(#call_stat{id=Id ,{<<"handled_timestamp">>, HandledT} ,{<<"processed_timestamp">>, ProcessedT} ,{<<"hung_up_by">>, HungUpBy} + ,{<<"entered_position">>, EnteredPos} + ,{<<"exited_position">>, ExitedPos} ,{<<"abandoned_reason">>, AbandonedR} + ,{<<"is_callback">>, IsCallback} ,{<<"misses">>, misses_to_docs(Misses)} ,{<<"status">>, Status} ,{<<"caller_id_name">>, CallerIdName} ,{<<"caller_id_number">>, CallerIdNumber} ,{<<"caller_priority">>, CallerPriority} + ,{<<"required_skills">>, RequiredSkills} ,{<<"wait_time">>, wait_time(EnteredT, AbandonedT, HandledT)} ,{<<"talk_time">>, talk_time(HandledT, ProcessedT)} ]) @@ -705,6 +1284,35 @@ call_stat_to_doc(#call_stat{id=Id ] ). + +-spec call_summary_stat_to_doc(call_summary_stat()) -> kz_json:object(). +call_summary_stat_to_doc(#call_summary_stat{ + id=Id + ,account_id=AccountId + ,queue_id=QueueId + ,call_id=CallId + ,status=Status + ,entered_position=EnteredPos + ,wait_time=WaitTime + ,talk_time=TalkTime + ,timestamp=Timestamp + }) -> + kz_doc:update_pvt_parameters(kz_json:from_list( + [{<<"_id">>, <<"css-", Id/binary>>} + ,{<<"call_id">>, CallId} + ,{<<"queue_id">>, QueueId} + ,{<<"entered_position">>, EnteredPos} + ,{<<"status">>, Status} + ,{<<"wait_time">>, WaitTime} + ,{<<"talk_time">>, TalkTime} + ,{<<"timestamp">>, Timestamp} + ]) + ,acdc_stats_util:db_name(AccountId) + ,[{'account_id', AccountId} + ,{'type', <<"call_summary_stat">>} + ] + ). + -spec call_stat_to_json(call_stat()) -> kz_json:object(). call_stat_to_json(#call_stat{id=Id ,call_id=CallId @@ -716,11 +1324,15 @@ call_stat_to_json(#call_stat{id=Id ,handled_timestamp=HandledT ,processed_timestamp=ProcessedT ,hung_up_by=HungUpBy + ,entered_position=EnteredPos + ,exited_position=ExitedPos ,abandoned_reason=AbandonedR + ,is_callback=IsCallback ,misses=Misses ,status=Status ,caller_id_name=CallerIdName ,caller_id_number=CallerIdNumber + ,caller_priority=CallerPriority }) -> kz_json:from_list( [{<<"Id">>, Id} @@ -733,13 +1345,17 @@ call_stat_to_json(#call_stat{id=Id ,{<<"Handled-Timestamp">>, HandledT} ,{<<"Processed-Timestamp">>, ProcessedT} ,{<<"Hung-Up-By">>, HungUpBy} + ,{<<"Entered-Position">>, EnteredPos} + ,{<<"Exited-Position">>, ExitedPos} ,{<<"Abandoned-Reason">>, AbandonedR} + ,{<<"Is-Callback">>, IsCallback} ,{<<"Misses">>, misses_to_docs(Misses)} ,{<<"Status">>, Status} ,{<<"Caller-ID-Name">>, CallerIdName} ,{<<"Caller-ID-Number">>, CallerIdNumber} ,{<<"Wait-Time">>, wait_time(EnteredT, AbandonedT, HandledT)} ,{<<"Talk-Time">>, talk_time(HandledT, ProcessedT)} + ,{<<"Caller-Priority">>, CallerPriority} ]). wait_time(E, _, H) when is_integer(E), is_integer(H) -> H - E; @@ -772,15 +1388,13 @@ maybe_created_db(DbName, 'false') -> case kz_datamgr:db_exists(DbName) of 'true' -> lager:debug("database ~s already created, refreshing view", [DbName]), - _ = kapps_maintenance:refresh(DbName), - 'ok'; + kz_datamgr:revise_views_from_folder(DbName, 'acdc'); 'false' -> lager:debug("modb ~s was not created", [DbName]) end; maybe_created_db(DbName, 'true') -> lager:debug("created db ~s, adding views", [DbName]), - _ = kapps_maintenance:refresh(DbName), - 'ok'. + kz_datamgr:revise_views_from_folder(DbName, 'acdc'). -spec call_stat_id(kz_json:object()) -> kz_term:ne_binary(). call_stat_id(JObj) -> @@ -797,11 +1411,15 @@ handle_waiting_stat(JObj, Props) -> Id = call_stat_id(JObj), case find_call_stat(Id) of - 'undefined' -> create_call_stat(Id, JObj, Props); + 'undefined' -> gen_listener:cast(props:get_value('server', Props), {'create_call', JObj}); _Stat -> Updates = props:filter_undefined( [{#call_stat.caller_id_name, kz_json:get_value(<<"Caller-ID-Name">>, JObj)} ,{#call_stat.caller_id_number, kz_json:get_value(<<"Caller-ID-Number">>, JObj)} + ,{#call_stat.entered_timestamp, kz_json:get_value(<<"Entered-Timestamp">>, JObj)} + ,{#call_stat.entered_position, kz_json:get_value(<<"Entered-Position">>, JObj)} + ,{#call_stat.caller_priority, kz_json:get_integer_value(<<"Caller-Priority">>, JObj)} + ,{#call_stat.required_skills, kz_json:get_list_value(<<"Required-Skills">>, JObj, [])} ]), update_call_stat(Id, Updates, Props) end. @@ -813,9 +1431,7 @@ handle_missed_stat(JObj, Props) -> Id = call_stat_id(JObj), case find_call_stat(Id) of 'undefined' -> lager:debug("can't update stat ~s with missed data, missing", [Id]); - #call_stat{misses=Misses} -> - Updates = [{#call_stat.misses, [create_miss(JObj) | Misses]}], - update_call_stat(Id, Updates, Props) + _ -> gen_listener:cast(props:get_value('server', Props), {'add_miss', JObj}) end. -spec create_miss(kz_json:object()) -> agent_miss(). @@ -829,11 +1445,27 @@ create_miss(JObj) -> handle_abandoned_stat(JObj, Props) -> 'true' = kapi_acdc_stats:call_abandoned_v(JObj), + Id = call_stat_id(JObj), + %% If caller leaves quickly, the waiting entry might not have arrived yet + case find_call_stat(Id) of + 'undefined' -> gen_listener:cast(props:get_value('server', Props), {'create_call', JObj}); + _Stat -> + Updates = props:filter_undefined( + [{#call_stat.abandoned_reason, kz_json:get_value(<<"Abandon-Reason">>, JObj)} + ,{#call_stat.abandoned_timestamp, kz_json:get_value(<<"Abandon-Timestamp">>, JObj)} + ,{#call_stat.status, <<"abandoned">>} + ]), + update_call_stat(Id, Updates, Props) + end. + +-spec handle_marked_callback_stat(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_marked_callback_stat(JObj, Props) -> + 'true' = kapi_acdc_stats:call_marked_callback_v(JObj), + Id = call_stat_id(JObj), Updates = props:filter_undefined( - [{#call_stat.abandoned_reason, kz_json:get_value(<<"Abandon-Reason">>, JObj)} - ,{#call_stat.abandoned_timestamp, kz_json:get_value(<<"Abandon-Timestamp">>, JObj)} - ,{#call_stat.status, <<"abandoned">>} + [{#call_stat.is_callback, 'true'} + ,{#call_stat.caller_id_name, kz_json:get_value(<<"Caller-ID-Name">>, JObj)} ]), update_call_stat(Id, Updates, Props). @@ -847,17 +1479,7 @@ handle_handled_stat(JObj, Props) -> ,{#call_stat.handled_timestamp, kz_json:get_value(<<"Handled-Timestamp">>, JObj)} ,{#call_stat.status, <<"handled">>} ]), - - Stat = find_call_stat(Id), - case handled_stat_should_update(Stat) of - 'true' -> update_call_stat(Id, Updates, Props); - 'false' -> 'ok' - end. - --spec handled_stat_should_update(call_stat()) -> boolean(). - % Handle stats where processed came in before handled (hungup quickly after pickup) -handled_stat_should_update(#call_stat{status= <<"processed">>}) -> 'false'; -handled_stat_should_update(_) -> 'true'. + update_call_stat(Id, Updates, Props). -spec handle_processed_stat(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_processed_stat(JObj, Props) -> @@ -870,18 +1492,15 @@ handle_processed_stat(JObj, Props) -> ,{#call_stat.hung_up_by, kz_json:get_value(<<"Hung-Up-By">>, JObj)} ,{#call_stat.status, <<"processed">>} ]), + update_call_stat(Id, Updates, Props). - Stat = find_call_stat(Id), - Updates1 = processed_stat_maybe_fix_update(Stat, Updates), - - update_call_stat(Id, Updates1, Props). +-spec handle_exited_stat(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_exited_stat(JObj, Props) -> + 'true' = kapi_acdc_stats:call_exited_position_v(JObj), --spec processed_stat_maybe_fix_update(call_stat(), kz_term:proplist()) -> kz_term:proplist(). -processed_stat_maybe_fix_update(#call_stat{handled_timestamp='undefined'}, Updates) -> - % Handle stats where processed came in before handled (hungup quickly after pickup) - ProcessedTimestamp = props:get_integer_value(#call_stat.processed_timestamp, Updates), - [{#call_stat.handled_timestamp, ProcessedTimestamp} | Updates]; -processed_stat_maybe_fix_update(_, Updates) -> Updates. + Id = call_stat_id(JObj), + Updates = props:filter_undefined([{#call_stat.exited_position, kz_json:get_value(<<"Exited-Position">>, JObj)}]), + update_call_stat(Id, Updates, Props). -spec flush_call_stat(kz_json:object(), kz_term:proplist()) -> 'ok'. flush_call_stat(JObj, Props) -> @@ -902,30 +1521,132 @@ find_call_stat(Id) -> [Stat] -> Stat end. --spec create_call_stat(kz_term:ne_binary(), kz_json:object(), kz_term:proplist()) -> 'ok'. -create_call_stat(Id, JObj, Props) -> - gen_listener:cast(props:get_value('server', Props) - ,{'create_call', #call_stat{id = Id - ,call_id = kz_json:get_value(<<"Call-ID">>, JObj) - ,account_id = kz_json:get_value(<<"Account-ID">>, JObj) - ,queue_id = kz_json:get_value(<<"Queue-ID">>, JObj) - ,entered_timestamp = kz_json:get_value(<<"Entered-Timestamp">>, JObj) - ,misses = [] - ,status = <<"waiting">> - ,caller_id_name = kz_json:get_value(<<"Caller-ID-Name">>, JObj) - ,caller_id_number = kz_json:get_value(<<"Caller-ID-Number">>, JObj) - ,caller_priority = kz_json:get_integer_value(<<"Caller-Priority">>, JObj) - } - }). - -type updates() :: [{pos_integer(), any()}]. -spec update_call_stat(kz_term:ne_binary(), updates(), kz_term:proplist()) -> 'ok'. update_call_stat(Id, Updates, Props) -> gen_listener:cast(props:get_value('server', Props), {'update_call', Id, Updates}). +-spec maybe_add_summary_stat(call_stat()) -> boolean(). +maybe_add_summary_stat(#call_stat{status=Status}=Stat) + when Status =:= <<"processed">> + orelse Status =:= <<"abandoned">> -> + ets:insert(call_summary_table_id(), call_stat_to_summary_stat(Stat)); +maybe_add_summary_stat(_) -> 'false'. + +-spec maybe_add_agent_call_stat(call_stat()) -> boolean(). +maybe_add_agent_call_stat(#call_stat{status= <<"handled">>}=Stat) -> + ets:insert(agent_call_table_id(), call_stat_to_agent_call_stat(Stat)); +maybe_add_agent_call_stat(_) -> 'false'. + +-spec add_agent_call_stat_miss(call_stat(), kz_term:ne_binary(), non_neg_integer()) -> 'true'. +add_agent_call_stat_miss(Stat, AgentId, Timestamp) -> + AgentStat = call_stat_to_agent_call_stat(Stat), + AgentStat1 = AgentStat#agent_call_stat{agent_id=AgentId + ,status= <<"missed">> + ,timestamp=Timestamp + }, + ets:insert(agent_call_table_id(), AgentStat1). + +-spec maybe_insert_agent_cur_status(status_stat(), status_stat()) -> boolean(). +maybe_insert_agent_cur_status(#status_stat{status= <<"ready">>} + ,#status_stat{status= <<"logged_in">>} + ) -> + %% Note do not update status ready to logged_in as it doesn't transistion back to ready + false; +maybe_insert_agent_cur_status(#status_stat{status= <<"logged_out">>, timestamp=Timestamp} + ,#status_stat{status= <<"pending_logged_out">>, timestamp=Timestamp1}=Stat + ) -> + %% Note the timestamp must be GREATER if new stat is pending (fix logout bug) + case Timestamp1 > Timestamp of + 'true' -> ets:insert(acdc_agent_stats:agent_cur_status_table_id(), Stat); + 'false' -> 'false' + end; +maybe_insert_agent_cur_status(#status_stat{timestamp=Timestamp} + ,#status_stat{timestamp=Timestamp1}=Stat + ) -> + case Timestamp1 >= Timestamp of + 'true' -> ets:insert(acdc_agent_stats:agent_cur_status_table_id(), Stat); + 'false' -> 'false' + end. + +-spec call_stat_to_summary_stat(call_stat()) -> call_summary_stat(). +call_stat_to_summary_stat(#call_stat{id=Id + ,call_id=CallId + ,account_id=AccountId + ,queue_id=QueueId + ,entered_timestamp=EnteredTimestamp + ,abandoned_timestamp=AbandonedTimestamp + ,handled_timestamp=HandledTimestamp + ,processed_timestamp=ProcessedTimestamp + ,entered_position=EnteredPos + ,status=Status + }) -> + #call_summary_stat{id=Id + ,account_id=AccountId + ,queue_id=QueueId + ,call_id=CallId + ,status=Status + ,entered_position=EnteredPos + ,wait_time=wait_time(EnteredTimestamp, AbandonedTimestamp, HandledTimestamp) + ,talk_time=talk_time(HandledTimestamp, ProcessedTimestamp) + ,timestamp=kz_time:current_tstamp() + }. + +-spec call_stat_to_agent_call_stat(call_stat()) -> agent_call_stat(). +call_stat_to_agent_call_stat(#call_stat{id=Id + ,call_id=CallId + ,account_id=AccountId + ,queue_id=QueueId + ,agent_id=AgentId + ,status=Status + ,handled_timestamp=HandledTimestamp + }) -> + #agent_call_stat{id=Id + ,account_id=AccountId + ,queue_id=QueueId + ,agent_id=AgentId + ,call_id=CallId + ,status=Status + ,timestamp=HandledTimestamp + }. +%% Logic to determine current queue where a call just ended and publish summary stats over EDR +%% This serves to reliably update the dashboard in case of duplicate events +-spec maybe_send_summary_stat(call_stat()) -> boolean(). +maybe_send_summary_stat(#call_stat{status=Status}=Stat) + when Status =:= <<"processed">> + orelse Status =:= <<"abandoned">> -> + JObj = call_stat_to_json(Stat), + Limit = acdc_stats_util:get_query_limit(JObj), + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + QueueId = kz_json:get_value(<<"Queue-ID">>, JObj), + StatQuery = kz_json:from_list( + [{<<"Account-ID">>, AccountId} + ,{<<"Queue-ID">>, QueueId} + ]), + Summary = case call_summary_build_match_spec(StatQuery) of + {'ok', Match} -> query_call_summary(Match, Limit); + {'error', _Errors}=E -> E + end, + EdrJObj = kz_json:from_list( + [{<<"Account-ID">>, AccountId} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Event">>, <<"call_summary">>} + ,{<<"Calls-Summary">>, kz_json:get_value([<<"Data">>, QueueId], kz_json:from_list(Summary))} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + edr_log_stats_summary(AccountId, EdrJObj); +maybe_send_summary_stat(_) -> 'false'. + +-spec call_state_change(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. call_state_change(AccountId, Status, Prop) -> Body = kz_json:normalize(kz_json:from_list([{<<"Event">>, <<"call_status_change">>} - ,{<<"Status">>, kz_term:to_binary(Status)} + ,{<<"Status">>, Status} | Prop ])), kz_edr:event(?APP_NAME, ?APP_VERSION, 'ok', 'info', Body, AccountId). + +-spec edr_log_stats_summary(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. +edr_log_stats_summary(AccountId, JObj) -> + lager:warning("emitting stats via EDR ~s: ~p", [AccountId, JObj]), + Body = kz_json:normalize(JObj), + kz_edr:event(?APP_NAME, ?APP_VERSION, 'ok', 'info', Body, AccountId). diff --git a/applications/acdc/src/acdc_stats.hrl b/applications/acdc/src/acdc_stats.hrl index eb53929eaf2..b140fb962b3 100644 --- a/applications/acdc/src/acdc_stats.hrl +++ b/applications/acdc/src/acdc_stats.hrl @@ -8,9 +8,12 @@ -define(STATS_QUERY_LIMITS_ENABLED, kapps_config:get_is_true(?CONFIG_CAT, <<"stats_query_limits_enabled">>, 'true')). -define(MAX_RESULT_SET, kapps_config:get_integer(?CONFIG_CAT, <<"max_result_set">>, 25)). +%% Wiggle room for queries in case the AMQP message is delayed a little +-define(QUERY_WINDOW_WIGGLE_ROOM_S, 5). + -record(agent_miss, {agent_id :: kz_term:api_binary() ,miss_reason :: kz_term:api_binary() - ,miss_timestamp = kz_time:now_s() :: pos_integer() + ,miss_timestamp = kz_time:current_tstamp() :: pos_integer() }). -type agent_miss() :: #agent_miss{}. -type agent_misses() :: [agent_miss()]. @@ -22,48 +25,68 @@ ,agent_id :: kz_term:api_binary() | '$3' | '_' % the handling agent - ,entered_timestamp = kz_time:now_s() :: pos_integer() | '$1' | '$5' | '_' + ,entered_timestamp = kz_time:current_tstamp() :: pos_integer() | '$1' | '$5' | '_' ,abandoned_timestamp :: kz_term:api_integer() | '$2' | '_' ,handled_timestamp :: kz_term:api_integer() | '$3' | '_' ,processed_timestamp :: kz_term:api_integer() | '_' ,hung_up_by :: kz_term:api_binary() | '_' - ,abandoned_reason :: kz_term:api_binary() | '_' + ,entered_position :: kz_term:api_integer() | '_' + ,exited_position :: kz_term:api_integer() | '_' + ,abandoned_reason :: kz_term:api_binary() | '_' + ,is_callback = 'false' :: boolean() | '_' ,misses = [] :: agent_misses() | '_' ,status :: kz_term:api_binary() | '$1' | '$2' | '$4' | '_' ,caller_id_name :: kz_term:api_binary() | '_' ,caller_id_number :: kz_term:api_binary() | '_' ,caller_priority :: kz_term:api_integer() | '_' + ,required_skills = [] :: kz_term:ne_binaries() | '_' ,is_archived = 'false' :: boolean() | '$2' | '$3' | '_' }). -type call_stat() :: #call_stat{}. --type call_stats() :: [call_stat()]. +-record(call_summary_stat, {id :: kz_term:api_binary() | '_' + ,account_id :: kz_term:api_binary() | '$1' + ,queue_id :: kz_term:api_binary() | '$2' | '_' + ,call_id :: kz_term:api_binary() | '_' + ,status :: kz_term:api_binary() | '$3' | '_' + ,entered_position :: kz_term:api_integer() | '_' + ,wait_time :: kz_term:api_integer() | '_' + ,talk_time :: kz_term:api_integer() | '_' + ,timestamp :: kz_term:api_integer() | '_' + ,is_archived = 'false' :: boolean() | '$1' | '$2' | '_' + }). +-type call_summary_stat() :: #call_summary_stat{}. + +-record(agent_call_stat, {id :: kz_term:api_binary() | '_' + ,account_id :: kz_term:api_binary() | '$1' + ,queue_id :: kz_term:api_binary() | '_' + ,agent_id :: kz_term:api_binary() | '_' + ,call_id :: kz_term:api_binary() | '_' + ,status :: kz_term:api_binary() | '_' + ,timestamp :: kz_term:api_integer() | '_' + }). +-type agent_call_stat() :: #agent_call_stat{}. -define(STATUS_STATUSES, [<<"logged_in">>, <<"logged_out">>, <<"ready">> ,<<"connecting">>, <<"connected">> ,<<"wrapup">>, <<"paused">>, <<"outbound">> ]). - -%% This key optimizes lookups in the ordered_set ETS table --record(status_stat_key, {account_id = '_' :: kz_term:ne_binary() | '$1' | '_' - ,agent_id = '_' :: kz_term:ne_binary() | '$2' | '_' - ,timestamp = '_' :: pos_integer() | '$1' | '$3' | '_' - }). --type status_stat_key() :: #status_stat_key{}. --record(status_stat, {key = '_' :: status_stat_key() | '_' - ,id :: kz_term:api_binary() | '_' +-record(status_stat, {id :: kz_term:api_binary() | '_' + ,agent_id :: kz_term:api_binary() | '$2' | '_' + ,account_id :: kz_term:api_binary() | '$1' | '_' ,status :: kz_term:api_binary() | '$4' | '_' + ,timestamp :: kz_term:api_pos_integer() | '$1' | '$3' | '$5' | '_' ,wait_time :: kz_term:api_integer() | '_' ,pause_time :: kz_term:api_integer() | '_' + ,pause_alias :: kz_term:api_binary() | '_' ,callid :: kz_term:api_binary() | '_' ,caller_id_name :: kz_term:api_binary() | '_' ,caller_id_number :: kz_term:api_binary() | '_' - ,queue_id :: kz_term:api_binary() | '_' ,is_archived = 'false' :: boolean() | '$1' | '$2' | '_' }). -type status_stat() :: #status_stat{}. diff --git a/applications/acdc/src/acdc_presence_realm_lookup.erl b/applications/acdc/src/acdc_stats_etsmgr.erl similarity index 51% rename from applications/acdc/src/acdc_presence_realm_lookup.erl rename to applications/acdc/src/acdc_stats_etsmgr.erl index f375646636b..6171c4967d4 100644 --- a/applications/acdc/src/acdc_presence_realm_lookup.erl +++ b/applications/acdc/src/acdc_stats_etsmgr.erl @@ -1,25 +1,20 @@ %%%----------------------------------------------------------------------------- -%%% @copyright (C) 2019-, Voxter Communications Inc -%%% @author Daniel Finke -%%% @doc Serialize requests to look up an account ID by realm in order to reduce -%%% overhead when Kamailio nodes are restarted and presence probes are performed -%%% for all new registrations -%%% +%%% @copyright (C) 2010-2020, 2600Hz +%%% @doc Manage the ETS table lookup for token server to account/client IP +%%% @author James Aimonetti %%% +%%% @author James Aimonetti %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. %%% %%% @end %%%----------------------------------------------------------------------------- --module(acdc_presence_realm_lookup). - +-module(acdc_stats_etsmgr). -behaviour(gen_server). %% API --export([start_link/0 - ,lookup/1 - ]). +-export([start_link/2]). %% gen_server callbacks -export([init/1 @@ -34,7 +29,10 @@ -define(SERVER, ?MODULE). --record(state, {}). +-record(state, {table_id :: ets:tid() | 'undefined' + ,etssrv :: kz_term:api_pid() + ,give_away_ref :: kz_term:api_reference() + }). -type state() :: #state{}. %%%============================================================================= @@ -43,70 +41,109 @@ %%------------------------------------------------------------------------------ %% @doc Starts the server -%% %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). -start_link() -> - gen_server:start_link({'local', ?SERVER}, ?MODULE, [], []). - -%%------------------------------------------------------------------------------ -%% @doc Look up the account ID corresponding to a given realm -%% -%% @end -%%------------------------------------------------------------------------------ --spec lookup(kz_term:ne_binary()) -> kz_term:ne_binary() | 'not_found'. -lookup(Realm) -> - gen_server:call(?SERVER, {'lookup', Realm}). +-spec start_link(ets:tab(), any()) -> kz_term:startlink_ret(). +start_link(TableId, TableOptions) -> + gen_server:start_link(?SERVER, [TableId, TableOptions], []). %%%============================================================================= %%% gen_server callbacks %%%============================================================================= %%------------------------------------------------------------------------------ +%% @private %% @doc Initializes the server -%% %% @end %%------------------------------------------------------------------------------ --spec init([]) -> {'ok', state()}. -init([]) -> +-spec init(list()) -> {'ok', #state{}}. +init([TableId, TableOptions]) -> + kz_log:put_callid(?MODULE), + gen_server:cast(self(), {'begin', TableId, TableOptions}), + lager:debug("started etsmgr for stats for ~s", [TableId]), {'ok', #state{}}. %%------------------------------------------------------------------------------ +%% @private %% @doc Handling call messages %% %% @end %%------------------------------------------------------------------------------ -spec handle_call(any(), kz_term:pid_ref(), state()) -> kz_types:handle_call_ret_state(state()). -handle_call({'lookup', Realm}, _, State) -> - case kapps_util:get_account_by_realm(Realm) of - {'ok', AcctDb} -> - AccountId = kzs_util:format_account_id(AcctDb), - {'reply', AccountId, State}; - _ -> {'reply', 'not_found', State} - end; handle_call(_Request, _From, State) -> - {'reply', 'ok', State}. + lager:debug("unhandled call: ~p", [_Request]), + {'reply', {'error', 'not_implemented'}, State}. %%------------------------------------------------------------------------------ +%% @private %% @doc Handling cast messages %% %% @end %%------------------------------------------------------------------------------ -spec handle_cast(any(), state()) -> kz_types:handle_cast_ret_state(state()). +handle_cast({'begin', TableId, TableOptions}, State) -> + Tbl = ets:new(TableId, TableOptions), + + ets:setopts(Tbl, {'heir', self(), 'ok'}), + {'noreply', State#state{table_id=Tbl + ,give_away_ref=send_give_away_retry(Tbl, 'ok', 0) + }}; handle_cast(_Msg, State) -> + lager:debug("unhandled cast: ~p", [_Msg]), {'noreply', State}. %%------------------------------------------------------------------------------ +%% @private %% @doc Handling all non call/cast messages %% %% @end %%------------------------------------------------------------------------------ -spec handle_info(any(), state()) -> kz_types:handle_info_ret_state(state()). +handle_info({'ETS-TRANSFER', Tbl, Etssrv, Data}, #state{table_id=Tbl + ,etssrv=Etssrv + ,give_away_ref='undefined' + }=State) -> + lager:debug("ets table ~p transferred back to ourselves", [Tbl]), + {'noreply', State#state{etssrv='undefined' + ,give_away_ref=send_give_away_retry(Tbl, Data, 0) + }}; +handle_info({'give_away', Tbl, Data}, #state{table_id=Tbl + ,etssrv='undefined' + ,give_away_ref=Ref + }=State) when is_reference(Ref) -> + lager:debug("give away ~p: ~p", [Tbl, Data]), + case find_ets_mgr(Tbl, Data) of + P when is_pid(P) -> + lager:debug("handing tbl ~p back to ~p and then to ~p", [Tbl, self(), P]), + {'noreply', State#state{etssrv=P + ,give_away_ref='undefined' + }}; + Ref when is_reference(Ref) -> + lager:debug("ets mgr died already, hasn't resumed life yet; waiting"), + {'noreply', State#state{etssrv='undefined' + ,give_away_ref=Ref + }} + end; handle_info(_Info, State) -> + lager:debug("unhandled message: ~p", [_Info]), {'noreply', State}. +find_ets_mgr(Tbl, Data) -> + case acdc_stats_sup:stats_srv() of + {'error', 'not_found'} -> send_give_away_retry(Tbl, Data); + {'ok', P} when is_pid(P) -> + link(P), + ets:give_away(Tbl, P, Data), + P + end. + +send_give_away_retry(Tbl, Data) -> + send_give_away_retry(Tbl, Data, 10). +send_give_away_retry(Tbl, Data, Timeout) -> + erlang:send_after(Timeout, self(), {'give_away', Tbl, Data}). + %%------------------------------------------------------------------------------ +%% @private %% @doc This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates @@ -116,9 +153,11 @@ handle_info(_Info, State) -> %%------------------------------------------------------------------------------ -spec terminate(any(), state()) -> 'ok'. terminate(_Reason, _State) -> - 'ok'. + lager:debug("ETS mgr going down: ~p", [_Reason]), + ok. %%------------------------------------------------------------------------------ +%% @private %% @doc Convert process state when code is changed %% %% @end diff --git a/applications/acdc/src/acdc_stats_sup.erl b/applications/acdc/src/acdc_stats_sup.erl index 6084408d0de..ff023aed8a0 100644 --- a/applications/acdc/src/acdc_stats_sup.erl +++ b/applications/acdc/src/acdc_stats_sup.erl @@ -3,6 +3,7 @@ %%% @doc Manage the bucket servers %%% @author James Aimonetti %%% +%%% @author James Aimonetti %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -25,12 +26,11 @@ -define(SERVER, ?MODULE). --define(ETSMGR_OPTS(TableId, TableOpts), [{'table_id', TableId} - ,{'table_options', TableOpts} - ,{'find_me_function', fun etsmgr_find_me_fun/0} - ]). --define(CHILDREN, [?WORKER_NAME_ARGS('kazoo_etsmgr_srv', 'acdc_stats_call', [?ETSMGR_OPTS(acdc_stats:call_table_id(), acdc_stats:call_table_opts())]) - ,?WORKER_NAME_ARGS('kazoo_etsmgr_srv', 'acdc_stats_status', [?ETSMGR_OPTS(acdc_agent_stats:status_table_id(), acdc_agent_stats:status_table_opts())]) +-define(CHILDREN, [?WORKER_NAME_ARGS('acdc_stats_etsmgr', 'acdc_stats_call', [acdc_stats:call_table_id(), acdc_stats:call_table_opts()]) + ,?WORKER_NAME_ARGS('acdc_stats_etsmgr', 'acdc_stats_call_summary', [acdc_stats:call_summary_table_id(), acdc_stats:call_summary_table_opts()]) + ,?WORKER_NAME_ARGS('acdc_stats_etsmgr', 'acdc_stats_agent_call', [acdc_stats:agent_call_table_id(), acdc_stats:agent_call_table_opts()]) + ,?WORKER_NAME_ARGS('acdc_stats_etsmgr', 'acdc_stats_status', [acdc_agent_stats:status_table_id(), acdc_agent_stats:status_table_opts()]) + ,?WORKER_NAME_ARGS('acdc_stats_etsmgr', 'acdc_stats_agent_cur_status', [acdc_agent_stats:agent_cur_status_table_id(), acdc_agent_stats:agent_cur_status_table_opts()]) ,?WORKER('acdc_stats') ]). @@ -39,10 +39,10 @@ %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Starts the supervisor. +%% @doc Starts the supervisor %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> supervisor:start_link({'local', ?SERVER}, ?MODULE, []). @@ -55,19 +55,13 @@ stats_srv() -> _ -> {'error', 'not_found'} end. --spec etsmgr_find_me_fun() -> kz_term:api_pid(). -etsmgr_find_me_fun() -> - case stats_srv() of - {'ok', P} -> P; - {'error', 'not_found'} -> 'undefined' - end. - %%%============================================================================= %%% Supervisor callbacks %%%============================================================================= %%------------------------------------------------------------------------------ -%% @doc Whenever a supervisor is started using `supervisor:start_link/[2,3]', +%% @private +%% @doc Whenever a supervisor is started using supervisor:start_link/[2,3], %% this function is called by the new process to find out about %% restart strategy, maximum restart frequency and child %% specifications. diff --git a/applications/acdc/src/acdc_stats_util.erl b/applications/acdc/src/acdc_stats_util.erl index f84b96c734d..7f34bbf07e0 100644 --- a/applications/acdc/src/acdc_stats_util.erl +++ b/applications/acdc/src/acdc_stats_util.erl @@ -3,6 +3,7 @@ %%% @doc Stat util functions %%% @author James Aimonetti %%% +%%% @author James Aimonetti %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -15,10 +16,19 @@ ,pause_time/2 ,caller_id_name/2 ,caller_id_number/2 - ,queue_id/2 ,get_query_limit/1 + ,apply_query_window_wiggle_room/2 ,db_name/1 + ,prev_modb/1 + + ,cleanup_old_stats/0 + ,cleanup_old_calls/1, cleanup_old_statuses/1 + + ,call_summary_req/1 + ,publish_summary_data/4 + ,publish_call_query_errors/3 + ,publish_query_errors/4 ]). -include("acdc.hrl"). @@ -36,7 +46,7 @@ pause_time(<<"paused">>, JObj) -> end; pause_time(_, _JObj) -> 'undefined'. --spec caller_id_name(any(), kz_json:object()) -> kz_term:api_ne_binary(). +-spec caller_id_name(any(), kz_json:object()) -> api_kz_term:ne_binary(). caller_id_name(_, JObj) -> kz_json:get_value(<<"Caller-ID-Name">>, JObj). @@ -44,10 +54,6 @@ caller_id_name(_, JObj) -> caller_id_number(_, JObj) -> kz_json:get_value(<<"Caller-ID-Number">>, JObj). --spec queue_id(any(), kz_json:object()) -> kz_term:ne_binary(). -queue_id(_, JObj) -> - kz_json:get_value(<<"Queue-ID">>, JObj). - -spec get_query_limit(kz_json:object()) -> pos_integer() | 'no_limit'. get_query_limit(JObj) -> get_query_limit(JObj, ?STATS_QUERY_LIMITS_ENABLED). @@ -68,6 +74,212 @@ get_query_limit(JObj, 'false') -> N -> N end. +%%------------------------------------------------------------------------------ +%% @doc If a query timestamp value is less than the minimum permitted by +%% validation, allow a little wiggle room in case the request just took a little +%% while to be processed. +%% @end +%%------------------------------------------------------------------------------ +-spec apply_query_window_wiggle_room(pos_integer(), pos_integer()) -> pos_integer(). +apply_query_window_wiggle_room(Timestamp, Minimum) -> + Offset = Minimum - Timestamp, + WithinWiggleRoom = Offset < ?QUERY_WINDOW_WIGGLE_ROOM_S, + case Offset =< 0 of + 'true' -> Timestamp; + 'false' when WithinWiggleRoom -> Minimum; + 'false' -> Timestamp + end. + -spec db_name(kz_term:ne_binary()) -> kz_term:ne_binary(). db_name(Account) -> kzs_util:format_account_mod_id(Account). +db_name(Account, {Yr, Mn}) -> + kzs_util:format_account_mod_id(Account, Yr, Mn); +db_name(Account, Timestamp) -> + kzs_util:format_account_mod_id(Account, Timestamp). + + +-spec prev_modb(kz_term:ne_binary()) -> kz_term:ne_binary(). +prev_modb(Account) -> + {{Year, Month, _}, _} = calendar:now_to_universal_time(os:timestamp()), + prev_modb(Account, Year, Month-1). + +-spec prev_modb(kz_term:ne_binary(), calendar:year(), integer()) -> kz_term:ne_binary(). +prev_modb(Account, Year, 0) -> + prev_modb(Account, Year-1, 12); +prev_modb(Account, Year, Month) -> + kzs_util:format_account_id(Account, Year, Month). + +-spec cleanup_old_stats() -> 'ok'. +cleanup_old_stats() -> + cleanup_old_calls(1200), + cleanup_old_statuses(14400). + +-spec cleanup_old_calls(pos_integer()) -> 'ok'. +cleanup_old_calls(Window) -> + acdc_stats:manual_cleanup_calls(Window). + +-spec cleanup_old_statuses(pos_integer()) -> 'ok'. +cleanup_old_statuses(Window) -> + acdc_stats:manual_cleanup_statuses(Window). + + +-spec call_summary_req(kz_json:object()) -> 'ok'. +call_summary_req(JObj) -> + RespQ = kz_json:get_value(<<"Server-ID">>, JObj), + MsgId = kz_json:get_value(<<"Msg-ID">>, JObj), + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + StartRange = kz_json:get_value(<<"Start-Range">>, JObj), + EndRange = kz_json:get_value(<<"End-Range">>, JObj), + Queues = + case kz_json:get_value(<<"Queue-ID">>, JObj) of + undefined -> [ {A,Q,StartRange,EndRange} || {_, {A, Q}} <- acdc_queues_sup:queues_running(), A == AccountId]; + Else -> [ {AccountId,Else,StartRange,EndRange}] + end, + Summary = query_call_summary(Queues), + publish_summary_data(RespQ, MsgId, Summary, []). + +-spec query_call_summary([kz_term:ne_binary()]) -> kz_term:proplist(). +query_call_summary(Queues) -> + QueryResults = + lists:filter(fun(X) -> not kz_json:is_empty(X) end, + lists:foldl(fun query_call_summary_fold/2, [], Queues)), + + JsonResult = lists:foldl(fun(QR, JObj) -> + TotalCalls = kz_json:get_value(<<"calls">>, QR), + AbandonedCalls = kz_json:get_value(<<"abandoned">>, QR), + QueueId = kz_json:get_value(<<"Queue-ID">>, QR), + QueueJObj = kz_json:set_values([{<<"total_calls">>, TotalCalls } + ,{<<"abandoned_calls">>, AbandonedCalls} + ,{<<"average_wait_time">>, kz_json:get_value(<<"wait_time">>, QR) div TotalCalls} + ,{<<"average_talk_time">>, kz_json:get_value(<<"talk_time">>, QR) div (TotalCalls - AbandonedCalls)} + ,{<<"max_entered_position">>, kz_json:get_value(<<"entered_position">>, QR)} + ] + ,kz_json:new()), + kz_json:set_value(QueueId, QueueJObj, JObj) + end + ,kz_json:new() + ,QueryResults), + + [{<<"Data">>, JsonResult}]. + +-spec query_call_summary_fold(kz_term:ne_binary(), kz_term:proplist()) -> [kz_json:object()]. +query_call_summary_fold({AccountId, _QueueId, StartRange, EndRange} = Data, Acc) -> + StartMODB = db_name(AccountId, StartRange), + EndMODB = db_name(AccountId, EndRange), + case StartMODB =:= EndMODB of + true -> [get_results_from_db(StartMODB, Data)|Acc]; + false -> [get_results_from_dbs(modb_range(AccountId, StartRange, EndRange), Data)|Acc] + end. + +get_results_from_db(DB, {AccountId, QueueId, StartRange, EndRange}) -> + Opts = [{'startkey', [QueueId, StartRange]} + ,{'endkey', [QueueId, EndRange]} + ,{'limit', 1} + ,{'reduce', true} + ], + case kz_datamgr:get_results(DB, <<"call_stats/call_summary">>, Opts) of + {'ok', []} -> kz_json:new(); + {'ok', [JObj]} -> + V1 = kz_json:get_value(<<"value">>, JObj), + V2 = kz_json:set_values([{<<"Account-ID">>, AccountId}, + {<<"Queue-ID">>,QueueId}], + V1), + V2; + {'error', _E} -> + lager:debug("error querying view: ~p", [_E]), + kz_json:new() + end. + +get_results_from_dbs(DBs, Data) -> + lists:foldl(fun(DB, Acc) -> + H = get_results_from_db(DB, Data), + merge_results(H, Acc) + end, kz_json:new(), DBs). + +merge_results(JObj1, JObj2) -> + Fun = fun(_,{both, V1, V2}) when is_integer(V1), is_integer(V2) -> {ok, V1 + V2}; + (_,{both, V, V}) -> {ok, V} end, + kz_json:merge(Fun, [JObj1, JObj2]). + +modb_range(AccountId, StartRange, EndRange) -> + {{SY,SM,_}, _} = calendar:gregorian_seconds_to_datetime(StartRange), + {{EY,EM,_}, _} = calendar:gregorian_seconds_to_datetime(EndRange), + modb_db_list(AccountId, {SY,SM}, {EY,EM}, []). + +modb_db_list(AccountId, Next, End, Acc) + when Next =:= End -> + [db_name(AccountId, Next)|Acc]; +modb_db_list(AccountId, Next, End, Acc) -> + modb_db_list(AccountId, next_modb(Next), End, [db_name(AccountId, Next)|Acc]). + +next_modb({Yr,Mn}) -> + case Mn of + 12 -> {Yr+1, 1}; + _ -> {Yr, Mn + 1} + end. + +-spec publish_summary_data(kz_term:ne_binary() + ,kz_term:ne_binary() + ,kz_term:proplist() | {'error', _} + ,kz_term:proplist() | {'error', _}) -> 'ok'. +publish_summary_data(RespQ, MsgId, {'error', Errors}, _) -> + publish_call_summary_query_errors(RespQ, MsgId, Errors); +publish_summary_data(RespQ, MsgId, _, {'error', Errors}) -> + publish_call_query_errors(RespQ, MsgId, Errors); +publish_summary_data(RespQ, MsgId, Summary, []) -> + Resp = Summary ++ + kz_api:default_headers(?APP_NAME, ?APP_VERSION) ++ + [{<<"Query-Time">>, kz_time:current_tstamp()} + ,{<<"Msg-ID">>, MsgId} + ], + kapi_acdc_stats:publish_call_summary_resp(RespQ, Resp); +publish_summary_data(RespQ, MsgId, Summary, Active) -> + Resp = Summary ++ + remove_missed(Active) ++ + kz_api:default_headers(?APP_NAME, ?APP_VERSION) ++ + [{<<"Query-Time">>, kz_time:current_tstamp()} + ,{<<"Msg-ID">>, MsgId} + ], + kapi_acdc_stats:publish_call_summary_resp(RespQ, Resp). + + +-spec publish_call_query_errors(kz_term:ne_binary() + ,kz_term:ne_binary() + ,kz_term:proplist() | {'error', _}) -> 'ok'. +publish_call_query_errors(RespQ, MsgId, Errors) -> + publish_query_errors(RespQ, MsgId, Errors, fun kapi_acdc_stats:publish_current_calls_err/2). + +-spec publish_call_summary_query_errors(kz_term:ne_binary() + ,kz_term:ne_binary() + ,kz_term:proplist() | {'error', _}) -> 'ok'. +publish_call_summary_query_errors(RespQ, MsgId, Errors) -> + publish_query_errors(RespQ, MsgId, Errors, fun kapi_acdc_stats:publish_call_summary_err/2). + +-spec publish_query_errors(kz_term:ne_binary() + ,kz_term:ne_binary() + ,kz_term:proplist() | {'error', _} + ,fun()) -> 'ok'. +publish_query_errors(RespQ, MsgId, Errors, PubFun) -> + API = [{<<"Error-Reason">>, Errors} + ,{<<"Msg-ID">>, MsgId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + lager:debug("responding with errors to req ~s: ~p", [MsgId, Errors]), + PubFun(RespQ, API). + +-spec remove_missed(kz_term:proplist()) -> kz_term:proplist(). +remove_missed(Active) -> + [{<<"Waiting">>, remove_misses_fold(props:get_value(<<"Waiting">>, Active, []))} + ,{<<"Handled">>, remove_misses_fold(props:get_value(<<"Handled">>, Active, []))} + ]. + +-spec remove_misses_fold(kz_json:objects()) -> kz_json:objects(). +remove_misses_fold(JObjs) -> + remove_misses_fold(JObjs, []). + +-spec remove_misses_fold(kz_json:objects(), kz_json:objects()) -> kz_json:objects(). +remove_misses_fold([], Acc) -> + Acc; +remove_misses_fold([JObj|JObjs], Acc) -> + remove_misses_fold(JObjs, [kz_json:delete_key(<<"misses">>, JObj) | Acc]). diff --git a/applications/acdc/src/acdc_sup.erl b/applications/acdc/src/acdc_sup.erl index 1e35919e585..97609df741b 100644 --- a/applications/acdc/src/acdc_sup.erl +++ b/applications/acdc/src/acdc_sup.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -24,7 +23,6 @@ -define(SERVER, ?MODULE). -define(CHILDREN, [?CACHE(?CACHE_NAME) - ,?WORKER('acdc_presence_realm_lookup') ,?SUPER('acdc_recordings_sup') ,?SUPER('acdc_agents_sup') ,?SUPER('acdc_queues_sup') @@ -35,21 +33,21 @@ ,?WORKER('acdc_listener') ]). -%%============================================================================== +%% =================================================================== %% API functions -%%============================================================================== +%% =================================================================== %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ --spec start_link() -> kz_types:startlink_ret(). +-spec start_link() -> kz_term:startlink_ret(). start_link() -> supervisor:start_link({'local', ?SERVER}, ?MODULE, []). -%%============================================================================== +%% =================================================================== %% Supervisor callbacks -%%============================================================================== +%% =================================================================== %%------------------------------------------------------------------------------ %% @doc diff --git a/applications/acdc/src/acdc_util.erl b/applications/acdc/src/acdc_util.erl index 5c85673424b..357d9491143 100644 --- a/applications/acdc/src/acdc_util.erl +++ b/applications/acdc/src/acdc_util.erl @@ -2,7 +2,6 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -13,6 +12,7 @@ -export([get_endpoints/2 ,bind_to_call_events/1, bind_to_call_events/2 + ,b_bind_to_call_events/2 ,unbind_from_call_events/1 ,unbind_from_call_events/2 ,agents_in_queue/2 @@ -22,11 +22,15 @@ ,agent_presence_update/2 ,presence_update/3, presence_update/4 ,send_cdr/2 + ,caller_id/1 ,hangup_cause/1 + ,max_priority/2 ]). -include("acdc.hrl"). +-define(CB_AGENTS_LIST, <<"queues/agents_listing">>). + -define(CALL_EVENT_RESTRICTIONS, ['CHANNEL_CREATE' ,'CHANNEL_ANSWER' ,'CHANNEL_BRIDGE', 'CHANNEL_UNBRIDGE' @@ -38,27 +42,27 @@ ]). -spec queue_presence_update(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -queue_presence_update(AcctId, QueueId) -> - case kapi_acdc_queue:queue_size(AcctId, QueueId) of - 0 -> presence_update(AcctId, QueueId, ?PRESENCE_GREEN); - N when is_integer(N), N > 0 -> presence_update(AcctId, QueueId, ?PRESENCE_RED_FLASH); - _N -> lager:debug("queue size for ~s(~s): ~p", [QueueId, AcctId, _N]) +queue_presence_update(AccountId, QueueId) -> + case kapi_acdc_queue:queue_size(AccountId, QueueId) of + 0 -> presence_update(AccountId, QueueId, ?PRESENCE_GREEN); + N when is_integer(N), N > 0 -> presence_update(AccountId, QueueId, ?PRESENCE_RED_FLASH); + _N -> lager:debug("queue size for ~s(~s): ~p", [QueueId, AccountId, _N]) end. -spec agent_presence_update(kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -agent_presence_update(AcctId, AgentId) -> - case acdc_agents_sup:find_agent_supervisor(AcctId, AgentId) of - 'undefined' -> presence_update(AcctId, AgentId, ?PRESENCE_RED_SOLID); - P when is_pid(P) -> presence_update(AcctId, AgentId, ?PRESENCE_GREEN) +agent_presence_update(AccountId, AgentId) -> + case acdc_agents_sup:find_agent_supervisor(AccountId, AgentId) of + 'undefined' -> presence_update(AccountId, AgentId, ?PRESENCE_RED_SOLID); + P when is_pid(P) -> presence_update(AccountId, AgentId, ?PRESENCE_GREEN) end. -spec presence_update(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -presence_update(AcctId, PresenceId, State) -> - presence_update(AcctId, PresenceId, State, kz_term:to_hex_binary(crypto:hash('md5', PresenceId))). +presence_update(AccountId, PresenceId, State) -> + presence_update(AccountId, PresenceId, State, kz_term:to_hex_binary(crypto:hash('md5', PresenceId))). -spec presence_update(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. -presence_update(AcctId, PresenceId, State, CallId) -> - {'ok', AcctDoc} = kzd_accounts:fetch(AcctId), +presence_update(AccountId, PresenceId, State, CallId) -> + {'ok', AcctDoc} = kzd_accounts:fetch(AccountId), To = <>, AcctDoc))/binary>>, lager:debug("sending presence update '~s' to '~s'", [State, To]), @@ -85,17 +89,15 @@ send_cdr(Url, JObj, Retries) -> end. %% Returns the list of agents configured for the queue --spec agents_in_queue(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_json:path(). +-spec agents_in_queue(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_json:objects(). agents_in_queue(AcctDb, QueueId) -> - case kz_datamgr:get_results(AcctDb, <<"queues/agents_listing">> - ,[{'startkey', [QueueId]} - ,{'endkey', [QueueId, kz_json:new()]} + case kz_datamgr:get_results(AcctDb, ?CB_AGENTS_LIST + ,[{'key', QueueId} ,{'reduce', 'false'} ]) of - {'ok', []} -> []; {'error', _E} -> lager:debug("failed to lookup agents for ~s: ~p", [QueueId, _E]), []; - {'ok', As} -> [kz_json:get_value(<<"value">>, A) || A <- As] + {'ok', As} -> As end. -spec agent_devices(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_json:objects(). @@ -111,7 +113,9 @@ agent_devices(AcctDb, AgentId) -> -spec get_endpoints(kapps_call:call(), kz_term:ne_binary() | kazoo_data:get_results_return()) -> kz_json:objects(). get_endpoints(Call, ?NE_BINARY = AgentId) -> - Params = kz_json:from_list([{<<"source">>, kz_term:to_binary(?MODULE)}]), + Params = kz_json:from_list([{<<"source">>, kz_term:to_binary(?MODULE)} + ,{<<"can_call_self">>, 'true'} + ]), kz_endpoints:by_owner_id(AgentId, Params, Call). %% Handles subscribing/unsubscribing from call events @@ -126,6 +130,11 @@ bind_to_call_events(?NE_BINARY = CallId, Pid) -> bind_to_call_events({CallId, _}, Pid) -> bind_to_call_events(CallId, Pid); bind_to_call_events(Call, Pid) -> bind_to_call_events(kapps_call:call_id(Call), Pid). +-spec b_bind_to_call_events(kz_term:api_binary(), pid()) -> 'ok'. +b_bind_to_call_events('undefined', _) -> 'ok'; +b_bind_to_call_events(CallId, Pid) -> + gen_listener:b_add_binding(Pid, 'call', [{'callid', CallId}]). + -spec unbind_from_call_events(kz_term:api_binary() | {kz_term:api_binary(), any()} | kapps_call:call()) -> 'ok'. unbind_from_call_events(Call) -> unbind_from_call_events(Call, self()). @@ -133,7 +142,10 @@ unbind_from_call_events(Call) -> -spec unbind_from_call_events(kz_term:api_binary() | {kz_term:api_binary(), any()} | kapps_call:call(), pid()) -> 'ok'. unbind_from_call_events('undefined', _Pid) -> 'ok'; unbind_from_call_events(?NE_BINARY = CallId, Pid) -> - gen_listener:rm_binding(Pid, 'call', [{'callid', CallId}]); + gen_listener:rm_binding(Pid, 'call', [{'callid', CallId}]), + gen_listener:rm_binding(Pid, 'acdc_agent', [{'callid', CallId} + ,{'restrict_to', ['stats_req']} + ]); unbind_from_call_events({CallId, _}, Pid) -> unbind_from_call_events(CallId, Pid); unbind_from_call_events(Call, Pid) -> unbind_from_call_events(kapps_call:call_id(Call), Pid). @@ -146,9 +158,31 @@ proc_id(Pid) -> proc_id(Pid, node()). -spec proc_id(pid(), atom() | kz_term:ne_binary()) -> kz_term:ne_binary(). proc_id(Pid, Node) -> list_to_binary([kz_term:to_binary(Node), "-", pid_to_list(Pid)]). +-spec caller_id(kapps_call:call()) -> {kz_term:api_binary(), kz_term:api_binary()}. +caller_id(Call) -> + CallerIdType = case kapps_call:inception(Call) of + 'undefined' -> <<"internal">>; + _Else -> <<"external">> + end, + kz_attributes:caller_id(CallerIdType, Call). + -spec hangup_cause(kz_json:object()) -> kz_term:ne_binary(). hangup_cause(JObj) -> case kz_json:get_value(<<"Hangup-Cause">>, JObj) of 'undefined' -> <<"unknown">>; Cause -> Cause end. + +-spec max_priority(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:api_integer(). +max_priority(AccountDb, QueueId) -> + case kz_datamgr:open_cache_doc(AccountDb, QueueId) of + {'ok', QueueJObj} -> max_priority(QueueJObj); + _ -> kapps_config:get_integer(?CONFIG_CAT, <<"default_queue_max_priority">>) + end. + +-spec max_priority(kz_json:object()) -> kz_term:api_integer(). +max_priority(QueueJObj) -> + case kz_json:get_integer_value(<<"max_priority">>, QueueJObj) of + 'undefined' -> kapps_config:get_integer(?CONFIG_CAT, <<"default_queue_max_priority">>); + Priority -> Priority + end. diff --git a/applications/acdc/src/bh_acdc_agent.erl b/applications/acdc/src/bh_acdc_agent.erl new file mode 100644 index 00000000000..d0d3ce66485 --- /dev/null +++ b/applications/acdc/src/bh_acdc_agent.erl @@ -0,0 +1,92 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2012-2019, Kage DS Ltd +%%% @doc +%%% @author Alan R Evans +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(bh_acdc_agent). + +-export([init/0 + ,validate/2 + ,bindings/2 + ]). + +-include_lib("../blackhole/src/blackhole.hrl"). + +-define(BINDING(), + [ + <<"acdc.agent.action.*">> + ,<<"acdc.agent.change.*">> + ,<<"acdc_stats.status.*">> + ]). + +-spec init() -> any(). +init() -> + init_bindings(), + _ = blackhole_bindings:bind(<<"blackhole.events.validate.acdc">>, ?MODULE, 'validate'), + _ = blackhole_bindings:bind(<<"blackhole.events.bindings.acdc">>, ?MODULE, 'bindings'), + _ = blackhole_bindings:bind(<<"blackhole.events.validate.acdc_stats">>, ?MODULE, 'validate'), + blackhole_bindings:bind(<<"blackhole.events.bindings.acdc_stats">>, ?MODULE, 'bindings'). + +init_bindings() -> + Bindings = ?BINDING(), + case kapps_config:set_default(?CONFIG_CAT, [<<"bindings">>, <<"agent">>], Bindings) of + {'ok', _} -> lager:debug("initialized ACDC agent bindings"); + {'error', _E} -> lager:info("failed to initialize ACDC agent bindings: ~p", [_E]) + end. + +-spec validate(bh_context:context(), map()) -> bh_context:context(). +validate(Context, #{keys := [<<"agent">>,<<"action">>,<<"*">>] + }) -> + Context; +validate(Context, #{keys := [<<"agent_change">>,<<"*">>] + }) -> + Context; +validate(Context, #{keys := [<<"status">>, _AccountId, _AgentId] + }) -> + Context; +validate(Context, #{keys := Keys}) -> + bh_context:add_error(Context, <<"invalid format for agents subscription : ", (kz_binary:join(Keys))/binary>>). + +-spec bindings(bh_context:context(), map()) -> map(). +bindings(_Context, #{account_id := AccountId + ,keys := [<<"agent">>, <<"action">>, Action] + }=Map) -> + Requested = <<"acdc.agent.action.", Action/binary>>, + Subscribed = [<<"acdc.agent.action.", Action/binary, ".", AccountId/binary, ".*">>], + Listeners = [{'amqp', 'acdc_agent', bind_options(AccountId)}], + Map#{requested => Requested + ,subscribed => Subscribed + ,listeners => Listeners + }; +bindings(_Context, #{account_id := AccountId + ,keys := [<<"agent_change">>, QueueId] + }=Map) -> + Requested = <<"acdc.agent.change.", QueueId/binary>>, + Subscribed = [<<"acdc.queue.agent_change.", AccountId/binary, ".", QueueId/binary>>], + Listeners = [{'amqp', 'acdc_queue', bind_options(AccountId)}], + Map#{requested => Requested + ,subscribed => Subscribed + ,listeners => Listeners + }; +bindings(_Context, #{account_id := AccountId + ,keys := [<<"status">>, AccountId, AgentId] + }=Map) -> + Requested = <<"acdc_stats.status.", AccountId/binary, ".", AgentId/binary>>, + Subscribed = [<<"acdc_stats.status.", AccountId/binary, ".", AgentId/binary>>], + Listeners = [{'amqp', 'acdc_stats', bind_options(AccountId)}], + Map#{requested => Requested + ,subscribed => Subscribed + ,listeners => Listeners + }. + +-spec bind_options(kz_term:ne_binary()) -> kz_term:proplist(). +bind_options(AccountId) -> + [ + {'account_id', AccountId} + ,'federate' + ]. diff --git a/applications/acdc/src/bh_acdc_queue.erl b/applications/acdc/src/bh_acdc_queue.erl new file mode 100644 index 00000000000..2968db19408 --- /dev/null +++ b/applications/acdc/src/bh_acdc_queue.erl @@ -0,0 +1,68 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2012-2019, Kage DS Ltd +%%% @doc +%%% @author Alan R Evans +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(bh_acdc_queue). + +-export([init/0 + ,validate/2 + ,bindings/2 + ]). + +-include_lib("../blackhole/src/blackhole.hrl"). + +-define(BINDING(), + [ + <<"acdc_queue.doc_created">> + ,<<"acdc_queue.doc_edited">> + ,<<"acdc_queue.doc_deleted">> + ]). + +-spec init() -> any(). +init() -> + init_bindings(), + _ = blackhole_bindings:bind(<<"blackhole.events.validate.acdc_queue">>, ?MODULE, 'validate'), + blackhole_bindings:bind(<<"blackhole.events.bindings.acdc_queue">>, ?MODULE, 'bindings'). + +init_bindings() -> + Bindings = ?BINDING(), + case kapps_config:set_default(?CONFIG_CAT, [<<"bindings">>, <<"queue">>], Bindings) of + {'ok', _} -> lager:debug("initialized ACDC queue bindings"); + {'error', _E} -> lager:info("failed to initialize ACDC queue bindings: ~p", [_E]) + end. + +-spec validate(bh_context:context(), map()) -> bh_context:context(). +validate(Context, #{keys := [Action] + }) when Action =:= <<"doc_created">> + ; Action =:= <<"doc_edited">> + ; Action =:= <<"doc_deleted">> -> + Context; +validate(Context, #{keys := Keys}) -> + bh_context:add_error(Context, <<"invalid format for queues subscription : ", (kz_binary:join(Keys))/binary>>). + +-spec bindings(bh_context:context(), map()) -> map(). +bindings(_Context, #{account_id := AccountId + ,keys := [Action] + }=Map) -> + Requested = <<"acdc_queue.", Action/binary>>, + AccountDb = kzs_util:format_account_db(AccountId), + Subscribed = [<>], + Listeners = [{'amqp', 'conf', bind_options(Action, <<"queue">>, AccountDb)}], + Map#{requested => Requested + ,subscribed => Subscribed + ,listeners => Listeners + }. + +-spec bind_options(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:proplist(). +bind_options(Action, Type, Db) -> + [{'action', Action} + ,{'db', Db} + ,{'doc_type', Type} + ,'federate' + ]. diff --git a/applications/acdc/src/cb_acdc_call_stats.erl b/applications/acdc/src/cb_acdc_call_stats.erl index 2a752ae714c..3bf91cc16c9 100644 --- a/applications/acdc/src/cb_acdc_call_stats.erl +++ b/applications/acdc/src/cb_acdc_call_stats.erl @@ -10,7 +10,6 @@ %%% %%% %%% @author Sponsored by Raffel Internet B.V., Implemented by Conversant Ltd -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -36,14 +35,21 @@ -define(COLUMNS ,[{<<"id">>, fun col_id/1} + ,{<<"entered_timestamp">>, fun col_entered_timestamp/1} + ,{<<"abandoned_timestamp">>, fun col_abandoned_timestamp/1} ,{<<"handled_timestamp">>, fun col_handled_timestamp/1} + ,{<<"processed_timestamp">>, fun col_processed_timestamp/1} ,{<<"caller_id_number">>, fun col_caller_id_number/1} ,{<<"caller_id_name">>, fun col_caller_id_name/1} ,{<<"entered_position">>, fun col_entered_position/1} + ,{<<"exited_position">>, fun col_exited_position/1} ,{<<"status">>, fun col_status/1} ,{<<"agent_id">>, fun col_agent_id/1} ,{<<"wait_time">>, fun col_wait_time/1} ,{<<"talk_time">>, fun col_talk_time/1} + ,{<<"misses">>, fun col_misses/1} + ,{<<"required_skills">>, fun col_required_skills/1} + ,{<<"call_id">>, fun col_call_id/1} ,{<<"queue_id">>, fun col_queue_id/1} ]). @@ -155,12 +161,19 @@ normalize_stat_to_csv(Context, JObj) -> end. col_id(JObj) -> kz_doc:id(JObj, <<>>). +col_entered_timestamp(JObj) -> kz_json:get_value(<<"entered_timestamp">>, JObj, <<>>). +col_abandoned_timestamp(JObj) -> kz_json:get_value(<<"abandoned_timestamp">>, JObj, <<>>). col_handled_timestamp(JObj) -> kz_json:get_value(<<"handled_timestamp">>, JObj, <<>>). +col_processed_timestamp(JObj) -> kz_json:get_value(<<"processed_timestamp">>, JObj, <<>>). col_caller_id_number(JObj) -> kz_json:get_value(<<"caller_id_number">>, JObj, <<>>). col_caller_id_name(JObj) -> kz_json:get_value(<<"caller_id_name">>, JObj, <<>>). col_entered_position(JObj) -> kz_json:get_value(<<"entered_position">>, JObj, <<>>). +col_exited_position(JObj) -> kz_json:get_value(<<"exited_position">>, JObj, <<>>). col_status(JObj) -> kz_json:get_value(<<"status">>, JObj, <<>>). col_agent_id(JObj) -> kz_json:get_value(<<"agent_id">>, JObj, <<>>). col_wait_time(JObj) -> kz_json:get_value(<<"wait_time">>, JObj, <<>>). col_talk_time(JObj) -> kz_json:get_value(<<"talk_time">>, JObj, <<>>). +col_misses(JObj) -> kz_json:get_value(<<"misses">>, JObj, <<>>). +col_required_skills(JObj) -> kz_json:get_value(<<"required_skills">>, JObj, <<>>). +col_call_id(JObj) -> kz_json:get_value(<<"call_id">>, JObj, <<>>). col_queue_id(JObj) -> kz_json:get_value(<<"queue_id">>, JObj, <<>>). diff --git a/applications/acdc/src/cb_agents.erl b/applications/acdc/src/cb_agents.erl index 14320286adb..7406df35f27 100644 --- a/applications/acdc/src/cb_agents.erl +++ b/applications/acdc/src/cb_agents.erl @@ -6,6 +6,8 @@ %%% %%% /agents/stats %%% GET: stats for all agents for the last hour +%%% /agents/stats_summary +%%% GET: aggregate stats for agents %%% /agents/statuses %%% GET: statuses for each agents %%% /agents/AID @@ -13,13 +15,16 @@ %%% /agents/AID/queue_status %%% POST: login/logout agent to/from queue %%% +%%% /agents/AID/restart +%%% POST: force-restart a stuck agent +%%% %%% /agents/AID/status %%% GET: last 10 status updates %%% %%% %%% @author Karl Anderson %%% @author James Aimonetti -%%% +%%% @author Daniel Finke %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -29,6 +34,7 @@ -module(cb_agents). -export([init/0 + ,authorize/3 ,allowed_methods/0, allowed_methods/1, allowed_methods/2 ,resource_exists/0, resource_exists/1, resource_exists/2 ,content_types_provided/1, content_types_provided/2, content_types_provided/3 @@ -46,8 +52,10 @@ -define(CB_LIST, <<"agents/crossbar_listing">>). -define(STATS_PATH_TOKEN, <<"stats">>). +-define(STATS_SUMMARY_PATH_TOKEN, <<"stats_summary">>). -define(STATUS_PATH_TOKEN, <<"status">>). -define(QUEUE_STATUS_PATH_TOKEN, <<"queue_status">>). +-define(RESTART_PATH_TOKEN, <<"restart">>). %%%============================================================================= %%% API @@ -66,38 +74,55 @@ init() -> _ = crossbar_bindings:bind(<<"*.resource_exists.agents">>, ?MODULE, 'resource_exists'), _ = crossbar_bindings:bind(<<"*.content_types_provided.agents">>, ?MODULE, 'content_types_provided'), _ = crossbar_bindings:bind(<<"*.execute.post.agents">>, ?MODULE, 'post'), - _ = crossbar_bindings:bind(<<"*.validate.agents">>, ?MODULE, 'validate'). + _ = crossbar_bindings:bind(<<"*.validate.agents">>, ?MODULE, 'validate'), + _ = crossbar_bindings:bind(<<"*.authorize.agents">>, ?MODULE, 'authorize'). + +%%------------------------------------------------------------------------------ +%% @doc Authorizes the incoming request, returning true if the requestor is +%% allowed to access the resource, or false if not. +%% @end +%%------------------------------------------------------------------------------ +-spec authorize(cb_context:context(), path_token(), path_token()) -> boolean(). +authorize(Context, _, ?RESTART_PATH_TOKEN) -> + case cb_context:is_superduper_admin(Context) of + 'true' -> 'true'; + 'false' -> + Context1 = cb_context:add_system_error('forbidden', Context), + {'halt', Context1} + end. %%------------------------------------------------------------------------------ %% @doc Given the path tokens related to this module, what HTTP methods are %% going to be responded to. %% @end %%------------------------------------------------------------------------------ - -spec allowed_methods() -> http_methods(). allowed_methods() -> [?HTTP_GET]. -spec allowed_methods(path_token()) -> http_methods(). allowed_methods(?STATUS_PATH_TOKEN) -> [?HTTP_GET]; allowed_methods(?STATS_PATH_TOKEN) -> [?HTTP_GET]; +allowed_methods(?STATS_SUMMARY_PATH_TOKEN) -> [?HTTP_GET]; allowed_methods(_UserId) -> [?HTTP_GET]. -spec allowed_methods(path_token(), path_token()) -> http_methods(). allowed_methods(?STATUS_PATH_TOKEN, _UserId) -> [?HTTP_GET, ?HTTP_POST]; allowed_methods(_UserId, ?STATUS_PATH_TOKEN) -> [?HTTP_GET, ?HTTP_POST]; -allowed_methods(_UserId, ?QUEUE_STATUS_PATH_TOKEN) -> [?HTTP_GET, ?HTTP_POST]. +allowed_methods(_UserId, ?QUEUE_STATUS_PATH_TOKEN) -> [?HTTP_GET, ?HTTP_POST]; +allowed_methods(_UserId, ?RESTART_PATH_TOKEN) -> [?HTTP_POST]. %%------------------------------------------------------------------------------ -%% @doc Does the path point to a valid resource. +%% @doc Does the path point to a valid resource +%% %% For example: +%% %% ``` -%% /agents => [] +%% /agents => []. %% /agents/foo => [<<"foo">>] %% /agents/foo/bar => [<<"foo">>, <<"bar">>] -%%% ''' +%% ''' %% @end %%------------------------------------------------------------------------------ - -spec resource_exists() -> 'true'. resource_exists() -> 'true'. @@ -107,13 +132,15 @@ resource_exists(_) -> 'true'. -spec resource_exists(path_token(), path_token()) -> 'true'. resource_exists(_, ?STATUS_PATH_TOKEN) -> 'true'; resource_exists(?STATUS_PATH_TOKEN, _) -> 'true'; -resource_exists(_, ?QUEUE_STATUS_PATH_TOKEN) -> 'true'. +resource_exists(_, ?QUEUE_STATUS_PATH_TOKEN) -> 'true'; +resource_exists(_, ?RESTART_PATH_TOKEN) -> 'true'. %%------------------------------------------------------------------------------ +%% @private %% @doc Add content types accepted and provided by this module +%% %% @end %%------------------------------------------------------------------------------ - -spec content_types_provided(cb_context:context()) -> cb_context:context(). content_types_provided(Context) -> Context. @@ -135,17 +162,17 @@ content_types_provided(Context, ?STATS_PATH_TOKEN) -> -spec content_types_provided(cb_context:context(), path_token(), path_token()) -> cb_context:context(). content_types_provided(Context, ?STATUS_PATH_TOKEN, _) -> Context; content_types_provided(Context, _, ?STATUS_PATH_TOKEN) -> Context; -content_types_provided(Context, _, ?QUEUE_STATUS_PATH_TOKEN) -> Context. +content_types_provided(Context, _, ?QUEUE_STATUS_PATH_TOKEN) -> Context; +content_types_provided(Context, _, ?RESTART_PATH_TOKEN) -> Context. %%------------------------------------------------------------------------------ %% @doc Check the request (request body, query string params, path tokens, etc) %% and load necessary information. -%% /agents might load a list of agent objects +%% /agents mights load a list of agent objects %% /agents/123 might load the agent object 123 %% Generally, use crossbar_doc to manipulate the cb_context{} record %% @end %%------------------------------------------------------------------------------ - -spec validate(cb_context:context()) -> cb_context:context(). validate(Context) -> @@ -160,6 +187,8 @@ validate_agent(Context, ?STATUS_PATH_TOKEN, ?HTTP_GET) -> fetch_all_agent_statuses(Context); validate_agent(Context, ?STATS_PATH_TOKEN, ?HTTP_GET) -> fetch_all_agent_stats(Context); +validate_agent(Context, ?STATS_SUMMARY_PATH_TOKEN, ?HTTP_GET) -> + fetch_stats_summary(Context); validate_agent(Context, Id, ?HTTP_GET) -> read(Id, Context). @@ -178,7 +207,9 @@ validate_agent_action(Context, AgentId, ?QUEUE_STATUS_PATH_TOKEN, ?HTTP_POST) -> OnSuccess = fun (C) -> maybe_queues_change(read(AgentId, C)) end, cb_context:validate_request_data(<<"queue_update">>, Context, OnSuccess); validate_agent_action(Context, AgentId, ?QUEUE_STATUS_PATH_TOKEN, ?HTTP_GET) -> - fetch_agent_queues(read(AgentId, Context)). + fetch_agent_queues(read(AgentId, Context)); +validate_agent_action(Context, AgentId, ?RESTART_PATH_TOKEN, ?HTTP_POST) -> + read(AgentId, Context). -spec maybe_queues_change(cb_context:context()) -> cb_context:context(). maybe_queues_change(Context) -> @@ -226,7 +257,10 @@ post(Context, AgentId, ?QUEUE_STATUS_PATH_TOKEN) -> cb_context:set_resp_data(Context1, Queues); _Status -> Context1 - end. + end; +post(Context, AgentId, ?RESTART_PATH_TOKEN) -> + publish_restart(Context, AgentId), + crossbar_util:response(kz_json:new(), Context). -spec publish_action(cb_context:context(), kz_term:ne_binary()) -> 'ok'. publish_action(Context, AgentId) -> @@ -250,37 +284,56 @@ publish_update(Context, AgentId, PubFun) -> [{<<"Account-ID">>, cb_context:account_id(Context)} ,{<<"Agent-ID">>, AgentId} ,{<<"Time-Limit">>, cb_context:req_value(Context, <<"timeout">>)} + ,{<<"Alias">>, cb_context:req_value(Context, <<"alias">>)} ,{<<"Presence-ID">>, cb_context:req_value(Context, <<"presence_id">>)} ,{<<"Presence-State">>, cb_context:req_value(Context, <<"presence_state">>)} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), kz_amqp_worker:cast(Update, PubFun). +-spec publish_restart(cb_context:context(), kz_term:ne_binary()) -> 'ok'. +publish_restart(Context, AgentId) -> + Payload = [{<<"Account-ID">>, cb_context:account_id(Context)} + ,{<<"Agent-ID">>, AgentId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kz_amqp_worker:cast(Payload, fun kapi_acdc_agent:publish_restart/1). + %%------------------------------------------------------------------------------ +%% @private %% @doc Load an instance from the database %% @end %%------------------------------------------------------------------------------ --spec read(path_token(), cb_context:context()) -> cb_context:context(). -read(Id, Context) -> - crossbar_doc:load(Id, Context, ?TYPE_CHECK_OPTION(kzd_users:type())). +-spec read(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). +read(UserId, Context) -> + Context1 = crossbar_doc:load(UserId, Context, ?TYPE_CHECK_OPTION(kzd_users:type())), + case cb_context:resp_status(Context1) of + 'success' -> + cb_context:setters(Context1 + ,[{fun cb_context:set_user_id/2, UserId} + ,{fun cb_context:set_resp_status/2, 'success'} + ]); + _Status -> Context1 + end. -define(CB_AGENTS_LIST, <<"users/crossbar_listing">>). -spec fetch_all_agent_statuses(cb_context:context()) -> cb_context:context(). fetch_all_agent_statuses(Context) -> - fetch_all_current_statuses(Context - ,'undefined' - ,cb_context:req_value(Context, <<"status">>) - ). + case kz_term:is_true(cb_context:req_value(Context, <<"recent">>)) of + 'false' -> + fetch_current_status(Context, 'undefined'); + 'true' -> + fetch_all_current_statuses(Context + ,'undefined' + ,cb_context:req_value(Context, <<"status">>) + ) + end. -spec fetch_agent_status(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). fetch_agent_status(AgentId, Context) -> case kz_term:is_true(cb_context:req_value(Context, <<"recent">>)) of 'false' -> - fetch_current_status(Context - ,AgentId - ,kz_term:is_true(cb_context:req_value(Context, <<"full">>)) - ); - + fetch_current_status(Context, AgentId); 'true' -> fetch_all_current_statuses(Context ,AgentId @@ -295,6 +348,26 @@ fetch_all_agent_stats(Context) -> StartRange -> fetch_ranged_agent_stats(Context, StartRange) end. +-spec fetch_stats_summary(cb_context:context()) -> cb_context:context(). +fetch_stats_summary(Context) -> + Req = props:filter_undefined( + [{<<"Account-ID">>, cb_context:account_id(Context)} + ,{<<"Agent-ID">>, cb_context:req_value(Context, <<"agent_id">>)} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + case kz_amqp_worker:call(Req + ,fun kapi_acdc_stats:publish_agent_calls_req/1 + ,fun kapi_acdc_stats:agent_calls_resp_v/1 + ) + of + {'error', E} -> + crossbar_util:response('error', <<"stat request had errors">>, 400 + ,kz_json:get_value(<<"Error-Reason">>, E) + ,Context + ); + {'ok', Resp} -> crossbar_util:response(kz_json:get_value(<<"Data">>, Resp, []), Context) + end. + -spec fetch_all_current_agent_stats(cb_context:context()) -> cb_context:context(). fetch_all_current_agent_stats(Context) -> fetch_all_current_stats(Context @@ -303,7 +376,7 @@ fetch_all_current_agent_stats(Context) -> -spec fetch_all_current_stats(cb_context:context(), kz_term:api_binary()) -> cb_context:context(). fetch_all_current_stats(Context, AgentId) -> - Now = kz_time:now_s(), + Now = kz_time:current_tstamp(), From = Now - min(?SECONDS_IN_DAY, ?ACDC_CLEANUP_WINDOW), Req = props:filter_undefined( @@ -315,20 +388,15 @@ fetch_all_current_stats(Context, AgentId) -> ]), fetch_stats_from_amqp(Context, Req). --spec fetch_current_status(cb_context:context(), kz_term:api_binary(), kz_term:api_boolean()) -> cb_context:context(). -fetch_current_status(Context, AgentId, 'false') -> - {'ok', Resp} = acdc_agent_util:most_recent_status(cb_context:account_id(Context), AgentId), - crossbar_util:response(Resp, Context); -fetch_current_status(Context, AgentId, 'true') -> +-spec fetch_current_status(cb_context:context(), kz_term:api_binary()) -> cb_context:context(). +fetch_current_status(Context, AgentId) -> Req = props:filter_undefined( [{<<"Account-ID">>, cb_context:account_id(Context)} ,{<<"Agent-ID">>, AgentId} - ,{<<"Limit">>, 1} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), case kz_amqp_worker:call(Req - ,fun kapi_acdc_stats:publish_status_req/1 - ,fun kapi_acdc_stats:status_resp_v/1 + ,fun kapi_acdc_stats:publish_agent_cur_status_req/1 ) of {'error', E} -> @@ -339,14 +407,14 @@ fetch_current_status(Context, AgentId, 'true') -> ,Context ); {'ok', Resp} -> - Agents = kz_json:get_value(<<"Agents">>, Resp, kz_json:new()), - Agents1 = kz_json:map(fun(K, AgentStats) -> {K, remove_timestamps(AgentStats)} end, Agents), - crossbar_util:response(kz_json:get_json_value(AgentId, Agents1), Context) + Data = fetch_current_status_response(kz_json:get_value(<<"Event-Category">>, Resp), kz_json:get_value(<<"Event-Name">>, Resp), Resp), + crossbar_util:response(Data, Context) end. +fetch_current_status_response(<<"acdc_stat">>, <<"agent_cur_status_resp">>, Resp) -> + kz_json:get_value(<<"Agents">>, Resp, kz_json:new()); +fetch_current_status_response(<<"acdc_stat">>, <<"agent_cur_status_err">>, _Resp) -> + kz_json:new(). -remove_timestamps(AgentStats) -> - [Key|_] = kz_json:get_keys(AgentStats), - kz_json:get_json_value(Key, AgentStats). -spec fetch_all_current_statuses(cb_context:context(), kz_term:api_binary(), kz_term:api_binary()) -> cb_context:context(). @@ -369,7 +437,7 @@ fetch_all_current_statuses(Context, AgentId, Status) -> fetch_ranged_agent_stats(Context, StartRange) -> MaxRange = ?ACDC_ARCHIVE_WINDOW, - Now = kz_time:now_s(), + Now = kz_time:current_tstamp(), Past = Now - MaxRange, To = kz_term:to_integer(cb_context:req_value(Context, <<"end_range">>, Now)), @@ -395,7 +463,7 @@ fetch_ranged_agent_stats(Context, StartRange) -> -spec fetch_ranged_agent_stats(cb_context:context(), pos_integer(), pos_integer(), boolean()) -> cb_context:context(). fetch_ranged_agent_stats(Context, From, To, 'true') -> - lager:debug("ranged query from ~b to ~b(~b) of current stats (now ~b)", [From, To, To-From, kz_time:now_s()]), + lager:debug("ranged query from ~b to ~b(~b) of current stats (now ~b)", [From, To, To-From, kz_time:current_tstamp()]), Req = props:filter_undefined( [{<<"Account-ID">>, cb_context:account_id(Context)} ,{<<"Status">>, cb_context:req_value(Context, <<"status">>)} @@ -432,11 +500,13 @@ format_stats(Context, Resp) -> ++ kz_json:get_value(<<"Waiting">>, Resp, []) ++ kz_json:get_value(<<"Processed">>, Resp, []), - FormattedStats = lists:foldl(fun format_stats_fold/2 - ,kz_json:new() - ,Stats - ), - crossbar_util:response(FormattedStats, Context). + crossbar_util:response( + lists:foldl(fun format_stats_fold/2 + ,kz_json:new() + ,Stats + ) + ,Context + ). -spec format_stats_fold(kz_json:object(), kz_json:object()) -> kz_json:object(). @@ -534,16 +604,19 @@ add_miss(Miss, Acc, QueueId) -> ). %%------------------------------------------------------------------------------ +%% @private %% @doc Attempt to load a summarized listing of all instances of this %% resource. %% @end %%------------------------------------------------------------------------------ -spec summary(cb_context:context()) -> cb_context:context(). summary(Context) -> - crossbar_view:load(Context, ?CB_LIST, [{'mapper', fun normalize_view_results/2}]). +%% crossbar_view:load(Context, ?CB_LIST ,[{'mapper', crossbar_view:get_value_fun()}]). +crossbar_view:load(Context, ?CB_LIST ,[{'mapper', fun normalize_view_results/2}]). %%------------------------------------------------------------------------------ -%% @doc Normalizes the results of a view +%% @private +%% @doc Normalizes the resuts of a view %% @end %%------------------------------------------------------------------------------ -spec normalize_view_results(kz_json:object(), kz_json:objects()) -> @@ -575,14 +648,15 @@ validate_status_change(Context, S) -> 'true' -> validate_status_change_params(Context, S); 'false' -> lager:debug("status ~s not valid", [S]), - cb_context:add_validation_error(<<"status">> - ,<<"enum">> - ,kz_json:from_list( - [{<<"message">>, <<"value is not a valid status">>} - ,{<<"cause">>, S} - ]) - ,Context - ) + cb_context:add_validation_error( + <<"status">> + ,<<"enum">> + ,kz_json:from_list( + [{<<"message">>, <<"value is not a valid status">>} + ,{<<"cause">>, S} + ]) + ,Context + ) end. -spec check_for_status_error(cb_context:context(), kz_term:api_binary()) -> @@ -592,14 +666,15 @@ check_for_status_error(Context, S) -> 'true' -> Context; 'false' -> lager:debug("status ~s not found", [S]), - cb_context:add_validation_error(<<"status">> - ,<<"enum">> - ,kz_json:from_list( - [{<<"message">>, <<"value is not a valid status">>} - ,{<<"cause">>, S} - ]) - ,Context - ) + cb_context:add_validation_error( + <<"status">> + ,<<"enum">> + ,kz_json:from_list( + [{<<"message">>, <<"value is not a valid status">>} + ,{<<"cause">>, S} + ]) + ,Context + ) end. -spec validate_status_change_params(cb_context:context(), kz_term:ne_binary()) -> @@ -610,25 +685,17 @@ validate_status_change_params(Context, <<"pause">>) -> N when N >= 0 -> cb_context:set_resp_status(Context, 'success'); N -> lager:debug("bad int for pause: ~p", [N]), - cb_context:add_validation_error(<<"timeout">> - ,<<"minimum">> - ,kz_json:from_list( - [{<<"message">>, <<"value must be at least greater than or equal to 0">>} - ,{<<"cause">>, N} - ]) - ,Context - ) + cb_context:add_validation_error( + <<"timeout">> + ,<<"minimum">> + ,kz_json:from_list( + [{<<"message">>, <<"value must be at least greater than or equal to 0">>} + ,{<<"cause">>, N} + ]) + ,Context + ) catch - _E:_R -> - lager:debug("bad int for pause: ~s: ~p", [_E, _R]), - cb_context:add_validation_error(<<"timeout">> - ,<<"type">> - ,kz_json:from_list( - [{<<"message">>, <<"value must be an integer greater than or equal to 0">>} - ,{<<"cause">>, Value} - ]) - ,Context - ) + _E:_R -> cb_context:set_resp_status(Context, 'success') end; validate_status_change_params(Context, _S) -> lager:debug("great success for ~s", [_S]), diff --git a/applications/acdc/src/cb_queues.erl b/applications/acdc/src/cb_queues.erl index da45ad88420..1ecb603a8c9 100644 --- a/applications/acdc/src/cb_queues.erl +++ b/applications/acdc/src/cb_queues.erl @@ -16,6 +16,8 @@ %%% %%% /queues/QID/stats %%% GET: retrieve stats for this queue +%%% /queues/QID/stats_summary +%%% GET: retrieve minimal current stats for queues %%% /queues/QID/stats/realtime %%% GET: retrieve realtime stats for the queues %%% @@ -31,7 +33,6 @@ %%% %%% %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -51,6 +52,7 @@ ,delete/2, delete/3 ,delete_account/2 ]). +-export([maybe_add_queue_to_agent/2, maybe_rm_queue_from_agent/2]). -include_lib("crossbar/src/crossbar.hrl"). -include("acdc_config.hrl"). @@ -58,9 +60,10 @@ -define(MOD_CONFIG_CAT, <<(?CONFIG_CAT)/binary, ".queues">>). -define(CB_LIST, <<"queues/crossbar_listing">>). --define(CB_AGENTS_LIST, <<"queues/agents_listing">>). %{agent_id, queue_id} +-define(CB_AGENTS_LIST, <<"queues/agents_listing">>). -define(STATS_PATH_TOKEN, <<"stats">>). +-define(STATS_SUMMARY_PATH_TOKEN, <<"stats_summary">>). -define(ROSTER_PATH_TOKEN, <<"roster">>). -define(EAVESDROP_PATH_TOKEN, <<"eavesdrop">>). @@ -94,6 +97,9 @@ %%------------------------------------------------------------------------------ -spec init() -> 'ok'. init() -> + _ = kz_datamgr:db_create(?KZ_ACDC_DB), + _ = kz_datamgr:revise_doc_from_file(?KZ_ACDC_DB, 'crossbar', <<"views/acdc.json">>), + _ = kapi_acdc_agent:declare_exchanges(), _ = kapi_acdc_stats:declare_exchanges(), @@ -122,6 +128,8 @@ allowed_methods() -> -spec allowed_methods(path_token()) -> http_methods(). allowed_methods(?STATS_PATH_TOKEN) -> [?HTTP_GET]; +allowed_methods(?STATS_SUMMARY_PATH_TOKEN) -> + [?HTTP_GET]; allowed_methods(?EAVESDROP_PATH_TOKEN) -> [?HTTP_PUT]; allowed_methods(_QueueId) -> @@ -130,20 +138,23 @@ allowed_methods(_QueueId) -> -spec allowed_methods(path_token(), path_token()) -> http_methods(). allowed_methods(_QueueId, ?ROSTER_PATH_TOKEN) -> [?HTTP_GET, ?HTTP_POST, ?HTTP_DELETE]; +allowed_methods(_QueueId, ?STATS_SUMMARY_PATH_TOKEN) -> + [?HTTP_GET]; allowed_methods(_QueueId, ?EAVESDROP_PATH_TOKEN) -> [?HTTP_PUT]. %%------------------------------------------------------------------------------ -%% @doc Does the path point to a valid resource. +%% @doc Does the path point to a valid resource +%% %% For example: +%% %% ``` -%% /queues => [] +%% /queues => []. %% /queues/foo => [<<"foo">>] %% /queues/foo/bar => [<<"foo">>, <<"bar">>] %% ''' %% @end %%------------------------------------------------------------------------------ - -spec resource_exists() -> 'true'. resource_exists() -> 'true'. @@ -152,13 +163,15 @@ resource_exists(_) -> 'true'. -spec resource_exists(path_token(), path_token()) -> 'true'. resource_exists(_, ?ROSTER_PATH_TOKEN) -> 'true'; +resource_exists(_, ?STATS_SUMMARY_PATH_TOKEN) -> 'true'; resource_exists(_, ?EAVESDROP_PATH_TOKEN) -> 'true'. %%------------------------------------------------------------------------------ +%% @private %% @doc Add content types accepted and provided by this module +%% %% @end %%------------------------------------------------------------------------------ - -spec content_types_provided(cb_context:context()) -> cb_context:context(). content_types_provided(Context) -> Context. @@ -169,7 +182,9 @@ content_types_provided(Context, ?STATS_PATH_TOKEN) -> cb_context:add_content_types_provided(Context ,[{'to_json', ?JSON_CONTENT_TYPES} ,{'to_csv', ?CSV_CONTENT_TYPES} - ]). + ]); +content_types_provided(Context, ?STATS_SUMMARY_PATH_TOKEN) -> Context; +content_types_provided(Context, _) -> Context. %%------------------------------------------------------------------------------ %% @doc Check the request (request body, query string params, path tokens, etc) @@ -179,7 +194,6 @@ content_types_provided(Context, ?STATS_PATH_TOKEN) -> %% Generally, use crossbar_doc to manipulate the cb_context{} record %% @end %%------------------------------------------------------------------------------ - -spec validate(cb_context:context()) -> cb_context:context(). validate(Context) -> @@ -195,6 +209,8 @@ validate(Context, PathToken) -> validate_queue(Context, ?STATS_PATH_TOKEN, ?HTTP_GET) -> fetch_all_queue_stats(Context); +validate_queue(Context, ?STATS_SUMMARY_PATH_TOKEN, ?HTTP_GET) -> + fetch_stats_summary(Context, 'all'); validate_queue(Context, ?EAVESDROP_PATH_TOKEN, ?HTTP_PUT) -> validate_eavesdrop_on_call(Context); validate_queue(Context, Id, ?HTTP_GET) -> @@ -213,6 +229,8 @@ validate(Context, Id, Token) -> validate_queue_operation(Context, Id, ?ROSTER_PATH_TOKEN, ?HTTP_GET) -> load_agent_roster(Id, Context); +validate_queue_operation(Context, Id, ?STATS_SUMMARY_PATH_TOKEN, ?HTTP_GET) -> + fetch_stats_summary(Context, Id); validate_queue_operation(Context, Id, ?ROSTER_PATH_TOKEN, ?HTTP_POST) -> add_queue_to_agents(Id, Context); validate_queue_operation(Context, Id, ?ROSTER_PATH_TOKEN, ?HTTP_DELETE) -> @@ -259,14 +277,15 @@ is_valid_mode(Context, Data) -> 'true' -> 'true'; 'false' -> {'false' - ,cb_context:add_validation_error(<<"mode">> - ,<<"enum">> - ,kz_json:from_list( - [{<<"message">>, <<"Value not found in enumerated list of values">>} - ,{<<"cause">>, Mode} - ]) - ,Context - ) + ,cb_context:add_validation_error( + <<"mode">> + ,<<"enum">> + ,kz_json:from_list( + [{<<"message">>, <<"Value not found in enumerated list of values">>} + ,{<<"cause">>, Mode} + ]) + ,Context + ) } end. @@ -277,13 +296,14 @@ is_valid_call(Context, Data) -> case kz_json:get_binary_value(<<"call_id">>, Data) of 'undefined' -> {'false' - ,cb_context:add_validation_error(<<"call_id">> - ,<<"required">> - ,kz_json:from_list( - [{<<"message">>, <<"Field is required but missing">>}] - ) - ,Context - ) + ,cb_context:add_validation_error( + <<"call_id">> + ,<<"required">> + ,kz_json:from_list( + [{<<"message">>, <<"Field is required but missing">>}] + ) + ,Context + ) }; CallId -> is_active_call(Context, CallId) @@ -297,32 +317,34 @@ is_active_call(Context, CallId) -> {'error', _E} -> lager:debug("is not valid call: ~p", [_E]), {'false' - ,cb_context:add_validation_error(<<"call_id">> - ,<<"not_found">> - ,kz_json:from_list( - [{<<"message">>, <<"Call was not found">>} - ,{<<"cause">>, CallId} - ]) - ,Context - ) + ,cb_context:add_validation_error( + <<"call_id">> + ,<<"not_found">> + ,kz_json:from_list( + [{<<"message">>, <<"Call was not found">>} + ,{<<"cause">>, CallId} + ]) + ,Context + ) }; {'ok', _} -> 'true' end. -is_valid_queue(Context, <>) -> - AccountDb = cb_context:db_name(Context), - case kz_datamgr:open_cache_doc(AccountDb, QueueId) of +is_valid_queue(Context, <<_/binary>> = QueueId) -> + AcctDb = cb_context:db_name(Context), + case kz_datamgr:open_cache_doc(AcctDb, QueueId) of {'ok', QueueJObj} -> is_valid_queue(Context, QueueJObj); {'error', _} -> {'false' - ,cb_context:add_validation_error(<<"queue_id">> - ,<<"not_found">> - ,kz_json:from_list( - [{<<"message">>, <<"Queue was not found">>} - ,{<<"cause">>, QueueId} - ]) - ,Context - ) + ,cb_context:add_validation_error( + <<"queue_id">> + ,<<"not_found">> + ,kz_json:from_list( + [{<<"message">>, <<"Queue was not found">>} + ,{<<"cause">>, QueueId} + ]) + ,Context + ) } end; is_valid_queue(Context, QueueJObj) -> @@ -330,29 +352,31 @@ is_valid_queue(Context, QueueJObj) -> <<"queue">> -> 'true'; _ -> {'false' - ,cb_context:add_validation_error(<<"queue_id">> - ,<<"type">> - ,kz_json:from_list([{<<"message">>, <<"Id did not represent a queue">>}]) - ,Context - ) + ,cb_context:add_validation_error( + <<"queue_id">> + ,<<"type">> + ,kz_json:from_list([{<<"message">>, <<"Id did not represent a queue">>}]) + ,Context + ) } end. is_valid_endpoint(Context, DataJObj) -> - AccountDb = cb_context:db_name(Context), + AcctDb = cb_context:db_name(Context), Id = kz_doc:id(DataJObj), - case kz_datamgr:open_cache_doc(AccountDb, Id) of + case kz_datamgr:open_cache_doc(AcctDb, Id) of {'ok', CallMeJObj} -> is_valid_endpoint_type(Context, CallMeJObj); {'error', _} -> {'false' - ,cb_context:add_validation_error(<<"id">> - ,<<"not_found">> - ,kz_json:from_list( - [{<<"message">>, <<"Id was not found">>} - ,{<<"cause">>, Id} - ]) - ,Context - ) + ,cb_context:add_validation_error( + <<"id">> + ,<<"not_found">> + ,kz_json:from_list( + [{<<"message">>, <<"Id was not found">>} + ,{<<"cause">>, Id} + ]) + ,Context + ) } end. @@ -361,22 +385,22 @@ is_valid_endpoint_type(Context, CallMeJObj) -> <<"device">> -> 'true'; Type -> {'false' - ,cb_context:add_validation_error(<<"id">> - ,<<"type">> - ,kz_json:from_list( - [{<<"message">>, <<"Id did not represent a valid endpoint">>} - ,{<<"cause">>, Type} - ]) - ,Context - ) + ,cb_context:add_validation_error( + <<"id">> + ,<<"type">> + ,kz_json:from_list( + [{<<"message">>, <<"Id did not represent a valid endpoint">>} + ,{<<"cause">>, Type} + ]) + ,Context + ) } end. %%------------------------------------------------------------------------------ -%% @doc If the HTTP verb is PUT, execute the actual action, usually a db save. +%% @doc If the HTTP verib is PUT, execute the actual action, usually a db save. %% @end %%------------------------------------------------------------------------------ - -spec put(cb_context:context()) -> cb_context:context(). put(Context) -> @@ -420,10 +444,11 @@ eavesdrop_req(Context, Prop) -> of {'ok', Resp} -> crossbar_util:response(filter_response_fields(Resp), Context); {'error', 'timeout'} -> - cb_context:add_system_error('timeout' - ,kz_json:from_list([{<<"cause">>, <<"eavesdrop failed to start">>}]) - ,Context - ); + cb_context:add_system_error( + 'timeout' + ,kz_json:from_list([{<<"cause">>, <<"eavesdrop failed to start">>}]) + ,Context + ); {'error', E} -> crossbar_util:response('error', <<"error">>, 500, E, Context) end. @@ -441,15 +466,14 @@ filter_response_fields(JObj) -> ). %%------------------------------------------------------------------------------ -%% @doc If the HTTP verb is POST, execute the actual action, usually a db save +%% @doc If the HTTP verib is POST, execute the actual action, usually a db save %% (after a merge perhaps). %% @end %%------------------------------------------------------------------------------ - -spec post(cb_context:context(), path_token()) -> cb_context:context(). -post(Context, _) -> +post(Context, Id) -> activate_account_for_acdc(Context), - crossbar_doc:save(Context). + read(Id, crossbar_doc:save(unset_agents_key(Context))). -spec post(cb_context:context(), path_token(), path_token()) -> cb_context:context(). post(Context, Id, ?ROSTER_PATH_TOKEN) -> @@ -464,10 +488,9 @@ post(Context, Id, ?ROSTER_PATH_TOKEN) -> patch(Context, Id) -> post(Context, Id). %%------------------------------------------------------------------------------ -%% @doc If the HTTP verb is DELETE, execute the actual action, usually a db delete +%% @doc If the HTTP verib is DELETE, execute the actual action, usually a db delete %% @end %%------------------------------------------------------------------------------ - -spec delete(cb_context:context(), path_token()) -> cb_context:context(). delete(Context, _) -> activate_account_for_acdc(Context), @@ -489,18 +512,20 @@ delete_account(Context, AccountId) -> %%%============================================================================= %%------------------------------------------------------------------------------ +%% @private %% @doc Load an instance from the database %% @end %%------------------------------------------------------------------------------ -spec read(kz_term:ne_binary(), cb_context:context()) -> cb_context:context(). read(Id, Context) -> - Context1 = crossbar_doc:load(Id, Context, ?TYPE_CHECK_OPTION(<<"queue">>)), + Context1 = crossbar_doc:load(Id, Context, ?TYPE_CHECK_OPTION(kzd_queues:type())), case cb_context:resp_status(Context1) of 'success' -> load_queue_agents(Id, Context1); _Status -> Context1 end. %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ @@ -509,6 +534,7 @@ validate_request(QueueId, Context) -> check_queue_schema(QueueId, Context). %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ @@ -527,6 +553,7 @@ on_successful_validation(QueueId, Context) -> crossbar_doc:load_merge(QueueId, Context, ?TYPE_CHECK_OPTION(<<"queue">>)). %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ @@ -534,25 +561,19 @@ load_queue_agents(Id, Context) -> Context1 = load_agent_roster(Id, Context), case cb_context:resp_status(Context1) of 'success' -> - Agents = kz_json:set_value(<<"agents">> - ,cb_context:resp_data(Context1) - ,cb_context:resp_data(Context) - ), - cb_context:setters(Context, [{fun cb_context:set_resp_data/2, Agents} - %% Because the response can be dynamic depending on the results of Context1, the - %% etag of Context cannot be trusted as a strong cache validator - ,{fun cb_context:set_resp_etag/2, 'undefined'} - ]); + cb_context:set_resp_data(Context + ,kz_json:set_value(<<"agents">> + ,cb_context:resp_data(Context1) + ,cb_context:resp_data(Context) + ) + ); _Status -> Context1 end. load_agent_roster(Id, Context) -> - Options = [{'startkey', [Id]} - ,{'endkey', [Id, kz_json:new()]} - ,{'mapper', crossbar_view:get_id_fun()} - ,{'reduce', 'false'} - ], - crossbar_view:load(Context, ?CB_AGENTS_LIST, Options). + crossbar_view:load(Context, ?CB_AGENTS_LIST, [{'key', Id} + ,{'reduce', 'false'} + ,{'mapper', fun normalize_agents_results/2}]). add_queue_to_agents(Id, Context) -> add_queue_to_agents(Id, Context, cb_context:req_data(Context)). @@ -645,6 +666,7 @@ maybe_rm_queue_from_agent(Id, A) -> kz_json:set_value(<<"queues">>, lists:delete(Id, Qs), A). %%------------------------------------------------------------------------------ +%% @private %% @doc %% @end %%------------------------------------------------------------------------------ @@ -655,40 +677,103 @@ fetch_all_queue_stats(Context) -> StartRange -> fetch_ranged_queue_stats(Context, StartRange) end. --spec fetch_all_current_queue_stats(cb_context:context()) -> cb_context:context(). -fetch_all_current_queue_stats(Context) -> - lager:debug("querying for all recent stats"), - Now = kz_time:now_s(), - From = Now - min(?SECONDS_IN_DAY, ?ACDC_CLEANUP_WINDOW), +-spec fetch_stats_summary(cb_context:context(), kz_term:ne_binary()|'all') -> cb_context:context(). +fetch_stats_summary(Context, QueueId) -> + case cb_context:req_value(Context, <<"start_range">>) of + 'undefined' -> fetch_current_stats_summary(Context, QueueId); + StartRange -> fetch_ranged_stats_summary(Context, StartRange, QueueId) + end. +-spec fetch_current_stats_summary(cb_context:context(), kz_term:ne_binary() | 'all') -> cb_context:context(). +fetch_current_stats_summary(Context, QueueId) -> Req = props:filter_undefined( [{<<"Account-ID">>, cb_context:account_id(Context)} ,{<<"Status">>, cb_context:req_value(Context, <<"status">>)} - ,{<<"Agent-ID">>, cb_context:req_value(Context, <<"agent_id">>)} - ,{<<"Start-Range">>, From} - ,{<<"End-Range">>, Now} + ,{<<"Queue-ID">>, case QueueId of + 'all' -> cb_context:req_value(Context, <<"queue_id">>); + Else -> Else + end} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), - fetch_from_amqp(Context, Req). + case kz_amqp_worker:call(Req + ,fun kapi_acdc_stats:publish_call_summary_req/1 + ,fun kapi_acdc_stats:call_summary_resp_v/1 + ) + of + {'error', E} -> + crossbar_util:response('error', <<"stat request had errors">>, 400 + ,kz_json:get_value(<<"Error-Reason">>, E) + ,Context + ); + {'ok', Resp} -> + RespJObj = kz_json:set_values([{<<"current_timestamp">>, kz_time:current_tstamp()} + ,{<<"Summarized">>, kz_json:get_value(<<"Data">>, Resp, [])} + %% ,{<<"Waiting">>, kz_doc:public_fields(kz_json:get_value(<<"Waiting">>, Resp, []))} + %% ,{<<"Handled">>, kz_doc:public_fields(kz_json:get_value(<<"Handled">>, Resp, []))} + ], kz_json:new()), + crossbar_util:response(RespJObj, Context) + end. -format_stats(Context, Resp) -> - Stats = kz_json:from_list([{<<"current_timestamp">>, kz_time:now_s()} - ,{<<"stats">>, - kz_doc:public_fields( - kz_json:get_value(<<"Handled">>, Resp, []) ++ - kz_json:get_value(<<"Abandoned">>, Resp, []) ++ - kz_json:get_value(<<"Waiting">>, Resp, []) ++ - kz_json:get_value(<<"Processed">>, Resp, []) - )} - ]), - cb_context:set_resp_status(cb_context:set_resp_data(Context, Stats) - ,'success' - ). + +fetch_ranged_stats_summary(Context, StartRange, QueueId) -> + MaxRange = ?SECONDS_IN_YEAR, + + Now = kz_time:current_tstamp(), + %% Past = Now - MaxRange, + + To = kz_term:to_integer(cb_context:req_value(Context, <<"end_range">>, Now)), + + case kz_term:to_integer(StartRange) of + F when F > To -> + %% start_range is larger than end_range + Msg = kz_json:from_list([{<<"message">>, <<"value is greater than start_range">>} + ,{<<"cause">>, StartRange} + ]), + cb_context:add_validation_error(<<"end_range">>, <<"maximum">>, Msg, Context); + F when (To - F) >= MaxRange -> + %% range is too large + Msg = kz_term:to_binary(io_lib:format("end_range ~b is more than ~b seconds from start_range ~b", [To, MaxRange, F])), + JObj = kz_json:from_list([{<<"message">>, Msg}, {<<"cause">>, StartRange}]), + cb_context:add_validation_error(<<"end_range">>, <<"date_range">>, JObj, Context); + F -> + fetch_ranged_stats_summary(Context, F, To, QueueId) + end. + +fetch_ranged_stats_summary(Context, From, To, QueueId) -> + Req = props:filter_undefined( + [{<<"Account-ID">>, cb_context:account_id(Context)} + ,{<<"Status">>, cb_context:req_value(Context, <<"status">>)} + ,{<<"Queue-ID">>, case QueueId of + 'all' -> cb_context:req_value(Context, <<"queue_id">>); + Else -> Else + end} + ,{<<"Start-Range">>, From} + ,{<<"End-Range">>, To} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + case kz_amqp_worker:call(Req + ,fun kapi_acdc_stats:publish_call_summary_req/1 + ,fun kapi_acdc_stats:call_summary_resp_v/1 + ) + of + {'error', E} -> + crossbar_util:response('error', <<"stat request had errors">>, 400 + ,kz_json:get_value(<<"Error-Reason">>, E) + ,Context + ); + {'ok', Resp} -> + RespJObj = kz_json:set_values([{<<"current_timestamp">>, kz_time:current_tstamp()} + ,{<<"Summarized">>, kz_json:get_value(<<"Data">>, Resp, [])} + %% ,{<<"Waiting">>, kz_doc:public_fields(kz_json:get_value(<<"Waiting">>, Resp, []))} + %% ,{<<"Handled">>, kz_doc:public_fields(kz_json:get_value(<<"Handled">>, Resp, []))} + ], kz_json:new()), + crossbar_util:response(RespJObj, Context) + end. fetch_ranged_queue_stats(Context, StartRange) -> MaxRange = ?ACDC_CLEANUP_WINDOW, - Now = kz_time:now_s(), + Now = kz_time:current_tstamp(), Past = Now - MaxRange, To = kz_term:to_integer(cb_context:req_value(Context, <<"end_range">>, Now)), @@ -708,7 +793,7 @@ fetch_ranged_queue_stats(Context, StartRange) -> end. fetch_ranged_queue_stats(Context, From, To, 'true') -> - lager:debug("ranged query from ~b to ~b(~b) of current stats (now ~b)", [From, To, To-From, kz_time:now_s()]), + lager:debug("ranged query from ~b to ~b(~b) of current stats (now ~b)", [From, To, To-From, kz_time:current_tstamp()]), Req = props:filter_undefined( [{<<"Account-ID">>, cb_context:account_id(Context)} ,{<<"Status">>, cb_context:req_value(Context, <<"status">>)} @@ -722,6 +807,38 @@ fetch_ranged_queue_stats(Context, From, To, 'false') -> lager:debug("ranged query from ~b to ~b of archived stats", [From, To]), Context. +-spec fetch_all_current_queue_stats(cb_context:context()) -> cb_context:context(). +fetch_all_current_queue_stats(Context) -> + lager:debug("querying for all recent stats"), + Now = kz_time:now_s(), + From = Now - min(?SECONDS_IN_DAY, ?ACDC_CLEANUP_WINDOW), + + Req = props:filter_undefined( + [{<<"Account-ID">>, cb_context:account_id(Context)} + ,{<<"Status">>, cb_context:req_value(Context, <<"status">>)} + ,{<<"Agent-ID">>, cb_context:req_value(Context, <<"agent_id">>)} + ,{<<"Start-Range">>, From} + ,{<<"End-Range">>, Now} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + fetch_from_amqp(Context, Req). + +format_stats(Context, Resp) -> + Stats = kz_json:from_list([{<<"current_timestamp">>, kz_time:current_tstamp()} + ,{<<"stats">>, + kz_doc:public_fields( + kz_json:get_value(<<"Handled">>, Resp, []) ++ + kz_json:get_value(<<"Abandoned">>, Resp, []) ++ + kz_json:get_value(<<"Waiting">>, Resp, []) ++ + kz_json:get_value(<<"Processed">>, Resp, []) + )} + ]), + cb_context:set_resp_status( + cb_context:set_resp_data(Context, Stats) + ,'success' + ). + + -spec fetch_from_amqp(cb_context:context(), kz_term:proplist()) -> cb_context:context(). fetch_from_amqp(Context, Req) -> case kz_amqp_worker:call(Req @@ -736,15 +853,20 @@ fetch_from_amqp(Context, Req) -> end. %%------------------------------------------------------------------------------ +%% @private %% @doc Attempt to load a summarized listing of all instances of this %% resource. %% @end %%------------------------------------------------------------------------------ -spec summary(cb_context:context()) -> cb_context:context(). summary(Context) -> - crossbar_view:load(Context, ?CB_LIST, [{'mapper', crossbar_view:get_value_fun()}]). + crossbar_view:load(Context, ?CB_LIST ,[{'mapper', crossbar_view:get_value_fun()}]). + +normalize_agents_results(JObj, Acc) -> + [kz_doc:id(JObj) | Acc]. %%------------------------------------------------------------------------------ +%% @private %% @doc Creates an entry in the acdc db of the account's participation in acdc %% @end %%------------------------------------------------------------------------------ @@ -777,3 +899,16 @@ deactivate_account_for_acdc(AccountId) -> lager:debug("failed to remove ~s: ~p", [AccountId, _E]) end end. + +%%------------------------------------------------------------------------------ +%% @private +%% @doc Remove deprecated agents key from the queues jobj +%% @end +%%------------------------------------------------------------------------------ +-spec unset_agents_key(cb_context:context()) -> cb_context:context(). +unset_agents_key(Context) -> + cb_context:update_doc(Context + ,fun(Doc) -> + kz_json:delete_key(<<"agents">>, Doc) + end + ). diff --git a/applications/acdc/src/cf_acdc_agent.erl b/applications/acdc/src/cf_acdc_agent.erl index 295684ae20d..4d257da3361 100644 --- a/applications/acdc/src/cf_acdc_agent.erl +++ b/applications/acdc/src/cf_acdc_agent.erl @@ -1,6 +1,7 @@ %%%----------------------------------------------------------------------------- %%% @copyright (C) 2012-2020, 2600Hz -%%% @doc Handles changing an agent's status +%%% @doc +%%% Handles changing an agent's status %%% "data":{ %%% "action":["login","logout","paused","resume"] // one of these %%% ,"timeout":600 // in seconds, for "paused" status @@ -11,7 +12,6 @@ %%% } %%% %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -56,8 +56,8 @@ handle(Data, Call) -> cf_exe:continue(Call). -spec find_agent_status(kapps_call:call() | kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -find_agent_status(?NE_BINARY = AcctId, AgentId) -> - fix_agent_status(acdc_agent_util:most_recent_status(AcctId, AgentId)); +find_agent_status(?NE_BINARY = AccountId, AgentId) -> + fix_agent_status(acdc_agent_util:most_recent_status(AccountId, AgentId)); find_agent_status(Call, AgentId) -> find_agent_status(kapps_call:account_id(Call), AgentId). @@ -117,16 +117,21 @@ maybe_login_agent(Call, AgentId, Data) -> end. maybe_pause_agent(Call, AgentId, <<"ready">>, Data) -> - pause_agent(Call, AgentId, Data); + Timeout = kapps_call:kvs_fetch('cf_capture_group', Call), + lager:info("agent pause time: ~p", [Timeout]), + case Timeout of + undefined -> pause_agent(Call, AgentId, Data); + T -> pause_agent(Call, AgentId, Data, binary_to_integer(T) * 60) + end; maybe_pause_agent(Call, _AgentId, FromStatus, _Data) -> lager:info("unable to go from ~s to paused", [FromStatus]), play_agent_invalid(Call). --spec login_agent(kapps_call:call(), kz_term:ne_binary()) -> kz_term:api_ne_binary(). +-spec login_agent(kapps_call:call(), kz_term:ne_binary()) -> api_kz_term:ne_binary(). login_agent(Call, AgentId) -> login_agent(Call, AgentId, kz_json:new()). --spec login_agent(kapps_call:call(), kz_term:ne_binary(), kz_json:object()) -> kz_term:api_ne_binary(). +-spec login_agent(kapps_call:call(), kz_term:ne_binary(), kz_json:object()) -> api_kz_term:ne_binary(). login_agent(Call, AgentId, Data) -> Update = props:filter_undefined( [{<<"Account-ID">>, kapps_call:account_id(Call)} @@ -155,7 +160,7 @@ logout_agent(Call, AgentId) -> logout_agent(Call, AgentId, Data) -> update_agent_status(Call, AgentId, Data, fun kapi_acdc_agent:publish_logout/1). -pause_agent(Call, AgentId, Data, Timeout) when is_integer(Timeout) -> +pause_agent(Call, AgentId, Data, Timeout) -> _ = play_agent_pause(Call), update_agent_status(Call, AgentId, Data, fun kapi_acdc_agent:publish_pause/1, Timeout). pause_agent(Call, AgentId, Data) -> @@ -186,10 +191,10 @@ send_new_status(Call, AgentId, Data, PubFun, Timeout) -> ]), PubFun(Update). --spec presence_id(kz_json:object()) -> kz_term:api_ne_binary(). +-spec presence_id(kz_json:object()) -> api_kz_term:ne_binary(). presence_id(Data) -> kz_json:get_ne_binary_value(<<"presence_id">>, Data). --spec presence_state(kz_json:object()) -> kz_term:api_ne_binary(). +-spec presence_state(kz_json:object()) -> api_kz_term:ne_binary(). presence_state(Data) -> format_presence_state(kz_json:get_ne_binary_value(<<"presence_state">>, Data)). diff --git a/applications/acdc/src/cf_acdc_agent_availability.erl b/applications/acdc/src/cf_acdc_agent_availability.erl new file mode 100644 index 00000000000..ea007400c1f --- /dev/null +++ b/applications/acdc/src/cf_acdc_agent_availability.erl @@ -0,0 +1,49 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2016 Voxter Communications Inc. +%%% @doc Data: { +%%% "id":"queue id" +%%% } +%%% +%%% +%%% @author Daniel Finke +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(cf_acdc_agent_availability). + +-export([handle/2]). + +-include_lib("callflow/src/callflow.hrl"). + +-define(AVAILABLE_BRANCH_KEY, <<"available">>). +-define(UNAVAILABLE_BRANCH_KEY, <<"unavailable">>). + +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. +handle(Data, Call) -> + QueueId = kz_doc:id(Data), + Req = props:filter_undefined([{<<"Account-ID">>, kapps_call:account_id(Call)} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Skills">>, kapps_call:kvs_fetch('acdc_required_skills', Call)} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ]), + case kz_amqp_worker:call(Req + ,fun kapi_acdc_queue:publish_agents_available_req/1 + ,fun kapi_acdc_queue:agents_available_resp_v/1 + ) of + {'error', E} -> + lager:debug("error ~p when getting agents availability in queue ~s", [E, QueueId]), + cf_exe:attempt(?AVAILABLE_BRANCH_KEY, Call); + {'ok', Resp} -> branch_on_availability(kz_json:get_integer_value(<<"Agent-Count">>, Resp), Call) + end, + 'ok'. + +-spec branch_on_availability(non_neg_integer(), kapps_call:call()) -> {'attempt_resp', 'ok' | {'error', 'empty'}}. +branch_on_availability(0, Call) -> cf_exe:attempt(?UNAVAILABLE_BRANCH_KEY, Call); +branch_on_availability(_, Call) -> cf_exe:attempt(?AVAILABLE_BRANCH_KEY, Call). diff --git a/applications/acdc/src/cf_acdc_member.erl b/applications/acdc/src/cf_acdc_member.erl index 26eb7f117d1..9097c2153b8 100644 --- a/applications/acdc/src/cf_acdc_member.erl +++ b/applications/acdc/src/cf_acdc_member.erl @@ -1,13 +1,19 @@ %%%----------------------------------------------------------------------------- %%% @copyright (C) 2012-2020, 2600Hz -%%% @doc Data: { +%%% @doc +%%% data: { %%% "id":"queue id" -%%% } +%%% } %%% +%%% data: { +%%% "id":"queue id", +%%% "enter_as_callback","Boolean, if true, enter the queue as a callback from the start" +%%% } %%% -%%% @author James Aimonetti -%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC %%% +%%% @author James Aimonetti +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC +%%% @author Daniel Finke %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -22,16 +28,28 @@ -type max_wait() :: pos_integer() | 'infinity'. +-define(DEFAULT_AMQP_MGT_URL, <<"http://guest:guest@127.0.0.1:15672">>). + -define(MEMBER_TIMEOUT, <<"member_timeout">>). -define(MEMBER_HANGUP, <<"member_hangup">>). --record(member_call, {call :: kapps_call:call() - ,queue_id :: kz_term:api_binary() - ,config_data = [] :: kz_term:proplist() - ,max_wait = 60 :: max_wait() +-record(member_call, {call :: kapps_call:call() + ,queue_id :: kz_term:api_binary() + ,config_data = [] :: kz_term:proplist() + ,breakout_media :: kz_term:api_object() + ,max_wait = 60 * ?MILLISECONDS_IN_SECOND :: max_wait() + ,enter_as_callback :: boolean() + ,queue_jobj :: kz_json:object() }). -type member_call() :: #member_call{}. +-record(breakout_state, {active = 'false' :: boolean() + ,retries = 3 :: non_neg_integer() + ,callback_number :: kz_term:api_binary() + ,callback_entering = 'false' :: boolean() + }). +-type breakout_state() :: #breakout_state{}. + %%------------------------------------------------------------------------------ %% @doc %% @end @@ -55,22 +73,44 @@ handle(Data, Call) -> {'ok', QueueJObj} = kz_datamgr:open_cache_doc(kapps_call:account_db(Call), QueueId), MaxWait = max_wait(kz_json:get_integer_value(<<"connection_timeout">>, QueueJObj, 3600)), - MaxQueueSize = max_queue_size(kz_json:get_integer_value(<<"max_queue_size">>, QueueJObj, 0)), + MaxQueueSize = max_queue_size(kz_json:get_integer_value(<<"max_queue_size">>, QueueJObj, 'undefined')), - Call1 = kapps_call:kvs_store('caller_exit_key', kz_json:get_value(<<"caller_exit_key">>, QueueJObj, <<"#">>), Call), + Call1 = maybe_enable_callback( + kapps_call:kvs_store_proplist([{'caller_exit_key', kz_json:get_value(<<"caller_exit_key">>, QueueJObj)}] + ,Call + ) + ,QueueJObj + ), - CurrQueueSize = kapi_acdc_queue:queue_size(kapps_call:account_id(Call1), QueueId), + CurrQueueSize = current_queue_size(kapps_call:account_id(Call1), QueueId), lager:info("max size: ~p curr size: ~p", [MaxQueueSize, CurrQueueSize]), maybe_enter_queue(#member_call{call=Call1 ,config_data=MemberCall + ,breakout_media=kz_json:get_value([<<"breakout">>, <<"media">>], QueueJObj, kz_json:new()) ,queue_id=QueueId ,max_wait=MaxWait + ,enter_as_callback=kz_json:is_true(<<"enter_as_callback">>, Data) + ,queue_jobj=QueueJObj } ,is_queue_full(MaxQueueSize, CurrQueueSize) ). +-spec maybe_enable_callback(kapps_call:call(), kz_json:object()) -> kapps_call:call(). +maybe_enable_callback(Call, QueueJObj) -> + RestrictedClassifiers = kz_json:get_json_value([<<"breakout">>, <<"classifiers">>], QueueJObj, kz_json:new()), + CallerClassification = knm_converters:classify(kapps_call:from_user(Call)), + BreakoutKey = kz_json:get_ne_binary_value([<<"breakout">>, <<"dtmf">>], QueueJObj), + case BreakoutKey =/= 'undefined' + andalso not callback_restricted(RestrictedClassifiers, CallerClassification) + of + 'true' -> + lager:debug("callbacks are enabled"), + kapps_call:kvs_store_proplist([{'breakout_key', BreakoutKey}], Call); + 'false' -> Call + end. + -spec lookup_priority(kz_json:object(), kapps_call:call()) -> kz_term:api_binary(). lookup_priority(Data, Call) -> FromData = kz_json:get_integer_value(<<"priority">>, Data), @@ -86,37 +126,100 @@ maybe_enter_queue(#member_call{call=Call}, 'true') -> lager:info("queue has reached max size"), cf_exe:continue(Call); maybe_enter_queue(#member_call{call=Call - ,config_data=MemberCall + ,breakout_media=BreakoutMedia + ,enter_as_callback='true' + ,queue_jobj=QueueJObj + }=MC + ,'false') -> + RestrictedClassifiers = kz_json:get_json_value([<<"breakout">>, <<"classifiers">>], QueueJObj, kz_json:new()), + CallerClassification = knm_converters:classify(kapps_call:from_user(Call)), + case callback_restricted(RestrictedClassifiers, CallerClassification) of + 'false' -> + kapps_call_command:flush(Call), + kapps_call_command:hold(<<"silence_stream://0">>, Call), + kapps_call_command:answer(Call), + kapps_call_command:prompt(breakout_prompt(BreakoutMedia), kapps_call:language(Call), Call), + enter_as_callback_loop(MC, #breakout_state{}); + 'true' -> + lager:info("queue restricted callback from caller with classification \"~s\"", [CallerClassification]), + cf_exe:continue(Call) + end; +maybe_enter_queue(#member_call{call=Call ,queue_id=QueueId ,max_wait=MaxWait + ,config_data=MemberCall }=MC ,'false') -> - lager:info("asking for an agent, waiting up to ~p ms", [MaxWait]), + case kapps_call_command:b_channel_status(kapps_call:call_id(Call)) of + {'ok', _} -> + lager:info("asking for an agent, waiting up to ~p ms", [MaxWait]), + cf_exe:amqp_send(Call, MemberCall, fun kapi_acdc_queue:publish_member_call/1), + _ = kapps_call_command:flush_dtmf(Call), + wait_for_bridge(MC#member_call{call=kapps_call:kvs_store('queue_id', QueueId, Call) + } + ,#breakout_state{} + ,MaxWait + ); + {'error', E} -> + lager:info("not entering queue; call was destroyed already (~s)", [E]), + cf_exe:stop(Call) + end. - cf_exe:amqp_send(Call, MemberCall, fun kapi_acdc_queue:publish_member_call/1), - _ = kapps_call_command:flush_dtmf(Call), - wait_for_bridge(MC#member_call{call=kapps_call:kvs_store('queue_id', QueueId, Call)} - ,MaxWait - ). +-spec enter_as_callback_loop(member_call(), breakout_state()) -> 'ok'. +enter_as_callback_loop(MC, BreakoutState) -> + enter_as_callback_loop(MC, BreakoutState, 15000). --spec wait_for_bridge(member_call(), max_wait()) -> 'ok'. -wait_for_bridge(MC, Timeout) -> - wait_for_bridge(MC, Timeout, kz_time:start_time()). +-spec enter_as_callback_loop(member_call(), breakout_state(), integer()) -> 'ok'. +enter_as_callback_loop(#member_call{call=Call} + ,#breakout_state{retries=0} + ,_) -> + lager:info("maximum number of retries reached"), + kapps_call_command:flush_dtmf(Call), + cf_exe:continue(Call); +enter_as_callback_loop(#member_call{call=Call}=MC + ,BreakoutState + ,Timeout) -> + Wait = os:timestamp(), + receive + {'amqp_msg', JObj} -> + case kz_util:get_event_type(JObj) of + {<<"call_event">>, <<"DTMF">>} -> + DTMF = kz_json:get_value(<<"DTMF-Digit">>, JObj), + enter_as_callback_handle_dtmf(DTMF, MC, BreakoutState); + {<<"call_event">>, <<"CHANNEL_DESTROY">>} -> + lager:info("member hungup during enter as callback"), + cf_exe:stop(Call); + _ -> + enter_as_callback_loop(MC, BreakoutState, kz_time:decr_timeout(Timeout, Wait)) + end + after Timeout -> + BreakoutState1 = breakout_invalid_selection(Call, BreakoutState, <<>>), + enter_as_callback_loop(MC, BreakoutState1) + end. + +-spec enter_as_callback_handle_dtmf(binary(), member_call(), breakout_state()) -> 'ok'. +enter_as_callback_handle_dtmf(DTMF, #member_call{call=Call}=MC, BreakoutState) -> + kapps_call_command:flush(Call), + case process_breakout_message(DTMF, MC, BreakoutState) of + 'callback_registered' -> cf_exe:control_usurped(Call); + 'cancel' -> cf_exe:continue(Call); + BreakoutState1 -> enter_as_callback_loop(MC, BreakoutState1) + end. --spec wait_for_bridge(member_call(), max_wait(), kz_time:start_time()) -> 'ok'. -wait_for_bridge(#member_call{call=Call}, Timeout, _Start) when Timeout < 0 -> +-spec wait_for_bridge(member_call(), breakout_state(), max_wait()) -> 'ok'. +wait_for_bridge(MC, BreakoutState, Timeout) -> + wait_for_bridge(MC, BreakoutState, Timeout, os:timestamp()). + +-spec wait_for_bridge(member_call(), breakout_state(), max_wait(), kz_term:kz_now()) -> 'ok'. +wait_for_bridge(#member_call{call=Call}, _, Timeout, _Start) when Timeout < 0 -> lager:debug("timeout is less than 0: ~p", [Timeout]), end_member_call(Call); -wait_for_bridge(#member_call{call=Call}=MC, Timeout, Start) -> - Wait = kz_time:start_time(), - TimeoutMs = case Timeout of - 'infinity' -> 'infinity'; - _ -> Timeout * ?MILLISECONDS_IN_SECOND - end, +wait_for_bridge(#member_call{call=Call}=MC, BreakoutState, Timeout, Start) -> + Wait = os:timestamp(), receive {'amqp_msg', JObj} -> - process_message(MC, Timeout, Start, Wait, JObj, kz_util:get_event_type(JObj)) - after TimeoutMs -> + process_message(MC, BreakoutState, Timeout, Start, Wait, JObj, kz_util:get_event_type(JObj)) + after Timeout -> lager:info("failed to handle the call in time, proceeding"), end_member_call(Call) end. @@ -126,20 +229,20 @@ end_member_call(Call) -> stop_hold_music(Call), cf_exe:continue(Call). --spec process_message(member_call(), max_wait(), kz_time:start_time() - ,kz_time:start_time(), kz_json:object() +-spec process_message(member_call(), breakout_state(), max_wait(), kz_term:kz_now() + ,kz_term:kz_now(), kz_json:object() ,{kz_term:ne_binary(), kz_term:ne_binary()} ) -> 'ok'. -process_message(#member_call{call=Call}, _, Start, _Wait, _JObj, {<<"call_event">>,<<"CHANNEL_BRIDGE">>}) -> +process_message(#member_call{call=Call}, _, _, Start, _Wait, _JObj, {<<"call_event">>,<<"CHANNEL_BRIDGE">>}) -> lager:info("member was bridged to agent, yay! took ~b s", [kz_time:elapsed_s(Start)]), cf_exe:control_usurped(Call); -process_message(#member_call{call=Call}, _, Start, _Wait, _JObj, {<<"call_event">>,<<"CHANNEL_DESTROY">>}) -> +process_message(#member_call{call=Call}, _, _, Start, _Wait, _JObj, {<<"call_event">>,<<"CHANNEL_DESTROY">>}) -> lager:info("member hungup while waiting in the queue (was there ~b s)", [kz_time:elapsed_s(Start)]), cancel_member_call(Call, ?MEMBER_HANGUP), cf_exe:stop(Call); process_message(#member_call{call=Call ,queue_id=QueueId - }=MC, Timeout, Start, Wait, JObj, {<<"member">>, <<"call_fail">>}) -> + }=MC, BreakoutState, Timeout, Start, Wait, JObj, {<<"member">>, <<"call_fail">>}) -> case QueueId =:= kz_json:get_value(<<"Queue-ID">>, JObj) of 'true' -> Failure = kz_json:get_value(<<"Failure-Reason">>, JObj), @@ -151,49 +254,216 @@ process_message(#member_call{call=Call cf_exe:continue(Call); 'false' -> lager:info("failure json was for a different queue, ignoring"), - wait_for_bridge(MC, kz_time:decr_timeout(Timeout, Wait), Start) + wait_for_bridge(MC, BreakoutState, kz_time:decr_timeout(Timeout, Wait), Start) end; -process_message(#member_call{call=Call}=MC, Timeout, Start, Wait, JObj, {<<"call_event">>, <<"DTMF">>}) -> - DigitPressed = kz_json:get_value(<<"DTMF-Digit">>, JObj), - case DigitPressed =:= kapps_call:kvs_fetch('caller_exit_key', Call) of - 'true' -> - lager:info("caller pressed the exit key(~s), moving to next callflow action", [DigitPressed]), +process_message(#member_call{call=Call}, _, _, Start, _Wait, _JObj, {<<"member">>, <<"call_success">>}) -> + lager:info("call was processed by queue (took ~b s)", [kz_time:elapsed_s(Start)]), + kapps_call_command:flush(Call), + cf_exe:control_usurped(Call); +process_message(MC, BreakoutState, Timeout, Start, Wait, JObj, {<<"call_event">>, <<"DTMF">>}) -> + DTMF = kz_json:get_value(<<"DTMF-Digit">>, JObj), + process_dtmf(DTMF, MC, BreakoutState, Timeout, Start, Wait); +process_message(MC, BreakoutState, Timeout, Start, Wait, _JObj, _Type) -> + wait_for_bridge(MC, BreakoutState, kz_time:decr_timeout(Timeout, Wait), Start). + +-spec process_dtmf(binary(), member_call(), breakout_state(), max_wait(), kz_term:kz_now(), kz_term:kz_now()) -> 'ok'. +process_dtmf(DTMF, #member_call{call=Call + ,breakout_media=BreakoutMedia + }=MC + ,#breakout_state{active='false'}=BreakoutState, Timeout, Start, Wait) -> + CallerExitKey = kapps_call:kvs_fetch('caller_exit_key', Call), + BreakoutKey = kapps_call:kvs_fetch('breakout_key', Call), + case DTMF of + CallerExitKey -> + lager:info("caller pressed the exit key(~s), moving to next callflow action", [DTMF]), cancel_member_call(Call, <<"dtmf_exit">>), _ = kapps_call_command:flush_dtmf(Call), timer:sleep(?MILLISECONDS_IN_SECOND), cf_exe:continue(Call); - 'false' -> - lager:info("caller pressed ~s, ignoring", [DigitPressed]), - wait_for_bridge(MC, kz_time:decr_timeout(Timeout, Wait), Start) + BreakoutKey -> + lager:info("caller pressed the breakout menu key(~s)", [DTMF]), + kapps_call_command:flush(Call), + kapps_call_command:hold(<<"silence_stream://0">>, Call), + kapps_call_command:prompt(breakout_prompt(BreakoutMedia), kapps_call:language(Call), Call), + wait_for_bridge(MC, BreakoutState#breakout_state{active='true'}, kz_time:decr_timeout(Timeout, Wait), Start); + _ -> + lager:info("caller pressed ~s, ignoring", [DTMF]), + wait_for_bridge(MC, BreakoutState, kz_time:decr_timeout(Timeout, Wait), Start) end; -process_message(#member_call{call=Call}, _, Start, _Wait, _JObj, {<<"member">>, <<"call_success">>}) -> - lager:info("call was processed by queue (took ~b s)", [kz_time:elapsed_s(Start)]), - cf_exe:control_usurped(Call); -process_message(MC, Timeout, Start, Wait, _JObj, _Type) -> - wait_for_bridge(MC, kz_time:decr_timeout(Timeout, Wait), Start). +process_dtmf(DTMF, #member_call{call=Call}=MC, BreakoutState, Timeout, Start, Wait) -> + case breakout_loop(DTMF, MC, BreakoutState) of + #breakout_state{}=NextState -> + wait_for_bridge(MC, NextState, kz_time:decr_timeout(Timeout, Wait), Start); + 'callback_registered' -> + lager:debug("member callback registered, stopping callflow"), + cf_exe:control_usurped(Call); + 'cancel' -> + wait_for_bridge(MC, #breakout_state{}, kz_time:decr_timeout(Timeout, Wait), Start) + end. + +-spec breakout_loop(binary(), member_call(), breakout_state()) -> breakout_state() | 'callback_registered' | 'cancel'. +breakout_loop(_, #member_call{call=Call}, #breakout_state{retries=0}) -> + lager:info("maximum number of retries reached"), + kapps_call_command:flush_dtmf(Call), + kapps_call_command:hold(Call), + #breakout_state{}; +breakout_loop(DTMF, #member_call{call=Call}=MC, State) -> + kapps_call_command:flush(Call), + process_breakout_message(DTMF, MC, State). +-spec process_breakout_message(binary(), member_call(), breakout_state()) -> breakout_state() | 'callback_registered' | 'cancel'. +process_breakout_message(DTMF, #member_call{call=Call + ,breakout_media=BreakoutMedia + } + ,#breakout_state{callback_number='undefined'}=State) -> + case DTMF of + <<"1">> -> + From = kapps_call:from_user(Call), + breakout_number_correct(Call, BreakoutMedia, State#breakout_state{callback_number=From}); + <<"2">> -> 'cancel'; + DTMF -> breakout_invalid_selection(Call, State, DTMF) + end; +process_breakout_message(DTMF + ,#member_call{call=Call + ,breakout_media=BreakoutMedia + }=MC + ,#breakout_state{callback_number=Number + ,callback_entering='false' + }=State + ) -> + case DTMF of + <<"1">> -> + lager:debug("accepted callback for number ~s", [Number]), + register_callback(MC, Number), + + %% PromptVars = kz_json:from_list([{<<"var1">>, <<"breakout-callback_registered">>}]), + kapps_call_command:prompt(callback_registered(BreakoutMedia), kapps_call:language(Call), Call), + kapps_call_command:queued_hangup(Call), + 'callback_registered'; + <<"2">> -> + kapps_call_command:prompt(enter_callback_number(BreakoutMedia), Call), + State#breakout_state{callback_number= <<>> + ,callback_entering='true' + }; + DTMF -> breakout_invalid_selection(Call, State, DTMF) + end; +process_breakout_message(DTMF + ,#member_call{call=Call + ,breakout_media=BreakoutMedia + } + ,#breakout_state{callback_number=Number + ,callback_entering='true' + }=State + ) -> + case DTMF of + <<"#">> -> breakout_number_correct(Call, BreakoutMedia, State#breakout_state{callback_entering='false'}); + _ -> State#breakout_state{callback_number= <>} + end. + +-spec register_callback(member_call(), kz_term:ne_binary()) -> 'ok'. +register_callback(#member_call{call=Call + ,queue_id=QueueId + ,enter_as_callback='false' + }, Number) -> + Payload = [{<<"Account-ID">>, kapps_call:account_id(Call)} + ,{<<"Queue-ID">>, QueueId} + ,{<<"Call-ID">>, kapps_call:call_id(Call)} + ,{<<"Number">>, Number} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + kapi_acdc_queue:publish_member_callback_reg(Payload); +register_callback(#member_call{call=Call + ,config_data=MemberCall + }, Number) -> + MemberCall1 = props:set_values([{<<"Callback-Number">>, Number} + ,{<<"Enter-As-Callback">>, 'true'} + ], MemberCall), + cf_exe:amqp_send(Call, MemberCall1, fun kapi_acdc_queue:publish_member_call/1). + +-spec breakout_number_correct(kapps_call:call(), kz_term:ne_binary(), breakout_state()) -> breakout_state(). +breakout_number_correct(Call, BreakoutMedia, #breakout_state{callback_number=Number}=State) -> + Prompt = [{'prompt', call_back_at(BreakoutMedia)} + ,{'say', Number} + ,{'prompt', number_correct(BreakoutMedia)} + ], + kapps_call_command:audio_macro(Prompt, Call), + State. + +-spec breakout_invalid_selection(kapps_call:call(), breakout_state(), binary()) -> breakout_state(). +breakout_invalid_selection(Call, #breakout_state{retries=Retries}=State, DTMF) -> + lager:debug("invalid selection ~s", [DTMF]), + kapps_call_command:prompt(<<"menu-invalid_entry">>, Call), + State#breakout_state{retries=Retries-1}. + +-spec callback_restricted(kz_json:object(), kz_term:api_binary()) -> boolean(). +callback_restricted(RestrictedClassifiers, CallerClassification) -> + kz_json:is_false(CallerClassification, RestrictedClassifiers). + +-spec breakout_prompt(kz_json:object()) -> kz_term:ne_binary(). +breakout_prompt(JObj) -> + kz_json:get_ne_value(<<"prompt">>, JObj, <<"breakout-prompt">>). + +-spec callback_registered(kz_json:object()) -> kz_term:ne_binary(). +callback_registered(JObj) -> + kz_json:get_ne_value(<<"callback_registered">>, JObj, <<"breakout-callback_registered">>). + +-spec enter_callback_number(kz_json:object()) -> kz_term:ne_binary(). +enter_callback_number(JObj) -> + kz_json:get_ne_value(<<"enter_callback_number">>, JObj, <<"breakout-enter_callback_number">>). + +-spec call_back_at(kz_json:object()) -> kz_term:ne_binary(). +call_back_at(JObj) -> + kz_json:get_ne_value(<<"call_back_at">>, JObj, <<"breakout-call_back_at">>). + +-spec number_correct(kz_json:object()) -> kz_term:ne_binary(). +number_correct(JObj) -> + kz_json:get_ne_value(<<"number_correct">>, JObj, <<"breakout-number_correct">>). + +%% convert from seconds to milliseconds, or infinity -spec max_wait(integer()) -> max_wait(). max_wait(N) when N < 1 -> 'infinity'; -max_wait(N) -> N. +max_wait(N) -> N * ?MILLISECONDS_IN_SECOND. max_queue_size(N) when is_integer(N), N > 0 -> N; -max_queue_size(_) -> 0. +max_queue_size(_) -> undefined. --spec is_queue_full(non_neg_integer(), non_neg_integer()) -> boolean(). -is_queue_full(0, _) -> 'false'; +-spec is_queue_full(integer()|'undefined', integer()|'undefined') -> boolean(). +is_queue_full('undefined', _) -> 'false'; +is_queue_full(_, 'undefined') -> 'false'; is_queue_full(MaxQueueSize, CurrQueueSize) -> CurrQueueSize >= MaxQueueSize. +-spec current_queue_size(kz_term:ne_binary(), kz_term:ne_binary()) -> integer() | 'undefined'. +current_queue_size(AccountId, QueueId) -> + [MGT] = kz_config:get(<<"amqp">>, <<"mgt_url">>, [?DEFAULT_AMQP_MGT_URL]), + URL = hackney_url:make_url(MGT + ,<<"/api/queues/%2F/acdc.queue." + ,AccountId/binary + ,"." + ,QueueId/binary>> + ,[{<<"columns">>, <<"messages">>}]), + Headers = [{<<"Content-Type">>, <<"application/json">>}], + case hackney:request('get', URL, Headers, [], []) of + {ok, _, _, ClientRef} -> + {ok, Body} = hackney:body(ClientRef), + JObj = kz_json:decode(Body), + kz_json:get_integer_value(<<"messages">>, JObj); + _Else -> + lager:warning("rabbitMQ Management plugin problem, check that 'rabbitmq_management' is enabled in", + "/etc/kazoo/rabbitmq/enabled_plugins"), + 'undefined' + end. + -spec cancel_member_call(kapps_call:call(), kz_term:ne_binary()) -> 'ok'. cancel_member_call(Call, <<"timeout">>) -> lager:info("update reason from `timeout` to `member_timeout`"), cancel_member_call(Call, ?MEMBER_TIMEOUT); cancel_member_call(Call, Reason) -> - AcctId = kapps_call:account_id(Call), + AccountId = kapps_call:account_id(Call), {'ok', QueueId} = kapps_call:kvs_find('queue_id', Call), CallId = kapps_call:call_id(Call), Req = props:filter_undefined( - [{<<"Account-ID">>, AcctId} + [{<<"Account-ID">>, AccountId} ,{<<"Queue-ID">>, QueueId} ,{<<"Call-ID">>, CallId} ,{<<"Reason">>, Reason} diff --git a/applications/acdc/src/cf_acdc_queue.erl b/applications/acdc/src/cf_acdc_queue.erl deleted file mode 100644 index fb6008ca2d2..00000000000 --- a/applications/acdc/src/cf_acdc_queue.erl +++ /dev/null @@ -1,95 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2012-2020, 2600Hz -%%% @doc Handles changing an agent's status -%%% "data":{ -%%% "action":["login","logout"] // one of these -%%% ,"id":"queue_id" // which queue to login/logout the caller -%%% } -%%% -%%% @author James Aimonetti -%%% -%%% This Source Code Form is subject to the terms of the Mozilla Public -%%% License, v. 2.0. If a copy of the MPL was not distributed with this -%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(cf_acdc_queue). - --export([handle/2]). - --include_lib("callflow/src/callflow.hrl"). - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ --spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. -handle(Data, Call) -> - kapps_call_command:answer(Call), - _ = case cf_acdc_agent:find_agent(Call) of - {'ok', 'undefined'} -> - lager:info("not an agent calling in"), - cf_acdc_agent:play_not_an_agent(Call); - {'ok', AgentId} -> - Action = kz_json:get_ne_binary_value(<<"action">>, Data), - QueueId = kz_json:get_ne_binary_value(<<"id">>, Data), - Status = cf_acdc_agent:find_agent_status(Call, AgentId), - - update_queues(Call, AgentId, QueueId, Action), - maybe_update_status(Call, AgentId, QueueId, Status, Action); - {'error', 'multiple_owners'} -> - lager:info("too many owners of device ~s, not logging in", [kapps_call:authorizing_id(Call)]), - cf_acdc_agent:play_agent_invalid(Call) - end, - cf_exe:continue(Call). - -maybe_update_status(Call, AgentId, QueueId, <<"logout">>, <<"login">>) -> - lager:info("agent ~s is logged out, log in to queue ~s", [AgentId, QueueId]), - - update_status(Call, AgentId, <<"login">>), - - send_agent_message(Call, AgentId, QueueId, fun kapi_acdc_agent:publish_login_queue/1), - kapps_call_command:b_prompt(<<"agent-logged_in">>, Call); -maybe_update_status(Call, AgentId, QueueId, _Curr, <<"login">>) -> - send_agent_message(Call, AgentId, QueueId, fun kapi_acdc_agent:publish_login_queue/1), - kapps_call_command:b_prompt(<<"agent-logged_in">>, Call); -maybe_update_status(Call, AgentId, _QueueId, <<"logout">>, _Action) -> - lager:debug("agent ~s is logged out completely already", [AgentId]), - cf_acdc_agent:play_agent_invalid(Call); -maybe_update_status(Call, AgentId, QueueId, _Status, <<"logout">>) -> - send_agent_message(Call, AgentId, QueueId, fun kapi_acdc_agent:publish_logout_queue/1), - kapps_call_command:b_prompt(<<"agent-logged_out">>, Call); -maybe_update_status(Call, _AgentId, _QueueId, _Status, _Action) -> - lager:info("invalid agent action: ~s to ~s", [_Status, _Action]), - cf_acdc_agent:play_agent_invalid(Call). - -update_status(Call, AgentId, Status) -> - Extra = [{<<"call_id">>, kapps_call:call_id(Call)} - ,{<<"method">>, <<"callflow">>} - ], - - 'ok' = acdc_agent_util:update_status(kapps_call:account_id(Call), AgentId, Status, Extra). - -send_agent_message(Call, AgentId, QueueId, PubFun) -> - Prop = props:filter_undefined( - [{<<"Account-ID">>, kapps_call:account_id(Call)} - ,{<<"Agent-ID">>, AgentId} - ,{<<"Queue-ID">>, QueueId} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ]), - PubFun(Prop). - --spec update_queues(kapps_call:call(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> - {'ok', kz_json:object()} - | kz_datamgr:data_error(). -update_queues(Call, AgentId, QueueId, <<"login">>) -> - kz_datamgr:update_cache_doc(kapps_call:account_db(Call) - ,AgentId - ,fun (JObj) -> kzd_agent:maybe_add_queue(JObj, QueueId, 'skip') end - ); -update_queues(Call, AgentId, QueueId, <<"logout">>) -> - kz_datamgr:update_cache_doc(kapps_call:account_db(Call) - ,AgentId - ,fun (JObj) -> kzd_agent:maybe_rm_queue(JObj, QueueId, 'skip') end - ). diff --git a/applications/acdc/src/cf_acdc_required_skills.erl b/applications/acdc/src/cf_acdc_required_skills.erl new file mode 100644 index 00000000000..c828b4b3d0e --- /dev/null +++ b/applications/acdc/src/cf_acdc_required_skills.erl @@ -0,0 +1,71 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2018, Voxter Communications Inc +%%% @doc Data: { +%%% "add": [ +%%% "skill1", +%%% ... +%%% ], +%%% "remove": [ +%%% "skill2", +%%% ... +%%% ] +%%% } +%%% +%%% +%%% @author Daniel Finke +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(cf_acdc_required_skills). + +-export([handle/2]). + +-include_lib("callflow/src/callflow.hrl"). + +-define(KVS_KEY, 'acdc_required_skills'). + +%%------------------------------------------------------------------------------ +%% Handle execution of this callflow module +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. +handle(Data, Call) -> + Add = kz_json:get_list_value(<<"add">>, Data, []), + Remove = kz_json:get_list_value(<<"remove">>, Data, []), + + Skills = kapps_call:kvs_fetch(?KVS_KEY, [], Call), + Skills1 = remove_skills(Remove, add_skills(Add, Skills)), + + lager:info("resulting list of required skills: ~p", [Skills1]), + + Call1 = kapps_call:kvs_store(?KVS_KEY, Skills1, Call), + cf_exe:set_call(Call1), + cf_exe:continue(Call1). + +%%------------------------------------------------------------------------------ +%% @private +%% Add new skills to a list of required skills +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec add_skills(kz_term:ne_binaries(), kz_term:ne_binaries()) -> kz_term:ne_binaries(). +add_skills(Add, Skills) -> + lists:usort(Skills ++ Add). + +%%------------------------------------------------------------------------------ +%% @private +%% Remove skills from a list of required skills +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec remove_skills(kz_term:ne_binaries(), kz_term:ne_binaries()) -> kz_term:ne_binaries(). +remove_skills(Remove, Skills) -> + lists:filter(fun(Skill) -> + not lists:member(Skill, Remove) + end + ,Skills + ). diff --git a/applications/acdc/src/cf_acdc_wait_time.erl b/applications/acdc/src/cf_acdc_wait_time.erl index d967c7d5ed1..37a4a604d97 100644 --- a/applications/acdc/src/cf_acdc_wait_time.erl +++ b/applications/acdc/src/cf_acdc_wait_time.erl @@ -1,15 +1,12 @@ %%%----------------------------------------------------------------------------- -%%% @copyright (C) 2018-, Voxter Communications Inc -%%% @doc Handles branching the callflow based on the current average wait time -%%% of a queue -%%% Data: { +%%% @copyright (C) 2018, Voxter Communications Inc +%%% @doc Data: { %%% "id":"queue id", %%% "window":900 // Window over which average wait time is calc'd %%% } %%% %%% %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -23,15 +20,22 @@ -include_lib("callflow/src/callflow.hrl"). %%------------------------------------------------------------------------------ -%% @doc Handle execution of this callflow module +%% Handle execution of this callflow module +%% @doc %% @end %%------------------------------------------------------------------------------ -spec handle(kz_json:object(), kapps_call:call()) -> 'ok'. handle(Data, Call) -> AccountId = kapps_call:account_id(Call), - QueueId = kz_json:get_ne_binary_value(<<"id">>, Data), + QueueId = kz_doc:id(Data), + Skills = maybe_include_skills(QueueId, Call), Window = kz_json:get_integer_value(<<"window">>, Data), + case Skills of + 'undefined' -> 'ok'; + _ -> lager:info("evaluating average wait time for skill set ~p", [Skills]) + end, + case Window of 'undefined' -> 'ok'; _ -> lager:info("evaluating average wait time over last ~b seconds", [Window]) @@ -40,6 +44,7 @@ handle(Data, Call) -> Req = props:filter_undefined( [{<<"Account-ID">>, AccountId} ,{<<"Queue-ID">>, QueueId} + ,{<<"Skills">>, Skills} ,{<<"Window">>, Window} | kz_api:default_headers(?APP_NAME, ?APP_VERSION) ]), @@ -59,6 +64,23 @@ handle(Data, Call) -> end. %%------------------------------------------------------------------------------ +%% @private +%% @doc If the selected strategy on the requested queue is skills-based +%% round robin, skills should be considered in the wait time eval. +%% @end +%%------------------------------------------------------------------------------ +-spec maybe_include_skills(kz_term:ne_binary(), kapps_call:call()) -> api_kz_term:ne_binaries(). +maybe_include_skills(QueueId, Call) -> + AccountDb = kapps_call:account_db(Call), + {'ok', JObj} = kz_datamgr:open_cache_doc(AccountDb, QueueId), + case kz_json:get_ne_binary_value(<<"strategy">>, JObj) of + <<"skills_based_round_robin">> -> + kapps_call:kvs_fetch('acdc_required_skills', [], Call); + _ -> 'undefined' + end. + +%%------------------------------------------------------------------------------ +%% @private %% @doc Continue to the branch of the callflow with the highest exceeded %% threshold %% @end diff --git a/applications/acdc/src/kapi_acdc_agent.erl b/applications/acdc/src/kapi_acdc_agent.erl index 22c41f429a0..20756d4d0ca 100644 --- a/applications/acdc/src/kapi_acdc_agent.erl +++ b/applications/acdc/src/kapi_acdc_agent.erl @@ -3,6 +3,8 @@ %%% @doc Bindings and JSON APIs for dealing with agents, as part of ACDc %%% @author James Aimonetti %%% +%%% @author James Aimonetti +%%% @author Daniel Finke %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -22,8 +24,12 @@ ,end_wrapup/1, end_wrapup_v/1 ,login_queue/1, login_queue_v/1 ,logout_queue/1, logout_queue_v/1 + ,restart/1, restart_v/1 ,login_resp/1, login_resp_v/1 + + ,shared_originate_failure/1, shared_originate_failure_v/1 + ,shared_call_id/1, shared_call_id_v/1 ]). -export([bind_q/2 @@ -42,8 +48,12 @@ ,publish_end_wrapup/1, publish_end_wrapup/2 ,publish_login_queue/1, publish_login_queue/2 ,publish_logout_queue/1, publish_logout_queue/2 + ,publish_restart/1, publish_restart/2 ,publish_login_resp/2, publish_login_resp/3 + + ,publish_shared_originate_failure/1, publish_shared_originate_failure/2 + ,publish_shared_call_id/1, publish_shared_call_id/2 ]). -include_lib("kazoo_stdlib/include/kz_types.hrl"). @@ -84,30 +94,31 @@ sync_req_v(JObj) -> -spec sync_req_routing_key(kz_json:object() | kz_term:proplist()) -> kz_term:ne_binary(). sync_req_routing_key(Props) when is_list(Props) -> Id = props:get_value(<<"Agent-ID">>, Props, <<"*">>), - AcctId = props:get_value(<<"Account-ID">>, Props, <<"*">>), - sync_req_routing_key(AcctId, Id); + AccountId = props:get_value(<<"Account-ID">>, Props, <<"*">>), + sync_req_routing_key(AccountId, Id); sync_req_routing_key(JObj) -> Id = kz_json:get_value(<<"Agent-ID">>, JObj, <<"*">>), - AcctId = kz_json:get_value(<<"Account-ID">>, JObj, <<"*">>), - sync_req_routing_key(AcctId, Id). + AccountId = kz_json:get_value(<<"Account-ID">>, JObj, <<"*">>), + sync_req_routing_key(AccountId, Id). -spec sync_req_routing_key(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -sync_req_routing_key(AcctId, Id) -> - <>. +sync_req_routing_key(AccountId, Id) -> + <>. %% And the response -define(SYNC_RESP_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>, <<"Status">>]). -define(OPTIONAL_SYNC_RESP_HEADERS, [<<"Call-ID">>, <<"Time-Left">>, <<"Process-ID">>]). -define(SYNC_RESP_VALUES, [{<<"Event-Category">>, <<"agent">>} ,{<<"Event-Name">>, <<"sync_resp">>} - ,{<<"Status">>, [<<"init">> - ,<<"sync">> + ,{<<"Status">>, [<<"sync">> ,<<"ready">> - ,<<"waiting">> ,<<"ringing">> + ,<<"ringing_callback">> + ,<<"awaiting_callback">> ,<<"answered">> ,<<"wrapup">> ,<<"paused">> + ,<<"outbound">> ]} ]). -define(SYNC_RESP_TYPES, []). @@ -134,11 +145,11 @@ sync_resp_v(JObj) -> %% agent, within an account %%------------------------------------------------------------------------------ -%% agent.stats_req.ACCTID.AGENT_ID +%% agent.stats_req.AccountId.AGENT_ID -define(STATS_REQ_KEY, "acdc.agent.stats_req."). -define(STATS_REQ_HEADERS, [<<"Account-ID">>]). --define(OPTIONAL_STATS_REQ_HEADERS, [<<"Agent-ID">>]). +-define(OPTIONAL_STATS_REQ_HEADERS, [<<"Agent-ID">>, <<"Call-ID">>]). -define(STATS_REQ_VALUES, [{<<"Event-Category">>, <<"agent">>} ,{<<"Event-Name">>, <<"stats_req">>} ]). @@ -164,33 +175,46 @@ stats_req_v(JObj) -> stats_req_routing_key(Props) when is_list(Props) -> Id = props:get_value(<<"Account-ID">>, Props, <<"*">>), AgentId = props:get_value(<<"Agent-ID">>, Props, <<"*">>), - stats_req_routing_key(Id, AgentId); + CallId = props:get_value(<<"Call-ID">>, Props, <<"*">>), + stats_req_routing_key(Id, AgentId, CallId); stats_req_routing_key(Id) when is_binary(Id) -> <>; stats_req_routing_key(JObj) -> Id = kz_json:get_value(<<"Account-ID">>, JObj, <<"*">>), AgentId = kz_json:get_value(<<"Agent-ID">>, JObj, <<"*">>), - stats_req_routing_key(Id, AgentId). + CallId = kz_json:get_value(<<"Call-ID">>, JObj, <<"*">>), + stats_req_routing_key(Id, AgentId, CallId). -spec stats_req_publish_key(kz_json:object() | kz_term:proplist() | kz_term:ne_binary()) -> kz_term:ne_binary(). stats_req_publish_key(Props) when is_list(Props) -> stats_req_routing_key(props:get_value(<<"Account-ID">>, Props) ,props:get_value(<<"Agent-ID">>, Props) + ,props:get_value(<<"Call-ID">>, Props) ); stats_req_publish_key(JObj) -> stats_req_routing_key(kz_json:get_value(<<"Account-ID">>, JObj) ,kz_json:get_value(<<"Agent-ID">>, JObj) + ,kz_json:get_value(<<"Call-ID">>, JObj) ). -spec stats_req_routing_key(kz_term:ne_binary(), kz_term:api_binary()) -> kz_term:ne_binary(). stats_req_routing_key(Id, 'undefined') -> - stats_req_routing_key(Id); + stats_req_routing_key(Id, 'undefined', 'undefined'); stats_req_routing_key(Id, AgentId) -> - <>. + stats_req_routing_key(Id, AgentId, 'undefined'). + +stats_req_routing_key(Id, 'undefined', 'undefined') -> + stats_req_routing_key(Id); +stats_req_routing_key(Id, AgentId, 'undefined') -> + <>; +stats_req_routing_key('undefined', 'undefined', CallId) -> + <>; +stats_req_routing_key(Id, AgentId, CallId) -> + <>. %% And the response -define(STATS_RESP_HEADERS, [<<"Account-ID">>]). --define(OPTIONAL_STATS_RESP_HEADERS, [<<"Current-Calls">>, <<"Current-Stats">>, <<"Current-Statuses">>]). +-define(OPTIONAL_STATS_RESP_HEADERS, [<<"Current-Calls">>, <<"Current-Stats">>, <<"Current-Statuses">>, <<"Agent-Call-IDs">>]). -define(STATS_RESP_VALUES, [{<<"Event-Category">>, <<"agent">>} ,{<<"Event-Name">>, <<"stats_resp">>} ]). @@ -217,16 +241,16 @@ stats_resp_v(JObj) -> -define(AGENT_KEY, "acdc.agent.action."). % append queue ID -define(AGENT_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>]). --define(OPTIONAL_AGENT_HEADERS, [<<"Presence-ID">> - ,<<"Presence-State">> - ,<<"Queue-ID">> - ,<<"Time-Limit">> +-define(OPTIONAL_AGENT_HEADERS, [<<"Time-Limit">>, <<"Queue-ID">> + ,<<"Presence-ID">>, <<"Presence-State">> ]). -define(AGENT_VALUES, [{<<"Event-Category">>, <<"agent">>} ,{<<"Presence-State">>, kapi_presence:presence_states()} ]). -define(AGENT_TYPES, []). +-define(OPTIONAL_PAUSE_HEADERS, [<<"Alias">>]). + -define(LOGIN_VALUES, [{<<"Event-Name">>, <<"login">>} | ?AGENT_VALUES]). -define(LOGOUT_VALUES, [{<<"Event-Name">>, <<"logout">>} | ?AGENT_VALUES]). -define(PAUSE_VALUES, [{<<"Event-Name">>, <<"pause">>} | ?AGENT_VALUES]). @@ -234,6 +258,7 @@ stats_resp_v(JObj) -> -define(END_WRAPUP_VALUES, [{<<"Event-Name">>, <<"end_wrapup">>} | ?AGENT_VALUES]). -define(LOGIN_QUEUE_VALUES, [{<<"Event-Name">>, <<"login_queue">>} | ?AGENT_VALUES]). -define(LOGOUT_QUEUE_VALUES, [{<<"Event-Name">>, <<"logout_queue">>} | ?AGENT_VALUES]). +-define(RESTART_VALUES, [{<<"Event-Name">>, <<"restart">>} | ?AGENT_VALUES]). -spec login(kz_term:api_terms()) -> {'ok', iolist()} | @@ -309,7 +334,7 @@ logout_queue_v(JObj) -> {'error', string()}. pause(Props) when is_list(Props) -> case pause_v(Props) of - 'true' -> kz_api:build_message(Props, ?AGENT_HEADERS, ?OPTIONAL_AGENT_HEADERS); + 'true' -> kz_api:build_message(Props, ?AGENT_HEADERS, ?OPTIONAL_AGENT_HEADERS ++ ?OPTIONAL_PAUSE_HEADERS); 'false' -> {'error', "Proplist failed validation for agent_pause"} end; pause(JObj) -> @@ -351,16 +376,31 @@ end_wrapup_v(Prop) when is_list(Prop) -> kz_api:validate(Prop, ?AGENT_HEADERS, ?END_WRAPUP_VALUES, ?AGENT_TYPES); end_wrapup_v(JObj) -> end_wrapup_v(kz_json:to_proplist(JObj)). +-spec restart(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +restart(Props) when is_list(Props) -> + case restart_v(Props) of + 'true' -> kz_api:build_message(Props, ?AGENT_HEADERS, ?OPTIONAL_AGENT_HEADERS); + 'false' -> {'error', "Proplist failed validation for agent_restart"} + end; +restart(JObj) -> restart(kz_json:to_proplist(JObj)). + +-spec restart_v(kz_term:api_terms()) -> boolean(). +restart_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENT_HEADERS, ?RESTART_VALUES, ?AGENT_TYPES); +restart_v(JObj) -> restart_v(kz_json:to_proplist(JObj)). + -spec agent_status_routing_key(kz_term:proplist()) -> kz_term:ne_binary(). agent_status_routing_key(Props) when is_list(Props) -> Id = props:get_value(<<"Agent-ID">>, Props, <<"*">>), - AcctId = props:get_value(<<"Account-ID">>, Props, <<"*">>), + AccountId = props:get_value(<<"Account-ID">>, Props, <<"*">>), Status = props:get_value(<<"Event-Name">>, Props, <<"*">>), - agent_status_routing_key(AcctId, Id, Status). + agent_status_routing_key(AccountId, Id, Status). -spec agent_status_routing_key(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -agent_status_routing_key(AcctId, AgentId, Status) -> - <>. +agent_status_routing_key(AccountId, AgentId, Status) -> + <>. -define(LOGIN_RESP_HEADERS, [<<"Status">>]). -define(OPTIONAL_LOGIN_RESP_HEADERS, []). @@ -387,6 +427,95 @@ login_resp_v(Prop) when is_list(Prop) -> login_resp_v(JObj) -> login_resp_v(kz_json:to_proplist(JObj)). +%%------------------------------------------------------------------------------ +%% Sharing of originate_failure to all agent FSMs +%%------------------------------------------------------------------------------ +-define(FSM_SHARED_KEY, "acdc.agent.fsm_shared."). + +-spec fsm_shared_routing_key(kz_term:proplist()) -> kz_term:ne_binary(). +fsm_shared_routing_key(Props) when is_list(Props) -> + Id = props:get_value(<<"Agent-ID">>, Props, <<"*">>), + AccountId = props:get_value(<<"Account-ID">>, Props, <<"*">>), + fsm_shared_routing_key(AccountId, Id). + +-spec fsm_shared_routing_key(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). +fsm_shared_routing_key(AccountId, AgentId) -> + <>. + +-define(SHARED_FAILURE_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>]). +-define(OPTIONAL_SHARED_FAILURE_HEADERS, [<<"Blame">>]). +-define(SHARED_FAILURE_VALUES, [{<<"Event-Category">>, <<"agent">>} + ,{<<"Event-Name">>, <<"shared_failure">>} + ,{<<"Blame">>, [<<"member">>]} + ]). +-define(SHARED_FAILURE_TYPES, []). + +-spec shared_originate_failure(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +shared_originate_failure(Props) when is_list(Props) -> + case shared_originate_failure_v(Props) of + 'true' -> kz_api:build_message(Props, ?SHARED_FAILURE_HEADERS, ?OPTIONAL_SHARED_FAILURE_HEADERS); + 'false' -> {'error', "Proplist failed validation for shared_originate_failure"} + end; +shared_originate_failure(JObj) -> shared_originate_failure(kz_json:to_proplist(JObj)). + +-spec shared_originate_failure_v(kz_term:api_terms()) -> boolean(). +shared_originate_failure_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?SHARED_FAILURE_HEADERS, ?SHARED_FAILURE_VALUES, ?SHARED_FAILURE_TYPES); +shared_originate_failure_v(JObj) -> shared_originate_failure_v(kz_json:to_proplist(JObj)). + +%%------------------------------------------------------------------------------ +%% Sharing of answered call id to all agent FSMs +%%------------------------------------------------------------------------------ +-define(SHARED_CALL_ID_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>]). +-define(OPTIONAL_SHARED_CALL_ID_HEADERS, [<<"Agent-Call-ID">>, <<"Member-Call-ID">>]). +-define(SHARED_CALL_ID_VALUES, [{<<"Event-Category">>, <<"agent">>} + ,{<<"Event-Name">>, <<"shared_call_id">>} + ]). +-define(SHARED_CALL_ID_TYPES, []). + +-spec shared_call_id(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +shared_call_id(Props) when is_list(Props) -> + case shared_call_id_v(Props) of + 'true' -> kz_api:build_message(Props, ?SHARED_CALL_ID_HEADERS, ?OPTIONAL_SHARED_CALL_ID_HEADERS); + 'false' -> {'error', "Proplist failed validation for shared_call_id"} + end; +shared_call_id(JObj) -> shared_call_id(kz_json:to_proplist(JObj)). + +-spec shared_call_id_v(kz_term:api_terms()) -> boolean(). +shared_call_id_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?SHARED_CALL_ID_HEADERS, ?SHARED_CALL_ID_VALUES, ?SHARED_CALL_ID_TYPES); +shared_call_id_v(JObj) -> shared_call_id_v(kz_json:to_proplist(JObj)). + +%%------------------------------------------------------------------------------ +%% Shared routing key for member_connect_satisfied +%%------------------------------------------------------------------------------ +-spec member_connect_satisfied_routing_key(kz_term:api_terms() | kz_term:ne_binary()) -> kz_term:ne_binary(). +member_connect_satisfied_routing_key(Props) when is_list(Props) -> + AgentId = props:get_value(<<"Agent-ID">>, Props), + member_connect_satisfied_routing_key(AgentId); +member_connect_satisfied_routing_key(AgentId) when is_binary(AgentId) -> + <<"acdc.member.connect_satisfied.", AgentId/binary>>; +member_connect_satisfied_routing_key(JObj) -> + AgentId = kz_json:get_value(<<"Agent-ID">>, JObj), + member_connect_satisfied_routing_key(AgentId). + +%%------------------------------------------------------------------------------ +%% Shared routing key for member_connect_win +%%------------------------------------------------------------------------------ +-spec member_connect_win_routing_key(kz_term:api_terms() | kz_term:ne_binary()) -> kz_term:ne_binary(). +member_connect_win_routing_key(Props) when is_list(Props) -> + AgentId = props:get_value(<<"Agent-ID">>, Props), + member_connect_win_routing_key(AgentId); +member_connect_win_routing_key(AgentId) when is_binary(AgentId) -> + <<"acdc.member.connect_win.", AgentId/binary>>; +member_connect_win_routing_key(JObj) -> + AgentId = kz_json:get_value(<<"Agent-ID">>, JObj), + member_connect_win_routing_key(AgentId). + %%------------------------------------------------------------------------------ %% Bind/Unbind the queue as appropriate %%------------------------------------------------------------------------------ @@ -394,27 +523,41 @@ login_resp_v(JObj) -> -spec bind_q(binary(), kz_term:proplist()) -> 'ok'. bind_q(Q, Props) -> AgentId = props:get_value('agent_id', Props, <<"*">>), - AcctId = props:get_value('account_id', Props, <<"*">>), + AccountId = props:get_value('account_id', Props, <<"*">>), + CallId = props:get_value('callid', Props, <<"*">>), Status = props:get_value('status', Props, <<"*">>), - bind_q(Q, {AcctId, AgentId, Status}, props:get_value('restrict_to', Props)). - --spec bind_q(binary(), {kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()}, 'undefined' | list()) -> 'ok'. -bind_q(Q, {AcctId, AgentId, Status}, 'undefined') -> - kz_amqp_util:bind_q_to_kapps(Q, agent_status_routing_key(AcctId, AgentId, Status)), - kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AcctId, AgentId)), - kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AcctId)), - kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AcctId, AgentId)); -bind_q(Q, {AcctId, AgentId, Status}=Ids, ['status'|T]) -> - kz_amqp_util:bind_q_to_kapps(Q, agent_status_routing_key(AcctId, AgentId, Status)), + bind_q(Q, {AccountId, AgentId, CallId, Status}, props:get_value('restrict_to', Props)). + +-spec bind_q(binary(), {kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()}, 'undefined' | list()) -> 'ok'. +bind_q(Q, {AccountId, AgentId, _, Status}, 'undefined') -> + kz_amqp_util:bind_q_to_kapps(Q, agent_status_routing_key(AccountId, AgentId, Status)), + kz_amqp_util:bind_q_to_kapps(Q, fsm_shared_routing_key(AccountId, AgentId)), + kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AccountId, AgentId)), + kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AccountId)), + kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AccountId, AgentId)); +bind_q(Q, {_, AgentId, _, _}=Ids, ['member_connect_win'|T]) -> + kz_amqp_util:bind_q_to_callmgr(Q, member_connect_win_routing_key(AgentId)), + bind_q(Q, Ids, T); +bind_q(Q, {_, AgentId, _, _}=Ids, ['member_connect_satisfied'|T]) -> + kz_amqp_util:bind_q_to_callmgr(Q, member_connect_satisfied_routing_key(AgentId)), + bind_q(Q, Ids, T); +bind_q(Q, {AccountId, AgentId, _, Status}=Ids, ['status'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, agent_status_routing_key(AccountId, AgentId, Status)), + bind_q(Q, Ids, T); +bind_q(Q, {AccountId, AgentId, _, _}=Ids, ['fsm_shared'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, fsm_shared_routing_key(AccountId, AgentId)), bind_q(Q, Ids, T); -bind_q(Q, {AcctId, AgentId, _}=Ids, ['sync'|T]) -> - kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AcctId, AgentId)), +bind_q(Q, {AccountId, AgentId, _, _}=Ids, ['sync'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AccountId, AgentId)), bind_q(Q, Ids, T); -bind_q(Q, {AcctId, <<"*">>, _}=Ids, ['stats_req'|T]) -> - kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AcctId)), +bind_q(Q, {<<"*">>, <<"*">>, CallId, _}=Ids, ['stats_req'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key('undefined', 'undefined', CallId)), bind_q(Q, Ids, T); -bind_q(Q, {AcctId, AgentId, _}=Ids, ['stats_req'|T]) -> - kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AcctId, AgentId)), +bind_q(Q, {AccountId, <<"*">>, <<"*">>, _}=Ids, ['stats_req'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AccountId)), + bind_q(Q, Ids, T); +bind_q(Q, {AccountId, AgentId, <<"*">>, _}=Ids, ['stats_req'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, stats_req_routing_key(AccountId, AgentId)), bind_q(Q, Ids, T); bind_q(Q, Ids, [_|T]) -> bind_q(Q, Ids, T); bind_q(_, _, []) -> 'ok'. @@ -422,33 +565,47 @@ bind_q(_, _, []) -> 'ok'. -spec unbind_q(binary(), kz_term:proplist()) -> 'ok'. unbind_q(Q, Props) -> AgentId = props:get_value('agent_id', Props, <<"*">>), - AcctId = props:get_value('account_id', Props, <<"*">>), + AccountId = props:get_value('account_id', Props, <<"*">>), + CallId = props:get_value('callid', Props, <<"*">>), Status = props:get_value('status', Props, <<"*">>), - unbind_q(Q, {AcctId, AgentId, Status}, props:get_value('restrict_to', Props)). + unbind_q(Q, {AccountId, AgentId, CallId, Status}, props:get_value('restrict_to', Props)). --spec unbind_q(binary(), {kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()}, 'undefined' | list()) -> 'ok'. -unbind_q(Q, {AcctId, AgentId, Status}, 'undefined') -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_status_routing_key(AcctId, AgentId, Status)), - _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AcctId, AgentId)), - kz_amqp_util:unbind_q_from_kapps(Q, stats_req_routing_key(AcctId)); -unbind_q(Q, {AcctId, AgentId, Status}=Ids, ['status'|T]) -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_status_routing_key(AcctId, AgentId, Status)), +-spec unbind_q(binary(), {kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()}, 'undefined' | list()) -> 'ok'. +unbind_q(Q, {AccountId, AgentId, _, Status}, 'undefined') -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_status_routing_key(AccountId, AgentId, Status)), + _ = kz_amqp_util:unbind_q_from_kapps(Q, fsm_shared_routing_key(AccountId, AgentId)), + _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AccountId, AgentId)), + kz_amqp_util:unbind_q_from_kapps(Q, stats_req_routing_key(AccountId)); +unbind_q(Q, {_, AgentId, _, _}=Ids, ['member_connect_win'|T]) -> + kz_amqp_util:unbind_q_from_callmgr(Q, member_connect_win_routing_key(AgentId)), + unbind_q(Q, Ids, T); +unbind_q(Q, {_, AgentId, _, _}=Ids, ['member_connect_satisfied'|T]) -> + kz_amqp_util:unbind_q_from_callmgr(Q, member_connect_satisfied_routing_key(AgentId)), + unbind_q(Q, Ids, T); +unbind_q(Q, {AccountId, AgentId, _, Status}=Ids, ['status'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_status_routing_key(AccountId, AgentId, Status)), + unbind_q(Q, Ids, T); +unbind_q(Q, {AccountId, AgentId, _, _}=Ids, ['fsm_shared'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, fsm_shared_routing_key(AccountId, AgentId)), unbind_q(Q, Ids, T); -unbind_q(Q, {AcctId, AgentId, _}=Ids, ['sync'|T]) -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AcctId, AgentId)), +unbind_q(Q, {AccountId, AgentId, _, _}=Ids, ['sync'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AccountId, AgentId)), unbind_q(Q, Ids, T); -unbind_q(Q, {AcctId, <<"*">>, _}=Ids, ['stats'|T]) -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, stats_req_routing_key(AcctId)), +unbind_q(Q, {<<"*">>, <<"*">>, CallId, _}=Ids, ['stats_req'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, stats_req_routing_key('undefined', 'undefined', CallId)), unbind_q(Q, Ids, T); -unbind_q(Q, {AcctId, AgentId, _}=Ids, ['stats'|T]) -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, stats_req_routing_key(AcctId, AgentId)), +unbind_q(Q, {AccountId, <<"*">>, <<"*">>, _}=Ids, ['stats'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, stats_req_routing_key(AccountId)), + unbind_q(Q, Ids, T); +unbind_q(Q, {AccountId, AgentId, <<"*">>, _}=Ids, ['stats'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, stats_req_routing_key(AccountId, AgentId)), unbind_q(Q, Ids, T); unbind_q(Q, Ids, [_|T]) -> unbind_q(Q, Ids, T); unbind_q(_, _, []) -> 'ok'. %%------------------------------------------------------------------------------ -%% @doc Declare the exchanges used by this API +%% @doc declare the exchanges used by this API %% @end %%------------------------------------------------------------------------------ -spec declare_exchanges() -> 'ok'. @@ -458,7 +615,6 @@ declare_exchanges() -> %%------------------------------------------------------------------------------ %% Publishers for convenience %%------------------------------------------------------------------------------ - -spec publish_sync_req(kz_term:api_terms()) -> 'ok'. publish_sync_req(JObj) -> publish_sync_req(JObj, ?DEFAULT_CONTENT_TYPE). @@ -559,6 +715,15 @@ publish_end_wrapup(API, ContentType) -> {'ok', Payload} = end_wrapup((API1 = kz_api:prepare_api_payload(API, ?END_WRAPUP_VALUES))), kz_amqp_util:kapps_publish(agent_status_routing_key(API1), Payload, ContentType). +-spec publish_restart(kz_term:api_terms()) -> 'ok'. +publish_restart(JObj) -> + publish_restart(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_restart(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_restart(API, ContentType) -> + {'ok', Payload} = restart((API1 = kz_api:prepare_api_payload(API, ?RESTART_VALUES))), + kz_amqp_util:kapps_publish(agent_status_routing_key(API1), Payload, ContentType). + -spec publish_login_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. publish_login_resp(RespQ, JObj) -> publish_login_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). @@ -567,3 +732,21 @@ publish_login_resp(RespQ, JObj) -> publish_login_resp(RespQ, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?LOGIN_RESP_VALUES, fun login_resp/1), kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + +-spec publish_shared_originate_failure(kz_term:api_terms()) -> 'ok'. +publish_shared_originate_failure(JObj) -> + publish_shared_originate_failure(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_shared_originate_failure(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_shared_originate_failure(API, ContentType) -> + {'ok', Payload} = shared_originate_failure((API1 = kz_api:prepare_api_payload(API, ?SHARED_FAILURE_VALUES))), + kz_amqp_util:kapps_publish(fsm_shared_routing_key(API1), Payload, ContentType). + +-spec publish_shared_call_id(kz_term:api_terms()) -> 'ok'. +publish_shared_call_id(JObj) -> + publish_shared_call_id(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_shared_call_id(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_shared_call_id(API, ContentType) -> + {'ok', Payload} = shared_call_id((API1 = kz_api:prepare_api_payload(API, ?SHARED_CALL_ID_VALUES))), + kz_amqp_util:kapps_publish(fsm_shared_routing_key(API1), Payload, ContentType). diff --git a/applications/acdc/src/kapi_acdc_queue.erl b/applications/acdc/src/kapi_acdc_queue.erl index 24287d1c5d2..ade79494d3f 100644 --- a/applications/acdc/src/kapi_acdc_queue.erl +++ b/applications/acdc/src/kapi_acdc_queue.erl @@ -2,9 +2,8 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -25,12 +24,16 @@ ,agent_timeout/1, agent_timeout_v/1 ,member_connect_retry/1, member_connect_retry_v/1 ,member_connect_accepted/1, member_connect_accepted_v/1 + ,member_callback_accepted/1, member_callback_accepted_v/1 ,member_hungup/1, member_hungup_v/1 ,sync_req/1, sync_req_v/1 ,sync_resp/1, sync_resp_v/1 ,agent_change/1, agent_change_v/1 + ,agents_available_req/1, agents_available_req_v/1 + ,agents_available_resp/1, agents_available_resp_v/1 ,queue_member_add/1, queue_member_add_v/1 ,queue_member_remove/1, queue_member_remove_v/1 + ,member_callback_reg/1, member_callback_reg_v/1 ]). -export([agent_change_available/0 @@ -51,17 +54,21 @@ ,publish_member_call_cancel/1, publish_member_call_cancel/2 ,publish_member_connect_req/1, publish_member_connect_req/2 ,publish_member_connect_resp/2, publish_member_connect_resp/3 - ,publish_member_connect_win/2, publish_member_connect_win/3 - ,publish_member_connect_satisfied/2, publish_member_connect_satisfied/3 + ,publish_member_connect_win/1, publish_member_connect_win/2 + ,publish_member_connect_satisfied/1, publish_member_connect_satisfied/2 ,publish_agent_timeout/2, publish_agent_timeout/3 ,publish_member_connect_retry/2, publish_member_connect_retry/3 ,publish_member_connect_accepted/2, publish_member_connect_accepted/3 + ,publish_member_callback_accepted/2, publish_member_callback_accepted/3 ,publish_member_hungup/2, publish_member_hungup/3 ,publish_sync_req/1, publish_sync_req/2 ,publish_sync_resp/2, publish_sync_resp/3 ,publish_agent_change/1, publish_agent_change/2 + ,publish_agents_available_req/1, publish_agents_available_req/2 + ,publish_agents_available_resp/2, publish_agents_available_resp/3 ,publish_queue_member_add/1, publish_queue_member_add/2 ,publish_queue_member_remove/1, publish_queue_member_remove/2 + ,publish_member_callback_reg/1, publish_member_callback_reg/2 ]). -export([queue_size/2, shared_queue_name/2]). @@ -72,12 +79,14 @@ %% Member Call %%------------------------------------------------------------------------------ -define(MEMBER_CALL_HEADERS, [<<"Account-ID">>, <<"Queue-ID">>, <<"Call">>]). --define(OPTIONAL_MEMBER_CALL_HEADERS, [<<"Member-Priority">>]). +-define(OPTIONAL_MEMBER_CALL_HEADERS, [<<"Callback-Number">>, <<"Enter-As-Callback">>, <<"Member-Priority">>]). -define(MEMBER_CALL_VALUES, [{<<"Event-Category">>, <<"member">>} ,{<<"Event-Name">>, <<"call">>} ]). -define(MEMBER_CALL_TYPES, [{<<"Queue-ID">>, fun erlang:is_binary/1} ,{<<"Member-Priority">>, fun is_integer/1} + ,{<<"Callback-Number">>, fun is_binary/1} + ,{<<"Enter-As-Callback">>, fun is_boolean/1} ]). -spec member_call(kz_term:api_terms()) -> @@ -100,16 +109,16 @@ member_call_v(JObj) -> -spec member_call_routing_key(kz_term:api_terms()) -> kz_term:ne_binary(). member_call_routing_key(Props) when is_list(Props) -> Id = props:get_value(<<"Queue-ID">>, Props, <<"*">>), - AcctId = props:get_value(<<"Account-ID">>, Props), - member_call_routing_key(AcctId, Id); + AccountId = props:get_value(<<"Account-ID">>, Props), + member_call_routing_key(AccountId, Id); member_call_routing_key(JObj) -> Id = kz_json:get_value(<<"Queue-ID">>, JObj, <<"*">>), - AcctId = kz_json:get_value(<<"Account-ID">>, JObj), - member_call_routing_key(AcctId, Id). + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + member_call_routing_key(AccountId, Id). -spec member_call_routing_key(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -member_call_routing_key(AcctId, QueueId) -> - <<"acdc.member.call.", AcctId/binary, ".", QueueId/binary>>. +member_call_routing_key(AccountId, QueueId) -> + <<"acdc.member.call.", AccountId/binary, ".", QueueId/binary>>. %%------------------------------------------------------------------------------ %% Member Call Fail - if the queue is unable to properly handle the call @@ -196,19 +205,19 @@ member_call_cancel_v(JObj) -> -spec member_call_result_routing_key(kz_term:api_terms()) -> kz_term:ne_binary(). member_call_result_routing_key(Props) when is_list(Props) -> - AcctId = props:get_value(<<"Account-ID">>, Props), + AccountId = props:get_value(<<"Account-ID">>, Props), QueueId = props:get_value(<<"Queue-ID">>, Props, <<"*">>), CallId = props:get_value(<<"Call-ID">>, Props, <<"#">>), - member_call_result_routing_key(AcctId, QueueId, CallId); + member_call_result_routing_key(AccountId, QueueId, CallId); member_call_result_routing_key(JObj) -> - AcctId = kz_json:get_value(<<"Account-ID">>, JObj), + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), QueueId = kz_json:get_value(<<"Queue-ID">>, JObj, <<"*">>), CallId = kz_json:get_value(<<"Call-ID">>, JObj, <<"#">>), - member_call_result_routing_key(AcctId, QueueId, CallId). + member_call_result_routing_key(AccountId, QueueId, CallId). -spec member_call_result_routing_key(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -member_call_result_routing_key(AcctId, QueueId, CallId) -> - <<"acdc.member.call_result.", AcctId/binary, ".", QueueId/binary, ".", CallId/binary>>. +member_call_result_routing_key(AccountId, QueueId, CallId) -> + <<"acdc.member.call_result.", AccountId/binary, ".", QueueId/binary, ".", CallId/binary>>. %%------------------------------------------------------------------------------ %% Member Connect Request @@ -240,16 +249,16 @@ member_connect_req_v(JObj) -> -spec member_connect_req_routing_key(kz_term:api_terms()) -> kz_term:ne_binary(). member_connect_req_routing_key(Props) when is_list(Props) -> Id = props:get_value(<<"Queue-ID">>, Props, <<"*">>), - AcctId = props:get_value(<<"Account-ID">>, Props), - member_connect_req_routing_key(AcctId, Id); + AccountId = props:get_value(<<"Account-ID">>, Props), + member_connect_req_routing_key(AccountId, Id); member_connect_req_routing_key(JObj) -> Id = kz_json:get_value(<<"Queue-ID">>, JObj, <<"*">>), - AcctId = kz_json:get_value(<<"Account-ID">>, JObj), - member_connect_req_routing_key(AcctId, Id). + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + member_connect_req_routing_key(AccountId, Id). -spec member_connect_req_routing_key(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -member_connect_req_routing_key(AcctId, QID) -> - <<"acdc.member.connect_req.", AcctId/binary, ".", QID/binary>>. +member_connect_req_routing_key(AccountId, QID) -> + <<"acdc.member.connect_req.", AccountId/binary, ".", QID/binary>>. %%------------------------------------------------------------------------------ %% Member Connect Response @@ -286,7 +295,7 @@ member_connect_resp_v(JObj) -> ,<<"Wrapup-Timeout">>, <<"CDR-Url">> ,<<"Process-ID">>, <<"Agent-Process-IDs">> ,<<"Record-Caller">>, <<"Recording-URL">> - ,<<"Notifications">> + ,<<"Notifications">>, <<"Callback-Details">> ]). -define(MEMBER_CONNECT_WIN_VALUES, [{<<"Event-Category">>, <<"member">>} ,{<<"Event-Name">>, <<"connect_win">>} @@ -310,11 +319,20 @@ member_connect_win_v(Prop) when is_list(Prop) -> member_connect_win_v(JObj) -> member_connect_win_v(kz_json:to_proplist(JObj)). +-spec member_connect_win_routing_key(kz_term:api_terms() | kz_term:ne_binary()) -> kz_term:ne_binary(). +member_connect_win_routing_key(Props) when is_list(Props) -> + AgentId = props:get_value(<<"Agent-ID">>, Props), + member_connect_win_routing_key(AgentId); +member_connect_win_routing_key(AgentId) when is_binary(AgentId) -> + <<"acdc.member.connect_win.", AgentId/binary>>; +member_connect_win_routing_key(JObj) -> + AgentId = kz_json:get_value(<<"Agent-ID">>, JObj), + member_connect_win_routing_key(AgentId). %%------------------------------------------------------------------------------ %% Member Connect Satisfied %%------------------------------------------------------------------------------ --define(MEMBER_CONNECT_SATISFIED_HEADERS, [<<"Queue-ID">>, <<"Call">>]). +-define(MEMBER_CONNECT_SATISFIED_HEADERS, [<<"Queue-ID">>, <<"Call">>, <<"Agent-ID">>, <<"Accept-Agent-ID">>]). -define(OPTIONAL_MEMBER_CONNECT_SATISFIED_HEADERS, [<<"Process-ID">>, <<"Agent-Process-IDs">>]). -define(MEMBER_CONNECT_SATISFIED_VALUES, [{<<"Event-Category">>, <<"member">>} ,{<<"Event-Name">>, <<"connect_satisfied">>} @@ -338,11 +356,21 @@ member_connect_satisfied_v(Prop) when is_list(Prop) -> member_connect_satisfied_v(JObj) -> member_connect_satisfied_v(kz_json:to_proplist(JObj)). +-spec member_connect_satisfied_routing_key(kz_term:api_terms() | kz_term:ne_binary()) -> kz_term:ne_binary(). +member_connect_satisfied_routing_key(Props) when is_list(Props) -> + AgentId = props:get_value(<<"Agent-ID">>, Props), + member_connect_satisfied_routing_key(AgentId); +member_connect_satisfied_routing_key(AgentId) when is_binary(AgentId) -> + <<"acdc.member.connect_satisfied.", AgentId/binary>>; +member_connect_satisfied_routing_key(JObj) -> + AgentId = kz_json:get_value(<<"Agent-ID">>, JObj), + member_connect_satisfied_routing_key(AgentId). + %%------------------------------------------------------------------------------ %% Agent Timeout %%------------------------------------------------------------------------------ -define(AGENT_TIMEOUT_HEADERS, [<<"Queue-ID">>, <<"Call-ID">>]). --define(OPTIONAL_AGENT_TIMEOUT_HEADERS, [<<"Agent-Process-IDs">>]). +-define(OPTIONAL_AGENT_TIMEOUT_HEADERS, [<<"Agent-Process-ID">>]). -define(AGENT_TIMEOUT_VALUES, [{<<"Event-Category">>, <<"agent">>} ,{<<"Event-Name">>, <<"connect_timeout">>} ]). @@ -368,7 +396,7 @@ agent_timeout_v(JObj) -> %% Member Connect Accepted %%------------------------------------------------------------------------------ -define(MEMBER_CONNECT_ACCEPTED_HEADERS, [<<"Call-ID">>]). --define(OPTIONAL_MEMBER_CONNECT_ACCEPTED_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>, <<"Process-ID">>]). +-define(OPTIONAL_MEMBER_CONNECT_ACCEPTED_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>, <<"Process-ID">>, <<"Old-Call-ID">>]). -define(MEMBER_CONNECT_ACCEPTED_VALUES, [{<<"Event-Category">>, <<"member">>} ,{<<"Event-Name">>, <<"connect_accepted">>} ]). @@ -391,6 +419,33 @@ member_connect_accepted_v(Prop) when is_list(Prop) -> member_connect_accepted_v(JObj) -> member_connect_accepted_v(kz_json:to_proplist(JObj)). +%%------------------------------------------------------------------------------ +%% Member Call Back Accepted +%%------------------------------------------------------------------------------ +-define(MEMBER_CALLBACK_ACCEPTED_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>, <<"Process-ID">>, <<"Call-ID">>]). +-define(OPTIONAL_MEMBER_CALLBACK_ACCEPTED_HEADERS, []). +-define(MEMBER_CALLBACK_ACCEPTED_VALUES, [{<<"Event-Category">>, <<"member">>} + ,{<<"Event-Name">>, <<"callback_accepted">>} + ]). +-define(MEMBER_CALLBACK_ACCEPTED_TYPES, []). + +-spec member_callback_accepted(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +member_callback_accepted(Props) when is_list(Props) -> + case member_callback_accepted_v(Props) of + 'true' -> kz_api:build_message(Props, ?MEMBER_CALLBACK_ACCEPTED_HEADERS, ?OPTIONAL_MEMBER_CALLBACK_ACCEPTED_HEADERS); + 'false' -> {'error', "Proplist failed validation for member_callback_accepted"} + end; +member_callback_accepted(JObj) -> + member_callback_accepted(kz_json:to_proplist(JObj)). + +-spec member_callback_accepted_v(kz_term:api_terms()) -> boolean(). +member_callback_accepted_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?MEMBER_CALLBACK_ACCEPTED_HEADERS, ?MEMBER_CALLBACK_ACCEPTED_VALUES, ?MEMBER_CALLBACK_ACCEPTED_TYPES); +member_callback_accepted_v(JObj) -> + member_callback_accepted_v(kz_json:to_proplist(JObj)). + %%------------------------------------------------------------------------------ %% Member Connect Retry %% Sent by the agent process that dialed its agent endpoints when the agent @@ -454,20 +509,19 @@ member_hungup_v(JObj) -> %% Sync Req/Resp %% Depending on the queue strategy, get the other queue's strategy state %%------------------------------------------------------------------------------ - -spec sync_req_routing_key(kz_term:api_terms()) -> kz_term:ne_binary(). sync_req_routing_key(Props) when is_list(Props) -> Id = props:get_value(<<"Queue-ID">>, Props, <<"*">>), - AcctId = props:get_value(<<"Account-ID">>, Props), - sync_req_routing_key(AcctId, Id); + AccountId = props:get_value(<<"Account-ID">>, Props), + sync_req_routing_key(AccountId, Id); sync_req_routing_key(JObj) -> Id = kz_json:get_value(<<"Queue-ID">>, JObj, <<"*">>), - AcctId = kz_json:get_value(<<"Account-ID">>, JObj), - sync_req_routing_key(AcctId, Id). + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + sync_req_routing_key(AccountId, Id). -spec sync_req_routing_key(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -sync_req_routing_key(AcctId, QID) -> - <<"acdc.queue.sync_req.", AcctId/binary, ".", QID/binary>>. +sync_req_routing_key(AccountId, QID) -> + <<"acdc.queue.sync_req.", AccountId/binary, ".", QID/binary>>. -define(SYNC_REQ_HEADERS, [<<"Account-ID">>, <<"Queue-ID">>]). -define(OPTIONAL_SYNC_REQ_HEADERS, [<<"Process-ID">>]). @@ -522,7 +576,9 @@ sync_resp_v(JObj) -> %%------------------------------------------------------------------------------ %% Agent Change %% available: when an agent logs in, tell its configured queues -%% ringing: when an agent is being run, forward queues' round robin +%% ringing: remove agent from acdc_queue_manager temporarily +%% busy: remove agent from acdc_queue_manager temporarily, mark as busy +%% unavailable: fully remove agent from acdc_queue_manager %%------------------------------------------------------------------------------ agent_change_publish_key(Prop) when is_list(Prop) -> agent_change_routing_key(props:get_value(<<"Account-ID">>, Prop) @@ -533,8 +589,8 @@ agent_change_publish_key(JObj) -> ,kz_json:get_value(<<"Queue-ID">>, JObj) ). -agent_change_routing_key(AcctId, QueueId) -> - <<"acdc.queue.agent_change.", AcctId/binary, ".", QueueId/binary>>. +agent_change_routing_key(AccountId, QueueId) -> + <<"acdc.queue.agent_change.", AccountId/binary, ".", QueueId/binary>>. -define(AGENT_CHANGE_AVAILABLE, <<"available">>). -define(AGENT_CHANGE_RINGING, <<"ringing">>). @@ -559,7 +615,9 @@ agent_change_busy() -> ?AGENT_CHANGE_BUSY. agent_change_unavailable() -> ?AGENT_CHANGE_UNAVAILABLE. -define(AGENT_CHANGE_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>, <<"Queue-ID">>, <<"Change">>]). --define(OPTIONAL_AGENT_CHANGE_HEADERS, [<<"Process-ID">>]). +-define(OPTIONAL_AGENT_CHANGE_HEADERS, [<<"Priority">>, <<"Process-ID">>, <<"Skills">>, <<"Call-Direction">>, + <<"Caller-ID-Number">>, <<"Caller-ID-Name">>, + <<"Callee-ID-Number">>, <<"Callee-ID-Name">>]). -define(AGENT_CHANGE_VALUES, [{<<"Event-Category">>, <<"queue">>} ,{<<"Event-Name">>, <<"agent_change">>} ,{<<"Change">>, ?AGENT_CHANGES} @@ -582,31 +640,92 @@ agent_change_v(Prop) when is_list(Prop) -> agent_change_v(JObj) -> agent_change_v(kz_json:to_proplist(JObj)). %%------------------------------------------------------------------------------ +%% Querying for availability of agents to take queue calls +%%------------------------------------------------------------------------------ +-spec agents_availability_routing_key(kz_term:api_terms()) -> kz_term:ne_binary(). +agents_availability_routing_key(Props) when is_list(Props) -> + AccountId = props:get_value(<<"Account-ID">>, Props), + QueueId = props:get_value(<<"Queue-ID">>, Props, <<"*">>), + agents_availability_routing_key(AccountId, QueueId); +agents_availability_routing_key(JObj) -> + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + QueueId = kz_json:get_value(<<"Queue-ID">>, JObj, <<"*">>), + agents_availability_routing_key(AccountId, QueueId). + +-spec agents_availability_routing_key(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). +agents_availability_routing_key(AccountId, QueueId) -> + <<"acdc.queue.agents_availability.", AccountId/binary, ".", QueueId/binary>>. + +-define(AGENTS_AVAILABLE_REQ_HEADERS, [<<"Account-ID">>, <<"Queue-ID">>]). +-define(OPTIONAL_AGENTS_AVAILABLE_REQ_HEADERS, [<<"Skills">>]). +-define(AGENTS_AVAILABLE_REQ_VALUES, [{<<"Event-Category">>, <<"queue">>} + ,{<<"Event-Name">>, <<"agents_available_req">>} + ]). +-define(AGENTS_AVAILABLE_REQ_TYPES, [{<<"Skills">>, fun kz_term:is_ne_binaries/1}]). + +-spec agents_available_req(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agents_available_req(Prop) when is_list(Prop) -> + case agents_available_req_v(Prop) of + 'true' -> kz_api:build_message(Prop, ?AGENTS_AVAILABLE_REQ_HEADERS, ?OPTIONAL_AGENTS_AVAILABLE_REQ_HEADERS); + 'false' -> {'error', "proplist failed validation for agents_available_req"} + end; +agents_available_req(JObj) -> agents_available_req(kz_json:to_proplist(JObj)). + +-spec agents_available_req_v(kz_term:api_terms()) -> boolean(). +agents_available_req_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENTS_AVAILABLE_REQ_HEADERS, ?AGENTS_AVAILABLE_REQ_VALUES, ?AGENTS_AVAILABLE_REQ_TYPES); +agents_available_req_v(JObj) -> agents_available_req_v(kz_json:to_proplist(JObj)). + +-define(AGENTS_AVAILABLE_RESP_HEADERS, [<<"Account-ID">>, <<"Queue-ID">>, <<"Agent-Count">>]). +-define(OPTIONAL_AGENTS_AVAILABLE_RESP_HEADERS, []). +-define(AGENTS_AVAILABLE_RESP_VALUES, [{<<"Event-Category">>, <<"queue">>} + ,{<<"Event-Name">>, <<"agents_available_resp">>} + ]). +-define(AGENTS_AVAILABLE_RESP_TYPES, []). + +-spec agents_available_resp(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agents_available_resp(Prop) when is_list(Prop) -> + case agents_available_resp_v(Prop) of + 'true' -> kz_api:build_message(Prop, ?AGENTS_AVAILABLE_RESP_HEADERS, ?OPTIONAL_AGENTS_AVAILABLE_RESP_HEADERS); + 'false' -> {'error', "proplist failed validation for agents_available_resp"} + end; +agents_available_resp(JObj) -> agents_available_resp(kz_json:to_proplist(JObj)). + +-spec agents_available_resp_v(kz_term:api_terms()) -> boolean(). +agents_available_resp_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENTS_AVAILABLE_RESP_HEADERS, ?AGENTS_AVAILABLE_RESP_VALUES, ?AGENTS_AVAILABLE_RESP_TYPES); +agents_available_resp_v(JObj) -> agents_available_resp_v(kz_json:to_proplist(JObj)). %%------------------------------------------------------------------------------ %% Queue Position tracking %%------------------------------------------------------------------------------ - -spec queue_member_routing_key(kz_term:api_terms()) -> kz_term:ne_binary(). queue_member_routing_key(Props) when is_list(Props) -> Id = props:get_value(<<"Queue-ID">>, Props, <<"*">>), - AcctId = props:get_value(<<"Account-ID">>, Props), - queue_member_routing_key(AcctId, Id); + AccountId = props:get_value(<<"Account-ID">>, Props), + queue_member_routing_key(AccountId, Id); queue_member_routing_key(JObj) -> Id = kz_json:get_value(<<"Queue-ID">>, JObj, <<"*">>), - AcctId = kz_json:get_value(<<"Account-ID">>, JObj), - queue_member_routing_key(AcctId, Id). + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + queue_member_routing_key(AccountId, Id). -spec queue_member_routing_key(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -queue_member_routing_key(AcctId, QID) -> - <<"acdc.queue.position.", AcctId/binary, ".", QID/binary>>. +queue_member_routing_key(AccountId, QID) -> + <<"acdc.queue.position.", AccountId/binary, ".", QID/binary>>. --define(QUEUE_MEMBER_ADD_HEADERS, [<<"Account-ID">>, <<"Queue-ID">>, <<"Call">>]). --define(OPTIONAL_QUEUE_MEMBER_ADD_HEADERS, []). +-define(QUEUE_MEMBER_ADD_HEADERS, [<<"Account-ID">>, <<"Queue-ID">>, <<"Call">>, <<"Enter-As-Callback">>]). +-define(OPTIONAL_QUEUE_MEMBER_ADD_HEADERS, [<<"Callback-Number">>, <<"Member-Priority">>]). -define(QUEUE_MEMBER_ADD_VALUES, [{<<"Event-Category">>, <<"queue">>} ,{<<"Event-Name">>, <<"member_add">>} ]). --define(QUEUE_MEMBER_ADD_TYPES, []). +-define(QUEUE_MEMBER_ADD_TYPES, [{<<"Callback-Number">>, fun is_binary/1} + ,{<<"Enter-As-Callback">>, fun is_boolean/1} + ,{<<"Member-Priority">>, fun is_integer/1} + ]). -spec queue_member_add(kz_term:api_terms()) -> {'ok', iolist()} | @@ -644,17 +763,69 @@ queue_member_remove(JObj) -> queue_member_remove(kz_json:to_proplist(JObj)). queue_member_remove_v(Prop) when is_list(Prop) -> kz_api:validate(Prop, ?QUEUE_MEMBER_REMOVE_HEADERS, ?QUEUE_MEMBER_REMOVE_VALUES, ?QUEUE_MEMBER_REMOVE_TYPES); queue_member_remove_v(JObj) -> queue_member_remove_v(kz_json:to_proplist(JObj)). + +%%------------------------------------------------------------------------------ +%% Member Call Back - let the caller leave the queue but be called back +%% when their turn comes up +%%------------------------------------------------------------------------------ +-spec member_callback_reg_routing_key(kz_term:api_terms()) -> kz_term:ne_binary(). +member_callback_reg_routing_key(Props) when is_list(Props) -> + AccountId = props:get_value(<<"Account-ID">>, Props), + QueueId = props:get_value(<<"Queue-ID">>, Props, <<"*">>), + CallId = props:get_value(<<"Call-ID">>, Props, <<"#">>), + member_callback_reg_routing_key(AccountId, QueueId, CallId); +member_callback_reg_routing_key(JObj) -> + AccountId = kz_json:get_value(<<"Account-ID">>, JObj), + QueueId = kz_json:get_value(<<"Queue-ID">>, JObj, <<"*">>), + CallId = kz_json:get_value(<<"Call-ID">>, JObj, <<"#">>), + member_callback_reg_routing_key(AccountId, QueueId, CallId). + +-spec member_callback_reg_routing_key(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). +member_callback_reg_routing_key(AccountId, QueueId, CallId) -> + <<"acdc.member.callback_reg.", AccountId/binary, ".", QueueId/binary, ".", CallId/binary>>. + +-define(MEMBER_CALLBACK_HEADERS, [<<"Call-ID">>, <<"Account-ID">>, <<"Queue-ID">>, <<"Number">>]). +-define(OPTIONAL_MEMBER_CALLBACK_HEADERS, []). +-define(MEMBER_CALLBACK_VALUES, [{<<"Event-Category">>, <<"member">>} + ,{<<"Event-Name">>, <<"callback_reg">>} + ]). +-define(MEMBER_CALLBACK_TYPES, []). + +-spec member_callback_reg(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +member_callback_reg(Props) when is_list(Props) -> + case member_callback_reg_v(Props) of + 'true' -> kz_api:build_message(Props, ?MEMBER_CALLBACK_HEADERS, ?OPTIONAL_MEMBER_CALLBACK_HEADERS); + 'false' -> {'error', "Proplist failed validation for member_callback_reg"} + end; +member_callback_reg(JObj) -> + member_callback_reg(kz_json:to_proplist(JObj)). + +-spec member_callback_reg_v(kz_term:api_terms()) -> boolean(). +member_callback_reg_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?MEMBER_CALLBACK_HEADERS, ?MEMBER_CALLBACK_VALUES, ?MEMBER_CALLBACK_TYPES); +member_callback_reg_v(JObj) -> + member_callback_reg_v(kz_json:to_proplist(JObj)). + +%%------------------------------------------------------------------------------ %% Bind/Unbind the queue as appropriate %%------------------------------------------------------------------------------ -spec shared_queue_name(kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -shared_queue_name(AcctId, QueueId) -> - <<"acdc.queue.", AcctId/binary, ".", QueueId/binary>>. +shared_queue_name(AccountId, QueueId) -> + <<"acdc.queue.", AccountId/binary, ".", QueueId/binary>>. -spec queue_size(kz_term:ne_binary(), kz_term:ne_binary()) -> integer() | 'undefined'. -queue_size(AcctId, QueueId) -> - Q = shared_queue_name(AcctId, QueueId), +queue_size(AccountId, QueueId) -> + Q = shared_queue_name(AccountId, QueueId), + Priority = acdc_util:max_priority(kzs_util:format_account_db(AccountId), QueueId), try kz_amqp_util:new_queue(Q, [{'return_field', 'all'} - ,{'passive', 'true'} + ,{'exclusive', 'false'} + ,{'arguments', [{<<"x-message-ttl">>, ?MILLISECONDS_IN_DAY} + ,{<<"x-max-length">>, 1000} + ,{<<"x-max-priority">>, Priority} + ] + } ]) of {'error', {'server_initiated_close', 404, _Msg}} -> @@ -678,77 +849,93 @@ queue_size(AcctId, QueueId) -> -spec bind_q(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. bind_q(Q, Props) -> QID = props:get_value('queue_id', Props, <<"*">>), - AcctId = props:get_value('account_id', Props), + AccountId = props:get_value('account_id', Props), CallId = props:get_value('callid', Props, <<"#">>), - bind_q(Q, AcctId, QID, CallId, props:get_value('restrict_to', Props)). - -bind_q(Q, AcctId, QID, CallId, 'undefined') -> - kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AcctId, QID)), - kz_amqp_util:bind_q_to_kapps(Q, agent_change_routing_key(AcctId, QID)), - kz_amqp_util:bind_q_to_callmgr(Q, member_call_routing_key(AcctId, QID)), - kz_amqp_util:bind_q_to_callmgr(Q, member_call_result_routing_key(AcctId, QID, CallId)), - kz_amqp_util:bind_q_to_callmgr(Q, member_connect_req_routing_key(AcctId, QID)), - kz_amqp_util:bind_q_to_kapps(Q, queue_member_routing_key(AcctId, QID)); -bind_q(Q, AcctId, QID, CallId, ['member_call'|T]) -> - kz_amqp_util:bind_q_to_callmgr(Q, member_call_routing_key(AcctId, QID)), - bind_q(Q, AcctId, QID, CallId, T); -bind_q(Q, AcctId, QID, CallId, ['member_call_result'|T]) -> - kz_amqp_util:bind_q_to_callmgr(Q, member_call_result_routing_key(AcctId, QID, CallId)), - bind_q(Q, AcctId, QID, CallId, T); -bind_q(Q, AcctId, QID, CallId, ['member_connect_req'|T]) -> - kz_amqp_util:bind_q_to_callmgr(Q, member_connect_req_routing_key(AcctId, QID)), - bind_q(Q, AcctId, QID, CallId, T); -bind_q(Q, AcctId, QID, CallId, ['sync_req'|T]) -> - kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AcctId, QID)), - bind_q(Q, AcctId, QID, CallId, T); -bind_q(Q, AcctId, QID, CallId, ['agent_change'|T]) -> - kz_amqp_util:bind_q_to_kapps(Q, agent_change_routing_key(AcctId, QID)), - bind_q(Q, AcctId, QID, CallId, T); -bind_q(Q, AcctId, QID, CallId, ['member_addremove'|T]) -> - kz_amqp_util:bind_q_to_kapps(Q, queue_member_routing_key(AcctId, QID)), - bind_q(Q, AcctId, QID, CallId, T); -bind_q(Q, AcctId, QID, CallId, [_|T]) -> bind_q(Q, AcctId, QID, CallId, T); + bind_q(Q, AccountId, QID, CallId, props:get_value('restrict_to', Props)). + +bind_q(Q, AccountId, QID, CallId, 'undefined') -> + kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AccountId, QID)), + kz_amqp_util:bind_q_to_kapps(Q, agent_change_routing_key(AccountId, QID)), + kz_amqp_util:bind_q_to_kapps(Q, agents_availability_routing_key(AccountId, QID)), + kz_amqp_util:bind_q_to_callmgr(Q, member_call_routing_key(AccountId, QID)), + kz_amqp_util:bind_q_to_callmgr(Q, member_call_result_routing_key(AccountId, QID, CallId)), + kz_amqp_util:bind_q_to_callmgr(Q, member_connect_req_routing_key(AccountId, QID)), + kz_amqp_util:bind_q_to_callmgr(Q, member_callback_reg_routing_key(AccountId, QID, CallId)), + kz_amqp_util:bind_q_to_kapps(Q, queue_member_routing_key(AccountId, QID)); +bind_q(Q, AccountId, QID, CallId, ['member_call'|T]) -> + kz_amqp_util:bind_q_to_callmgr(Q, member_call_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, ['member_call_result'|T]) -> + kz_amqp_util:bind_q_to_callmgr(Q, member_call_result_routing_key(AccountId, QID, CallId)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, ['member_connect_req'|T]) -> + kz_amqp_util:bind_q_to_callmgr(Q, member_connect_req_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, ['sync_req'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, sync_req_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, ['agent_change'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, agent_change_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, ['agents_availability'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, agents_availability_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, ['member_addremove'|T]) -> + kz_amqp_util:bind_q_to_kapps(Q, queue_member_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, ['member_callback_reg'|T]) -> + kz_amqp_util:bind_q_to_callmgr(Q, member_callback_reg_routing_key(AccountId, QID, CallId)), + bind_q(Q, AccountId, QID, CallId, T); +bind_q(Q, AccountId, QID, CallId, [_|T]) -> bind_q(Q, AccountId, QID, CallId, T); bind_q(_, _, _, _, []) -> 'ok'. -spec unbind_q(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. unbind_q(Q, Props) -> QID = props:get_value('queue_id', Props, <<"*">>), - AcctId = props:get_value('account_id', Props), + AccountId = props:get_value('account_id', Props), CallId = props:get_value('callid', Props, <<"#">>), - unbind_q(Q, AcctId, QID, CallId, props:get_value('restrict_to', Props)). - -unbind_q(Q, AcctId, QID, CallId, 'undefined') -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AcctId, QID)), - _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_change_routing_key(AcctId, QID)), - _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_routing_key(AcctId, QID)), - _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_result_routing_key(AcctId, QID, CallId)), - _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_connect_req_routing_key(AcctId, QID)), - _ = kz_amqp_util:unbind_q_from_kapps(Q, queue_member_routing_key(AcctId, QID)); -unbind_q(Q, AcctId, QID, CallId, ['member_call'|T]) -> - _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_routing_key(AcctId, QID)), - unbind_q(Q, AcctId, QID, CallId, T); -unbind_q(Q, AcctId, QID, CallId, ['member_call_result'|T]) -> - _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_result_routing_key(AcctId, QID, CallId)), - unbind_q(Q, AcctId, QID, CallId, T); -unbind_q(Q, AcctId, QID, CallId, ['member_connect_req'|T]) -> - _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_connect_req_routing_key(AcctId, QID)), - unbind_q(Q, AcctId, QID, CallId, T); -unbind_q(Q, AcctId, QID, CallId, ['sync_req'|T]) -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AcctId, QID)), - unbind_q(Q, AcctId, QID, CallId, T); -unbind_q(Q, AcctId, QID, CallId, ['agent_change'|T]) -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_change_routing_key(AcctId, QID)), - unbind_q(Q, AcctId, QID, CallId, T); -unbind_q(Q, AcctId, QID, CallId, ['member_addremove'|T]) -> - _ = kz_amqp_util:unbind_q_from_kapps(Q, queue_member_routing_key(AcctId, QID)), - unbind_q(Q, AcctId, QID, CallId, T); -unbind_q(Q, AcctId, QID, CallId, [_|T]) -> - unbind_q(Q, AcctId, QID, CallId, T); + unbind_q(Q, AccountId, QID, CallId, props:get_value('restrict_to', Props)). + +unbind_q(Q, AccountId, QID, CallId, 'undefined') -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AccountId, QID)), + _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_change_routing_key(AccountId, QID)), + _ = kz_amqp_util:unbind_q_from_kapps(Q, agents_availability_routing_key(AccountId, QID)), + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_routing_key(AccountId, QID)), + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_result_routing_key(AccountId, QID, CallId)), + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_connect_req_routing_key(AccountId, QID)), + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_callback_reg_routing_key(AccountId, QID, CallId)), + _ = kz_amqp_util:unbind_q_from_kapps(Q, queue_member_routing_key(AccountId, QID)); +unbind_q(Q, AccountId, QID, CallId, ['member_call'|T]) -> + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, ['member_call_result'|T]) -> + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_call_result_routing_key(AccountId, QID, CallId)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, ['member_connect_req'|T]) -> + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_connect_req_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, ['sync_req'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, sync_req_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, ['agent_change'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, agent_change_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, ['agents_availability'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, agents_availability_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, ['member_addremove'|T]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, queue_member_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, ['member_callback_reg'|T]) -> + _ = kz_amqp_util:unbind_q_from_callmgr(Q, member_callback_reg_routing_key(AccountId, QID, CallId)), + unbind_q(Q, AccountId, QID, CallId, T); +unbind_q(Q, AccountId, QID, CallId, [_|T]) -> + unbind_q(Q, AccountId, QID, CallId, T); unbind_q(_, _, _, _, []) -> 'ok'. %%------------------------------------------------------------------------------ -%% @doc Declare the exchanges used by this API +%% @doc declare the exchanges used by this API %% @end %%------------------------------------------------------------------------------ -spec declare_exchanges() -> 'ok'. @@ -759,7 +946,6 @@ declare_exchanges() -> %%------------------------------------------------------------------------------ %% Publishers for convenience %%------------------------------------------------------------------------------ - -spec publish_member_call(kz_term:api_terms()) -> 'ok'. publish_member_call(JObj) -> publish_member_call(JObj, ?DEFAULT_CONTENT_TYPE). @@ -790,19 +976,19 @@ publish_shared_member_call(JObj) -> ). -spec publish_shared_member_call(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_shared_member_call(AcctId, QueueId, JObj) -> - publish_shared_member_call(AcctId, QueueId, JObj, ?DEFAULT_CONTENT_TYPE). +publish_shared_member_call(AccountId, QueueId, JObj) -> + publish_shared_member_call(AccountId, QueueId, JObj, ?DEFAULT_CONTENT_TYPE). -spec publish_shared_member_call(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. -publish_shared_member_call(AcctId, QueueId, Props, ContentType) when is_list(Props) -> - publish_shared_member_call(AcctId, QueueId, kz_json:from_list(Props), ContentType); -publish_shared_member_call(AcctId, QueueId, JObj, ContentType) -> +publish_shared_member_call(AccountId, QueueId, Props, ContentType) when is_list(Props) -> + publish_shared_member_call(AccountId, QueueId, kz_json:from_list(Props), ContentType); +publish_shared_member_call(AccountId, QueueId, JObj, ContentType) -> Priority = kz_json:get_integer_value(<<"Member-Priority">>, JObj), Props = props:filter_undefined([{'priority', Priority} ,{'mandatory', 'true'} ]), {'ok', Payload} = kz_api:prepare_api_payload(JObj, ?MEMBER_CALL_VALUES, fun member_call/1), - kz_amqp_util:targeted_publish(shared_queue_name(AcctId, QueueId), Payload, ContentType, Props). + kz_amqp_util:targeted_publish(shared_queue_name(AccountId, QueueId), Payload, ContentType, Props). -spec publish_member_call_failure(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. publish_member_call_failure(Q, JObj) -> @@ -842,23 +1028,23 @@ publish_member_connect_resp(Q, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?MEMBER_CONNECT_RESP_VALUES, fun member_connect_resp/1), kz_amqp_util:targeted_publish(Q, Payload, ContentType). --spec publish_member_connect_win(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_member_connect_win(Q, JObj) -> - publish_member_connect_win(Q, JObj, ?DEFAULT_CONTENT_TYPE). +-spec publish_member_connect_win(kz_term:api_terms()) -> 'ok'. +publish_member_connect_win(JObj) -> + publish_member_connect_win(JObj, ?DEFAULT_CONTENT_TYPE). --spec publish_member_connect_win(kz_term:ne_binary(), kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. -publish_member_connect_win(Q, API, ContentType) -> +-spec publish_member_connect_win(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_member_connect_win(API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?MEMBER_CONNECT_WIN_VALUES, fun member_connect_win/1), - kz_amqp_util:targeted_publish(Q, Payload, ContentType). + kz_amqp_util:callmgr_publish(Payload, ContentType, member_connect_win_routing_key(API)). --spec publish_member_connect_satisfied(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_member_connect_satisfied(Q, JObj) -> - publish_member_connect_satisfied(Q, JObj, ?DEFAULT_CONTENT_TYPE). +-spec publish_member_connect_satisfied(kz_term:api_terms()) -> 'ok'. +publish_member_connect_satisfied(JObj) -> + publish_member_connect_satisfied(JObj, ?DEFAULT_CONTENT_TYPE). --spec publish_member_connect_satisfied(kz_term:ne_binary(), kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. -publish_member_connect_satisfied(Q, API, ContentType) -> +-spec publish_member_connect_satisfied(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_member_connect_satisfied(API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?MEMBER_CONNECT_SATISFIED_VALUES, fun member_connect_satisfied/1), - kz_amqp_util:targeted_publish(Q, Payload, ContentType). + kz_amqp_util:callmgr_publish(Payload, ContentType, member_connect_satisfied_routing_key(API)). -spec publish_agent_timeout(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. publish_agent_timeout(Q, JObj) -> @@ -878,6 +1064,15 @@ publish_member_connect_accepted(Q, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?MEMBER_CONNECT_ACCEPTED_VALUES, fun member_connect_accepted/1), kz_amqp_util:targeted_publish(Q, Payload, ContentType). +-spec publish_member_callback_accepted(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_member_callback_accepted(Q, JObj) -> + publish_member_callback_accepted(Q, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_member_callback_accepted(kz_term:ne_binary(), kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_member_callback_accepted(Q, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?MEMBER_CALLBACK_ACCEPTED_VALUES, fun member_callback_accepted/1), + kz_amqp_util:targeted_publish(Q, Payload, ContentType). + -spec publish_member_connect_retry(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. publish_member_connect_retry(Q, JObj) -> publish_member_connect_retry(Q, JObj, ?DEFAULT_CONTENT_TYPE). @@ -923,6 +1118,24 @@ publish_agent_change(API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENT_CHANGE_VALUES, fun agent_change/1), kz_amqp_util:kapps_publish(agent_change_publish_key(API), Payload, ContentType). +-spec publish_agents_available_req(kz_term:api_terms()) -> 'ok'. +publish_agents_available_req(JObj) -> + publish_agents_available_req(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agents_available_req(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_agents_available_req(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENTS_AVAILABLE_REQ_VALUES, fun agents_available_req/1), + kz_amqp_util:kapps_publish(agents_availability_routing_key(API), Payload, ContentType). + +-spec publish_agents_available_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_agents_available_resp(RespQ, JObj) -> + publish_agents_available_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agents_available_resp(kz_term:ne_binary(), kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_agents_available_resp(RespQ, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENTS_AVAILABLE_RESP_VALUES, fun agents_available_resp/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + -spec publish_queue_member_add(kz_term:api_terms()) -> 'ok'. publish_queue_member_add(JObj) -> publish_queue_member_add(JObj, ?DEFAULT_CONTENT_TYPE). @@ -940,3 +1153,12 @@ publish_queue_member_remove(JObj) -> publish_queue_member_remove(API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?QUEUE_MEMBER_REMOVE_VALUES, fun queue_member_remove/1), kz_amqp_util:kapps_publish(queue_member_routing_key(API), Payload, ContentType). + +-spec publish_member_callback_reg(kz_term:api_terms()) -> 'ok'. +publish_member_callback_reg(JObj) -> + publish_member_callback_reg(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_member_callback_reg(kz_term:api_terms(), kz_term:ne_binary()) -> 'ok'. +publish_member_callback_reg(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?MEMBER_CALLBACK_VALUES, fun member_callback_reg/1), + kz_amqp_util:callmgr_publish(Payload, ContentType, member_callback_reg_routing_key(API)). diff --git a/applications/acdc/src/kapi_acdc_stats.erl b/applications/acdc/src/kapi_acdc_stats.erl index e7fd8afedb9..5522425e11f 100644 --- a/applications/acdc/src/kapi_acdc_stats.erl +++ b/applications/acdc/src/kapi_acdc_stats.erl @@ -2,9 +2,8 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% @author Sponsored by GTNetwork LLC, Implemented by SIPLABS LLC +%%% @author KAZOO-3596: Sponsored by GTNetwork LLC, implemented by SIPLABS LLC %%% @author Daniel Finke -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. @@ -17,15 +16,26 @@ -export([call_waiting/1, call_waiting_v/1 ,call_missed/1, call_missed_v/1 ,call_abandoned/1, call_abandoned_v/1 + ,call_marked_callback/1, call_marked_callback_v/1 ,call_handled/1, call_handled_v/1 ,call_processed/1, call_processed_v/1 + ,call_exited_position/1, call_exited_position_v/1 + ,call_flush/1, call_flush_v/1 ,current_calls_req/1, current_calls_req_v/1 ,current_calls_err/1, current_calls_err_v/1 ,current_calls_resp/1, current_calls_resp_v/1 + ,call_summary_req/1, call_summary_req_v/1 + ,call_summary_err/1, call_summary_err_v/1 + ,call_summary_resp/1, call_summary_resp_v/1 + + ,agent_calls_req/1, agent_calls_req_v/1 + ,agent_calls_err/1, agent_calls_err_v/1 + ,agent_calls_resp/1, agent_calls_resp_v/1 + ,average_wait_time_req/1, average_wait_time_req_v/1 ,average_wait_time_err/1, average_wait_time_err_v/1 ,average_wait_time_resp/1, average_wait_time_resp_v/1 @@ -34,6 +44,10 @@ ,status_err/1, status_err_v/1 ,status_resp/1, status_resp_v/1 + ,agent_cur_status_req/1, agent_cur_status_req_v/1 + ,agent_cur_status_err/1, agent_cur_status_err_v/1 + ,agent_cur_status_resp/1, agent_cur_status_resp_v/1 + ,status_ready/1, status_ready_v/1 ,status_logged_in/1, status_logged_in_v/1 ,status_logged_out/1, status_logged_out_v/1 @@ -43,6 +57,7 @@ ,status_wrapup/1, status_wrapup_v/1 ,status_paused/1, status_paused_v/1 ,status_outbound/1, status_outbound_v/1 + ,status_inbound/1, status_inbound_v/1 ,status_update/1, status_update_v/1 ]). @@ -54,15 +69,26 @@ -export([publish_call_waiting/1, publish_call_waiting/2 ,publish_call_missed/1, publish_call_missed/2 ,publish_call_abandoned/1, publish_call_abandoned/2 + ,publish_call_marked_callback/1, publish_call_marked_callback/2 ,publish_call_handled/1, publish_call_handled/2 ,publish_call_processed/1, publish_call_processed/2 + ,publish_call_exited_position/1, publish_call_exited_position/2 + ,publish_call_flush/1, publish_call_flush/2 ,publish_current_calls_req/1, publish_current_calls_req/2 ,publish_current_calls_err/2, publish_current_calls_err/3 ,publish_current_calls_resp/2, publish_current_calls_resp/3 + ,publish_call_summary_req/1, publish_call_summary_req/2 + ,publish_call_summary_err/2, publish_call_summary_err/3 + ,publish_call_summary_resp/2, publish_call_summary_resp/3 + + ,publish_agent_calls_req/1, publish_agent_calls_req/2 + ,publish_agent_calls_err/2, publish_agent_calls_err/3 + ,publish_agent_calls_resp/2, publish_agent_calls_resp/3 + ,publish_average_wait_time_req/1, publish_average_wait_time_req/2 ,publish_average_wait_time_err/2, publish_average_wait_time_err/3 ,publish_average_wait_time_resp/2, publish_average_wait_time_resp/3 @@ -71,6 +97,10 @@ ,publish_status_err/2, publish_status_err/3 ,publish_status_resp/2, publish_status_resp/3 + ,publish_agent_cur_status_req/1, publish_agent_cur_status_req/2 + ,publish_agent_cur_status_err/2, publish_agent_cur_status_err/3 + ,publish_agent_cur_status_resp/2, publish_agent_cur_status_resp/3 + ,publish_status_ready/1, publish_status_ready/2 ,publish_status_logged_in/1, publish_status_logged_in/2 ,publish_status_logged_out/1, publish_status_logged_out/2 @@ -80,6 +110,7 @@ ,publish_status_wrapup/1, publish_status_wrapup/2 ,publish_status_paused/1, publish_status_paused/2 ,publish_status_outbound/1, publish_status_outbound/2 + ,publish_status_inbound/1, publish_status_inbound/2 ,publish_status_update/1, publish_status_update/2 ]). @@ -91,10 +122,11 @@ ]). -define(WAITING_HEADERS, [<<"Caller-ID-Name">>, <<"Caller-ID-Number">> - ,<<"Entered-Timestamp">>, <<"Caller-Priority">> + ,<<"Entered-Timestamp">>, <<"Entered-Position">>, <<"Caller-Priority">> + ,<<"Required-Skills">> ]). -define(WAITING_VALUES, ?CALL_REQ_VALUES(<<"waiting">>)). --define(WAITING_TYPES, []). +-define(WAITING_TYPES, [{<<"Required-Skills">>, fun kz_term:is_ne_binaries/1}]). -define(MISS_HEADERS, [<<"Agent-ID">>, <<"Miss-Reason">>, <<"Miss-Timestamp">>]). -define(MISS_VALUES, ?CALL_REQ_VALUES(<<"missed">>)). @@ -104,6 +136,10 @@ -define(ABANDON_VALUES, ?CALL_REQ_VALUES(<<"abandoned">>)). -define(ABANDON_TYPES, []). +-define(MARKED_CALLBACK_HEADERS, [<<"Caller-ID-Name">>]). +-define(MARKED_CALLBACK_VALUES, ?CALL_REQ_VALUES(<<"marked_callback">>)). +-define(MARKED_CALLBACK_TYPES, []). + -define(HANDLED_HEADERS, [<<"Agent-ID">>, <<"Handled-Timestamp">>]). -define(HANDLED_VALUES, ?CALL_REQ_VALUES(<<"handled">>)). -define(HANDLED_TYPES, []). @@ -112,6 +148,10 @@ -define(PROCESS_VALUES, ?CALL_REQ_VALUES(<<"processed">>)). -define(PROCESS_TYPES, []). +-define(EXITED_HEADERS, [<<"Exited-Position">>]). +-define(EXITED_VALUES, ?CALL_REQ_VALUES(<<"exited-position">>)). +-define(EXITED_TYPES, []). + -define(FLUSH_HEADERS, [<<"Call-ID">>]). -define(FLUSH_VALUES, ?CALL_REQ_VALUES(<<"flush">>)). -define(FLUSH_TYPES, []). @@ -167,6 +207,23 @@ call_abandoned_v(Prop) when is_list(Prop) -> call_abandoned_v(JObj) -> call_abandoned_v(kz_json:to_proplist(JObj)). +-spec call_marked_callback(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +call_marked_callback(Props) when is_list(Props) -> + case call_marked_callback_v(Props) of + 'true' -> kz_api:build_message(Props, ?CALL_REQ_HEADERS, ?MARKED_CALLBACK_HEADERS); + 'false' -> {'error', "Proplist failed validation for call_marked_callback"} + end; +call_marked_callback(JObj) -> + call_marked_callback(kz_json:to_proplist(JObj)). + +-spec call_marked_callback_v(kz_term:api_terms()) -> boolean(). +call_marked_callback_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?CALL_REQ_HEADERS, ?MARKED_CALLBACK_VALUES, ?MARKED_CALLBACK_TYPES); +call_marked_callback_v(JObj) -> + call_marked_callback_v(kz_json:to_proplist(JObj)). + -spec call_handled(kz_term:api_terms()) -> {'ok', iolist()} | {'error', string()}. @@ -201,6 +258,23 @@ call_processed_v(Prop) when is_list(Prop) -> call_processed_v(JObj) -> call_processed_v(kz_json:to_proplist(JObj)). +-spec call_exited_position(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +call_exited_position(Props) when is_list(Props) -> + case call_exited_position_v(Props) of + 'true' -> kz_api:build_message(Props, ?CALL_REQ_HEADERS, ?EXITED_HEADERS); + 'false' -> {'error', "Proplist failed validation for call_exited_position"} + end; +call_exited_position(JObj) -> + call_exited_position(kz_json:to_proplist(JObj)). + +-spec call_exited_position_v(kz_term:api_terms()) -> boolean(). +call_exited_position_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?CALL_REQ_HEADERS, ?EXITED_VALUES, ?EXITED_TYPES); +call_exited_position_v(JObj) -> + call_exited_position_v(kz_json:to_proplist(JObj)). + -spec call_flush(kz_term:api_terms()) -> {'ok', iolist()} | {'error', string()}. @@ -272,6 +346,7 @@ current_calls_err_v(JObj) -> -define(CURRENT_CALLS_RESP_HEADERS, [<<"Query-Time">>]). -define(OPTIONAL_CURRENT_CALLS_RESP_HEADERS, [<<"Waiting">>, <<"Handled">> ,<<"Abandoned">>, <<"Processed">> + ,<<"Entered-Position">>, <<"Exited-Position">> ]). -define(CURRENT_CALLS_RESP_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} ,{<<"Event-Name">>, <<"current_calls_resp">>} @@ -295,13 +370,168 @@ current_calls_resp_v(Prop) when is_list(Prop) -> current_calls_resp_v(JObj) -> current_calls_resp_v(kz_json:to_proplist(JObj)). +-define(CALL_SUMMARY_REQ_HEADERS, [<<"Account-ID">>]). +-define(OPTIONAL_CALL_SUMMARY_REQ_HEADERS, [<<"Queue-ID">>, <<"Agent-ID">> + ,<<"Status">> + ,<<"Start-Range">>, <<"End-Range">> + ]). +-define(CALL_SUMMARY_REQ_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"call_summary_req">>} + ]). +-define(CALL_SUMMARY_REQ_TYPES, []). + +-spec call_summary_req(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +call_summary_req(Props) when is_list(Props) -> + case call_summary_req_v(Props) of + 'true' -> kz_api:build_message(Props, ?CALL_SUMMARY_REQ_HEADERS, ?OPTIONAL_CALL_SUMMARY_REQ_HEADERS); + 'false' -> {'error', "Proplist failed validation for call_summary_req"} + end; +call_summary_req(JObj) -> + call_summary_req(kz_json:to_proplist(JObj)). + +-spec call_summary_req_v(kz_term:api_terms()) -> boolean(). +call_summary_req_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?CALL_SUMMARY_REQ_HEADERS, ?CALL_SUMMARY_REQ_VALUES, ?CALL_SUMMARY_REQ_TYPES); +call_summary_req_v(JObj) -> + call_summary_req_v(kz_json:to_proplist(JObj)). + +-define(CALL_SUMMARY_ERR_HEADERS, [<<"Error-Reason">>]). +-define(OPTIONAL_CALL_SUMMARY_ERR_HEADERS, []). +-define(CALL_SUMMARY_ERR_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"call_summary_err">>} + ]). +-define(CALL_SUMMARY_ERR_TYPES, []). + +-spec call_summary_err(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +call_summary_err(Props) when is_list(Props) -> + case call_summary_err_v(Props) of + 'true' -> kz_api:build_message(Props, ?CALL_SUMMARY_ERR_HEADERS, ?OPTIONAL_CALL_SUMMARY_ERR_HEADERS); + 'false' -> {'error', "Proplist failed validation for call_summary_err"} + end; +call_summary_err(JObj) -> + call_summary_err(kz_json:to_proplist(JObj)). + +-spec call_summary_err_v(kz_term:api_terms()) -> boolean(). +call_summary_err_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?CALL_SUMMARY_ERR_HEADERS, ?CALL_SUMMARY_ERR_VALUES, ?CALL_SUMMARY_ERR_TYPES); +call_summary_err_v(JObj) -> + call_summary_err_v(kz_json:to_proplist(JObj)). + +-define(CALL_SUMMARY_RESP_HEADERS, [<<"Query-Time">>]). +-define(OPTIONAL_CALL_SUMMARY_RESP_HEADERS, [<<"Data">> + ,<<"Waiting">>, <<"Handled">> + ,<<"Abandoned">>, <<"Processed">> + ,<<"Entered-Position">>, <<"Exited-Position">> + ]). +-define(CALL_SUMMARY_RESP_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"call_summary_resp">>} + ]). +-define(CALL_SUMMARY_RESP_TYPES, []). + +-spec call_summary_resp(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +call_summary_resp(Props) when is_list(Props) -> + case call_summary_resp_v(Props) of + 'true' -> kz_api:build_message(Props, ?CALL_SUMMARY_RESP_HEADERS, ?OPTIONAL_CALL_SUMMARY_RESP_HEADERS); + 'false' -> {'error', "Proplist failed validation for call_summary_resp"} + end; +call_summary_resp(JObj) -> + call_summary_resp(kz_json:to_proplist(JObj)). + +-spec call_summary_resp_v(kz_term:api_terms()) -> boolean(). +call_summary_resp_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?CALL_SUMMARY_RESP_HEADERS, ?CALL_SUMMARY_RESP_VALUES, ?CALL_SUMMARY_RESP_TYPES); +call_summary_resp_v(JObj) -> + call_summary_resp_v(kz_json:to_proplist(JObj)). + +-define(AGENT_CALLS_REQ_HEADERS, [<<"Account-ID">>]). +-define(OPTIONAL_AGENT_CALLS_REQ_HEADERS, [<<"Queue-ID">>, <<"Agent-ID">> + ,<<"Status">> + ,<<"Start-Range">>, <<"End-Range">> + ]). +-define(AGENT_CALLS_REQ_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"agent_calls_req">>} + ]). +-define(AGENT_CALLS_REQ_TYPES, []). + +-spec agent_calls_req(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agent_calls_req(Props) when is_list(Props) -> + case agent_calls_req_v(Props) of + 'true' -> kz_api:build_message(Props, ?AGENT_CALLS_REQ_HEADERS, ?OPTIONAL_AGENT_CALLS_REQ_HEADERS); + 'false' -> {'error', "Proplist failed validation for agent_calls_req"} + end; +agent_calls_req(JObj) -> + agent_calls_req(kz_json:to_proplist(JObj)). + +-spec agent_calls_req_v(kz_term:api_terms()) -> boolean(). +agent_calls_req_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENT_CALLS_REQ_HEADERS, ?AGENT_CALLS_REQ_VALUES, ?AGENT_CALLS_REQ_TYPES); +agent_calls_req_v(JObj) -> + agent_calls_req_v(kz_json:to_proplist(JObj)). + +-define(AGENT_CALLS_ERR_HEADERS, [<<"Error-Reason">>]). +-define(OPTIONAL_AGENT_CALLS_ERR_HEADERS, []). +-define(AGENT_CALLS_ERR_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"agent_calls_err">>} + ]). +-define(AGENT_CALLS_ERR_TYPES, []). + +-spec agent_calls_err(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agent_calls_err(Props) when is_list(Props) -> + case agent_calls_err_v(Props) of + 'true' -> kz_api:build_message(Props, ?AGENT_CALLS_ERR_HEADERS, ?OPTIONAL_AGENT_CALLS_ERR_HEADERS); + 'false' -> {'error', "Proplist failed validation for agent_calls_err"} + end; +agent_calls_err(JObj) -> + agent_calls_err(kz_json:to_proplist(JObj)). + +-spec agent_calls_err_v(kz_term:api_terms()) -> boolean(). +agent_calls_err_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENT_CALLS_ERR_HEADERS, ?AGENT_CALLS_ERR_VALUES, ?AGENT_CALLS_ERR_TYPES); +agent_calls_err_v(JObj) -> + agent_calls_err_v(kz_json:to_proplist(JObj)). + +-define(AGENT_CALLS_RESP_HEADERS, [<<"Query-Time">>]). +-define(OPTIONAL_AGENT_CALLS_RESP_HEADERS, [<<"Data">>]). +-define(AGENT_CALLS_RESP_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"agent_calls_resp">>} + ]). +-define(AGENT_CALLS_RESP_TYPES, []). + +-spec agent_calls_resp(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agent_calls_resp(Props) when is_list(Props) -> + case agent_calls_resp_v(Props) of + 'true' -> kz_api:build_message(Props, ?AGENT_CALLS_RESP_HEADERS, ?OPTIONAL_AGENT_CALLS_RESP_HEADERS); + 'false' -> {'error', "Proplist failed validation for agent_calls_resp"} + end; +agent_calls_resp(JObj) -> + agent_calls_resp(kz_json:to_proplist(JObj)). + +-spec agent_calls_resp_v(kz_term:api_terms()) -> boolean(). +agent_calls_resp_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENT_CALLS_RESP_HEADERS, ?AGENT_CALLS_RESP_VALUES, ?AGENT_CALLS_RESP_TYPES); +agent_calls_resp_v(JObj) -> + agent_calls_resp_v(kz_json:to_proplist(JObj)). + -define(AVERAGE_WAIT_TIME_REQ_HEADERS, [<<"Account-ID">>, <<"Queue-ID">>]). --define(OPTIONAL_AVERAGE_WAIT_TIME_REQ_HEADERS, [<<"Window">>]). +-define(OPTIONAL_AVERAGE_WAIT_TIME_REQ_HEADERS, [<<"Skills">>, <<"Window">>]). -define(AVERAGE_WAIT_TIME_REQ_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} ,{<<"Event-Name">>, <<"average_wait_time_req">>} ]). -define(AVERAGE_WAIT_TIME_REQ_TYPES, [{<<"Account-ID">>, fun kz_term:is_ne_binary/1} ,{<<"Queue-ID">>, fun kz_term:is_ne_binary/1} + ,{<<"Skills">>, fun kz_term:is_ne_binaries/1} ,{<<"Window">>, fun is_integer/1} ]). @@ -444,10 +674,81 @@ status_resp_v(Prop) when is_list(Prop) -> status_resp_v(JObj) -> status_resp_v(kz_json:to_proplist(JObj)). +-define(AGENT_CUR_STATUS_REQ_HEADERS, [<<"Account-ID">>]). +-define(OPTIONAL_AGENT_CUR_STATUS_REQ_HEADERS, [<<"Agent-ID">>]). +-define(AGENT_CUR_STATUS_REQ_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"agent_cur_status_req">>} + ]). +-define(AGENT_CUR_STATUS_REQ_TYPES, []). + +-spec agent_cur_status_req(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agent_cur_status_req(Props) when is_list(Props) -> + case agent_cur_status_req_v(Props) of + 'true' -> kz_api:build_message(Props, ?AGENT_CUR_STATUS_REQ_HEADERS, ?OPTIONAL_AGENT_CUR_STATUS_REQ_HEADERS); + 'false' -> {'error', "Proplist failed validation for agent_cur_status_req"} + end; +agent_cur_status_req(JObj) -> + agent_cur_status_req(kz_json:to_proplist(JObj)). + +-spec agent_cur_status_req_v(kz_term:api_terms()) -> boolean(). +agent_cur_status_req_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENT_CUR_STATUS_REQ_HEADERS, ?AGENT_CUR_STATUS_REQ_VALUES, ?AGENT_CUR_STATUS_REQ_TYPES); +agent_cur_status_req_v(JObj) -> + agent_cur_status_req_v(kz_json:to_proplist(JObj)). + +-define(AGENT_CUR_STATUS_ERR_HEADERS, [<<"Error-Reason">>]). +-define(OPTIONAL_AGENT_CUR_STATUS_ERR_HEADERS, []). +-define(AGENT_CUR_STATUS_ERR_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"agent_cur_status_err">>} + ]). +-define(AGENT_CUR_STATUS_ERR_TYPES, []). + +-spec agent_cur_status_err(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agent_cur_status_err(Props) when is_list(Props) -> + case agent_cur_status_err_v(Props) of + 'true' -> kz_api:build_message(Props, ?AGENT_CUR_STATUS_ERR_HEADERS, ?OPTIONAL_AGENT_CUR_STATUS_ERR_HEADERS); + 'false' -> {'error', "Proplist failed validation for agent_cur_status_err"} + end; +agent_cur_status_err(JObj) -> + agent_cur_status_err(kz_json:to_proplist(JObj)). + +-spec agent_cur_status_err_v(kz_term:api_terms()) -> boolean(). +agent_cur_status_err_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENT_CUR_STATUS_ERR_HEADERS, ?AGENT_CUR_STATUS_ERR_VALUES, ?AGENT_CUR_STATUS_ERR_TYPES); +agent_cur_status_err_v(JObj) -> + agent_cur_status_err_v(kz_json:to_proplist(JObj)). + +-define(AGENT_CUR_STATUS_RESP_HEADERS, [<<"Agents">>]). +-define(OPTIONAL_AGENT_CUR_STATUS_RESP_HEADERS, []). +-define(AGENT_CUR_STATUS_RESP_VALUES, [{<<"Event-Category">>, <<"acdc_stat">>} + ,{<<"Event-Name">>, <<"agent_cur_status_resp">>} + ]). +-define(AGENT_CUR_STATUS_RESP_TYPES, []). + +-spec agent_cur_status_resp(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +agent_cur_status_resp(Props) when is_list(Props) -> + case agent_cur_status_resp_v(Props) of + 'true' -> kz_api:build_message(Props, ?AGENT_CUR_STATUS_RESP_HEADERS, ?OPTIONAL_AGENT_CUR_STATUS_RESP_HEADERS); + 'false' -> {'error', "Proplist failed validation for agent_cur_status_resp"} + end; +agent_cur_status_resp(JObj) -> + agent_cur_status_resp(kz_json:to_proplist(JObj)). + +-spec agent_cur_status_resp_v(kz_term:api_terms()) -> boolean(). +agent_cur_status_resp_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?AGENT_CUR_STATUS_RESP_HEADERS, ?AGENT_CUR_STATUS_RESP_VALUES, ?AGENT_CUR_STATUS_RESP_TYPES); +agent_cur_status_resp_v(JObj) -> + agent_cur_status_resp_v(kz_json:to_proplist(JObj)). + -define(STATUS_HEADERS, [<<"Account-ID">>, <<"Agent-ID">>, <<"Timestamp">>]). --define(STATUS_OPTIONAL_HEADERS, [<<"Wait-Time">>, <<"Pause-Time">>, <<"Call-ID">> +-define(STATUS_OPTIONAL_HEADERS, [<<"Wait-Time">>, <<"Pause-Time">>, <<"Pause-Alias">>, <<"Call-ID">> ,<<"Caller-ID-Name">>, <<"Caller-ID-Number">> - ,<<"Queue-ID">> ]). -define(STATUS_VALUES(Name), [{<<"Event-Category">>, <<"acdc_status_stat">>} ,{<<"Event-Name">>, Name} @@ -625,62 +926,82 @@ status_outbound_v(Prop) when is_list(Prop) -> status_outbound_v(JObj) -> status_outbound_v(kz_json:to_proplist(JObj)). +-spec status_inbound(kz_term:api_terms()) -> + {'ok', iolist()} | + {'error', string()}. +status_inbound(Props) when is_list(Props) -> + case status_inbound_v(Props) of + 'true' -> kz_api:build_message(Props, ?STATUS_HEADERS, ?STATUS_OPTIONAL_HEADERS); + 'false' -> {'error', "Proplist failed validation for status_inbound"} + end; +status_inbound(JObj) -> + status_inbound(kz_json:to_proplist(JObj)). + +-spec status_inbound_v(kz_term:api_terms()) -> boolean(). +status_inbound_v(Prop) when is_list(Prop) -> + kz_api:validate(Prop, ?STATUS_HEADERS, ?STATUS_VALUES(<<"inbound">>), ?STATUS_TYPES); +status_inbound_v(JObj) -> + status_inbound_v(kz_json:to_proplist(JObj)). + -spec bind_q(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. -bind_q(AMQPQueue, Props) -> - QueueId = props:get_value('queue_id', Props, <<"*">>), - AgentId = props:get_value('agent_id', Props, <<"*">>), +bind_q(Q, Props) -> + QID = props:get_value('queue_id', Props, <<"*">>), + AID = props:get_value('agent_id', Props, <<"*">>), AccountId = props:get_value('account_id', Props, <<"*">>), - bind_q(AMQPQueue, AccountId, QueueId, AgentId, props:get_value('restrict_to', Props)). - -bind_q(AMQPQueue, AccountId, QueueId, AgentId, 'undefined') -> - kz_amqp_util:bind_q_to_kapps(AMQPQueue, call_stat_routing_key(AccountId, QueueId)), - kz_amqp_util:bind_q_to_kapps(AMQPQueue, status_stat_routing_key(AccountId, AgentId)), - kz_amqp_util:bind_q_to_kapps(AMQPQueue, query_call_stat_routing_key(AccountId, QueueId)), - kz_amqp_util:bind_q_to_kapps(AMQPQueue, query_status_stat_routing_key(AccountId, AgentId)); -bind_q(AMQPQueue, AccountId, QueueId, AgentId, ['call_stat'|L]) -> - kz_amqp_util:bind_q_to_kapps(AMQPQueue, call_stat_routing_key(AccountId, QueueId)), - bind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -bind_q(AMQPQueue, AccountId, QueueId, AgentId, ['status_stat'|L]) -> - kz_amqp_util:bind_q_to_kapps(AMQPQueue, status_stat_routing_key(AccountId, AgentId)), - bind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -bind_q(AMQPQueue, AccountId, QueueId, AgentId, ['query_call_stat'|L]) -> - kz_amqp_util:bind_q_to_kapps(AMQPQueue, query_call_stat_routing_key(AccountId, QueueId)), - bind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -bind_q(AMQPQueue, AccountId, QueueId, AgentId, ['query_status_stat'|L]) -> - kz_amqp_util:bind_q_to_kapps(AMQPQueue, query_status_stat_routing_key(AccountId, AgentId)), - bind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -bind_q(AMQPQueue, AccountId, QueueId, AgentId, [_|L]) -> - bind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -bind_q(_AMQPQueue, _AccountId, _QueueId, _AgentId, []) -> 'ok'. + bind_q(Q, AccountId, QID, AID, props:get_value('restrict_to', Props)). + +bind_q(Q, AccountId, QID, AID, 'undefined') -> + kz_amqp_util:bind_q_to_kapps(Q, call_stat_routing_key(AccountId, QID)), + kz_amqp_util:bind_q_to_kapps(Q, status_stat_routing_key(AccountId, AID)), + kz_amqp_util:bind_q_to_kapps(Q, query_call_stat_routing_key(AccountId, QID)), + kz_amqp_util:bind_q_to_kapps(Q, query_status_stat_routing_key(AccountId, AID)); +bind_q(Q, AccountId, QID, AID, ['call_stat'|L]) -> + kz_amqp_util:bind_q_to_kapps(Q, call_stat_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, AID, L); +bind_q(Q, AccountId, QID, AID, ['status_stat'|L]) -> + kz_amqp_util:bind_q_to_kapps(Q, status_stat_routing_key(AccountId, AID)), + bind_q(Q, AccountId, QID, AID, L); +bind_q(Q, AccountId, QID, AID, ['query_call_stat'|L]) -> + kz_amqp_util:bind_q_to_kapps(Q, query_call_stat_routing_key(AccountId, QID)), + bind_q(Q, AccountId, QID, AID, L); +bind_q(Q, AccountId, QID, AID, ['query_status_stat'|L]) -> + kz_amqp_util:bind_q_to_kapps(Q, query_status_stat_routing_key(AccountId, AID)), + bind_q(Q, AccountId, QID, AID, L); +bind_q(Q, AccountId, QID, AID, [_|L]) -> + bind_q(Q, AccountId, QID, AID, L); +bind_q(_Q, _AccountId, _QID, _AID, []) -> 'ok'. -spec unbind_q(kz_term:ne_binary(), kz_term:proplist()) -> 'ok'. -unbind_q(AMQPQueue, Props) -> - QueueId = props:get_value('queue_id', Props, <<"*">>), - AgentId = props:get_value('agent_id', Props, <<"*">>), +unbind_q(Q, Props) -> + QID = props:get_value('queue_id', Props, <<"*">>), + AID = props:get_value('agent_id', Props, <<"*">>), AccountId = props:get_value('account_id', Props, <<"*">>), - unbind_q(AMQPQueue, AccountId, QueueId, AgentId, props:get_value('restrict_to', Props)). - -unbind_q(AMQPQueue, AccountId, QueueId, AgentId, 'undefined') -> - unbind_q(AMQPQueue, AccountId, QueueId, AgentId, ['call_stat', 'status_stat', 'query_call_stat', 'query_status_stat']); -unbind_q(AMQPQueue, AccountId, QueueId, AgentId, ['call_stat'|L]) -> - _ = kz_amqp_util:unbind_q_from_kapps(AMQPQueue, call_stat_routing_key(AccountId, QueueId)), - unbind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -unbind_q(AMQPQueue, AccountId, QueueId, AgentId, ['status_stat'|L]) -> - _ = kz_amqp_util:unbind_q_from_kapps(AMQPQueue, status_stat_routing_key(AccountId, AgentId)), - unbind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -unbind_q(AMQPQueue, AccountId, QueueId, AgentId, ['query_call_stat'|L]) -> - _ = kz_amqp_util:unbind_q_from_kapps(AMQPQueue, query_call_stat_routing_key(AccountId, QueueId)), - unbind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -unbind_q(AMQPQueue, AccountId, QueueId, AgentId, ['query_status_stat'|L]) -> - _ = kz_amqp_util:unbind_q_from_kapps(AMQPQueue, query_status_stat_routing_key(AccountId, AgentId)), - unbind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -unbind_q(AMQPQueue, AccountId, QueueId, AgentId, [_|L]) -> - unbind_q(AMQPQueue, AccountId, QueueId, AgentId, L); -unbind_q(_AMQPQueue, _AccountId, _QueueId, _AgentId, []) -> 'ok'. + unbind_q(Q, AccountId, QID, AID, props:get_value('restrict_to', Props)). + +unbind_q(Q, AccountId, QID, AID, 'undefined') -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, call_stat_routing_key(AccountId, QID)), + _ = kz_amqp_util:unbind_q_from_kapps(Q, status_stat_routing_key(AccountId, AID)), + _ = kz_amqp_util:unbind_q_from_kapps(Q, query_call_stat_routing_key(AccountId, QID)), + kz_amqp_util:unbind_q_from_kapps(Q, query_status_stat_routing_key(AccountId, AID)); +unbind_q(Q, AccountId, QID, AID, ['call_stat'|L]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, call_stat_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, AID, L); +unbind_q(Q, AccountId, QID, AID, ['status_stat'|L]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, status_stat_routing_key(AccountId, AID)), + unbind_q(Q, AccountId, QID, AID, L); +unbind_q(Q, AccountId, QID, AID, ['query_call_stat'|L]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, query_call_stat_routing_key(AccountId, QID)), + unbind_q(Q, AccountId, QID, AID, L); +unbind_q(Q, AccountId, QID, AID, ['query_status_stat'|L]) -> + _ = kz_amqp_util:unbind_q_from_kapps(Q, query_status_stat_routing_key(AccountId, AID)), + unbind_q(Q, AccountId, QID, AID, L); +unbind_q(Q, AccountId, QID, AID, [_|L]) -> + unbind_q(Q, AccountId, QID, AID, L); +unbind_q(_Q, _AccountId, _QID, _AID, []) -> 'ok'. %%------------------------------------------------------------------------------ -%% @doc Declare the exchanges used by this API +%% @doc declare the exchanges used by this API %% @end %%------------------------------------------------------------------------------ -spec declare_exchanges() -> 'ok'. @@ -714,6 +1035,15 @@ publish_call_abandoned(API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?ABANDON_VALUES, fun call_abandoned/1), kz_amqp_util:kapps_publish(call_stat_routing_key(API), Payload, ContentType). +-spec publish_call_marked_callback(kz_term:api_terms()) -> 'ok'. +publish_call_marked_callback(JObj) -> + publish_call_marked_callback(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_call_marked_callback(kz_term:api_terms(), binary()) -> 'ok'. +publish_call_marked_callback(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?MARKED_CALLBACK_VALUES, fun call_marked_callback/1), + kz_amqp_util:kapps_publish(call_stat_routing_key(API), Payload, ContentType). + -spec publish_call_handled(kz_term:api_terms()) -> 'ok'. publish_call_handled(JObj) -> publish_call_handled(JObj, ?DEFAULT_CONTENT_TYPE). @@ -732,6 +1062,15 @@ publish_call_processed(API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?PROCESS_VALUES, fun call_processed/1), kz_amqp_util:kapps_publish(call_stat_routing_key(API), Payload, ContentType). +-spec publish_call_exited_position(kz_term:api_terms()) -> 'ok'. +publish_call_exited_position(JObj) -> + publish_call_exited_position(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_call_exited_position(kz_term:api_terms(), binary()) -> 'ok'. +publish_call_exited_position(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?EXITED_VALUES, fun call_exited_position/1), + kz_amqp_util:kapps_publish(call_stat_routing_key(API), Payload, ContentType). + -spec publish_call_flush(kz_term:api_terms()) -> 'ok'. publish_call_flush(JObj) -> publish_call_flush(JObj, ?DEFAULT_CONTENT_TYPE). @@ -833,6 +1172,15 @@ publish_status_outbound(API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?STATUS_VALUES(<<"outbound">>), fun status_outbound/1), kz_amqp_util:kapps_publish(status_stat_routing_key(API), Payload, ContentType). +-spec publish_status_inbound(kz_term:api_terms()) -> 'ok'. +publish_status_inbound(JObj) -> + publish_status_inbound(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_status_inbound(kz_term:api_terms(), binary()) -> 'ok'. +publish_status_inbound(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?STATUS_VALUES(<<"inbound">>), fun status_inbound/1), + kz_amqp_util:kapps_publish(status_stat_routing_key(API), Payload, ContentType). + -spec publish_current_calls_req(kz_term:api_terms()) -> 'ok'. publish_current_calls_req(JObj) -> publish_current_calls_req(JObj, ?DEFAULT_CONTENT_TYPE). @@ -843,22 +1191,76 @@ publish_current_calls_req(API, ContentType) -> kz_amqp_util:kapps_publish(query_call_stat_routing_key(API), Payload, ContentType). -spec publish_current_calls_err(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_current_calls_err(RespAMQPQueue, JObj) -> - publish_current_calls_err(RespAMQPQueue, JObj, ?DEFAULT_CONTENT_TYPE). +publish_current_calls_err(RespQ, JObj) -> + publish_current_calls_err(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). -spec publish_current_calls_err(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. -publish_current_calls_err(RespAMQPQueue, API, ContentType) -> +publish_current_calls_err(RespQ, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?CURRENT_CALLS_ERR_VALUES, fun current_calls_err/1), - kz_amqp_util:targeted_publish(RespAMQPQueue, Payload, ContentType). + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). -spec publish_current_calls_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_current_calls_resp(RespAMQPQueue, JObj) -> - publish_current_calls_resp(RespAMQPQueue, JObj, ?DEFAULT_CONTENT_TYPE). +publish_current_calls_resp(RespQ, JObj) -> + publish_current_calls_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). -spec publish_current_calls_resp(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. -publish_current_calls_resp(RespAMQPQueue, API, ContentType) -> +publish_current_calls_resp(RespQ, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?CURRENT_CALLS_RESP_VALUES, fun current_calls_resp/1), - kz_amqp_util:targeted_publish(RespAMQPQueue, Payload, ContentType). + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + +-spec publish_call_summary_req(kz_term:api_terms()) -> 'ok'. +publish_call_summary_req(JObj) -> + publish_call_summary_req(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_call_summary_req(kz_term:api_terms(), binary()) -> 'ok'. +publish_call_summary_req(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?CALL_SUMMARY_REQ_VALUES, fun call_summary_req/1), + kz_amqp_util:kapps_publish(query_call_stat_routing_key(API), Payload, ContentType). + +-spec publish_call_summary_err(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_call_summary_err(RespQ, JObj) -> + publish_call_summary_err(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_call_summary_err(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. +publish_call_summary_err(RespQ, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?CALL_SUMMARY_ERR_VALUES, fun call_summary_err/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + +-spec publish_call_summary_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_call_summary_resp(RespQ, JObj) -> + publish_call_summary_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_call_summary_resp(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. +publish_call_summary_resp(RespQ, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?CALL_SUMMARY_RESP_VALUES, fun call_summary_resp/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + +-spec publish_agent_calls_req(kz_term:api_terms()) -> 'ok'. +publish_agent_calls_req(JObj) -> + publish_agent_calls_req(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agent_calls_req(kz_term:api_terms(), binary()) -> 'ok'. +publish_agent_calls_req(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENT_CALLS_REQ_VALUES, fun agent_calls_req/1), + kz_amqp_util:kapps_publish(query_call_stat_routing_key(API), Payload, ContentType). + +-spec publish_agent_calls_err(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_agent_calls_err(RespQ, JObj) -> + publish_agent_calls_err(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agent_calls_err(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. +publish_agent_calls_err(RespQ, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENT_CALLS_ERR_VALUES, fun agent_calls_err/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + +-spec publish_agent_calls_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_agent_calls_resp(RespQ, JObj) -> + publish_agent_calls_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agent_calls_resp(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. +publish_agent_calls_resp(RespQ, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENT_CALLS_RESP_VALUES, fun agent_calls_resp/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). -spec publish_average_wait_time_req(kz_term:api_terms()) -> 'ok'. publish_average_wait_time_req(JObj) -> @@ -870,22 +1272,22 @@ publish_average_wait_time_req(API, ContentType) -> kz_amqp_util:kapps_publish(query_call_stat_routing_key(API), Payload, ContentType). -spec publish_average_wait_time_err(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_average_wait_time_err(RespAMQPQueue, JObj) -> - publish_average_wait_time_err(RespAMQPQueue, JObj, ?DEFAULT_CONTENT_TYPE). +publish_average_wait_time_err(RespQ, JObj) -> + publish_average_wait_time_err(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). -spec publish_average_wait_time_err(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. -publish_average_wait_time_err(RespAMQPQueue, API, ContentType) -> +publish_average_wait_time_err(RespQ, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?AVERAGE_WAIT_TIME_ERR_VALUES, fun average_wait_time_err/1), - kz_amqp_util:targeted_publish(RespAMQPQueue, Payload, ContentType). + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). -spec publish_average_wait_time_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_average_wait_time_resp(RespAMQPQueue, JObj) -> - publish_average_wait_time_resp(RespAMQPQueue, JObj, ?DEFAULT_CONTENT_TYPE). +publish_average_wait_time_resp(RespQ, JObj) -> + publish_average_wait_time_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). -spec publish_average_wait_time_resp(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. -publish_average_wait_time_resp(RespAMQPQueue, API, ContentType) -> +publish_average_wait_time_resp(RespQ, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?AVERAGE_WAIT_TIME_RESP_VALUES, fun average_wait_time_resp/1), - kz_amqp_util:targeted_publish(RespAMQPQueue, Payload, ContentType). + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). -spec publish_status_req(kz_term:api_terms()) -> 'ok'. publish_status_req(JObj) -> @@ -897,22 +1299,49 @@ publish_status_req(API, ContentType) -> kz_amqp_util:kapps_publish(query_status_stat_routing_key(API), Payload, ContentType). -spec publish_status_err(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_status_err(RespAMQPQueue, JObj) -> - publish_status_err(RespAMQPQueue, JObj, ?DEFAULT_CONTENT_TYPE). +publish_status_err(RespQ, JObj) -> + publish_status_err(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). -spec publish_status_err(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. -publish_status_err(RespAMQPQueue, API, ContentType) -> +publish_status_err(RespQ, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?STATUS_ERR_VALUES, fun status_err/1), - kz_amqp_util:targeted_publish(RespAMQPQueue, Payload, ContentType). + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). -spec publish_status_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. -publish_status_resp(RespAMQPQueue, JObj) -> - publish_status_resp(RespAMQPQueue, JObj, ?DEFAULT_CONTENT_TYPE). +publish_status_resp(RespQ, JObj) -> + publish_status_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). -spec publish_status_resp(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. -publish_status_resp(RespAMQPQueue, API, ContentType) -> +publish_status_resp(RespQ, API, ContentType) -> {'ok', Payload} = kz_api:prepare_api_payload(API, ?STATUS_RESP_VALUES, fun status_resp/1), - kz_amqp_util:targeted_publish(RespAMQPQueue, Payload, ContentType). + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + +-spec publish_agent_cur_status_req(kz_term:api_terms()) -> 'ok'. +publish_agent_cur_status_req(JObj) -> + publish_agent_cur_status_req(JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agent_cur_status_req(kz_term:api_terms(), binary()) -> 'ok'. +publish_agent_cur_status_req(API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENT_CUR_STATUS_REQ_VALUES, fun agent_cur_status_req/1), + kz_amqp_util:kapps_publish(query_status_stat_routing_key(API), Payload, ContentType). + +-spec publish_agent_cur_status_err(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_agent_cur_status_err(RespQ, JObj) -> + publish_agent_cur_status_err(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agent_cur_status_err(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. +publish_agent_cur_status_err(RespQ, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENT_CUR_STATUS_ERR_VALUES, fun agent_cur_status_err/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). + +-spec publish_agent_cur_status_resp(kz_term:ne_binary(), kz_term:api_terms()) -> 'ok'. +publish_agent_cur_status_resp(RespQ, JObj) -> + publish_agent_cur_status_resp(RespQ, JObj, ?DEFAULT_CONTENT_TYPE). + +-spec publish_agent_cur_status_resp(kz_term:ne_binary(), kz_term:api_terms(), binary()) -> 'ok'. +publish_agent_cur_status_resp(RespQ, API, ContentType) -> + {'ok', Payload} = kz_api:prepare_api_payload(API, ?AGENT_CUR_STATUS_RESP_VALUES, fun agent_cur_status_resp/1), + kz_amqp_util:targeted_publish(RespQ, Payload, ContentType). call_stat_routing_key(Prop) when is_list(Prop) -> call_stat_routing_key(props:get_value(<<"Account-ID">>, Prop) @@ -922,8 +1351,8 @@ call_stat_routing_key(JObj) -> call_stat_routing_key(kz_json:get_value(<<"Account-ID">>, JObj) ,kz_json:get_value(<<"Queue-ID">>, JObj) ). -call_stat_routing_key(AccountId, QueueId) -> - <<"acdc_stats.call.", AccountId/binary, ".", QueueId/binary>>. +call_stat_routing_key(AccountId, QID) -> + <<"acdc_stats.call.", AccountId/binary, ".", QID/binary>>. status_stat_routing_key(Prop) when is_list(Prop) -> status_stat_routing_key(props:get_value(<<"Account-ID">>, Prop) @@ -933,8 +1362,8 @@ status_stat_routing_key(JObj) -> status_stat_routing_key(kz_json:get_value(<<"Account-ID">>, JObj) ,kz_json:get_value(<<"Agent-ID">>, JObj) ). -status_stat_routing_key(AccountId, AgentId) -> - <<"acdc_stats.status.", AccountId/binary, ".", AgentId/binary>>. +status_stat_routing_key(AccountId, AID) -> + <<"acdc_stats.status.", AccountId/binary, ".", AID/binary>>. query_call_stat_routing_key(Prop) when is_list(Prop) -> query_call_stat_routing_key(props:get_value(<<"Account-ID">>, Prop) @@ -947,8 +1376,8 @@ query_call_stat_routing_key(JObj) -> query_call_stat_routing_key(AccountId, 'undefined') -> <<"acdc_stats.query_call.", AccountId/binary, ".all">>; -query_call_stat_routing_key(AccountId, QueueId) -> - <<"acdc_stats.query_call.", AccountId/binary, ".", QueueId/binary>>. +query_call_stat_routing_key(AccountId, QID) -> + <<"acdc_stats.query_call.", AccountId/binary, ".", QID/binary>>. query_status_stat_routing_key(Prop) when is_list(Prop) -> query_status_stat_routing_key(props:get_value(<<"Account-ID">>, Prop) @@ -961,8 +1390,8 @@ query_status_stat_routing_key(JObj) -> query_status_stat_routing_key(AccountId, 'undefined') -> <<"acdc_stats.query_status.", AccountId/binary, ".all">>; -query_status_stat_routing_key(AccountId, QueueId) -> - <<"acdc_stats.query_status.", AccountId/binary, ".", QueueId/binary>>. +query_status_stat_routing_key(AccountId, QID) -> + <<"acdc_stats.query_status.", AccountId/binary, ".", QID/binary>>. status_value(API) when is_list(API) -> props:get_value(<<"Status">>, API); diff --git a/applications/acdc/test/acdc_agent_fsm_tests.erl b/applications/acdc/test/acdc_agent_fsm_test.erl similarity index 97% rename from applications/acdc/test/acdc_agent_fsm_tests.erl rename to applications/acdc/test/acdc_agent_fsm_test.erl index bea84aa1af0..fd51be549c2 100644 --- a/applications/acdc/test/acdc_agent_fsm_tests.erl +++ b/applications/acdc/test/acdc_agent_fsm_test.erl @@ -2,14 +2,13 @@ %%% @copyright (C) 2012-2020, 2600Hz %%% @doc %%% @author James Aimonetti -%%% %%% This Source Code Form is subject to the terms of the Mozilla Public %%% License, v. 2.0. If a copy of the MPL was not distributed with this %%% file, You can obtain one at https://mozilla.org/MPL/2.0/. %%% %%% @end %%%----------------------------------------------------------------------------- --module(acdc_agent_fsm_tests). +-module(acdc_agent_fsm_test). -include_lib("eunit/include/eunit.hrl"). diff --git a/applications/acdc/test/acdc_queue_manager_test.erl b/applications/acdc/test/acdc_queue_manager_test.erl new file mode 100644 index 00000000000..dedb79de2a4 --- /dev/null +++ b/applications/acdc/test/acdc_queue_manager_test.erl @@ -0,0 +1,427 @@ +%%%----------------------------------------------------------------------------- +%%% @copyright (C) 2016, Voxter Communications Inc. +%%% @doc +%%% @author Daniel Finke +%%% This Source Code Form is subject to the terms of the Mozilla Public +%%% License, v. 2.0. If a copy of the MPL was not distributed with this +%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(acdc_queue_manager_test). + +-include_lib("eunit/include/eunit.hrl"). + +-include("../src/acdc.hrl"). +-include("../src/acdc_queue_manager.hrl"). + +-define(ACCOUNT_ID, <<"account_id">>). +-define(AGENT_ID, <<"agent_id">>). +-define(QUEUE_ID, <<"queue_id">>). +-define(SBRR_LOAD_TEST_ENABLED, 'true'). + +%%% ===== +%%% TESTS +%%% ===== + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +ss_size_empty_test_() -> + SS = #strategy_state{agents=[]}, + [?_assertEqual(0, acdc_queue_manager:ss_size('mi', SS, 'free')) + ,?_assertEqual(0, acdc_queue_manager:ss_size('mi', SS, 'logged_in'))]. + +ss_size_one_busy_test_() -> + State = #state{strategy='mi' + ,strategy_state=#strategy_state{agents=[]} + }, + SS1 = acdc_queue_manager:update_strategy_with_agent(State, ?AGENT_ID, 0, [], 'add', 'undefined'), + State1 = State#state{strategy_state=SS1}, + SS2 = acdc_queue_manager:update_strategy_with_agent(State1, ?AGENT_ID, 0, [], 'remove', 'busy'), + [?_assertEqual(0, acdc_queue_manager:ss_size('mi', SS2, 'free')) + ,?_assertEqual(1, acdc_queue_manager:ss_size('mi', SS2, 'logged_in'))]. + +%%-------------------------------------------------------------------- +%% @doc Test a skills-based round robin situation where multiple agents may +%% be the candidate for a call, but there's an ideal mapping that +%% should be used. +%% @end +%%-------------------------------------------------------------------- +sbrr_multiple_candidates_test_() -> + S = create_state(), + + %% Add agents 3-5, then 1, 2 + Agents = [{<<"3">>, [<<"A">>, <<"B">>, <<"C">>]} + ,{<<"4">>, [<<"A">>, <<"B">>, <<"C">>]} + ,{<<"5">>, [<<"A">>, <<"B">>]} + ,{<<"1">>, [<<"A">>]} + ,{<<"2">>, [<<"A">>, <<"B">>, <<"C">>]} + ], + ExpectSkillMap = #{[] => sets:from_list([<<"2">>, <<"1">>, <<"5">>, <<"4">>, <<"3">>]) + ,[<<"A">>] => sets:from_list([<<"2">>, <<"1">>, <<"5">>, <<"4">>, <<"3">>]) + ,[<<"A">>, <<"B">>] => sets:from_list([<<"2">>, <<"5">>, <<"4">>, <<"3">>]) + ,[<<"A">>, <<"B">>, <<"C">>] => sets:from_list([<<"2">>, <<"4">>, <<"3">>]) + ,[<<"A">>, <<"C">>] => sets:from_list([<<"2">>, <<"4">>, <<"3">>]) + ,[<<"B">>] => sets:from_list([<<"2">>, <<"5">>, <<"4">>, <<"3">>]) + ,[<<"B">>, <<"C">>] => sets:from_list([<<"2">>, <<"4">>, <<"3">>]) + ,[<<"C">>] => sets:from_list([<<"2">>, <<"4">>, <<"3">>]) + }, + S1 = #state{strategy_state=#strategy_state{agents=#{rr_queue := RRQueue + ,skill_map := SkillMap + }}} = lists:foldl(fun add_agent/2, S, Agents), + + %% Add calls 1-4 + Calls = [{<<"1">>, [<<"A">>, <<"B">>]} + ,{<<"2">>, [<<"A">>, <<"B">>]} + ,{<<"3">>, [<<"A">>, <<"B">>, <<"C">>]} + ,{<<"4">>, [<<"A">>]} + ], + ExpectAgentIdMap = #{<<"1">> => <<"4">> + ,<<"3">> => <<"2">> + ,<<"4">> => <<"3">> + ,<<"5">> => <<"1">> + }, + ExpectCallIdMap = #{<<"1">> => <<"5">> + ,<<"2">> => <<"3">> + ,<<"3">> => <<"4">> + ,<<"4">> => <<"1">> + }, + #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap + ,call_id_map := CallIdMap + }}} = lists:foldl(fun add_call/2, S1, Calls), + + [?_assertEqual([<<"3">>, <<"4">>, <<"5">>, <<"1">>, <<"2">>], pqueue4:to_list(RRQueue)) + ,?_assertEqual(ExpectSkillMap, SkillMap) + ,?_assertEqual(ExpectAgentIdMap, AgentIdMap) + ,?_assertEqual(ExpectCallIdMap, CallIdMap) + ]. + +%%-------------------------------------------------------------------- +%% @doc Test a skills-based round robin queue with a series of adds/removes +%% of calls and agents. Verifies the state as it mutates. +%% @end +%%-------------------------------------------------------------------- +sbrr_multi_phase_test_() -> + S = create_state(), + + %% Add agents 1-3 + Agents = [{<<"1">>, [<<"EN">>]} + ,{<<"2">>, [<<"EN">>, <<"SP">>, <<"RF">>]} + ,{<<"3">>, [<<"EN">>]} + ], + ExpectSkillMap = #{[] => sets:from_list([<<"3">>, <<"2">>, <<"1">>]) + ,[<<"EN">>] => sets:from_list([<<"3">>, <<"2">>, <<"1">>]) + ,[<<"EN">>, <<"RF">>] => sets:from_list([<<"2">>]) + ,[<<"EN">>, <<"RF">>, <<"SP">>] => sets:from_list([<<"2">>]) + ,[<<"EN">>, <<"SP">>] => sets:from_list([<<"2">>]) + ,[<<"RF">>] => sets:from_list([<<"2">>]) + ,[<<"RF">>, <<"SP">>] => sets:from_list([<<"2">>]) + ,[<<"SP">>] => sets:from_list([<<"2">>]) + }, + S1 = #state{strategy_state=#strategy_state{agents=#{rr_queue := RRQueue + ,skill_map := SkillMap + }}} = lists:foldl(fun add_agent/2, S, Agents), + + %% Add calls 1-5 + Calls = [{<<"1">>, [<<"EN">>, <<"RF">>]} + ,{<<"2">>, [<<"EN">>]} + ,{<<"3">>, [<<"SP">>, <<"RF">>]} + ,{<<"4">>, [<<"EN">>]} + ,{<<"5">>, [<<"EN">>]} + ], + ExpectAgentIdMap = #{<<"1">> => <<"2">> + ,<<"2">> => <<"1">> + ,<<"3">> => <<"4">> + }, + ExpectCallIdMap = #{<<"1">> => <<"2">> + ,<<"2">> => <<"1">> + ,<<"4">> => <<"3">> + }, + S2 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap + ,call_id_map := CallIdMap + }}} = lists:foldl(fun add_call/2, S1, Calls), + + %% Remove agent 2 + ExpectAgentIdMap1 = #{<<"1">> => <<"2">> + ,<<"3">> => <<"4">> + }, + ExpectCallIdMap1 = #{<<"2">> => <<"1">> + ,<<"4">> => <<"3">> + }, + ExpectSkillMap1 = #{[] => sets:from_list([<<"3">>, <<"1">>]) + ,[<<"EN">>] => sets:from_list([<<"3">>, <<"1">>]) + }, + S3 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap1 + ,call_id_map := CallIdMap1 + ,rr_queue := RRQueue1 + ,skill_map := SkillMap1 + }}} = remove_agent(<<"2">>, S2), + + %% Re-add agent 1 (shift to back of rr_queue) + ExpectAgentIdMap2 = #{<<"1">> => <<"4">> + ,<<"3">> => <<"2">> + }, + ExpectCallIdMap2 = #{<<"2">> => <<"3">> + ,<<"4">> => <<"1">> + }, + ExpectSkillMap2 = #{[] => sets:from_list([<<"3">>, <<"1">>]) + ,[<<"EN">>] => sets:from_list([<<"3">>, <<"1">>]) + }, + S4 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap2 + ,call_id_map := CallIdMap2 + ,rr_queue := RRQueue2 + ,skill_map := SkillMap2 + }}} = add_agent({<<"1">>, [<<"EN">>]}, S3), + + %% Remove calls 1, 2, 4 + RemoveCalls = [<<"1">>, <<"2">>, <<"4">>], + ExpectAgentIdMap3 = #{<<"3">> => <<"5">>}, + ExpectCallIdMap3 = #{<<"5">> => <<"3">>}, + S5 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap3 + ,call_id_map := CallIdMap3 + }}} = lists:foldl(fun remove_call/2, S4, RemoveCalls), + + %% Add calls 6, 7 + Calls1 = [{<<"6">>, [<<"DE">>]} + ,{<<"7">>, [<<"EN">>, <<"RF">>]} + ], + ExpectAgentIdMap4 = #{<<"3">> => <<"5">>}, + ExpectCallIdMap4 = #{<<"5">> => <<"3">>}, + S6 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap4 + ,call_id_map := CallIdMap4 + }}} = lists:foldl(fun add_call/2, S5, Calls1), + + %% Remove agent 3 + ExpectAgentIdMap5 = #{<<"1">> => <<"5">>}, + ExpectCallIdMap5 = #{<<"5">> => <<"1">>}, + ExpectSkillMap5 = #{[] => sets:from_list([<<"1">>]) + ,[<<"EN">>] => sets:from_list([<<"1">>]) + }, + S7 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap5 + ,call_id_map := CallIdMap5 + ,rr_queue := RRQueue5 + ,skill_map := SkillMap5 + }}} = remove_agent(<<"3">>, S6), + + %% Add agents 2, 4 (different skills this time) + Agents1 = [{<<"2">>, [<<"EN">>, <<"SP">>, <<"RF">>]} + ,{<<"4">>, [<<"EN">>, <<"DE">>, <<"RF">>]} + ], + ExpectAgentIdMap6 = #{<<"1">> => <<"5">> + ,<<"2">> => <<"3">> + ,<<"4">> => <<"6">> + }, + ExpectCallIdMap6 = #{<<"3">> => <<"2">> + ,<<"5">> => <<"1">> + ,<<"6">> => <<"4">> + }, + ExpectSkillMap6 = #{[] => sets:from_list([<<"4">>, <<"2">>, <<"1">>]) + ,[<<"DE">>] => sets:from_list([<<"4">>]) + ,[<<"DE">>, <<"EN">>] => sets:from_list([<<"4">>]) + ,[<<"DE">>, <<"EN">>, <<"RF">>] => sets:from_list([<<"4">>]) + ,[<<"DE">>, <<"RF">>] => sets:from_list([<<"4">>]) + ,[<<"EN">>] => sets:from_list([<<"4">>, <<"2">>, <<"1">>]) + ,[<<"EN">>, <<"RF">>] => sets:from_list([<<"4">>, <<"2">>]) + ,[<<"EN">>, <<"RF">>, <<"SP">>] => sets:from_list([<<"2">>]) + ,[<<"EN">>, <<"SP">>] => sets:from_list([<<"2">>]) + ,[<<"RF">>] => sets:from_list([<<"4">>,<<"2">>]) + ,[<<"RF">>, <<"SP">>] => sets:from_list([<<"2">>]) + ,[<<"SP">>] => sets:from_list([<<"2">>]) + }, + S8 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap6 + ,call_id_map := CallIdMap6 + ,rr_queue := RRQueue6 + ,skill_map := SkillMap6 + }}} = lists:foldl(fun add_agent/2, S7, Agents1), + + %% Remove calls 3, 5, 6 + RemoveCalls1 = [<<"3">>, <<"5">>, <<"6">>], + ExpectAgentIdMap7 = #{<<"2">> => <<"7">>}, + ExpectCallIdMap7 = #{<<"7">> => <<"2">>}, + S9 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap7 + ,call_id_map := CallIdMap7 + }}} = lists:foldl(fun remove_call/2, S8, RemoveCalls1), + + %% Call 7 exits, returns with different skills + ExpectAgentIdMap8 = #{<<"1">> => <<"7">>}, + ExpectCallIdMap8 = #{<<"7">> => <<"1">>}, + S10 = #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap8 + ,call_id_map := CallIdMap8 + }}} = add_call({<<"7">>, [<<"EN">>]}, remove_call(<<"7">>, S9)), + + %% Add call 8 + ExpectAgentIdMap9 = #{<<"1">> => <<"7">> + ,<<"4">> => <<"8">> + }, + ExpectCallIdMap9 = #{<<"7">> => <<"1">> + ,<<"8">> => <<"4">> + }, + #state{strategy_state=#strategy_state{agents=#{agent_id_map := AgentIdMap9 + ,call_id_map := CallIdMap9 + }}} = add_call({<<"8">>, [<<"DE">>]}, S10), + + [?_assertEqual([<<"1">>, <<"2">>, <<"3">>], pqueue4:to_list(RRQueue)) + ,?_assertEqual(ExpectSkillMap, SkillMap) + ,?_assertEqual(ExpectAgentIdMap, AgentIdMap) + ,?_assertEqual(ExpectCallIdMap, CallIdMap) + ,?_assertEqual([<<"1">>, <<"3">>], pqueue4:to_list(RRQueue1)) + ,?_assertEqual(ExpectAgentIdMap1, AgentIdMap1) + ,?_assertEqual(ExpectCallIdMap1, CallIdMap1) + ,?_assertEqual(ExpectSkillMap1, SkillMap1) + ,?_assertEqual([<<"3">>, <<"1">>], pqueue4:to_list(RRQueue2)) + ,?_assertEqual(ExpectAgentIdMap2, AgentIdMap2) + ,?_assertEqual(ExpectCallIdMap2, CallIdMap2) + ,?_assertEqual(ExpectSkillMap2, SkillMap2) + ,?_assertEqual(ExpectAgentIdMap3, AgentIdMap3) + ,?_assertEqual(ExpectCallIdMap3, CallIdMap3) + ,?_assertEqual(ExpectAgentIdMap4, AgentIdMap4) + ,?_assertEqual(ExpectCallIdMap4, CallIdMap4) + ,?_assertEqual([<<"1">>], pqueue4:to_list(RRQueue5)) + ,?_assertEqual(ExpectAgentIdMap5, AgentIdMap5) + ,?_assertEqual(ExpectCallIdMap5, CallIdMap5) + ,?_assertEqual(ExpectSkillMap5, SkillMap5) + ,?_assertEqual([<<"1">>, <<"2">>, <<"4">>], pqueue4:to_list(RRQueue6)) + ,?_assertEqual(ExpectAgentIdMap6, AgentIdMap6) + ,?_assertEqual(ExpectCallIdMap6, CallIdMap6) + ,?_assertEqual(ExpectSkillMap6, SkillMap6) + ,?_assertEqual(ExpectAgentIdMap7, AgentIdMap7) + ,?_assertEqual(ExpectCallIdMap7, CallIdMap7) + ,?_assertEqual(ExpectAgentIdMap8, AgentIdMap8) + ,?_assertEqual(ExpectCallIdMap8, CallIdMap8) + ,?_assertEqual(ExpectAgentIdMap9, AgentIdMap9) + ,?_assertEqual(ExpectCallIdMap9, CallIdMap9) + ]. + +%%-------------------------------------------------------------------- +%% @private +%% @doc Runs a load test on the sbrr strategy with a given number of +%% agents, calls, and unique skills. Each agent and call has a random +%% assortment of skills assigned to it. +%% @end +%%-------------------------------------------------------------------- +sbrr_load_test_() -> + sbrr_load_test(?SBRR_LOAD_TEST_ENABLED). + +sbrr_load_test('false') -> []; +sbrr_load_test('true') -> + NumAgents = 1000, + NumCalls = 500, + NumSkills = 10, + S = create_state(), + + %% Generate skills + Start = kz_time:now(), + SkillPool = apply_n({fun(Skills) -> + Skill = <<"skill", (kz_term:to_binary(length(Skills)))/binary>>, + [Skill | Skills] + end, []} + ,[] + ,NumSkills + ), + ?debugFmt("skill generation time: ~b", [kz_time:elapsed_ms(Start)]), + + %% Add agents + Start1 = kz_time:now(), + AgentPool = apply_n({fun(Agents) -> + AgentId = <<"agent", (kz_term:to_binary(length(Agents)))/binary>>, + Skills = lists:filter(fun(_) -> + rand:uniform(2) > 1 + end + ,SkillPool + ), + [{AgentId, Skills} | Agents] + end, []} + ,[] + ,NumAgents + ), + ?debugFmt("agent generation time: ~b", [kz_time:elapsed_ms(Start1)]), + Start2 = kz_time:now(), + S1 = lists:foldl(fun add_agent/2, S, AgentPool), + ?debugFmt("agent average add time: ~f", [kz_time:elapsed_ms(Start2) / NumAgents]), + + %% Add calls + Start3 = kz_time:now(), + CallPool = apply_n({fun(Calls) -> + CallId = <<"call", (kz_term:to_binary(length(Calls)))/binary>>, + Skills = lists:filter(fun(_) -> + rand:uniform(2) > 1 + end + ,SkillPool + ), + [{CallId, Skills} | Calls] + end, []} + ,[] + ,NumCalls + ), + ?debugFmt("call generation time: ~b", [kz_time:elapsed_ms(Start3)]), + Start4 = kz_time:now(), + lists:foldl(fun add_call/2, S1, CallPool), + ?debugFmt("call average add/assign time: ~f", [kz_time:elapsed_ms(Start4) / NumCalls]), + + []. + +apply_n(_, State, 0) -> State; +apply_n({Fun, Args}, State, N) -> + apply_n({Fun, Args}, apply(Fun, [State | Args]), N-1). + +%%-------------------------------------------------------------------- +%% @private +%% @doc Create a state for skills-based round robin tests. +%% @end +%%-------------------------------------------------------------------- +create_state() -> + SS = #strategy_state{agents=#{agent_id_map => #{} + ,call_id_map => #{} + ,rr_queue => pqueue4:new() + ,skill_map => #{} + } + }, + {'state', dict:new(), ?ACCOUNT_ID, ?QUEUE_ID, 'undefined', 'sbrr', SS, 'true', 'undefined', [], [], #{}, []}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc Add an agent with specified skills to an sbrr queue state. +%% @end +%%-------------------------------------------------------------------- +add_agent({AgentId, Skills}, State) -> + SS = acdc_queue_manager:update_strategy_with_agent(State, AgentId, 0, Skills, 'add', 'undefined'), + State#state{strategy_state=SS}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc Remove an agent from an sbrr queue state. +%% @end +%%-------------------------------------------------------------------- +remove_agent(AgentId, State) -> + SS = acdc_queue_manager:update_strategy_with_agent(State, AgentId, 0, [], 'remove', 'busy'), + State#state{strategy_state=SS}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc Add a call with specified required skills to an sbrr queue state. +%% @end +%%-------------------------------------------------------------------- +add_call({CallId, Skills}, #state{current_member_calls=Calls}=State) -> + %% Sort skills because we aren't going through add_queue_member which would + %% sort for us + Skills1 = lists:sort(Skills), + Call = kapps_call:set_call_id(CallId, kapps_call:kvs_store(?ACDC_REQUIRED_SKILLS_KEY, Skills1, kapps_call:new())), + Calls1 = Calls ++ [{0, Call}], + State1 = #state{strategy_state=#strategy_state{agents=SBRRSS}=SS} = State#state{current_member_calls=Calls1}, + SBRRSS1 = acdc_queue_manager:reseed_sbrrss_maps(SBRRSS, acdc_queue_manager:ss_size('sbrr', SS, 'free'), Calls1), + State1#state{strategy_state=SS#strategy_state{agents=SBRRSS1}}. + +%%-------------------------------------------------------------------- +%% @private +%% @doc Remove a call from an sbrr queue state. +%% @end +%%-------------------------------------------------------------------- +remove_call(CallId, #state{current_member_calls=Calls}=State) -> + Calls1 = lists:filter(fun({_, Call1}) -> + kapps_call:call_id(Call1) =/= CallId + end, Calls), + State1 = #state{strategy_state=#strategy_state{agents=SBRRSS}=SS} = State#state{current_member_calls=Calls1}, + SBRRSS1 = acdc_queue_manager:reseed_sbrrss_maps(SBRRSS, acdc_queue_manager:ss_size('sbrr', SS, 'free'), Calls1), + State1#state{strategy_state=SS#strategy_state{agents=SBRRSS1}}. diff --git a/applications/acdc/test/acdc_queue_manager_tests.erl b/applications/acdc/test/acdc_queue_manager_tests.erl deleted file mode 100644 index 2989b3dab14..00000000000 --- a/applications/acdc/test/acdc_queue_manager_tests.erl +++ /dev/null @@ -1,39 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @copyright (C) 2016, Voxter Communications Inc. -%%% @doc -%%% @author Daniel Finke -%%% -%%% This Source Code Form is subject to the terms of the Mozilla Public -%%% License, v. 2.0. If a copy of the MPL was not distributed with this -%%% file, You can obtain one at https://mozilla.org/MPL/2.0/. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(acdc_queue_manager_tests). - --include_lib("eunit/include/eunit.hrl"). - --include("../src/acdc.hrl"). --include("../src/acdc_queue_manager.hrl"). - --define(AGENT_ID, <<"agent_id">>). - -%%% ===== -%%% TESTS -%%% ===== - -%%------------------------------------------------------------------------------ -%% @doc -%% @end -%%------------------------------------------------------------------------------ -ss_size_empty_test_() -> - SS = #strategy_state{agents=[]}, - [?_assertEqual(0, acdc_queue_manager:ss_size(SS, 'free')) - ,?_assertEqual(0, acdc_queue_manager:ss_size(SS, 'logged_in'))]. - -ss_size_one_busy_test_() -> - SS = #strategy_state{agents=[]}, - SS1 = acdc_queue_manager:update_strategy_with_agent('mi', SS, ?AGENT_ID, 'add', 'undefined'), - SS2 = acdc_queue_manager:update_strategy_with_agent('mi', SS1, ?AGENT_ID, 'remove', 'busy'), - [?_assertEqual(0, acdc_queue_manager:ss_size(SS2, 'free')) - ,?_assertEqual(1, acdc_queue_manager:ss_size(SS2, 'logged_in'))]. diff --git a/applications/crossbar/doc/queues.md b/applications/crossbar/doc/queues.md index 4187c6fcd7e..1c9fa4b38fe 100644 --- a/applications/crossbar/doc/queues.md +++ b/applications/crossbar/doc/queues.md @@ -35,7 +35,7 @@ Key | Description | Type | Default | Required | Support Level `record_caller` | When enabled, a caller's audio will be recorded | `boolean()` | `false` | `false` | `recording_url` | An optional HTTP URL to PUT the call recording after the call ends (and should respond to GET for retrieving the audio data) | `string()` | | `false` | `ring_simultaneously` | The number of agents to try in parallel when connecting a caller | `integer()` | `1` | `false` | -`strategy` | The queue strategy for connecting agents to callers | `string('round_robin' | 'most_idle')` | `round_robin` | `false` | +`strategy` | The queue strategy for connecting agents to callers | `string('round_robin' | 'most_idle' | 'skills_based_round_robin' | 'ring_all')` | `round_robin` | `false` | diff --git a/applications/crossbar/doc/ref/agents.md b/applications/crossbar/doc/ref/agents.md index a93db02b4a1..003c21bf6b2 100644 --- a/applications/crossbar/doc/ref/agents.md +++ b/applications/crossbar/doc/ref/agents.md @@ -28,6 +28,16 @@ curl -v -X GET \ ## Fetch +> GET /v2/accounts/{ACCOUNT_ID}/agents/stats_summary + +```shell +curl -v -X GET \ + -H "X-Auth-Token: {AUTH_TOKEN}" \ + http://{SERVER}:8000/v2/accounts/{ACCOUNT_ID}/agents/stats_summary +``` + +## Fetch + > GET /v2/accounts/{ACCOUNT_ID}/agents/stats ```shell @@ -46,6 +56,16 @@ curl -v -X GET \ http://{SERVER}:8000/v2/accounts/{ACCOUNT_ID}/agents/status ``` +## Change + +> POST /v2/accounts/{ACCOUNT_ID}/agents/{USER_ID}/restart + +```shell +curl -v -X POST \ + -H "X-Auth-Token: {AUTH_TOKEN}" \ + http://{SERVER}:8000/v2/accounts/{ACCOUNT_ID}/agents/{USER_ID}/restart +``` + ## Fetch > GET /v2/accounts/{ACCOUNT_ID}/agents/{USER_ID}/queue_status diff --git a/applications/crossbar/doc/ref/queues.md b/applications/crossbar/doc/ref/queues.md index cebba5a8896..f2d02d2fe0a 100644 --- a/applications/crossbar/doc/ref/queues.md +++ b/applications/crossbar/doc/ref/queues.md @@ -33,7 +33,7 @@ Key | Description | Type | Default | Required | Support Level `record_caller` | When enabled, a caller's audio will be recorded | `boolean()` | `false` | `false` | `recording_url` | An optional HTTP URL to PUT the call recording after the call ends (and should respond to GET for retrieving the audio data) | `string()` | | `false` | `ring_simultaneously` | The number of agents to try in parallel when connecting a caller | `integer()` | `1` | `false` | -`strategy` | The queue strategy for connecting agents to callers | `string('round_robin' | 'most_idle')` | `round_robin` | `false` | +`strategy` | The queue strategy for connecting agents to callers | `string('round_robin' | 'most_idle' | 'skills_based_round_robin' | 'ring_all')` | `round_robin` | `false` | @@ -109,6 +109,16 @@ curl -v -X PUT \ ## Fetch +> GET /v2/accounts/{ACCOUNT_ID}/queues/stats_summary + +```shell +curl -v -X GET \ + -H "X-Auth-Token: {AUTH_TOKEN}" \ + http://{SERVER}:8000/v2/accounts/{ACCOUNT_ID}/queues/stats_summary +``` + +## Fetch + > GET /v2/accounts/{ACCOUNT_ID}/queues/stats ```shell @@ -129,6 +139,16 @@ curl -v -X PUT \ ## Fetch +> GET /v2/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/stats_summary + +```shell +curl -v -X GET \ + -H "X-Auth-Token: {AUTH_TOKEN}" \ + http://{SERVER}:8000/v2/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/stats_summary +``` + +## Fetch + > GET /v2/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/roster ```shell diff --git a/applications/crossbar/doc/users.md b/applications/crossbar/doc/users.md index 23dad0a79b6..71ce9939254 100644 --- a/applications/crossbar/doc/users.md +++ b/applications/crossbar/doc/users.md @@ -23,6 +23,7 @@ Key | Description | Type | Default | Required | Support Level `call_forward` | The device call forward parameters | `object()` | | `false` | `call_recording` | endpoint recording settings | [#/definitions/call_recording](#call_recording) | | `false` | `call_restriction` | Device level call restrictions for each available number classification | `object()` | `{}` | `false` | +`call_waiting` | Parameters for server-side call waiting | [#/definitions/call_waiting](#call_waiting) | | `false` | `caller_id` | The device caller ID parameters | [#/definitions/caller_id](#caller_id) | | `false` | `caller_id_options.outbound_privacy` | Determines what appears as caller id for offnet outbound calls. Values: full - hides name and number; name - hides only name; number - hides only number; none - hides nothing | `string('full' | 'name' | 'number' | 'none')` | | `false` | `caller_id_options` | custom properties for configuring caller_id | `object()` | | `false` | @@ -103,6 +104,15 @@ Key | Description | Type | Default | Required | Support Level `offnet` | settings for calls from offnet networks | [#/definitions/call_recording.parameters](#call_recordingparameters) | | `false` | `onnet` | settings for calls from onnet networks | [#/definitions/call_recording.parameters](#call_recordingparameters) | | `false` | +### call_waiting + +Parameters for server-side call waiting + + +Key | Description | Type | Default | Required | Support Level +--- | ----------- | ---- | ------- | -------- | ------------- +`enabled` | Determines if server side call waiting is enabled/disabled | `boolean()` | | `false` | + ### caller_id Defines caller ID settings based on the type of call being made diff --git a/applications/crossbar/priv/api/swagger.json b/applications/crossbar/priv/api/swagger.json index f2925f5c6dd..36cb33dbc40 100644 --- a/applications/crossbar/priv/api/swagger.json +++ b/applications/crossbar/priv/api/swagger.json @@ -6949,14 +6949,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "end_wrapup" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" @@ -6997,14 +6995,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "login" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" @@ -7045,14 +7041,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "login_queue" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" @@ -7087,19 +7081,17 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "login_resp" - ], - "type": "string" + ] }, "Status": { "enum": [ - "success", - "failed" + "failed", + "success" ], "type": "string" } @@ -7121,14 +7113,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "logout" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" @@ -7169,14 +7159,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "logout_queue" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" @@ -7214,18 +7202,65 @@ "Agent-ID": { "type": "string" }, + "Alias": { + "type": "string" + }, "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "pause" + ] + }, + "Presence-ID": { + "type": "string" + }, + "Presence-State": { + "enum": [ + "confirmed", + "early", + "offline", + "online", + "terminated", + "trying" ], "type": "string" }, + "Queue-ID": { + "type": "string" + }, + "Time-Limit": { + "type": "integer" + } + }, + "required": [ + "Account-ID", + "Agent-ID" + ], + "type": "object" + }, + "kapi.acdc_agent.restart": { + "description": "AMQP API for acdc_agent.restart", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "agent" + ] + }, + "Event-Name": { + "enum": [ + "restart" + ] + }, "Presence-ID": { "type": "string" }, @@ -7265,14 +7300,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "resume" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" @@ -7301,26 +7334,91 @@ ], "type": "object" }, - "kapi.acdc_agent.stats_req": { - "description": "AMQP API for acdc_agent.stats_req", + "kapi.acdc_agent.shared_call_id": { + "description": "AMQP API for acdc_agent.shared_call_id", "properties": { "Account-ID": { "type": "string" }, + "Agent-Call-ID": { + "type": "string" + }, "Agent-ID": { "type": "string" }, "Event-Category": { "enum": [ "agent" + ] + }, + "Event-Name": { + "enum": [ + "shared_call_id" + ] + }, + "Member-Call-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Agent-ID" + ], + "type": "object" + }, + "kapi.acdc_agent.shared_originate_failure": { + "description": "AMQP API for acdc_agent.shared_originate_failure", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Blame": { + "enum": [ + "member" ], "type": "string" }, + "Event-Category": { + "enum": [ + "agent" + ] + }, "Event-Name": { "enum": [ - "stats_req" - ], + "shared_failure" + ] + } + }, + "required": [ + "Account-ID", + "Agent-ID" + ], + "type": "object" + }, + "kapi.acdc_agent.stats_req": { + "description": "AMQP API for acdc_agent.stats_req", + "properties": { + "Account-ID": { "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "agent" + ] + }, + "Event-Name": { + "enum": [ + "stats_req" + ] } }, "required": [ @@ -7334,6 +7432,9 @@ "Account-ID": { "type": "string" }, + "Agent-Call-IDs": { + "type": "string" + }, "Current-Calls": { "type": "string" }, @@ -7346,14 +7447,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "stats_resp" - ], - "type": "string" + ] }, "Stats": { "type": "object" @@ -7376,14 +7475,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_req" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -7410,28 +7507,27 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_resp" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" }, "Status": { "enum": [ - "init", - "sync", + "answered", + "awaiting_callback", + "outbound", + "paused", "ready", - "waiting", "ringing", - "answered", - "wrapup", - "paused" + "ringing_callback", + "sync", + "wrapup" ], "type": "string" }, @@ -7455,11 +7551,26 @@ "Agent-ID": { "type": "string" }, + "Call-Direction": { + "type": "string" + }, + "Callee-ID-Name": { + "type": "string" + }, + "Callee-ID-Number": { + "type": "string" + }, + "Caller-ID-Name": { + "type": "string" + }, + "Caller-ID-Number": { + "type": "string" + }, "Change": { "enum": [ "available", - "ringing", "busy", + "ringing", "unavailable" ], "type": "string" @@ -7467,13 +7578,14 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "agent_change" - ], + ] + }, + "Priority": { "type": "string" }, "Process-ID": { @@ -7481,6 +7593,9 @@ }, "Queue-ID": { "type": "string" + }, + "Skills": { + "type": "string" } }, "required": [ @@ -7494,8 +7609,8 @@ "kapi.acdc_queue.agent_timeout": { "description": "AMQP API for acdc_queue.agent_timeout", "properties": { - "Agent-Process-IDs": { - "type": "array" + "Agent-Process-ID": { + "type": "string" }, "Call-ID": { "type": "string" @@ -7503,14 +7618,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_timeout" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" @@ -7522,6 +7635,68 @@ ], "type": "object" }, + "kapi.acdc_queue.agents_available_req": { + "description": "AMQP API for acdc_queue.agents_available_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "queue" + ] + }, + "Event-Name": { + "enum": [ + "agents_available_req" + ] + }, + "Queue-ID": { + "type": "string" + }, + "Skills": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "Account-ID", + "Queue-ID" + ], + "type": "object" + }, + "kapi.acdc_queue.agents_available_resp": { + "description": "AMQP API for acdc_queue.agents_available_resp", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-Count": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "queue" + ] + }, + "Event-Name": { + "enum": [ + "agents_available_resp" + ] + }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Agent-Count", + "Queue-ID" + ], + "type": "object" + }, "kapi.acdc_queue.member_call": { "description": "AMQP API for acdc_queue.member_call", "properties": { @@ -7531,17 +7706,21 @@ "Call": { "type": "object" }, + "Callback-Number": { + "type": "string" + }, + "Enter-As-Callback": { + "type": "boolean" + }, "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call" - ], - "type": "string" + ] }, "Member-Priority": { "type": "integer" @@ -7569,14 +7748,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call_cancel" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" @@ -7607,14 +7784,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call_fail" - ], - "type": "string" + ] }, "Failure-Reason": { "type": "string" @@ -7648,17 +7823,81 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call_success" - ], + ] + }, + "Process-ID": { "type": "string" }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Queue-ID" + ], + "type": "object" + }, + "kapi.acdc_queue.member_callback_accepted": { + "description": "AMQP API for acdc_queue.member_callback_accepted", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "member" + ] + }, + "Event-Name": { + "enum": [ + "callback_accepted" + ] + }, "Process-ID": { "type": "string" + } + }, + "required": [ + "Account-ID", + "Agent-ID", + "Call-ID", + "Process-ID" + ], + "type": "object" + }, + "kapi.acdc_queue.member_callback_reg": { + "description": "AMQP API for acdc_queue.member_callback_reg", + "properties": { + "Account-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "member" + ] + }, + "Event-Name": { + "enum": [ + "callback_reg" + ] + }, + "Number": { + "type": "string" }, "Queue-ID": { "type": "string" @@ -7666,6 +7905,8 @@ }, "required": [ "Account-ID", + "Call-ID", + "Number", "Queue-ID" ], "type": "object" @@ -7685,13 +7926,14 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_accepted" - ], + ] + }, + "Old-Call-ID": { "type": "string" }, "Process-ID": { @@ -7715,14 +7957,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_req" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -7747,14 +7987,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_resp" - ], - "type": "string" + ] }, "Idle-Time": { "type": "integer" @@ -7780,14 +8018,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_retry" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -7801,8 +8037,14 @@ "kapi.acdc_queue.member_connect_satisfied": { "description": "AMQP API for acdc_queue.member_connect_satisfied", "properties": { + "Accept-Agent-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, "Agent-Process-IDs": { - "type": "array" + "type": "string" }, "Call": { "type": "object" @@ -7810,14 +8052,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_satisfied" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -7827,6 +8067,8 @@ } }, "required": [ + "Accept-Agent-ID", + "Agent-ID", "Call", "Queue-ID" ], @@ -7836,7 +8078,7 @@ "description": "AMQP API for acdc_queue.member_connect_win", "properties": { "Agent-Process-IDs": { - "type": "array" + "type": "string" }, "CDR-Url": { "type": "string" @@ -7844,20 +8086,21 @@ "Call": { "type": "object" }, + "Callback-Details": { + "type": "string" + }, "Caller-Exit-Key": { "type": "string" }, "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_win" - ], - "type": "string" + ] }, "Notifications": { "type": "object" @@ -7896,14 +8139,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "hungup" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -7923,17 +8164,24 @@ "Call": { "type": "object" }, + "Callback-Number": { + "type": "string" + }, + "Enter-As-Callback": { + "type": "boolean" + }, "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "member_add" - ], - "type": "string" + ] + }, + "Member-Priority": { + "type": "integer" }, "Queue-ID": { "type": "string" @@ -7942,6 +8190,7 @@ "required": [ "Account-ID", "Call", + "Enter-As-Callback", "Queue-ID" ], "type": "object" @@ -7958,14 +8207,12 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "member_remove" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" @@ -7987,14 +8234,12 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_req" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -8021,14 +8266,12 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_resp" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -8047,6 +8290,159 @@ ], "type": "object" }, + "kapi.acdc_stats.agent_calls_err": { + "description": "AMQP API for acdc_stats.agent_calls_err", + "properties": { + "Error-Reason": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_calls_err" + ] + } + }, + "required": [ + "Error-Reason" + ], + "type": "object" + }, + "kapi.acdc_stats.agent_calls_req": { + "description": "AMQP API for acdc_stats.agent_calls_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "End-Range": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_calls_req" + ] + }, + "Queue-ID": { + "type": "string" + }, + "Start-Range": { + "type": "string" + }, + "Status": { + "type": "string" + } + }, + "required": [ + "Account-ID" + ], + "type": "object" + }, + "kapi.acdc_stats.agent_calls_resp": { + "description": "AMQP API for acdc_stats.agent_calls_resp", + "properties": { + "Data": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_calls_resp" + ] + }, + "Query-Time": { + "type": "integer" + } + }, + "required": [ + "Query-Time" + ], + "type": "object" + }, + "kapi.acdc_stats.agent_cur_status_err": { + "description": "AMQP API for acdc_stats.agent_cur_status_err", + "properties": { + "Error-Reason": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_cur_status_err" + ] + } + }, + "required": [ + "Error-Reason" + ], + "type": "object" + }, + "kapi.acdc_stats.agent_cur_status_req": { + "description": "AMQP API for acdc_stats.agent_cur_status_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_cur_status_req" + ] + } + }, + "required": [ + "Account-ID" + ], + "type": "object" + }, + "kapi.acdc_stats.agent_cur_status_resp": { + "description": "AMQP API for acdc_stats.agent_cur_status_resp", + "properties": { + "Agents": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_cur_status_resp" + ] + } + }, + "required": [ + "Agents" + ], + "type": "object" + }, "kapi.acdc_stats.average_wait_time_err": { "description": "AMQP API for acdc_stats.average_wait_time_err", "properties": { @@ -8056,14 +8452,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "average_wait_time_err" - ], - "type": "string" + ] } }, "required": [ @@ -8081,19 +8475,23 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "average_wait_time_req" - ], - "type": "string" + ] }, "Queue-ID": { "minLength": 1, "type": "string" }, + "Skills": { + "items": { + "type": "string" + }, + "type": "array" + }, "Window": { "type": "integer" } @@ -8113,14 +8511,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "average_wait_time_resp" - ], - "type": "string" + ] } }, "required": [ @@ -8146,13 +8542,44 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "abandoned" - ], + ] + }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Call-ID", + "Queue-ID" + ], + "type": "object" + }, + "kapi.acdc_stats.call_exited_position": { + "description": "AMQP API for acdc_stats.call_exited_position", + "properties": { + "Account-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_call_stat" + ] + }, + "Event-Name": { + "enum": [ + "exited-position" + ] + }, + "Exited-Position": { "type": "string" }, "Queue-ID": { @@ -8178,14 +8605,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "flush" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" @@ -8213,14 +8638,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "handled" - ], - "type": "string" + ] }, "Handled-Timestamp": { "type": "string" @@ -8236,6 +8659,39 @@ ], "type": "object" }, + "kapi.acdc_stats.call_marked_callback": { + "description": "AMQP API for acdc_stats.call_marked_callback", + "properties": { + "Account-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Caller-ID-Name": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_call_stat" + ] + }, + "Event-Name": { + "enum": [ + "marked_callback" + ] + }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Call-ID", + "Queue-ID" + ], + "type": "object" + }, "kapi.acdc_stats.call_missed": { "description": "AMQP API for acdc_stats.call_missed", "properties": { @@ -8251,14 +8707,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "missed" - ], - "type": "string" + ] }, "Miss-Reason": { "type": "string" @@ -8292,14 +8746,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "processed" - ], - "type": "string" + ] }, "Hung-Up-By": { "type": "string" @@ -8318,6 +8770,108 @@ ], "type": "object" }, + "kapi.acdc_stats.call_summary_err": { + "description": "AMQP API for acdc_stats.call_summary_err", + "properties": { + "Error-Reason": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "call_summary_err" + ] + } + }, + "required": [ + "Error-Reason" + ], + "type": "object" + }, + "kapi.acdc_stats.call_summary_req": { + "description": "AMQP API for acdc_stats.call_summary_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "End-Range": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "call_summary_req" + ] + }, + "Queue-ID": { + "type": "string" + }, + "Start-Range": { + "type": "string" + }, + "Status": { + "type": "string" + } + }, + "required": [ + "Account-ID" + ], + "type": "object" + }, + "kapi.acdc_stats.call_summary_resp": { + "description": "AMQP API for acdc_stats.call_summary_resp", + "properties": { + "Abandoned": { + "type": "string" + }, + "Data": { + "type": "string" + }, + "Entered-Position": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "call_summary_resp" + ] + }, + "Exited-Position": { + "type": "string" + }, + "Handled": { + "type": "string" + }, + "Processed": { + "type": "string" + }, + "Query-Time": { + "type": "integer" + }, + "Waiting": { + "type": "string" + } + }, + "required": [ + "Query-Time" + ], + "type": "object" + }, "kapi.acdc_stats.call_waiting": { "description": "AMQP API for acdc_stats.call_waiting", "properties": { @@ -8336,23 +8890,30 @@ "Caller-Priority": { "type": "string" }, + "Entered-Position": { + "type": "string" + }, "Entered-Timestamp": { "type": "string" }, "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "waiting" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" + }, + "Required-Skills": { + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ @@ -8371,14 +8932,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "current_calls_err" - ], - "type": "string" + ] } }, "required": [ @@ -8401,14 +8960,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "current_calls_req" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" @@ -8431,16 +8988,20 @@ "Abandoned": { "type": "string" }, + "Entered-Position": { + "type": "string" + }, "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "current_calls_resp" - ], + ] + }, + "Exited-Position": { "type": "string" }, "Handled": { @@ -8482,21 +9043,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connected" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8532,21 +9091,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connecting" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8570,14 +9127,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "status_err" - ], - "type": "string" + ] } }, "required": [ @@ -8585,6 +9140,54 @@ ], "type": "object" }, + "kapi.acdc_stats.status_inbound": { + "description": "AMQP API for acdc_stats.status_inbound", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Caller-ID-Name": { + "type": "string" + }, + "Caller-ID-Number": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_status_stat" + ] + }, + "Event-Name": { + "enum": [ + "inbound" + ] + }, + "Pause-Alias": { + "type": "string" + }, + "Pause-Time": { + "type": "integer" + }, + "Timestamp": { + "type": "string" + }, + "Wait-Time": { + "type": "integer" + } + }, + "required": [ + "Account-ID", + "Agent-ID", + "Timestamp" + ], + "type": "object" + }, "kapi.acdc_stats.status_logged_in": { "description": "AMQP API for acdc_stats.status_logged_in", "properties": { @@ -8606,21 +9209,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "logged_in" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8656,21 +9257,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "logged_out" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8706,21 +9305,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "outbound" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8756,21 +9353,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "paused" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8806,21 +9401,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "pending_logged_out" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8856,21 +9449,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "ready" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -8900,14 +9491,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "status_req" - ], - "type": "string" + ] }, "Limit": { "type": "string" @@ -8933,14 +9522,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "status_resp" - ], - "type": "string" + ] } }, "required": [ @@ -8969,16 +9556,15 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": {}, + "Pause-Alias": { + "type": "string" + }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -9014,21 +9600,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "wrapup" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, @@ -32497,7 +33081,9 @@ "description": "The queue strategy for connecting agents to callers", "enum": [ "round_robin", - "most_idle" + "most_idle", + "skills_based_round_robin", + "ring_all" ], "type": "string" } @@ -43001,6 +43587,23 @@ } } }, + "/accounts/{ACCOUNT_ID}/agents/stats_summary": { + "get": { + "parameters": [ + { + "$ref": "#/parameters/auth_token_header" + }, + { + "$ref": "#/parameters/ACCOUNT_ID" + } + ], + "responses": { + "200": { + "description": "request succeeded" + } + } + } + }, "/accounts/{ACCOUNT_ID}/agents/status": { "get": { "parameters": [ @@ -43114,6 +43717,26 @@ } } }, + "/accounts/{ACCOUNT_ID}/agents/{USER_ID}/restart": { + "post": { + "parameters": [ + { + "$ref": "#/parameters/auth_token_header" + }, + { + "$ref": "#/parameters/ACCOUNT_ID" + }, + { + "$ref": "#/parameters/USER_ID" + } + ], + "responses": { + "200": { + "description": "request succeeded" + } + } + } + }, "/accounts/{ACCOUNT_ID}/agents/{USER_ID}/status": { "get": { "parameters": [ @@ -48889,6 +49512,23 @@ } } }, + "/accounts/{ACCOUNT_ID}/queues/stats_summary": { + "get": { + "parameters": [ + { + "$ref": "#/parameters/auth_token_header" + }, + { + "$ref": "#/parameters/ACCOUNT_ID" + } + ], + "responses": { + "200": { + "description": "request succeeded" + } + } + } + }, "/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}": { "delete": { "parameters": [ @@ -49063,6 +49703,26 @@ } } }, + "/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/stats_summary": { + "get": { + "parameters": [ + { + "$ref": "#/parameters/auth_token_header" + }, + { + "$ref": "#/parameters/ACCOUNT_ID" + }, + { + "$ref": "#/parameters/QUEUE_ID" + } + ], + "responses": { + "200": { + "description": "request succeeded" + } + } + } + }, "/accounts/{ACCOUNT_ID}/rate_limits": { "delete": { "parameters": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.end_wrapup.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.end_wrapup.json index bf043fbf054..a7bcf665226 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.end_wrapup.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.end_wrapup.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "end_wrapup" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.pause.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.pause.json index 15e2f37a7c8..f78e6942c5c 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.pause.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.pause.json @@ -9,17 +9,18 @@ "Agent-ID": { "type": "string" }, + "Alias": { + "type": "string" + }, "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "pause" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.restart.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.restart.json new file mode 100644 index 00000000000..43ab6ecb5c3 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.restart.json @@ -0,0 +1,48 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_agent.restart", + "description": "AMQP API for acdc_agent.restart", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "agent" + ] + }, + "Event-Name": { + "enum": [ + "restart" + ] + }, + "Presence-ID": { + "type": "string" + }, + "Presence-State": { + "enum": [ + "confirmed", + "early", + "offline", + "online", + "terminated", + "trying" + ], + "type": "string" + }, + "Queue-ID": { + "type": "string" + }, + "Time-Limit": { + "type": "integer" + } + }, + "required": [ + "Account-ID", + "Agent-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.resume.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.resume.json index 28a7dd99286..8ff48329819 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.resume.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.resume.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "resume" - ], - "type": "string" + ] }, "Presence-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.shared_call_id.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.shared_call_id.json new file mode 100644 index 00000000000..e4487ae0411 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.shared_call_id.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_agent.shared_call_id", + "description": "AMQP API for acdc_agent.shared_call_id", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-Call-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "agent" + ] + }, + "Event-Name": { + "enum": [ + "shared_call_id" + ] + }, + "Member-Call-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Agent-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.shared_originate_failure.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.shared_originate_failure.json new file mode 100644 index 00000000000..6f50a0b89ac --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.shared_originate_failure.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_agent.shared_originate_failure", + "description": "AMQP API for acdc_agent.shared_originate_failure", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Blame": { + "enum": [ + "member" + ], + "type": "string" + }, + "Event-Category": { + "enum": [ + "agent" + ] + }, + "Event-Name": { + "enum": [ + "shared_failure" + ] + } + }, + "required": [ + "Account-ID", + "Agent-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_req.json index 117e6a569f5..e5723f48f09 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_req.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_req.json @@ -9,17 +9,18 @@ "Agent-ID": { "type": "string" }, + "Call-ID": { + "type": "string" + }, "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "stats_req" - ], - "type": "string" + ] } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_resp.json index 41a5e3e342b..403db6b967a 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_resp.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.stats_resp.json @@ -6,6 +6,9 @@ "Account-ID": { "type": "string" }, + "Agent-Call-IDs": { + "type": "string" + }, "Current-Calls": { "type": "string" }, @@ -18,14 +21,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "stats_resp" - ], - "type": "string" + ] }, "Stats": { "type": "object" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_req.json index 7569e5ab414..ed4084e7485 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_req.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_req.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_req" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_resp.json index e955b5ae1f2..5efe9f11f06 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_resp.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.sync_resp.json @@ -15,28 +15,27 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_resp" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" }, "Status": { "enum": [ - "init", - "sync", + "answered", + "awaiting_callback", + "outbound", + "paused", "ready", - "waiting", "ringing", - "answered", - "wrapup", - "paused" + "ringing_callback", + "sync", + "wrapup" ], "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_change.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_change.json index 70b0a5364e4..42a1874cd17 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_change.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_change.json @@ -9,11 +9,26 @@ "Agent-ID": { "type": "string" }, + "Call-Direction": { + "type": "string" + }, + "Callee-ID-Name": { + "type": "string" + }, + "Callee-ID-Number": { + "type": "string" + }, + "Caller-ID-Name": { + "type": "string" + }, + "Caller-ID-Number": { + "type": "string" + }, "Change": { "enum": [ "available", - "ringing", "busy", + "ringing", "unavailable" ], "type": "string" @@ -21,13 +36,14 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "agent_change" - ], + ] + }, + "Priority": { "type": "string" }, "Process-ID": { @@ -35,6 +51,9 @@ }, "Queue-ID": { "type": "string" + }, + "Skills": { + "type": "string" } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_timeout.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_timeout.json index 9ddd8683c3e..d175b24619b 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_timeout.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agent_timeout.json @@ -3,8 +3,8 @@ "_id": "kapi.acdc_queue.agent_timeout", "description": "AMQP API for acdc_queue.agent_timeout", "properties": { - "Agent-Process-IDs": { - "type": "array" + "Agent-Process-ID": { + "type": "string" }, "Call-ID": { "type": "string" @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "agent" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_timeout" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agents_available_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agents_available_req.json new file mode 100644 index 00000000000..248d75b4a7f --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agents_available_req.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_queue.agents_available_req", + "description": "AMQP API for acdc_queue.agents_available_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "queue" + ] + }, + "Event-Name": { + "enum": [ + "agents_available_req" + ] + }, + "Queue-ID": { + "type": "string" + }, + "Skills": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "Account-ID", + "Queue-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agents_available_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agents_available_resp.json new file mode 100644 index 00000000000..9c96c2cd5ca --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.agents_available_resp.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_queue.agents_available_resp", + "description": "AMQP API for acdc_queue.agents_available_resp", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-Count": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "queue" + ] + }, + "Event-Name": { + "enum": [ + "agents_available_resp" + ] + }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Agent-Count", + "Queue-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call.json index 61bcc4599be..41226befdba 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call.json @@ -9,17 +9,21 @@ "Call": { "type": "object" }, + "Callback-Number": { + "type": "string" + }, + "Enter-As-Callback": { + "type": "boolean" + }, "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call" - ], - "type": "string" + ] }, "Member-Priority": { "type": "integer" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_cancel.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_cancel.json index e62ae7ed727..f05aad99373 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_cancel.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_cancel.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call_cancel" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_failure.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_failure.json index e89d003c6a3..b2394ecaa75 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_failure.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_failure.json @@ -15,14 +15,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call_fail" - ], - "type": "string" + ] }, "Failure-Reason": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_success.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_success.json index 580db8c8051..4ea8bb33920 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_success.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_call_success.json @@ -15,14 +15,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "call_success" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_callback_accepted.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_callback_accepted.json new file mode 100644 index 00000000000..5e472f2be10 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_callback_accepted.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_queue.member_callback_accepted", + "description": "AMQP API for acdc_queue.member_callback_accepted", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "member" + ] + }, + "Event-Name": { + "enum": [ + "callback_accepted" + ] + }, + "Process-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Agent-ID", + "Call-ID", + "Process-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_callback_reg.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_callback_reg.json new file mode 100644 index 00000000000..a2da6554345 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_callback_reg.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_queue.member_callback_reg", + "description": "AMQP API for acdc_queue.member_callback_reg", + "properties": { + "Account-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "member" + ] + }, + "Event-Name": { + "enum": [ + "callback_reg" + ] + }, + "Number": { + "type": "string" + }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Call-ID", + "Number", + "Queue-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_accepted.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_accepted.json index dbc4f51429c..df5942f3852 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_accepted.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_accepted.json @@ -15,13 +15,14 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_accepted" - ], + ] + }, + "Old-Call-ID": { "type": "string" }, "Process-ID": { diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_req.json index bc1ec1dc843..9225a42cea5 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_req.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_req.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_req" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_resp.json index 98cf705c8f6..b106e659567 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_resp.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_resp.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_resp" - ], - "type": "string" + ] }, "Idle-Time": { "type": "integer" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_retry.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_retry.json index f684b71ea4e..70348146af6 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_retry.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_retry.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_retry" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_satisfied.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_satisfied.json index 9191ad1fab0..e74b9a062db 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_satisfied.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_satisfied.json @@ -3,8 +3,14 @@ "_id": "kapi.acdc_queue.member_connect_satisfied", "description": "AMQP API for acdc_queue.member_connect_satisfied", "properties": { + "Accept-Agent-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, "Agent-Process-IDs": { - "type": "array" + "type": "string" }, "Call": { "type": "object" @@ -12,14 +18,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_satisfied" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" @@ -29,6 +33,8 @@ } }, "required": [ + "Accept-Agent-ID", + "Agent-ID", "Call", "Queue-ID" ], diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_win.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_win.json index b78f881cd2e..d6f54fcfad4 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_win.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_connect_win.json @@ -4,7 +4,7 @@ "description": "AMQP API for acdc_queue.member_connect_win", "properties": { "Agent-Process-IDs": { - "type": "array" + "type": "string" }, "CDR-Url": { "type": "string" @@ -12,20 +12,21 @@ "Call": { "type": "object" }, + "Callback-Details": { + "type": "string" + }, "Caller-Exit-Key": { "type": "string" }, "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connect_win" - ], - "type": "string" + ] }, "Notifications": { "type": "object" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_hungup.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_hungup.json index 89f19bb2687..c0263e59ffb 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_hungup.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.member_hungup.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "member" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "hungup" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_add.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_add.json index c1bbe805149..3111111fe27 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_add.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_add.json @@ -9,17 +9,24 @@ "Call": { "type": "object" }, + "Callback-Number": { + "type": "string" + }, + "Enter-As-Callback": { + "type": "boolean" + }, "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "member_add" - ], - "type": "string" + ] + }, + "Member-Priority": { + "type": "integer" }, "Queue-ID": { "type": "string" @@ -28,6 +35,7 @@ "required": [ "Account-ID", "Call", + "Enter-As-Callback", "Queue-ID" ], "type": "object" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_remove.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_remove.json index b554287334d..21559ed1a8b 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_remove.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.queue_member_remove.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "member_remove" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_req.json index f7b5bdaa2b4..bdb7042cf11 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_req.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_req.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_req" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_resp.json index 739b30e22df..3b581e71082 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_resp.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_queue.sync_resp.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "queue" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "sync_resp" - ], - "type": "string" + ] }, "Process-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_err.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_err.json new file mode 100644 index 00000000000..5de9c475f32 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_err.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.agent_calls_err", + "description": "AMQP API for acdc_stats.agent_calls_err", + "properties": { + "Error-Reason": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_calls_err" + ] + } + }, + "required": [ + "Error-Reason" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_req.json new file mode 100644 index 00000000000..b59728d76d5 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_req.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.agent_calls_req", + "description": "AMQP API for acdc_stats.agent_calls_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "End-Range": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_calls_req" + ] + }, + "Queue-ID": { + "type": "string" + }, + "Start-Range": { + "type": "string" + }, + "Status": { + "type": "string" + } + }, + "required": [ + "Account-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_resp.json new file mode 100644 index 00000000000..533a390a4fc --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_calls_resp.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.agent_calls_resp", + "description": "AMQP API for acdc_stats.agent_calls_resp", + "properties": { + "Data": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_calls_resp" + ] + }, + "Query-Time": { + "type": "integer" + } + }, + "required": [ + "Query-Time" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_err.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_err.json new file mode 100644 index 00000000000..6ed8fe036af --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_err.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.agent_cur_status_err", + "description": "AMQP API for acdc_stats.agent_cur_status_err", + "properties": { + "Error-Reason": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_cur_status_err" + ] + } + }, + "required": [ + "Error-Reason" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_req.json new file mode 100644 index 00000000000..a29b7c5b143 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_req.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.agent_cur_status_req", + "description": "AMQP API for acdc_stats.agent_cur_status_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_cur_status_req" + ] + } + }, + "required": [ + "Account-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_resp.json new file mode 100644 index 00000000000..ccec0ab4aae --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.agent_cur_status_resp.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.agent_cur_status_resp", + "description": "AMQP API for acdc_stats.agent_cur_status_resp", + "properties": { + "Agents": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "agent_cur_status_resp" + ] + } + }, + "required": [ + "Agents" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_err.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_err.json index 592c5388ba5..6c59e250508 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_err.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_err.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "average_wait_time_err" - ], - "type": "string" + ] } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_req.json index 95628f110cc..12c52e7c107 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_req.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_req.json @@ -10,19 +10,23 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "average_wait_time_req" - ], - "type": "string" + ] }, "Queue-ID": { "minLength": 1, "type": "string" }, + "Skills": { + "items": { + "type": "string" + }, + "type": "array" + }, "Window": { "type": "integer" } diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_resp.json index e2bd36da38f..bcc8018af8f 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_resp.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.average_wait_time_resp.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "average_wait_time_resp" - ], - "type": "string" + ] } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_abandoned.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_abandoned.json index 48a2861ae06..38f6d5541dd 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_abandoned.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_abandoned.json @@ -18,14 +18,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "abandoned" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_exited_position.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_exited_position.json new file mode 100644 index 00000000000..31ddc059761 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_exited_position.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.call_exited_position", + "description": "AMQP API for acdc_stats.call_exited_position", + "properties": { + "Account-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_call_stat" + ] + }, + "Event-Name": { + "enum": [ + "exited-position" + ] + }, + "Exited-Position": { + "type": "string" + }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Call-ID", + "Queue-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_flush.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_flush.json index c3386649fff..bc66150df48 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_flush.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_flush.json @@ -12,14 +12,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "flush" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_handled.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_handled.json index 9ef6647b060..13f495101f5 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_handled.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_handled.json @@ -15,14 +15,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "handled" - ], - "type": "string" + ] }, "Handled-Timestamp": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_marked_callback.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_marked_callback.json new file mode 100644 index 00000000000..3727df2c606 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_marked_callback.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.call_marked_callback", + "description": "AMQP API for acdc_stats.call_marked_callback", + "properties": { + "Account-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Caller-ID-Name": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_call_stat" + ] + }, + "Event-Name": { + "enum": [ + "marked_callback" + ] + }, + "Queue-ID": { + "type": "string" + } + }, + "required": [ + "Account-ID", + "Call-ID", + "Queue-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_missed.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_missed.json index f141044ffd0..dbc6dc1b9c0 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_missed.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_missed.json @@ -15,14 +15,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "missed" - ], - "type": "string" + ] }, "Miss-Reason": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_processed.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_processed.json index aca0a28a807..b151159a653 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_processed.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_processed.json @@ -15,14 +15,12 @@ "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "processed" - ], - "type": "string" + ] }, "Hung-Up-By": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_err.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_err.json new file mode 100644 index 00000000000..60f73bb77f7 --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_err.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.call_summary_err", + "description": "AMQP API for acdc_stats.call_summary_err", + "properties": { + "Error-Reason": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "call_summary_err" + ] + } + }, + "required": [ + "Error-Reason" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_req.json new file mode 100644 index 00000000000..b147f00a4fc --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_req.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.call_summary_req", + "description": "AMQP API for acdc_stats.call_summary_req", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "End-Range": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "call_summary_req" + ] + }, + "Queue-ID": { + "type": "string" + }, + "Start-Range": { + "type": "string" + }, + "Status": { + "type": "string" + } + }, + "required": [ + "Account-ID" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_resp.json new file mode 100644 index 00000000000..0a35f23124a --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_summary_resp.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.call_summary_resp", + "description": "AMQP API for acdc_stats.call_summary_resp", + "properties": { + "Abandoned": { + "type": "string" + }, + "Data": { + "type": "string" + }, + "Entered-Position": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_stat" + ] + }, + "Event-Name": { + "enum": [ + "call_summary_resp" + ] + }, + "Exited-Position": { + "type": "string" + }, + "Handled": { + "type": "string" + }, + "Processed": { + "type": "string" + }, + "Query-Time": { + "type": "integer" + }, + "Waiting": { + "type": "string" + } + }, + "required": [ + "Query-Time" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_waiting.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_waiting.json index 98a49650e6a..a50d6f1a51b 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_waiting.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.call_waiting.json @@ -18,23 +18,30 @@ "Caller-Priority": { "type": "string" }, + "Entered-Position": { + "type": "string" + }, "Entered-Timestamp": { "type": "string" }, "Event-Category": { "enum": [ "acdc_call_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "waiting" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" + }, + "Required-Skills": { + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_err.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_err.json index 253e27a8ade..faa54c22425 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_err.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_err.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "current_calls_err" - ], - "type": "string" + ] } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_req.json index be4c2f1dcce..bed363b6646 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_req.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_req.json @@ -15,14 +15,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "current_calls_req" - ], - "type": "string" + ] }, "Queue-ID": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_resp.json index d646112820a..cf2a7ae3b4a 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_resp.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.current_calls_resp.json @@ -6,16 +6,20 @@ "Abandoned": { "type": "string" }, + "Entered-Position": { + "type": "string" + }, "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "current_calls_resp" - ], + ] + }, + "Exited-Position": { "type": "string" }, "Handled": { diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connected.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connected.json index 3b7fa350b2b..48ba91ad386 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connected.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connected.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connected" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connecting.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connecting.json index 5bf6ce978c2..4e6b08c2d8a 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connecting.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_connecting.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "connecting" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_err.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_err.json index d4fef139d3c..eb6c05e2a2b 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_err.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_err.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "status_err" - ], - "type": "string" + ] } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_inbound.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_inbound.json new file mode 100644 index 00000000000..728cbf4ff4c --- /dev/null +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_inbound.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "_id": "kapi.acdc_stats.status_inbound", + "description": "AMQP API for acdc_stats.status_inbound", + "properties": { + "Account-ID": { + "type": "string" + }, + "Agent-ID": { + "type": "string" + }, + "Call-ID": { + "type": "string" + }, + "Caller-ID-Name": { + "type": "string" + }, + "Caller-ID-Number": { + "type": "string" + }, + "Event-Category": { + "enum": [ + "acdc_status_stat" + ] + }, + "Event-Name": { + "enum": [ + "inbound" + ] + }, + "Pause-Alias": { + "type": "string" + }, + "Pause-Time": { + "type": "integer" + }, + "Timestamp": { + "type": "string" + }, + "Wait-Time": { + "type": "integer" + } + }, + "required": [ + "Account-ID", + "Agent-ID", + "Timestamp" + ], + "type": "object" +} diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_in.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_in.json index ed4e597e8aa..ad24ee7df0c 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_in.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_in.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "logged_in" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_out.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_out.json index b0f0b906fb3..e634d1913a2 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_out.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_logged_out.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "logged_out" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_outbound.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_outbound.json index 2049eae15d9..e5d4e034daf 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_outbound.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_outbound.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "outbound" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_paused.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_paused.json index fd7d723175a..79795eb8b53 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_paused.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_paused.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "paused" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_pending_logged_out.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_pending_logged_out.json index be6ca2e39fb..b6319b0d6b0 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_pending_logged_out.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_pending_logged_out.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "pending_logged_out" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_ready.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_ready.json index 8f0cdfc1e6d..44ad99da4bb 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_ready.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_ready.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "ready" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_req.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_req.json index 224b01bc574..38610ffbc50 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_req.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_req.json @@ -15,14 +15,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "status_req" - ], - "type": "string" + ] }, "Limit": { "type": "string" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_resp.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_resp.json index d5cfbb28846..d8ef218f2ff 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_resp.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_resp.json @@ -9,14 +9,12 @@ "Event-Category": { "enum": [ "acdc_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "status_resp" - ], - "type": "string" + ] } }, "required": [ diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_update.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_update.json index 24b48f0de5c..7f1c2df92bf 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_update.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_update.json @@ -21,16 +21,15 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": {}, + "Pause-Alias": { + "type": "string" + }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_wrapup.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_wrapup.json index 7942fb3f1db..2b133858638 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_wrapup.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_stats.status_wrapup.json @@ -21,21 +21,19 @@ "Event-Category": { "enum": [ "acdc_status_stat" - ], - "type": "string" + ] }, "Event-Name": { "enum": [ "wrapup" - ], + ] + }, + "Pause-Alias": { "type": "string" }, "Pause-Time": { "type": "integer" }, - "Queue-ID": { - "type": "string" - }, "Timestamp": { "type": "string" }, diff --git a/applications/crossbar/priv/couchdb/schemas/queues.json b/applications/crossbar/priv/couchdb/schemas/queues.json index 6c367832ce0..2be75b2e9c6 100644 --- a/applications/crossbar/priv/couchdb/schemas/queues.json +++ b/applications/crossbar/priv/couchdb/schemas/queues.json @@ -144,7 +144,9 @@ "description": "The queue strategy for connecting agents to callers", "enum": [ "round_robin", - "most_idle" + "most_idle", + "skills_based_round_robin", + "ring_all" ], "type": "string" } diff --git a/applications/crossbar/priv/oas3/oas3-schemas.yml b/applications/crossbar/priv/oas3/oas3-schemas.yml index 6b32abf8bec..76af563fb9c 100644 --- a/applications/crossbar/priv/oas3/oas3-schemas.yml +++ b/applications/crossbar/priv/oas3/oas3-schemas.yml @@ -7776,6 +7776,8 @@ 'enum': - round_robin - most_idle + - skills_based_round_robin + - ring_all 'type': string 'required': - name diff --git a/applications/crossbar/priv/oas3/openapi.yml b/applications/crossbar/priv/oas3/openapi.yml index 0b1f2206789..cbbf5960d1e 100644 --- a/applications/crossbar/priv/oas3/openapi.yml +++ b/applications/crossbar/priv/oas3/openapi.yml @@ -22,6 +22,8 @@ '$ref': 'paths/agents.yml#/paths/~1accounts~1{ACCOUNT_ID}~1agents' '/accounts/{ACCOUNT_ID}/agents/stats': '$ref': 'paths/agents.yml#/paths/~1accounts~1{ACCOUNT_ID}~1agents~1stats' + '/accounts/{ACCOUNT_ID}/agents/stats_summary': + '$ref': 'paths/agents.yml#/paths/~1accounts~1{ACCOUNT_ID}~1agents~1stats_summary' '/accounts/{ACCOUNT_ID}/agents/status': '$ref': 'paths/agents.yml#/paths/~1accounts~1{ACCOUNT_ID}~1agents~1status' '/accounts/{ACCOUNT_ID}/agents/status/{USER_ID}': @@ -32,6 +34,9 @@ '/accounts/{ACCOUNT_ID}/agents/{USER_ID}/queue_status': '$ref': >- paths/agents.yml#/paths/~1accounts~1{ACCOUNT_ID}~1agents~1{USER_ID}~1queue_status + '/accounts/{ACCOUNT_ID}/agents/{USER_ID}/restart': + '$ref': >- + paths/agents.yml#/paths/~1accounts~1{ACCOUNT_ID}~1agents~1{USER_ID}~1restart '/accounts/{ACCOUNT_ID}/agents/{USER_ID}/status': '$ref': >- paths/agents.yml#/paths/~1accounts~1{ACCOUNT_ID}~1agents~1{USER_ID}~1status @@ -418,6 +423,8 @@ '$ref': 'paths/queues.yml#/paths/~1accounts~1{ACCOUNT_ID}~1queues~1eavesdrop' '/accounts/{ACCOUNT_ID}/queues/stats': '$ref': 'paths/queues.yml#/paths/~1accounts~1{ACCOUNT_ID}~1queues~1stats' + '/accounts/{ACCOUNT_ID}/queues/stats_summary': + '$ref': 'paths/queues.yml#/paths/~1accounts~1{ACCOUNT_ID}~1queues~1stats_summary' '/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}': '$ref': 'paths/queues.yml#/paths/~1accounts~1{ACCOUNT_ID}~1queues~1{QUEUE_ID}' '/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/eavesdrop': @@ -426,6 +433,9 @@ '/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/roster': '$ref': >- paths/queues.yml#/paths/~1accounts~1{ACCOUNT_ID}~1queues~1{QUEUE_ID}~1roster + '/accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/stats_summary': + '$ref': >- + paths/queues.yml#/paths/~1accounts~1{ACCOUNT_ID}~1queues~1{QUEUE_ID}~1stats_summary '/accounts/{ACCOUNT_ID}/rate_limits': '$ref': 'paths/rate_limits.yml#/paths/~1accounts~1{ACCOUNT_ID}~1rate_limits' '/accounts/{ACCOUNT_ID}/recordings': diff --git a/applications/crossbar/priv/oas3/paths/agents.yml b/applications/crossbar/priv/oas3/paths/agents.yml index e339a71f7f1..f40f95e543e 100644 --- a/applications/crossbar/priv/oas3/paths/agents.yml +++ b/applications/crossbar/priv/oas3/paths/agents.yml @@ -23,6 +23,18 @@ paths: summary: Get stats of agents tags: - agents + /accounts/{ACCOUNT_ID}/agents/stats_summary: + get: + operationId: GetAccountsAccountIdAgentsStatsSummary + parameters: + - $ref: '../oas3-parameters.yml#/auth_token_header' + - $ref: '../oas3-parameters.yml#/ACCOUNT_ID' + responses: + 200: + description: Successful operation + summary: Get stats_summary of agents + tags: + - agents /accounts/{ACCOUNT_ID}/agents/status: get: operationId: GetAccountsAccountIdAgentsStatus @@ -98,6 +110,19 @@ paths: summary: Update an instance of agents tags: - agents + /accounts/{ACCOUNT_ID}/agents/{USER_ID}/restart: + post: + operationId: PostAccountsAccountIdAgentsUserIdRestart + parameters: + - $ref: '../oas3-parameters.yml#/auth_token_header' + - $ref: '../oas3-parameters.yml#/ACCOUNT_ID' + - $ref: '../oas3-parameters.yml#/USER_ID' + responses: + 200: + description: Successful operation + summary: Update an instance of agents + tags: + - agents /accounts/{ACCOUNT_ID}/agents/{USER_ID}/status: get: operationId: GetAccountsAccountIdAgentsUserIdStatus diff --git a/applications/crossbar/priv/oas3/paths/queues.yml b/applications/crossbar/priv/oas3/paths/queues.yml index 7dc9862d848..0743d255c4b 100644 --- a/applications/crossbar/priv/oas3/paths/queues.yml +++ b/applications/crossbar/priv/oas3/paths/queues.yml @@ -56,6 +56,18 @@ paths: summary: Get stats of queues tags: - queues + /accounts/{ACCOUNT_ID}/queues/stats_summary: + get: + operationId: GetAccountsAccountIdQueuesStatsSummary + parameters: + - $ref: '../oas3-parameters.yml#/auth_token_header' + - $ref: '../oas3-parameters.yml#/ACCOUNT_ID' + responses: + 200: + description: Successful operation + summary: Get stats_summary of queues + tags: + - queues /accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}: delete: operationId: DeleteAccountsAccountIdQueuesQueueId @@ -175,3 +187,16 @@ paths: summary: Update an instance of queues tags: - queues + /accounts/{ACCOUNT_ID}/queues/{QUEUE_ID}/stats_summary: + get: + operationId: GetAccountsAccountIdQueuesQueueIdStatsSummary + parameters: + - $ref: '../oas3-parameters.yml#/auth_token_header' + - $ref: '../oas3-parameters.yml#/ACCOUNT_ID' + - $ref: '../oas3-parameters.yml#/QUEUE_ID' + responses: + 200: + description: Successful operation + summary: Get stats_summary of queues + tags: + - queues diff --git a/applications/ecallmgr/src/ecallmgr_fs_channel.erl b/applications/ecallmgr/src/ecallmgr_fs_channel.erl index 804e2559cda..824c0e3c7f2 100644 --- a/applications/ecallmgr/src/ecallmgr_fs_channel.erl +++ b/applications/ecallmgr/src/ecallmgr_fs_channel.erl @@ -136,7 +136,8 @@ is_bridged(UUID) -> ], case ets:select(?CHANNELS_TBL, MatchSpec) of ['undefined'] -> lager:debug("channel is not bridged"), 'false'; - [Bin] when is_binary(Bin) -> lager:debug("is bridged to: ~s", [Bin]), 'true'; + [Bin] when is_binary(Bin) + andalso Bin =/= UUID -> lager:debug("is bridged to: ~s", [Bin]), 'true'; _E -> lager:debug("not bridged: ~p", [_E]), 'false' end. diff --git a/applications/ecallmgr/src/ecallmgr_originate.erl b/applications/ecallmgr/src/ecallmgr_originate.erl index efb9dfde321..ed6438a4035 100644 --- a/applications/ecallmgr/src/ecallmgr_originate.erl +++ b/applications/ecallmgr/src/ecallmgr_originate.erl @@ -217,6 +217,7 @@ handle_cast({'build_originate_args'}, #state{originate_req=JObj ,action = ?ORIGINATE_PARK ,fetch_id=FetchId ,dialstrings='undefined' + ,control_pid = CtrlPid }=State) -> case kz_json:is_true(<<"Originate-Immediate">>, JObj) of 'true' -> gen_listener:cast(self(), {'originate_execute'}); @@ -225,7 +226,7 @@ handle_cast({'build_originate_args'}, #state{originate_req=JObj Endpoints = [update_endpoint(Endpoint, State) || Endpoint <- kz_json:get_ne_value(<<"Endpoints">>, JObj, []) ], - {'noreply', State#state{dialstrings=build_originate_args_from_endpoints(?ORIGINATE_PARK, Endpoints, JObj, FetchId)}}; + {'noreply', State#state{dialstrings=build_originate_args_from_endpoints(?ORIGINATE_PARK, Endpoints, JObj, FetchId, CtrlPid)}}; handle_cast({'build_originate_args'}, #state{originate_req=JObj ,action = Action ,app = ?ORIGINATE_EAVESDROP @@ -496,35 +497,37 @@ get_eavesdrop_action(JObj) -> end. -spec build_originate_args(kz_term:ne_binary(), state(), kz_json:object(), kz_term:ne_binary()) -> kz_term:api_binary(). -build_originate_args(Action, State, JObj, FetchId) -> +build_originate_args(Action, #state{control_pid=CtrlPid} = State, JObj, FetchId) -> case kz_json:get_value(<<"Endpoints">>, JObj, []) of [] -> lager:warning("no endpoints defined in originate request"), 'undefined'; [Endpoint] -> lager:debug("only one endpoint, don't create per-endpoint UUIDs"), - build_originate_args_from_endpoints(Action, [update_endpoint(Endpoint, State)], JObj, FetchId); + build_originate_args_from_endpoints(Action, [update_endpoint(Endpoint, State)], JObj, FetchId, CtrlPid); Endpoints -> lager:debug("multiple endpoints defined, assigning uuids to each"), UpdatedEndpoints = [update_endpoint(Endpoint, State) || Endpoint <- Endpoints], - build_originate_args_from_endpoints(Action, UpdatedEndpoints, JObj, FetchId) + build_originate_args_from_endpoints(Action, UpdatedEndpoints, JObj, FetchId, CtrlPid) end. --spec build_originate_args_from_endpoints(kz_term:ne_binary(), kz_json:objects(), kz_json:object(), kz_term:ne_binary()) -> +-spec build_originate_args_from_endpoints(kz_term:ne_binary(), kz_json:objects(), kz_json:object(), kz_term:ne_binary(), kz_term:ne_binary()) -> kz_term:ne_binary(). -build_originate_args_from_endpoints(Action, Endpoints, JObj, FetchId) -> +build_originate_args_from_endpoints(Action, Endpoints, JObj, FetchId, CtrlPid) -> lager:debug("building originate command arguments"), DialSeparator = ecallmgr_util:get_dial_separator(JObj, Endpoints), DialStrings = ecallmgr_util:build_bridge_string(Endpoints, DialSeparator), ChannelVars = get_channel_vars(JObj, FetchId), + CtrlQ = ecallmgr_call_control:queue_name(CtrlPid), - list_to_binary([ChannelVars, DialStrings, " ", Action]). + list_to_binary([ChannelVars, "[^^!Call-Control-Queue='", CtrlQ, "']", DialStrings, " ", Action]). -spec get_channel_vars(kz_json:object(), kz_term:ne_binary()) -> iolist(). get_channel_vars(JObj, FetchId) -> InteractionId = kz_json:get_value([<<"Custom-Channel-Vars">>, <>], JObj, ?CALL_INTERACTION_DEFAULT), + CCVs = [{<<"Fetch-ID">>, FetchId} ,{<<"Ecallmgr-Node">>, kz_term:to_binary(node())} ,{<>, InteractionId} diff --git a/core/kazoo_documents/src/kzd_queues.erl b/core/kazoo_documents/src/kzd_queues.erl index 734f002283d..f816156ea95 100644 --- a/core/kazoo_documents/src/kzd_queues.erl +++ b/core/kazoo_documents/src/kzd_queues.erl @@ -36,6 +36,8 @@ -export([strategy/1, strategy/2, set_strategy/2]). +-export([type/0]). + -include("kz_documents.hrl"). -type doc() :: kz_json:object(). @@ -43,6 +45,9 @@ -define(SCHEMA, <<"queues">>). +-spec type() -> kz_term:ne_binary(). +type() -> <<"queue">>. + -spec new() -> doc(). new() -> kz_json_schema:default_object(?SCHEMA). diff --git a/doc/mkdocs/mkdocs.yml b/doc/mkdocs/mkdocs.yml index 2e61bea0c3e..016f5aae4d1 100644 --- a/doc/mkdocs/mkdocs.yml +++ b/doc/mkdocs/mkdocs.yml @@ -177,6 +177,8 @@ nav: - 'Average Wait Time': 'applications/callflow/doc/acdc_wait_time.md' - 'applications/acdc/doc/maintenance.md' - 'applications/acdc/doc/acdc_agent_maintenance.md' + - 'applications/acdc/doc/features.md' + - 'applications/acdc/doc/issues.md' - 'Ananke': - 'applications/ananke/doc/README.md' - 'Braintree': diff --git a/make/deps.mk b/make/deps.mk index ff5ddcdbb2d..3326f1f3459 100644 --- a/make/deps.mk +++ b/make/deps.mk @@ -23,6 +23,7 @@ DEPS ?= amqp_client \ nklib \ plists \ poolboy \ + pqueue \ proper \ ra \ ranch \ @@ -125,6 +126,8 @@ dep_nklib = git https://github.com/2600hz/erlang-nklib v0.4.1 dep_plists = git https://github.com/2600hz/erlang-plists 1.0.0 # used by a handful of core apps +dep_pqueue = hex 1.7.2 + dep_proper = git https://github.com/2600hz/erlang-proper v1.3 # used by kazoo_proper, knm, kazoo_caches, kazoo_bindings, kz_util_tests, kazoo_token_buckets, kazoo_stdlib # used by apps hotornot and callflow