Ë
    þEBj£-  ã                  ó˜   — d Z ddlmZ ddlZddlZddlZddlmZ ddlmZ ddl	m
Z
  ej                  e«      Zg d¢ZdZd	Z G d
„ d«      Zy)aŠ  Account guard: detects FB account threats and suspends scraping.

Monitors Apify results for signs of blocking (checkpoint, captcha, private
group errors, consecutive failures). On threat detection:
  - Sets account_suspended flag in DB
  - Sends email alarm via SendGrid
  - scheduled_run checks flag on start and skips the cycle

Unsuspend is MANUAL (delete the flag row or run unsuspend()).
é    )ÚannotationsN)Údatetime)ÚPath)ÚSettings)Ú
checkpointÚcaptchazverify your identityzverify your accountÚblockedÚ	suspendedztemporarily lockedzunusual activityzconfirm your identityzPRIVATE GROUPÚloginzlog in to continueé   é   c                  ó    — e Zd ZdZdd„Zdd„Zdd„Zdd„Zdd„Zdd„Z	dd„Z
dd	„Zdd
„Zdd„Zdd„Zdd„Zdd„Zdd„Zdd„Zd d„Zdd„Zdd„Zy)!ÚAccountGuardz4Monitors FB scraping health and suspends on threats.c                óÆ   — || _         |j                  | _        d| _        d| _        d| _        t        j                  | j                  «      | _        | j                  «        y )Nr   )
Ú	_settingsÚdb_pathÚ_db_pathÚ_consecutive_failuresÚ_groups_with_zero_postsÚ_total_groups_this_cycleÚsqlite3ÚconnectÚ_connÚ_ensure_schema)ÚselfÚsettingss     úF/var/www/html/agent.samochodowy/src/agent_samochodowy/account_guard.pyÚ__init__zAccountGuard.__init__1   sN   € Ø!ˆŒØ ×(Ñ(ˆŒØ%&ˆÔ"Ø'(ˆÔ$Ø()ˆÔ%Ü—_‘_ T§]¡]Ó3ˆŒ
Ø×ÑÕó    c                ó¤   — | j                   j                  d«       | j                   j                  d«       | j                   j                  «        y )NzÂ
            CREATE TABLE IF NOT EXISTS account_guard (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
        z©
            CREATE TABLE IF NOT EXISTS daily_scan_count (
                date TEXT PRIMARY KEY,
                count INTEGER NOT NULL DEFAULT 0
            )
        )r   ÚexecuteÚcommit©r   s    r   r   zAccountGuard._ensure_schema>   sD   € Ø�
‰
×Ñð ô 	ð 	�
‰
×Ñð ô 	ð 	�
‰
×ÑÕr   c                óp   — | j                   j                  d«      j                  «       }|duxr |d   dk(  S )z<Check if account is suspended. Pipeline should skip if True.z7SELECT value FROM account_guard WHERE key = 'suspended'Nr   Útrue)r   r!   Úfetchone)r   Úrows     r   Úis_suspendedzAccountGuard.is_suspendedR   s=   € à�j‰j× Ñ ØEó
ç
‰(‹*ð 	ð ˜$ˆÒ3 3 q¡6¨VÑ#3Ð3r   c                ó@  — t        j                  «       j                  «       }| j                  j	                  d|f«       | j                  j	                  d||f«       | j                  j                  «        t        j                  d|«       | j                  |«       y)z)Suspend the account and send alarm email.z]INSERT OR REPLACE INTO account_guard (key, value, updated_at) VALUES ('suspended', 'true', ?)z]INSERT OR REPLACE INTO account_guard (key, value, updated_at) VALUES ('suspend_reason', ?, ?)zACCOUNT SUSPENDED: %sN)	r   ÚnowÚ	isoformatr   r!   r"   ÚloggerÚerrorÚ_send_alarm)r   Úreasonr*   s      r   ÚsuspendzAccountGuard.suspendY   s�   € ä�l‰l‹n×&Ñ&Ó(ˆØ�
‰
×Ñð.àˆFô	
ð
 	�
‰
×Ñð.à�SˆMô	
ð
 	�
‰
×ÑÔÜ�‰Ð,¨fÔ5Ø×Ñ˜Õ r   c                ó:  — | j                   j                  d«       | j                   j                  d«       | j                   j                  d«       | j                   j                  d«       | j                   j                  «        t        j	                  d«       y)zManually unsuspend the account.z1DELETE FROM account_guard WHERE key = 'suspended'z6DELETE FROM account_guard WHERE key = 'suspend_reason'z<DELETE FROM account_guard WHERE key = 'consecutive_failures'z8DELETE FROM account_guard WHERE key = 'zero_post_cycles'zAccount unsuspended manuallyN)r   r!   r"   r,   Úinfor#   s    r   Ú	unsuspendzAccountGuard.unsuspendj   sk   € à�
‰
×ÑÐNÔOØ�
‰
×ÑÐSÔTØ�
‰
×ÑÐYÔZØ�
‰
×ÑÐUÔVØ�
‰
×ÑÔÜ�‰Ð2Õ3r   c                óâ   — t        j                  «       j                  d«      }| j                  j	                  d|f«      j                  «       }|r|d   nd}|| j                  j                  k  S )z7Returns True if we're still under the daily scan limit.ú%Y-%m-%dú1SELECT count FROM daily_scan_count WHERE date = ?r   )r   r*   Ústrftimer   r!   r&   r   Úmax_daily_scans)r   Útodayr'   Úcurrents       r   Úcheck_daily_capzAccountGuard.check_daily_capw   sa   € ä—‘“×'Ñ'¨
Ó3ˆØ�j‰j× Ñ Ø?À%Àó
ç
‰(‹*ð 	ñ  �#�a’& QˆØ˜Ÿ™×7Ñ7Ñ7Ð7r   c                ó  — t        j                  «       j                  d«      }| j                  j	                  d|f«       | j                  j                  «        | j                  j	                  d|f«      j                  «       }|d   S )z*Increment and return the daily scan count.r5   zjINSERT INTO daily_scan_count (date, count) VALUES (?, 1) ON CONFLICT(date) DO UPDATE SET count = count + 1r6   r   )r   r*   r7   r   r!   r"   r&   ©r   r9   r'   s      r   Úincrement_daily_countz"AccountGuard.increment_daily_count€   sw   € ä—‘“×'Ñ'¨
Ó3ˆØ�
‰
×Ñð@àˆHô	
ð
 	�
‰
×ÑÔØ�j‰j× Ñ Ø?À%Àó
ç
‰(‹*ð 	ð �1‰vˆr   c                ó°   — t        j                  «       j                  d«      }| j                  j	                  d|f«      j                  «       }|r|d   S dS )Nr5   r6   r   )r   r*   r7   r   r!   r&   r=   s      r   Údaily_countzAccountGuard.daily_countŽ   sP   € Ü—‘“×'Ñ'¨
Ó3ˆØ�j‰j× Ñ Ø?À%Àó
ç
‰(‹*ð 	ñ ˆs�1‰vÐ# !Ð#r   c           	     óX  — g }|D ]¢  }|j                  d«      xs |j                  d«      xs d}|re| j                  t        |«      «      r'| j                  d|› dt        |«      dd › �«       g c S t        j                  d|t        |«      dd «       Œ’|j                  |«       Œ¤ |S )	z™Check Apify items for threat signals. Returns clean items (no errors).

        If threat detected, suspends immediately and returns empty list.
        r-   ÚerrorDescriptionÚ zFB threat detected in group 'z': NéÈ   zApify error item for %s: %s)ÚgetÚ
