valouas commited on
Commit
ad13c80
·
verified ·
1 Parent(s): e1b26b2

Upload 2.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 2.py +1830 -0
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()