o
    @boj+o                     @   s  U d dl Z d dlZd dlZd dlmZmZ d dlmZ d dlm	Z	 d dl
mZmZ d dlmZmZ d dlmZ d dlmZmZ d d	lmZ e eZe Zee ed
< eh dZe dej!Z"eedB e#eef gedB f Z$eeged f Z%G dd dZ&dS )    N)	AwaitableCallable)Any)	AriClient)log_ari_eventsummarize_channel)CallDirection	CallState)CallRegistry)Settingsget_settings)MediaManagerPROCESSED_CHANNELS>   hagentmediainboundcustomeroutboundz>^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$c                   @   s  e Zd Z						dSdedededB dedB dedB deg e	dB f dB deg e	dB f dB d	e
dB d
dfddZdee	ef d
edB fddZeded
e	fddZede	d
efddZdddee de	dB d
eee	dB e	dB e	dB f fddZdee	ef d
e	fddZdee	ef d
e	dB fddZdee	ef d
e	dB fd d!Zdee	ef d
efd"d#Zd$ed
dfd%d&Zd$ed'e	d
dfd(d)Zd$ed*ed
dfd+d,Zd$ed
dfd-d.Z dee	ef d
edB fd/d0Z!dee	ef d
edB fd1d2Z"d3e	d'e	dee	ef d
efd4d5Z#d'e	dee	ef d
edB fd6d7Z$dee	ef d
e	fd8d9Z%d$ed
e	fd:d;Z&d$ed'e	d
dfd<d=Z'd
e	dB fd>d?Z(d$ed
dfd@dAZ)d$ed
dfdBdCZ*d$ed'e	dee	ef d
dfdDdEZ+d'e	dB dee	ef d
edB fdFdGZ,dee	ef d$ed
efdHdIZ-d$edJe	d
dfdKdLZ.dee	ef d
edB fdMdNZ/dee	ef d
edB fdOdPZ0dee	ef d
edB fdQdRZ1dS )TCallStateMachineNariregistrysettingsmedia_managercall_resolverget_agent_endpointget_registered_extensionon_bot_readyreturnc	           	      C   s:   || _ || _|p
t | _|| _|| _|| _|| _|| _d S N)	r   r   r   r   r   _call_resolver_get_agent_endpoint_get_registered_extension_on_bot_ready)	selfr   r   r   r   r   r   r   r    r%   =/var/www/html/bot-asterisk-ari/backend/calls/state_machine.py__init__   s   
zCallStateMachine.__init__eventc                    sF   | d}| j| j| j| j| jd}| |}|r!||I d H S d S )Ntype)StasisStart	StasisEndChannelStateChangeChannelDestroyedPlaybackFinished)get_on_stasis_start_on_stasis_end_on_channel_state_change_on_channel_destroyed_on_playback_finished)r$   r(   
event_typehandlershandlerr%   r%   r&   handle_event1   s   

zCallStateMachine.handle_eventrawc                 C   s   t |  dS )uE   Quita espacios y comillas del dialplan (ej. '21601605' → 21601605).z'")strstrip)r9   r%   r%   r&   _normalize_arg?   s   zCallStateMachine._normalize_argvaluec                 C   s   t t| S )z/UUID de la API; nunca un DNI u otra cifra pura.)bool_UUID_REmatch)r=   r%   r%   r&   _looks_like_call_idD   s   z$CallStateMachine._looks_like_call_iddialplan_extenargsrC   c                   sB  d}d}g }|D ]5}|  |}|sq|dv rd}q|dkr d}q|tv r%q| r/|| q| |r7|}qtd| qd}d}	|rI|  |nd   rg |v rg } fdd|D }
|
rd|
d	 nd}	n4t|d
krw|d	 |d }}	n$t|dkr  r|d	  kr|d	 }n  r }|d	 }	n|d	 }||||	fS )u   
        Devuelve (direction, call_id, dialed_ext, document_id).

        Dialplan bot típico: Stasis(App,${EXTEN},${DOCUMENTO})
        API saliente: Stasis args [uuid, customer]
        r   N)r   r   r   z$Arg Stasis ignorado como call_id: %r c                       g | ]}| kr|qS r%   r%   ).0ndpr%   r&   