_is_threatÚstrr0   r,   ÚwarningÚappend)r   Ú
group_nameÚ	raw_itemsÚcleanÚitemr-   s         r   Úcheck_itemszAccountGuard.check_items™   s²   € ð
 ˆØò 	ˆDØ—H‘H˜WÓ%ÒK¨¯©Ð2DÓ)EÒKÈˆEÙØ—?‘?¤3 u£:Ô.Ø—L‘LØ7¸
°|À3ÄsÈ5ÃzÐRVÐSVÐGWÐFXÐYôð ’Iä—‘Ð<¸jÌ#ÈeË*ÐUYÐVYÐJZÔ[ØØ�L‰L˜Õð	ð ˆr   c                ód   — | xj                   dz  c_         |dk(  r| xj                  dz  c_        yy)z1Track per-group results for zero-posts detection.é   r   N)r   r   )r   Úposts_counts     r   Úrecord_group_resultz AccountGuard.record_group_result­   s1   € à×%Ò%¨Ñ*Õ%Ø˜!ÒØ×(Ò(¨AÑ-Ö(ð r   c                ó  — |dk7  rh| xj                   dz  c_         | j                  d| j                   «       | j                   t        k\  r#| j                  d| j                   › d|› d�«       yyd| _         | j                  dd«       y)	z%Track consecutive Apify run failures.Ú	SUCCEEDEDrP   Úconsecutive_failureszApify: z( consecutive run failures (last status: ú)r   N)r   Ú_persist_counterÚ_CONSECUTIVE_FAILURES_ALARMr0   )r   Ú
run_statuss     r   Úcheck_run_failurezAccountGuard.check_run_failure³   s�   € à˜Ò$Ø×&Ò&¨!Ñ+Õ&Ø×!Ñ!Ð"8¸$×:TÑ:TÔUØ×)Ñ)Ô-HÒHØ—‘Ø˜d×8Ñ8Ð9ð :%Ø%/ L°ð3õð Ið *+ˆDÔ&Ø×!Ñ!Ð"8¸!Õ<r   c                ó$  — | j                   dk(  ry| j                  | j                   k\  rG| j                  d«      }|dz   }| j                  d|«       |t        k\  r(| j                  d|› d�«       n| j                  dd«       d| _        d| _         y)zCCall at end of scraping phase. Checks zero-posts across all groups.r   NÚzero_post_cyclesrP   z All groups returned 0 posts for z7 consecutive cycles. Possible silent block by Facebook.)r   r   Ú_load_counterrW   Ú_ZERO_POSTS_ALARM_CYCLESr0   )r   ÚprevÚ	new_counts      r   Úend_of_cycle_checkzAccountGuard.end_of_cycle_checkÁ   sž   € à×(Ñ(¨AÒ-Øà×'Ñ'¨4×+HÑ+HÒHà×%Ñ%Ð&8Ó9ˆDØ˜q™ˆIØ×!Ñ!Ð"4°iÔ@ØÔ4Ò4Ø—‘Ø6°y°kð B9ð :õð ×!Ñ!Ð"4°aÔ8ð ()ˆÔ$Ø()ˆÕ%r   c                ó  — | j                   j                  }|sy	 t        j                  |d«      }t        j
                  «       }||k\  ry||z
  j                  }d}||z
  }|dk  ry|dk  ry|dk  ryy	# t        $ r Y yw xY w)
