
    `gj                    x   d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlmZ ddlmZmZ ddlmZ ddlmZmZmZmZmZ ddlmZmZ ddlZddlmZmZ dd	lm Z m!Z!m"Z"m#Z#m$Z$m%Z%m&Z&m'Z'm(Z( dd
l)m*Z* ddl+m,Z, ddl-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5m6Z6  ejn                  e8      Z9dZ:dZ;dZ<dZ=dZ>dZ?dZ@deAdeeA   fdZBdeCdeAfdZDdeAdeEfdZFdeAdeEfdZGdddddd d!d"d#d$d%d&d'd(ZHdeAdeAfd)ZId*eCdeeeCeAf      fd+ZJd,eAd-eKdeAfd.ZLdeAdeEfd/ZMdeAdeEfd0ZNdeEfd1ZOd2edeEfd3ZP G d4 d5e       ZQy)6a  Signal messenger platform adapter.

Connects to a signal-cli daemon running in HTTP mode.
Inbound messages arrive via SSE (Server-Sent Events) streaming.
Outbound messages and actions use JSON-RPC 2.0 over HTTP.

Based on PR #268 by ibhagwan, rebuilt with bug fixes.

Requires:
  - signal-cli installed and running: signal-cli daemon --http 127.0.0.1:8080
  - SIGNAL_HTTP_URL and SIGNAL_ACCOUNT environment variables set
    N)OrderedDict)datetimetimezone)Path)AnyDictListOptionalTuple)quoteunquote)PlatformPlatformConfig)	BasePlatformAdapterMessageEventMessageTypeProcessingOutcome
SendResultcache_image_from_bytescache_audio_from_bytescache_document_from_bytescache_image_from_url)redact_phonemarkdown_to_signal)	$SIGNAL_BATCH_PACING_NOTICE_THRESHOLDSIGNAL_MAX_ATTACHMENTS_PER_MSGSIGNAL_RATE_LIMIT_MAX_ATTEMPTSSignalRateLimitError_extract_retry_after_seconds_format_wait_is_signal_rate_limit_error_signal_send_timeoutget_scheduleri  @i@  g       @       @      N@      >@g      ^@valuereturnc                     | j                  d      D cg c]#  }|j                         s|j                         % c}S c c}w )zASplit a comma-separated string into a list, stripping whitespace.,)splitstrip)r(   vs     K/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/signal.py_parse_comma_listr0   N   s-    ${{3/=!1779AGGI===s   ??datac                 8   | dd dk(  ry| dd dk(  ry| dd dk(  ry	t        |       d
k\  r| dd dk(  r	| dd
 dk(  ry| dd dk(  ryt        |       dk\  r	| dd dk(  ry| dd dk(  ryt        |       dk\  r | d   dk(  r| d   dz  dk(  r| d   dz  dk(  ryy| dd dk(  ryy) a  Guess file extension from magic bytes.

    Android Signal delivers voice notes as raw ADTS AAC frames, which share
    the ``0xFF 0xFx`` sync word with MPEG-1/2 Layer 3 (MP3). The byte-1
    layout disambiguates: ADTS packs ``ID layer protection_absent`` into
    bits 3-0, where ``ID`` is 0 for MPEG-2/4 AAC and ``layer`` is always
    0 for ADTS. A real MP3 frame has ``ID=1`` and ``layer`` in {1, 2, 3}.
    N   s   PNG.png   s   .jpgs   GIF8.gif   s   RIFF   s   WEBP.webps   %PDF.pdfs   ftyp.mp4s   OggS.oggr                  .aac.mp3s   PK.zipz.bin)len)r1   s    r/   _guess_extensionrG   S   s     BQx:BQx;BQx7
4yB48w.4":3HBQx7
4yA~$q)w.BQx7
4yA~$q'T/tAw~$.F
 GdNt#BQx5    extc                 &    | j                         dv S )N>   r7   r6   r4   .jpegr:   lowerrI   s    r/   _is_image_extrO   w   s    99;DDDrH   c                 &    | j                         dv S )N>   rC   .m4arD   r=   .wavrL   rN   s    r/   _is_audio_extrS   {   s    99;BBBrH   z
image/jpegz	image/pngz	image/gifz
image/webpz	audio/oggz
audio/mpegz	audio/wavz	audio/mp4z	audio/aacz	video/mp4zapplication/pdfzapplication/zip)r6   rK   r4   r7   r:   r=   rD   rR   rQ   rC   r<   r;   rE   c                 J    t         j                  | j                         d      S )z Map file extension to MIME type.zapplication/octet-stream)_EXT_TO_MIMEgetrM   rN   s    r/   _ext_to_mimerW      s    CIIK)CDDrH   aac_datac                    t        j                  d      }|sOdD ]J  }t        j                  j	                  |      s#t        j
                  |t        j                        sH|} n |st        j                  d       y	 t        j                  dd      5 }|j                  |        |j                  }ddd       dd d	z   }	 t        j                  |d
ddd|dddd|gdd      }|j                  dk7  r_t        j!                  d|j                  |j"                  j%                  dd      dd        	 ||fD ]  }	 t        j&                  |        yt+        |d      5 }|j-                         d	fcddd       ||fD ]  }	 t        j&                  |        S # 1 sw Y   xY w# t(        $ r Y zw xY w# t(        $ r Y Bw xY w# 1 sw Y   nxY w	 ||fD ]'  }	 t        j&                  |       # t(        $ r Y %w xY w y# ||fD ]'  }	 t        j&                  |       # t(        $ r Y %w xY w w xY w# t        j.                  $ r t        j!                  d       Y yt0        $ r t        j3                  d       Y yw xY w)uD  Losslessly remux raw ADTS AAC bytes into an MP4 (.m4a) container.

    Used by the Signal attachment cache so Android voice notes land on disk
    in a container that every major STT API (Groq, OpenAI, xAI, Mistral
    Voxtral) will accept. ``ffmpeg -c:a copy`` is a single demux/remux —
    no re-encode, no quality loss, sub-100ms for typical voice-note sizes.

    Returns ``(m4a_bytes, ".m4a")`` on success, or ``None`` if ffmpeg is
    missing, input is invalid, or remux fails for any reason. Callers
    must treat ``None`` as "pass through unchanged" and not raise.
    ffmpeg)z/opt/homebrew/bin/ffmpegz/usr/local/bin/ffmpegu2   Signal: ffmpeg not found, skipping AAC→M4A remuxNrC   F)suffixdeleterQ   z-yz	-loglevelerrorz-iz-c:acopyz	-movflagsz
+faststartT
   )capture_outputtimeoutr   u3   Signal: AAC→M4A remux failed (ffmpeg exit %d): %szutf-8replacei,  rbu(   Signal: AAC→M4A remux timed out (>10s)u   Signal: AAC→M4A remux error)shutilwhichospathisfileaccessX_OKloggerdebugtempfileNamedTemporaryFilewritename
subprocessrun
returncodewarningstderrdecodeunlinkOSErroropenreadTimeoutExpired	Exception	exception)	rX   rZ   prefixsrcsrc_pathdst_pathprocpfs	            r/   _remux_aac_to_m4ar      s    \\(#FK 	Fww~~f%"))FBGG*D	 IJ((uE 	 IIhxxH	  CR=6)	>>{GT8lHF#RD
 !#IOOT[[%7%7%KDS%Q  ) IIaL h% (vvx'( ( ) IIaL'	  	 ,  w ( ( ( ) IIaL h) IIaL 
 $$ AB 89s   H7 F5H7 A0H 7H7 ?F*H7 H #G5	H >H7 F9H7 F'#H7 *	F63H7 5F66H7 9	GH7 GH7 GH H7 G32H7 3	G?<H7 >G??H7 H4H#"H4#	H/	,H4.H/	/H44H7 7(J!J Jtextmentionsc                    |rd| vr| S t        |d d      }|D ]d  }|j                  dd      }|j                  dd      }|j                  d	      xs |j                  d
      xs d}d| }| d| |z   | ||z   d z   } f | S )zReplace Signal mention placeholders (\uFFFC) with readable @identifiers.

    Signal encodes @mentions as the Unicode object replacement character
    with out-of-band metadata containing the mentioned user's UUID/number.
    u   ￼c                 &    | j                  dd      S )Nstartr   rV   )ms    r/   <lambda>z"_render_mentions.<locals>.<lambda>   s    QUU7A5F rH   T)keyreverser   r   lengthr?   numberuuiduser@N)sortedrV   )r   r   sorted_mentionsmentionr   r   
identifierreplacements           r/   _render_mentionsr      s     xt+ X+FPTUO" BGQ'Xq)[[*Kgkk&.AKV
*&FU|k)D,AAB KrH   c                     | sy| j                  d      s| j                  d      ry	 t        j                  |        y# t        t        t
        f$ r Y yw xY w)zFReturn True if *value* already looks like a Signal service identifier.FzPNI:zu:T)
startswithr   UUID
ValueErrorAttributeError	TypeError)r(   s    r/   _is_signal_service_idr      sS    5#3#3D#9		%	2 s   > AAc                     | r| j                  d      sy| dd }|j                         xr dt        |      cxk  xr dk  S c S )z/Return True for a plausible E.164 phone number.+Fr?   N      )r   isdigitrF   )r(   digitss     r/   _looks_like_e164_numberr      sF    ((-12YF>>6S[ 6B 66 66rH   c                       y)z3Check if Signal runtime dependencies are available.T r   rH   r/   check_signal_requirementsr      s    rH   configc                 "   t        | di       xs i }|j                  dd      xs t        j                  dd      j	                         }|j                  dd      xs t        j                  dd      j	                         }t        |xr |      S )z-Check if Signal has enough config to connect.extrahttp_url SIGNAL_HTTP_URLaccountSIGNAL_ACCOUNT)getattrrV   rg   getenvr-   bool)r   r   r   r   s       r/   validate_signal_configr      sy    FGR(.BE		*b)MRYY7H"-MTTVHyyB'J2995Er+JQQSG$W%%rH   c                   6    e Zd ZdZej
                  ZdZdef fdZ	ddde
de
fdZd\d
Zd\dZd\dZd\dZdedd	fdZdee   dee   dd	fdZededee   fd       Zdee   dee   de
fdZdedd	fdZdededee   fdZdedefdZdedefd Z	 d]d!dd"d#d$ed%ed&ed'e
d(e
d)edefd*Zed+edeee e   f   fd,       Z!d-edefd.Z"d/edee
ee   f   fd0Z#	 	 d^ded-ed1ee   d2ee$eef      de%f
d3Z&d\d4Z'de
fd5Z(d]dedd	fd6Z)	 	 d_ded7e*e+eef      d2ee$eef      d8edd	f
d9Z,ded:e-d;e-d<edd	f
d=Z.	 d]ded>ed?ee   de%fd@Z/	 d]dedAedBed?ee   de%f
dCZ0	 	 d^dedAed?ee   dDee   de%f
dEZ1	 	 d^dedFed?ee   d1ee   de%f
dGZ2	 	 d^dedHed?ee   d1ee   de%f
dIZ3	 	 d^dedJed?ee   d1ee   de%f
dKZ4dedd	fdLZ5dedd	fdMZ6dedNedOedPe-de
f
dQZ7dedOedPe-de
fdRZ8dSe9dee   fdTZ:d]dSdUde
fdVZ;dSe9dd	fdWZ<dSe9dXdYdd	fdZZ=dede$eef   fd[Z> xZ?S )`SignalAdapterz6Signal messenger adapter using signal-cli HTTP daemon.Fr   c                 d   t         |   |t        j                         |j                  xs i }|j                  dd      j                  d      | _        |j                  dd      | _        |j                  dd      | _	        t        j                  dd      }t        t        |            | _        |j                  d	      }|t        |      | _        n+t        j                  d
d      j#                         dv | _        t        j                  dd      }t        t        |            | _        d | _        d | _        d | _        i | _        i | _        i | _        d| _        d| _        d | _        | j                  j9                         | _        t=               | _        d| _         d| _!        t=               | _"        d| _#        i | _$        i | _%        tM        jN                         | _(        tR        jU                  d| j                  tW        | j                        | j                  rd       y d       y )Nr   zhttp://127.0.0.1:8080/r   r   ignore_storiesTSIGNAL_GROUP_ALLOWED_USERSrequire_mentionSIGNAL_REQUIRE_MENTIONfalse)true1yesonSIGNAL_ALLOWED_USERS*F        i   g     r@i  z7Signal adapter initialized: url=%s account=%s groups=%senableddisabled),super__init__r   SIGNALr   rV   rstripr   r   r   rg   r   setr0   group_allow_fromr   r   rM   dm_allow_fromclient	_sse_task_health_monitor_task_typing_tasks_typing_failures_typing_skip_until_running_last_sse_activity_sse_responser-   _account_normalizedr   _recent_sent_timestamps_max_recent_timestamps_recent_sent_ttl_seconds_sent_message_timestamps_max_sent_message_timestamps_recipient_uuid_by_number_recipient_number_by_uuidasyncioLock_recipient_cache_lockrl   infor   )selfr   r   group_allowed_str_rm_cfgdm_allowed_str	__class__s         r/   r   zSignalAdapter.__init__  s   1"		*.EFMMcRyyB/#ii(8$? II&BBG #$56G$H I ))-.#'=D #%99-Ew#O#U#U#W[u#uD  #93? !2>!BC 48 26<@!68 1346"%7; $(<<#5#5#7  CN-$&)#(-% CN-%,/) :<&9;&%,\\^"M]]L$>"&"7"7Y	I=G	IrH   )is_reconnectr   r)   c                x  K   | j                   r| j                  st        j                  d       yd}	 | j	                  d| j                  d      syd}dd	lm} t        j                  d
 |             | _        	 	 | j                  j                  | j                    dd       d{   }|j                  dk7  rwt        j                  d|j                         	 | j                  sI| j                  r)| j                  j                          d{    d| _        |r| j!                          yyy	 d| _        t#        j"                         | _        t'        j(                  | j+                               | _        t'        j(                  | j/                               | _        t        j3                  d| j                          	 | j                  sI| j                  r)| j                  j                          d{    d| _        |r| j!                          yyy# t
        $ r!}t        j                  d|       Y d}~d}~ww xY w7 7 9# t
        $ r}t        j                  d| j                   |       Y d}~| j                  sJ| j                  r*| j                  j                          d{  7   d| _        |r| j!                          yyyd}~ww xY w7 # | j                  sJ| j                  r*| j                  j                          d{  7   d| _        |r| j!                          w w w xY ww)z4Connect to signal-cli daemon and start SSE listener.z7Signal: SIGNAL_HTTP_URL and SIGNAL_ACCOUNT are requiredFzsignal-phonezSignal accountTz4Signal: Could not acquire phone lock (non-fatal): %sNr   )platform_httpx_limitsr'   )rb   limits/api/v1/check      $@rb      z'Signal: health check failed (status %d)z)Signal: cannot reach signal-cli at %s: %szSignal: connected to %s)r   r   rl   r^   _acquire_platform_lockr}   ru   %gateway.platforms._http_client_limitsr   httpxAsyncClientr   rV   status_coder   aclose_release_platform_locktimer   r   create_task_sse_listenerr   _health_monitorr   r   )r   r   lock_acquireder   resps         r/   connectzSignalAdapter.connect]  s    }}DLLLLRS 	V..~t||M]^ M
 	P''=R=TU	2![[__m-LVZ_[[##s*LL!JDL\L\]  ==;;++,,..."&DK //1 !	 ! + !DM&*iikD#$001C1C1EFDN(/(;(;D<P<P<R(SD%KK14==A==;;++,,..."&DK //1 !	 !3  	VNNQSTUU	V \$ /  H$--YZ[ ==;;++,,..."&DK //1 !	 ! / ==;;++,,..."&DK //1 !	 !s   1L:H L:H 'L:=-I *I
+3I 5L:I L:5BK 5L:<K= L:	I&I<L:IL:
I L:	K!K:K >5L:3J64!L:KK L:6L7L!L77L:Nc                   K   d| _         | j                  r/| j                  j                          	 | j                   d{    | j
                  r/| j
                  j                          	 | j
                   d{    | j                  j                         D ]  }|j                           | j                  j                          | j                  r)| j                  j                          d{    d| _	        | j                          t        j                  d       y7 # t        j                  $ r Y w xY w7 # t        j                  $ r Y w xY w7 gw)zStop SSE listener and clean up.FNzSignal: disconnected)r   r   cancelr   CancelledErrorr   r   valuesclearr   r   r   rl   r   r   tasks     r/   
disconnectzSignalAdapter.disconnect  s    >>NN!!#nn$$ $$%%,,.////
 &&--/ 	DKKM	  ";;++$$&&&DK##%*+- %))  0))  's}   .ED&  D$D& &E,E ;D?<E  A2E2E31E$D& &D<9E;D<<E?E EEEEc           	      z  K   | j                    dt        | j                  d       }t        }| j                  r	 t
        j                  d|       | j                  j                  d|ddid	      4 d{   }|| _	        t        }t        j                         | _        t
        j                  d
       d}|j                         2 3 d{   }| j                  s n||z  }d|v s!|j                  dd      \  }}|j                         }|s-|j!                  d      rt        j                         | _        X|j!                  d      r^|dd j                         }|st        j                         | _        	 t#        j$                  |      }| j'                  |       d{    d|v rǌddd      d{    | j                  rM|dz  t9        j8                         z  }
