Ë
    ÝÄ'jÑ!  ã                   ó²  — U 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mZmZm	Z	m
Z
 ddlmZ ddlmZ ddlmZ ddlmZmZ ddlmZ dd	lmZ d
dl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$m%Z%m&Z' d
dl(m)Z) d
dl*m+Z, d
dl-m.Z.  e.«         ej^                  e0«      Z1 ee¬«      Z2 edejf                  ¬«      Z4e2e4jj                  _2        e4jm                  ee«       e4jo                  d ed¬«      d¬«       e4jq                  e,«        G d„ de«      Z9ejt                  jw                  d«      D � ch c]#  } | jy                  «       sŒ| jy                  «       ’Œ% c} Z=e>e?   e@d<   deAde?d eBfd!„ZCd"ed dfd#„ZDd$eEd dfd%„ZFe4j�                  d&d'¬(«      e2j‘                  ej’                  «      d"ed)e
d*e9fd+„«       «       ZJe4j—                  d,d-¬(«      d.„ «       ZLyc c} w )/a  
FastAPI application entry point.
Handles webhook ingestion from Zammad and orchestrates:
  1. New-ticket guard (skip if already has replies)
  2. AI-generated acknowledgment comment posted back to Zammad
  3. Azure DevOps task creation under the current monthly User Story
é    N)ÚFastAPIÚRequestÚHTTPExceptionÚBackgroundTasks)ÚStaticFiles)Ú	BaseModel)ÚAny)ÚLimiterÚ_rate_limit_exceeded_handler)Úget_remote_address)ÚRateLimitExceededé   )Úsettings)Úensure_and_create)Úpost_commentÚ
get_ticketÚget_ticket_articlesÚget_customer_nameÚget_agent_name)Úgenerate_acknowledgmentÚclassify_activityÚsuggest_tagsÚ_has_provider©Úpublish)Úrouter)Úsetup_logging)Úkey_funczZammad Automation)ÚtitleÚdebugz/staticz
app/static)Ú	directoryÚstatic)Únamec                   ó&   — e Zd ZU dZeeef   ed<   y)ÚWebhookPayloadz0Expected body shape from Zammad webhook trigger.ÚticketN)Ú__name__Ú
__module__Ú__qualname__Ú__doc__ÚdictÚstrr	   Ú__annotations__© ó    ú+/var/www/html/zammad-automation/app/main.pyr%   r%   (   s   … Ù:Ø��c�‰NÔr/   r%   ú,Ú_allowed_ipsÚbodyÚ
sig_headerÚreturnc                 óô   — t         j                  sydt        j                  t         j                  j	                  «       | t
        j                  «      j                  «       z   }t        j                  ||«      S )z†Validate HMAC-SHA1 signature sent by Zammad (X-Hub-Signature header).
    Returns True unconditionally when WEBHOOK_SECRET is not set.Tzsha1=)	r   Úwebhook_secretÚhmacÚnewÚencodeÚhashlibÚsha1Ú	hexdigestÚcompare_digest)r3   r4   Úexpecteds      r0   Ú_verify_signaturer@   3   s[   € ô ×"Ò"ØØœŸ™Ü×Ñ×&Ñ&Ó(¨$´·±óç�iƒkñ€Hô ×Ñ˜x¨Ó4Ð4r/   Úrequestc                 óR  — t         sy| j                  j                  dd«      j                  d«      }|d   j	                  «       r|d   j	                  «       n#| j
                  r| j
                  j                  nd}|t         vr#t        j                  d|«       t        dd¬	«      ‚y)
z‡Block request if client IP is not in WEBHOOK_ALLOWED_IPS.
    Reads X-Forwarded-For first (set by Traefik/nginx) to get real client IP.NzX-Forwarded-ForÚ r1   r   z'Webhook blocked: IP %s not in allowlisti“  Ú	Forbidden©Ústatus_codeÚdetail)