<listcomp>p       z7CallStateMachine._parse_stasis_args.<locals>.<listcomp>r         )r<   _STASIS_KEYWORDSisdigitappendrA   loggerdebuglen)r$   rD   rC   	directioncall_id_argnumericsr9   r=   
dialed_extdocument_idrestr%   rI   r&   _parse_stasis_argsI   sF   



z#CallStateMachine._parse_stasis_argschannelc                 C   st   | dpi }| dpi }| dp| d}|rt|S | dp)| dp)d}d|v r6|ddd S |p9d	S )
N	connectedcallernumbernamerE   /rN   Desconocido)r/   r:   rsplit)r$   r\   r]   r^   r_   r`   r%   r%   r&   _caller_number   s   zCallStateMachine._caller_numberc                 C   s6   | dpi }| dpi  d}|d urt|S d S )Nr\   dialplanexten)r/   r:   )r$   r(   r\   rg   r%   r%   r&   _dialplan_exten   s   z CallStateMachine._dialplan_extenc                 C   sT   | dpg }| jt|| |d\}}}}|r|S | |}|r(| r(|S d S )NrD   rB   )r/   r[   listrh   rP   )r$   r(   rD   _rX   rg   r%   r%   r&   _extract_dialed_extension   s   
z*CallStateMachine._extract_dialed_extensionc                    s   | dpg } jt| |d\}}}}|rdS  js dS   }|s(dS d|v r9t fdd|D s9dS  |}|rF||krFdS dS )NrD   rB   TFr   c                 3   s    | ]
}  | V  qd S r   )r<   rP   )rG   ar$   r%   r&   	<genexpr>   s    
z4CallStateMachine._inbound_allowed.<locals>.<genexpr>)r/   r[   ri   rh   r"   anyrk   )r$   r(   rD   rj   rY   
registereddialedr%   rm   r&   _inbound_allowed   s&   
z!CallStateMachine._inbound_allowedcallc                    s   |j dkr
td|jdvrtd|jr|jd nd }|s#td| |I d H  | ||I d H  | j|I d H  d|_| |I d H  d S )Nr   zNo es una llamada entrante)ringingu   La llamada no está sonandor   zCanal entrante no disponibleanswered)	rU   
ValueErrorstatuschannel_ids_ensure_bridge_add_to_call_bridger   answer_attach_webrtc_or_agent)r$   rs   
channel_idr%   r%   r&   answer_inbound_call   s   

z$CallStateMachine.answer_inbound_callr}   c              
      s  |j s|jr| |I dH  dS z| j|I dH  W n ty; } ztd||j| d|_	W Y d}~dS d}~ww d|_	t
d|j||j z| |I dH  | ||I dH  W n tyz } ztd|j| d|_	W Y d}~dS d}~ww 	 d|_| jjr| jrz| j|| j| jI dH  d|_t
d|j W n ty } ztd	|j| W Y d}~dS d}~ww | |I dH  dS )
zFContesta, reproduce el documento y prepara RTP del bot conversacional.Nz+No se pudo contestar canal %s (call=%s): %sfailedru   z-Bot auto-answer call=%s canal=%s documento=%sz'No se pudo armar puente bot call=%s: %sTzMedia RTP del bot lista call=%sz-No se pudo adjuntar media del bot call=%s: %s)bot_startedbot_media_ready_maybe_start_botr   r{   	ExceptionrR   errorcall_idrw   inforY   ry   rz   document_playback_doner   bot_enabledr   attach_bot_external_mediar   )r$   rs   r}   excr%   r%   r&   _confirm_document_playback   sp   


z+CallStateMachine._confirm_document_playbacktimeoutc                    sN   t |I dH  |jdv s|jrdS d|_td|j | |I dH  dS )zCSi no llega PlaybackFinished, arranca el bot igual tras el timeout.Nendedr   Tu4   Timeout readout documento call=%s — arrancando bot)asynciosleeprw   r   rR   warningr   r   )r$   rs   r   r%   r%   r&   _document_playback_timeout  s   z+CallStateMachine._document_playback_timeoutc              
      s   | j jr|jr
dS |jdv rdS |jr|jsdS | js%td|j	 dS d|_d|_
z| |I dH  W dS  tyW } zd|_d|_
td|j	| W Y d}~dS d}~ww )	uH   Arranca la conversación cuando hay media y terminó el readout del DNI.Nr   z0Bot listo call=%s pero sin callback on_bot_readyTstartingFr   z/Fallo al iniciar bot conversacional call=%s: %s)r   r   r   rw   r   r   r#   rR   r   r   agent_stater   r   r$   rs   r   r%   r%   r&   r   '  s6   
z!CallStateMachine._maybe_start_botc                    sz   | dpi }| d}|sd S d }| j D ]}|j|kr#|} nq|s(d S d|_td|j| | |I d H  |S )NplaybackidTz1Playback documento finalizado call=%s playback=%s)	r/   r   list_alldocument_playback_idr   rR   r   r   r   )r$   r(   r   playback_idrs   	candidater%   r%   r&   r4   B  s*   

z&CallStateMachine._on_playback_finishedc              
      s4  | di }| d}td| |sd S | dpg }| jt|| |d\}}}}| |}	|tv r{| ||}
|
r^|	dkr^|
j	s^|
j
rJ|
S td|
j| | |
||I d H  |
S |
rs|	dkrs|
j	ss| |
||I d H  |
S td	| |
S t| | ||}
|
s|r| |r| |||}
|
r|r|
j
s||
_
| j|
| |
j
r|	dkr| |
|I d H  |
S |
jd
ks|	dv rtd|
j||	 | |
||I d H  |
S td||| | |}|rt|gdd||d}| j| | ||I d H  |S |dkr@| |s@td|| z| j|I d H  W n ty8 } ztd| W Y d }~nd }~ww t| d S |g|d|dkrK|n|pV| di  dd}|r`||d< tdi |}| j| |dkrtd| |p|d|j|j |S |d
ks|	dkr| |||I d H  |S )Nr\   r   zEvento StasisStart: %srD   rB   r   z(Reintento setup media call=%s channel=%sr   zCanal ya procesado: %sr   )r   r   r   z&Setup media call=%s channel=%s role=%sz5StasisStart nueva llamada (channel=%s args=%s doc=%s)r   rt   )rx   rU   rw   r_   rY   z/Llamada entrante rechazada (channel=%s args=%s)z'No se pudo colgar inbound rechazada: %srf   rg   )rx   rU   rw   r_   r   z2Llamada entrante para ext %s desde %s (call_id=%s)?r%   )r/   rR   r   r[   ri   rh   _stasis_roler   _resolve_existing_callexternal_media_attachedrY   r   _setup_call_mediarS   addrA   _recover_callr   link_channelr   rU   re   r	   rr   r   hangupr   r   discardrk   r_   )r$   r(   r\   r}   rD   rU   rV   _dialed_extrY   roleexistingcaller_numberrs   r   call_fieldsr%   r%   r&   r0   Y  s   