t/        j:                  ||
z          d{    t=        |dz  t>              }| j                  rd| _	        y7 7 g7 # t"        j(                  $ r t
        j                  d|dd        Y t*        $ r t
        j-                  d       Y w xY w6 7 # 1 d{  7  sw Y   xY w# t.        j0                  $ r
 Y d| _	        yt2        j4                  $ r.}	| j                  rt
        j7                  d|	|       Y d}	~	8d}	~	wt*        $ r.}	| j                  rt
        j7                  d|	|       Y d}	~	md}	~	ww xY w7 4w)z-Listen for SSE events from signal-cli daemon.z/api/v1/events?account=r   )safezSignal SSE: connecting to %sGETAcceptztext/event-streamN)headersrb   zSignal SSE: connected
r?   :zdata:   zSignal SSE: invalid JSON: %sd   z Signal SSE: error handling eventz2Signal SSE: HTTP error: %s (reconnecting in %.0fs)z-Signal SSE: error: %s (reconnecting in %.0fs)g?r5   ) r   r   r   SSE_RETRY_DELAY_INITIALr   rl   rm   r   streamr   r   r   r   
aiter_textr,   r-   r   jsonloads_handle_envelopeJSONDecodeErrorr}   r~   r   r  r   	HTTPErrorru   randomsleepminSSE_RETRY_DELAY_MAX)r   urlbackoffresponsebufferchunklinedata_strr1   r   jitters              r/   r   zSignalAdapter._sse_listener  s    6uT\\PR7S6TU)mm1`;SA;;--3%':;  .  &Y &Y )1D&5G.2iikD+KK 78F'/':':'< Y Ye#}}!%"fn+1<<a+@LD&#'::<D#' (  $s3:>))+ 7 (#w7+/8>>+;'/$,:>))+ 7!Y+/::h+?D*.*?*?*E$E$E' #fn&Y &Yb }} 38mmGf$4555gk+>?q mmt "o&YY. %F'+';'; !a$*LL1OQYZ^[^Q_$`'0 !Y$*$4$45W$X!Y5 (=&Y &Y &Y &YP ))  " ?? e==NN#WYZ\cd `==NN#RTUW^_` 6s  :L;;J$ 8H29J$ <AJ
JH5JJ+BJ?)H:(H8)H:-J2J3J$ >J?J$ AL;L8#L;*L;2J$ 5J8H::,J&J(JJJJJ$ J!JJ!J$ $L57	L; L5#K;5L;;L5#L0*L;0L55L;c                   K   | j                   rt        j                  t               d{    | j                   syt	        j                         | j
                  z
  }|t        kD  rt        j                  d|       	 | j                  j                  | j                   dd       d{   }|j                  dk(  r/t	        j                         | _        t        j                  d       n0t        j                  d|j                         | j                          | j                   ryy7 7 # t        $ r0}t        j                  d	|       | j                          Y d}~Id}~ww xY ww)
z;Monitor SSE connection health and force reconnect if stale.Nz2Signal: SSE idle for %.0fs, checking daemon healthr   r   r   r   z Signal: daemon healthy, SSE idlez3Signal: health check failed (%d), forcing reconnectz1Signal: health check error: %s, forcing reconnect)r   r   r  HEALTH_CHECK_INTERVALr   r   HEALTH_CHECK_STALE_THRESHOLDrl   ru   r   rV   r   r   rm   _force_reconnectr}   )r   elapsedr  r   s       r/   r   zSignalAdapter._health_monitor  s    mm-- 5666==iikD$;$;;G55SU\],!%==/7 "1 " D ''3. 37))+/%GH'\^b^n^no--/' mm6 ! ,NN#VXYZ))++,sS   *E0D/AE0?-D4 ,D2-A2D4 E0-E02D4 4	E-=&E(#E0(E--E0c                 T   | j                   r| j                   j                  sv	 t        j                  | j                   j	                               }| j
                  j                  |       |j                  | j
                  j                         d| _         yyy# t        $ r Y w xY w)z7Force SSE reconnection by closing the current response.N)
