File Coverage

blib/lib/SignalWire/Agents/Relay/Call.pm
Criterion Covered Total %
statement 41 162 25.3
branch 9 34 26.4
condition 7 32 21.8
subroutine 8 52 15.3
pod 0 42 0.0
total 65 322 20.1


line stmt bran cond sub pod time code
1             package SignalWire::Agents::Relay::Call;
2 4     4   146227 use strict;
  4         82  
  4         197  
3 4     4   24 use warnings;
  4         8  
  4         326  
4 4     4   706 use Moo;
  4         10760  
  4         34  
5              
6 4     4   4957 use SignalWire::Agents::Relay::Action;
  4         35  
  4         278  
7 4     4   714 use SignalWire::Agents::Relay::Constants qw(CALL_TERMINAL_STATES ACTION_TERMINAL_STATES);
  4         52  
  4         11448  
8              
9             has 'call_id' => ( is => 'ro', required => 1 );
10             has 'node_id' => ( is => 'rw', default => sub { '' } );
11             has 'tag' => ( is => 'ro', default => sub { '' } );
12             has 'state' => ( is => 'rw', default => sub { 'created' } );
13             has 'device' => ( is => 'rw', default => sub { {} } );
14             has 'end_reason' => ( is => 'rw', default => sub { '' } );
15             has 'peer' => ( is => 'rw', default => sub { {} } );
16             has 'context' => ( is => 'rw', default => sub { '' } );
17             has 'dial_winner' => ( is => 'rw', default => sub { 0 } );
18              
19             has '_client' => ( is => 'rw', default => sub { undef } );
20             has '_actions' => ( is => 'rw', default => sub { {} } ); # control_id => Action
21             has '_on_event' => ( is => 'rw', default => sub { [] } ); # event callbacks
22              
23             # Helper to generate a UUID-like control_id
24             sub _generate_uuid {
25 0     0   0 my @hex = map { sprintf('%02x', int(rand(256))) } 1..16;
  0         0  
26 0         0 $hex[6] = sprintf('%02x', (hex($hex[6]) & 0x0f) | 0x40);
27 0         0 $hex[8] = sprintf('%02x', (hex($hex[8]) & 0x3f) | 0x80);
28 0         0 return join('-',
29             join('', @hex[0..3]),
30             join('', @hex[4..5]),
31             join('', @hex[6..7]),
32             join('', @hex[8..9]),
33             join('', @hex[10..15]),
34             );
35             }
36              
37             sub _base_params {
38 0     0   0 my ($self) = @_;
39             return (
40 0         0 node_id => $self->node_id,
41             call_id => $self->call_id,
42             );
43             }
44              
45             sub _execute {
46 0     0   0 my ($self, $method, %extra) = @_;
47 0         0 my $client = $self->_client;
48 0 0       0 die "No client attached to call" unless $client;
49 0         0 my %params = ($self->_base_params, %extra);
50 0         0 return $client->execute($method, \%params);
51             }
52              
53             # Start an action-based method: creates the Action, registers it, executes the RPC
54             sub _start_action {
55 0     0   0 my ($self, $method, $action_class, %extra) = @_;
56 0         0 my $control_id = _generate_uuid();
57 0         0 my %params = ($self->_base_params, control_id => $control_id, %extra);
58              
59 0         0 my $action = $action_class->new(
60             control_id => $control_id,
61             call_id => $self->call_id,
62             node_id => $self->node_id,
63             _client => $self->_client,
64             );
65 0         0 $self->_actions->{$control_id} = $action;
66              
67 0         0 my $client = $self->_client;
68 0 0       0 if ($client) {
69 0         0 my $result = $client->execute($method, \%params);
70             # If call is gone (404/410), resolve action immediately
71 0 0 0     0 if (ref $result eq 'HASH' && $result->{code} && $result->{code} =~ /^(404|410)$/) {
      0        
72 0         0 $action->_resolve(undef);
73             }
74             }
75              
76 0         0 return $action;
77             }
78              
79             # --- Event dispatch ---
80              
81             sub dispatch_event {
82 15     15 0 81 my ($self, $event) = @_;
83 15   50     76 my $event_type = $event->event_type // '';
84              
85             # Update call state from state events
86 15 50       45 if ($event_type eq 'calling.call.state') {
    0          
87 15   50     50 my $new_state = $event->call_state // '';
88 15         60 $self->state($new_state);
89 15 100 66     120 $self->end_reason($event->end_reason) if $event->can('end_reason') && $event->end_reason;
90 15 50 33     102 $self->peer($event->peer) if $event->can('peer') && ref $event->peer eq 'HASH' && %{$event->peer};
  15   33     55  
91              
92             # If call ended, resolve all pending actions
93 15 100       46 if (CALL_TERMINAL_STATES->{$new_state}) {
94 5         20 $self->_resolve_all_actions;
95             }
96             }
97             elsif ($event_type eq 'calling.call.connect') {
98 0 0       0 $self->peer($event->peer) if $event->can('peer');
99             }
100              
101             # Route to action by control_id
102 15 50       56 my $control_id = $event->can('control_id') ? $event->control_id : '';
103 15 0 33     58 if ($control_id && exists $self->_actions->{$control_id}) {
104 0         0 my $action = $self->_actions->{$control_id};
105 0         0 $action->_handle_event($event);
106              
107             # Check if action reached terminal state
108 0   0     0 my $terminal = ACTION_TERMINAL_STATES->{$event_type} // {};
109 0 0 0     0 my $action_state = $event->can('state') ? ($event->state // '') : '';
110 0 0       0 if ($terminal->{$action_state}) {
111 0         0 $action->_resolve($event);
112 0         0 delete $self->_actions->{$control_id};
113             }
114             }
115              
116             # Fire registered event callbacks
117 15         48 for my $cb (@{$self->_on_event}) {
  15         78  
118 4         9 eval { $cb->($self, $event) };
  4         11  
119 4 50       24 warn "Call event callback error: $@" if $@;
120             }
121             }
122              
123             # Register an event listener
124             sub on {
125 4     4 0 70 my ($self, $cb) = @_;
126 4         7 push @{$self->_on_event}, $cb;
  4         16  
127 4         10 return $self;
128             }
129              
130             # Resolve all pending actions (e.g., on call ended or call-gone)
131             sub _resolve_all_actions {
132 5     5   14 my ($self) = @_;
133 5         12 for my $action (values %{$self->_actions}) {
  5         23  
134 2 50       34 $action->_resolve(undef) unless $action->completed;
135             }
136 5         25 $self->_actions({});
137             }
138              
139             # --- Simple fire-and-response methods ---
140              
141             sub answer {
142 0     0 0   my ($self, %opts) = @_;
143 0           return $self->_execute('calling.answer', %opts);
144             }
145              
146             sub hangup {
147 0     0 0   my ($self, %opts) = @_;
148 0           return $self->_execute('calling.end', %opts);
149             }
150              
151             sub pass {
152 0     0 0   my ($self) = @_;
153 0           return $self->_execute('calling.pass');
154             }
155              
156             sub connect {
157 0     0 0   my ($self, %opts) = @_;
158 0           return $self->_execute('calling.connect', %opts);
159             }
160              
161             sub disconnect {
162 0     0 0   my ($self) = @_;
163 0           return $self->_execute('calling.disconnect');
164             }
165              
166             sub hold {
167 0     0 0   my ($self) = @_;
168 0           return $self->_execute('calling.hold');
169             }
170              
171             sub unhold {
172 0     0 0   my ($self) = @_;
173 0           return $self->_execute('calling.unhold');
174             }
175              
176             sub denoise {
177 0     0 0   my ($self) = @_;
178 0           return $self->_execute('calling.denoise');
179             }
180              
181             sub denoise_stop {
182 0     0 0   my ($self) = @_;
183 0           return $self->_execute('calling.denoise.stop');
184             }
185              
186             sub transfer {
187 0     0 0   my ($self, %opts) = @_;
188 0           return $self->_execute('calling.transfer', %opts);
189             }
190              
191             sub join_conference {
192 0     0 0   my ($self, %opts) = @_;
193 0           return $self->_execute('calling.join_conference', %opts);
194             }
195              
196             sub leave_conference {
197 0     0 0   my ($self, %opts) = @_;
198 0           return $self->_execute('calling.leave_conference', %opts);
199             }
200              
201             sub echo {
202 0     0 0   my ($self, %opts) = @_;
203 0           return $self->_execute('calling.echo', %opts);
204             }
205              
206             sub bind_digit {
207 0     0 0   my ($self, %opts) = @_;
208 0           return $self->_execute('calling.bind_digit', %opts);
209             }
210              
211             sub clear_digit_bindings {
212 0     0 0   my ($self, %opts) = @_;
213 0           return $self->_execute('calling.clear_digit_bindings', %opts);
214             }
215              
216             sub live_transcribe {
217 0     0 0   my ($self, %opts) = @_;
218 0           return $self->_execute('calling.live_transcribe', %opts);
219             }
220              
221             sub live_translate {
222 0     0 0   my ($self, %opts) = @_;
223 0           return $self->_execute('calling.live_translate', %opts);
224             }
225              
226             sub join_room {
227 0     0 0   my ($self, %opts) = @_;
228 0           return $self->_execute('calling.join_room', %opts);
229             }
230              
231             sub leave_room {
232 0     0 0   my ($self) = @_;
233 0           return $self->_execute('calling.leave_room');
234             }
235              
236             sub amazon_bedrock {
237 0     0 0   my ($self, %opts) = @_;
238 0           return $self->_execute('calling.amazon_bedrock', %opts);
239             }
240              
241             sub ai_message {
242 0     0 0   my ($self, %opts) = @_;
243 0           return $self->_execute('calling.ai_message', %opts);
244             }
245              
246             sub ai_hold {
247 0     0 0   my ($self, %opts) = @_;
248 0           return $self->_execute('calling.ai_hold', %opts);
249             }
250              
251             sub ai_unhold {
252 0     0 0   my ($self, %opts) = @_;
253 0           return $self->_execute('calling.ai_unhold', %opts);
254             }
255              
256             sub user_event {
257 0     0 0   my ($self, %opts) = @_;
258 0           return $self->_execute('calling.user_event', %opts);
259             }
260              
261             sub queue_enter {
262 0     0 0   my ($self, %opts) = @_;
263 0           return $self->_execute('calling.queue.enter', %opts);
264             }
265              
266             sub queue_leave {
267 0     0 0   my ($self, %opts) = @_;
268 0           return $self->_execute('calling.queue.leave', %opts);
269             }
270              
271             sub refer {
272 0     0 0   my ($self, %opts) = @_;
273 0           return $self->_execute('calling.refer', %opts);
274             }
275              
276             sub send_digits {
277 0     0 0   my ($self, %opts) = @_;
278 0           return $self->_execute('calling.send_digits', %opts);
279             }
280              
281             # --- Action-based methods (control_id tracking) ---
282              
283             sub play {
284 0     0 0   my ($self, %opts) = @_;
285 0           return $self->_start_action('calling.play', 'SignalWire::Agents::Relay::Action::Play', %opts);
286             }
287              
288             sub record {
289 0     0 0   my ($self, %opts) = @_;
290 0           return $self->_start_action('calling.record', 'SignalWire::Agents::Relay::Action::Record', %opts);
291             }
292              
293             sub detect {
294 0     0 0   my ($self, %opts) = @_;
295 0           return $self->_start_action('calling.detect', 'SignalWire::Agents::Relay::Action::Detect', %opts);
296             }
297              
298             sub collect {
299 0     0 0   my ($self, %opts) = @_;
300 0           return $self->_start_action('calling.collect', 'SignalWire::Agents::Relay::Action::Collect', %opts);
301             }
302              
303             sub play_and_collect {
304 0     0 0   my ($self, %opts) = @_;
305 0           return $self->_start_action('calling.play_and_collect', 'SignalWire::Agents::Relay::Action::Collect', %opts);
306             }
307              
308             sub send_fax {
309 0     0 0   my ($self, %opts) = @_;
310 0           my $action = $self->_start_action('calling.send_fax', 'SignalWire::Agents::Relay::Action::Fax', %opts);
311 0           return $action;
312             }
313              
314             sub receive_fax {
315 0     0 0   my ($self, %opts) = @_;
316 0           my $control_id = _generate_uuid();
317 0           my %params = ($self->_base_params, control_id => $control_id, %opts);
318              
319 0           my $action = SignalWire::Agents::Relay::Action::Fax->new(
320             control_id => $control_id,
321             call_id => $self->call_id,
322             node_id => $self->node_id,
323             _client => $self->_client,
324             _fax_type => 'receive',
325             );
326 0           $self->_actions->{$control_id} = $action;
327              
328 0           my $client = $self->_client;
329 0 0         if ($client) {
330 0           my $result = $client->execute('calling.receive_fax', \%params);
331 0 0 0       if (ref $result eq 'HASH' && $result->{code} && $result->{code} =~ /^(404|410)$/) {
      0        
332 0           $action->_resolve(undef);
333             }
334             }
335              
336 0           return $action;
337             }
338              
339             sub tap {
340 0     0 0   my ($self, %opts) = @_;
341 0           return $self->_start_action('calling.tap', 'SignalWire::Agents::Relay::Action::Tap', %opts);
342             }
343              
344             sub stream {
345 0     0 0   my ($self, %opts) = @_;
346 0           return $self->_start_action('calling.stream', 'SignalWire::Agents::Relay::Action::Stream', %opts);
347             }
348              
349             sub pay {
350 0     0 0   my ($self, %opts) = @_;
351 0           return $self->_start_action('calling.pay', 'SignalWire::Agents::Relay::Action::Pay', %opts);
352             }
353              
354             sub transcribe {
355 0     0 0   my ($self, %opts) = @_;
356 0           return $self->_start_action('calling.transcribe', 'SignalWire::Agents::Relay::Action::Transcribe', %opts);
357             }
358              
359             sub ai {
360 0     0 0   my ($self, %opts) = @_;
361 0           return $self->_start_action('calling.ai', 'SignalWire::Agents::Relay::Action::AI', %opts);
362             }
363              
364             1;