Class: Phone::Pbx
- Inherits:
-
Object
- Object
- Phone::Pbx
- Includes:
- Singleton
- Defined in:
- app/services/phone/pbx.rb
Overview
Service object: pbx.
Defined Under Namespace
Classes: ApiError, PbxResponse
Constant Summary collapse
- ALL_QUEUES =
Queues.
'all'- QUEUES =
{ tech_24x7: 1147, sales: 1142, tech1: 1143, tech2: 1148, operator: 1141, test: 1162, customer_contact: 1200, management: 1204, accounting: 1225, sales_homeowner: 1276, sales_commercial: 1277, sales_bg: 1278, sales_trade: 1279, sales_ecommerce: 1280 }.freeze
- STATUSES =
Statuses.
{ dnd: 11, available: 7 }.freeze
- UNIFIED_STATUS =
Unified status.
{ available: { pbx: 7, queue: true }, available_non_queue: { pbx: 7, queue: false }, dnd: { pbx: 11, queue: false }, not_working: { pbx: 9, queue: false } }.freeze
- DEFAULT_OPEN_TIMEOUT =
Queue status is materially slower than the other JSON endpoints: Tech 1
answered in 7.96s from an otherwise-idle production container on 2026-08-09.
Give hourly snapshots a narrow margin after serializing background traffic,
but cap the whole 15-queue pass so a full outage cannot occupy the only PBX
worker for 15 × 15 seconds. Web and other calls retain the 10s default,
apart from the existing 4s ring-order verification reads. 5- DEFAULT_READ_TIMEOUT =
10- QUEUE_SNAPSHOT_TIMEOUT =
15- QUEUE_SNAPSHOT_BUDGET =
60- MONOTONIC_CLOCK =
-> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }.freeze
- SERVER_TIME_FORMAT =
Server time format.
"%Y-%m-%d %H:%M:%S"
Instance Attribute Summary collapse
-
#logger ⇒ Object
readonly
Returns the value of attribute logger.
-
#server_time_zone ⇒ Object
readonly
Returns the value of attribute server_time_zone.
-
#switchvox ⇒ Object
readonly
Returns the value of attribute switchvox.
-
#uri ⇒ Object
readonly
Returns the value of attribute uri.
Instance Method Summary collapse
-
#call_log_search(api_params = {}, &block) ⇒ Object, ...
Call Log search.
-
#convert_to_obj(arg) ⇒ OpenStruct, ...
Converts API response hashes to objects with method-style access.
-
#current_calls ⇒ Array<Hash>
Current calls as hashes.
-
#employee_places_call(employee, to_number, options = {}) ⇒ Object
This method extracts all the necessary information for an employee to make an outbound call to a number.
-
#format_datetime(datetime) ⇒ String
Switchvox-formatted datetime in the PBX timezone.
-
#get_presence_options_list(switchvox_account_id) ⇒ Array<Hash>
Get list of presence statuses.
-
#get_presence_status(switchvox_account_id) ⇒ Object?
Returns the presence status for one employee based on switchvox_account_id, will look like: 13:15:43", :id=>"11", :presence=>"dnd", :message=>nil, :sub_presence=>nil or nil will be returned if nothing was found or on error.
-
#get_queue_info(queue_account_id) ⇒ OpenStruct, Phone::Pbx::PbxResponse
Full configuration of one call queue; members arrive in ring order.
- #get_queue_members_status(queue_ids = nil) ⇒ Hash
-
#get_queues_snapshot(queue_ids, monotonic_clock: MONOTONIC_CLOCK) ⇒ Hash{Integer => Hash}
Live ring-order snapshot for a set of queues, for the switchboard roster.
-
#get_queues_status(queue_ids = nil) ⇒ Hash{Integer => Hash{Integer => String}}
Calls switchvox api and retrieve a hash of switchvox_account_id with queue_account_id and status in that queue.
-
#initialize(options = {}) ⇒ Pbx
constructor
A new instance of Pbx.
-
#member_queue_log_search(api_params = {}, &block) ⇒ Object, ...
QueueMemberLogs.search.
-
#missed_call_search(missed_call_uniqueid) ⇒ Object
Switchvox missed-call result.
- #parse_datetime(datetime_string) ⇒ ActiveSupport::TimeWithZone
-
#place_call(from_number, to_number, caller_account_id, options = {}) ⇒ Object
Generic method wrapper to place call through switchvox (click to call style) http://developers.digium.com/switchvox/wiki/index.php/Switchvox.users.call.
-
#prepare_json_payload(hash) ⇒ String
JSON request body.
-
#process_request(api_method, api_params, block = nil) ⇒ Object, ...
Wrapper method to call the switchvox api and paginate results, calls block with each page.
-
#prune_account_ids(account_ids) ⇒ Array<Integer>
Account ids that still exist on the PBX.
- #queue_log_search(api_params = {}, &block) ⇒ Object, ...
-
#queue_status_from_snapshot(snapshot, queue_ids = QUEUES.values) ⇒ Hash{Integer => Hash{Integer => String}}
Derives #get_queues_status's shape from an already-pulled snapshot, so a caller that needs both the ring order and each member's logged-in status pays one getCurrentStatus per queue instead of two.
-
#report_api_failure(api_method, http_status: nil, body_snippet: nil, cause: nil, severity: :error) ⇒ void
Report a Switchvox-side failure to AppSignal so it surfaces as an incident rather than disappearing into the logs.
-
#retrieve_extension_account_id(pbx_extension) ⇒ Integer
For a given extension code (e.g 800) retrieves the associated switchvox account id via api call.
-
#retrieve_extension_info(pbx_extension = nil, options = {}) ⇒ Object
Wrapper for switchvox.extensions.search.
- #start_packet_capture(duration) ⇒ Boolean
-
#stringify_values(hash) ⇒ Hash
The hash with values coerced to strings.
-
#switchvox_request(api_method, options = {}, ignore_error_codes = [], timeout: nil) ⇒ Object, ...
Performs a Switchvox API request, following pagination when needed.
-
#update_presence_status(account_id, presence_option_id) ⇒ Boolean
Updates the PBX Presence flag.
-
#update_queue_member_order(queue_account_id, ordered_member_ids) ⇒ Phone::Pbx::PbxResponse
Sets a queue's ring order to +ordered_member_ids+ (Switchvox member account ids).
-
#update_queue_status(account_id:, log_in_queue:, call_queue_account_ids: []) ⇒ Boolean
Account id: the switchvox account id of the user log in queue: true to login, false to log out call_queue_account_id: an optional queue account id to sign in/out of, default to all.
-
#update_unified_presence(account_id:, status_id:, log_in_queue: false, call_queue_account_ids: []) ⇒ Boolean
Synchronized status update using a unified symbol map.
-
#valid_sip_account_ids ⇒ Array<Integer>
Switchvox SIP account ids.
Constructor Details
#initialize(options = {}) ⇒ Pbx
Returns a new instance of Pbx.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'app/services/phone/pbx.rb', line 71 def initialize( = {}) @logger = [:logger] || Rails.logger host = Heatwave::Configuration.fetch(:switchvox, :host) username = Heatwave::Configuration.fetch(:switchvox, :username) password = Heatwave::Configuration.fetch(:switchvox, :password) # Validate credentials are present to fail fast with helpful error raise ArgumentError, 'Switchvox host not configured. Add switchvox.host to Rails credentials.' if host.blank? raise ArgumentError, 'Switchvox credentials not configured. Add switchvox.username and switchvox.password to Rails credentials.' if username.blank? || password.blank? @uri = "https://#{host}/json" @domain = "https://#{host}" # SSL verification off — Switchvox PBX uses a self-signed cert. Explicit # timeouts so a hung PBX can't pin a web thread or worker forever (the # roster snapshot chains one call per queue, so a single hang multiplies). @client = Faraday.new( ssl: { verify: false }, request: { open_timeout: DEFAULT_OPEN_TIMEOUT, timeout: DEFAULT_READ_TIMEOUT } ) do |f| f.request :authorization, :basic, username, password f.adapter Faraday.default_adapter end @server_time_zone = ActiveSupport::TimeZone.new("Central Time (US & Canada)") end |
Instance Attribute Details
#logger ⇒ Object (readonly)
Returns the value of attribute logger.
66 67 68 |
# File 'app/services/phone/pbx.rb', line 66 def logger @logger end |
#server_time_zone ⇒ Object (readonly)
Returns the value of attribute server_time_zone.
66 67 68 |
# File 'app/services/phone/pbx.rb', line 66 def server_time_zone @server_time_zone end |
#switchvox ⇒ Object (readonly)
Returns the value of attribute switchvox.
66 67 68 |
# File 'app/services/phone/pbx.rb', line 66 def switchvox @switchvox end |
#uri ⇒ Object (readonly)
Returns the value of attribute uri.
66 67 68 |
# File 'app/services/phone/pbx.rb', line 66 def uri @uri end |
Instance Method Details
#call_log_search(api_params = {}, &block) ⇒ Object, ...
Call Log search
172 173 174 175 176 177 178 179 180 |
# File 'app/services/phone/pbx.rb', line 172 def call_log_search(api_params = {}, &block) api_params['start_date'] ||= Time.current.beginning_of_day api_params['end_date'] ||= Time.current.end_of_day api_params['start_date'] = format_datetime(api_params['start_date']) api_params['end_date'] = format_datetime(api_params['end_date']) api_params['sort_field'] = 'start_time' api_params['sort_direction'] = 'ASC' process_request "switchvox.callLogs.search", api_params, block end |
#convert_to_obj(arg) ⇒ OpenStruct, ...
OpenStruct is intentionally kept here because API responses have dynamic
structures that vary by endpoint. Data.define requires known members at compile time.
Converts API response hashes to objects with method-style access.
715 716 717 718 719 720 721 722 723 724 |
# File 'app/services/phone/pbx.rb', line 715 def convert_to_obj(arg) if arg.is_a? Hash arg.each { |k, v| arg[k] = convert_to_obj(v) } OpenStruct.new(arg) elsif arg.is_a? Array arg.map! { |v| convert_to_obj(v) } else arg end end |
#current_calls ⇒ Array<Hash>
Returns current calls as hashes.
254 255 256 257 258 259 260 261 262 263 |
# File 'app/services/phone/pbx.rb', line 254 def current_calls result = switchvox_request("switchvox.currentCalls.getList", {}) return [] if result.is_a?(PbxResponse) calls = [result.current_calls&.current_call].flatten.compact calls.map do |v| v.start_time = parse_datetime(v.start_time) v.marshal_dump end end |
#employee_places_call(employee, to_number, options = {}) ⇒ Object
This method extracts all the necessary information for an employee to
make an outbound call to a number
156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'app/services/phone/pbx.rb', line 156 def employee_places_call(employee, to_number, = {}) caller_account_id = employee.employee_phone_status.switchvox_account_id [:caller_id_name] ||= employee.full_name raise "Employee is not setup with switchvox account id" if caller_account_id.blank? pbx_extension = employee.pbx_extension raise "Employee is not setup with pbx extension" if pbx_extension.blank? pbx = Phone::Pbx.instance pbx.place_call(pbx_extension, to_number, caller_account_id, ) end |
#format_datetime(datetime) ⇒ String
Returns Switchvox-formatted datetime in the PBX timezone.
99 100 101 |
# File 'app/services/phone/pbx.rb', line 99 def format_datetime(datetime) datetime.in_time_zone(server_time_zone).strftime(SERVER_TIME_FORMAT) end |
#get_presence_options_list(switchvox_account_id) ⇒ Array<Hash>
Get list of presence statuses
557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 |
# File 'app/services/phone/pbx.rb', line 557 def (switchvox_account_id) api_params = { account_id: switchvox_account_id }.with_indifferent_access # 84711 = "That account_id is invalid" — typically a stale # `switchvox_account_id` on an EmployeePhoneStatus row (former employee, # deprovisioned Switchvox account, or garbage test data). The graceful- # degrade-to-`[]` path below already handles it correctly; suppress the # AppSignal incident so the SyncPhoneStatusWorker (every 10 minutes per # phone-enabled employee) doesn't fire one notification per stale row # per tick. Surfaced as incident #5971 (~92 occurrences/24h). result = switchvox_request("switchvox.users.presence.options.getList", api_params, ['84711']) # Return empty array on error (PbxResponse) or if errors present in API response if result.is_a?(PbxResponse) || result.try(:[], :errors).present? [] else [result..presence_option].flatten.map(&:marshal_dump) end end |
#get_presence_status(switchvox_account_id) ⇒ Object?
Returns the presence status for one employee based on switchvox_account_id, will look like:
13:15:43", :id=>"11", :presence=>"dnd", :message=>nil, :sub_presence=>nil
or nil will be returned if nothing was found or on error
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
# File 'app/services/phone/pbx.rb', line 283 def get_presence_status(switchvox_account_id) api_params = { account_id: switchvox_account_id }.with_indifferent_access # 10014 = Switchvox rejecting the `account_id` parameter value as invalid. # It belongs to Switchvox's generic request-validation family (neighbours # 10011 "missing required parameter" / 10013 "invalid number of parameters"; # genuine server-side faults are 12003 / 99999, and transport failures arrive # as non-2xx HTTP) — i.e. the value we sent was bad, not a Switchvox outage. # It fires when an EmployeePhoneStatus carries a blank/stale # `switchvox_account_id` (employee not provisioned for click-to-call, # deprovisioned account, test data) — the same class of problem the sibling # presence.options.getList path already silences via ['84711']. Both callers # degrade gracefully (Crm::OutboundCallsController#create falls through to its # "set your status" branch; #pull_presence returns :api_call_failure), so # reporting it to AppSignal is pure noise. Suppress these codes so a real # Switchvox-side failure (any other fault, non-2xx, or timeout) still surfaces. # 84711 = "That account_id is invalid" — the same stale/deprovisioned-account # condition, returned by getInfo for accounts getList already silences. It was # tolerable at the 10-minute SyncPhoneStatusWorker cadence but PullPhonePresenceWorker # now polls getInfo for every agent every ~15s, so each stale row flooded # AppSignal (incident #6173). Surfaced earlier as #5980 (2 occurrences/7d). result = switchvox_request("switchvox.users.presence.getInfo", api_params, %w[10014 84711]) return nil if result.is_a?(PbxResponse) result.presence end |
#get_queue_info(queue_account_id) ⇒ OpenStruct, Phone::Pbx::PbxResponse
Full configuration of one call queue; members arrive in ring order.
373 374 375 376 377 378 379 |
# File 'app/services/phone/pbx.rb', line 373 def get_queue_info(queue_account_id) api_params = { account_id: queue_account_id }.with_indifferent_access result = switchvox_request("switchvox.extensions.callQueues.getInfo", api_params) return result if result.is_a?(PbxResponse) result.extension end |
#get_queue_members_status(queue_ids = nil) ⇒ Hash
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'app/services/phone/pbx.rb', line 342 def get_queue_members_status(queue_ids = nil) results_hsh = {} queues = QUEUES queues = queues.select { |_k, v| queue_ids.include?(v) } if queue_ids.present? queues.each do |queue_name, queue_id| logger.info " Retrieving queue status for queue id: #{queue_id} #{queue_name}" api_params = { account_id: queue_id }.with_indifferent_access result = switchvox_request("switchvox.callQueues.getCurrentStatus", api_params) next if result.is_a?(PbxResponse) next if result.call_queue&.queue_members&.queue_member.blank? queue_members = result.call_queue.queue_members.queue_member queue_name = result.call_queue.call_queue_name queue_strategy = result.call_queue.strategy results_hsh[queue_id] ||= {} results_hsh[queue_id]['name'] = queue_name results_hsh[queue_id]['strategy'] = queue_strategy queue_members.each do |member| results_hsh[queue_id]['members'] ||= [] results_hsh[queue_id]['members'] << member.fullname end end results_hsh end |
#get_queues_snapshot(queue_ids, monotonic_clock: MONOTONIC_CLOCK) ⇒ Hash{Integer => Hash}
Live ring-order snapshot for a set of queues, for the switchboard roster.
Queues that error are skipped rather than failing the whole snapshot.
Unlike #get_queue_members_status (report-oriented, member names only)
this keeps each member's account id and ring position.
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 |
# File 'app/services/phone/pbx.rb', line 392 def get_queues_snapshot(queue_ids, monotonic_clock: MONOTONIC_CLOCK) snapshot = {} deadline = monotonic_clock.call + QUEUE_SNAPSHOT_BUDGET queue_ids.each do |queue_id| remaining = deadline - monotonic_clock.call if remaining <= 0 logger.warn '[pbx:switchvox.callQueues.getCurrentStatus] Queue snapshot budget exhausted; remaining queues skipped' break end api_params = { account_id: queue_id }.with_indifferent_access request_timeout = [QUEUE_SNAPSHOT_TIMEOUT, remaining].min result = switchvox_request("switchvox.callQueues.getCurrentStatus", api_params, [], timeout: request_timeout) next if result.is_a?(PbxResponse) call_queue = result.call_queue next if call_queue.blank? members = [call_queue.queue_members&.queue_member].flatten.compact snapshot[queue_id] = { 'name' => call_queue.call_queue_name, 'strategy' => call_queue.strategy, 'members' => members.sort_by { |member| member.order.to_i }.map do |member| { 'account_id' => member.account_id.to_s, 'fullname' => member.fullname, 'order' => member.order.to_i, 'logged_in_status' => member.logged_in_status } end } end snapshot end |
#get_queues_status(queue_ids = nil) ⇒ Hash{Integer => Hash{Integer => String}}
Calls switchvox api and retrieve a hash of switchvox_account_id with queue_account_id and status in that queue
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 |
# File 'app/services/phone/pbx.rb', line 312 def get_queues_status(queue_ids = nil) results_hsh = {} queues = QUEUES queues = queues.select { |_k, v| queue_ids.include?(v) } if queue_ids.present? queues.each do |queue_name, queue_id| logger.info " Retrieving queue status for queue id: #{queue_id} #{queue_name}" api_params = { account_id: queue_id }.with_indifferent_access result = switchvox_request("switchvox.callQueues.getCurrentStatus", api_params) next if result.is_a?(PbxResponse) next if result.call_queue&.queue_members&.queue_member.blank? queue_members = result.call_queue.queue_members.queue_member # queue_name from API response available via result.call_queue.call_queue_name if needed if queue_members.is_a?(Array) queue_members.each do |member| switchvox_account_id = member.account_id.to_i results_hsh[switchvox_account_id] ||= {} results_hsh[switchvox_account_id][queue_id] = member.logged_in_status end else switchvox_account_id = queue_members.account_id.to_i results_hsh[switchvox_account_id] ||= {} results_hsh[switchvox_account_id][queue_id] = queue_members.logged_in_status end end results_hsh end |
#member_queue_log_search(api_params = {}, &block) ⇒ Object, ...
QueueMemberLogs.search
212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'app/services/phone/pbx.rb', line 212 def member_queue_log_search(api_params = {}, &block) api_params = api_params.with_indifferent_access api_params['start_date'] ||= Time.current.beginning_of_day api_params['end_date'] ||= Time.current.end_of_day api_params['start_date'] = format_datetime(api_params['start_date']) api_params['end_date'] = format_datetime(api_params['end_date']) api_params['member_account_ids'] ||= valid_sip_account_ids api_params['sort_field'] = 'start_time' api_params['sort_direction'] = 'ASC' api_params['call_types'] = %w[missed_calls completed_calls] process_request "switchvox.callQueueMemberLogs.search", api_params, block end |
#missed_call_search(missed_call_uniqueid) ⇒ Object
Returns Switchvox missed-call result.
183 184 185 186 187 188 189 |
# File 'app/services/phone/pbx.rb', line 183 def missed_call_search(missed_call_uniqueid) api_params = {} api_params['uniqueid'] = missed_call_uniqueid api_params['sort_field'] = 'missed_time' api_params['sort_direction'] = 'ASC' process_request "switchvox.callQueueMissedCalls.getList", api_params end |
#parse_datetime(datetime_string) ⇒ ActiveSupport::TimeWithZone
104 105 106 |
# File 'app/services/phone/pbx.rb', line 104 def parse_datetime(datetime_string) server_time_zone.parse(datetime_string) end |
#place_call(from_number, to_number, caller_account_id, options = {}) ⇒ Object
Generic method wrapper to place call through switchvox (click to call style)
http://developers.digium.com/switchvox/wiki/index.php/Switchvox.users.call
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'app/services/phone/pbx.rb', line 119 def place_call(from_number, to_number, caller_account_id, = {}) caller_id_name = [:caller_id_name] || "WARMLYYOURS" vars = [] vars << "party_id=#{[:party_id]}" if [:party_id].present? vars << "activity_id=#{[:activity_id]}" if [:activity_id].present? if from_number.to_s.size > 3 && (pf = PhoneNumber.parse(from_number)) from_number = pf.dial_string end ignore_user_call_rules = [:ignore_user_call_rules].to_b ? 1 : 0 if to_number.to_s.size > 3 && (pt = PhoneNumber.parse(to_number)) to_number = pt.dial_string end api_params = { caller_id_name: caller_id_name, dial_as_account_id: caller_account_id, dial_first: from_number, dial_second: to_number, timeout: 60, ignore_user_api_settings: 0, ignore_user_call_rules: ignore_user_call_rules, # This might be useful timeout_second_call: 60, auto_answer: 1, variables: vars }.with_indifferent_access switchvox_request("switchvox.call", api_params) end |
#prepare_json_payload(hash) ⇒ String
Returns JSON request body.
727 728 729 730 731 732 |
# File 'app/services/phone/pbx.rb', line 727 def prepare_json_payload(hash) hash_string = stringify_values(hash) json = ActiveSupport::JSON.encode(hash_string) json.gsub!(/^\s{8}/, '') json end |
#process_request(api_method, api_params, block = nil) ⇒ Object, ...
Wrapper method to call the switchvox api and paginate results, calls block with each page.
api_params is a hash, :items_per_page defaults to 100, :page_number defaults to 1
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 |
# File 'app/services/phone/pbx.rb', line 578 def process_request(api_method, api_params, block = nil) logger.debug("Starting switchvox request", api_method: api_method) api_params['items_per_page'] ||= 100 api_params['page_number'] ||= 1 results = [] total_pages = nil total_items = nil loop do logger.debug("Starting switchvox paginated request", api_method: api_method, page: api_params['page_number'], total_pages: total_pages) page_results = switchvox_request(api_method, api_params) break if page_results.is_a?(PbxResponse) # Error occurred break if page_results&.calls&.call.blank? total_pages ||= page_results.calls.total_pages.to_i total_items ||= page_results.calls.total_items.to_i new_results = [page_results.calls.call].flatten if block block.call(new_results) else results += new_results end api_params['page_number'] += 1 end if block total_items else results end end |
#prune_account_ids(account_ids) ⇒ Array<Integer>
Returns account ids that still exist on the PBX.
274 275 276 |
# File 'app/services/phone/pbx.rb', line 274 def prune_account_ids(account_ids) account_ids & valid_sip_account_ids end |
#queue_log_search(api_params = {}, &block) ⇒ Object, ...
195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'app/services/phone/pbx.rb', line 195 def queue_log_search(api_params = {}, &block) api_params = api_params.with_indifferent_access api_params['start_date'] ||= Time.current.beginning_of_day api_params['end_date'] ||= Time.current.end_of_day api_params['start_date'] = format_datetime(api_params['start_date']) api_params['end_date'] = format_datetime(api_params['end_date']) api_params['queue_account_ids'] = QUEUES.values api_params['call_types'] = %w[completed_calls abandoned_calls redirected_calls] api_params['sort_field'] = 'start_time' api_params['sort_direction'] = 'ASC' process_request "switchvox.callQueueLogs.search", api_params, block end |
#queue_status_from_snapshot(snapshot, queue_ids = QUEUES.values) ⇒ Hash{Integer => Hash{Integer => String}}
Derives #get_queues_status's shape from an already-pulled snapshot, so a
caller that needs both the ring order and each member's logged-in status
pays one getCurrentStatus per queue instead of two.
433 434 435 436 437 438 439 |
# File 'app/services/phone/pbx.rb', line 433 def queue_status_from_snapshot(snapshot, queue_ids = QUEUES.values) snapshot.slice(*queue_ids).each_with_object({}) do |(queue_id, queue), statuses| queue['members'].each do |member| (statuses[member['account_id'].to_i] ||= {})[queue_id] = member['logged_in_status'] end end end |
#report_api_failure(api_method, http_status: nil, body_snippet: nil, cause: nil, severity: :error) ⇒ void
This method returns an undefined value.
Report a Switchvox-side failure to AppSignal so it surfaces as an incident
rather than disappearing into the logs. Class+method-level message keeps
related failures grouped into a single incident during a sustained outage
(AppSignal dedupes by class+message).
697 698 699 700 701 702 703 704 705 706 707 708 |
# File 'app/services/phone/pbx.rb', line 697 def report_api_failure(api_method, http_status: nil, body_snippet: nil, cause: nil, severity: :error) error = ApiError.new( api_method: api_method, http_status: http_status, body_snippet: body_snippet, cause_message: cause&. ) ErrorReporting.public_send(severity, error, source: :background) rescue StandardError => e # Never let the reporter itself break a worker — log and move on. logger.warn "[pbx:#{api_method}] ErrorReporting itself failed: #{e.class}: #{e.}" end |
#retrieve_extension_account_id(pbx_extension) ⇒ Integer
For a given extension code (e.g 800) retrieves the associated switchvox account id
via api call
269 270 271 |
# File 'app/services/phone/pbx.rb', line 269 def retrieve_extension_account_id(pbx_extension) retrieve_extension_info(pbx_extension).account_id.to_i end |
#retrieve_extension_info(pbx_extension = nil, options = {}) ⇒ Object
Wrapper for switchvox.extensions.search
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'app/services/phone/pbx.rb', line 231 def retrieve_extension_info(pbx_extension = nil, = {}) api_params = {}.with_indifferent_access if pbx_extension api_params[:min_extension] = pbx_extension api_params[:max_extension] = pbx_extension end api_params[:min_extension] ||= 800 api_params[:max_extension] ||= 899 api_params[:extension_types] = [:extension_types] || ['sip'] result = switchvox_request("switchvox.extensions.search", api_params) return [] if result.is_a?(PbxResponse) result.extensions&.extension || [] end |
#start_packet_capture(duration) ⇒ Boolean
510 511 512 513 514 515 516 517 518 519 520 |
# File 'app/services/phone/pbx.rb', line 510 def start_packet_capture(duration) result = switchvox_request("switchvox.debug.pcap.startSession", duration) = "Starting packet capture session with duration #{duration}" if result.try(:success) logger.info true else logger.error false end end |
#stringify_values(hash) ⇒ Hash
Returns the hash with values coerced to strings.
735 736 737 738 739 740 741 742 743 744 745 746 747 |
# File 'app/services/phone/pbx.rb', line 735 def stringify_values(hash) new_hsh = {} hash.each do |key, value| new_hsh[key.to_s] = if value.is_a?(Array) value.map(&:to_s) elsif value.is_a?(Hash) stringify_values(value) else value.to_s end end new_hsh end |
#switchvox_request(api_method, options = {}, ignore_error_codes = [], timeout: nil) ⇒ Object, ...
Performs a Switchvox API request, following pagination when needed.
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 |
# File 'app/services/phone/pbx.rb', line 618 def switchvox_request(api_method, = {}, ignore_error_codes = [], timeout: nil) logger.debug("[pbx] Initiated", api_method: api_method) body_hsh = { request: { method: api_method, parameters: } } body_json = prepare_json_payload(body_hsh) logger.info "[pbx:#{api_method}] Raw request: #{body_json}" begin res = @client.post(@uri) do |req| req.body = body_json req.headers['Content-Type'] = 'application/json' # Per-request override (ring-order verification re-reads use a short # budget so a hung PBX can't hold a web thread for the full timeout). req..timeout = timeout if timeout req..open_timeout = [DEFAULT_OPEN_TIMEOUT, timeout].min if timeout end response_body = res.body # For now just log in info, later switch to debug logger.info "[pbx:#{api_method}] Raw response: #{response_body}" # Distinguish network-acl rejection (HTTP 403 + Switchvox's main/http_forbidden.html # landing page) and other 4xx/5xx from real JSON. Faraday isn't configured to # raise on non-2xx, so without this check the next line tries to Oj.load HTML # and we lose the actual signal in the JSON::ParserError. unless (200..299).cover?(res.status) report_api_failure(api_method, http_status: res.status, body_snippet: response_body.to_s[0, 400]) return PbxResponse.new(success: false, errors: ["PBX HTTP #{res.status}. body=#{response_body.to_s[0, 200]}"]) end parsed_response = Oj.load(response_body) obj = convert_to_obj(parsed_response["response"]) if obj.result obj.result elsif obj.errors && !(ignore_error_codes.present? && ignore_error_codes.include?(obj.errors.error.code)) logger.error "[pbx:#{api_method}] Error returned #{obj.errors.inspect}" report_api_failure(api_method, http_status: res.status, body_snippet: "switchvox_error=#{obj.errors.inspect[0, 300]}") PbxResponse.new(success: false, errors: obj.errors) else PbxResponse.new(success: true, errors: nil) end rescue Oj::ParseError, JSON::ParserError => e logger.error "[pbx:#{api_method}] JSON parse error: #{e.class} - #{e.}" report_api_failure(api_method, http_status: res&.status, body_snippet: "json_parse=#{e.[0, 200]}", cause: e) PbxResponse.new(success: false, errors: ["Malformed JSON response. #{e.class}: #{e.}"]) rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, SocketError, Faraday::ConnectionFailed => e logger.error "[pbx:#{api_method}] Connection error: #{e.class} - #{e.}" report_api_failure(api_method, body_snippet: "connection_error=#{e.class}", cause: e, severity: :warning) PbxResponse.new(success: false, errors: ["PBX connection error. #{e.class}: #{e.}"]) rescue Net::ReadTimeout, Net::OpenTimeout, Faraday::TimeoutError => e logger.error "[pbx:#{api_method}] Timeout error: #{e.class} - #{e.}" report_api_failure(api_method, body_snippet: "timeout=#{e.class}", cause: e) PbxResponse.new(success: false, errors: ["PBX timeout. #{e.class}: #{e.}"]) end end |
#update_presence_status(account_id, presence_option_id) ⇒ Boolean
Updates the PBX Presence flag
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 |
# File 'app/services/phone/pbx.rb', line 486 def update_presence_status(account_id, presence_option_id) api_params = { presence_option_id: presence_option_id, account_id: account_id }.with_indifferent_access # 10014 / 84711: stale or deprovisioned switchvox_account_id — same codes # getInfo / getList already swallow. SyncPhoneStatusWorker pushes every # phone-enabled employee every 10 minutes, so one bad row is #5971. # A PbxResponse here is never a successful write (ignored errors still # come back as PbxResponse); do not persist local presence on it. result = switchvox_request("switchvox.users.presence.update", api_params, %w[10014 84711]) = "Update presence status for account_id #{account_id} with api_params #{api_params} returned #{result.inspect}" if result.is_a?(PbxResponse) logger.info "#{} (ignored/empty)" if result.success? logger.error unless result.success? false elsif result.try(:success) logger.info true else logger.error false end end |
#update_queue_member_order(queue_account_id, ordered_member_ids) ⇒ Phone::Pbx::PbxResponse
Sets a queue's ring order to +ordered_member_ids+ (Switchvox member
account ids). The JSON endpoint cannot encode the nested queue_members
structure ("Not an ARRAY reference"), so this one write goes through the
XML endpoint. update replaces membership wholesale, so the full current
member list is re-sent in the new order — and when the submitted ids do
not exactly match the current membership the call is refused BEFORE any
write, so a stale page can never drop a member.
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 |
# File 'app/services/phone/pbx.rb', line 453 def update_queue_member_order(queue_account_id, ordered_member_ids) info = get_queue_info(queue_account_id) return info if info.is_a?(PbxResponse) current_members = [info.queue_members&.queue_member].flatten.compact.map { |member| normalize_queue_member(member) } current_by_id = current_members.index_by { |member| member[:id] } submitted_ids = ordered_member_ids.map(&:to_s) if submitted_ids.sort != current_by_id.keys.sort logger.error "[pbx:switchvox.extensions.callQueues.update] member set mismatch for queue #{queue_account_id}: " \ "submitted #{submitted_ids} vs current #{current_by_id.keys} — refusing to write" return PbxResponse.new(success: false, errors: ['member set mismatch']) end ordered_members = submitted_ids.map { |id| current_by_id.fetch(id) } result = switchvox_xml_request(build_queue_update_xml(queue_account_id, ordered_members)) return result if result.failure? verify_queue_member_order(queue_account_id, submitted_ids) end |
#update_queue_status(account_id:, log_in_queue:, call_queue_account_ids: []) ⇒ Boolean
Account id: the switchvox account id of the user
log in queue: true to login, false to log out
call_queue_account_id: an optional queue account id to sign in/out of, default to all
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 |
# File 'app/services/phone/pbx.rb', line 527 def update_queue_status(account_id:, log_in_queue:, call_queue_account_ids: []) if call_queue_account_ids.blank? logger.error "Update queue status for account_id #{account_id} not possible without call_queue_account_ids specified" return false end cmd = log_in_queue ? 'login' : 'logout' # Log in/out of each queue. Attempt every requested queue even if one fails, # then report aggregate success so callers can leave the transition due for # a later retry. call_queue_account_ids.map do |call_queue_account_id| api_params = { call_queue_account_id: call_queue_account_id, account_id: account_id }.with_indifferent_access # Ignore error code 78956 which will be returned if user is already logged out result = switchvox_request("switchvox.users.callQueues.#{cmd}", api_params, ['78956']) = "Update queue status for account_id #{account_id} with api_params #{api_params} returned #{result.inspect}" if result.try(:success) logger.info true else logger.error false end end.all? end |
#update_unified_presence(account_id:, status_id:, log_in_queue: false, call_queue_account_ids: []) ⇒ Boolean
Synchronized status update using a unified symbol map
476 477 478 479 480 481 |
# File 'app/services/phone/pbx.rb', line 476 def update_unified_presence(account_id:, status_id:, log_in_queue: false, call_queue_account_ids: []) return false unless update_presence_status account_id, status_id return true if call_queue_account_ids.blank? update_queue_status account_id: account_id, log_in_queue: log_in_queue, call_queue_account_ids: call_queue_account_ids end |
#valid_sip_account_ids ⇒ Array<Integer>
Returns Switchvox SIP account ids.
249 250 251 |
# File 'app/services/phone/pbx.rb', line 249 def valid_sip_account_ids retrieve_extension_info.map(&:account_id).map(&:to_i).sort end |