r   is_stream_consumedr   r   r   _background_tasksaddadd_done_callbackdiscardr}   r  s     r/   r+  zSignalAdapter._force_reconnect  s    d&8&8&K&K**4+=+=+D+D+FG&&**40&&t'='='E'EF "&D 'L
  s   A-B 	B'&B'envelopec                   ,K   |j                  d|      }d}d|v r|j                  d      }|rt        |t              r|j                  d      }|rt        |t              r|j                  d      xs |j                  d      }|j                  d      }|j                  d      xs i }|r|j                  d	      nd
}	|| j                  k(  s|	r| j	                  |      ry
d}i |d|i}|sy
|j                  d      xs$ |j                  d      xs |j                  d      }
|j                  dd      }|j                  dd      }| j                  |
|       |
st        j                  d       y
| j                  r|
| j                  k(  r|sy
| j                  r|j                  d      ry
|j                  d      xs$ |j                  d      xs i j                  d      }|sy
|j                  d      }|r|j                  d	      nd
}t        |      }|ra| j                  st        j                  d       y
d| j                  vr1|| j                  vr#t        j                  d|r
|d
d        y
d       y
|s|
nd| }|rdnd}|j                  dd      }|j                  dg       }|r|rt        ||      }|rh| j                  r\| j                  ,,xr d, |xs dv }t        ,fd |j                  d      xs g D              }|s|st        j                  d!       y
|rw|ru| j                  ,,rg|j                  d, d      }| j                  j                  ,      }|r|j                  d| d      }|j                  d"d#      j!                         }|j                  d$      xs i }|j                  d%      rt#        |j                  d%            nd
}|j                  d&      }| j%                  |      }|j                  d'      xs |j                  d(      }| j'                  ||      }|j                  d)g       }g }g } |rt)        | d*d      s|D ]  }!|!j                  d%      }"|!j                  d+d,      }#|"s)|#t*        kD  rt        j-                  d-|#       I	 | j/                  |"       d
{   \  }$}%|$r@|!j                  d.      xs t1        |%      }&|j3                  |$       | j3                  |&        |r|j!                         s5|s3t        j                  d0t9        |
      |rt;        |             y
