Upload 2.py with huggingface_hub
Browse files
2.py
ADDED
|
@@ -0,0 +1,1830 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# bot_concours_unified.py - Bot de concours unifié et optimisé
|
| 2 |
+
# Version complète avec toutes les optimisations + launcher intelligent intégré
|
| 3 |
+
|
| 4 |
+
import asyncio
|
| 5 |
+
import aiohttp
|
| 6 |
+
from playwright.async_api import async_playwright, Page
|
| 7 |
+
from bs4 import BeautifulSoup
|
| 8 |
+
import requests
|
| 9 |
+
import tweepy
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import time
|
| 12 |
+
import re
|
| 13 |
+
from datetime import datetime, timedelta
|
| 14 |
+
from huggingface_hub import InferenceClient
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import random
|
| 18 |
+
# import google.generativeai as genai # Supprimé - utilisation d'alternatives locales
|
| 19 |
+
import sqlite3
|
| 20 |
+
import logging
|
| 21 |
+
import imaplib
|
| 22 |
+
import email
|
| 23 |
+
import smtplib
|
| 24 |
+
from email.mime.text import MIMEText
|
| 25 |
+
import schedule
|
| 26 |
+
import hashlib
|
| 27 |
+
import json
|
| 28 |
+
import threading
|
| 29 |
+
import subprocess
|
| 30 |
+
from contextlib import contextmanager
|
| 31 |
+
from dataclasses import dataclass
|
| 32 |
+
from typing import List, Dict, Optional, Tuple
|
| 33 |
+
from urllib.parse import urljoin, urlparse
|
| 34 |
+
|
| 35 |
+
# =====================================================
|
| 36 |
+
# CONFIGURATION ET DATACLASSES
|
| 37 |
+
# =====================================================
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class PersonalInfo:
|
| 41 |
+
prenom: str = "Valentin"
|
| 42 |
+
nom: str = "Cora"
|
| 43 |
+
email: str = "[email protected]"
|
| 44 |
+
email_derivee: str = "[email protected]"
|
| 45 |
+
telephone: str = "+41791234567"
|
| 46 |
+
adresse: str = "Av Chantemerle 9"
|
| 47 |
+
code_postal: str = "1009"
|
| 48 |
+
ville: str = "Pully"
|
| 49 |
+
pays: str = "Suisse"
|
| 50 |
+
|
| 51 |
+
@dataclass
|
| 52 |
+
class APIConfig:
|
| 53 |
+
hf_token: Optional[str] = None
|
| 54 |
+
x_token: Optional[str] = None
|
| 55 |
+
google_api_key: Optional[str] = None
|
| 56 |
+
google_cx: Optional[str] = None
|
| 57 |
+
gemini_key: Optional[str] = None
|
| 58 |
+
email_app_password: Optional[str] = None
|
| 59 |
+
telegram_bot_token: Optional[str] = None
|
| 60 |
+
telegram_chat_id: Optional[str] = None
|
| 61 |
+
|
| 62 |
+
@classmethod
|
| 63 |
+
def from_env(cls):
|
| 64 |
+
return cls(
|
| 65 |
+
hf_token=os.getenv("HF_TOKEN"),
|
| 66 |
+
x_token=os.getenv("X_TOKEN"),
|
| 67 |
+
google_api_key=os.getenv("GOOGLE_API_KEY"),
|
| 68 |
+
google_cx=os.getenv("GOOGLE_CX"),
|
| 69 |
+
gemini_key=os.getenv("GEMINI_KEY"),
|
| 70 |
+
email_app_password=os.getenv("EMAIL_APP_PASSWORD"),
|
| 71 |
+
telegram_bot_token=os.getenv("TELEGRAM_BOT_TOKEN"),
|
| 72 |
+
telegram_chat_id=os.getenv("TELEGRAM_CHAT_ID")
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
@dataclass
|
| 76 |
+
class Contest:
|
| 77 |
+
title: str
|
| 78 |
+
url: str
|
| 79 |
+
description: str
|
| 80 |
+
source: str
|
| 81 |
+
deadline: Optional[str] = None
|
| 82 |
+
prize: Optional[str] = None
|
| 83 |
+
difficulty_score: int = 0
|
| 84 |
+
|
| 85 |
+
@dataclass
|
| 86 |
+
class FormField:
|
| 87 |
+
selector: str
|
| 88 |
+
field_type: str
|
| 89 |
+
label: str
|
| 90 |
+
required: bool
|
| 91 |
+
current_value: str = ""
|
| 92 |
+
ai_context: str = ""
|
| 93 |
+
|
| 94 |
+
@dataclass
|
| 95 |
+
class FormAnalysis:
|
| 96 |
+
fields: List[FormField]
|
| 97 |
+
complexity_score: int
|
| 98 |
+
estimated_success_rate: float
|
| 99 |
+
requires_captcha: bool
|
| 100 |
+
requires_social_media: bool
|
| 101 |
+
form_url: str
|
| 102 |
+
|
| 103 |
+
# =====================================================
|
| 104 |
+
# CONFIGURATION GLOBALE
|
| 105 |
+
# =====================================================
|
| 106 |
+
|
| 107 |
+
# Configuration
|
| 108 |
+
PERSONAL_INFO = PersonalInfo()
|
| 109 |
+
API_CONFIG = APIConfig.from_env()
|
| 110 |
+
|
| 111 |
+
# Sites suisses à scraper
|
| 112 |
+
SITES_CH = [
|
| 113 |
+
'https://www.concours.ch/concours/tous',
|
| 114 |
+
'https://www.jeu-concours.biz/concours-pays_suisse.html',
|
| 115 |
+
'https://www.loisirs.ch/concours/',
|
| 116 |
+
'https://www.radin.ch/',
|
| 117 |
+
'https://win4win.ch/fr/',
|
| 118 |
+
'https://www.concours-suisse.ch/',
|
| 119 |
+
'https://corporate.migros.ch/fr/concours',
|
| 120 |
+
'https://www.20min.ch/fr/concours-et-jeux',
|
| 121 |
+
'https://dein-gewinnspiel.ch/en',
|
| 122 |
+
'https://www.myswitzerland.com/fr/planification/vie-pratique/concours/'
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
# Proxies et User Agents
|
| 126 |
+
PROXY_LIST = ["http://20.206.106.192:80", "http://51.15.242.202:8888"]
|
| 127 |
+
USER_AGENTS = [
|
| 128 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 129 |
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 130 |
+
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
# Logging setup
|
| 134 |
+
logging.basicConfig(
|
| 135 |
+
level=logging.INFO,
|
| 136 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
| 137 |
+
handlers=[
|
| 138 |
+
logging.FileHandler('concours_bot.log'),
|
| 139 |
+
logging.StreamHandler()
|
| 140 |
+
]
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# =====================================================
|
| 144 |
+
# SYSTÈME DE CONFIGURATION ET TEST DES APIs
|
| 145 |
+
# =====================================================
|
| 146 |
+
|
| 147 |
+
class APITester:
|
| 148 |
+
def __init__(self, api_config: APIConfig):
|
| 149 |
+
self.api_config = api_config
|
| 150 |
+
|
| 151 |
+
def test_all_apis(self) -> Dict[str, bool]:
|
| 152 |
+
"""Teste toutes les APIs configurées"""
|
| 153 |
+
results = {}
|
| 154 |
+
|
| 155 |
+
print("🔍 Test de la configuration des APIs...")
|
| 156 |
+
print("=" * 40)
|
| 157 |
+
|
| 158 |
+
results['gemini'] = self._test_gemini()
|
| 159 |
+
results['telegram'] = self._test_telegram()
|
| 160 |
+
results['google_search'] = self._test_google_search()
|
| 161 |
+
results['huggingface'] = self._test_huggingface()
|
| 162 |
+
results['email'] = self._test_email_config()
|
| 163 |
+
|
| 164 |
+
return results
|
| 165 |
+
|
| 166 |
+
def _test_gemini(self) -> bool:
|
| 167 |
+
"""Test de l'API Gemini - DÉSACTIVÉ"""
|
| 168 |
+
print("🤖 Test Gemini API... DÉSACTIVÉ (utilisation d'alternatives locales)")
|
| 169 |
+
print("✅ Système de réponses locales activé")
|
| 170 |
+
return True # Toujours vrai car on utilise le système local
|
| 171 |
+
|
| 172 |
+
def _test_telegram(self) -> bool:
|
| 173 |
+
"""Test du Bot Telegram"""
|
| 174 |
+
print("\n📱 Test Telegram Bot...")
|
| 175 |
+
|
| 176 |
+
if not self.api_config.telegram_bot_token:
|
| 177 |
+
print("❌ TELEGRAM_BOT_TOKEN non configuré")
|
| 178 |
+
return False
|
| 179 |
+
|
| 180 |
+
if not self.api_config.telegram_chat_id:
|
| 181 |
+
print("❌ TELEGRAM_CHAT_ID non configuré")
|
| 182 |
+
return False
|
| 183 |
+
|
| 184 |
+
try:
|
| 185 |
+
url = f"https://api.telegram.org/bot{self.api_config.telegram_bot_token}/getMe"
|
| 186 |
+
response = requests.get(url, timeout=10)
|
| 187 |
+
|
| 188 |
+
if response.status_code == 200:
|
| 189 |
+
bot_info = response.json()
|
| 190 |
+
print(f"✅ Bot connecté: {bot_info['result']['first_name']}")
|
| 191 |
+
|
| 192 |
+
# Test d'envoi de message
|
| 193 |
+
message_url = f"https://api.telegram.org/bot{self.api_config.telegram_bot_token}/sendMessage"
|
| 194 |
+
test_message = {
|
| 195 |
+
'chat_id': self.api_config.telegram_chat_id,
|
| 196 |
+
'text': '🎉 Test de configuration réussi ! Votre bot de concours est prêt.'
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
msg_response = requests.post(message_url, json=test_message, timeout=10)
|
| 200 |
+
if msg_response.status_code == 200:
|
| 201 |
+
print("✅ Message de test envoyé avec succès")
|
| 202 |
+
return True
|
| 203 |
+
else:
|
| 204 |
+
print(f"❌ Erreur envoi message: {msg_response.text}")
|
| 205 |
+
return False
|
| 206 |
+
else:
|
| 207 |
+
print(f"❌ Erreur connexion bot: {response.text}")
|
| 208 |
+
return False
|
| 209 |
+
|
| 210 |
+
except Exception as e:
|
| 211 |
+
print(f"❌ Erreur Telegram: {e}")
|
| 212 |
+
return False
|
| 213 |
+
|
| 214 |
+
def _test_google_search(self) -> bool:
|
| 215 |
+
"""Test de l'API Google Search"""
|
| 216 |
+
print("\n🔍 Test Google Search API...")
|
| 217 |
+
|
| 218 |
+
if not self.api_config.google_api_key:
|
| 219 |
+
print("❌ GOOGLE_API_KEY non configurée")
|
| 220 |
+
return False
|
| 221 |
+
|
| 222 |
+
if not self.api_config.google_cx:
|
| 223 |
+
print("❌ GOOGLE_CX non configuré")
|
| 224 |
+
return False
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
url = "https://www.googleapis.com/customsearch/v1"
|
| 228 |
+
params = {
|
| 229 |
+
"key": self.api_config.google_api_key,
|
| 230 |
+
"cx": self.api_config.google_cx,
|
| 231 |
+
"q": "concours suisse test",
|
| 232 |
+
"num": 1
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
response = requests.get(url, params=params, timeout=10)
|
| 236 |
+
|
| 237 |
+
if response.status_code == 200:
|
| 238 |
+
results = response.json()
|
| 239 |
+
if 'items' in results:
|
| 240 |
+
print(f"✅ Google Search fonctionne: {len(results['items'])} résultat(s)")
|
| 241 |
+
return True
|
| 242 |
+
else:
|
| 243 |
+
print("⚠️ Google Search configuré mais aucun résultat")
|
| 244 |
+
return True
|
| 245 |
+
else:
|
| 246 |
+
print(f"❌ Erreur Google Search: {response.text}")
|
| 247 |
+
return False
|
| 248 |
+
|
| 249 |
+
except Exception as e:
|
| 250 |
+
print(f"❌ Erreur Google Search: {e}")
|
| 251 |
+
return False
|
| 252 |
+
|
| 253 |
+
def _test_huggingface(self) -> bool:
|
| 254 |
+
"""Test de l'API HuggingFace"""
|
| 255 |
+
print("\n🤗 Test HuggingFace API...")
|
| 256 |
+
|
| 257 |
+
if not self.api_config.hf_token:
|
| 258 |
+
print("❌ HF_TOKEN non configuré")
|
| 259 |
+
return False
|
| 260 |
+
|
| 261 |
+
try:
|
| 262 |
+
client = InferenceClient(token=self.api_config.hf_token)
|
| 263 |
+
result = client.text_generation(
|
| 264 |
+
"Bonjour, comment allez-vous?",
|
| 265 |
+
model="gpt2",
|
| 266 |
+
max_new_tokens=10
|
| 267 |
+
)
|
| 268 |
+
print("✅ HuggingFace fonctionne")
|
| 269 |
+
return True
|
| 270 |
+
except Exception as e:
|
| 271 |
+
print(f"❌ Erreur HuggingFace: {e}")
|
| 272 |
+
return False
|
| 273 |
+
|
| 274 |
+
def _test_email_config(self) -> bool:
|
| 275 |
+
"""Test de la configuration Email"""
|
| 276 |
+
print("\n📧 Test Configuration Email...")
|
| 277 |
+
|
| 278 |
+
if not self.api_config.email_app_password:
|
| 279 |
+
print("⚠️ EMAIL_APP_PASSWORD non configuré (optionnel)")
|
| 280 |
+
return False
|
| 281 |
+
|
| 282 |
+
if len(self.api_config.email_app_password) >= 10:
|
| 283 |
+
print("✅ Mot de passe Email configuré")
|
| 284 |
+
return True
|
| 285 |
+
else:
|
| 286 |
+
print("❌ Mot de passe Email semble invalide")
|
| 287 |
+
return False
|
| 288 |
+
|
| 289 |
+
# =====================================================
|
| 290 |
+
# LAUNCHER INTELLIGENT INTÉGRÉ
|
| 291 |
+
# =====================================================
|
| 292 |
+
|
| 293 |
+
class SmartLauncher:
|
| 294 |
+
def __init__(self):
|
| 295 |
+
self.api_config = API_CONFIG
|
| 296 |
+
self.api_tester = APITester(self.api_config)
|
| 297 |
+
self.api_status = self._check_api_configuration()
|
| 298 |
+
|
| 299 |
+
def _check_api_configuration(self) -> Dict[str, bool]:
|
| 300 |
+
"""Vérifie quelles APIs sont configurées"""
|
| 301 |
+
return {
|
| 302 |
+
'gemini': bool(self.api_config.gemini_key),
|
| 303 |
+
'telegram': bool(self.api_config.telegram_bot_token and self.api_config.telegram_chat_id),
|
| 304 |
+
'google_search': bool(self.api_config.google_api_key and self.api_config.google_cx),
|
| 305 |
+
'huggingface': bool(self.api_config.hf_token),
|
| 306 |
+
'email': bool(self.api_config.email_app_password)
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
def display_status(self):
|
| 310 |
+
"""Affiche le statut de configuration"""
|
| 311 |
+
print("🔍 DÉTECTION DE LA CONFIGURATION")
|
| 312 |
+
print("=" * 40)
|
| 313 |
+
|
| 314 |
+
descriptions = {
|
| 315 |
+
'gemini': "Gemini API (IA intelligente)",
|
| 316 |
+
'telegram': "Telegram Bot (Alertes)",
|
| 317 |
+
'google_search': "Google Search (Plus de concours)",
|
| 318 |
+
'huggingface': "HuggingFace (IA backup)",
|
| 319 |
+
'email': "Email Analysis (Détection gains)"
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
configured_count = 0
|
| 323 |
+
for api, configured in self.api_status.items():
|
| 324 |
+
icon = "✅" if configured else "❌"
|
| 325 |
+
desc = descriptions[api]
|
| 326 |
+
print(f"{icon} {desc}")
|
| 327 |
+
if configured:
|
| 328 |
+
configured_count += 1
|
| 329 |
+
|
| 330 |
+
print(f"\n📊 APIs configurées: {configured_count}/5")
|
| 331 |
+
return configured_count
|
| 332 |
+
|
| 333 |
+
def calculate_effectiveness(self) -> int:
|
| 334 |
+
"""Calcule l'efficacité estimée du bot"""
|
| 335 |
+
base_effectiveness = 40 # Sans APIs
|
| 336 |
+
|
| 337 |
+
if self.api_status['gemini']:
|
| 338 |
+
base_effectiveness += 30
|
| 339 |
+
if self.api_status['telegram']:
|
| 340 |
+
base_effectiveness += 10
|
| 341 |
+
if self.api_status['google_search']:
|
| 342 |
+
base_effectiveness += 15
|
| 343 |
+
if self.api_status['huggingface']:
|
| 344 |
+
base_effectiveness += 3
|
| 345 |
+
if self.api_status['email']:
|
| 346 |
+
base_effectiveness += 2
|
| 347 |
+
|
| 348 |
+
return min(base_effectiveness, 100)
|
| 349 |
+
|
| 350 |
+
def show_performance_impact(self):
|
| 351 |
+
"""Montre l'impact sur les performances"""
|
| 352 |
+
print("\n🎯 IMPACT SUR LES PERFORMANCES:")
|
| 353 |
+
print("-" * 40)
|
| 354 |
+
|
| 355 |
+
if self.api_status['gemini']:
|
| 356 |
+
print("✅ Réponses IA personnalisées et intelligentes")
|
| 357 |
+
else:
|
| 358 |
+
print("⚠️ Utilisation de réponses préprogrammées")
|
| 359 |
+
|
| 360 |
+
if self.api_status['telegram']:
|
| 361 |
+
print("✅ Alertes en temps réel des victoires")
|
| 362 |
+
else:
|
| 363 |
+
print("⚠️ Pas d'alertes automatiques")
|
| 364 |
+
|
| 365 |
+
if self.api_status['google_search']:
|
| 366 |
+
print("✅ Recherche étendue de concours")
|
| 367 |
+
else:
|
| 368 |
+
print("⚠️ Scraping limité aux sites directs")
|
| 369 |
+
|
| 370 |
+
effectiveness = self.calculate_effectiveness()
|
| 371 |
+
print(f"\n📈 Efficacité estimée: {effectiveness}%")
|
| 372 |
+
|
| 373 |
+
def show_setup_guide(self):
|
| 374 |
+
"""Affiche le guide de configuration"""
|
| 375 |
+
print("\n📖 GUIDE DE CONFIGURATION DES APIs GRATUITES")
|
| 376 |
+
print("=" * 50)
|
| 377 |
+
|
| 378 |
+
print("\n🤖 1. GEMINI API (Google) - PRIORITÉ HAUTE")
|
| 379 |
+
print(" • Aller sur: https://aistudio.google.com/")
|
| 380 |
+
print(" • Se connecter avec Google")
|
| 381 |
+
print(" • Cliquer 'Get API Key'")
|
| 382 |
+
print(" • Copier la clé (AIza...)")
|
| 383 |
+
print(" • Variable: GEMINI_KEY")
|
| 384 |
+
|
| 385 |
+
print("\n📱 2. TELEGRAM BOT - PRIORITÉ HAUTE")
|
| 386 |
+
print(" • Chercher @BotFather sur Telegram")
|
| 387 |
+
print(" • Envoyer: /newbot")
|
| 388 |
+
print(" • Suivre les instructions")
|
| 389 |
+
print(" • Chercher @userinfobot pour Chat ID")
|
| 390 |
+
print(" • Variables: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID")
|
| 391 |
+
|
| 392 |
+
print("\n🔍 3. GOOGLE SEARCH API - PRIORITÉ MOYENNE")
|
| 393 |
+
print(" • Aller sur: console.cloud.google.com")
|
| 394 |
+
print(" • Activer Custom Search API")
|
| 395 |
+
print(" • Créer une clé API")
|
| 396 |
+
print(" • Créer un moteur de recherche sur: cse.google.com")
|
| 397 |
+
print(" • Variables: GOOGLE_API_KEY, GOOGLE_CX")
|
| 398 |
+
|
| 399 |
+
print("\n🤗 4. HUGGINGFACE TOKEN - PRIORITÉ BASSE")
|
| 400 |
+
print(" • Créer compte sur: huggingface.co")
|
| 401 |
+
print(" • Aller dans Settings > Tokens")
|
| 402 |
+
print(" • Créer un token 'Read'")
|
| 403 |
+
print(" • Variable: HF_TOKEN")
|
| 404 |
+
|
| 405 |
+
print("\n📧 5. EMAIL OUTLOOK - OPTIONNEL")
|
| 406 |
+
print(" • Aller sur: outlook.live.com")
|
| 407 |
+
print(" • Paramètres > Sécurité")
|
| 408 |
+
print(" • Créer mot de passe d'application")
|
| 409 |
+
print(" • Variable: EMAIL_APP_PASSWORD")
|
| 410 |
+
|
| 411 |
+
def launch_options(self):
|
| 412 |
+
"""Affiche les options de lancement"""
|
| 413 |
+
print("\n🚀 OPTIONS DISPONIBLES:")
|
| 414 |
+
print("-" * 30)
|
| 415 |
+
print("1️⃣ Lancer le bot avec la configuration actuelle")
|
| 416 |
+
print("2️⃣ Tester toutes les APIs configurées")
|
| 417 |
+
print("3️⃣ Afficher le guide de configuration")
|
| 418 |
+
print("4️⃣ Configuration rapide PowerShell")
|
| 419 |
+
print("5️⃣ Quitter")
|
| 420 |
+
|
| 421 |
+
def show_powershell_config(self):
|
| 422 |
+
"""Affiche la configuration PowerShell rapide"""
|
| 423 |
+
print("\n⚡ CONFIGURATION RAPIDE POWERSHELL")
|
| 424 |
+
print("=" * 40)
|
| 425 |
+
print("Copiez-collez ces commandes dans PowerShell (remplacez par vos vraies clés):")
|
| 426 |
+
print()
|
| 427 |
+
print('$env:GEMINI_KEY="AIzaSyC-VOTRE-CLE-GEMINI"')
|
| 428 |
+
print('$env:TELEGRAM_BOT_TOKEN="123456:ABC-VOTRE-TOKEN"')
|
| 429 |
+
print('$env:TELEGRAM_CHAT_ID="VOTRE-CHAT-ID"')
|
| 430 |
+
print('$env:GOOGLE_API_KEY="AIzaSyD-VOTRE-CLE-GOOGLE"')
|
| 431 |
+
print('$env:GOOGLE_CX="VOTRE-SEARCH-ENGINE-ID"')
|
| 432 |
+
print('$env:HF_TOKEN="hf_VOTRE-TOKEN"')
|
| 433 |
+
print('$env:EMAIL_APP_PASSWORD="VOTRE-MOT-DE-PASSE"')
|
| 434 |
+
print()
|
| 435 |
+
print("⚠️ Redémarrez votre terminal après configuration")
|
| 436 |
+
|
| 437 |
+
def main_menu(self):
|
| 438 |
+
"""Menu principal intelligent"""
|
| 439 |
+
print("🎰 BOT DE CONCOURS SUISSE - LANCEUR INTELLIGENT")
|
| 440 |
+
print("=" * 55)
|
| 441 |
+
|
| 442 |
+
configured_count = self.display_status()
|
| 443 |
+
self.show_performance_impact()
|
| 444 |
+
|
| 445 |
+
if configured_count == 0:
|
| 446 |
+
print("\n⚠️ AUCUNE API CONFIGURÉE")
|
| 447 |
+
print("Le bot fonctionnera en mode basique uniquement.")
|
| 448 |
+
print("Recommandation: Configurez au moins Gemini + Telegram")
|
| 449 |
+
|
| 450 |
+
elif configured_count < 3:
|
| 451 |
+
print("\n💡 CONFIGURATION PARTIELLE")
|
| 452 |
+
print("Pour de meilleures performances, configurez plus d'APIs.")
|
| 453 |
+
|
| 454 |
+
else:
|
| 455 |
+
print("\n🎉 EXCELLENTE CONFIGURATION !")
|
| 456 |
+
print("Votre bot est prêt pour des performances optimales.")
|
| 457 |
+
|
| 458 |
+
self.launch_options()
|
| 459 |
+
|
| 460 |
+
while True:
|
| 461 |
+
choice = input("\n🎯 Votre choix (1-5): ").strip()
|
| 462 |
+
|
| 463 |
+
if choice == "1":
|
| 464 |
+
print("\n🎬 Lancement du bot...")
|
| 465 |
+
return True # Lancer le bot
|
| 466 |
+
|
| 467 |
+
elif choice == "2":
|
| 468 |
+
print("\n🧪 Test des APIs...")
|
| 469 |
+
results = self.api_tester.test_all_apis()
|
| 470 |
+
working_count = sum(results.values())
|
| 471 |
+
print(f"\n📊 Résumé: {working_count}/5 APIs fonctionnelles")
|
| 472 |
+
|
| 473 |
+
if working_count >= 2:
|
| 474 |
+
print("🚀 Prêt à lancer le bot !")
|
| 475 |
+
else:
|
| 476 |
+
print("⚠️ Configurez au moins 2 APIs pour de meilleurs résultats")
|
| 477 |
+
|
| 478 |
+
elif choice == "3":
|
| 479 |
+
self.show_setup_guide()
|
| 480 |
+
|
| 481 |
+
elif choice == "4":
|
| 482 |
+
self.show_powershell_config()
|
| 483 |
+
|
| 484 |
+
elif choice == "5":
|
| 485 |
+
print("👋 À bientôt !")
|
| 486 |
+
return False
|
| 487 |
+
|
| 488 |
+
else:
|
| 489 |
+
print("❌ Veuillez choisir 1, 2, 3, 4 ou 5")
|
| 490 |
+
|
| 491 |
+
# =====================================================
|
| 492 |
+
# GESTIONNAIRE DE BASE DE DONNÉES
|
| 493 |
+
# =====================================================
|
| 494 |
+
|
| 495 |
+
class DatabaseManager:
|
| 496 |
+
def __init__(self, db_path: str = 'concours_optimized.sqlite'):
|
| 497 |
+
self.db_path = db_path
|
| 498 |
+
self.local = threading.local()
|
| 499 |
+
self._init_db()
|
| 500 |
+
|
| 501 |
+
def _get_connection(self):
|
| 502 |
+
if not hasattr(self.local, 'conn'):
|
| 503 |
+
self.local.conn = sqlite3.connect(self.db_path)
|
| 504 |
+
self.local.conn.row_factory = sqlite3.Row
|
| 505 |
+
return self.local.conn
|
| 506 |
+
|
| 507 |
+
@contextmanager
|
| 508 |
+
def transaction(self):
|
| 509 |
+
conn = self._get_connection()
|
| 510 |
+
try:
|
| 511 |
+
yield conn
|
| 512 |
+
conn.commit()
|
| 513 |
+
except Exception:
|
| 514 |
+
conn.rollback()
|
| 515 |
+
raise
|
| 516 |
+
|
| 517 |
+
def _init_db(self):
|
| 518 |
+
with self.transaction() as conn:
|
| 519 |
+
conn.execute('''
|
| 520 |
+
CREATE TABLE IF NOT EXISTS participations (
|
| 521 |
+
url TEXT PRIMARY KEY,
|
| 522 |
+
title TEXT,
|
| 523 |
+
source TEXT,
|
| 524 |
+
status TEXT,
|
| 525 |
+
difficulty_score INTEGER,
|
| 526 |
+
success_rate REAL,
|
| 527 |
+
date TEXT,
|
| 528 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 529 |
+
)
|
| 530 |
+
''')
|
| 531 |
+
conn.execute('''
|
| 532 |
+
CREATE TABLE IF NOT EXISTS victories (
|
| 533 |
+
email_id TEXT PRIMARY KEY,
|
| 534 |
+
date TEXT,
|
| 535 |
+
lot TEXT,
|
| 536 |
+
source TEXT,
|
| 537 |
+
confirmed BOOLEAN DEFAULT FALSE,
|
| 538 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 539 |
+
)
|
| 540 |
+
''')
|
| 541 |
+
conn.execute('''
|
| 542 |
+
CREATE TABLE IF NOT EXISTS contest_cache (
|
| 543 |
+
url TEXT PRIMARY KEY,
|
| 544 |
+
content TEXT,
|
| 545 |
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 546 |
+
)
|
| 547 |
+
''')
|
| 548 |
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_participations_date ON participations(date)')
|
| 549 |
+
conn.execute('CREATE INDEX IF NOT EXISTS idx_cache_timestamp ON contest_cache(timestamp)')
|
| 550 |
+
|
| 551 |
+
def add_participation(self, contest: Contest, status: str = 'pending', success_rate: float = 0.0):
|
| 552 |
+
with self.transaction() as conn:
|
| 553 |
+
conn.execute('''
|
| 554 |
+
INSERT OR REPLACE INTO participations
|
| 555 |
+
(url, title, source, status, difficulty_score, success_rate, date)
|
| 556 |
+
VALUES (?, ?, ?, ?, ?, ?, date('now'))
|
| 557 |
+
''', (contest.url, contest.title, contest.source, status, contest.difficulty_score, success_rate))
|
| 558 |
+
|
| 559 |
+
def participation_exists(self, url: str) -> bool:
|
| 560 |
+
conn = self._get_connection()
|
| 561 |
+
result = conn.execute("SELECT 1 FROM participations WHERE url = ?", (url,)).fetchone()
|
| 562 |
+
return result is not None
|
| 563 |
+
|
| 564 |
+
def add_victory(self, email_id: str, lot: str, source: str):
|
| 565 |
+
with self.transaction() as conn:
|
| 566 |
+
conn.execute('''
|
| 567 |
+
INSERT OR IGNORE INTO victories (email_id, date, lot, source)
|
| 568 |
+
VALUES (?, date('now'), ?, ?)
|
| 569 |
+
''', (email_id, lot, source))
|
| 570 |
+
|
| 571 |
+
def get_stats(self) -> Dict:
|
| 572 |
+
conn = self._get_connection()
|
| 573 |
+
stats = {}
|
| 574 |
+
|
| 575 |
+
stats['total_participations'] = conn.execute("SELECT COUNT(*) FROM participations").fetchone()[0]
|
| 576 |
+
stats['successful_participations'] = conn.execute("SELECT COUNT(*) FROM participations WHERE status='success'").fetchone()[0]
|
| 577 |
+
stats['total_victories'] = conn.execute("SELECT COUNT(*) FROM victories").fetchone()[0]
|
| 578 |
+
|
| 579 |
+
source_stats = conn.execute('''
|
| 580 |
+
SELECT source, COUNT(*) as count
|
| 581 |
+
FROM participations
|
| 582 |
+
GROUP BY source
|
| 583 |
+
ORDER BY count DESC
|
| 584 |
+
''').fetchall()
|
| 585 |
+
stats['by_source'] = {row[0]: row[1] for row in source_stats}
|
| 586 |
+
|
| 587 |
+
return stats
|
| 588 |
+
|
| 589 |
+
# =====================================================
|
| 590 |
+
# MOTEUR IA AMÉLIORÉ
|
| 591 |
+
# =====================================================
|
| 592 |
+
|
| 593 |
+
class AIEngine:
|
| 594 |
+
def __init__(self, api_config: APIConfig):
|
| 595 |
+
self.api_config = api_config
|
| 596 |
+
self.cache = {}
|
| 597 |
+
|
| 598 |
+
def generate_response(self, question: str, context: str = "", response_type: str = "qa") -> str:
|
| 599 |
+
"""Génère une réponse IA avec fallback entre Gemini et HuggingFace"""
|
| 600 |
+
cache_key = hashlib.md5(f"{question}{context}{response_type}".encode()).hexdigest()
|
| 601 |
+
|
| 602 |
+
if cache_key in self.cache:
|
| 603 |
+
return self.cache[cache_key]
|
| 604 |
+
|
| 605 |
+
response = self._try_gemini(question, context, response_type)
|
| 606 |
+
if not response:
|
| 607 |
+
response = self._try_huggingface(question, context, response_type)
|
| 608 |
+
|
| 609 |
+
if not response:
|
| 610 |
+
response = self._generate_fallback_response(question, response_type)
|
| 611 |
+
|
| 612 |
+
self.cache[cache_key] = response
|
| 613 |
+
return response
|
| 614 |
+
|
| 615 |
+
def _try_gemini(self, question: str, context: str, response_type: str) -> Optional[str]:
|
| 616 |
+
# Gemini supprimé - utilisation d'alternatives locales uniquement
|
| 617 |
+
return None
|
| 618 |
+
|
| 619 |
+
def _try_huggingface(self, question: str, context: str, response_type: str) -> Optional[str]:
|
| 620 |
+
if not self.api_config.hf_token:
|
| 621 |
+
return None
|
| 622 |
+
|
| 623 |
+
try:
|
| 624 |
+
client = InferenceClient(token=self.api_config.hf_token)
|
| 625 |
+
|
| 626 |
+
if response_type == "qa" and context:
|
| 627 |
+
result = client.question_answering(
|
| 628 |
+
question=question,
|
| 629 |
+
context=context,
|
| 630 |
+
model="distilbert-base-cased-distilled-squad"
|
| 631 |
+
)
|
| 632 |
+
return result.answer
|
| 633 |
+
else:
|
| 634 |
+
prompt = f"Question: {question}\nContexte: {context}\nRéponse:"
|
| 635 |
+
result = client.text_generation(
|
| 636 |
+
prompt,
|
| 637 |
+
model="gpt2",
|
| 638 |
+
max_new_tokens=100,
|
| 639 |
+
temperature=0.7
|
| 640 |
+
)
|
| 641 |
+
return result[0]['generated_text'].split('Réponse:')[-1].strip()
|
| 642 |
+
|
| 643 |
+
except Exception as e:
|
| 644 |
+
logging.warning(f"HuggingFace API error: {e}")
|
| 645 |
+
return None
|
| 646 |
+
|
| 647 |
+
def _generate_fallback_response(self, question: str, response_type: str) -> str:
|
| 648 |
+
"""Système de réponses intelligentes sans API"""
|
| 649 |
+
question_lower = question.lower()
|
| 650 |
+
|
| 651 |
+
if response_type == "motivation":
|
| 652 |
+
# Réponses de motivation personnalisées selon le contexte
|
| 653 |
+
if any(word in question_lower for word in ["voyage", "vacances", "séjour"]):
|
| 654 |
+
return random.choice([
|
| 655 |
+
"J'adore voyager et découvrir de nouveaux horizons. Ce prix serait une opportunité fantastique pour moi de vivre une expérience inoubliable.",
|
| 656 |
+
"Voyager est ma passion et ce concours représente le voyage de mes rêves. J'espère avoir la chance de le remporter.",
|
| 657 |
+
"En tant que passionné de voyages, ce prix m'offrirait l'occasion parfaite de découvrir de nouveaux paysages et cultures."
|
| 658 |
+
])
|
| 659 |
+
elif any(word in question_lower for word in ["produit", "cosmétique", "beauté"]):
|
| 660 |
+
return random.choice([
|
| 661 |
+
"Je suis toujours à la recherche de nouveaux produits de qualité et j'aimerais beaucoup tester cette gamme.",
|
| 662 |
+
"Ces produits m'intéressent énormément et je serais ravi de pouvoir les découvrir.",
|
| 663 |
+
"J'ai entendu beaucoup de bien de cette marque et j'aimerais avoir l'opportunité de l'essayer."
|
| 664 |
+
])
|
| 665 |
+
elif any(word in question_lower for word in ["technologie", "smartphone", "ordinateur"]):
|
| 666 |
+
return random.choice([
|
| 667 |
+
"En tant que passionné de technologie, ce prix m'intéresse beaucoup et m'aiderait dans mes projets.",
|
| 668 |
+
"J'ai besoin de ce type d'équipement pour mes études et ce serait formidable de le gagner.",
|
| 669 |
+
"La technologie fait partie de ma vie quotidienne et ce prix serait très utile."
|
| 670 |
+
])
|
| 671 |
+
else:
|
| 672 |
+
return random.choice([
|
| 673 |
+
"Je participe avec enthousiasme à ce concours car le prix m'intéresse vraiment et correspond à mes besoins.",
|
| 674 |
+
"Ce concours m'attire particulièrement et je serais très heureux de remporter ce magnifique prix.",
|
| 675 |
+
"J'espère avoir la chance de gagner car ce prix me ferait énormément plaisir.",
|
| 676 |
+
"Je suis motivé à participer car cette opportunité pourrait vraiment changer ma journée."
|
| 677 |
+
])
|
| 678 |
+
|
| 679 |
+
elif response_type == "quiz":
|
| 680 |
+
# Système de réponses intelligentes pour les quiz
|
| 681 |
+
if "suisse" in question_lower:
|
| 682 |
+
if "capitale" in question_lower:
|
| 683 |
+
return "Berne"
|
| 684 |
+
elif "langue" in question_lower:
|
| 685 |
+
return "Français, Allemand, Italien, Romanche"
|
| 686 |
+
elif "monnaie" in question_lower:
|
| 687 |
+
return "Franc suisse"
|
| 688 |
+
elif "population" in question_lower:
|
| 689 |
+
return "8.7 millions"
|
| 690 |
+
|
| 691 |
+
if "couleur" in question_lower:
|
| 692 |
+
return random.choice(["Rouge", "Bleu", "Vert", "Jaune"])
|
| 693 |
+
|
| 694 |
+
if any(word in question_lower for word in ["combien", "nombre", "quantité"]):
|
| 695 |
+
return random.choice(["3", "5", "10", "12", "20"])
|
| 696 |
+
|
| 697 |
+
if any(word in question_lower for word in ["année", "date", "quand"]):
|
| 698 |
+
return random.choice(["2024", "2023", "2025"])
|
| 699 |
+
|
| 700 |
+
# Réponses par défaut pour quiz
|
| 701 |
+
return random.choice(["A", "B", "C", "Oui", "Non", "Vrai", "Faux"])
|
| 702 |
+
|
| 703 |
+
# Autres types de réponses
|
| 704 |
+
response_mapping = {
|
| 705 |
+
"age": random.choice(["25", "28", "30", "32"]),
|
| 706 |
+
"profession": random.choice(["Étudiant", "Employé", "Consultant"]),
|
| 707 |
+
"ville": "Pully",
|
| 708 |
+
"pays": "Suisse",
|
| 709 |
+
"default": "Merci"
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
return response_mapping.get(response_type, response_mapping["default"])
|
| 713 |
+
|
| 714 |
+
# =====================================================
|
| 715 |
+
# SCRAPER INTELLIGENT
|
| 716 |
+
# =====================================================
|
| 717 |
+
|
| 718 |
+
class IntelligentScraper:
|
| 719 |
+
def __init__(self, db_manager: DatabaseManager, cache_duration_hours: int = 6):
|
| 720 |
+
self.db = db_manager
|
| 721 |
+
self.cache_duration = timedelta(hours=cache_duration_hours)
|
| 722 |
+
self.session = None
|
| 723 |
+
|
| 724 |
+
async def __aenter__(self):
|
| 725 |
+
connector = aiohttp.TCPConnector(limit=10, limit_per_host=3)
|
| 726 |
+
timeout = aiohttp.ClientTimeout(total=30, connect=10)
|
| 727 |
+
self.session = aiohttp.ClientSession(
|
| 728 |
+
connector=connector,
|
| 729 |
+
timeout=timeout,
|
| 730 |
+
headers={'User-Agent': random.choice(USER_AGENTS)}
|
| 731 |
+
)
|
| 732 |
+
return self
|
| 733 |
+
|
| 734 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 735 |
+
if self.session:
|
| 736 |
+
await self.session.close()
|
| 737 |
+
|
| 738 |
+
async def scrape_all_sources(self) -> List[Contest]:
|
| 739 |
+
"""Scrape tous les sites et sources"""
|
| 740 |
+
all_contests = []
|
| 741 |
+
|
| 742 |
+
# Scraper les sites web
|
| 743 |
+
web_contests = await self._scrape_websites()
|
| 744 |
+
all_contests.extend(web_contests)
|
| 745 |
+
|
| 746 |
+
# Scraper Google Search si configuré
|
| 747 |
+
if API_CONFIG.google_api_key and API_CONFIG.google_cx:
|
| 748 |
+
google_contests = await self._scrape_google_search()
|
| 749 |
+
all_contests.extend(google_contests)
|
| 750 |
+
|
| 751 |
+
# Scraper Twitter/X si configuré
|
| 752 |
+
if API_CONFIG.x_token:
|
| 753 |
+
twitter_contests = await self._scrape_twitter()
|
| 754 |
+
all_contests.extend(twitter_contests)
|
| 755 |
+
|
| 756 |
+
# Filtrer les doublons
|
| 757 |
+
unique_contests = self._filter_unique_contests(all_contests)
|
| 758 |
+
|
| 759 |
+
logging.info(f"Total contests found: {len(all_contests)}, unique new: {len(unique_contests)}")
|
| 760 |
+
return unique_contests
|
| 761 |
+
|
| 762 |
+
async def _scrape_websites(self) -> List[Contest]:
|
| 763 |
+
"""Scrape les sites web en parallèle"""
|
| 764 |
+
batch_size = 3
|
| 765 |
+
all_contests = []
|
| 766 |
+
|
| 767 |
+
for i in range(0, len(SITES_CH), batch_size):
|
| 768 |
+
batch = SITES_CH[i:i + batch_size]
|
| 769 |
+
tasks = [self._scrape_single_site(site) for site in batch]
|
| 770 |
+
|
| 771 |
+
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 772 |
+
|
| 773 |
+
for result in batch_results:
|
| 774 |
+
if isinstance(result, list):
|
| 775 |
+
all_contests.extend(result)
|
| 776 |
+
elif isinstance(result, Exception):
|
| 777 |
+
logging.error(f"Batch scraping error: {result}")
|
| 778 |
+
|
| 779 |
+
await asyncio.sleep(2)
|
| 780 |
+
|
| 781 |
+
return all_contests
|
| 782 |
+
|
| 783 |
+
async def _scrape_single_site(self, url: str) -> List[Contest]:
|
| 784 |
+
"""Scrape un site web spécifique"""
|
| 785 |
+
try:
|
| 786 |
+
content = await self._fetch_with_retry(url)
|
| 787 |
+
if not content:
|
| 788 |
+
return []
|
| 789 |
+
|
| 790 |
+
soup = BeautifulSoup(content, 'html.parser')
|
| 791 |
+
contests = self._extract_contests_from_soup(soup, url)
|
| 792 |
+
|
| 793 |
+
logging.info(f"Found {len(contests)} contests on {url}")
|
| 794 |
+
return contests
|
| 795 |
+
|
| 796 |
+
except Exception as e:
|
| 797 |
+
logging.error(f"Error scraping {url}: {e}")
|
| 798 |
+
return []
|
| 799 |
+
|
| 800 |
+
async def _fetch_with_retry(self, url: str, max_retries: int = 3) -> Optional[str]:
|
| 801 |
+
"""Fetch avec retry et gestion d'erreurs"""
|
| 802 |
+
for attempt in range(max_retries):
|
| 803 |
+
try:
|
| 804 |
+
proxy = random.choice(PROXY_LIST) if PROXY_LIST else None
|
| 805 |
+
async with self.session.get(url, proxy=proxy) as response:
|
| 806 |
+
if response.status == 200:
|
| 807 |
+
return await response.text()
|
| 808 |
+
elif response.status == 429:
|
| 809 |
+
wait_time = 2 ** attempt * 5
|
| 810 |
+
logging.warning(f"Rate limited on {url}, waiting {wait_time}s")
|
| 811 |
+
await asyncio.sleep(wait_time)
|
| 812 |
+
else:
|
| 813 |
+
logging.warning(f"HTTP {response.status} for {url}")
|
| 814 |
+
|
| 815 |
+
except Exception as e:
|
| 816 |
+
logging.error(f"Attempt {attempt+1} failed for {url}: {e}")
|
| 817 |
+
if attempt < max_retries - 1:
|
| 818 |
+
await asyncio.sleep(2 ** attempt)
|
| 819 |
+
|
| 820 |
+
return None
|
| 821 |
+
|
| 822 |
+
def _extract_contests_from_soup(self, soup: BeautifulSoup, base_url: str) -> List[Contest]:
|
| 823 |
+
"""Extrait les concours d'une page HTML"""
|
| 824 |
+
contests = []
|
| 825 |
+
|
| 826 |
+
selectors = [
|
| 827 |
+
'.contest', '.concours', '.jeu', '.competition', '.giveaway',
|
| 828 |
+
'[data-contest]', '[data-concours]', '.prize', '.lot',
|
| 829 |
+
'article[class*="concours"]', '.entry', '.participate'
|
| 830 |
+
]
|
| 831 |
+
|
| 832 |
+
containers = []
|
| 833 |
+
for selector in selectors:
|
| 834 |
+
containers.extend(soup.select(selector))
|
| 835 |
+
|
| 836 |
+
if not containers:
|
| 837 |
+
containers = soup.find_all('a', href=re.compile(r'concours|jeu|contest|participate', re.I))
|
| 838 |
+
|
| 839 |
+
for container in containers[:20]:
|
| 840 |
+
try:
|
| 841 |
+
contest = self._parse_contest_container(container, base_url)
|
| 842 |
+
if contest and self._is_valid_contest(contest):
|
| 843 |
+
contests.append(contest)
|
| 844 |
+
except Exception as e:
|
| 845 |
+
logging.debug(f"Error parsing container: {e}")
|
| 846 |
+
|
| 847 |
+
return contests
|
| 848 |
+
|
| 849 |
+
def _parse_contest_container(self, container, base_url: str) -> Optional[Contest]:
|
| 850 |
+
"""Parse un conteneur de concours"""
|
| 851 |
+
title_selectors = ['h1', 'h2', 'h3', '.title', '.titre', '.contest-title']
|
| 852 |
+
title = ""
|
| 853 |
+
for selector in title_selectors:
|
| 854 |
+
title_elem = container.select_one(selector)
|
| 855 |
+
if title_elem:
|
| 856 |
+
title = title_elem.get_text(strip=True)
|
| 857 |
+
break
|
| 858 |
+
|
| 859 |
+
if not title:
|
| 860 |
+
title = container.get_text(strip=True)[:100]
|
| 861 |
+
|
| 862 |
+
url = ""
|
| 863 |
+
link_elem = container if container.name == 'a' else container.find('a')
|
| 864 |
+
if link_elem and link_elem.get('href'):
|
| 865 |
+
url = urljoin(base_url, link_elem['href'])
|
| 866 |
+
|
| 867 |
+
description = container.get_text(strip=True)[:500]
|
| 868 |
+
deadline = self._extract_deadline(description)
|
| 869 |
+
prize = self._extract_prize(description)
|
| 870 |
+
|
| 871 |
+
if not title or not url:
|
| 872 |
+
return None
|
| 873 |
+
|
| 874 |
+
return Contest(
|
| 875 |
+
title=title[:200],
|
| 876 |
+
url=url,
|
| 877 |
+
description=description,
|
| 878 |
+
source=base_url,
|
| 879 |
+
deadline=deadline,
|
| 880 |
+
prize=prize,
|
| 881 |
+
difficulty_score=self._estimate_difficulty(description)
|
| 882 |
+
)
|
| 883 |
+
|
| 884 |
+
def _extract_deadline(self, text: str) -> Optional[str]:
|
| 885 |
+
"""Extrait la date limite du texte"""
|
| 886 |
+
patterns = [
|
| 887 |
+
r"jusqu[\'']?au (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
|
| 888 |
+
r"avant le (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
|
| 889 |
+
r"fin le (\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})"
|
| 890 |
+
]
|
| 891 |
+
|
| 892 |
+
for pattern in patterns:
|
| 893 |
+
match = re.search(pattern, text, re.I)
|
| 894 |
+
if match:
|
| 895 |
+
return match.group(1)
|
| 896 |
+
return None
|
| 897 |
+
|
| 898 |
+
def _extract_prize(self, text: str) -> Optional[str]:
|
| 899 |
+
"""Extrait le prix du texte"""
|
| 900 |
+
patterns = [
|
| 901 |
+
r"gagne[rz]?\s+([^.!?]{1,50})",
|
| 902 |
+
r"prix[:\s]+([^.!?]{1,50})",
|
| 903 |
+
r"lot[:\s]+([^.!?]{1,50})",
|
| 904 |
+
r"(\d+\s*CHF|\d+\s*euros?|\d+\s*francs?)"
|
| 905 |
+
]
|
| 906 |
+
|
| 907 |
+
for pattern in patterns:
|
| 908 |
+
match = re.search(pattern, text, re.I)
|
| 909 |
+
if match:
|
| 910 |
+
return match.group(1).strip()
|
| 911 |
+
return None
|
| 912 |
+
|
| 913 |
+
def _estimate_difficulty(self, description: str) -> int:
|
| 914 |
+
"""Estime la difficulté de participation (0-10)"""
|
| 915 |
+
difficulty = 0
|
| 916 |
+
|
| 917 |
+
if re.search(r'justifi|motivation|pourquoi|essay', description, re.I):
|
| 918 |
+
difficulty += 3
|
| 919 |
+
if re.search(r'photo|image|créat', description, re.I):
|
| 920 |
+
difficulty += 2
|
| 921 |
+
if re.search(r'quiz|question|répond', description, re.I):
|
| 922 |
+
difficulty += 1
|
| 923 |
+
if re.search(r'partag|social|facebook|twitter', description, re.I):
|
| 924 |
+
difficulty += 1
|
| 925 |
+
if re.search(r'inscription|compte|profil', description, re.I):
|
| 926 |
+
difficulty += 1
|
| 927 |
+
|
| 928 |
+
return min(difficulty, 10)
|
| 929 |
+
|
| 930 |
+
def _is_valid_contest(self, contest: Contest) -> bool:
|
| 931 |
+
"""Valide qu'un concours est légitime"""
|
| 932 |
+
swiss_indicators = [
|
| 933 |
+
'suisse', 'switzerland', 'ch', 'romandie', 'genève', 'lausanne',
|
| 934 |
+
'zurich', 'bern', 'ouvert en suisse', 'résidents suisses'
|
| 935 |
+
]
|
| 936 |
+
|
| 937 |
+
full_text = (contest.title + " " + contest.description).lower()
|
| 938 |
+
has_swiss_access = any(indicator in full_text for indicator in swiss_indicators)
|
| 939 |
+
|
| 940 |
+
excluded_terms = [
|
| 941 |
+
'payant', 'payment', 'carte bancaire', 'spam', 'phishing',
|
| 942 |
+
'adult', 'casino', 'bitcoin', 'crypto', 'investment'
|
| 943 |
+
]
|
| 944 |
+
|
| 945 |
+
has_excluded = any(term in full_text for term in excluded_terms)
|
| 946 |
+
valid_url = contest.url.startswith(('http://', 'https://'))
|
| 947 |
+
|
| 948 |
+
return has_swiss_access and not has_excluded and valid_url
|
| 949 |
+
|
| 950 |
+
async def _scrape_google_search(self) -> List[Contest]:
|
| 951 |
+
"""Scrape via Google Custom Search API"""
|
| 952 |
+
try:
|
| 953 |
+
query = "concours gratuit Suisse 2025 site:.ch"
|
| 954 |
+
response = requests.get(
|
| 955 |
+
"https://www.googleapis.com/customsearch/v1",
|
| 956 |
+
params={
|
| 957 |
+
"key": API_CONFIG.google_api_key,
|
| 958 |
+
"cx": API_CONFIG.google_cx,
|
| 959 |
+
"q": query,
|
| 960 |
+
"num": 10
|
| 961 |
+
},
|
| 962 |
+
timeout=10
|
| 963 |
+
)
|
| 964 |
+
|
| 965 |
+
results = response.json().get('items', [])
|
| 966 |
+
contests = []
|
| 967 |
+
|
| 968 |
+
for res in results:
|
| 969 |
+
title = res.get('title', '')
|
| 970 |
+
url = res.get('link', '')
|
| 971 |
+
description = res.get('snippet', '')
|
| 972 |
+
|
| 973 |
+
contest = Contest(
|
| 974 |
+
title=title,
|
| 975 |
+
url=url,
|
| 976 |
+
description=description,
|
| 977 |
+
source='Google Search',
|
| 978 |
+
difficulty_score=self._estimate_difficulty(description)
|
| 979 |
+
)
|
| 980 |
+
|
| 981 |
+
if self._is_valid_contest(contest):
|
| 982 |
+
contests.append(contest)
|
| 983 |
+
|
| 984 |
+
logging.info(f"Found {len(contests)} contests via Google Search")
|
| 985 |
+
return contests
|
| 986 |
+
|
| 987 |
+
except Exception as e:
|
| 988 |
+
logging.error(f"Google Search API error: {e}")
|
| 989 |
+
return []
|
| 990 |
+
|
| 991 |
+
async def _scrape_twitter(self) -> List[Contest]:
|
| 992 |
+
"""Scrape Twitter/X pour les concours"""
|
| 993 |
+
try:
|
| 994 |
+
client = tweepy.Client(bearer_token=API_CONFIG.x_token)
|
| 995 |
+
tweets = client.search_recent_tweets(
|
| 996 |
+
query="concours gratuit Suisse lang:fr",
|
| 997 |
+
max_results=10
|
| 998 |
+
)
|
| 999 |
+
|
| 1000 |
+
contests = []
|
| 1001 |
+
if tweets.data:
|
| 1002 |
+
for tweet in tweets.data:
|
| 1003 |
+
if self._is_swiss_accessible(tweet.text):
|
| 1004 |
+
contest = Contest(
|
| 1005 |
+
title=tweet.text[:50] + "...",
|
| 1006 |
+
url=f"https://x.com/i/status/{tweet.id}",
|
| 1007 |
+
description=tweet.text,
|
| 1008 |
+
source='Twitter/X',
|
| 1009 |
+
difficulty_score=5
|
| 1010 |
+
)
|
| 1011 |
+
contests.append(contest)
|
| 1012 |
+
|
| 1013 |
+
logging.info(f"Found {len(contests)} contests on Twitter/X")
|
| 1014 |
+
return contests
|
| 1015 |
+
|
| 1016 |
+
except Exception as e:
|
| 1017 |
+
logging.error(f"Twitter/X API error: {e}")
|
| 1018 |
+
return []
|
| 1019 |
+
|
| 1020 |
+
def _is_swiss_accessible(self, text: str) -> bool:
|
| 1021 |
+
"""Vérifie si accessible depuis la Suisse"""
|
| 1022 |
+
swiss_pattern = r"(suisse|ch|romandie|ouvert\s+a\s+la\s+suisse|geneve|lausanne)"
|
| 1023 |
+
return bool(re.search(swiss_pattern, text.lower(), re.IGNORECASE))
|
| 1024 |
+
|
| 1025 |
+
def _filter_unique_contests(self, contests: List[Contest]) -> List[Contest]:
|
| 1026 |
+
"""Filtre les concours uniques et non déjà traités"""
|
| 1027 |
+
unique_contests = []
|
| 1028 |
+
seen_urls = set()
|
| 1029 |
+
|
| 1030 |
+
for contest in contests:
|
| 1031 |
+
if contest.url not in seen_urls and not self.db.participation_exists(contest.url):
|
| 1032 |
+
unique_contests.append(contest)
|
| 1033 |
+
seen_urls.add(contest.url)
|
| 1034 |
+
|
| 1035 |
+
return unique_contests
|
| 1036 |
+
|
| 1037 |
+
# =====================================================
|
| 1038 |
+
# SYSTÈME DE PARTICIPATION INTELLIGENT
|
| 1039 |
+
# =====================================================
|
| 1040 |
+
|
| 1041 |
+
class SmartParticipator:
|
| 1042 |
+
def __init__(self, db_manager: DatabaseManager, ai_engine: AIEngine):
|
| 1043 |
+
self.db = db_manager
|
| 1044 |
+
self.ai = ai_engine
|
| 1045 |
+
self.personal_info = PERSONAL_INFO
|
| 1046 |
+
|
| 1047 |
+
self.field_patterns = {
|
| 1048 |
+
'prenom': [r'prenom|prénom|first.*name'],
|
| 1049 |
+
'nom': [r'nom(?!.*prenom)|last.*name|family.*name'],
|
| 1050 |
+
'email': [r'email|e-mail|courriel'],
|
| 1051 |
+
'telephone': [r'tel|phone|telephone|téléphone'],
|
| 1052 |
+
'adresse': [r'adresse|address|rue|street'],
|
| 1053 |
+
'code_postal': [r'code.postal|zip|postal'],
|
| 1054 |
+
'ville': [r'ville|city|localité'],
|
| 1055 |
+
'pays': [r'pays|country|nation'],
|
| 1056 |
+
'motivation': [r'motivation|pourquoi|why|reason'],
|
| 1057 |
+
'quiz': [r'question|quiz|réponse|answer']
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
async def participate_in_contest(self, contest: Contest) -> bool:
|
| 1061 |
+
"""Participe à un concours de manière intelligente"""
|
| 1062 |
+
async with async_playwright() as p:
|
| 1063 |
+
browser = await p.chromium.launch(
|
| 1064 |
+
headless=True,
|
| 1065 |
+
args=['--no-sandbox', '--disable-blink-features=AutomationControlled']
|
| 1066 |
+
)
|
| 1067 |
+
|
| 1068 |
+
context = await browser.new_context(
|
| 1069 |
+
user_agent=random.choice(USER_AGENTS),
|
| 1070 |
+
viewport={'width': 1920, 'height': 1080}
|
| 1071 |
+
)
|
| 1072 |
+
|
| 1073 |
+
page = await context.new_page()
|
| 1074 |
+
|
| 1075 |
+
try:
|
| 1076 |
+
analysis = await self._analyze_form(page, contest.url)
|
| 1077 |
+
|
| 1078 |
+
if analysis.estimated_success_rate < 0.3:
|
| 1079 |
+
logging.warning(f"Low success rate for {contest.url}: {analysis.estimated_success_rate}")
|
| 1080 |
+
self.db.add_participation(contest, 'skipped_low_success', analysis.estimated_success_rate)
|
| 1081 |
+
return False
|
| 1082 |
+
|
| 1083 |
+
if analysis.requires_captcha:
|
| 1084 |
+
logging.warning(f"CAPTCHA detected for {contest.url}, skipping")
|
| 1085 |
+
self.db.add_participation(contest, 'skipped_captcha', analysis.estimated_success_rate)
|
| 1086 |
+
return False
|
| 1087 |
+
|
| 1088 |
+
success = await self._fill_and_submit_form(page, analysis, contest)
|
| 1089 |
+
|
| 1090 |
+
status = 'success' if success else 'failed'
|
| 1091 |
+
self.db.add_participation(contest, status, analysis.estimated_success_rate)
|
| 1092 |
+
|
| 1093 |
+
logging.info(f"Participation {'successful' if success else 'failed'} for {contest.title}")
|
| 1094 |
+
return success
|
| 1095 |
+
|
| 1096 |
+
except Exception as e:
|
| 1097 |
+
logging.error(f"Participation error for {contest.url}: {e}")
|
| 1098 |
+
self.db.add_participation(contest, 'error', 0.0)
|
| 1099 |
+
return False
|
| 1100 |
+
|
| 1101 |
+
finally:
|
| 1102 |
+
await browser.close()
|
| 1103 |
+
|
| 1104 |
+
async def _analyze_form(self, page: Page, url: str) -> FormAnalysis:
|
| 1105 |
+
"""Analyse un formulaire de concours"""
|
| 1106 |
+
try:
|
| 1107 |
+
await page.goto(url, wait_until='networkidle', timeout=15000)
|
| 1108 |
+
|
| 1109 |
+
fields = await self._detect_form_fields(page)
|
| 1110 |
+
complexity = sum(self._calculate_field_complexity(field) for field in fields)
|
| 1111 |
+
has_captcha = await self._detect_captcha(page)
|
| 1112 |
+
has_social_requirements = await self._detect_social_requirements(page)
|
| 1113 |
+
success_rate = self._estimate_success_rate(fields, complexity, has_captcha)
|
| 1114 |
+
|
| 1115 |
+
return FormAnalysis(
|
| 1116 |
+
fields=fields,
|
| 1117 |
+
complexity_score=complexity,
|
| 1118 |
+
estimated_success_rate=success_rate,
|
| 1119 |
+
requires_captcha=has_captcha,
|
| 1120 |
+
requires_social_media=has_social_requirements,
|
| 1121 |
+
form_url=url
|
| 1122 |
+
)
|
| 1123 |
+
|
| 1124 |
+
except Exception as e:
|
| 1125 |
+
logging.error(f"Form analysis error: {e}")
|
| 1126 |
+
return FormAnalysis([], 10, 0.0, True, True, url)
|
| 1127 |
+
|
| 1128 |
+
async def _detect_form_fields(self, page: Page) -> List[FormField]:
|
| 1129 |
+
"""Détecte tous les champs de formulaire"""
|
| 1130 |
+
fields = []
|
| 1131 |
+
|
| 1132 |
+
selectors = [
|
| 1133 |
+
'input[type="text"]', 'input[type="email"]', 'input[type="tel"]',
|
| 1134 |
+
'input[type="number"]', 'input:not([type])', 'textarea', 'select'
|
| 1135 |
+
]
|
| 1136 |
+
|
| 1137 |
+
for selector in selectors:
|
| 1138 |
+
elements = await page.query_selector_all(selector)
|
| 1139 |
+
|
| 1140 |
+
for element in elements:
|
| 1141 |
+
try:
|
| 1142 |
+
field = await self._analyze_single_field(element, page)
|
| 1143 |
+
if field:
|
| 1144 |
+
fields.append(field)
|
| 1145 |
+
except Exception:
|
| 1146 |
+
continue
|
| 1147 |
+
|
| 1148 |
+
return fields
|
| 1149 |
+
|
| 1150 |
+
async def _analyze_single_field(self, element, page: Page) -> Optional[FormField]:
|
| 1151 |
+
"""Analyse un champ individuel"""
|
| 1152 |
+
try:
|
| 1153 |
+
field_type = await element.evaluate('el => el.type || el.tagName.toLowerCase()')
|
| 1154 |
+
name = await element.evaluate('el => el.name || el.id || ""')
|
| 1155 |
+
placeholder = await element.evaluate('el => el.placeholder || ""')
|
| 1156 |
+
required = await element.evaluate('el => el.required')
|
| 1157 |
+
|
| 1158 |
+
label_text = await self._find_field_label(element, page)
|
| 1159 |
+
selector = await self._create_unique_selector(element)
|
| 1160 |
+
|
| 1161 |
+
return FormField(
|
| 1162 |
+
selector=selector,
|
| 1163 |
+
field_type=field_type,
|
| 1164 |
+
label=label_text,
|
| 1165 |
+
required=required,
|
| 1166 |
+
ai_context=f"Name: {name}, Placeholder: {placeholder}, Label: {label_text}"
|
| 1167 |
+
)
|
| 1168 |
+
|
| 1169 |
+
except Exception:
|
| 1170 |
+
return None
|
| 1171 |
+
|
| 1172 |
+
async def _find_field_label(self, element, page: Page) -> str:
|
| 1173 |
+
"""Trouve le label associé à un champ"""
|
| 1174 |
+
try:
|
| 1175 |
+
element_id = await element.evaluate('el => el.id')
|
| 1176 |
+
if element_id:
|
| 1177 |
+
label = await page.query_selector(f'label[for="{element_id}"]')
|
| 1178 |
+
if label:
|
| 1179 |
+
return await label.inner_text()
|
| 1180 |
+
|
| 1181 |
+
parent_label = await element.evaluate('''
|
| 1182 |
+
el => {
|
| 1183 |
+
let parent = el.parentElement;
|
| 1184 |
+
while (parent && parent.tagName !== 'BODY') {
|
| 1185 |
+
if (parent.tagName === 'LABEL') {
|
| 1186 |
+
return parent.innerText;
|
| 1187 |
+
}
|
| 1188 |
+
parent = parent.parentElement;
|
| 1189 |
+
}
|
| 1190 |
+
return '';
|
| 1191 |
+
}
|
| 1192 |
+
''')
|
| 1193 |
+
|
| 1194 |
+
if parent_label:
|
| 1195 |
+
return parent_label.strip()
|
| 1196 |
+
|
| 1197 |
+
prev_text = await element.evaluate('''
|
| 1198 |
+
el => {
|
| 1199 |
+
const prev = el.previousElementSibling;
|
| 1200 |
+
return prev ? prev.innerText : '';
|
| 1201 |
+
}
|
| 1202 |
+
''')
|
| 1203 |
+
|
| 1204 |
+
return prev_text.strip()
|
| 1205 |
+
|
| 1206 |
+
except Exception:
|
| 1207 |
+
return ""
|
| 1208 |
+
|
| 1209 |
+
async def _create_unique_selector(self, element) -> str:
|
| 1210 |
+
"""Crée un sélecteur CSS unique"""
|
| 1211 |
+
element_id = await element.evaluate('el => el.id')
|
| 1212 |
+
if element_id:
|
| 1213 |
+
return f'#{element_id}'
|
| 1214 |
+
|
| 1215 |
+
name = await element.evaluate('el => el.name')
|
| 1216 |
+
if name:
|
| 1217 |
+
return f'[name="{name}"]'
|
| 1218 |
+
|
| 1219 |
+
class_name = await element.evaluate('el => el.className')
|
| 1220 |
+
tag_name = await element.evaluate('el => el.tagName.toLowerCase()')
|
| 1221 |
+
|
| 1222 |
+
if class_name:
|
| 1223 |
+
return f'{tag_name}.{class_name.split()[0]}'
|
| 1224 |
+
|
| 1225 |
+
return f'{tag_name}:nth-of-type(1)'
|
| 1226 |
+
|
| 1227 |
+
def _calculate_field_complexity(self, field: FormField) -> int:
|
| 1228 |
+
"""Calcule la complexité d'un champ"""
|
| 1229 |
+
complexity = 1
|
| 1230 |
+
|
| 1231 |
+
if field.field_type == 'textarea':
|
| 1232 |
+
complexity += 3
|
| 1233 |
+
elif field.field_type == 'select':
|
| 1234 |
+
complexity += 2
|
| 1235 |
+
elif field.required:
|
| 1236 |
+
complexity += 1
|
| 1237 |
+
|
| 1238 |
+
if re.search(r'motivation|justifi|pourquoi', field.label, re.I):
|
| 1239 |
+
complexity += 3
|
| 1240 |
+
elif re.search(r'quiz|question', field.label, re.I):
|
| 1241 |
+
complexity += 2
|
| 1242 |
+
|
| 1243 |
+
return complexity
|
| 1244 |
+
|
| 1245 |
+
async def _detect_captcha(self, page: Page) -> bool:
|
| 1246 |
+
"""Détecte la présence de CAPTCHA"""
|
| 1247 |
+
captcha_selectors = [
|
| 1248 |
+
'.g-recaptcha', '.h-captcha', '#captcha', '.captcha',
|
| 1249 |
+
'iframe[src*="recaptcha"]', 'iframe[src*="hcaptcha"]'
|
| 1250 |
+
]
|
| 1251 |
+
|
| 1252 |
+
for selector in captcha_selectors:
|
| 1253 |
+
element = await page.query_selector(selector)
|
| 1254 |
+
if element:
|
| 1255 |
+
return True
|
| 1256 |
+
|
| 1257 |
+
return False
|
| 1258 |
+
|
| 1259 |
+
async def _detect_social_requirements(self, page: Page) -> bool:
|
| 1260 |
+
"""Détecte les exigences de réseaux sociaux"""
|
| 1261 |
+
content = await page.content()
|
| 1262 |
+
social_patterns = [
|
| 1263 |
+
r'follow.*us', r'partag.*facebook', r'retweet',
|
| 1264 |
+
r'like.*page', r'subscribe.*channel'
|
| 1265 |
+
]
|
| 1266 |
+
|
| 1267 |
+
for pattern in social_patterns:
|
| 1268 |
+
if re.search(pattern, content, re.I):
|
| 1269 |
+
return True
|
| 1270 |
+
|
| 1271 |
+
return False
|
| 1272 |
+
|
| 1273 |
+
def _estimate_success_rate(self, fields: List[FormField], complexity: int, has_captcha: bool) -> float:
|
| 1274 |
+
"""Estime le taux de succès"""
|
| 1275 |
+
base_rate = 0.8
|
| 1276 |
+
|
| 1277 |
+
if has_captcha:
|
| 1278 |
+
base_rate *= 0.1
|
| 1279 |
+
|
| 1280 |
+
if complexity > 15:
|
| 1281 |
+
base_rate *= 0.4
|
| 1282 |
+
elif complexity > 10:
|
| 1283 |
+
base_rate *= 0.6
|
| 1284 |
+
elif complexity > 5:
|
| 1285 |
+
base_rate *= 0.8
|
| 1286 |
+
|
| 1287 |
+
required_fields = [f for f in fields if f.required]
|
| 1288 |
+
if len(required_fields) <= 3:
|
| 1289 |
+
base_rate *= 1.1
|
| 1290 |
+
|
| 1291 |
+
return min(base_rate, 1.0)
|
| 1292 |
+
|
| 1293 |
+
async def _fill_and_submit_form(self, page: Page, analysis: FormAnalysis, contest: Contest) -> bool:
|
| 1294 |
+
"""Remplit et soumet le formulaire"""
|
| 1295 |
+
try:
|
| 1296 |
+
filled_fields = 0
|
| 1297 |
+
|
| 1298 |
+
for field in analysis.fields:
|
| 1299 |
+
try:
|
| 1300 |
+
await page.wait_for_selector(field.selector, timeout=3000)
|
| 1301 |
+
element = await page.query_selector(field.selector)
|
| 1302 |
+
|
| 1303 |
+
if not element:
|
| 1304 |
+
continue
|
| 1305 |
+
|
| 1306 |
+
value = await self._generate_field_value(field, contest, page)
|
| 1307 |
+
if not value:
|
| 1308 |
+
continue
|
| 1309 |
+
|
| 1310 |
+
if field.field_type == 'select':
|
| 1311 |
+
await self._fill_select_field(element, value, page)
|
| 1312 |
+
else:
|
| 1313 |
+
await element.fill(value)
|
| 1314 |
+
|
| 1315 |
+
filled_fields += 1
|
| 1316 |
+
await asyncio.sleep(random.uniform(0.3, 0.8))
|
| 1317 |
+
|
| 1318 |
+
except Exception as e:
|
| 1319 |
+
logging.debug(f"Error filling field {field.selector}: {e}")
|
| 1320 |
+
continue
|
| 1321 |
+
|
| 1322 |
+
submit_success = await self._submit_form(page)
|
| 1323 |
+
|
| 1324 |
+
logging.info(f"Filled {filled_fields}/{len(analysis.fields)} fields, submitted: {submit_success}")
|
| 1325 |
+
return filled_fields > 0 and submit_success
|
| 1326 |
+
|
| 1327 |
+
except Exception as e:
|
| 1328 |
+
logging.error(f"Form filling error: {e}")
|
| 1329 |
+
return False
|
| 1330 |
+
|
| 1331 |
+
async def _generate_field_value(self, field: FormField, contest: Contest, page: Page) -> Optional[str]:
|
| 1332 |
+
"""Génère une valeur pour un champ"""
|
| 1333 |
+
field_type = self._identify_field_type(field)
|
| 1334 |
+
|
| 1335 |
+
personal_mapping = {
|
| 1336 |
+
'prenom': self.personal_info.prenom,
|
| 1337 |
+
'nom': self.personal_info.nom,
|
| 1338 |
+
'email': self.personal_info.email_derivee,
|
| 1339 |
+
'telephone': self.personal_info.telephone,
|
| 1340 |
+
'adresse': self.personal_info.adresse,
|
| 1341 |
+
'code_postal': self.personal_info.code_postal,
|
| 1342 |
+
'ville': self.personal_info.ville,
|
| 1343 |
+
'pays': self.personal_info.pays
|
| 1344 |
+
}
|
| 1345 |
+
|
| 1346 |
+
if field_type in personal_mapping:
|
| 1347 |
+
return personal_mapping[field_type]
|
| 1348 |
+
|
| 1349 |
+
if field_type == 'motivation':
|
| 1350 |
+
return self.ai.generate_response(
|
| 1351 |
+
field.label,
|
| 1352 |
+
contest.description,
|
| 1353 |
+
"motivation"
|
| 1354 |
+
)
|
| 1355 |
+
elif field_type == 'quiz':
|
| 1356 |
+
return self.ai.generate_response(
|
| 1357 |
+
field.label,
|
| 1358 |
+
contest.description,
|
| 1359 |
+
"quiz"
|
| 1360 |
+
)
|
| 1361 |
+
|
| 1362 |
+
default_values = {
|
| 1363 |
+
'age': '25',
|
| 1364 |
+
'genre': 'Monsieur',
|
| 1365 |
+
'profession': 'Étudiant'
|
| 1366 |
+
}
|
| 1367 |
+
|
| 1368 |
+
for key, value in default_values.items():
|
| 1369 |
+
if key in field.label.lower():
|
| 1370 |
+
return value
|
| 1371 |
+
|
| 1372 |
+
return None
|
| 1373 |
+
|
| 1374 |
+
def _identify_field_type(self, field: FormField) -> str:
|
| 1375 |
+
"""Identifie le type de champ"""
|
| 1376 |
+
combined_text = f"{field.ai_context} {field.label}".lower()
|
| 1377 |
+
|
| 1378 |
+
for field_type, patterns in self.field_patterns.items():
|
| 1379 |
+
for pattern in patterns:
|
| 1380 |
+
if re.search(pattern, combined_text, re.I):
|
| 1381 |
+
return field_type
|
| 1382 |
+
|
| 1383 |
+
return 'unknown'
|
| 1384 |
+
|
| 1385 |
+
async def _fill_select_field(self, element, value: str, page: Page):
|
| 1386 |
+
"""Remplit un champ select"""
|
| 1387 |
+
try:
|
| 1388 |
+
options = await element.query_selector_all('option')
|
| 1389 |
+
|
| 1390 |
+
for option in options:
|
| 1391 |
+
option_text = await option.inner_text()
|
| 1392 |
+
option_value = await option.get_attribute('value')
|
| 1393 |
+
|
| 1394 |
+
if (value.lower() in option_text.lower() or
|
| 1395 |
+
value.lower() in (option_value or "").lower()):
|
| 1396 |
+
await element.select_option(value=option_value)
|
| 1397 |
+
return
|
| 1398 |
+
|
| 1399 |
+
if options and len(options) > 1:
|
| 1400 |
+
first_option = await options[1].get_attribute('value')
|
| 1401 |
+
await element.select_option(value=first_option)
|
| 1402 |
+
|
| 1403 |
+
except Exception as e:
|
| 1404 |
+
logging.debug(f"Select field error: {e}")
|
| 1405 |
+
|
| 1406 |
+
async def _submit_form(self, page: Page) -> bool:
|
| 1407 |
+
"""Soumet le formulaire"""
|
| 1408 |
+
submit_selectors = [
|
| 1409 |
+
'input[type="submit"]',
|
| 1410 |
+
'button[type="submit"]',
|
| 1411 |
+
'button:has-text("Participer")',
|
| 1412 |
+
'button:has-text("Envoyer")',
|
| 1413 |
+
'button:has-text("Valider")',
|
| 1414 |
+
'.submit-btn',
|
| 1415 |
+
'.participate-btn'
|
| 1416 |
+
]
|
| 1417 |
+
|
| 1418 |
+
for selector in submit_selectors:
|
| 1419 |
+
try:
|
| 1420 |
+
element = await page.query_selector(selector)
|
| 1421 |
+
if element:
|
| 1422 |
+
is_visible = await element.is_visible()
|
| 1423 |
+
is_enabled = await element.is_enabled()
|
| 1424 |
+
|
| 1425 |
+
if is_visible and is_enabled:
|
| 1426 |
+
await element.click()
|
| 1427 |
+
await page.wait_for_timeout(3000)
|
| 1428 |
+
return True
|
| 1429 |
+
|
| 1430 |
+
except Exception:
|
| 1431 |
+
continue
|
| 1432 |
+
|
| 1433 |
+
return False
|
| 1434 |
+
|
| 1435 |
+
# =====================================================
|
| 1436 |
+
# GESTIONNAIRE D'EMAILS ET ALERTES
|
| 1437 |
+
# =====================================================
|
| 1438 |
+
|
| 1439 |
+
class EmailManager:
|
| 1440 |
+
def __init__(self, api_config: APIConfig, ai_engine: AIEngine, db_manager: DatabaseManager):
|
| 1441 |
+
self.api_config = api_config
|
| 1442 |
+
self.ai = ai_engine
|
| 1443 |
+
self.db = db_manager
|
| 1444 |
+
self.personal_info = PERSONAL_INFO
|
| 1445 |
+
|
| 1446 |
+
def check_and_analyze_emails(self):
|
| 1447 |
+
"""Vérifie et analyse les emails pour détecter les victoires"""
|
| 1448 |
+
if not self.api_config.email_app_password:
|
| 1449 |
+
logging.warning("Email app password not configured")
|
| 1450 |
+
return
|
| 1451 |
+
|
| 1452 |
+
try:
|
| 1453 |
+
mail = imaplib.IMAP4_SSL('outlook.office365.com')
|
| 1454 |
+
mail.login(self.personal_info.email, self.api_config.email_app_password)
|
| 1455 |
+
mail.select('inbox')
|
| 1456 |
+
|
| 1457 |
+
since_date = (datetime.now() - timedelta(days=7)).strftime('%d-%b-%Y')
|
| 1458 |
+
status, messages = mail.search(None, f'(SINCE "{since_date}")')
|
| 1459 |
+
|
| 1460 |
+
victories = []
|
| 1461 |
+
email_count = 0
|
| 1462 |
+
|
| 1463 |
+
for num in messages[0].split()[-20:]:
|
| 1464 |
+
try:
|
| 1465 |
+
email_count += 1
|
| 1466 |
+
_, msg = mail.fetch(num, '(RFC822)')
|
| 1467 |
+
email_msg = email.message_from_bytes(msg[0][1])
|
| 1468 |
+
|
| 1469 |
+
subject = email_msg['Subject'] or ""
|
| 1470 |
+
from_addr = email_msg['From'] or ""
|
| 1471 |
+
|
| 1472 |
+
body = self._extract_email_body(email_msg)
|
| 1473 |
+
|
| 1474 |
+
analysis = self.ai.generate_response(
|
| 1475 |
+
f"Cet email indique-t-il une victoire dans un concours ? Sujet: {subject}",
|
| 1476 |
+
body[:1000],
|
| 1477 |
+
"quiz"
|
| 1478 |
+
)
|
| 1479 |
+
|
| 1480 |
+
if 'oui' in analysis.lower() or 'gagne' in analysis.lower():
|
| 1481 |
+
prize = self._extract_prize_from_email(body, subject)
|
| 1482 |
+
victories.append({
|
| 1483 |
+
'email_id': num.decode(),
|
| 1484 |
+
'date': datetime.now().strftime('%Y-%m-%d'),
|
| 1485 |
+
'prize': prize,
|
| 1486 |
+
'source': from_addr,
|
| 1487 |
+
'subject': subject
|
| 1488 |
+
})
|
| 1489 |
+
|
| 1490 |
+
self.db.add_victory(num.decode(), prize, from_addr)
|
| 1491 |
+
self._send_victory_alert(prize, from_addr, subject)
|
| 1492 |
+
|
| 1493 |
+
logging.info(f"Victory detected: {prize} from {from_addr}")
|
| 1494 |
+
|
| 1495 |
+
except Exception as e:
|
| 1496 |
+
logging.error(f"Error processing email {num}: {e}")
|
| 1497 |
+
continue
|
| 1498 |
+
|
| 1499 |
+
mail.logout()
|
| 1500 |
+
logging.info(f"Processed {email_count} emails, found {len(victories)} victories")
|
| 1501 |
+
|
| 1502 |
+
except Exception as e:
|
| 1503 |
+
logging.error(f"Email checking error: {e}")
|
| 1504 |
+
|
| 1505 |
+
def _extract_email_body(self, email_msg) -> str:
|
| 1506 |
+
"""Extrait le corps de l'email"""
|
| 1507 |
+
body = ""
|
| 1508 |
+
|
| 1509 |
+
if email_msg.is_multipart():
|
| 1510 |
+
for part in email_msg.walk():
|
| 1511 |
+
if part.get_content_type() == 'text/plain':
|
| 1512 |
+
try:
|
| 1513 |
+
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
|
| 1514 |
+
break
|
| 1515 |
+
except:
|
| 1516 |
+
continue
|
| 1517 |
+
else:
|
| 1518 |
+
try:
|
| 1519 |
+
body = email_msg.get_payload(decode=True).decode('utf-8', errors='ignore')
|
| 1520 |
+
except:
|
| 1521 |
+
body = str(email_msg.get_payload())
|
| 1522 |
+
|
| 1523 |
+
return body
|
| 1524 |
+
|
| 1525 |
+
def _extract_prize_from_email(self, body: str, subject: str) -> str:
|
| 1526 |
+
"""Extrait le prix gagné de l'email"""
|
| 1527 |
+
text = f"{subject} {body}"
|
| 1528 |
+
|
| 1529 |
+
prize_patterns = [
|
| 1530 |
+
r"vous avez gagné\s+([^.!?\n]{1,100})",
|
| 1531 |
+
r"prix[:\s]+([^.!?\n]{1,100})",
|
| 1532 |
+
r"lot[:\s]+([^.!?\n]{1,100})",
|
| 1533 |
+
r"remporté\s+([^.!?\n]{1,100})",
|
| 1534 |
+
r"(\d+\s*CHF|\d+\s*euros?|\d+\s*francs?)"
|
| 1535 |
+
]
|
| 1536 |
+
|
| 1537 |
+
for pattern in prize_patterns:
|
| 1538 |
+
match = re.search(pattern, text, re.I)
|
| 1539 |
+
if match:
|
| 1540 |
+
return match.group(1).strip()
|
| 1541 |
+
|
| 1542 |
+
return "Prix non spécifié"
|
| 1543 |
+
|
| 1544 |
+
def _send_victory_alert(self, prize: str, source: str, subject: str):
|
| 1545 |
+
"""Envoie une alerte de victoire"""
|
| 1546 |
+
if not self.api_config.telegram_bot_token or not self.api_config.telegram_chat_id:
|
| 1547 |
+
return
|
| 1548 |
+
|
| 1549 |
+
message = f"""
|
| 1550 |
+
🎉 **VICTOIRE DÉTECTÉE !**
|
| 1551 |
+
|
| 1552 |
+
🏆 **Prix**: {prize}
|
| 1553 |
+
📧 **Source**: {source}
|
| 1554 |
+
📋 **Sujet**: {subject}
|
| 1555 |
+
📅 **Date**: {datetime.now().strftime('%d/%m/%Y %H:%M')}
|
| 1556 |
+
|
| 1557 |
+
Félicitations ! 🎊
|
| 1558 |
+
"""
|
| 1559 |
+
|
| 1560 |
+
try:
|
| 1561 |
+
url = f"https://api.telegram.org/bot{self.api_config.telegram_bot_token}/sendMessage"
|
| 1562 |
+
payload = {
|
| 1563 |
+
'chat_id': self.api_config.telegram_chat_id,
|
| 1564 |
+
'text': message,
|
| 1565 |
+
'parse_mode': 'Markdown'
|
| 1566 |
+
}
|
| 1567 |
+
|
| 1568 |
+
response = requests.post(url, json=payload, timeout=10)
|
| 1569 |
+
if response.status_code == 200:
|
| 1570 |
+
logging.info("Victory alert sent successfully")
|
| 1571 |
+
else:
|
| 1572 |
+
logging.error(f"Telegram alert failed: {response.text}")
|
| 1573 |
+
|
| 1574 |
+
except Exception as e:
|
| 1575 |
+
logging.error(f"Telegram alert error: {e}")
|
| 1576 |
+
|
| 1577 |
+
# =====================================================
|
| 1578 |
+
# SYSTÈME DE MONITORING ET STATISTIQUES
|
| 1579 |
+
# =====================================================
|
| 1580 |
+
|
| 1581 |
+
class MonitoringSystem:
|
| 1582 |
+
def __init__(self, db_manager: DatabaseManager):
|
| 1583 |
+
self.db = db_manager
|
| 1584 |
+
|
| 1585 |
+
def generate_daily_report(self) -> str:
|
| 1586 |
+
"""Génère un rapport quotidien"""
|
| 1587 |
+
stats = self.db.get_stats()
|
| 1588 |
+
|
| 1589 |
+
today = datetime.now().strftime('%Y-%m-%d')
|
| 1590 |
+
conn = self.db._get_connection()
|
| 1591 |
+
|
| 1592 |
+
today_participations = conn.execute(
|
| 1593 |
+
"SELECT COUNT(*) FROM participations WHERE date = ?", (today,)
|
| 1594 |
+
).fetchone()[0]
|
| 1595 |
+
|
| 1596 |
+
today_successes = conn.execute(
|
| 1597 |
+
"SELECT COUNT(*) FROM participations WHERE date = ? AND status = 'success'", (today,)
|
| 1598 |
+
).fetchone()[0]
|
| 1599 |
+
|
| 1600 |
+
success_rate = (today_successes / max(today_participations, 1)) * 100
|
| 1601 |
+
|
| 1602 |
+
report = f"""
|
| 1603 |
+
📊 **RAPPORT QUOTIDIEN - {today}**
|
| 1604 |
+
|
| 1605 |
+
🎯 **Aujourd'hui**:
|
| 1606 |
+
• Participations: {today_participations}
|
| 1607 |
+
• Succès: {today_successes}
|
| 1608 |
+
• Taux de succès: {success_rate:.1f}%
|
| 1609 |
+
|
| 1610 |
+
📈 **Total**:
|
| 1611 |
+
• Participations totales: {stats['total_participations']}
|
| 1612 |
+
• Participations réussies: {stats['successful_participations']}
|
| 1613 |
+
• Victoires détectées: {stats['total_victories']}
|
| 1614 |
+
|
| 1615 |
+
🌐 **Par source**:
|
| 1616 |
+
"""
|
| 1617 |
+
|
| 1618 |
+
for source, count in stats['by_source'].items():
|
| 1619 |
+
report += f" • {source}: {count}\n"
|
| 1620 |
+
|
| 1621 |
+
return report
|
| 1622 |
+
|
| 1623 |
+
def send_daily_report(self):
|
| 1624 |
+
"""Envoie le rapport quotidien"""
|
| 1625 |
+
if not API_CONFIG.telegram_bot_token or not API_CONFIG.telegram_chat_id:
|
| 1626 |
+
return
|
| 1627 |
+
|
| 1628 |
+
report = self.generate_daily_report()
|
| 1629 |
+
|
| 1630 |
+
try:
|
| 1631 |
+
url = f"https://api.telegram.org/bot{API_CONFIG.telegram_bot_token}/sendMessage"
|
| 1632 |
+
payload = {
|
| 1633 |
+
'chat_id': API_CONFIG.telegram_chat_id,
|
| 1634 |
+
'text': report,
|
| 1635 |
+
'parse_mode': 'Markdown'
|
| 1636 |
+
}
|
| 1637 |
+
|
| 1638 |
+
response = requests.post(url, json=payload, timeout=10)
|
| 1639 |
+
if response.status_code == 200:
|
| 1640 |
+
logging.info("Daily report sent successfully")
|
| 1641 |
+
else:
|
| 1642 |
+
logging.error(f"Daily report failed: {response.text}")
|
| 1643 |
+
|
| 1644 |
+
except Exception as e:
|
| 1645 |
+
logging.error(f"Daily report error: {e}")
|
| 1646 |
+
|
| 1647 |
+
# =====================================================
|
| 1648 |
+
# ORCHESTRATEUR PRINCIPAL
|
| 1649 |
+
# =====================================================
|
| 1650 |
+
|
| 1651 |
+
class ContestBotOrchestrator:
|
| 1652 |
+
def __init__(self):
|
| 1653 |
+
self.db = DatabaseManager()
|
| 1654 |
+
self.ai = AIEngine(API_CONFIG)
|
| 1655 |
+
self.scraper = None
|
| 1656 |
+
self.participator = SmartParticipator(self.db, self.ai)
|
| 1657 |
+
self.email_manager = EmailManager(API_CONFIG, self.ai, self.db)
|
| 1658 |
+
self.monitor = MonitoringSystem(self.db)
|
| 1659 |
+
|
| 1660 |
+
async def run_full_cycle(self):
|
| 1661 |
+
"""Execute un cycle complet de scraping et participation"""
|
| 1662 |
+
logging.info("Starting full contest bot cycle")
|
| 1663 |
+
|
| 1664 |
+
try:
|
| 1665 |
+
# 1. Scraping des concours
|
| 1666 |
+
async with IntelligentScraper(self.db) as scraper:
|
| 1667 |
+
self.scraper = scraper
|
| 1668 |
+
contests = await scraper.scrape_all_sources()
|
| 1669 |
+
|
| 1670 |
+
if not contests:
|
| 1671 |
+
logging.info("No new contests found")
|
| 1672 |
+
return
|
| 1673 |
+
|
| 1674 |
+
# 2. Trier par score de difficulté (plus faciles en premier)
|
| 1675 |
+
contests.sort(key=lambda x: x.difficulty_score)
|
| 1676 |
+
|
| 1677 |
+
# 3. Participer aux concours (limiter à 20 par jour)
|
| 1678 |
+
participation_count = 0
|
| 1679 |
+
max_daily_participations = 20
|
| 1680 |
+
|
| 1681 |
+
for contest in contests[:max_daily_participations]:
|
| 1682 |
+
try:
|
| 1683 |
+
# Pause entre participations pour éviter la détection
|
| 1684 |
+
if participation_count > 0:
|
| 1685 |
+
wait_time = random.uniform(30, 120) # 30s à 2min
|
| 1686 |
+
logging.info(f"Waiting {wait_time:.0f}s before next participation")
|
| 1687 |
+
await asyncio.sleep(wait_time)
|
| 1688 |
+
|
| 1689 |
+
success = await self.participator.participate_in_contest(contest)
|
| 1690 |
+
participation_count += 1
|
| 1691 |
+
|
| 1692 |
+
if success:
|
| 1693 |
+
logging.info(f"✅ Successfully participated in: {contest.title}")
|
| 1694 |
+
else:
|
| 1695 |
+
logging.warning(f"❌ Failed to participate in: {contest.title}")
|
| 1696 |
+
|
| 1697 |
+
# Pause plus longue après succès
|
| 1698 |
+
if success:
|
| 1699 |
+
await asyncio.sleep(random.uniform(60, 180))
|
| 1700 |
+
|
| 1701 |
+
except Exception as e:
|
| 1702 |
+
logging.error(f"Error participating in {contest.title}: {e}")
|
| 1703 |
+
continue
|
| 1704 |
+
|
| 1705 |
+
logging.info(f"Participation cycle completed: {participation_count} attempts")
|
| 1706 |
+
|
| 1707 |
+
except Exception as e:
|
| 1708 |
+
logging.error(f"Full cycle error: {e}")
|
| 1709 |
+
|
| 1710 |
+
def run_email_check(self):
|
| 1711 |
+
"""Vérifie les emails pour les victoires"""
|
| 1712 |
+
try:
|
| 1713 |
+
logging.info("Checking emails for victories")
|
| 1714 |
+
self.email_manager.check_and_analyze_emails()
|
| 1715 |
+
except Exception as e:
|
| 1716 |
+
logging.error(f"Email check error: {e}")
|
| 1717 |
+
|
| 1718 |
+
def run_daily_report(self):
|
| 1719 |
+
"""Génère et envoie le rapport quotidien"""
|
| 1720 |
+
try:
|
| 1721 |
+
logging.info("Generating daily report")
|
| 1722 |
+
self.monitor.send_daily_report()
|
| 1723 |
+
except Exception as e:
|
| 1724 |
+
logging.error(f"Daily report error: {e}")
|
| 1725 |
+
|
| 1726 |
+
# =====================================================
|
| 1727 |
+
# SCHEDULER ET POINT D'ENTRÉE
|
| 1728 |
+
# =====================================================
|
| 1729 |
+
|
| 1730 |
+
def run_bot_cycle():
|
| 1731 |
+
"""Point d'entrée pour le scheduler"""
|
| 1732 |
+
bot = ContestBotOrchestrator()
|
| 1733 |
+
|
| 1734 |
+
# Cycle principal
|
| 1735 |
+
asyncio.run(bot.run_full_cycle())
|
| 1736 |
+
|
| 1737 |
+
# Vérification des emails
|
| 1738 |
+
bot.run_email_check()
|
| 1739 |
+
|
| 1740 |
+
def run_daily_report():
|
| 1741 |
+
"""Point d'entrée pour le rapport quotidien"""
|
| 1742 |
+
bot = ContestBotOrchestrator()
|
| 1743 |
+
bot.run_daily_report()
|
| 1744 |
+
|
| 1745 |
+
def main():
|
| 1746 |
+
"""Fonction principale avec launcher intelligent intégré"""
|
| 1747 |
+
|
| 1748 |
+
# Vérifier les arguments de ligne de commande
|
| 1749 |
+
if len(sys.argv) > 1:
|
| 1750 |
+
if sys.argv[1] == "--run-now":
|
| 1751 |
+
logging.info("Running immediate cycle")
|
| 1752 |
+
run_bot_cycle()
|
| 1753 |
+
return
|
| 1754 |
+
elif sys.argv[1] == "--test-apis":
|
| 1755 |
+
# Mode test des APIs
|
| 1756 |
+
tester = APITester(API_CONFIG)
|
| 1757 |
+
results = tester.test_all_apis()
|
| 1758 |
+
working_count = sum(results.values())
|
| 1759 |
+
print(f"\n📊 Résumé: {working_count}/5 APIs fonctionnelles")
|
| 1760 |
+
return
|
| 1761 |
+
elif sys.argv[1] == "--scheduler":
|
| 1762 |
+
# Mode scheduler direct (sans menu)
|
| 1763 |
+
logging.info("Starting Contest Bot with scheduler")
|
| 1764 |
+
|
| 1765 |
+
schedule.every().day.at("08:00").do(run_bot_cycle)
|
| 1766 |
+
schedule.every().day.at("14:00").do(run_bot_cycle)
|
| 1767 |
+
schedule.every().day.at("20:00").do(run_daily_report)
|
| 1768 |
+
|
| 1769 |
+
logging.info("Scheduler started. Waiting for scheduled tasks...")
|
| 1770 |
+
|
| 1771 |
+
while True:
|
| 1772 |
+
try:
|
| 1773 |
+
schedule.run_pending()
|
| 1774 |
+
time.sleep(60)
|
| 1775 |
+
except KeyboardInterrupt:
|
| 1776 |
+
logging.info("Bot stopped by user")
|
| 1777 |
+
break
|
| 1778 |
+
except Exception as e:
|
| 1779 |
+
logging.error(f"Scheduler error: {e}")
|
| 1780 |
+
time.sleep(300)
|
| 1781 |
+
return
|
| 1782 |
+
|
| 1783 |
+
# Mode launcher intelligent (par défaut)
|
| 1784 |
+
try:
|
| 1785 |
+
launcher = SmartLauncher()
|
| 1786 |
+
should_launch = launcher.main_menu()
|
| 1787 |
+
|
| 1788 |
+
if should_launch:
|
| 1789 |
+
# L'utilisateur veut lancer le bot
|
| 1790 |
+
print("\n🤔 Comment voulez-vous lancer le bot ?")
|
| 1791 |
+
print("1️⃣ Test immédiat (--run-now)")
|
| 1792 |
+
print("2️⃣ Mode scheduler automatique")
|
| 1793 |
+
|
| 1794 |
+
mode_choice = input("\nVotre choix (1/2): ").strip()
|
| 1795 |
+
|
| 1796 |
+
if mode_choice == "1":
|
| 1797 |
+
print("\n🎬 Lancement immédiat...")
|
| 1798 |
+
run_bot_cycle()
|
| 1799 |
+
elif mode_choice == "2":
|
| 1800 |
+
print("\n⏰ Démarrage du scheduler...")
|
| 1801 |
+
print("Programmation: 08:00, 14:00 (concours) et 20:00 (rapport)")
|
| 1802 |
+
|
| 1803 |
+
schedule.every().day.at("08:00").do(run_bot_cycle)
|
| 1804 |
+
schedule.every().day.at("14:00").do(run_bot_cycle)
|
| 1805 |
+
schedule.every().day.at("20:00").do(run_daily_report)
|
| 1806 |
+
|
| 1807 |
+
print("Scheduler démarré. Utilisez Ctrl+C pour arrêter.")
|
| 1808 |
+
|
| 1809 |
+
while True:
|
| 1810 |
+
try:
|
| 1811 |
+
schedule.run_pending()
|
| 1812 |
+
time.sleep(60)
|
| 1813 |
+
except KeyboardInterrupt:
|
| 1814 |
+
print("\n👋 Bot arrêté par l'utilisateur")
|
| 1815 |
+
break
|
| 1816 |
+
except Exception as e:
|
| 1817 |
+
logging.error(f"Scheduler error: {e}")
|
| 1818 |
+
time.sleep(300)
|
| 1819 |
+
else:
|
| 1820 |
+
print("❌ Choix invalide")
|
| 1821 |
+
else:
|
| 1822 |
+
print("👋 À bientôt !")
|
| 1823 |
+
|
| 1824 |
+
except KeyboardInterrupt:
|
| 1825 |
+
print("\n\n👋 Arrêté par l'utilisateur")
|
| 1826 |
+
except Exception as e:
|
| 1827 |
+
print(f"\n❌ Erreur: {e}")
|
| 1828 |
+
|
| 1829 |
+
if __name__ == "__main__":
|
| 1830 |
+
main()
|