r2   ÚheadersÚgetÚsplitÚstripÚclientÚhostÚlogÚwarningr   )rA   Ú	forwardedÚ	client_ips      r0   Ú
_verify_iprR   >   sŽ   € õ ØØ—‘×#Ñ#Ð$5°rÓ:×@Ñ@ÀÓE€IØ(1°!©×(:Ñ(:Ô(<�	˜!‘×"Ñ"Ô$ÐZa×ZhÒZhÀ7Ç>Á>×CVÒCVÐnp€IØœÑ$Ü�‰Ð=¸yÔIÜ¨°KÔ@Ð@ð %r/   Ú	ticket_idc           
   ƒ   óp  ‡‡‡‡K  — g Šd;dt         dt        dt         ddfˆfd„}	 t        | «      ƒ d{  –—† }t        |d   «      Š|j                  d	d«      Št        j
                  › d
| › �Št        |j                  dd«      «      ƒ d{  –—† }t        «       ƒ d{  –—† }i fdt        fˆˆˆˆfd„}d}	 t        | «      ƒ d{  –—† }|r|d   j                  dd«      nd} |dd«       t        |«      dkD  rAt        j                  d‰t        |«      «        |ddt        |«      › d�«        |ddi«       y |ddd«       t        «       r^	 t!        ‰|||«      ƒ d{  –—† }|j#                  dd«      }	t%        | |	«      ƒ d{  –—†  t        j                  d‰«        |d dd!«       n
 |d dd$«       t        j&                  s+t        j                  d%‰«        |d&dd'«        |dd(i«       yt)        j*                  t-        ‰|«      t/        ‰|«      «      ƒ d{  –—† \  }}t        j                  d)‰||«       t1        | ‰‰‰||¬*«      ƒ d{  –—† }|d+   r,t        j                  d,‰|d-   «        |d&dd.|d-   › d/�«       n+t        j                  d0‰|d-   «        |d&dd.|d-   › d1�«        ||d+   rd+nd2|d-   |d3   d4œ«       y7 �Œš7 �ŒE7 �Œ67 �Œ# t        $ r- t        j                  d‰«        |ddd«        |ddi«       Y yw xY w7 �Œ²7 �Œ�# t        $ r7}
t        j                  d"‰«        |d dt        |
«      dd# «       Y d}
~
�Œ�d}
~
ww xY w7 �Œ=7 �Œ# t        $ rI t        j                  d5| «       ‰j3                  d6dd7d8œ«       t5        d9t        | «      dd‰dd:œ«       Y yw xY w­w)<a‘  Background task: process a newly created Zammad ticket.

    Steps:
      1. Fetch ticket metadata + articles
      2. Guard: skip if ticket already has >1 article (already processed / updated)
      3. Send AI-generated acknowledgment comment to Zammad
      4. Create Azure DevOps task under current monthly User Story (if enabled)

    All steps emit SSE progress events visible in the web UI.
    rC   ÚlabelÚokrG   r5   Nc                 ó0   •— ‰j                  | ||dœ«       y )N©rU   rV   rG   )Úappend)rU   rV   rG   Ústepss      €r0   Ú_stepz_process_ticket.<locals>._stepW   s   ø€ Ø�‰˜u¨B¸&ÑAÕBr/   Únumberr   z/#ticket/zoom/Úcustomer_idr   Úextrac                 ó,   •— t        d‰‰‰‰dœ| ¥«       y )NÚprogress)ÚtypeÚticket_numberÚticket_titleÚ