d,       y
| j=                  ||r|j                  d1      n|||
|xs |
|r|nd
|r|nd
2      }'t>        j@                  }(| ryt        d3 | D              rt>        jB                  }(nVt        d4 | D              rt>        jD                  }(n3t        d5 | D              rt>        jF                  }(nt>        jH                  }(|j                  dd,      })|)r*	 tK        jL                  |)d6z  tN        jP                  7      }*n$tK        jV                  tN        jP                  7      }*tY        |'|xs d|(|| |*|
|)|r|nd
d8|||||9      }+t        j                  d:t9        |
      |d
d; |xs dd
d<        | j[                  |+       d
{    y
7 # t4        $ r t        j7                  d/|"       Y w xY w# tR        tT        f$ r' tK        jV                  tN        jP                  7      }*Y w xY w7 gw)=z(Process an incoming signal-cli envelope.r3  FsyncMessagesentMessagedestinationNumberdestination	timestamp	groupInfogroupIdNTdataMessagesourceNumber
sourceUuidsource
sourceNamer   z(Signal: ignoring envelope with no senderstoryMessageeditMessagez>Signal: ignoring group message (no SIGNAL_GROUP_ALLOWED_USERS)r   z!Signal: group %s not in allowlistr9   ?group:groupdmmessager   r   c              3   r   K   | ].  }|j                  d       k(  xs |j                  d      k(   0 yw)r   r   Nr   ).0r   account_norms     r/   	<genexpr>z1SignalAdapter._handle_envelope.<locals>.<genexpr>w  s;      ( h</P155=L3PP(s   47zHSignal: ignoring group message (require_mention=true, bot not mentioned)z   r   idr   
authorNameauthorProfileNameattachmentsignore_attachmentssizer   z1Signal: attachment too large (%d bytes), skippingcontentTypez%Signal: failed to fetch attachment %sz>Signal: skipping contentless envelope from %s (%d attachments)	groupName)chat_id	chat_name	chat_typeuser_id	user_nameuser_id_altchat_id_altc              3   >   K   | ]  }|j                  d         yw)zaudio/Nr   rI  mts     r/   rK  z1SignalAdapter._handle_envelope.<locals>.<genexpr>  s     Ar2==*A   c              3   >   K   | ]  }|j                  d         yw)zimage/Nr]  r^  s     r/   rK  z1SignalAdapter._handle_envelope.<locals>.<genexpr>       CR]]8,Cr`  c              3   >   K   | ]  }|j                  d         yw)zvideo/Nr]  r^  s     r/   rK  z1SignalAdapter._handle_envelope.<locals>.<genexpr>  rb  r`    )tz)sendertimestamp_msr   )r?  r   message_type
media_urlsmedia_typesr9  raw_messagereply_to_message_idreply_to_textreply_to_author_idreply_to_author_namereply_to_is_own_messagez!Signal: message from %s in %s: %s   2   ).rV   
isinstancedictr   _consume_sent_timestamp_remember_recipient_identifiersrl   rm   r   r   r   r   r   anyrc   r   r-   str_extract_quote_author_quote_references_own_messager   SIGNAL_MAX_ATTACHMENT_SIZEru   _fetch_attachmentrW   appendr}   r~   r   rF   build_sourcer   TEXTVOICEPHOTOVIDEODOCUMENTr   fromtimestampr   utcr   ry   nowr   handle_message)-r   r3  envelope_datais_note_to_selfsync_msgsent_msgdestsent_tssent_msg_group_infosent_msg_group_idrf  sender_namesender_uuiddata_message
group_infogroup_idis_grouprU  rW  r   r   mentioned_in_textmentioned_in_metadatabot_uuid
quote_datareply_to_idrm  reply_to_authorro  reply_to_is_ownattachments_datari  rj  attatt_idatt_sizecached_pathrI   content_typer?  msg_typets_msr9  eventrJ  s-                                               @r/   r  zSignalAdapter._handle_envelope  s;     !Z:  M)$((7HJx6#<<6
8T :#<<(;<[]@[D&ll;7G*2,,{*C*Ir'Na(;(?(?	(Jgk%t777;L77@"*.(R=(R-(R" n- +  .+  * 	
 $''b9#''b9,,V[ALLCD ##$2J2J(JSb =#4#4^#D
 m, K!!-06B;;MJ 	  "%%k2
0::>>),> ((]^$///HDDYDY4Y@RZ(2A,d add !)&xj.A'GT	 	2.##J3H#D(3D ,,33L , !L>"tzr2  %( (&**:6<"( %! %-B^  33L||a~$6;99==lK<<!H:;D
 ||D#.446 "%%g.4"
3=>>$3Gc*.../T"v.44Z@)~~l;bz~~Na?b<<[/Z (++M2>
GD2F$N' V7761-88NN#VX`aV-1-C-CF-K'K$K"'*ww}'='RcAR"))+6#**<8V. DJJL*LLPV$c*o  JK  ""5?jnn[1[!+V'2$,$ # 
 ##A[AA&,,C{CC&,,C{CC&,, '// !!+q1:$2254<HLLQ	 !5I
 !!#  %'1t
 !,'.!5$3!
