fix(voice): harden agent-label parsing and prevent started_at drift

- Conference::Manager.extract_user_id now requires the account id in the
  participant label to match call.account_id, so a spoofed label can't
  attach a user from another account to the call.
- CallStatus::Manager keeps the earliest started_at across multiple
  in-progress webhooks (Twilio can emit answered + in-progress, plus
  retries) so duration_seconds isn't undercounted.
This commit is contained in:
Muhsin
2026-04-17 23:02:28 +04:00
parent d6d6f87145
commit 7635a538af
2 changed files with 15 additions and 6 deletions
@@ -17,7 +17,10 @@ class Voice::CallStatus::Manager
ts = timestamp || now_seconds
if status == 'in_progress'
attrs[:started_at] = Time.zone.at(ts)
# Twilio can emit multiple in-progress updates (answered + in-progress, retries).
# Keep the earliest timestamp so duration_seconds doesn't shift forward.
started_at = Time.zone.at(ts)
attrs[:started_at] = started_at if call.started_at.nil? || started_at < call.started_at
elsif Call::TERMINAL_STATUSES.include?(status)
call.ended_at = ts
attrs[:meta] = call.meta
@@ -35,6 +35,17 @@ class Voice::Conference::Manager
status_manager.process_status_update('in_progress', timestamp: now)
end
# Parses agent user_id from participant_label. Only returns an id when the
# label's embedded account id matches the call's account — protects against
# a spoofed/cross-account label attaching a foreign user to the call.
def extract_user_id
match = participant_label.to_s.match(AGENT_LABEL_PATTERN)
return unless match
return unless match[2].to_i == call.account_id
match[1].to_i
end
def handle_leave!
case call.status
when 'ringing'
@@ -54,11 +65,6 @@ class Voice::Conference::Manager
participant_label.to_s.start_with?('agent-')
end
def extract_user_id
match = participant_label.to_s.match(AGENT_LABEL_PATTERN)
match && match[1].to_i
end
def now
Time.zone.now.to_i
end