z!CallStateMachine._on_stasis_startr   c           
      C   s   | dpg }d|v sd|v rdnd}| di }| di }| dp,| di  d	}t||g|d
|d}	| j|	 td| |	S )zLRe-vincula Stasis con la llamada originada (p. ej. tras reload del proceso).rD   r   r   r   r\   r^   r_   rf   rg   rt   )r   rx   rU   rw   r_   z?Llamada %s recuperada desde StasisStart (no estaba en registry))r/   r	   r   r   rR   r   )
r$   r   r}   r(   rD   rU   r\   r^   r_   rs   r%   r%   r&   r     s&   zCallStateMachine._recover_callc                 C   sz   | j r|  ||}|r|S | j|}|r|S |dpg }|D ]}| |}|r:| |r:| j|}|r:|  S q d S )NrD   )r    r   get_by_channelr/   r<   rA   )r$   r}   r(   found
by_channelrD   r9   r=   r%   r%   r&   r     s    
z'CallStateMachine._resolve_existing_callc                 C   sd   | dpg }d|v rdS d|v rdS d|v sd|v rdS | dp#i  dp(d}|d	r0dS dS )
NrD   r   r   r   r   r\   r`   rE   UnicastRTP/)r/   
startswith)r$   r(   rD   channel_namer%   r%   r&   r     s   
zCallStateMachine._stasis_rolec                    sB   |j r|j S | j I d H }|d |_ td|d |j |j S )Nr   z Puente %s creado para llamada %s)	bridge_idr   create_bridgerR   r   r   )r$   rs   bridger%   r%   r&   ry   
  s   
zCallStateMachine._ensure_bridgec                    sD   |  |I d H }| j||I d H  td|||jt|j d S )Nu7   Canal %s añadido al puente %s (llamada %s, %d canales))ry   r   add_to_bridgerR   r   r   rT   rx   )r$   rs   r}   r   r%   r%   r&   rz     s   z$CallStateMachine._add_to_call_bridgec                 C   s"   | j r|   }|r|S | jjpd S r   )r!   r   agent_endpoint)r$   endpointr%   r%   r&   _resolve_agent_endpoint  s
   z(CallStateMachine._resolve_agent_endpointc              
      s   |   }|r
|jrd S d|_z)| jj|| jjd|jdgdI d H }| j||d  t	
d|d ||j W d S  tyV } zd|_t	d|j| W Y d }~d S d }~ww )NTr   )	caller_id
use_stasisapp_argsr   u-   Pata agente originada: %s → %s (llamada %s)Fz+No se pudo originar pata agente para %s: %s)r   agent_leg_originatedr   originate_channelr   outbound_caller_idr   r   r   rR   r   r   r   )r$   rs   r   r\   r   r%   r%   r&   _originate_agent_leg$  s8   

z%CallStateMachine._originate_agent_legc              
      s   | j jr7| jr7|js7z| j|| j| jI d H  W d S  ty6 } zt	d|j
| W Y d }~d S d }~ww | |I d H  d S )Nu    externalMedia falló para %s: %s)r   webrtc_enabledr   r   attach_external_mediar   r   r   rR   r   r   r   r   r%   r%   r&   r|   ?  s,   
	z(CallStateMachine._attach_webrtc_or_agentc           	   
      s  |  |}|di }|dkrGz| ||I dH  d|_td|j|j W dS  tyF } zt	d|j| d|_
W Y d}~dS d}~ww |dkrvz| ||I dH  W dS  tyu } zt	d	|j| d|_
W Y d}~dS d}~ww |jr|jr|jrdS d|_| jjrtd
|jt| z| j|I dH }tdt| W n ty } ztd|| W Y d}~nd}~ww z| j|I dH  W n ty } ztd||j| W Y d}~nd}~ww z| ||I dH  W n ty } zt	d|j| d|_
W Y d}~dS d}~ww |j
dkrd|_
| |I dH  | jjs7|  s7td|j|d | jjrmz| j|I dH }td||d W dS  tyl } zt	d| W Y d}~dS d}~ww dS )z:Puente: destino + WebRTC (externalMedia) o AGENT_ENDPOINT.r\   r   NTuN   externalMedia en puente %s — audio navegador↔PJSIP habilitado (llamada %s)u9   Fallo al añadir externalMedia al puente (llamada %s): %sr   r   u2   Fallo al añadir agente al puente (llamada %s): %sz*StasisStart outbound call_id=%s channel=%szCanal en vivo (ARI): %sz!No se pudo consultar canal %s: %szAnswer canal %s (call=%s): %su3   Fallo al añadir cliente al puente (llamada %s): %srt   ru   u`   Llamada %s: solo hay 1 canal en el puente (%s). Activá WEBRTC_ENABLED o definí AGENT_ENDPOINT.r`   u7   Reproducción de prueba iniciada en %s (playback_id=%s)r   z*No se pudo reproducir sonido de prueba: %s)r   r/   rz   r   rR   r   r   r   r   r   rw   outbound_stasis_setupr   	ari_debugr   r   get_channelr   r{   r|   r   r   ari_debug_play_sound
play_sound)	r$   rs   r}   r(   r   r\   r   liver   r%   r%   r&   r   S  s   
	
	

z"CallStateMachine._setup_call_mediac                 C   sT   |sd S |  ||}|r|S | j D ]}||jv r|  S ||jkr'|  S qd S r   )r   r   r   rx   external_media_channel_id)r$   r}   r(   rs   r   r%   r%   r&   _find_call_for_channel  s   

z'CallStateMachine._find_call_for_channelc                 C   sT   | dpd}|drdS | d}|sdS ||jkrdS |dr%dS ||jv S )Nr`   rE   zPJSIP/Tr   Fr   )r/   r   r   rx   )r$   r\   rs   r`   r}   r%   r%   r&   _is_customer_channel  s   




z%CallStateMachine._is_customer_channelreasonc                 C   s<   |j dv rd S |r|js||_|d td|j| d S )Nr   r   zLlamada %s finalizada (%s))rw   disconnect_reasonfinalizerR   r   r   )r$   rs   r   r%   r%   r&   	_end_call  s   


zCallStateMachine._end_callc                    sD   | di }| d}| ||}|r | ||r | |d |S )Nr\   r   zStasisEnd cliente)r/   r   r   r   )r$   r(   r\   r}   rs   r%   r%   r&   r1     s   
zCallStateMachine._on_stasis_endc                    s~   | di }| d}| d}|sd S | j|}|sd S |dkr3|jdv r3d|_|js1d|_|S |dkr=|jd	kr=	 |S )
Nr\   r   stateUp)rt   ru   talkingspeakingRingingrt   )r/   r   r   rw   rY   r   )r$   r(   r\   r}   r   rs   r%   r%   r&   r2     s"   

z)CallStateMachine._on_channel_state_changec                    s   | di }| d  rt  |  |}|sd S  |jv r- fdd|jD |_|j kr8d |_d|_| ||rJ| |d| d  |S )Nr\   r   c                    rF   r%   r%   )rG   cr}   r%   r&   rK     rL   z:CallStateMachine._on_channel_destroyed.<locals>.<listcomp>Fzcanal destruido r`   )	r/   r   r   r   rx   r   r   r   r   )r$   r(   r\   rs   r%   r   r&   r3     s    



z&CallStateMachine._on_channel_destroyed)NNNNNN)2__name__
__module____qualname__r   r
   r   r   CallResolverr   r:   
BotReadyFnr'   dictr   r	   r8   staticmethodr<   r>   rA   ri   tupler   r[   re   rh   rk   rr   r~   r   floatr   r   r4   r0   r   r   r   ry   rz   r   r   r|   r   r   r   r   r1   r2   r3   r%   r%   r%   r&   r      s    	


6
Y
v





&l"r   )'loggingrer   collections.abcr   r   typingr   
ari.clientr   ari.debug_logr   r   calls.modelsr   r	   calls.registryr
   configr   r   media.managerr   	getLoggerr   rR   setr   r:   __annotations__	frozensetrO   compile
IGNORECASEr?   r   r   r   r   r%   r%   r%   r&   <module>   s*   
 
 