& 	8"6*GCRL4:2s:K	M !!%(((g (L ! V$$%LfUV^ ( :$LLHLL9	:< 	)so   R-]1['[$A	['D]/(\ B]]]$[''\
]	\

]3] ]]]r   
service_idc                 `    |r|rt        |      sy|| j                  |<   || j                  |<   y)u?   Cache any number↔UUID mapping observed from Signal envelopes.N)r   r   r   )r   r   r  s      r/   rv  z-SignalAdapter._remember_recipient_identifiers  s2    Z/DZ/P1;&&v.5;&&z2rH   r  c                 t    t        | t              sydD ]"  }| j                  |      }|st        |      c S  y)zGReturn the best available Signal sender identifier from quote metadata.N)authorauthorNumber
authorUuid	authorAciauthorServiceIdauthorServiceIdString)rs  rt  rV   rx  )r  r   r(   s      r/   ry  z#SignalAdapter._extract_quote_author  sD     *d+
 
	"C NN3'E5z!
	" rH   r  r  c                 n   |rt        |      | j                  v ry|syt        |      j                         }| j                  r|| j                  k(  ry| j                  j                  | j                        }|r||k(  ry| j                  j                  |      }t        |xr || j                  k(        S )zCTrue when a Signal quote points at this adapter's outbound message.TF)rx  r   r-   r   r   rV   r   r   )r   r  r  r  cached_uuidcached_numbers         r/   rz  z+SignalAdapter._quote_references_own_message  s     3{+t/L/LL_%++-##$2J2J(J44889Q9QR6[066::6BMOmt7O7O&OPPrH   r9  c                 :   |yt        |      }| j                  j                  |d       d| j                  |<   t        | j                        | j                  kD  r@| j                  j                  d       t        | j                        | j                  kD  r?yy)zFKeep a bounded cache of outbound Signal timestamps for quote matching.NFlast)rx  r   poprF   r   popitem)r   r9  r   s      r/    _remember_sent_message_timestampz.SignalAdapter._remember_sent_message_timestamp1  s    )n 	%%))#t4-1%%c*$//043T3TT))11u1= $//043T3TTrH   contactphone_numberc                 z   t        |t              sy|j                  d      }|j                  d      }|j                  d      xs |j                  d      }|sE|j                  d      }t        |t              r$|j                  d      xs |j                  d      }|rt        |      r||k(  xs ||k(  }|r|S y)zGBest-effort extraction of a Signal service ID from listContacts output.Nr   	recipientr   	serviceIdprofile)rs  rt  rV   r   )r   r  r  r   r  r  r  matches_numbers           r/   _extract_contact_uuidz#SignalAdapter._extract_contact_uuid>  s    '4(X&KK,	[[(DGKK,D
kk),G'4($[[5LV9L
/
;#|3PyL7PN!!rH   rU  c                   K   |r'|j                  d      st        |      st        |      s|S | j                  j	                  |      }|r|S | j
                  4 d{    | j                  j	                  |      }|r|cddd      d{    S | j                  d| j                  dd       d{   }t        |t              rT|D ]O  }t        |t              r|j	                  d      nd}| j                  ||      }|s;|s>| j                  ||       Q | j                  j	                  ||      cddd      d{    S 7 7 7 7 # 1 d{  7  sw Y   yxY ww)zCReturn the preferred Signal recipient identifier for a direct chat.rD  NlistContactsT)r   allRecipientsr   )r   r   r   r   rV   r   _rpcr   rs  listrt  r  rv  )r   rU  cachedcontactsr  r   r  s          r/   _resolve_recipientz SignalAdapter._resolve_recipientQ  sd     !!(+$W-*73N//33G<M-- 	H 	H3377@F	H 	H 	H
 "YY~<<!%8  H (D)' QG6@$6OW[[2UYF!%!;!;GW!MJ*<<VZP	Q 1155gwG!	H 	H 	H 	H
	H 	H 	H 	Hs   AE'E
E'!E E'EE'"E4E5AEE	/E8E'EE'E'EE'E$EE$ E'attachment_idc                    K   | j                  d| j                  |d       d{   }|syt        |t              r)|j	                  d      }|st
        j                  d       yt        j                  |      }t        |      }|dk(  r)t        j                  t        |       d{   }||\  }}t        |      rt        ||      }||fS t        |      rt!        ||      }||fS t#        ||      }||fS 7 7 Sw)zCFetch an attachment via JSON-RPC and cache it. Returns (path, ext).getAttachment)r   rM  N)Nr   r1   z.Signal: attachment response missing 'data' keyrC   )r  r   rs  rt  rV   rl   ru   base64	b64decoderG   r   	to_threadr   rO   r   rS   r   r   )r   r  resultraw_datarI   remuxedrh   s          r/   r|  zSignalAdapter._fetch_attachmentu  s    yy||3
  
  fd#ZZ'FOP ##F+x( &=9@9J9JK\^f9g3gG" '#)(C8D Sy 3)(C8D Sy -Xs;DSyM6 4hs#   #C>C:BC>(C<)AC><C>Tr'   )log_failuresraise_on_rate_limitrb   methodparamsrpc_idr  r  rb   c                D  K   | j                   st        j                  d       y|%| dt        t	        j                         dz         }d|||d}	 | j                   j                  | j                   d||       d{   }|j                          |j                         }	d	|	v r|	d	   }
|rYt        |
      rNt        |
t              rt        |
j                  d
d            n
t        |
      }t        |
      }t        ||      |rt        j                  d||
       yt        j!                  d||
       y|	j                  d      }t        |t              rm|rk|j                  d      }t        |t"              rJ|D ]E  }t        |t              s|j                  d      dk(  s)|j                  d      }t        d|       |S 7 K# t        $ r  t$        $ r;}|rt        j                  d||       nt        j!                  d||       Y d}~yd}~ww xY ww)u  Send a JSON-RPC 2.0 request to signal-cli daemon.

        When ``log_failures=False``, error and exception paths log at DEBUG
        instead of WARNING — used by the typing-indicator path to silence
        repeated NETWORK_FAILURE spam for unreachable recipients while
        still preserving visibility for the first occurrence and for
        unrelated RPCs.

        When ``raise_on_rate_limit=True``, a Signal ``[429]`` /
        ``RateLimitException`` response raises ``SignalRateLimitError``
        instead of being swallowed — lets callers (multi-attachment send)
        opt into backoff-retry without changing default behaviour.
        z+Signal: RPC called but client not connectedN_rd  z2.0)jsonrpcr  r  rM  z/api/v1/rpc)r  rb   r^   rG  r   )retry_afterzSignal RPC error (%s): %sr  resultstypeRATE_LIMIT_FAILUREretryAfterSecondsz!Rate limit exceeded for recipientzSignal RPC %s failed: %s)r   rl   ru   intr   postr   raise_for_statusr  r"   rs  rt  rx  rV   r    r   rm   r  r}   )r   r  r  r  r  r  rb   payloadr  r1   errerr_msgr  r  r  rr   s                    r/   r  zSignalAdapter._rpc  s     . {{NNHI>xqTYY[4%7!8 9:F 	
(	))==/- *  D
 !!#99;D$7m&237AKCQUAV#cggi&<"=\_`c\d&B3&G27TTNN#>L  LL!<fcJXXh'F&$',? **Y/gt,$ u%a.155=DX3X*+%%0C*DK"67Zhs"ttu
 M=@ $ 	 	961E7C	sh   AH .G GB!G $H %G <H =AG G , G H G H"1HH HH r   c                     t        |       S )zCBackward-compatible wrapper around shared Signal formatting helper.r   )r   s    r/   _markdown_to_signalz!SignalAdapter._markdown_to_signal  s     "$''rH   contentc                     |S )zStrip markdown for plain-text fallback (used by base class).

        The actual rich formatting happens in send() via _markdown_to_signal().
        r   )r   r  s     r/   format_messagezSignalAdapter.format_message  s	     rH   r  c                 ^   |rt        |t              sy|j                  d      }t        |t              ry|D ]t  }t        |t              s|j                  d      }|r|dk7  rdt	        |      fc S d|v s@|j                  d      rR|j                  d      }|rdt	        |      fc S  y y)	z^Validate signal-cli send response results.

        Returns (success, error_message).
        )TNr  r  SUCCESSFsuccessfailure)FzRecipient delivery failed)rs  rt  rV   r  rx  )r   r  r  r  rtypefails         r/   _validate_send_resultz#SignalAdapter._validate_send_result  s    
 Z5**Y'gt$ 