zFReturn group count based on warmup schedule, or None if not in warmup.Nr5   é   r   rP   é   r   é
   é   )r   Úwarmup_untilr   ÚstrptimeÚ
ValueErrorr*   Údays)r   rg   Úend_dater*   Údays_remainingÚtotal_warmup_daysÚ
day_numbers          r   Úwarmup_group_countzAccountGuard.warmup_group_countÜ   sš   € à—~‘~×2Ñ2ˆÙØð	Ü×(Ñ(¨°zÓBˆHô �l‰l‹nˆØ�(Š?Øà" S™.×.Ñ.ˆØÐØ&¨Ñ7ˆ
à˜Š?ØØ˜1Š_ØØ˜2ÒØàøô% ò 	Ùð	ús   ›A4 Á4	B Á?B c                óT   ‡— |j                  «       Št        ˆfd„t        D «       «      S )Nc              3  óB   •K  — | ]  }|j                  «       ‰v –— Œ y ­w©N)Úlower)Ú.0Úprs   s     €r   ú	<genexpr>z*AccountGuard._is_threat.<locals>.<genexpr>ý   s   øè ø€ Ò@¨!�1—7‘7“9 Ô%Ñ@ùs   ƒ)rs   ÚanyÚ_THREAT_PATTERNS)r   Útextrs   s     @r   rF   zAccountGuard._is_threatû   s    ø€ Ø—
‘
“ˆÜÓ@Ô/?Ô@Ó@Ð@r   c                óÈ   — | j                   j                  d|t        |«      t        j                  «       j                  «       f«       | j                   j                  «        y )NzNINSERT OR REPLACE INTO account_guard (key, value, updated_at) VALUES (?, ?, ?))r   r!   rG   r   r*   r+   r"   )r   ÚkeyÚvalues      r   rW   zAccountGuard._persist_counterÿ   sH   € Ø�
‰
×Ñðà”#�e“*œhŸl™l›n×6Ñ6Ó8Ð9ô	
ð
 	�
‰
×ÑÕr   c                ó|   — | j                   j                  d|f«      j                  «       }|rt        |d   «      S dS )Nz-SELECT value FROM account_guard WHERE key = ?r   )r   r!   r&   Úint)r   r{   r'   s      r   r]   zAccountGuard._load_counter  s>   € Ø�j‰j× Ñ Ø;¸c¸Vó
ç
‰(‹*ð 	ñ "Œs�3�q‘6‹{Ð( qÐ(r   c           	     ó‚  — | j                   j                  r| j                   j                  st        j	                  d«       y	 ddlm} ddlm} | j                   j                  j                  d«      D �cg c]"  }|j                  «       r|j                  «       ‘Œ$ }}|D ]S  } || j                   j                  |dd|› d	�¬
«      } || j                   j                  «      }|j                  |«       ŒU t        j                  dt        |«      «       yc c}w # t        $ r t        j!                  d«       Y yw xY w)z?Send alarm email via SendGrid. Failures are logged, not raised.z0Cannot send alarm email: SendGrid not configuredNr   )ÚSendGridAPIClient)ÚMailú,u0   ALARM: Agent Samochodowy â€” konto FB zagroÅ¼oneuW   <h2 style='color:#dc2626'>Konto FB zostaÅ‚o zawieszone</h2><p><strong>PowÃ³d:</strong> uÓ   </p><p>Skanowanie zostaÅ‚o <strong>automatycznie wstrzymane</strong>.</p><p>Po weryfikacji konta FB, odblokuj rÄ™cznie.</p><hr><p style='color:#6b7280;font-size:12px'>Agent Samochodowy â€” alarm automatyczny</p>)Ú
from_emailÚ	to_emailsÚsubjectÚhtml_contentz#Alarm email sent to %d recipient(s)zFailed to send alarm email)r   Úsendgrid_api_keyÚnotify_email_tor,   rH   Úsendgridr€   Úsendgrid.helpers.mailr�   ÚsplitÚstripÚnotify_email_fromÚsendr2   ÚlenÚ	ExceptionÚ	exception)	r   r/   r€   r�   ÚeÚ
recipientsÚ	recipientÚmessageÚsgs	            r   r.   zAccountGuard._send_alarm  s  € à�~‰~×.Ò.°d·n±n×6TÒ6TÜ�N‰NÐMÔNØð	;Ý2Ý2ð Ÿ™×7Ñ7×=Ñ=¸cÓBöàØ—7‘7”9ð —‘•	ðˆJð ð
 (ò !�	ÙØ#Ÿ~™~×?Ñ?Ø'ØNð7Ø7=°hð ?ð ô�ñ ' t§~¡~×'FÑ'FÓG�Ø—‘˜Õ ð!!ô" �K‰KÐ=¼sÀ:»ÕOùò-øô. ò 	;Ü×ÑÐ9Ö:ð	;ús%   Á3D Á7'DÂA9D ÄD ÄD>Ä=D>c                ól   — | j                   j                  «        | j                   j                  «        y rr   )r   r"   Úcloser#   s    r   r˜   zAccountGuard.close1  s"   € Ø�
‰
×ÑÔØ�
‰
×ÑÕr   N)r   r   ÚreturnÚNone)r™   rš   )r™   Úbool)r/   rG   r™   rš   )r™   r~   )rJ   rG   rK   ú
list[dict]r™   rœ   )rQ   r~   r™   rš   )rY   rG   r™   rš   )r™   z
int | None)ry   rG   r™   r›   )r{   rG   r|   r~   r™   rš   )r{   rG   r™   r~   )Ú__name__Ú
__module__Ú__qualname__Ú__doc__r   r   r(   r0   r3   r;   r>   r@   rN   rR   rZ   ra   ro   rF   rW   r]   r.   r˜   © r   r   r   r   .   sa   „ Ù>óóó(4ó!ó"4ó8óó$óó(.ó=ó*ó6ó>Aóó)ó";ôHr   r   )r    Ú
__future__r   ÚjsonÚloggingr   r   Úpathlibr   Úagent_samochodowy.configr   Ú	getLoggerr�   r,   rx   r^   rX   r   r¡   r   r   ú<module>r¨      sU   ðñ	õ #ã Û Û Ý Ý å -à	ˆ×	Ñ	˜8Ó	$€òÐ ð  Ð àÐ ÷Eò Er   