ticket_urlrZ   r   )r^   r\   rZ   rd   r   s    €€€€r0   Ú_publish_progressz*_process_ticket.<locals>._publish_progressb   s$   ø€ Ü˜Z¸&Ø%*¸*Ø#ñ.à',ð.õ /r/   r3   zFetch ticket articlesTu'   Ticket #%s â†’ failed to fetch articlesFzCould not retrieve articlesÚstatusÚerrorr   u?   Ticket #%s â†’ %d articles found, not a new ticket â€” skippingzNew ticket checku,    articles exist â€” ticket already processedÚskippedu   Only 1 article â€” new ticketú
z<br>u%   Ticket #%s â†’ AI acknowledgment sentzAI acknowledgmentzSent to Zammadu'   Ticket #%s â†’ AI acknowledgment failedéP   u!   Skipped â€” no API key configuredu(   Ticket #%s â†’ DevOps disabled, skippingzDevOps taskzDisabled (DEVOPS_ENABLED=false)Údoneu&   Ticket #%s â†’ activity='%s' tags='%s')rS   rb   rc   rd   ÚactivityÚtagsÚcreatedu   Ticket #%s â†’ task %s createdÚtask_idzTask #z createdu&   Ticket #%s â†’ task %s already existedz already existsÚexistsÚtask_url)rf   ro   rq   zFailed to process ticket %sÚ
Processingu   Unexpected error â€” check logsrX   r`   )ra   rb   rc   rd   rZ   rf   )rC   )r,   Úboolr   rI   r   Ú
zammad_urlr   r   r+   r   Ú	ExceptionrN   Ú	exceptionÚlenÚinfoÚ
ai_enabledr   Úreplacer   Údevops_enabledÚasyncioÚgatherr   r   r   rY   r   )rS   r[   r&   Úcustomer_nameÚ
agent_namere   Ú	body_htmlÚarticlesÚackÚack_htmlÚerl   rm   Úresultr\   rZ   rd   r   s                 @@@@r0   Ú_process_ticketr†   J   sæ  ûè ø€ ð €EñC”Sð Cœdð C¬Cð C¸õ CðY[Ü! )Ó,×,ˆÜ�V˜HÑ%Ó&ˆØ—
‘
˜7 BÓ'ˆÜ ×+Ñ+Ð,¨N¸9¸+ÐFˆ
Ü/°·
±
¸=È!Ó0LÓM×MˆÜ)Ó+×+ˆ
à,.ñ 	/¤T÷ 	/ð 	/ð ˆ	ð	Ü0°Ó;×;ˆHÙ7?˜ ™Ÿ™¨°Ô3ÀRˆIÙÐ)¨4Ô0ô ˆx‹=˜1ÒÜ�H‰HÐVÐX^Ô`cÐdlÓ`mÔnÙÐ$ e´°H³¨Ð>jÐ-kÔlÙ˜x¨Ð3Ô4ØáÐ  $Ð(GÔHô Œ<ð?Ü3°E¸9ÀmÐU_Ó`×`�ØŸ;™; t¨VÓ4�Ü" 9¨hÓ7×7Ð7Ü—‘Ð@À&ÔIÙÐ)¨4Ð1AÕBñ
 Ð% uÐ.QÔRô ×&Ò&Ü�H‰HÐ?ÀÔHÙ�- Ð(IÔJÙ˜x¨Ð0Ô1Øô  'Ÿ~™~Ü˜e YÓ/Ü˜ 	Ó*ó 
÷ 
‰ˆ�$ô 	�‰Ð9¸6À8ÈTÔRä(ØØ ØØ!ØØô
÷ 
ˆð �)ÒÜ�H‰HÐ5°v¸vÀiÑ?PÔQÙ�- ¨°°yÑ0AÐ/BÀ(Ð'KÕLä�H‰HÐ=¸vÀvÈiÑGXÔYÙ�- ¨&°¸	Ñ1BÐ0CÀ?Ð(SÔTáØ#)¨)Ò#4‘i¸(Ø˜iÑ(Ø˜zÑ*ñ
õ 	ð] -ùð NùØ+ùð <úô ò 	Ü�M‰MÐCÀVÔLÙÐ)¨5Ð2OÔPÙ˜x¨Ð1Ô2Ùð		úð& aùà7úô ò ?Ü—‘ÐGÈÔPÙÐ)¨5´#°a³&¸¸"°+×>Ò>ûð?úð
ùð
úô, ò [Ü�‰Ð3°YÔ?Ø�‰˜|°5ÐDeÑfÔgÜ˜´c¸)³nØ!#°2ÀÐQXñZö 	[ð[üs!  †N6¥M! ³K´AM! ÂKÂM! ÂKÂM! Â6K ÃKÃ&K Ã+AM! Ä9N6Ä:M! ÅL Å LÅ!%L ÆLÆ$L Æ+AM! Ç0N6Ç1-M! ÈMÈ3M! ÉMÉA9M! ËN6ËM! ËM! ËM! ËK Ë3LÌM! ÌN6ÌLÌM! ÌL ÌL Ì	MÌ!,MÍM! ÍMÍM! ÍM! Í!AN3Î0N6Î2N3Î3N6z/webhook/ticket-createdzZammad webhook receiver)ÚsummaryÚbackground_tasksÚpayloadc              ƒ   ó"  K  — t        | «       | j                  «       ƒ d{  –—† }t        j                  rOt        j                  dt        | j                  «      «       t        j                  d|j                  d¬«      «       | j                  j                  dd«      }t        ||«      st        dd	¬
«      ‚|j                  j                  d«      }|st        dd¬
«      ‚t        d|dœ«       |j                  t        |«       d|dœS 7 Œë­w)zpReceive ticket-created webhook from Zammad.
    Validates IP, HMAC signature, then queues background processing.NzWebhook headers: %szWebhook body: %srz   )ÚerrorszX-Hub-SignaturerC   i‘  zInvalid signaturerE   Úidi�  zNo ticket.id in payloadÚreceived)ra   rS   Úaccepted)rf   rS   )rR   r3   r   r    rN   r+   rH   ÚdecoderI   r@   r   r&   r   Úadd_taskr†   )rA   rˆ   r‰   r3   ÚsigrS   s         r0   Úticket_createdr’   ¶   sØ   è ø€ ô
 ˆwÔà—‘“×€Dä‡~‚~Ü�	‰	Ð'¬¨g¯o©oÓ)>Ô?Ü�	‰	Ð$ d§k¡k¸ kÓ&CÔDà
�/‰/×
Ñ
Ð/°Ó
4€CÜ˜T 3Ô'Ü¨Ð4GÔHÐHà—‘×"Ñ" 4Ó(€IÙÜ¨Ð4MÔNÐNä�Z¨iÑ8Ô9Ø×Ñœo¨yÔ9Ø ¨yÑ9Ð9ð!  ús   ‚D¡D¢C,Dz/healthzHealth checkc               ƒ   ó   K  — ddiS ­w)z8Simple liveness probe for load balancers and monitoring.rf   rV   r.   r.   r/   r0   Úhealthr”   Ð   s   è ø€ ð �dÐÐùs   ‚)Mr*   r|   r;   r8   ÚjsonÚloggingÚfastapir   r   r   r   Úfastapi.staticfilesr   Úpydanticr   Útypingr	   Úslowapir
   r   Úslowapi.utilr   Úslowapi.errorsr   Úconfigr   Údevopsr   Úzammadr   r   r   r   r   Úair   r   r   r   ry   Úbroadcasterr   Úwebr   Ú
web_routerÚlogging_configr   Ú	getLoggerr'   rN   Úlimiterr    ÚappÚstateÚadd_exception_handlerÚmountÚinclude_routerr%   Úwebhook_allowed_ipsrJ   rK   r2   Úsetr,   r-   Úbytesrs   r@   rR   Úintr†   ÚpostÚlimitÚwebhook_rate_limitr’   rI   r”   )Úips   0r0   ú<module>rµ      sÓ  ðòó Û Û Û Û ß DÓ DÝ +Ý Ý ß 9Ý +Ý ,Ý Ý %ß dÕ dß eÓ eÝ  Ý %Ý )á „Ø€g×Ñ˜Ó!€á
Ð-Ô
.€ÙÐ'¨x¯~©~Ô>€Ø€‡	�	Ô Ø × Ñ Ð+Ð-IÔ JØ ‡	�	ˆ)‘[¨<Ô8¸x€	Ô HØ × Ñ �:Ô ô�Yô ð "×5Ñ5×;Ñ;¸CÓ@öØÀBÇHÁHÅJ€B‡H�H…Jò€ˆc�#‰hó ð
5˜Eð 5¨sð 5°tó 5ð	A˜ð 	A Dó 	Aði[ Sð i[¨Tó i[ðX ‡�Ð
#Ð-F€ÓGØ‡�ˆx×*Ñ*Ó+ð: 'ð :¸_ð :ÐWeò :ó ,ó Hð:ð0 ‡�ˆ˜N€Ó+ñó ,ñùòGs   Ä/GÅG