>!!T*fUi/ #e*,,>!%%	*:55+D$c$i//=
> rH   reply_tometadatac                 P  K   | j                  |       d{    | j                  |      \  }}| j                  |d}|rt        |      dk(  r	|d   |d<   n||d<   |j	                  d      r	|dd |d	<   n| j                  |       d{   g|d
<   t        j                  dt        |      |       | j                  d|       d{   }|B| j                  |      \  }	}
|	st        d|
|      S | j                  |       t        dd      S t        dd      S 7 7 7 Zw)z2Send a text message with native Signal formatting.Nr   rG  r?   r   	textStyle
textStylesrD     r;  r  z*[Signal] Sending response (%d chars) to %ssendFr  r^   raw_responseT)r  
message_idzRPC send failedr  r^   )_stop_typing_indicatorr  r   rF   r   r  rl   r   r  r  r   _track_sent_timestamp)r   rU  r  r  r  
plain_texttext_stylesr  r  r  r  s              r/   r  zSignalAdapter.send  s;     ))'222"&":":7"C
K ||!"

 ;1$&1!n{#'2|$h' 'F9)-)@)@)I#I"JF;@#j/SZ[yy00#99&AGW!%wVTT&&v. dt<<%/@AAA 	3$ $J 1s5   D&DA3D&D"=D&	D$
AD&"D&$D&c                    t        |t              r|j                  d      nd}|r-| j                  |       t	        j
                         }| j                  j                  |d       || j                  |<   || j                  z
  }| j                  r_t        t        | j                  j                                     \  }}||k  r| j                  j                  d       nn| j                  r_t        | j                        | j                  kD  r@| j                  j                  d       t        | j                        | j                  kD  r?yyy)z:Record outbound message timestamp for echo-back filtering.r9  NFr  )rs  rt  rV   r  r   	monotonicr   r  r   nextiteritemsr  rF   r   )r   
rpc_resulttsr  cutoff	oldest_ts	oldest_ats          r/   r  z#SignalAdapter._track_sent_timestampI  s   ,6z4,HZ^^K(d11"5.."C((,,R6/2D((,4888F..'+D1M1M1S1S1U,V'W$	9v%0088e8D .. d223d6Q6QQ,,44%4@ d223d6Q6QQ rH   c                 ^    |r+|| j                   v r| j                   j                  |d       yy)z@Pop a timestamp if it matches one we sent. Returns True on echo.NTF)r   r  )r   r  s     r/   ru  z%SignalAdapter._consume_sent_timestamp^  s.    "444((,,R6rH   c                 z  K   t        j                         }| j                  j                  |d      }||k  ryd| j                  i}|j                  d      r	|dd |d<   n| j                  |       d{   g|d<   | j                  j                  |d      }| j                  d	|d
|dk(         d{   }|B|dz  }|| j                  |<   |dk\  r(t        ddd|dz
  z  z        }||z   | j                  |<   yy| j                  j                  |d       | j                  j                  |d       y7 7 w)a?  Send a typing indicator.

        base.py's ``_keep_typing`` refresh loop calls this every ~2s while
        the agent is processing. If signal-cli returns NETWORK_FAILURE for
        this recipient (offline, unroutable, group membership lost, etc.)
        the unmitigated behaviour is: a WARNING log every 2 seconds for as
        long as the agent keeps running. Instead we:

        - silence the WARNING after the first consecutive failure (subsequent
          attempts log at DEBUG) so transport issues are still visible once
          but don't flood the log,
        - skip the RPC entirely during an exponential cooldown window once
          three consecutive failures have happened, so we stop hammering
          signal-cli with requests it can't deliver.

        A successful sendTyping clears the counters.
        r   Nr   rD  r  r;  r  r   
sendTypingtypingr  r  r?      r&   g      0@r5   )r   r  r   rV   r   r   r  r   r  r  r  )	r   rU  r  r  
skip_untilr  failsr  r!  s	            r/   send_typingzSignalAdapter.send_typinge  sZ    $ nn,,00#>
 t||"
 h' 'F9)-)@)@)I#I"JF;%%))'15yy1*	 ! 
 
 >QJE-2D!!'* zdDA%!),<$=>36=''0  !!%%gt4##''6+ $J
s%   A3D;5D76?D;5D96BD;9D;imageshuman_delayc                 2  K   |syt               }t        j                  dt        |      |dd |j	                                | j                  |       d{    g }d}d}d}	|D ]  \  }
}|
j                  d      rt        |
dd       }n	 t        |
       d{   }|rt        |      j                         st        j                  d	|
       |dz  }qt        |      j                         j                  }|t        kD  rt        j                  d
||
       |	dz  }	|j!                  |        |s#t        j#                  dt        |      |||	       yt        j                  dt        |      t        |             | j$                  dd}|j                  d      r	|dd |d<   n| j'                  |       d{   g|d<   t)        dt        |      t*              D cg c]  }|||t*        z     }}t-        |      D ]u  \  }}t        |      }|j/                  |      }t        j1                  d|dz   t        |      ||       |t2        k\  r(| j5                  ||dz   t        |      |       d{    t7        ||      }t9        |      }t)        dt:        dz         D ]  }|j=                  |       d{    	 t?        j@                         }| jC                  d|d|       d{   }t?        j@                         |z
  }|| jE                  |      \  }}|rW| jG                  |       |jI                  ||       d{    t        j                  d|dz   t        |      |||t:               nt        j#                  d|dz   t        |      ||t:        ||       |t:        k  rd|z  }t        j                  d|dz   t        |      |       tK        jL                  |       d{    Rt        j#                  d|dz   t        |      ||t:        |       |t:        k  rHd|z  }t        j                  d|dz   t        |      |       tK        jL                  |       d{     u x y7 &7 # t        $ r'}t        j                  d|
|       |dz  }Y d}~Hd}~ww xY w7 c c}w 7 G7 7 7 |7 7 ]# tN        $ r}|jQ                  |jR                  |       |t:        k\  rHt        j#                  d|dz   t        |      ||jR                  r|jR                  ddnd        Y d}~ At        j                  d!|dz   t        |      |t:        |jR                  r|jR                  ddnd        Y d}~d}~ww xY ww)"u  Send a batch of images via chunked Signal RPC calls.

        Per-image alt texts are dropped — Signal's send RPC only carries
        one shared message body. Bad images (download failure, missing
        file, oversize) are skipped with a warning so one bad URL
        doesn't lose the rest of the batch. ``human_delay`` is ignored:
        the rate-limit scheduler handles inter-batch pacing.
        NuP   Signal send_multiple_images: received %d image(s) for %s — scheduler state: %s   r   file://r   z'Signal: failed to download image %s: %sr?   z#Signal: image file not found for %sz/Signal: image too large (%d bytes), skipping %szKSignal: no valid images in batch of %d (download=%d missing=%d oversize=%d)zBSignal send_multiple_images: %d/%d images valid, sending in chunksr   r  rD  r  r;  r  z8Signal batch %d/%d: %d attachments, estimated wait=%.1fs)rP  r  T)r  rb   z@Signal batch %d/%d: %d attachments sent in %.1fs (attempt %d/%d)z_Signal: RPC send failed for batch %d/%d (%d attachments, attempt %d/%d, rpc_duration=%.1fs): %sr%   z0Signal: retrying batch %d/%d after %.1fs backoffz[Signal: RPC send failed for batch %d/%d (%d attachments, attempt %d/%d, rpc_duration=%.1fs)z`Signal: rate-limit retries exhausted on batch %d/%d (%d attachments lost, server retry_after=%s)z.0fsunknownziSignal: rate-limited on batch %d/%d (attempt %d/%d, server retry_after=%s); scheduler will pace the retry)*r$   rl   r   rF   stater  r   r   r   r}   ru   r   existsstatst_sizer{  r}  r^   r   r  ranger   	enumerateestimate_waitrm   r   _notify_batch_pacingrt  r#   r   acquirer   r  r  r  r  report_rpc_durationr   r  r   feedbackr  )r   rU  r  r  r  	schedulerrP  skipped_downloadskipped_missingskipped_oversize	image_url	_alt_text	file_pathr   	file_sizebase_paramsiatt_batchesidx	att_batchn	estimatedr  send_timeoutattempt_rpc_t0r  _rpc_durationr  r  r!  s                                  r/   send_multiple_imagesz"SignalAdapter.send_multiple_images  s     !O	"K"y'8	
 ))'222!#$* 	* Iy##I.#IabM2	&:9&E EI DO$:$:$<DiP1$Y,,.66I55EyR[ !A% y)1	*4 LL7F-@P
 Pc&k	
 ||'
 h'%,QR[K	".2.E.Eg.N(N'OK$ 1c+.0NO
 !<<=
 

 (4 W	NCIA!//2ILLJa[)1i @@//S1Wc+&6	   +9=F/2L $BQ$FG H''***F"nn.G#'99D, $- $ F %)NN$4w$>M)+/+E+Ef+M(" 66v>"+"?"?q"QQQ"KK!2 #a[)91m ')G	 #LL!I #a[)91 ')G -w  ')GG*-. &$V$'!GS-=w!" '.mmG&< < < ( A!GS%5q#%C) #%CC&)WnG"KK R #a[)97 #*--"888$mHW	w 	3 !F  NN#LiYZ[$)$P )O
 + R, !=$ 9 , &&q}}a8"@@K!GS%5q78}}q}}S13)	 NN8 a[!1!?34==1==-Q/i s  AVQ<4VRQ?RDV=R5>$V"R85BV6R=7AV>S ?V,S0S1AS
