33from __future__ import annotations
44
55import asyncio
6+ import base64
67import json
78import logging
9+ import os
10+ import tempfile
811import time
912from collections .abc import Awaitable , Callable
13+ from pathlib import Path
1014from typing import TYPE_CHECKING , Any , Optional
1115
1216import nodriver as uc
@@ -120,6 +124,62 @@ class VintedService:
120124 _tab : Optional [Tab ] = None
121125 _VINTED_TIMEOUT_MS : int = 350
122126
127+ @classmethod
128+ async def _emit_auth_log (
129+ cls ,
130+ form_progress : FormProgressFn | None ,
131+ form_step : str ,
132+ message : str ,
133+ tab : Tab | None = None ,
134+ * ,
135+ detail : str | None = None ,
136+ with_screenshot : bool = False ,
137+ ) -> None :
138+ """Événements SSE pendant l’auth Vinted (sous-étapes + capture optionnelle)."""
139+ if not form_progress :
140+ return
141+ ev : dict [str , Any ] = {
142+ "type" : "log" ,
143+ "step" : "auth" ,
144+ "form_step" : form_step ,
145+ "message" : message ,
146+ }
147+ if detail is not None :
148+ ev ["detail" ] = detail
149+ if with_screenshot and tab is not None :
150+ shot = await cls ._tab_screenshot_data_url (tab )
151+ if shot :
152+ ev ["screenshot" ] = shot
153+ await form_progress (ev )
154+
155+ @classmethod
156+ async def _tab_screenshot_data_url (cls , tab : Tab ) -> str | None :
157+ """JPEG redimensionné (data URL) pour les logs SSE — évite les payloads énormes."""
158+ try :
159+ fd , path = tempfile .mkstemp (suffix = ".jpg" )
160+ os .close (fd )
161+ p = Path (path )
162+ await tab .save_screenshot (str (p ), format = "jpeg" )
163+ raw = p .read_bytes ()
164+ p .unlink (missing_ok = True )
165+ try :
166+ from io import BytesIO
167+
168+ from PIL import Image
169+
170+ im = Image .open (BytesIO (raw )).convert ("RGB" )
171+ im .thumbnail ((1280 , 1280 ))
172+ buf = BytesIO ()
173+ im .save (buf , format = "JPEG" , quality = 72 , optimize = True )
174+ raw = buf .getvalue ()
175+ except Exception :
176+ pass
177+ b64 = base64 .b64encode (raw ).decode ("ascii" )
178+ return f"data:image/jpeg;base64,{ b64 } "
179+ except Exception as exc : # noqa: BLE001
180+ logger .warning ("Vinted screenshot failed: %s" , exc )
181+ return None
182+
123183 @classmethod
124184 async def init_browser (cls ) -> None :
125185 """
@@ -568,7 +628,12 @@ async def _activate_se_connecter_switch(cls, tab: Tab, el: Element) -> None:
568628 await cls ._activate_auth_testid_span (tab , el , "auth-select-type--register-switch" )
569629
570630 @classmethod
571- async def _click_auth_se_connecter_switch (cls , tab : Tab , total_timeout_sec : float = 45.0 ) -> None :
631+ async def _click_auth_se_connecter_switch (
632+ cls ,
633+ tab : Tab ,
634+ total_timeout_sec : float = 45.0 ,
635+ form_progress : FormProgressFn | None = None ,
636+ ) -> None :
572637 """
573638 On the auth screen, click "Se connecter" to switch from the sign-up panel to the login panel.
574639
@@ -588,6 +653,13 @@ async def _click_auth_se_connecter_switch(cls, tab: Tab, total_timeout_sec: floa
588653
589654 if await cls ._login_username_field_is_visible (tab ):
590655 logger .info ("Login panel already visible; skipping %s." , selector )
656+ await cls ._emit_auth_log (
657+ form_progress ,
658+ "auth_skip_se_connecter" ,
659+ "Panneau connexion déjà affiché (champ identifiant visible)." ,
660+ tab ,
661+ detail = (tab .target .url or "" )[:400 ],
662+ )
591663 return
592664
593665 wait_timeout = min (5.0 , max (1.2 , min (total_timeout_sec , 8.0 )))
@@ -610,23 +682,51 @@ async def _click_auth_se_connecter_switch(cls, tab: Tab, total_timeout_sec: floa
610682
611683 await cls ._activate_se_connecter_switch (tab , el )
612684 logger .info ("Activated Se connecter (%s)." , selector )
685+ await cls ._emit_auth_log (
686+ form_progress ,
687+ "auth_se_connecter_clicked" ,
688+ "« Se connecter » activé — attente du champ identifiant (max 4 s)." ,
689+ tab ,
690+ detail = (tab .target .url or "" )[:400 ],
691+ )
613692 await cls ._scroll_page_top (tab )
693+ await tab .sleep (0.12 )
614694
615- settle_deadline = time .monotonic () + 1.5
695+ settle_deadline = time .monotonic () + 4.0
616696 while time .monotonic () < settle_deadline :
617697 await tab
618698 if await cls ._login_username_field_is_visible (tab ):
619699 logger .info ("Login username field is now visible." )
700+ await cls ._emit_auth_log (
701+ form_progress ,
702+ "auth_username_visible" ,
703+ "Champ identifiant visible après « Se connecter »." ,
704+ tab ,
705+ detail = (tab .target .url or "" )[:400 ],
706+ )
620707 return
621- await asyncio .sleep (0.05 )
708+ await asyncio .sleep (0.06 )
622709
623710 if not await cls ._login_username_field_is_visible (tab ):
624711 logger .warning (
625- "Se connecter was activated but username field did not become visible within 1.5s." ,
712+ "Se connecter was activated but username field did not become visible within 4s." ,
713+ )
714+ await cls ._emit_auth_log (
715+ form_progress ,
716+ "auth_username_wait" ,
717+ "Champ identifiant toujours absent après attente — capture d'écran." ,
718+ tab ,
719+ detail = (tab .target .url or "" )[:500 ],
720+ with_screenshot = True ,
626721 )
627722
628723 @classmethod
629- async def _click_auth_login_email_option (cls , tab : Tab , total_timeout_sec : float = 12.0 ) -> None :
724+ async def _click_auth_login_email_option (
725+ cls ,
726+ tab : Tab ,
727+ total_timeout_sec : float = 12.0 ,
728+ form_progress : FormProgressFn | None = None ,
729+ ) -> None :
630730 """
631731 After "Se connecter", choose login with e-mail (``auth-select-type--login-email``).
632732 """
@@ -635,6 +735,13 @@ async def _click_auth_login_email_option(cls, tab: Tab, total_timeout_sec: float
635735 await tab
636736 if await cls ._login_username_field_is_visible (tab ):
637737 logger .info ("Username field already visible; skipping e-mail login method picker." )
738+ await cls ._emit_auth_log (
739+ form_progress ,
740+ "auth_skip_login_email" ,
741+ "Connexion par e-mail inutile — champ identifiant déjà visible." ,
742+ tab ,
743+ detail = (tab .target .url or "" )[:400 ],
744+ )
638745 return
639746
640747 wait_timeout = min (5.0 , max (1.0 , min (total_timeout_sec , 8.0 )))
@@ -653,11 +760,25 @@ async def _click_auth_login_email_option(cls, tab: Tab, total_timeout_sec: float
653760
654761 await cls ._activate_auth_testid_span (tab , el , testid )
655762 logger .info ("Clicked login via e-mail (%s)." , selector )
763+ await cls ._emit_auth_log (
764+ form_progress ,
765+ "auth_login_email_clicked" ,
766+ "Option « Connexion par e-mail » activée." ,
767+ tab ,
768+ detail = (tab .target .url or "" )[:400 ],
769+ )
656770 await cls ._scroll_page_top (tab )
657- await tab .sleep (0.05 )
771+ await tab .sleep (0.12 )
658772
659773 @classmethod
660- async def _fill_and_submit_credentials (cls , tab : Tab , email : str , password : str ) -> None :
774+ async def _fill_and_submit_credentials (
775+ cls ,
776+ tab : Tab ,
777+ email : str ,
778+ password : str ,
779+ * ,
780+ form_progress : FormProgressFn | None = None ,
781+ ) -> None :
661782 """
662783 Fill username/password and submit the member login form (Continuer).
663784
@@ -669,12 +790,34 @@ async def _fill_and_submit_credentials(cls, tab: Tab, email: str, password: str)
669790 while time .monotonic () < form_deadline :
670791 await tab
671792 if await cls ._login_username_field_is_visible (tab ):
793+ await cls ._emit_auth_log (
794+ form_progress ,
795+ "auth_username_ready" ,
796+ "Champ identifiant visible — remplissage des identifiants." ,
797+ tab ,
798+ detail = (tab .target .url or "" )[:400 ],
799+ )
672800 break
673- await asyncio .sleep (0.06 )
801+ await asyncio .sleep (0.08 )
674802 else :
675803 logger .warning ("Username field did not become visible before credential fill." )
804+ await cls ._emit_auth_log (
805+ form_progress ,
806+ "auth_username_missing" ,
807+ "Champ identifiant non visible avant saisie — capture d'écran." ,
808+ tab ,
809+ detail = (tab .target .url or "" )[:500 ],
810+ with_screenshot = True ,
811+ )
676812
677813 await cls ._scroll_page_top (tab )
814+ await cls ._emit_auth_log (
815+ form_progress ,
816+ "auth_fill_start" ,
817+ "Saisie de l'identifiant / mot de passe…" ,
818+ tab ,
819+ detail = "Champs username + password" ,
820+ )
678821 if not await cls ._set_input_value_for_react (tab , "input#username" , email ):
679822 await cls ._set_input_value_for_react (tab , 'input[name="username"]' , email )
680823 await tab .sleep (0.05 )
@@ -715,6 +858,14 @@ async def _fill_and_submit_credentials(cls, tab: Tab, email: str, password: str)
715858 return_by_value = True ,
716859 )
717860 logger .info ("Submitted login via JS (Continuer / first submit)." )
861+ await cls ._emit_auth_log (
862+ form_progress ,
863+ "auth_submit_sent" ,
864+ "Formulaire de connexion envoyé (Continuer / submit)." ,
865+ tab ,
866+ detail = (tab .target .url or "" )[:400 ],
867+ with_screenshot = True ,
868+ )
718869 await tab .sleep (0.6 )
719870 await tab
720871
@@ -741,7 +892,13 @@ async def is_connected(cls) -> bool:
741892 return True
742893
743894 @classmethod
744- async def ensure_sign_in (cls , email : str , password : str ) -> None :
895+ async def ensure_sign_in (
896+ cls ,
897+ email : str ,
898+ password : str ,
899+ * ,
900+ form_progress : FormProgressFn | None = None ,
901+ ) -> None :
745902 """
746903 Attempt sign-in up to five times using :meth:`sign_in_vinted`.
747904
@@ -756,7 +913,7 @@ async def ensure_sign_in(cls, email: str, password: str) -> None:
756913 RuntimeError: If sign-in is still considered unsuccessful after all attempts.
757914 """
758915 max_attempts = 5
759- await cls .sign_in_vinted (email , password , from_home = True )
916+ await cls .sign_in_vinted (email , password , from_home = True , form_progress = form_progress )
760917 await cls ._require_tab ().sleep (1.8 )
761918 if await cls .is_connected ():
762919 logger .info (
@@ -770,7 +927,7 @@ async def ensure_sign_in(cls, email: str, password: str) -> None:
770927 attempt ,
771928 max_attempts - 1 ,
772929 )
773- await cls .sign_in_vinted (email , password , from_home = False )
930+ await cls .sign_in_vinted (email , password , from_home = False , form_progress = form_progress )
774931 await cls ._require_tab ().sleep (1.5 )
775932 if await cls .is_connected ():
776933 logger .info (
@@ -781,7 +938,14 @@ async def ensure_sign_in(cls, email: str, password: str) -> None:
781938 raise RuntimeError ("Failed to sign in after multiple attempts" )
782939
783940 @classmethod
784- async def sign_in_vinted (cls , email : str , password : str , * , from_home : bool = True ) -> None :
941+ async def sign_in_vinted (
942+ cls ,
943+ email : str ,
944+ password : str ,
945+ * ,
946+ from_home : bool = True ,
947+ form_progress : FormProgressFn | None = None ,
948+ ) -> None :
785949 """
786950 Navigate to auth, click Se connecter → e-mail, fill credentials, submit.
787951
@@ -796,22 +960,44 @@ async def sign_in_vinted(cls, email: str, password: str, *, from_home: bool = Tr
796960 """
797961 tab = cls ._require_tab ()
798962 if from_home :
963+ await cls ._emit_auth_log (
964+ form_progress ,
965+ "auth_nav_home" ,
966+ "Ouverture de vinted.fr (accueil)…" ,
967+ tab ,
968+ detail = "https://www.vinted.fr" ,
969+ )
799970 await tab .get ("https://www.vinted.fr" )
800971 await cls ._accept_onetrust_cookies (tab )
801972 await TimerService .wait (40 )
802973 await cls ._go_to_member_auth_entry (tab )
974+ await cls ._emit_auth_log (
975+ form_progress ,
976+ "auth_member_entry" ,
977+ "Page d'authentification membre chargée." ,
978+ tab ,
979+ detail = (tab .target .url or "" )[:500 ],
980+ with_screenshot = True ,
981+ )
803982 else :
983+ await cls ._emit_auth_log (
984+ form_progress ,
985+ "auth_retry_url" ,
986+ "Rechargement direct de l'URL de connexion membre (retry)." ,
987+ tab ,
988+ detail = VINTED_MEMBER_AUTH_URL ,
989+ )
804990 await tab .get (VINTED_MEMBER_AUTH_URL )
805991 await tab
806992 await tab .sleep (0.08 )
807993
808994 await cls ._accept_onetrust_cookies (tab , total_timeout_sec = 4.0 )
809995 await TimerService .wait (30 )
810996 await cls ._scroll_page_top (tab )
811- await cls ._click_auth_se_connecter_switch (tab , total_timeout_sec = 12.0 )
997+ await cls ._click_auth_se_connecter_switch (tab , total_timeout_sec = 12.0 , form_progress = form_progress )
812998 await tab .sleep (0.04 )
813- await cls ._click_auth_login_email_option (tab , total_timeout_sec = 8.0 )
814- await cls ._fill_and_submit_credentials (tab , email , password )
999+ await cls ._click_auth_login_email_option (tab , total_timeout_sec = 8.0 , form_progress = form_progress )
1000+ await cls ._fill_and_submit_credentials (tab , email , password , form_progress = form_progress )
8151001
8161002 @classmethod
8171003 async def open_sell_item_page (cls ) -> None :
0 commit comments