SB&S1S	2S6V8A5S-S.S2V?R	R2R-'V-R22V8V VSS	SS	VA&V<VAV	VVVnext_batch_idxtotal_batcheswait_sc           
         K   	 | j                  |dt        |       d| d| d       d{    y7 # t        $ r }t        j	                  d|       Y d}~yd}~ww xY ww)zInform the user when an inter-batch pacing wait crosses the
        notice threshold. Best-effort; logs and continues on failure.u!   (More images coming — pausing ~z for Signal rate limit, batch r   z.)Nz(Signal: failed to send pacing notice: %s)r  r!   r}   rl   ru   )r   rU  r:  r;  r<  r   s         r/   r"  z"SignalAdapter._notify_batch_pacingG  sq     	J))3L4H3I J00>/?qrS  
  	JNNEqII	Js6   A (4 24 A 4 	AAA AA r*  captionc                 r  K   | j                  |       d{    |j                  d      rt        |dd       }n	 t        |       d{   }|rt        |      j                         st        dd      S t        |      j                         j                  }|t        kD  rt        dd| d	      S | j                  |xs d
|gd}|j                  d      r	|dd |d<   n| j                  |       d{   g|d<   | j!                  d|       d{   }	|	A| j#                  |	      \  }
}|
st        d||	      S | j%                  |	       t        d      S t        dd      S 7 T7 ## t        $ r6}t
        j                  d|       t        dt        |            cY d}~S d}~ww xY w7 7 w)z4Send an image. Supports http(s):// and file:// URLs.Nr  r   z$Signal: failed to download image: %sFr  zImage file not foundzImage too large ( bytes)r   r   rG  rP  rD  r  r;  r  r  r  Tr  zRPC send with attachment failed)r  r   r   r   r}   rl   ru   r   rx  r   r  r  r  r{  r   r  r  r  r  )r   rU  r*  r>  kwargsr,  r   r-  r  r  r  r  s               r/   
send_imagezSignalAdapter.send_imageY  s     ))'222 	*	!".I?"6y"AA	
 Y 6 6 8e3IJJ O((*22	11e5FykQX3YZZ ||}"%;"
 h' 'F9)-)@)@)I#I"JF;yy00#99&AGW!%wVTT&&v.d++%/PQQO 	3 B ?EqI!%s1v>>?* $J0sp   F7E+$F7E1 E.E1 B(F78F39F7F5AF7.E1 1	F0:+F+%F0&F7+F00F75F7r,  media_labelc                   K   | j                  |       d{    	 t        |      j                         j                  }|t        kD  rt        d| d| d      S | j                  |xs d|gd}|j                  d	      r	|d
d |d<   n| j                  |       d{   g|d<   | j                  d|       d{   }|A| j                  |      \  }}	|st        d|	|      S | j                  |       t        d      S t        dd|j                          d      S 7 # t        $ r t        d| d|       cY S w xY w7 7 w)u   Send any file as a Signal attachment via RPC.

        Shared implementation for send_document, send_image_file, send_voice,
        and send_video — avoids duplicating the validation/routing/RPC logic.
        NFz file not found: r  z too large (r@  r   rA  rD  r  r;  r  r  r  TrB  z	RPC send z failed)r  r   r  r  FileNotFoundErrorr   r{  r   r   r  r  r  r  rM   )
r   rU  r,  rE  r>  r-  r  r  r  r  s
             r/   _send_attachmentzSignalAdapter._send_attachment  sm     ))'222	aY,,.66I 11ek],ykY`3abb ||}"%;"
 h' 'F9)-)@)@)I#I"JF;yy00#99&AGW!%wVTT&&v.d++%;;L;L;N:Ow/WXX9 	3 ! 	aek]BST]S^3_``	a $J0sR   ED$E#D'  AEE E=E
>A'E'EEEE
Efilenamec                 F   K   | j                  ||d|       d{   S 7 w)z Send a document/file attachment.FileNrH  )r   rU  r,  r>  rI  rC  s         r/   send_documentzSignalAdapter.send_document  s%      **7IvwOOOO   !!
image_pathc                 F   K   | j                  ||d|       d{   S 7 w)zSend a local image file as a native Signal attachment.

        Called by the gateway media delivery flow when MEDIA: tags containing
        image paths are extracted from agent responses.
        ImageNrL  )r   rU  rO  r>  r  rC  s         r/   send_image_filezSignalAdapter.send_image_file  %      **7JQQQQrN  
audio_pathc                 F   K   | j                  ||d|       d{   S 7 w)zSend an audio file as a Signal attachment.

        Signal does not distinguish voice messages from file attachments at
        the API level, so this routes through the same RPC send path.
        AudioNrL  )r   rU  rT  r>  r  rC  s         r/   
send_voicezSignalAdapter.send_voice  rS  rN  
video_pathc                 F   K   | j                  ||d|       d{   S 7 w)z)Send a video file as a Signal attachment.VideoNrL  )r   rU  rX  r>  r  rC  s         r/   
send_videozSignalAdapter.send_video  s%      **7JQQQQrN  c                   K   | j                   j                  |d      }|r|j                          	 | d{    	 d| j
                  i}|j                  d      r	|dd |d<   n| j                  |       d{   g|d<   d|d<   | j                  d	|d
d       d{    | j                  j                  |d       | j                  j                  |d       y7 # t        j                  $ r Y w xY w7 ~7 Z# t        $ r Y cw xY ww)z(Stop a typing indicator loop for a chat.Nr   rD  r  r;  r  Tstopr  ztyping-stopFr  )r   r  r  r   r  r   r   r  r  r}   r   r   )r   rU  r	  r  s       r/   r  z$SignalAdapter._stop_typing_indicator  s    !!%%gt4KKM

	&/%>F!!(+$+ABKy!-1-D-DW-M'M&N{#!F6N))$"	     	!!'40##GT27 ))  (N  	 	sy   /DC CC <C< 9C8:%C< C: C< $9DC C52D4C55D8C< :C< <	DDDDc                 B   K   | j                  |       d{    y7 w)u   Public interface for stopping typing — called by base adapter's
        _keep_typing finally block to clean up platform-level typing tasks.N)r  )r   rU  s     r/   stop_typingzSignalAdapter.stop_typing  s      ))'222s   emojitarget_authortarget_timestampc                    K   | j                   |||d}|j                  d      r	|dd |d<   n|g|d<   | j                  d|       d{   }|yt        j	                  d	|dd
 |       y7 "w)uf  Send a reaction emoji to a specific message via signal-cli RPC.

        Args:
            chat_id: The chat (phone number or "group:<id>")
            emoji: Reaction emoji string (e.g. "👀", "✅")
            target_author: Phone number / UUID of the message author
            target_timestamp: Signal timestamp (ms) of the message to react to
        )r   r`  targetAuthortargetTimestamprD  r  Nr;  r  sendReactionTz/Signal: sendReaction failed (chat=%s, emoji=%s)rq  F)r   r   r  rl   rm   )r   rU  r`  ra  rb  r  r  s          r/   send_reactionzSignalAdapter.send_reaction  s       ||)/	"
 h' 'F9#*)F;yy88FPSQSV[\	 9s   AA.	A,
#A.c                    K   | j                   d||dd}|j                  d      r	|dd |d<   n|g|d<   | j                  d	|       d{   }|duS 7 w)
z3Remove a reaction by sending an empty-string emoji.r   T)r   r`  rd  re  removerD  r  Nr;  r  rf  )r   r   r  )r   rU  ra  rb  r  r  s         r/   remove_reactionzSignalAdapter.remove_reaction9  sq      ||)/"
 h' 'F9#*)F;yy88T!! 9s   AA
A	Ar  c                     |j                   }t        |t              sy|j                  d      }|j                  d      }|r|sy||fS )zExtract (target_author, target_timestamp) from a MessageEvent.

        Returns None if the event doesn't carry the raw Signal envelope data
        needed for sendReaction.
        Nrf  rg  )rk  rs  rt  rV   )r   r  rawr  r  s        r/   _extract_reaction_targetz&SignalAdapter._extract_reaction_targetT  sJ     #t$"WW^$R|rH   r   c                     t        j                  dd      j                         dv ry|7t        t        |dd      dd      }|rd| j                  vr|| j                  vryy	)
u  Check if message reactions are enabled for this event.

        Two gates:
        1. SIGNAL_REACTIONS env var — set to false/0/no to disable globally.
        2. DM allowlist — if SIGNAL_ALLOWED_USERS is set, only react to
           messages from senders in that list.  This prevents unauthorized
           contacts from seeing the 👀 reaction (which fires before run.py's
           auth gate and would otherwise reveal that a bot is listening).
        SIGNAL_REACTIONSr   >   0nor   FNr?  rX  r   T)rg   r   rM   r   r   )r   r  rf  s      r/   _reactions_enabledz SignalAdapter._reactions_enabledc  se     99'0668<PPWUHd;YMF#T%7%77F$J\J\<\rH   c                    K   | j                  |      sy| j                  |      }|r1 | j                  |j                  j                  dg|  d{    yy7 w)u'   React with 👀 when processing begins.Nu   👀)rr  rm  rg  r?  rU  )r   r  targets      r/   on_processing_startz!SignalAdapter.on_processing_startu  sY     &&u-..u5$$$$U\\%9%96KFKKK Ks   AAAAoutcomer   c                   K   | j                  |      sy|t        j                  k(  ry| j                  |      }|sy|j                  j
                  } | j                  |g|  d{    |t        j                  k(  r | j                  |dg|  d{    y|t        j                  k(  r | j                  |dg|  d{    yy7 e7 87 
w)u   Swap the 👀 reaction for ✅ (success) or ❌ (failure).

        On CANCELLED we leave the 👀 in place — no terminal outcome means
        the reaction should keep reflecting "in progress" (matches Telegram).
        Nu   ✅u   ❌)
rr  r   	CANCELLEDrm  r?  rU  rj  r  rg  FAILURE)r   r  rv  rt  rU  s        r/   on_processing_completez$SignalAdapter.on_processing_complete}  s      &&u-'111..u5,,&&"d""74V444'///$$$$We=f===)111$$$$We=f=== 2 	5==s6   A'C)C*.CC/CC	CCCc                   K   |j                  d      r|d|dS | j                  d| j                  |d       d{   }|}|r8t        |t              r(|j                  d      xs |j                  d      xs |}|d	|dS 7 Fw)
z%Get information about a chat/contact.rD  rE  )rq   r  rU  
getContact)r   contactAddressNrq   profileNamerF  )r   r  r   rs  rt  rV   )r   rU  r  rq   s       r/   get_chat_infozSignalAdapter.get_chat_info  s     h'"  yy||%0
  
 j.::f%MM)BMgD 
 	
s   :BBAB)r)   N)N)NN)Nr   )@__name__
__module____qualname____doc__r   r   platformSUPPORTS_MESSAGE_EDITINGr   r   r   r  r
  r   r   r+  rt  r  r
   rx  rv  staticmethodr   ry  rz  r  r  r  tupler|  floatr  r  r  r  r  r   r   r  r  ru  r  r	   r   r9  r  r"  rD  rH  rM  rR  rW  r[  r  r_  rg  rj  r   rm  rr  ru  rz  r  __classcell__)r   s   @r/   r   r      s   @H  %OI~ OIj 5: *2T *2d *2X,F?"J,4	&i)t i) i)V<hsm <QYZ]Q^ <cg < # (3-  "Qc]Q "#Q 
	Q&># >$ >S  QT &H H HH(S (U (d 	M "$)MM M 	M M "M M 
Mf (# (%T#Y*? ( (c c C E$:M4N > #'-1(B(B (B 3-	(B
 4S>*(B 
(BTA*T 37 37 37r .2 kk U38_%k 4S>*	k
 k 
kZJJ J 	J
 J 
J, "&	/R/R /R #	/R 
/Rl "&(Y(Y (Y 	(Y
 #(Y 
(Y\ "&"&	P	P 	P #		P
 3-	P 
	P "&"&RR R #	R
 3-R 
R& "&"&RR R #	R
 3-R 
R& "&"&	R	R 	R #		R
 3-	R 
	R!3C !3D !3F3 3 3  	
  
B"" " 	"
 
"6l x  $ $L| L L>, >I\ >ae >2
3 
4S> 
rH   r   )Rr  r   r  r  loggingrg   r  re   rr   rn   r   r   collectionsr   r   r   pathlibr   r  r   r   r	   r
   r   urllib.parser   r   r   gateway.configr   r   gateway.platforms.baser   r   r   r   r   r   r   r   r   gateway.platforms.helpersr   gateway.platforms.signal_formatr   #gateway.platforms.signal_rate_limitr   r   r   r   r    r!   r"   r#   r$   	getLoggerr  rl   r{  MAX_MESSAGE_LENGTHTYPING_INTERVALr  r  r)  r*  rx  r0   bytesrG   r   rO   rS   rU   rW   r   r  r   r   r   r   r   r   r   rH   r/   <module>r     s       	       # '  3 3 '  3
 
 
 3 >
 
 
 
		8	$
 /     $ >S >T#Y >
!5 !S !HEs Et ECs Ct C
 <,{!2<MEc Ec E
4 4(53D*E 4n3 $ 3 *
 
 
73 74 74 
&> &d &n
' n
rH   